Merge pull request '도적 전직 구조를 Rogue 기준으로 정리' (#98) from codex/rogue-job-system into main

Reviewed-on: #98
This commit was merged in pull request #98.
This commit is contained in:
2026-06-30 01:55:18 +09:00
15 changed files with 416 additions and 191 deletions

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@
"unique": "f5def2e8022b4e59a17d3c16414034fe",
"legend": "cff71f2e472041ce80c6fbd296f42e2d"
},
"bandit": {
"rogue": {
"normal": "9487b06867bc46269ed1d855420f457f",
"unique": "b3081fb2fb1445fa90b12b01481a78ef",
"legend": "c357d2daf31a489d95b8fa47e50dd879"
@@ -25,11 +25,13 @@
"firepoison": "magician",
"icelightning": "magician",
"cleric": "magician",
"bandit": "bandit",
"curse": "bandit",
"shiv": "bandit",
"poisoner": "bandit",
"trickster": "bandit"
"curse": "rogue",
"shiv": "rogue",
"rogue": "rogue",
"assassin": "rogue",
"hermit": "rogue",
"thief": "rogue",
"thiefmaster": "rogue"
},
"rewardWeights": {
"normal": 70,

View File

@@ -376,7 +376,7 @@
"name": "무력화",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 3 줍니다. 약화를 1 부여합니다.",
"weak": 1,
@@ -387,7 +387,7 @@
"name": "타격",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 6 줍니다.",
"damage": 6,
@@ -397,7 +397,7 @@
"name": "생존자",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 8 얻습니다. 카드를 1장 버립니다.",
"block": 8,
@@ -408,7 +408,7 @@
"name": "수비",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 5 얻습니다.",
"block": 5,
@@ -418,7 +418,7 @@
"name": "칼질",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 6 줍니다.",
"damage": 6,
@@ -440,7 +440,7 @@
"name": "단검 분사",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "모든 적에게 피해를 4만큼 2번 줍니다.",
"aoe": true,
@@ -452,7 +452,7 @@
"name": "단검 투척",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 9 줍니다. 카드를 1장 뽑습니다. 카드를 1장 버립니다.",
"drawUntilHandSize": 6,
@@ -464,7 +464,7 @@
"name": "독 찌르기",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 6 줍니다. 중독을 3 부여합니다.",
"poison": 3,
@@ -475,7 +475,7 @@
"name": "불의의 일격",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 8 줍니다. 약화를 1 부여합니다.",
"weak": 1,
@@ -487,7 +487,7 @@
"name": "선제 타격",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 3 줍니다. 표창을 2장 손으로 가져옵니다.",
"damage": 3,
@@ -498,7 +498,7 @@
"name": "완수",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "피해를 7 줍니다. 손에 다른 카드가 5장 이상 있다면, 1번 추가로 적중합니다.",
"damage": 7,
@@ -510,7 +510,7 @@
"name": "재주넘기",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "교활. 모든 적에게 피해를 6 줍니다.",
"aoe": true,
@@ -522,7 +522,7 @@
"name": "도탄",
"cost": 2,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "교활. 무작위 적에게 피해를 3만큼 4번 줍니다.",
"damage": 3,
@@ -535,7 +535,7 @@
"name": "예비",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "카드를 1장 뽑습니다. 카드를 1장 버립니다.",
"blockPerDamageDealtThisTurn": 1,
@@ -546,7 +546,7 @@
"name": "예측",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "이번 턴 동안 민첩을 2 얻습니다.",
"dex": 2,
@@ -556,7 +556,7 @@
"name": "튕겨내기",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 4 얻습니다.",
"block": 4,
@@ -566,7 +566,7 @@
"name": "검무",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "표창을 3장 손으로 가져옵니다. 소멸.",
"addShiv": 3,
@@ -577,7 +577,7 @@
"name": "공중제비",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 5 얻습니다. 카드를 2장 뽑습니다.",
"block": 5,
@@ -588,7 +588,7 @@
"name": "구르기",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 4 얻습니다. 다음 턴에, 방어도를 4 얻습니다",
"block": 4,
@@ -599,7 +599,7 @@
"name": "귀를 찢는 비명",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "이번 턴 동안 모든 적이 힘을 6 잃습니다. 소멸.",
"draw": 1,
@@ -611,7 +611,7 @@
"name": "망토와 단검",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "방어도를 6 얻습니다. 표창을 1장 손으로 가져옵니다.",
"block": 6,
@@ -622,7 +622,7 @@
"name": "맹독",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "중독을 5 부여합니다.",
"poison": 5,
@@ -632,7 +632,7 @@
"name": "뱀 물기",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "보존. 중독을 7 부여합니다.",
"poison": 7,
@@ -643,7 +643,7 @@
"name": "범접 불가",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "normal",
"desc": "교활. 방어도를 6 얻습니다.",
"block": 6,
@@ -654,7 +654,7 @@
"name": "꼬챙이",
"cost": 2,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 8만큼 X번 줍니다.",
"useAllEnergy": true,
@@ -666,7 +666,7 @@
"name": "배신",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "선천성. 피해를 11 줍니다. 소멸.",
"innate": true,
@@ -677,7 +677,7 @@
"name": "정밀한 베기",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 13 줍니다. 손에 있는 다른 카드 1장당 피해량이 2 감소합니다.",
"damage": 13,
@@ -688,7 +688,7 @@
"name": "마무리",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "이번 턴에 사용한 공격 카드 1장당 피해를 6 줍니다.",
"damage": 0,
@@ -699,7 +699,7 @@
"name": "메멘토 모리",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 9 줍니다. 이번 턴에 버린 카드 1장당 피해량이 4 증가합니다.",
"damage": 9,
@@ -710,7 +710,7 @@
"name": "목 조르기",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 8 줍니다. 이번 턴에 카드를 사용할 때마다, 대상 적이 체력을 2 잃습니다.",
"damage": 8,
@@ -720,7 +720,7 @@
"name": "프레췌",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "손에 있는 스킬 카드 1장당 피해를 5 줍니다.",
"damage": 0,
@@ -731,7 +731,7 @@
"name": "덮치기",
"cost": 2,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 12 줍니다. 다음에 사용하는 스킬 카드의 비용이 0 이 됩니다.",
"damage": 12,
@@ -742,7 +742,7 @@
"name": "돌진",
"cost": 2,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "방어도를 10 얻습니다. 피해를 10 줍니다.",
"block": 10,
@@ -753,7 +753,7 @@
"name": "천적",
"cost": 2,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 15 줍니다. 다음 턴에, 카드를 2장 뽑습니다.",
"nextTurnDraw": 2,
@@ -764,7 +764,7 @@
"name": "정밀 사격",
"cost": 3,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "피해를 15 줍니다. 이번 턴에 스킬을 사용할 때마다 비용이 1 감소합니다.",
"damage": 15,
@@ -775,7 +775,7 @@
"name": "계산된 도박",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "손에 있는 모든 카드를 버린 뒤, 버린 카드의 수만큼 카드를 뽑습니다. 소멸.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
@@ -786,7 +786,7 @@
"name": "들춰내기",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "대상 적의 모든 인공물과 방어도를 제거합니다. 취약을 2 부여합니다. 소멸.",
"vuln": 2,
@@ -799,7 +799,7 @@
"name": "숨겨진 단검",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "카드를 2장 버립니다. 표창을 2장 손으로 가져옵니다.",
"discard": 2,
@@ -810,7 +810,7 @@
"name": "탈출구",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.",
"draw": 1,
@@ -821,7 +821,7 @@
"name": "곡예",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "카드를 3장 뽑습니다. 카드를 1장 버립니다.",
"draw": 3,
@@ -832,7 +832,7 @@
"name": "손기술",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "방어도를 7 얻습니다. 이번 턴 동안 손에 있는 스킬 카드 1장에 교활을 추가합니다.",
"block": 7,
@@ -843,7 +843,7 @@
"name": "신기루",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "모든 적에게 부여된 중독과 동일한 만큼의 방어도를 얻습니다. 소멸.",
"draw": 1,
@@ -853,7 +853,7 @@
"name": "전문성",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "손에 있는 카드가 6장이 될 때까지 카드를 뽑습니다.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
@@ -863,7 +863,7 @@
"name": "차오르는 독",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "적이 중독을 보유하고 있다면, 중독을 9 부여합니다.",
"poison": 9,
@@ -874,7 +874,7 @@
"name": "흐릿함",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "방어도를 5 얻습니다. 다음 턴 시작 시 방어도가 사라지지 않습니다.",
"block": 5,
@@ -885,7 +885,7 @@
"name": "다리 걸기",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "약화를 2 부여합니다. 방어도를 11 얻습니다.",
"block": 11,
@@ -896,7 +896,7 @@
"name": "비책",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "표창을 3장 손으로 가져옵니다. 이 카드의 비용이 1 감소합니다.",
"addShiv": 3,
@@ -907,7 +907,7 @@
"name": "탄성 플라스크",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "무작위 적에게 중독을 3만큼 3번 부여합니다.",
"poison": 3,
@@ -919,7 +919,7 @@
"name": "반사신경",
"cost": 3,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "교활. 카드를 2장 뽑습니다.",
"draw": 2,
@@ -930,7 +930,7 @@
"name": "아지랑이",
"cost": 3,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "교활. 모든 적에게 중독을 4 부여합니다.",
"poison": 4,
@@ -941,7 +941,7 @@
"name": "전략가",
"cost": 3,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "교활. 을 얻습니다.",
"gainEnergy": 1,
@@ -952,7 +952,7 @@
"name": "괜찮은 전략",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "내 턴 종료 시, 카드를 최대 1장까지 보존합니다.",
"powerEffect": "retainOne",
@@ -963,7 +963,7 @@
"name": "무한의 검날",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "내 턴 시작 시, 표창을 1장 손으로 가져옵니다.",
"turnStartShiv": 1,
@@ -973,7 +973,7 @@
"name": "발놀림",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "민첩을 2 얻습니다.",
"dex": 2,
@@ -983,7 +983,7 @@
"name": "발병",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "독이 3번 부여될 때마다 모든 적에게 11 피해를 줍니다.",
"image": "19361e72087946b1888684185b40d935",
@@ -994,7 +994,7 @@
"name": "유독 가스",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "내 턴 시작 시, 모든 적에게 중독을 2 부여합니다.",
"poison": 2,
@@ -1006,7 +1006,7 @@
"name": "정밀",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "표창의 피해량이 4 증가합니다.",
"shivDamageBonus": 4,
@@ -1016,7 +1016,7 @@
"name": "환영검",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "표창이 보존을 얻습니다. 매 턴마다 처음으로 사용하는 표창의 피해량이 9 증가합니다.",
"shivRetain": true,
@@ -1027,7 +1027,7 @@
"name": "스피드스터",
"cost": 2,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "unique",
"desc": "내 턴 동안 카드를 뽑을 때마다, 모든 적에게 피해를 2 줍니다.",
"aoe": true,
@@ -1038,7 +1038,7 @@
"name": "대단원의 막",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "뽑을 카드 더미에 카드가 없을 때만 사용할 수 있습니다. 모든 적에게 피해를 60 줍니다.",
"playableWhenDrawPileEmpty": true,
@@ -1050,7 +1050,7 @@
"name": "암살",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "선천성. 피해를 10 줍니다. 취약을 1 부여합니다. 소멸.",
"innate": true,
@@ -1062,7 +1062,7 @@
"name": "메아리 참격",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "모든 적에게 피해를 10 줍니다. 적을 처치할 때마다 이 효과를 반복합니다.",
"aoe": true,
@@ -1074,7 +1074,7 @@
"name": "사냥",
"cost": 1,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "피해를 10 줍니다. 치명타라면, 카드 보상을 추가로 얻습니다. 소멸.",
"damage": 10,
@@ -1085,7 +1085,7 @@
"name": "살해",
"cost": 3,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "피해를 1 줍니다. 이번 전투 동안 뽑은 카드 1장당 피해량이 1 증가합니다.",
"damage": 1,
@@ -1096,7 +1096,7 @@
"name": "불쾌",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "적이 힘을 X 잃습니다. 약화를 X 부여합니다. 소멸.",
"useAllEnergy": true,
@@ -1107,7 +1107,7 @@
"name": "아드레날린",
"cost": 0,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "를 얻습니다. 카드를 2장 뽑습니다. 소멸.",
"draw": 2,
@@ -1118,7 +1118,7 @@
"name": "강철의 폭풍",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "손에 있는 모든 카드를 버립니다. 버린 카드의 수만큼 표창을 손으로 가져옵니다.",
"discardAll": true,
@@ -1129,7 +1129,7 @@
"name": "그림자 걸음",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "손에 있는 모든 카드를 버립니다. 다음 턴에, 공격 카드의 피해량이 2배가 됩니다.",
"nextTurnAttackMultiplier": 2,
@@ -1140,7 +1140,7 @@
"name": "그림자 은신",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "이번 턴 동안 얻는 방어도가 2배가 됩니다.",
"blockGainMultiplier": 2,
@@ -1150,7 +1150,7 @@
"name": "부식성 파도",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "이번 턴에 카드를 뽑을 때마다, 모든 적에게 중독을 2 부여합니다.",
"drawPoison": 2,
@@ -1160,7 +1160,7 @@
"name": "잉크 칼날",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "잉크투성이 표창을 2장 손으로 가져옵니다.",
"addShiv": 2,
@@ -1170,7 +1170,7 @@
"name": "폭주",
"cost": 1,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "이번 턴에 다음에 사용하는 스킬 카드가 1번 추가로 사용됩니다.",
"draw": 1,
@@ -1181,7 +1181,7 @@
"name": "칼날 함정",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "대상 적에게 소멸된 카드 더미에 있는 모든 표창을 사용합니다.",
"draw": 1,
@@ -1191,7 +1191,7 @@
"name": "불릿 타임",
"cost": 3,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "이번 턴 동안 더 이상 카드를 뽑을 수 없습니다. 이번 턴 동안 손에 있는 모든 카드를 비용 없이 사용할 수 있습니다.",
"handCostZeroThisTurn": true,
@@ -1202,7 +1202,7 @@
"name": "악몽",
"cost": 3,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "카드를 1장 선택합니다. 다음 턴에, 그 카드의 복사본을 3장 손으로 가져옵니다. 소멸.",
"nextTurnCopies": 3,
@@ -1214,7 +1214,7 @@
"name": "작업 도구",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "내 턴 시작 시, 카드를 1장 뽑고 카드를 1장 버립니다.",
"turnStartDraw": 1,
@@ -1225,7 +1225,7 @@
"name": "잔상",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "카드를 사용할 때마다, 방어도를 1 얻습니다.",
"image": "0946f69d84464df29b24b94c744c868d",
@@ -1235,7 +1235,7 @@
"name": "촉진제",
"cost": 1,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "적 턴 시작 시 독이 한 번 더 틱합니다.",
"image": "19361e72087946b1888684185b40d935",
@@ -1245,7 +1245,7 @@
"name": "독 바르기",
"cost": 2,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "공격 카드가 막히지 않은 피해를 줄 때마다, 중독을 1 부여합니다.",
"attackPoison": 1,
@@ -1255,7 +1255,7 @@
"name": "설계의 대가",
"cost": 2,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "사용한 스킬 카드는 교활해집니다.",
"image": "c1e19219745e44c39ae6ac2f77e347d9",
@@ -1265,7 +1265,7 @@
"name": "추적",
"cost": 2,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "약화 상태의 적이 공격 카드로 받는 피해가 2배가 됩니다.",
"attackDamageVsWeakMultiplier": 2,
@@ -1275,7 +1275,7 @@
"name": "칼날 부채",
"cost": 2,
"kind": "Skill",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "표창이 이제 모든 적을 대상으로 합니다. 표창을 4장 손으로 가져옵니다.",
"addShiv": 4,
@@ -1286,7 +1286,7 @@
"name": "구렁이의 형상",
"cost": 3,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "카드를 사용할 때마다, 무작위 적에게 피해를 4 줍니다.",
"cardPlayedRandomDamage": 4,
@@ -1296,7 +1296,7 @@
"name": "연마",
"cost": 3,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "교활. 민첩을 1 얻습니다. 가시를 4 얻습니다.",
"dex": 1,
@@ -1308,7 +1308,7 @@
"name": "진압",
"cost": 0,
"kind": "Attack",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "선천성. 피해를 11 줍니다. 약화를 3 부여합니다.",
"innate": true,
@@ -1320,7 +1320,7 @@
"name": "유령의 형상",
"cost": 3,
"kind": "Power",
"class": "bandit",
"class": "rogue",
"rarity": "legend",
"desc": "불가침을 2 얻습니다. 내 턴 종료 시 민첩을 1 잃습니다.",
"intangible": 2,
@@ -1353,7 +1353,7 @@
"MagicGuard",
"MagicClaw"
],
"bandit": [
"rogue": [
"SilentStrike",
"SilentStrike",
"SilentStrike",

View File

@@ -1,7 +1,7 @@
{
"portraits": {
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"warrior": "28c88fdc5ab44f34a8b3fc1e19d4ce78",
"magician": "3b9ea1f066a744bb859df47fef817277",
"bandit": "efa920e58d31426486ef974106e7dc8b"
"rogue": "efa920e58d31426486ef974106e7dc8b"
}
}

View File

@@ -1,6 +1,6 @@
# Bandit Card Audit
# Rogue Card Audit
Current status of bandit cards and shared effect hooks.
Current status of rogue cards and shared effect hooks.
## Implemented

View File

@@ -0,0 +1,7 @@
# Codex Working Rules
1. 사용자가 특정 클래스만 수정하라고 했으면 그 클래스 외의 데이터, 설명문, 밸런스 문구는 건드리지 않는다.
2. 기존 한글 텍스트는 요청이 없으면 임의로 바꾸지 않는다.
3. 전직 구조를 바꿀 때는 실제 직업명만 사용한다. 임의의 내부 분류명이나 새 직업명을 사용자-facing 구조에 추가하지 않는다.
4. 대량 치환 전에 수정 대상 파일과 범위를 먼저 확인하고, 원본 문자열이 깨진 상태면 치환 작업을 진행하지 않는다.
5. 생성기 파일을 크게 수정할 때는 `node --check`와 생성기 실행으로 문법을 먼저 검증한 뒤 산출물을 갱신한다.

View File

@@ -11,14 +11,14 @@ self:RenderCharacterSelect()`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' },
]),
method('RenderCharacterSelect', `local base = "/ui/SelectUIGroup/CharacterSelectHud"
local arts = { { p = "/WarriorButton/Art", c = "warrior" }, { p = "/MageButton/Art", c = "magician" }, { p = "/BanditButton/Art", c = "bandit" } }
local arts = { { p = "/WarriorButton/Art", c = "warrior" }, { p = "/MageButton/Art", c = "magician" }, { p = "/BanditButton/Art", c = "rogue" } }
for i = 1, #arts do
local e = _EntityService:GetEntityByPath(base .. arts[i].p)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil and self.ClassPortraits ~= nil and self.ClassPortraits[arts[i].c] ~= nil then
e.SpriteGUIRendererComponent.ImageRUID = self.ClassPortraits[arts[i].c]
end
end
local btns = { { p = "/WarriorButton", c = "warrior" }, { p = "/MageButton", c = "magician" }, { p = "/BanditButton", c = "bandit" } }
local btns = { { p = "/WarriorButton", c = "warrior" }, { p = "/MageButton", c = "magician" }, { p = "/BanditButton", c = "rogue" } }
for i = 1, #btns do
local e = _EntityService:GetEntityByPath(base .. btns[i].p)
if e ~= nil then
@@ -44,9 +44,9 @@ if self.SelectedClass == "warrior" then
eng = "Warrior"
btnName = "/WarriorButton"
desc = "직업군 · 모험가" .. nl .. "방어를 쌓고 버티다 강하게 역공하는 단단한 탱커."
elseif self.SelectedClass == "bandit" then
elseif self.SelectedClass == "rogue" then
name = "도적"
eng = "Thief"
eng = "Rogue"
btnName = "/BanditButton"
desc = "직업군 · 모험가" .. nl .. "표창 난사와 독으로 빠르게 몰아치는 민첩한 직업."
elseif self.SelectedClass == "magician" then
@@ -65,7 +65,7 @@ end
self:SetText(base .. "/SelectedClass", name)
self:SetText(base .. "/SelectedClass/SelectedClassEng", eng)
self:SetText(base .. "/SelectedClassStatus", desc)`),
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "bandit" and self.SelectedClass ~= "magician" then
method('StartNewGame', `if self.SelectedClass ~= "warrior" and self.SelectedClass ~= "rogue" and self.SelectedClass ~= "magician" then
self:SetText("/ui/SelectUIGroup/CharacterSelectHud/SelectedClassStatus", "직업을 먼저 선택하세요")
return
end

View File

@@ -707,7 +707,7 @@ if anyAlive == false then
end
end
if node ~= nil and node.type == "boss" then
if self.PlayerJob == "" and self.Floor < self.RunLength then
if self:CanAdvanceJob() == true and self.Floor < self.RunLength then
self:ShowJobChoice()
else
if self.PlayerJob ~= "" then self:AwardSouls(1) end

View File

@@ -77,7 +77,7 @@ if thiefTab ~= nil and (thiefTab.ButtonComponent ~= nil or thiefTab:AddComponent
thiefTab:DisconnectEvent(ButtonClickEvent, self.ThiefDeckTabHandler)
self.ThiefDeckTabHandler = nil
end
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("bandit") end)
self.ThiefDeckTabHandler = thiefTab:ConnectEvent(ButtonClickEvent, function() self:SetClassDeckTab("rogue") end)
end
local mageTab = _EntityService:GetEntityByPath("/ui/DeckUIGroup/DeckAllHud/MageTab")
if mageTab ~= nil and (mageTab.ButtonComponent ~= nil or mageTab:AddComponent("ButtonComponent") ~= nil) then
@@ -101,8 +101,8 @@ end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], N
return
end
local className = self.SelectedClass
if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then
className = "bandit"
if className ~= "warrior" and className ~= "magician" and className ~= "rogue" then
className = "rogue"
end
self.CodexMode = false
self.ClassDeckMode = true
@@ -119,32 +119,30 @@ self:Toast("테스트 카드 추가 모드")`),
end
self.ClassDeckCards = {}
self.ClassDeckTitle = "직업 덱"
if className ~= "warrior" and className ~= "magician" and className ~= "bandit" then
className = "bandit"
if className ~= "warrior" and className ~= "magician" and className ~= "rogue" then
className = "rogue"
end
self.ClassDeckClass = className
local allowed = {}
local group = nil
if self.ClassGroups ~= nil then
group = self.ClassGroups[className]
end
if group == nil then
group = { className }
end
for i = 1, #group do
allowed[group[i]] = true
end
if className == "warrior" then
allowed["warrior"] = true
allowed["fighter"] = true
allowed["page"] = true
allowed["spearman"] = true
self.ClassDeckTitle = "전사 전체 덱"
elseif className == "magician" then
allowed["magician"] = true
allowed["firepoison"] = true
allowed["icelightning"] = true
allowed["cleric"] = true
self.ClassDeckTitle = "마법사 전체 덱"
else
allowed["bandit"] = true
allowed["shiv"] = true
allowed["poisoner"] = true
allowed["trickster"] = true
self.ClassDeckTitle = "도적 전체 덱"
end
for id, c in pairs(self.Cards) do
if c ~= nil and c.curse ~= true and allowed[c.class] == true then
if c ~= nil and c.curse ~= true and c.token ~= true and allowed[c.class] == true then
table.insert(self.ClassDeckCards, id)
end
end
@@ -162,7 +160,7 @@ self:RenderAllDeck()
self:RenderClassDeckTabs()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'className' }]),
method('RenderClassDeckTabs', `local tabs = {
{ path = "/ui/DeckUIGroup/DeckAllHud/WarriorTab", cls = "warrior" },
{ path = "/ui/DeckUIGroup/DeckAllHud/ThiefTab", cls = "bandit" },
{ path = "/ui/DeckUIGroup/DeckAllHud/ThiefTab", cls = "rogue" },
{ path = "/ui/DeckUIGroup/DeckAllHud/MageTab", cls = "magician" },
}
for i = 1, #tabs do

View File

@@ -1,9 +1,50 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
import { method } from '../lib/codeblock.mjs';
export const jobMethods = [
method('ShowJobChoice', `self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
method('BaseClassLabel', `if classId == "warrior" then
return "전사"
elseif classId == "rogue" then
return "Rogue"
elseif classId == "magician" then
return "마법사"
end
return "플레이어"`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'classId' }], 0, 'string'),
method('CurrentClassId', `if self.PlayerJob ~= nil and self.PlayerJob ~= "" then
return self.PlayerJob
end
return self.SelectedClass or ""`, [], 0, 'string'),
method('GetPlayableClasses', `local current = self:CurrentClassId()
if current == nil or current == "" then
return {}
end
if self.ClassLineages ~= nil and self.ClassLineages[current] ~= nil then
return self.ClassLineages[current]
end
return { current }`, [], 0, 'any'),
method('CanUseClassCard', `if cardClass == nil or cardClass == "" then
return false
end
if cardClass == "curse" then
return true
end
local playable = self:GetPlayableClasses()
for i = 1, #playable do
if playable[i] == cardClass then
return true
end
end
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardClass' }], 0, 'boolean'),
method('CanAdvanceJob', `local current = self:CurrentClassId()
if current == nil or current == "" or self.Jobs == nil then
return false
end
local opts = self.Jobs[current]
return opts ~= nil and #opts > 0`, [], 0, 'boolean'),
method('ShowJobChoice', `if self:CanAdvanceJob() ~= true then
self:ContinueAfterBoss()
return
end
self:SetEntityEnabled("/ui/RunUIGroup/CardHand", false)
self:SetEntityEnabled("/ui/RunUIGroup/DeckHud", false)
self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", true)`),
method('PickJobReward', `self:SetEntityEnabled("/ui/SelectUIGroup/JobChoiceHud", false)
@@ -20,9 +61,13 @@ if kind == "relic" then
else
self:ShowJobSelect()
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]),
method('ShowJobSelect', `local opts = self.Jobs[self.SelectedClass]
method('ShowJobSelect', `local current = self:CurrentClassId()
local opts = nil
if self.Jobs ~= nil then
opts = self.Jobs[current]
end
if opts == nil then
opts = self.Jobs["warrior"]
opts = {}
end
self.JobOpts = opts
for i = 1, 3 do
@@ -41,36 +86,30 @@ for i = 1, 3 do
end
end
self:SetEntityEnabled("/ui/SelectUIGroup/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
method('JobLabel', `if self.PlayerJob ~= "" and self.JobMeta ~= nil and self.JobMeta[self.PlayerJob] ~= nil then
return self.JobMeta[self.PlayerJob].name
end
if self.SelectedClass == "warrior" then
return "전사"
elseif self.SelectedClass == "bandit" then
return "도적"
elseif self.SelectedClass == "magician" then
return "마법사"
end
return "플레이어"`, [], 0, 'string'),
method('SetJob', `self.PlayerJob = jobId
return self:BaseClassLabel(self.SelectedClass)`, [], 0, 'string'),
method('SetJob', `local current = self:CurrentClassId()
local starter = ""
local opts = self.Jobs[self.SelectedClass] or {}
local tier = 2
local opts = {}
if self.Jobs ~= nil and self.Jobs[current] ~= nil then
opts = self.Jobs[current]
end
for i = 1, #opts do
if opts[i].id == jobId then
starter = opts[i].starter
starter = opts[i].starter or ""
tier = opts[i].tier or 2
break
end
end
self.PlayerJob = jobId
if starter ~= "" then
table.insert(self.RunDeck, starter)
local sc = self.Cards[starter]
if sc ~= nil then
self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 " .. sc.name)
self:Toast(tostring(tier) .. "차 전직: " .. self:JobLabel() .. "! 신규 카드 - " .. sc.name)
end
end
self:SetText("/ui/RunUIGroup/CombatHud/PlayerPanel/Name", self:JobLabel())

View File

@@ -5,7 +5,7 @@ import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER,
export const rewardMethods = [
method('CardPool', `local pool = {}
for id, c in pairs(self.Cards) do
if c.token ~= true and (c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob)) then
if c.token ~= true and self:CanUseClassCard(c.class) == true then
table.insert(pool, id)
end
end

View File

@@ -1,14 +1,14 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, JOB_META, CLASS_GROUPS, CLASS_LINEAGES, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaClassGroupsTable, luaClassLineagesTable, luaJobMetaTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const runMethods = [
method('StartRun', `if self.SelectedClass == "magician" then
self.PlayerMaxHp = ${CLASSES.magician.maxHp}
self.RunDeck = { ${CARDS.starterDecks.magician.map(luaStr).join(', ')} }
elseif self.SelectedClass == "bandit" then
self.PlayerMaxHp = ${CLASSES.bandit.maxHp}
self.RunDeck = { ${CARDS.starterDecks.bandit.map(luaStr).join(', ')} }
elseif self.SelectedClass == "rogue" then
self.PlayerMaxHp = ${CLASSES.rogue.maxHp}
self.RunDeck = { ${CARDS.starterDecks.rogue.map(luaStr).join(', ')} }
else
self.PlayerMaxHp = ${CLASSES.warrior.maxHp}
self.RunDeck = { ${CARDS.starterDecks.warrior.map(luaStr).join(', ')} }
@@ -30,6 +30,9 @@ self.CurrentNodeId = ""
self.CurrentEnemyId = ""
self.PlayerJob = ""
${luaJobsTable(JOBS)}
${luaJobMetaTable(JOB_META)}
${luaClassGroupsTable(CLASS_GROUPS)}
${luaClassLineagesTable(CLASS_LINEAGES)}
${luaFramesTable()}
${luaNodeIconsTable()}
${luaCharsTable()}

View File

@@ -75,7 +75,7 @@ if thief ~= nil and (thief.ButtonComponent ~= nil or thief:AddComponent("ButtonC
thief:DisconnectEvent(ButtonClickEvent, self.ThiefSelectHandler)
self.ThiefSelectHandler = nil
end
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("bandit") end)
self.ThiefSelectHandler = thief:ConnectEvent(ButtonClickEvent, function() self:SelectClass("rogue") end)
end
local mage = _EntityService:GetEntityByPath("/ui/SelectUIGroup/CharacterSelectHud/MageButton")
if mage ~= nil and (mage.ButtonComponent ~= nil or mage:AddComponent("ButtonComponent") ~= nil) then

View File

@@ -48,6 +48,9 @@ function writeCodeblocks() {
prop('any', 'AscPlusHandler'),
prop('any', 'JobOpts'),
prop('any', 'Jobs'),
prop('any', 'JobMeta'),
prop('any', 'ClassGroups'),
prop('any', 'ClassLineages'),
prop('number', 'AscensionLevel', '0'),
prop('number', 'AscensionUnlocked', '0'),
prop('any', 'StartGameHandler'),

View File

@@ -6,7 +6,7 @@ const ENEMIES = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
// 검증 (fail-fast): 잘못된 데이터면 생성 중단
const CLASSES = {
warrior: { label: '전사', maxHp: 80 },
bandit: { label: '도적', maxHp: 70 },
rogue: { label: '도적', maxHp: 70 },
magician: { label: '마법사', maxHp: 70 },
};
for (const cls of Object.keys(CLASSES)) {
@@ -15,22 +15,28 @@ for (const cls of Object.keys(CLASSES)) {
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' },
{ id: 'fighter', name: '파이터', desc: '공격 특화\n콤보 어택 · 버서크\n라이징 어택', starter: 'ComboAttack', tier: 2, parent: 'warrior' },
{ id: 'page', name: '페이지', desc: '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', starter: 'ThunderCharge', tier: 2, parent: 'warrior' },
{ id: 'spearman', name: '스피어맨', desc: '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', starter: 'Pierce', tier: 2, parent: 'warrior' },
],
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' },
{ id: 'firepoison', name: '위자드(불·독)', desc: '화염·독 특화\n파이어 애로우\n포이즌 브레스 · 앰플', starter: 'FireArrow', tier: 2, parent: 'magician' },
{ id: 'icelightning', name: '위자드(썬·콜)', desc: '광역·빙결 특화\n썬더 볼트(전체)\n콜드 빔 · 칠링 스텝', starter: 'ThunderBolt', tier: 2, parent: 'magician' },
{ id: 'cleric', name: '클레릭', desc: '회복·축복 특화\n힐 · 블레스\n홀리 애로우', starter: 'Heal', tier: 2, parent: 'magician' },
],
bandit: [
{ id: 'shiv', name: 'Shiv', desc: 'Many small attacks\nBlade Dance\nAccuracy · After Image', starter: 'BladeDance' },
{ id: 'poisoner', name: 'Poison', desc: 'Poison scaling\nDeadly Poison\nCatalyst · Noxious Fumes', starter: 'DeadlyPoison' },
{ id: 'trickster', name: 'Trickster', desc: 'Draw and tempo\nAcrobatics\nAdrenaline · Tools', starter: 'Acrobatics' },
rogue: [
{ id: 'assassin', name: 'Assassin', desc: '표창 중심 전직\n단일 화력과 독 압박\n빠른 마무리', starter: 'DeadlyPoison', tier: 2, parent: 'rogue' },
{ id: 'thief', name: 'Thief', desc: '단검 중심 전직\n드로우와 운영 강화\n빠른 연계', starter: 'Acrobatics', tier: 2, parent: 'rogue' },
],
assassin: [
{ id: 'hermit', name: 'Hermit', desc: 'Assassin의 3차 전직\n표창과 독 운영 심화\n누적 압박 강화', starter: 'NoxiousFumes', tier: 3, parent: 'assassin' },
],
thief: [
{ id: 'thiefmaster', name: 'Thief Master', desc: 'Thief의 3차 전직\n단검 운영 심화\n드로우와 템포 강화', starter: 'ToolsOfTheTrade', tier: 3, parent: 'thief' },
],
};
for (const [cls, jobs] of Object.entries(JOBS)) {
@@ -38,6 +44,42 @@ for (const [cls, jobs] of Object.entries(JOBS)) {
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
}
}
const CLASS_GROUPS = {
warrior: ['warrior', 'fighter', 'page', 'spearman'],
magician: ['magician', 'firepoison', 'icelightning', 'cleric'],
rogue: ['rogue', 'assassin', 'hermit', 'thief', 'thiefmaster'],
};
const CLASS_LINEAGES = {
warrior: ['warrior'],
fighter: ['warrior', 'fighter'],
page: ['warrior', 'page'],
spearman: ['warrior', 'spearman'],
magician: ['magician'],
firepoison: ['magician', 'firepoison'],
icelightning: ['magician', 'icelightning'],
cleric: ['magician', 'cleric'],
rogue: ['rogue'],
assassin: ['rogue', 'assassin'],
hermit: ['rogue', 'assassin', 'hermit'],
thief: ['rogue', 'thief'],
thiefmaster: ['rogue', 'thief', 'thiefmaster'],
};
const JOB_META = {};
for (const [sourceClass, jobs] of Object.entries(JOBS)) {
for (const job of jobs) {
JOB_META[job.id] = {
name: job.name,
starter: job.starter,
tier: job.tier ?? 2,
parent: job.parent ?? sourceClass,
sourceClass,
};
}
}
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
const SOUL_UNLOCKS = [
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
@@ -85,27 +127,23 @@ function luaCharsTable() {
}
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
const MAP_ROWS = 6;
const MAP_COLS = 4;
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
// 노드 맵 아이콘/배경 (공식 maplestory RUID, data/nodeicons.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const NODEICONS = JSON.parse(readFileSync('data/nodeicons.json', 'utf8'));
for (const t of ['combat', 'elite', 'boss', 'shop', 'rest', 'treasure']) {
if (!/^[0-9a-f]{32}$/.test((NODEICONS.icons || {})[t] || '')) throw new Error(`[gen-slaydeck] nodeicons.json icons.${t} RUID 누락/형식오류`);
}
if (!/^[0-9a-f]{32}$/.test(NODEICONS.background || '')) throw new Error('[gen-slaydeck] nodeicons.json background RUID 누락/형식오류');
// 캐릭터 선택 초상화 (메이커 임포트 RUID, data/characters.json 단일 소스 — 교체 시 이 파일만 수정 후 재생성)
const CHARS = JSON.parse(readFileSync('data/characters.json', 'utf8'));
for (const c of ['warrior', 'magician', 'bandit']) {
for (const c of ['warrior', 'magician', 'rogue']) {
if (!/^[0-9a-f]{32}$/.test((CHARS.portraits || {})[c] || '')) throw new Error(`[gen-slaydeck] characters.json portraits.${c} RUID 누락/형식오류`);
}
// 전투 카메라 고정값(StS2: 플레이어 좌·몬스터 우). KickCombatCamera가 StartCombat에서 재confine에 사용.
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
@@ -143,17 +181,33 @@ function luaEnemiesTable(enemies) {
`\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`);
return `self.Enemies = {\n${lines.join('\n')}\n}`;
}
// Lua 직렬화 헬퍼
function luaStr(s) {
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');
const items = list.map((j) =>
`\t\t{ id = ${luaStr(j.id)}, name = ${luaStr(j.name)}, desc = ${luaStr(j.desc)}, starter = ${luaStr(j.starter)}, tier = ${j.tier ?? 2}, parent = ${luaStr(j.parent ?? clsId)} },`).join('\n');
return `\t${clsId} = {\n${items}\n\t},`;
}).join('\n');
return `self.Jobs = {\n${cls}\n}`;
}
function luaClassGroupsTable(groups) {
const rows = Object.entries(groups).map(([clsId, list]) =>
`\t${clsId} = { ${list.map(luaStr).join(', ')} },`).join('\n');
return `self.ClassGroups = {\n${rows}\n}`;
}
function luaClassLineagesTable(lineages) {
const rows = Object.entries(lineages).map(([clsId, list]) =>
`\t${clsId} = { ${list.map(luaStr).join(', ')} },`).join('\n');
return `self.ClassLineages = {\n${rows}\n}`;
}
function luaJobMetaTable(meta) {
const rows = Object.entries(meta).map(([jobId, entry]) =>
`\t${jobId} = { name = ${luaStr(entry.name)}, starter = ${luaStr(entry.starter)}, tier = ${entry.tier}, parent = ${luaStr(entry.parent)}, sourceClass = ${luaStr(entry.sourceClass)} },`);
return `self.JobMeta = {\n${rows.join('\n')}\n}`;
}
function luaCardsTable(cards) {
const lines = Object.entries(cards).map(([id, c]) => {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
@@ -262,4 +316,11 @@ function luaDeckTable(deck) {
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
}
export { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable, luaCharsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable };
export {
CARDS, ENEMIES, CLASSES, JOBS, JOB_META, CLASS_GROUPS, CLASS_LINEAGES, SOUL_UNLOCKS,
luaSoulShopTable, CARDFRAMES, RARITIES, frameRuid, luaFramesTable, luaNodeIconsTable,
luaCharsTable, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS,
CAM, RELICS, luaRelicsTable, POTIONS, luaPotionsTable, luaIntentsArray, luaEnemiesTable,
luaStr, luaJobsTable, luaClassGroupsTable, luaClassLineagesTable, luaJobMetaTable,
luaCardsTable, luaDeckTable,
};