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:
2026-06-12 14:00:42 +09:00
8 changed files with 678 additions and 405 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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"
]
}
}

View 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 시그니처 변경 시 호출부(몬스터·플레이어) 동시 갱신 명시 ✓

View 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에러

View File

@@ -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 };
}

View File

@@ -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}`);
}
});

View File

@@ -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

View File

@@ -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",