Merge pull request 'feat(magician): 법사 클래스 — 1차 5종·2차 3계열·신규 메커니즘 4종 (배포 퀄리티 P10)' (#45) from feature/p10-magician into main
This commit was merged in pull request #45.
This commit is contained in:
File diff suppressed because one or more lines are too long
170
data/cards.json
170
data/cards.json
@@ -171,18 +171,164 @@
|
||||
"value": 3,
|
||||
"desc": "매턴 방어도 +3",
|
||||
"image": "b4020dbadee6401f9893a020fe4154b1"
|
||||
},
|
||||
"EnergyBolt": {
|
||||
"name": "에너지 볼트",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "magician",
|
||||
"damage": 6,
|
||||
"desc": "피해 6",
|
||||
"image": "a1ee3069fce14498b92998542679ae40"
|
||||
},
|
||||
"MagicGuard": {
|
||||
"name": "매직 가드",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "magician",
|
||||
"block": 5,
|
||||
"desc": "방어도 5",
|
||||
"image": "01b249c26eb34b8aaab774bf221907a1"
|
||||
},
|
||||
"MagicClaw": {
|
||||
"name": "매직 클로",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "magician",
|
||||
"damage": 3,
|
||||
"hits": 2,
|
||||
"desc": "피해 3 × 2회",
|
||||
"image": "d6e7c04c436f42f19e9806ac5b4401ae"
|
||||
},
|
||||
"Teleport": {
|
||||
"name": "텔레포트",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "magician",
|
||||
"block": 3,
|
||||
"draw": 1,
|
||||
"desc": "방어도 3, 드로 1",
|
||||
"image": "80c98c8e032b4f6c8371a24b4e1d8f14"
|
||||
},
|
||||
"Slow": {
|
||||
"name": "슬로우",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "magician",
|
||||
"weak": 2,
|
||||
"desc": "약화 2 부여",
|
||||
"image": "16f79f571a964430bf1953edc9a14c73"
|
||||
},
|
||||
"FireArrow": {
|
||||
"name": "파이어 애로우",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "firepoison",
|
||||
"damage": 8,
|
||||
"desc": "피해 8",
|
||||
"image": "78b9be4e711c440f84fc21e51e812bae"
|
||||
},
|
||||
"PoisonBreath": {
|
||||
"name": "포이즌 브레스",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "firepoison",
|
||||
"poison": 4,
|
||||
"desc": "독 4 부여",
|
||||
"image": "b4e8bd7508b54d208e4f2ad7414f8c0a"
|
||||
},
|
||||
"ElementAmp": {
|
||||
"name": "엘레멘트 앰플",
|
||||
"cost": 1,
|
||||
"kind": "Power",
|
||||
"class": "firepoison",
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"desc": "매 턴 힘 +1",
|
||||
"image": "9859f3ab41b945f797d56cd83f95b25f"
|
||||
},
|
||||
"ThunderBolt": {
|
||||
"name": "썬더 볼트",
|
||||
"cost": 2,
|
||||
"kind": "Attack",
|
||||
"class": "icelightning",
|
||||
"damage": 6,
|
||||
"aoe": true,
|
||||
"desc": "모든 적에게 피해 6",
|
||||
"image": "c6685d33cb2641f09d11cfa2d5cc820c"
|
||||
},
|
||||
"ColdBeam": {
|
||||
"name": "콜드 빔",
|
||||
"cost": 2,
|
||||
"kind": "Attack",
|
||||
"class": "icelightning",
|
||||
"damage": 7,
|
||||
"weak": 2,
|
||||
"desc": "피해 7, 약화 2",
|
||||
"image": "e8f7c148c79f497d83014e3361f59f5c"
|
||||
},
|
||||
"ChillingStep": {
|
||||
"name": "칠링 스텝",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "icelightning",
|
||||
"block": 8,
|
||||
"desc": "방어도 8",
|
||||
"image": "b2a7274d868241c78aa5780f2beecddf"
|
||||
},
|
||||
"Heal": {
|
||||
"name": "힐",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "cleric",
|
||||
"heal": 10,
|
||||
"desc": "HP 10 회복",
|
||||
"image": "b4127c181e2942e38821d4a9a1f14596"
|
||||
},
|
||||
"Bless": {
|
||||
"name": "블레스",
|
||||
"cost": 1,
|
||||
"kind": "Skill",
|
||||
"class": "cleric",
|
||||
"strength": 1,
|
||||
"block": 5,
|
||||
"desc": "힘 +1, 방어도 5",
|
||||
"image": "d45553db4a414011b67486dfa8a12fe5"
|
||||
},
|
||||
"HolyArrow": {
|
||||
"name": "홀리 애로우",
|
||||
"cost": 1,
|
||||
"kind": "Attack",
|
||||
"class": "cleric",
|
||||
"damage": 8,
|
||||
"desc": "피해 8",
|
||||
"image": "0265e103b4904f178b1c2bdcd54d5975"
|
||||
}
|
||||
},
|
||||
"starterDeck": [
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Bash"
|
||||
]
|
||||
"starterDecks": {
|
||||
"warrior": [
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Strike",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Defend",
|
||||
"Bash"
|
||||
],
|
||||
"magician": [
|
||||
"EnergyBolt",
|
||||
"EnergyBolt",
|
||||
"EnergyBolt",
|
||||
"EnergyBolt",
|
||||
"EnergyBolt",
|
||||
"MagicGuard",
|
||||
"MagicGuard",
|
||||
"MagicGuard",
|
||||
"MagicGuard",
|
||||
"MagicClaw"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
38
docs/superpowers/plans/2026-06-12-magician.md
Normal file
38
docs/superpowers/plans/2026-06-12-magician.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# P10 — 법사 클래스 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox 구문.
|
||||
|
||||
**Goal:** 법사 클래스(1차 5종 + 2차 3계열 9종)·신규 메커니즘 4종(독/AoE/회복/드로)·캐릭터 선택 오픈·전직 화면 동적화.
|
||||
|
||||
설계: `docs/superpowers/specs/2026-06-12-magician-design.md`
|
||||
|
||||
### Task 1: 이미지 RUID 10종 선별 (4종은 기존 후보 재사용)
|
||||
- [ ] 재사용 확정: FireArrow=78b9be4e(큰 불꽃)·ThunderBolt=c6685d33(낙뢰)·ColdBeam=e8f7c148(얼음)·ChillingStep=b2a7274d(빙수림)
|
||||
- [ ] 검색(마법/독/회복/빛/포털/정령) → 메이커 격자 미리보기 → EnergyBolt·MagicGuard·MagicClaw·Teleport·Slow·PoisonBreath·ElementAmp·Heal·Bless·HolyArrow 확정
|
||||
|
||||
### Task 2: 데이터 — cards.json
|
||||
- [ ] `starterDeck` → `starterDecks{warrior, magician}` (마법사: EnergyBolt×5·MagicGuard×4·MagicClaw×1), 생성기 검증 갱신
|
||||
- [ ] 신규 14종 추가 (설계 표 그대로: class=magician/firepoison/icelightning/cleric, draw/heal/poison/aoe 필드) → 커밋
|
||||
|
||||
### Task 3: 생성기 — 메커니즘 (Lua)
|
||||
- [ ] 직렬화: draw·heal·poison·aoe + starterDecks 주입(StartRun 클래스 분기: MaxHp 80/70·RunDeck)
|
||||
- [ ] PlayCard: `aoe` → `PlayAoeFx(image, total)` (단일 대상 로직과 동일 합산, 0.35s 후 전 생존 적에 각자 취약/방어 적용·슬롯별 팝업·KillMonster·CheckCombatEnd) / 공통부: heal(상한 클램프)·draw(`DrawCards`)·poison(타겟 `tm.poison += N`)
|
||||
- [ ] BuildMonsters `poison = 0` 초기화, EnemyActStep 행동 타이머 시작부에 독 틱(피해 팝업·사망 시 행동 생략 후 체인 계속), BuffsLabel 4번째 인자 poison(`독N`) — RenderCombat 호출부 갱신(플레이어는 0)
|
||||
- [ ] 커밋
|
||||
|
||||
### Task 4: 생성기 — 클래스 선택·전직 동적화
|
||||
- [ ] classCards Mage 활성화(enabled·tint·desc '마법 원거리 딜러'), BindMenuButtons MageButton→`SelectClass("magician")`, RenderCharacterSelect 2클래스 하이라이트·상태 텍스트, StartNewGame 가드 warrior|magician
|
||||
- [ ] JobSelectHud 패널 경로 `Job_slot{1..3}` 범용화, `ShowJobSelect`(JOBS 상수→JobOpts prop, 슬롯 텍스트 채움) 신설 — PickJobReward("job")가 호출, 바인딩은 슬롯 인덱스→`SetJob(self.JobOpts[i].id)`
|
||||
- [ ] SetJob 대표 카드 매핑(JOBS 테이블에 starter 포함: firepoison→FireArrow·icelightning→ThunderBolt·cleric→Heal), JobLabel 확장(마법사·위자드(불·독)·위자드(썬·콜)·클레릭)
|
||||
- [ ] 커밋
|
||||
|
||||
### Task 5: 시뮬 동기화 (TDD)
|
||||
- [ ] 실패 테스트: poison 틱·사망 / aoe 전체 피해 / heal 클램프 / draw / 법사 시작 덱은 시뮬 무관(주석) → 구현 → 전체 PASS → 커밋
|
||||
|
||||
### Task 6: 재생성·메이커 검증·PR
|
||||
- [ ] 재생성 + grep -c 카운트 + 전체 테스트 → 커밋
|
||||
- [ ] 메이커: 법사 선택 시작(HP70·시작 덱), 전직 화면 마법사 3직업 표기, 클레릭 전직→힐 동작, 독/AoE 실측 → 스크린샷
|
||||
- [ ] push → gitea-pr.mjs PR·머지 → main pull
|
||||
|
||||
## Self-Review
|
||||
- 설계 전 항목 매핑 ✓ / JobSelect 동적화로 P9 고정 경로 제거 명시 ✓ / BuffsLabel 시그니처 변경 시 호출부(몬스터·플레이어) 동시 갱신 명시 ✓
|
||||
63
docs/superpowers/specs/2026-06-12-magician-design.md
Normal file
63
docs/superpowers/specs/2026-06-12-magician-design.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# P10 — 법사 클래스 설계
|
||||
|
||||
날짜: 2026-06-12 (사용자 승인 — P9/P10/P11 중 2단계)
|
||||
브랜치: `feature/p10-magician`
|
||||
선행: P9 (클래스 모델·전직 흐름·CardPool 필터)
|
||||
|
||||
## 범위
|
||||
|
||||
1. **캐릭터 선택 오픈** — 시작 화면 전사/법사 2택 (법사 시작 HP 70, 전용 시작 덱)
|
||||
2. **법사 1차 카드 5종** + **2차 3계열 9종** (위자드(불·독)/위자드(썬·콜)/클레릭 — 실제 메이플 직업)
|
||||
3. **신규 메커니즘 4종**: 독(DoT)·전체 공격(AoE)·회복 카드·드로 카드 (Lua + 시뮬 동기화)
|
||||
4. 전직 선택 화면을 **클래스별 동적 구성**으로 리팩터 (P9의 고정 3패널 → 슬롯 3개 + 런타임 채움)
|
||||
|
||||
## 데이터
|
||||
|
||||
- `cards.json`: `starterDeck` → **`starterDecks`** `{ warrior: [...], magician: [에너지 볼트×5, 매직 가드×4, 매직 클로×1] }`
|
||||
- 신규 카드 필드: `draw`(드로 N)·`heal`(HP 회복)·`poison`(적에게 독 N)·`aoe`(true=전체 공격)
|
||||
- 클래스 상수(생성기): warrior HP 80 / magician HP 70
|
||||
|
||||
법사 카드 14종 (메이플 스킬명):
|
||||
|
||||
| id | 직업 | 이름 | 코 | 효과 |
|
||||
|----|------|------|----|------|
|
||||
| EnergyBolt | magician | 에너지 볼트 | 1 | 피해 6 |
|
||||
| MagicGuard | magician | 매직 가드 | 1 | 방어 5 |
|
||||
| MagicClaw | magician | 매직 클로 | 1 | 피해 3 × 2회 |
|
||||
| Teleport | magician | 텔레포트 | 1 | 방어 3, 드로 1 |
|
||||
| Slow | magician | 슬로우 | 1 | 약화 2 부여 |
|
||||
| FireArrow | firepoison | 파이어 애로우 | 1 | 피해 8 |
|
||||
| PoisonBreath | firepoison | 포이즌 브레스 | 1 | **독 4** 부여 |
|
||||
| ElementAmp | firepoison | 엘레멘트 앰플 | 1 | Power: 매턴 힘 +1 |
|
||||
| ThunderBolt | icelightning | 썬더 볼트 | 2 | **전체 적** 피해 6 |
|
||||
| ColdBeam | icelightning | 콜드 빔 | 2 | 피해 7, 약화 2 |
|
||||
| ChillingStep | icelightning | 칠링 스텝 | 1 | 방어 8 |
|
||||
| Heal | cleric | 힐 | 1 | **HP 10 회복** |
|
||||
| Bless | cleric | 블레스 | 1 | 힘 +1, 방어 5 |
|
||||
| HolyArrow | cleric | 홀리 애로우 | 1 | 피해 8 |
|
||||
|
||||
(설계 초안 대비 수치 미세 조정: 힐 12→10·블레스 방어 6→5·홀리 애로우 9→8 — 1코 효율 정렬)
|
||||
|
||||
## 신규 메커니즘 규칙
|
||||
|
||||
- **독**: 적 디버프. 해당 적 행동 시작 시 `hp -= poison` 후 `poison -= 1` (StS 동일). 방어 무시. 독 사망 시 행동 생략·체인 계속. 버프 라인에 `독N` 표시.
|
||||
- **AoE**(`aoe: true`): 생존 적 전원에게 각자 취약/방어 적용해 피해. 중앙 이펙트 1회(`PlayAoeFx`), 슬롯별 팝업.
|
||||
- **회복**(`heal`): `PlayerHp = min(+N, Max)`.
|
||||
- **드로**(`draw`): 사용 시 N장 드로 (손패 상한 5 초과분은 기존 DrawCards 동작 따름).
|
||||
|
||||
## 전직 화면 동적화
|
||||
|
||||
- `JobSelectHud`의 패널을 `Job_slot1..3`(범용)으로 변경, `ShowJobSelect`가 `SelectedClass`별 옵션 테이블(JOBS 상수 주입)로 이름/설명/대표 카드 텍스트를 채움. 클릭 → `SetJob(JobOpts[i].id)`.
|
||||
- JOBS: warrior=[fighter/page/spearman], magician=[firepoison(위자드 불·독)/icelightning(위자드 썬·콜)/cleric(클레릭)]
|
||||
- 대표 카드: firepoison→파이어 애로우, icelightning→썬더 볼트, cleric→힐
|
||||
- `JobLabel` 확장: 마법사/위자드(불·독)/위자드(썬·콜)/클레릭
|
||||
|
||||
## 캐릭터 선택
|
||||
|
||||
- 기존 `MageButton`(잠금) → 활성: key Mage, `SelectClass("magician")`, 하이라이트·상태 텍스트 클래스 공용화, `StartNewGame` 가드 warrior|magician 허용
|
||||
- `StartRun`: 클래스별 MaxHp·RunDeck 분기
|
||||
|
||||
## 검증
|
||||
|
||||
1. 시뮬: poison/aoe/heal/draw 재현 + 테스트 4건 이상 (전체 40건+)
|
||||
2. 메이커: 법사 선택→시작 덱 확인→전직(클레릭 등)→전용 카드 풀·독/AoE/힐 실동작, 빌드·런타임 0에러
|
||||
@@ -59,7 +59,8 @@ export function loadData() {
|
||||
if (!e) throw new Error(`simEncounter 적 없음: ${id}`);
|
||||
return { name: e.name, maxHp: e.maxHp, intents: e.intents };
|
||||
});
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, monsters };
|
||||
// 시뮬 기본 덱은 전사 시작 덱 (클래스별 시뮬은 starterDeck 직접 주입으로 가능)
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDecks.warrior, monsters };
|
||||
}
|
||||
|
||||
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||
@@ -105,7 +106,7 @@ export function simulateCombat(data, rng, stats) {
|
||||
let pStr = 0, pWeak = 0, pVuln = 0;
|
||||
const powers = [];
|
||||
const mob = monsters.map((m) => ({
|
||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0,
|
||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0, str: 0, weak: 0, vuln: 0, poison: 0,
|
||||
intents: m.intents, intentIdx: 0, alive: true,
|
||||
}));
|
||||
let turns = 0;
|
||||
@@ -148,18 +149,30 @@ export function simulateCombat(data, rng, stats) {
|
||||
const hitN = c.hits || 1;
|
||||
let totalNv = 0;
|
||||
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
|
||||
const dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (c.pierce === true) {
|
||||
target.hp -= dmg; // 방어 무시
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
let dmg = totalNv; // 통계 보고용 (aoe는 1대상 기준)
|
||||
if (c.aoe === true) {
|
||||
// 전체 공격 — 대상마다 취약/방어 개별 적용 (Lua PlayAoeFx 동기화)
|
||||
for (const m2 of aliveList()) {
|
||||
const d2 = m2.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
const r2 = applyDamage(m2.hp, m2.block, d2);
|
||||
m2.hp = r2.hp; m2.block = r2.block;
|
||||
if (m2.hp <= 0) m2.alive = false;
|
||||
}
|
||||
} else {
|
||||
const r = applyDamage(target.hp, target.block, dmg);
|
||||
target.hp = r.hp; target.block = r.block;
|
||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||
if (c.pierce === true) {
|
||||
target.hp -= dmg; // 방어 무시
|
||||
if (target.hp < 0) target.hp = 0;
|
||||
} else {
|
||||
const r = applyDamage(target.hp, target.block, dmg);
|
||||
target.hp = r.hp; target.block = r.block;
|
||||
}
|
||||
if (target.hp <= 0) target.alive = false;
|
||||
}
|
||||
if (target.hp <= 0) target.alive = false;
|
||||
if (c.block) pBlock += c.block;
|
||||
if (c.strength) pStr += c.strength;
|
||||
if (c.selfVuln) pVuln += c.selfVuln;
|
||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, dmg, c.block || 0);
|
||||
} else if (c.kind === 'Power') {
|
||||
if (c.powerEffect) powers.push(id);
|
||||
@@ -168,15 +181,18 @@ export function simulateCombat(data, rng, stats) {
|
||||
pBlock += c.block || 0;
|
||||
if (c.strength) pStr += c.strength;
|
||||
if (c.selfVuln) pVuln += c.selfVuln;
|
||||
if (c.weak || c.vuln) {
|
||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||
if (c.weak || c.vuln || c.poison) {
|
||||
const target = chooseTarget(alive, 0);
|
||||
if (c.weak) target.weak += c.weak;
|
||||
if (c.vuln) target.vuln += c.vuln;
|
||||
if (c.poison) target.poison += c.poison;
|
||||
}
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0);
|
||||
}
|
||||
hand.splice(idx, 1);
|
||||
if (c.kind !== 'Power') discard.push(id); // 파워는 소멸 — Lua 동기화
|
||||
if (c.draw) draw(c.draw);
|
||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
discard.push(...hand); hand = [];
|
||||
@@ -185,6 +201,12 @@ export function simulateCombat(data, rng, stats) {
|
||||
if (pVuln > 0) pVuln--;
|
||||
for (const m of mob) {
|
||||
if (!m.alive) continue;
|
||||
// 독 틱 — 행동 시작 시 (Lua EnemyActStep 동기화). 사망 시 행동 생략
|
||||
if (m.poison > 0) {
|
||||
m.hp -= m.poison;
|
||||
m.poison--;
|
||||
if (m.hp <= 0) { m.hp = 0; m.alive = false; continue; }
|
||||
}
|
||||
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||
const it = m.intents[m.intentIdx];
|
||||
if (it) {
|
||||
@@ -203,6 +225,8 @@ export function simulateCombat(data, rng, stats) {
|
||||
if (m.vuln > 0) m.vuln--;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
||||
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
||||
}
|
||||
|
||||
@@ -280,3 +280,59 @@ test('simulateCombat: blockPerTurn 파워 — 매턴 방어로 약공 무효', (
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 77);
|
||||
});
|
||||
|
||||
test('simulateCombat: poison — 적 행동 시작 시 틱·1 감소·독 사망 시 승리 처리', () => {
|
||||
const data = {
|
||||
cards: { PB: { name: '포이즌', cost: 3, kind: 'Skill', poison: 4 } },
|
||||
starterDeck: ['PB', 'PB', 'PB', 'PB', 'PB'],
|
||||
monsters: [{ name: '적', maxHp: 10, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// T1: 독4 부여 → 틱 4 (hp 6, 독 3). T2: +4 → 7 틱 → hp 0 사망 → 승리
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 2);
|
||||
});
|
||||
|
||||
test('simulateCombat: aoe — 모든 생존 적에게 피해', () => {
|
||||
const data = {
|
||||
cards: { TB: { name: '썬더 볼트', cost: 3, kind: 'Attack', damage: 6, aoe: true } },
|
||||
starterDeck: ['TB', 'TB', 'TB', 'TB', 'TB'],
|
||||
monsters: [
|
||||
{ name: 'A', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
{ name: 'B', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
{ name: 'C', maxHp: 6, intents: [{ kind: 'Attack', value: 5 }] },
|
||||
],
|
||||
};
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: heal — 최대 HP 클램프', () => {
|
||||
const data = {
|
||||
cards: { H: { name: '힐', cost: 1, kind: 'Skill', heal: 10 } },
|
||||
starterDeck: ['H', 'H', 'H', 'H', 'H'],
|
||||
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 10 }] }],
|
||||
};
|
||||
// 매턴: 힐로 80까지 회복(클램프) → 적 10 → 70. MAX_TURNS 도달 시 hp 70
|
||||
const r = simulateCombat(data, mulberry32(1));
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.playerHpRemaining, 70);
|
||||
});
|
||||
|
||||
test('simulateCombat: draw — 카드 드로로 손패 보충', () => {
|
||||
const data = {
|
||||
cards: {
|
||||
D: { name: '텔레포트류', cost: 0, kind: 'Skill', draw: 1, block: 0 },
|
||||
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
|
||||
},
|
||||
starterDeck: ['D', 'D', 'D', 'D', 'D', 'Hit', 'Hit', 'Hit'],
|
||||
monsters: [{ name: '적', maxHp: 4, intents: [{ kind: 'Defend', value: 0 }] }],
|
||||
};
|
||||
// 드로 덕에 첫 턴 히트 3장 전부 접근 → 늦어도 2턴 내 처치 (시드 무관)
|
||||
for (let s = 1; s <= 10; s++) {
|
||||
const r = simulateCombat(data, mulberry32(s));
|
||||
assert.equal(r.win, true, `seed ${s}`);
|
||||
assert.ok(r.turns <= 2, `seed ${s}: ${r.turns}턴`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,9 +4,32 @@ const CARDS = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||
const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||
|
||||
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
|
||||
for (const id of CARDS.starterDeck) {
|
||||
if (!CARDS.cards[id]) {
|
||||
throw new Error(`[gen-slaydeck] starterDeck에 없는 카드 id 참조: ${id}`);
|
||||
const CLASSES = {
|
||||
warrior: { label: '전사', maxHp: 80 },
|
||||
magician: { label: '마법사', maxHp: 70 },
|
||||
};
|
||||
for (const cls of Object.keys(CLASSES)) {
|
||||
if (!CARDS.starterDecks?.[cls]) throw new Error(`[gen-slaydeck] starterDecks.${cls} 없음`);
|
||||
for (const id of CARDS.starterDecks[cls]) {
|
||||
if (!CARDS.cards[id]) throw new Error(`[gen-slaydeck] starterDecks.${cls}에 없는 카드 id 참조: ${id}`);
|
||||
}
|
||||
}
|
||||
// 전직 옵션 (클래스별 2차 — JobSelectHud 동적 구성·SetJob 대표 카드)
|
||||
const JOBS = {
|
||||
warrior: [
|
||||
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack' },
|
||||
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge' },
|
||||
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce' },
|
||||
],
|
||||
magician: [
|
||||
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow' },
|
||||
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt' },
|
||||
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal' },
|
||||
],
|
||||
};
|
||||
for (const [cls, jobs] of Object.entries(JOBS)) {
|
||||
for (const j of jobs) {
|
||||
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
|
||||
}
|
||||
}
|
||||
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
|
||||
@@ -56,7 +79,14 @@ function luaEnemiesTable(enemies) {
|
||||
}
|
||||
// Lua 직렬화 헬퍼
|
||||
function luaStr(s) {
|
||||
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
||||
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
||||
}
|
||||
function luaJobsTable(jobs) {
|
||||
const cls = Object.entries(jobs).map(([clsId, list]) => {
|
||||
const items = list.map((j) => `\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)} },`).join('\n');
|
||||
return `\t${clsId} = {\n${items}\n\t},`;
|
||||
}).join('\n');
|
||||
return `self.Jobs = {\n${cls}\n}`;
|
||||
}
|
||||
function luaCardsTable(cards) {
|
||||
const lines = Object.entries(cards).map(([id, c]) => {
|
||||
@@ -73,6 +103,10 @@ function luaCardsTable(cards) {
|
||||
if (c.hits != null) fields.push(`hits = ${c.hits}`);
|
||||
if (c.pierce === true) fields.push('pierce = true');
|
||||
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
||||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||||
if (c.aoe === true) fields.push('aoe = true');
|
||||
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
|
||||
return `\t${id} = { ${fields.join(', ')} },`;
|
||||
});
|
||||
@@ -1952,10 +1986,11 @@ function upsertUi() {
|
||||
text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
// 범용 슬롯 3개 — ShowJobSelect(Lua)가 클래스별 JOBS로 텍스트를 채움 (P10 동적화)
|
||||
const jobs = [
|
||||
['fighter', '파이터', '공격 특화\n콤보 어택 · 버서크\n라이징 어택', '대표 카드: 콤보 어택', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }],
|
||||
['page', '페이지', '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', '대표 카드: 썬더 차지', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }],
|
||||
['spearman', '스피어맨', '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', '대표 카드: 피어스', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }],
|
||||
['slot1', '', '', '', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }],
|
||||
['slot2', '', '', '', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }],
|
||||
['slot3', '', '', '', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }],
|
||||
];
|
||||
jobs.forEach(([jobId, name, desc, starter, x, color], ji) => {
|
||||
const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`;
|
||||
@@ -2143,7 +2178,7 @@ function upsertUi() {
|
||||
const classCards = [
|
||||
{ key: 'Warrior', label: '\uC804\uC0AC', desc: '\uAC15\uD55C \uACF5\uACA9\uACFC \uBC29\uC5B4', x: -360, enabled: true, tint: { r: 0.74, g: 0.32, b: 0.28, a: 1 } },
|
||||
{ key: 'Thief', label: '\uB3C4\uC801', desc: '\uCD94\uD6C4 \uC5F4\uB9BC', x: 0, enabled: false, tint: { r: 0.18, g: 0.19, b: 0.21, a: 1 } },
|
||||
{ key: 'Mage', label: '\uB9C8\uBC95\uC0AC', desc: '\uCD94\uD6C4 \uC5F4\uB9BC', x: 360, enabled: false, tint: { r: 0.18, g: 0.19, b: 0.21, a: 1 } },
|
||||
{ key: 'Mage', label: '\uB9C8\uBC95\uC0AC', desc: '\uB9C8\uBC95 \uC6D0\uAC70\uB9AC \uB51C\uB7EC', x: 360, enabled: true, tint: { r: 0.3, g: 0.4, b: 0.75, a: 1 } },
|
||||
];
|
||||
for (let i = 0; i < classCards.length; i++) {
|
||||
const cls = classCards[i];
|
||||
@@ -2324,6 +2359,9 @@ function writeCodeblocks() {
|
||||
prop('any', 'EndTurnHandler'),
|
||||
prop('any', 'NewGameHandler'),
|
||||
prop('any', 'WarriorSelectHandler'),
|
||||
prop('any', 'MageSelectHandler'),
|
||||
prop('any', 'JobOpts'),
|
||||
prop('any', 'Jobs'),
|
||||
prop('any', 'StartGameHandler'),
|
||||
prop('string', 'SelectedClass', '""'),
|
||||
prop('any', 'DrawPileHandler'),
|
||||
@@ -2434,6 +2472,14 @@ if warrior ~= nil and warrior.ButtonComponent ~= nil then
|
||||
end
|
||||
self.WarriorSelectHandler = warrior:ConnectEvent(ButtonClickEvent, function() self:SelectClass("warrior") end)
|
||||
end
|
||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||||
if mage ~= nil and mage.ButtonComponent ~= nil then
|
||||
if self.MageSelectHandler ~= nil then
|
||||
mage:DisconnectEvent(ButtonClickEvent, self.MageSelectHandler)
|
||||
self.MageSelectHandler = nil
|
||||
end
|
||||
self.MageSelectHandler = mage:ConnectEvent(ButtonClickEvent, function() self:SelectClass("magician") end)
|
||||
end
|
||||
local start = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/StartButton")
|
||||
if start ~= nil and start.ButtonComponent ~= nil then
|
||||
if self.StartGameHandler ~= nil then
|
||||
@@ -2457,13 +2503,23 @@ if warrior ~= nil and warrior.SpriteGUIRendererComponent ~= nil then
|
||||
warrior.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||||
end
|
||||
end
|
||||
local mage = _EntityService:GetEntityByPath("/ui/DefaultGroup/CharacterSelectHud/MageButton")
|
||||
if mage ~= nil and mage.SpriteGUIRendererComponent ~= nil then
|
||||
if self.SelectedClass == "magician" then
|
||||
mage.SpriteGUIRendererComponent.Color = Color(0.28, 0.36, 0.46, 1)
|
||||
else
|
||||
mage.SpriteGUIRendererComponent.Color = Color(0.16, 0.2, 0.26, 1)
|
||||
end
|
||||
end
|
||||
if self.SelectedClass == "warrior" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사 선택됨")
|
||||
elseif self.SelectedClass == "magician" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "마법사 선택됨")
|
||||
else
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "전사를 선택하고 시작하세요")
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 선택하고 시작하세요")
|
||||
end`),
|
||||
method('StartNewGame', `if self.SelectedClass ~= "warrior" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "현재는 전사만 선택할 수 있습니다")
|
||||
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "magician" then
|
||||
self:SetText("/ui/DefaultGroup/CharacterSelectHud/Status", "직업을 먼저 선택하세요")
|
||||
return
|
||||
end
|
||||
self:StartRun()`),
|
||||
@@ -2474,12 +2530,17 @@ end`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' },
|
||||
]),
|
||||
method('StartRun', `self.PlayerMaxHp = 80
|
||||
method('StartRun', `if self.SelectedClass == "magician" then
|
||||
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
|
||||
else
|
||||
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
|
||||
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
|
||||
end
|
||||
self.PlayerHp = self.PlayerMaxHp
|
||||
self.Gold = 0
|
||||
self.Floor = 1
|
||||
self.RunLength = ${ACT_COUNT}
|
||||
self.RunDeck = { ${CARDS.starterDeck.map(luaStr).join(', ')} }
|
||||
self.RunActive = true
|
||||
self.RunRelics = {}
|
||||
self.RunPotions = {}
|
||||
@@ -2491,6 +2552,7 @@ ${luaEnemiesTable(ENEMIES.enemies)}
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self.PlayerJob = ""
|
||||
${luaJobsTable(JOBS)}
|
||||
self:GenerateMap()
|
||||
self:BindButtons()
|
||||
self:AddRelic("${RELICS.startingRelic}")
|
||||
@@ -2580,7 +2642,7 @@ for i = 1, n do
|
||||
end
|
||||
local maxHp = math.floor(e.maxHp * mult)
|
||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||||
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0,
|
||||
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
|
||||
intents = intents, intentIdx = 1, alive = true, slot = i }
|
||||
self:ReviveMonsterEntity(item.entity)
|
||||
self:PositionMonsterSlot(i)
|
||||
@@ -2763,12 +2825,15 @@ local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobB
|
||||
if jcJob ~= nil and jcJob.ButtonComponent ~= nil then
|
||||
jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end)
|
||||
end
|
||||
local jobIds = { "fighter", "page", "spearman" }
|
||||
for i = 1, #jobIds do
|
||||
local jid = jobIds[i]
|
||||
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_" .. jid)
|
||||
for i = 1, 3 do
|
||||
local slotIdx = i
|
||||
local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i))
|
||||
if jb ~= nil and jb.ButtonComponent ~= nil then
|
||||
jb:ConnectEvent(ButtonClickEvent, function() self:SetJob(jid) end)
|
||||
jb:ConnectEvent(ButtonClickEvent, function()
|
||||
if self.JobOpts ~= nil and self.JobOpts[slotIdx] ~= nil then
|
||||
self:SetJob(self.JobOpts[slotIdx].id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end`),
|
||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||
@@ -3062,7 +3127,11 @@ if c.kind == "Attack" then
|
||||
for h = 1, hitN do
|
||||
total = total + self:CalcPlayerAttack(c.damage)
|
||||
end
|
||||
self:PlayAttackFx(self.TargetIndex, c.image, total, c.pierce == true)
|
||||
if c.aoe == true then
|
||||
self:PlayAoeFx(c.image, total)
|
||||
else
|
||||
self:PlayAttackFx(self.TargetIndex, c.image, total, c.pierce == true)
|
||||
end
|
||||
end
|
||||
if c.block ~= nil then
|
||||
self.PlayerBlock = self.PlayerBlock + c.block
|
||||
@@ -3083,10 +3152,14 @@ end
|
||||
if c.selfVuln ~= nil then
|
||||
self.PlayerVuln = self.PlayerVuln + c.selfVuln
|
||||
end
|
||||
if c.weak ~= nil or c.vuln ~= nil then
|
||||
if c.heal ~= nil then
|
||||
self.PlayerHp = math.min(self.PlayerHp + c.heal, self.PlayerMaxHp)
|
||||
end
|
||||
if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
||||
local tm = self.Monsters[self.TargetIndex]
|
||||
if tm ~= nil and tm.alive == true then
|
||||
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
|
||||
if c.poison ~= nil then tm.poison = (tm.poison or 0) + c.poison end
|
||||
if c.vuln ~= nil then
|
||||
tm.vuln = tm.vuln + c.vuln
|
||||
if self:HasRelic("championBelt") then
|
||||
@@ -3099,6 +3172,9 @@ table.remove(self.Hand, slot)
|
||||
if c.kind ~= "Power" then
|
||||
table.insert(self.DiscardPile, cardId)
|
||||
end
|
||||
if c.draw ~= nil then
|
||||
self:DrawCards(c.draw)
|
||||
end
|
||||
self:RenderHand(false)
|
||||
self:RenderPiles()
|
||||
self:RenderCombat()
|
||||
@@ -3241,6 +3317,46 @@ end, 0.35)`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||
]),
|
||||
method('PlayAoeFx', `self.FxBusy = true
|
||||
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
|
||||
if fx ~= nil then
|
||||
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
|
||||
fx.SpriteGUIRendererComponent.ImageRUID = image
|
||||
end
|
||||
if fx.UITransformComponent ~= nil then
|
||||
fx.UITransformComponent.anchoredPosition = Vector2(300, 60)
|
||||
end
|
||||
fx.Enable = true
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if fx ~= nil then fx.Enable = false end
|
||||
self.FxBusy = false
|
||||
for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
local dmg = damage
|
||||
if m.vuln > 0 then
|
||||
dmg = math.floor(dmg * 1.5)
|
||||
end
|
||||
if m.block > 0 then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
m.hp = m.hp - dmg
|
||||
self:ShowDmgPop(i, dmg)
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:RenderCombat()
|
||||
self:CheckCombatEnd()
|
||||
end, 0.35)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
|
||||
]),
|
||||
method('KillMonster', `local m = self.Monsters[slot]
|
||||
if m == nil then
|
||||
return
|
||||
@@ -3301,6 +3417,19 @@ local m = self.Monsters[idx]
|
||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx)
|
||||
self:SetEntityEnabled(base .. "/ActFrame", true)
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if m.poison ~= nil and m.poison > 0 then
|
||||
m.hp = m.hp - m.poison
|
||||
self:ShowDmgPop(idx, m.poison)
|
||||
m.poison = m.poison - 1
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
self:RenderCombat()
|
||||
self:SetEntityEnabled(base .. "/ActFrame", false)
|
||||
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
|
||||
return
|
||||
end
|
||||
end
|
||||
m.block = 0
|
||||
local intent = m.intents[m.intentIdx]
|
||||
if intent ~= nil then
|
||||
@@ -3411,27 +3540,51 @@ if kind == "relic" then
|
||||
end
|
||||
self:ContinueAfterBoss()
|
||||
else
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)
|
||||
self:ShowJobSelect()
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
|
||||
method('JobLabel', `if self.PlayerJob == "fighter" then
|
||||
return "파이터"
|
||||
elseif self.PlayerJob == "page" then
|
||||
return "페이지"
|
||||
elseif self.PlayerJob == "spearman" then
|
||||
return "스피어맨"
|
||||
method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass]
|
||||
if opts == nil then
|
||||
opts = self.Jobs["warrior"]
|
||||
end
|
||||
self.JobOpts = opts
|
||||
for i = 1, 3 do
|
||||
local base = "/ui/DefaultGroup/JobSelectHud/Job_slot" .. tostring(i)
|
||||
local o = opts[i]
|
||||
if o ~= nil then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", o.name)
|
||||
self:SetText(base .. "/Desc", o.desc)
|
||||
local sc = self.Cards[o.starter]
|
||||
if sc ~= nil then
|
||||
self:SetText(base .. "/Starter", "대표 카드: " .. sc.name)
|
||||
end
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true)`),
|
||||
method('JobLabel', `if self.PlayerJob ~= "" and self.Jobs ~= nil then
|
||||
for cls, list in pairs(self.Jobs) do
|
||||
for i = 1, #list do
|
||||
if list[i].id == self.PlayerJob then
|
||||
return list[i].name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.SelectedClass == "warrior" then
|
||||
return "전사"
|
||||
elseif self.SelectedClass == "magician" then
|
||||
return "마법사"
|
||||
end
|
||||
return "플레이어"`, [], 0, 'string'),
|
||||
method('SetJob', `self.PlayerJob = jobId
|
||||
local starter = ""
|
||||
if jobId == "fighter" then
|
||||
starter = "ComboAttack"
|
||||
elseif jobId == "page" then
|
||||
starter = "ThunderCharge"
|
||||
elseif jobId == "spearman" then
|
||||
starter = "Pierce"
|
||||
local opts = self.Jobs[self.SelectedClass] or {}
|
||||
for i = 1, #opts do
|
||||
if opts[i].id == jobId then
|
||||
starter = opts[i].starter
|
||||
end
|
||||
end
|
||||
if starter ~= "" then
|
||||
table.insert(self.RunDeck, starter)
|
||||
@@ -3468,10 +3621,12 @@ _TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'st
|
||||
if str ~= nil and str > 0 then table.insert(parts, "힘+" .. tostring(str)) end
|
||||
if weak ~= nil and weak > 0 then table.insert(parts, "약화" .. tostring(weak)) end
|
||||
if vuln ~= nil and vuln > 0 then table.insert(parts, "취약" .. tostring(vuln)) end
|
||||
if poison ~= nil and poison > 0 then table.insert(parts, "독" .. tostring(poison)) end
|
||||
return table.concat(parts, " ")`, [
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'str' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'weak' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'vuln' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'poison' },
|
||||
], 0, 'string'),
|
||||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
|
||||
@@ -3509,7 +3664,7 @@ return table.concat(parts, " ")`, [
|
||||
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp, ${HP_BAR_W})
|
||||
self:SetEntityEnabled(base .. "/BlockBadge", m.block > 0)
|
||||
self:SetText(base .. "/BlockBadge/Value", string.format("%d", m.block))
|
||||
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln))
|
||||
self:SetText(base .. "/Buffs", self:BuffsLabel(m.str, m.weak, m.vuln, m.poison or 0))
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
@@ -3518,7 +3673,7 @@ self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/HpText", string.format("%d"
|
||||
self:SetHpBar("/ui/DefaultGroup/CombatHud/PlayerPanel/HpBarFill", self.PlayerHp, self.PlayerMaxHp, 220)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge", self.PlayerBlock > 0)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/BlockBadge/Value", string.format("%d", self.PlayerBlock))
|
||||
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln)
|
||||
local pb = self:BuffsLabel(self.PlayerStr, self.PlayerWeak, self.PlayerVuln, 0)
|
||||
if self.PlayerPowers ~= nil and #self.PlayerPowers > 0 then
|
||||
local names = {}
|
||||
for i = 1, #self.PlayerPowers do
|
||||
|
||||
@@ -75219,11 +75219,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e40000c-0000-4000-8000-00000e40000c",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent",
|
||||
"jsonString": {
|
||||
"name": "Job_fighter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter",
|
||||
"name": "Job_slot1",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -75407,11 +75407,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e40000d-0000-4000-8000-00000e40000d",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Name",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Name",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -75585,7 +75585,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "파이터",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -75595,11 +75595,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e40000e-0000-4000-8000-00000e40000e",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Desc",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Desc",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -75773,7 +75773,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "공격 특화\n콤보 어택 · 버서크\n라이징 어택",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -75783,11 +75783,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e40000f-0000-4000-8000-00000e40000f",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Starter",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_fighter/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot1/Starter",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -75961,7 +75961,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "대표 카드: 콤보 어택",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -75971,11 +75971,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400010-0000-4000-8000-00000e400010",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent",
|
||||
"jsonString": {
|
||||
"name": "Job_page",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page",
|
||||
"name": "Job_slot2",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -76159,11 +76159,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400011-0000-4000-8000-00000e400011",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Name",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Name",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -76337,7 +76337,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "페이지",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -76347,11 +76347,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400012-0000-4000-8000-00000e400012",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Desc",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Desc",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -76525,7 +76525,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "속성 차지 특화\n썬더/블리자드 차지\n파워 가드",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -76535,11 +76535,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400013-0000-4000-8000-00000e400013",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Starter",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_page/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot2/Starter",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -76713,7 +76713,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "대표 카드: 썬더 차지",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -76723,11 +76723,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400014-0000-4000-8000-00000e400014",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent",
|
||||
"jsonString": {
|
||||
"name": "Job_spearman",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman",
|
||||
"name": "Job_slot3",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -76911,11 +76911,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400015-0000-4000-8000-00000e400015",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Name",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Name",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Name",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -77089,7 +77089,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "스피어맨",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -77099,11 +77099,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400016-0000-4000-8000-00000e400016",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Desc",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Desc",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Desc",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -77277,7 +77277,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -77287,11 +77287,11 @@
|
||||
},
|
||||
{
|
||||
"id": "0e400017-0000-4000-8000-00000e400017",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Starter",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent",
|
||||
"jsonString": {
|
||||
"name": "Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_spearman/Starter",
|
||||
"path": "/ui/DefaultGroup/JobSelectHud/Job_slot3/Starter",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
@@ -77465,7 +77465,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "대표 카드: 피어스",
|
||||
"Text": "",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -289904,9 +289904,9 @@
|
||||
"PreserveSprite": 0,
|
||||
"StartFrameIndex": 0,
|
||||
"Color": {
|
||||
"r": 0.11,
|
||||
"g": 0.12,
|
||||
"b": 0.14,
|
||||
"r": 0.16,
|
||||
"g": 0.2,
|
||||
"b": 0.26,
|
||||
"a": 1
|
||||
},
|
||||
"DropShadow": false,
|
||||
@@ -289936,7 +289936,7 @@
|
||||
"a": 1
|
||||
},
|
||||
"OutlineWidth": 3,
|
||||
"RaycastTarget": false,
|
||||
"RaycastTarget": true,
|
||||
"Type": 1,
|
||||
"Enable": true
|
||||
},
|
||||
@@ -289985,7 +289985,7 @@
|
||||
"KeyCode": 0,
|
||||
"OverrideSorting": false,
|
||||
"Transition": 1,
|
||||
"Enable": false
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -290143,9 +290143,9 @@
|
||||
"DropShadowDistance": 32,
|
||||
"Font": 0,
|
||||
"FontColor": {
|
||||
"r": 0.55,
|
||||
"g": 0.58,
|
||||
"b": 0.62,
|
||||
"r": 0.94,
|
||||
"g": 0.74,
|
||||
"b": 0.26,
|
||||
"a": 1
|
||||
},
|
||||
"FontSize": 34,
|
||||
@@ -290280,9 +290280,9 @@
|
||||
"PreserveSprite": 0,
|
||||
"StartFrameIndex": 0,
|
||||
"Color": {
|
||||
"r": 0.18,
|
||||
"g": 0.19,
|
||||
"b": 0.21,
|
||||
"r": 0.3,
|
||||
"g": 0.4,
|
||||
"b": 0.75,
|
||||
"a": 1
|
||||
},
|
||||
"DropShadow": false,
|
||||
@@ -290472,9 +290472,9 @@
|
||||
"DropShadowDistance": 32,
|
||||
"Font": 0,
|
||||
"FontColor": {
|
||||
"r": 0.52,
|
||||
"g": 0.55,
|
||||
"b": 0.59,
|
||||
"r": 0.86,
|
||||
"g": 0.9,
|
||||
"b": 0.94,
|
||||
"a": 1
|
||||
},
|
||||
"FontSize": 20,
|
||||
@@ -290500,7 +290500,7 @@
|
||||
"bottom": 0
|
||||
},
|
||||
"SizeFit": false,
|
||||
"Text": "추후 열림",
|
||||
"Text": "마법 원거리 딜러",
|
||||
"UseOutLine": true,
|
||||
"Enable": true
|
||||
}
|
||||
@@ -290508,288 +290508,6 @@
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0e000098-0000-4000-8000-00000e000098",
|
||||
"path": "/ui/DefaultGroup/CharacterSelectHud/MageButton/LockBody",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent",
|
||||
"jsonString": {
|
||||
"name": "LockBody",
|
||||
"path": "/ui/DefaultGroup/CharacterSelectHud/MageButton/LockBody",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": true,
|
||||
"displayOrder": 3,
|
||||
"pathConstraints": "/////",
|
||||
"revision": 1,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "UISprite",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": null,
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "uisprite",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.UITransformComponent",
|
||||
"ActivePlatform": 255,
|
||||
"AlignmentOption": 0,
|
||||
"AnchorsMax": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"AnchorsMin": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"MobileOnly": false,
|
||||
"OffsetMax": {
|
||||
"x": 38,
|
||||
"y": 33
|
||||
},
|
||||
"OffsetMin": {
|
||||
"x": -38,
|
||||
"y": -25
|
||||
},
|
||||
"Pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"RectSize": {
|
||||
"x": 76,
|
||||
"y": 58
|
||||
},
|
||||
"UIMode": 1,
|
||||
"UIScale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"UIVersion": 2,
|
||||
"anchoredPosition": {
|
||||
"x": 0,
|
||||
"y": 4
|
||||
},
|
||||
"Position": {
|
||||
"x": 0,
|
||||
"y": 4,
|
||||
"z": 0
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteGUIRendererComponent",
|
||||
"AnimClipPlayType": 0,
|
||||
"EndFrameIndex": 2147483647,
|
||||
"ImageRUID": {
|
||||
"DataId": ""
|
||||
},
|
||||
"LocalPosition": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"LocalScale": {
|
||||
"x": 1,
|
||||
"y": 1
|
||||
},
|
||||
"OverrideSorting": false,
|
||||
"PlayRate": 1,
|
||||
"PreserveSprite": 0,
|
||||
"StartFrameIndex": 0,
|
||||
"Color": {
|
||||
"r": 0.78,
|
||||
"g": 0.69,
|
||||
"b": 0.42,
|
||||
"a": 1
|
||||
},
|
||||
"DropShadow": false,
|
||||
"DropShadowAngle": 30,
|
||||
"DropShadowColor": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 0.72
|
||||
},
|
||||
"DropShadowDistance": 32,
|
||||
"FillAmount": 1,
|
||||
"FillCenter": true,
|
||||
"FillClockWise": true,
|
||||
"FillMethod": 0,
|
||||
"FillOrigin": 0,
|
||||
"FlipX": false,
|
||||
"FlipY": false,
|
||||
"FrameColumn": 1,
|
||||
"FrameRate": 0,
|
||||
"FrameRow": 1,
|
||||
"Outline": false,
|
||||
"OutlineColor": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 1
|
||||
},
|
||||
"OutlineWidth": 3,
|
||||
"RaycastTarget": false,
|
||||
"Type": 1,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0e0000a2-0000-4000-8000-00000e0000a2",
|
||||
"path": "/ui/DefaultGroup/CharacterSelectHud/MageButton/LockShackle",
|
||||
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent",
|
||||
"jsonString": {
|
||||
"name": "LockShackle",
|
||||
"path": "/ui/DefaultGroup/CharacterSelectHud/MageButton/LockShackle",
|
||||
"nameEditable": true,
|
||||
"enable": true,
|
||||
"visible": true,
|
||||
"localize": true,
|
||||
"displayOrder": 4,
|
||||
"pathConstraints": "/////",
|
||||
"revision": 1,
|
||||
"origin": {
|
||||
"type": "Model",
|
||||
"entry_id": "UISprite",
|
||||
"sub_entity_id": null,
|
||||
"root_entity_id": null,
|
||||
"replaced_model_id": null
|
||||
},
|
||||
"modelId": "uisprite",
|
||||
"@components": [
|
||||
{
|
||||
"@type": "MOD.Core.UITransformComponent",
|
||||
"ActivePlatform": 255,
|
||||
"AlignmentOption": 0,
|
||||
"AnchorsMax": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"AnchorsMin": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"MobileOnly": false,
|
||||
"OffsetMax": {
|
||||
"x": 27,
|
||||
"y": 69
|
||||
},
|
||||
"OffsetMin": {
|
||||
"x": -27,
|
||||
"y": 27
|
||||
},
|
||||
"Pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"RectSize": {
|
||||
"x": 54,
|
||||
"y": 42
|
||||
},
|
||||
"UIMode": 1,
|
||||
"UIScale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"UIVersion": 2,
|
||||
"anchoredPosition": {
|
||||
"x": 0,
|
||||
"y": 48
|
||||
},
|
||||
"Position": {
|
||||
"x": 0,
|
||||
"y": 48,
|
||||
"z": 0
|
||||
},
|
||||
"QuaternionRotation": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0,
|
||||
"w": 1
|
||||
},
|
||||
"Scale": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "MOD.Core.SpriteGUIRendererComponent",
|
||||
"AnimClipPlayType": 0,
|
||||
"EndFrameIndex": 2147483647,
|
||||
"ImageRUID": {
|
||||
"DataId": ""
|
||||
},
|
||||
"LocalPosition": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"LocalScale": {
|
||||
"x": 1,
|
||||
"y": 1
|
||||
},
|
||||
"OverrideSorting": false,
|
||||
"PlayRate": 1,
|
||||
"PreserveSprite": 0,
|
||||
"StartFrameIndex": 0,
|
||||
"Color": {
|
||||
"r": 0.78,
|
||||
"g": 0.69,
|
||||
"b": 0.42,
|
||||
"a": 1
|
||||
},
|
||||
"DropShadow": false,
|
||||
"DropShadowAngle": 30,
|
||||
"DropShadowColor": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 0.72
|
||||
},
|
||||
"DropShadowDistance": 32,
|
||||
"FillAmount": 1,
|
||||
"FillCenter": true,
|
||||
"FillClockWise": true,
|
||||
"FillMethod": 0,
|
||||
"FillOrigin": 0,
|
||||
"FlipX": false,
|
||||
"FlipY": false,
|
||||
"FrameColumn": 1,
|
||||
"FrameRate": 0,
|
||||
"FrameRow": 1,
|
||||
"Outline": false,
|
||||
"OutlineColor": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 1
|
||||
},
|
||||
"OutlineWidth": 3,
|
||||
"RaycastTarget": false,
|
||||
"Type": 1,
|
||||
"Enable": true
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0e0000b4-0000-4000-8000-00000e0000b4",
|
||||
"path": "/ui/DefaultGroup/CharacterSelectHud/StartButton",
|
||||
|
||||
Reference in New Issue
Block a user