feat(card-frames): 커스텀 카드 프레임 — 직업×등급 프레임·보상 가중 추첨 (P13) #50
File diff suppressed because one or more lines are too long
23
RootDesk/MyDesk/bandit_legend.sprite
Normal file
23
RootDesk/MyDesk/bandit_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://c357d2daf31a489d95b8fa47e50dd879",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/c357d2daf31a489d95b8fa47e50dd879/639168711224334184",
|
||||
"upload_hash": "A1639991C7A8CB1025C97E6BE93F618088971DC609F03106C50D8B6EA145F6A2",
|
||||
"name": "bandit_legend",
|
||||
"resource_guid": "c357d2daf31a489d95b8fa47e50dd879",
|
||||
"resource_version": "6a2c16d2cc7e89479f128ffb"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit_normal.sprite
Normal file
23
RootDesk/MyDesk/bandit_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://9487b06867bc46269ed1d855420f457f",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/9487b06867bc46269ed1d855420f457f/639168711224307886",
|
||||
"upload_hash": "FD9F2140D16EFC7A77F0B09CA230702CA6CA25E3178AAF6ACD63EF07D5C2C83E",
|
||||
"name": "bandit_normal",
|
||||
"resource_guid": "9487b06867bc46269ed1d855420f457f",
|
||||
"resource_version": "6a2c16d23d5de2eb0c7d16a2"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/bandit_unique.sprite
Normal file
23
RootDesk/MyDesk/bandit_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/b3081fb2fb1445fa90b12b01481a78ef/639168711224396185",
|
||||
"upload_hash": "1F5218270148F873D060223A617DFC184AEEE61344D26C85705E40EECC086D5E",
|
||||
"name": "bandit_unique",
|
||||
"resource_guid": "b3081fb2fb1445fa90b12b01481a78ef",
|
||||
"resource_version": "6a2c16d21a7908d59b5dc059"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_legend.sprite
Normal file
23
RootDesk/MyDesk/mage_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://cff71f2e472041ce80c6fbd296f42e2d",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/cff71f2e472041ce80c6fbd296f42e2d/639168711224422293",
|
||||
"upload_hash": "040CA63C5D3D52E1661F3A008D229A182B1A7C09D919BB74E23D1305B1AA56A7",
|
||||
"name": "mage_legend",
|
||||
"resource_guid": "cff71f2e472041ce80c6fbd296f42e2d",
|
||||
"resource_version": "6a2c16d2a0766b148f66d799"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_normal.sprite
Normal file
23
RootDesk/MyDesk/mage_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://d788d09f6f50467ebc67f01dec45f9e2",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/d788d09f6f50467ebc67f01dec45f9e2/639168711224258598",
|
||||
"upload_hash": "5D91E6E333564F15A76554AE07402DC0ED39ABC02439F65C2CCB818C71FB6994",
|
||||
"name": "mage_normal",
|
||||
"resource_guid": "d788d09f6f50467ebc67f01dec45f9e2",
|
||||
"resource_version": "6a2c16d2e75b0d4ccdfcee8b"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/mage_unique.sprite
Normal file
23
RootDesk/MyDesk/mage_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://f5def2e8022b4e59a17d3c16414034fe",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/f5def2e8022b4e59a17d3c16414034fe/639168711224271729",
|
||||
"upload_hash": "B07075BE5F13B1D7BFB4AE36F4C47D97A10A3CC7EF763F02DD2A11C2AFC61E67",
|
||||
"name": "mage_unique",
|
||||
"resource_guid": "f5def2e8022b4e59a17d3c16414034fe",
|
||||
"resource_version": "6a2c16d2e75b0d4ccdfcee8a"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_legend.sprite
Normal file
23
RootDesk/MyDesk/warior_legend.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://6d741a60c60743cb98ee740a1e2dbfed",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/6d741a60c60743cb98ee740a1e2dbfed/639168711258121433",
|
||||
"upload_hash": "80E2D8FFC8E56ECE4694BED5A387413F49633841DD37B7AA9FA9AC713962602C",
|
||||
"name": "warior_legend",
|
||||
"resource_guid": "6d741a60c60743cb98ee740a1e2dbfed",
|
||||
"resource_version": "6a2c16d59c3c6c308bfd4bf8"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_normal.sprite
Normal file
23
RootDesk/MyDesk/warior_normal.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://4bb57ef88ef449fdaf958f6cf37fe44b",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/4bb57ef88ef449fdaf958f6cf37fe44b/639168711258172386",
|
||||
"upload_hash": "2832F3B6C4C9530DE69DF061E590FD314D8646BE6BDB65931AFBE68D38DBB0ED",
|
||||
"name": "warior_normal",
|
||||
"resource_guid": "4bb57ef88ef449fdaf958f6cf37fe44b",
|
||||
"resource_version": "6a2c16d53d5de2eb0c7d16a3"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
RootDesk/MyDesk/warior_unique.sprite
Normal file
23
RootDesk/MyDesk/warior_unique.sprite
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "sprite://4f71c124c8bc4e13b5e9fad392995f68",
|
||||
"ContentType": "x-mod/sprite",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "0.1.0.0",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"upload_keyname": "30/54/30542a379cb74d2d807104635740a8ea/sprite/4f71c124c8bc4e13b5e9fad392995f68/639168711257524633",
|
||||
"upload_hash": "F27B2D713455FB18C10E924D381C4582E5E039D5DCCA9CECD2FB0D8E9A4C8135",
|
||||
"name": "warior_unique",
|
||||
"resource_guid": "4f71c124c8bc4e13b5e9fad392995f68",
|
||||
"resource_version": "6a2c16d5a43134e1b8a34b65"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
data/cardframes.json
Normal file
12
data/cardframes.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"frames": {
|
||||
"warrior": { "normal": "4bb57ef88ef449fdaf958f6cf37fe44b", "unique": "4f71c124c8bc4e13b5e9fad392995f68", "legend": "6d741a60c60743cb98ee740a1e2dbfed" },
|
||||
"magician": { "normal": "d788d09f6f50467ebc67f01dec45f9e2", "unique": "f5def2e8022b4e59a17d3c16414034fe", "legend": "cff71f2e472041ce80c6fbd296f42e2d" },
|
||||
"bandit": { "normal": "9487b06867bc46269ed1d855420f457f", "unique": "b3081fb2fb1445fa90b12b01481a78ef", "legend": "c357d2daf31a489d95b8fa47e50dd879" }
|
||||
},
|
||||
"classToFrame": {
|
||||
"warrior": "warrior", "fighter": "warrior", "page": "warrior", "spearman": "warrior",
|
||||
"magician": "magician", "firepoison": "magician", "icelightning": "magician", "cleric": "magician"
|
||||
},
|
||||
"rewardWeights": { "normal": 70, "unique": 25, "legend": 5 }
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
"damage": 6,
|
||||
"desc": "피해 6",
|
||||
"image": "a71b116807904ef2b38e1dc013e2f9a2",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Defend": {
|
||||
"name": "아이언 바디",
|
||||
@@ -16,7 +17,8 @@
|
||||
"block": 5,
|
||||
"desc": "방어도 5",
|
||||
"image": "1ae9b6741c5947a8b528a0f515b50e3e",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Bash": {
|
||||
"name": "슬래시 블러스트",
|
||||
@@ -25,7 +27,8 @@
|
||||
"damage": 10,
|
||||
"desc": "피해 10",
|
||||
"image": "d5bc2953fcab4cfe9062af81c35aff86",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"WarLeap": {
|
||||
"name": "워 리프",
|
||||
@@ -35,7 +38,8 @@
|
||||
"block": 3,
|
||||
"desc": "피해 4, 방어도 3",
|
||||
"image": "992dabf6aff2400e92b2f4f705d8ebe7",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Brandish": {
|
||||
"name": "브랜디시",
|
||||
@@ -44,7 +48,8 @@
|
||||
"damage": 13,
|
||||
"desc": "피해 13",
|
||||
"image": "21af4bccc5054a5dbc8245dfa7f08681",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"ChargedBlow": {
|
||||
"name": "차지 블로우",
|
||||
@@ -54,7 +59,8 @@
|
||||
"vuln": 2,
|
||||
"desc": "피해 8, 취약 2",
|
||||
"image": "fe83c7635b0e49ed83d75a2833adb53e",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Threaten": {
|
||||
"name": "위협",
|
||||
@@ -63,7 +69,8 @@
|
||||
"weak": 2,
|
||||
"desc": "약화 2 부여",
|
||||
"image": "64daadf1a98e490d9c14ef52ec776e63",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Enrage": {
|
||||
"name": "인레이지",
|
||||
@@ -72,7 +79,8 @@
|
||||
"strength": 2,
|
||||
"desc": "힘 +2",
|
||||
"image": "09370fc7551e47238fd103a80fba558e",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Rage": {
|
||||
"name": "분노",
|
||||
@@ -82,7 +90,8 @@
|
||||
"value": 1,
|
||||
"desc": "매 턴 시작 시 힘 +1",
|
||||
"image": "379d86e3de064959aa4612f71e84ccfb",
|
||||
"class": "warrior"
|
||||
"class": "warrior",
|
||||
"rarity": "legend"
|
||||
},
|
||||
"ComboAttack": {
|
||||
"name": "콤보 어택",
|
||||
@@ -92,7 +101,8 @@
|
||||
"damage": 5,
|
||||
"hits": 2,
|
||||
"desc": "피해 5 × 2회",
|
||||
"image": "1bc3e52b330648faae9eafd5a205e37b"
|
||||
"image": "1bc3e52b330648faae9eafd5a205e37b",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Berserk": {
|
||||
"name": "버서크",
|
||||
@@ -103,7 +113,8 @@
|
||||
"value": 1,
|
||||
"selfVuln": 1,
|
||||
"desc": "매턴 에너지 +1, 취약 1 자가",
|
||||
"image": "cef30ea340c74e768bcee4e2cbe0577a"
|
||||
"image": "cef30ea340c74e768bcee4e2cbe0577a",
|
||||
"rarity": "legend"
|
||||
},
|
||||
"RisingAttack": {
|
||||
"name": "라이징 어택",
|
||||
@@ -112,7 +123,8 @@
|
||||
"class": "fighter",
|
||||
"damage": 12,
|
||||
"desc": "피해 12",
|
||||
"image": "3a3d4b8bb5bd4137847caf883e4bf38e"
|
||||
"image": "3a3d4b8bb5bd4137847caf883e4bf38e",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"ThunderCharge": {
|
||||
"name": "썬더 차지",
|
||||
@@ -122,7 +134,8 @@
|
||||
"damage": 7,
|
||||
"weak": 1,
|
||||
"desc": "피해 7, 약화 1",
|
||||
"image": "f1b7e3041909411eb67af884b446e1e1"
|
||||
"image": "f1b7e3041909411eb67af884b446e1e1",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"BlizzardCharge": {
|
||||
"name": "블리자드 차지",
|
||||
@@ -132,7 +145,8 @@
|
||||
"damage": 7,
|
||||
"vuln": 1,
|
||||
"desc": "피해 7, 취약 1",
|
||||
"image": "7915c70952ad432f99519ad79bf929a4"
|
||||
"image": "7915c70952ad432f99519ad79bf929a4",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"PowerGuard": {
|
||||
"name": "파워 가드",
|
||||
@@ -141,7 +155,8 @@
|
||||
"class": "page",
|
||||
"block": 10,
|
||||
"desc": "방어도 10",
|
||||
"image": "90a9bf8eeb844b578b4e2d93ac43fedf"
|
||||
"image": "90a9bf8eeb844b578b4e2d93ac43fedf",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Pierce": {
|
||||
"name": "피어스",
|
||||
@@ -151,7 +166,8 @@
|
||||
"damage": 9,
|
||||
"pierce": true,
|
||||
"desc": "피해 9, 방어 무시",
|
||||
"image": "e312e535a2bc4fed82d36f9c6027c9db"
|
||||
"image": "e312e535a2bc4fed82d36f9c6027c9db",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"IronWall": {
|
||||
"name": "아이언 월",
|
||||
@@ -160,7 +176,8 @@
|
||||
"class": "spearman",
|
||||
"block": 12,
|
||||
"desc": "방어도 12",
|
||||
"image": "92021d62341a4bce9cfd09d1b4b865db"
|
||||
"image": "92021d62341a4bce9cfd09d1b4b865db",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"HyperBody": {
|
||||
"name": "하이퍼 바디",
|
||||
@@ -170,7 +187,8 @@
|
||||
"powerEffect": "blockPerTurn",
|
||||
"value": 3,
|
||||
"desc": "매턴 방어도 +3",
|
||||
"image": "b4020dbadee6401f9893a020fe4154b1"
|
||||
"image": "b4020dbadee6401f9893a020fe4154b1",
|
||||
"rarity": "legend"
|
||||
},
|
||||
"EnergyBolt": {
|
||||
"name": "에너지 볼트",
|
||||
@@ -179,7 +197,8 @@
|
||||
"class": "magician",
|
||||
"damage": 6,
|
||||
"desc": "피해 6",
|
||||
"image": "a1ee3069fce14498b92998542679ae40"
|
||||
"image": "a1ee3069fce14498b92998542679ae40",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"MagicGuard": {
|
||||
"name": "매직 가드",
|
||||
@@ -188,7 +207,8 @@
|
||||
"class": "magician",
|
||||
"block": 5,
|
||||
"desc": "방어도 5",
|
||||
"image": "01b249c26eb34b8aaab774bf221907a1"
|
||||
"image": "01b249c26eb34b8aaab774bf221907a1",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"MagicClaw": {
|
||||
"name": "매직 클로",
|
||||
@@ -198,7 +218,8 @@
|
||||
"damage": 3,
|
||||
"hits": 2,
|
||||
"desc": "피해 3 × 2회",
|
||||
"image": "d6e7c04c436f42f19e9806ac5b4401ae"
|
||||
"image": "d6e7c04c436f42f19e9806ac5b4401ae",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Teleport": {
|
||||
"name": "텔레포트",
|
||||
@@ -208,7 +229,8 @@
|
||||
"block": 3,
|
||||
"draw": 1,
|
||||
"desc": "방어도 3, 드로 1",
|
||||
"image": "80c98c8e032b4f6c8371a24b4e1d8f14"
|
||||
"image": "80c98c8e032b4f6c8371a24b4e1d8f14",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"Slow": {
|
||||
"name": "슬로우",
|
||||
@@ -217,7 +239,8 @@
|
||||
"class": "magician",
|
||||
"weak": 2,
|
||||
"desc": "약화 2 부여",
|
||||
"image": "16f79f571a964430bf1953edc9a14c73"
|
||||
"image": "16f79f571a964430bf1953edc9a14c73",
|
||||
"rarity": "normal"
|
||||
},
|
||||
"FireArrow": {
|
||||
"name": "파이어 애로우",
|
||||
@@ -226,7 +249,8 @@
|
||||
"class": "firepoison",
|
||||
"damage": 8,
|
||||
"desc": "피해 8",
|
||||
"image": "78b9be4e711c440f84fc21e51e812bae"
|
||||
"image": "78b9be4e711c440f84fc21e51e812bae",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"PoisonBreath": {
|
||||
"name": "포이즌 브레스",
|
||||
@@ -235,7 +259,8 @@
|
||||
"class": "firepoison",
|
||||
"poison": 4,
|
||||
"desc": "독 4 부여",
|
||||
"image": "b4e8bd7508b54d208e4f2ad7414f8c0a"
|
||||
"image": "b4e8bd7508b54d208e4f2ad7414f8c0a",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"ElementAmp": {
|
||||
"name": "엘레멘트 앰플",
|
||||
@@ -245,7 +270,8 @@
|
||||
"powerEffect": "strengthPerTurn",
|
||||
"value": 1,
|
||||
"desc": "매 턴 힘 +1",
|
||||
"image": "9859f3ab41b945f797d56cd83f95b25f"
|
||||
"image": "9859f3ab41b945f797d56cd83f95b25f",
|
||||
"rarity": "legend"
|
||||
},
|
||||
"ThunderBolt": {
|
||||
"name": "썬더 볼트",
|
||||
@@ -255,7 +281,8 @@
|
||||
"damage": 6,
|
||||
"aoe": true,
|
||||
"desc": "모든 적에게 피해 6",
|
||||
"image": "c6685d33cb2641f09d11cfa2d5cc820c"
|
||||
"image": "c6685d33cb2641f09d11cfa2d5cc820c",
|
||||
"rarity": "legend"
|
||||
},
|
||||
"ColdBeam": {
|
||||
"name": "콜드 빔",
|
||||
@@ -265,7 +292,8 @@
|
||||
"damage": 7,
|
||||
"weak": 2,
|
||||
"desc": "피해 7, 약화 2",
|
||||
"image": "e8f7c148c79f497d83014e3361f59f5c"
|
||||
"image": "e8f7c148c79f497d83014e3361f59f5c",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"ChillingStep": {
|
||||
"name": "칠링 스텝",
|
||||
@@ -274,7 +302,8 @@
|
||||
"class": "icelightning",
|
||||
"block": 8,
|
||||
"desc": "방어도 8",
|
||||
"image": "b2a7274d868241c78aa5780f2beecddf"
|
||||
"image": "b2a7274d868241c78aa5780f2beecddf",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Heal": {
|
||||
"name": "힐",
|
||||
@@ -283,7 +312,8 @@
|
||||
"class": "cleric",
|
||||
"heal": 10,
|
||||
"desc": "HP 10 회복",
|
||||
"image": "b4127c181e2942e38821d4a9a1f14596"
|
||||
"image": "b4127c181e2942e38821d4a9a1f14596",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"Bless": {
|
||||
"name": "블레스",
|
||||
@@ -293,7 +323,8 @@
|
||||
"strength": 1,
|
||||
"block": 5,
|
||||
"desc": "힘 +1, 방어도 5",
|
||||
"image": "d45553db4a414011b67486dfa8a12fe5"
|
||||
"image": "d45553db4a414011b67486dfa8a12fe5",
|
||||
"rarity": "unique"
|
||||
},
|
||||
"HolyArrow": {
|
||||
"name": "홀리 애로우",
|
||||
@@ -302,7 +333,8 @@
|
||||
"class": "cleric",
|
||||
"damage": 8,
|
||||
"desc": "피해 8",
|
||||
"image": "0265e103b4904f178b1c2bdcd54d5975"
|
||||
"image": "0265e103b4904f178b1c2bdcd54d5975",
|
||||
"rarity": "unique"
|
||||
}
|
||||
},
|
||||
"starterDecks": {
|
||||
|
||||
79
docs/superpowers/plans/2026-06-12-card-frames.md
Normal file
79
docs/superpowers/plans/2026-06-12-card-frames.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# P13 — 커스텀 카드 프레임 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. 설계: `2026-06-12-card-frames-design.md`
|
||||
|
||||
**Goal:** 사용자 제작 프레임 이미지(직업×등급)를 카드 UI 전체에 적용하고 등급을 보상 확률에 반영.
|
||||
|
||||
**Architecture:** 단일 소스(`data/*.json` + `gen-slaydeck.mjs`) → 산출물 재생성. 카드 배경 스프라이트를 프레임 ImageRUID로 교체(A안), `ApplyCardFace` 중앙 함수에서 class×rarity 조회.
|
||||
|
||||
**Tech Stack:** Node.js 생성기, MSW Lua, node --test.
|
||||
|
||||
### Task 1: 리소스 커밋
|
||||
- [ ] `.sprite` 9종 커밋: `git add RootDesk/MyDesk/*.sprite && git commit -m "feat(card-frames): 카드 프레임 스프라이트 9종 로컬 임포트 (warior·mage·bandit × normal·unique·legend)"`
|
||||
|
||||
### Task 2: 데이터 — rarity + cardframes.json
|
||||
- [ ] `data/cardframes.json` 신설 (설계서 JSON 그대로)
|
||||
- [ ] `data/cards.json` 32종에 `"rarity"` 추가 (설계서 표 그대로 — node 스크립트로 일괄 주입 권장)
|
||||
- [ ] 커밋: `feat(card-frames): 카드 등급 배정·프레임 RUID 매핑 데이터`
|
||||
|
||||
### Task 3: 생성기 — 프레임 렌더링
|
||||
- [ ] `CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json'))` 로드, 카드별 검증(throw): rarity ∈ {normal,unique,legend}, class ∈ classToFrame
|
||||
- [ ] `luaCardsTable`: `fields.push(\`rarity = ${luaStr(c.rarity)}\`)`
|
||||
- [ ] OnBeginPlay 주입(luaCardsTable 옆): `luaFramesTable()` — `self.CardFrames = {...}` + `self.ClassToFrame = {...}` / `prop('any','CardFrames')`·`prop('any','ClassToFrame')` 선언
|
||||
- [ ] `ApplyCardFace` Lua: kind 틴트 분기 → 프레임 적용
|
||||
|
||||
```lua
|
||||
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
|
||||
local ruid = frames ~= nil and frames[c.rarity or "normal"] or nil
|
||||
if ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] `cardFaceLayout(W)` 헬퍼 신설(s=W/180): Cost pos(-68s,103s)/size 44s/font 26s · Name pos(4s,97s)/size(150s,26s)/font 18s · Art pos(0,16s)/size 110s · Desc pos(0,-85s)/size(152s,64s)/font 16s
|
||||
- [ ] 카드 생성 5곳(upsertUi 손패 ~523 · 조회 ~787 · 전체덱 ~928 · 보상 ~1443 · 상점 ~1660)에 헬퍼 적용, NamePlate/CostPlate 생성 제거, 카드 스프라이트 type 0·흰색·프리뷰 프레임 RUID
|
||||
- [ ] CardHand 잔존 단색판 제거: upsertUi 초입 필터에 `/ui/DefaultGroup/CardHand/Card\d+/(NamePlate|CostPlate)` 경로 제거 추가
|
||||
- [ ] 커밋: `feat(card-frames): 생성기 — 프레임 렌더링·레이아웃 통합`
|
||||
|
||||
### Task 4: 보상 가중 추첨 (TDD)
|
||||
- [ ] `tools/balance/sim-balance.test.mjs`에 실패 테스트: `rarityForRoll(70)==='normal'`, `(71)==='unique'`, `(95)==='unique'`, `(96)==='legend'` → 실행해 FAIL 확인
|
||||
- [ ] `tools/balance/sim-balance.mjs`: `export function rarityForRoll(roll){ if (roll > 95) return 'legend'; if (roll > 70) return 'unique'; return 'normal'; }` → PASS 확인
|
||||
- [ ] `OfferReward` Lua 교체:
|
||||
|
||||
```lua
|
||||
local pool = self:CardPool()
|
||||
local byRarity = {}
|
||||
for _, id in ipairs(pool) do
|
||||
local r = self.Cards[id].rarity or "normal"
|
||||
if byRarity[r] == nil then byRarity[r] = {} end
|
||||
table.insert(byRarity[r], id)
|
||||
end
|
||||
self.RewardChoices = {}
|
||||
for i = 1, 3 do
|
||||
local roll = math.random(1, 100)
|
||||
local want = "normal"
|
||||
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
|
||||
local bucket = byRarity[want]
|
||||
if bucket == nil or #bucket == 0 then bucket = pool end
|
||||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||
end
|
||||
```
|
||||
|
||||
- [ ] 커밋: `feat(card-frames): 보상 등급 가중 추첨 70/25/5 (+JS 미러 테스트)`
|
||||
|
||||
### Task 5: 재생성·검증·산출물 커밋
|
||||
- [ ] `node tools/deck/gen-slaydeck.mjs` → `grep -c "CardFrames" RootDesk/MyDesk/SlayDeckController.codeblock` ≥1, `grep -c "4bb57ef88ef449fdaf958f6cf37fe44b" ui/DefaultGroup.ui` ≥1
|
||||
- [ ] `node --test tools/balance/sim-balance.test.mjs tools/map/rogue-map.test.mjs` 전건 통과
|
||||
- [ ] 커밋: `feat(card-frames): 산출물 재생성`
|
||||
|
||||
### Task 6: 메이커 검증·튜닝
|
||||
- [ ] maker_refresh_workspace → 빌드 콘솔 0에러 → 플레이: 손패 프레임·등급 구분, `_ResourceService` 로드 확인, 보상·덱 조회 스크린샷
|
||||
- [ ] 텍스트/아트 위치 어긋나면 `cardFaceLayout` 수치 조정 → 재생성 → 재확인 (수정 시 커밋)
|
||||
|
||||
### Task 7: PR·머지·메모리
|
||||
- [ ] push → `node tools/git/gitea-pr.mjs create <spec.json>` → merge → main pull → 메모리 갱신 (slaymaple-build-status에 P13 추가)
|
||||
|
||||
## Self-Review
|
||||
- 설계 전 항목에 대응 Task 존재 ✓ / 코드 블록 placeholder 없음 ✓ / CardFrames·ClassToFrame·rarityForRoll 명칭 일관 ✓ / maker_save 덮어쓰기 주의(설계서 '주의' 절) Task 6에서 refresh만 사용 ✓
|
||||
81
docs/superpowers/specs/2026-06-12-card-frames-design.md
Normal file
81
docs/superpowers/specs/2026-06-12-card-frames-design.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# P13 — 커스텀 카드 프레임 설계
|
||||
|
||||
날짜: 2026-06-12 (사용자 승인 완료)
|
||||
브랜치: `feature/p13-card-frames`
|
||||
|
||||
## 범위
|
||||
|
||||
사용자 제작 카드 프레임 이미지(직업 3종 × 등급 3종)를 인게임 카드 UI 전체(손패·보상·상점·덱 조회)에 적용한다. 카드에 등급(rarity)을 도입하고 전투 보상 추첨 확률에 반영한다.
|
||||
|
||||
## 리소스 (임포트 완료 — RUID 수확됨)
|
||||
|
||||
원본: `C:\Users\jaeoh\Desktop\workspace\source\images\maple\card\*.png` (263×366, 카드 비율 180×250과 동일한 0.72)
|
||||
메이커 로컬 임포트 → `RootDesk/MyDesk/<name>.sprite` 디스크립터 9종 (커밋 대상).
|
||||
|
||||
| 프레임 | normal | unique | legend |
|
||||
|---|---|---|---|
|
||||
| warior | `4bb57ef88ef449fdaf958f6cf37fe44b` | `4f71c124c8bc4e13b5e9fad392995f68` | `6d741a60c60743cb98ee740a1e2dbfed` |
|
||||
| mage | `d788d09f6f50467ebc67f01dec45f9e2` | `f5def2e8022b4e59a17d3c16414034fe` | `cff71f2e472041ce80c6fbd296f42e2d` |
|
||||
| bandit | `9487b06867bc46269ed1d855420f457f` | `b3081fb2fb1445fa90b12b01481a78ef` | `c357d2daf31a489d95b8fa47e50dd879` |
|
||||
|
||||
bandit은 RUID 등록만 하고 보류 (도적 클래스 추가 시 사용).
|
||||
|
||||
프레임 슬롯 구조: 좌상단 육각 코스트 · 상단 이름 배너 · 중앙 아트 영역 · 하단 설명 박스.
|
||||
|
||||
## 데이터
|
||||
|
||||
### `data/cardframes.json` (신설)
|
||||
|
||||
```json
|
||||
{
|
||||
"frames": {
|
||||
"warrior": { "normal": "4bb57ef88ef449fdaf958f6cf37fe44b", "unique": "4f71c124c8bc4e13b5e9fad392995f68", "legend": "6d741a60c60743cb98ee740a1e2dbfed" },
|
||||
"magician": { "normal": "d788d09f6f50467ebc67f01dec45f9e2", "unique": "f5def2e8022b4e59a17d3c16414034fe", "legend": "cff71f2e472041ce80c6fbd296f42e2d" },
|
||||
"bandit": { "normal": "9487b06867bc46269ed1d855420f457f", "unique": "b3081fb2fb1445fa90b12b01481a78ef", "legend": "c357d2daf31a489d95b8fa47e50dd879" }
|
||||
},
|
||||
"classToFrame": {
|
||||
"warrior": "warrior", "fighter": "warrior", "page": "warrior", "spearman": "warrior",
|
||||
"magician": "magician", "firepoison": "magician", "icelightning": "magician", "cleric": "magician"
|
||||
},
|
||||
"rewardWeights": { "normal": 70, "unique": 25, "legend": 5 }
|
||||
}
|
||||
```
|
||||
|
||||
### `data/cards.json` — 전 카드에 `rarity` 추가
|
||||
|
||||
| 등급 | 카드 (32종) |
|
||||
|---|---|
|
||||
| normal (10) | Strike, Defend, Bash, WarLeap, Threaten, EnergyBolt, MagicGuard, MagicClaw, Teleport, Slow |
|
||||
| unique (17) | Brandish, ChargedBlow, Enrage, ComboAttack, RisingAttack, ThunderCharge, BlizzardCharge, PowerGuard, Pierce, IronWall, FireArrow, PoisonBreath, ColdBeam, ChillingStep, Heal, Bless, HolyArrow |
|
||||
| legend (5) | Rage, Berserk, HyperBody, ElementAmp, ThunderBolt |
|
||||
|
||||
기준: 시작 덱·기본기 = normal / 강화·2차 전직 주력기 = unique / 파워 카드·전체 공격 = legend.
|
||||
|
||||
생성기 검증: `rarity` 누락 또는 normal|unique|legend 외 값이면 throw. 카드 class가 `classToFrame`에 없으면 throw.
|
||||
|
||||
## 렌더링 (생성기 — A안: 카드 배경 교체)
|
||||
|
||||
- 카드 루트 스프라이트: 단색 틴트(kind별) → 프레임 `ImageRUID`(Type 0, 흰색). NamePlate/CostPlate 단색판 제거 — RewardHud 등 생성 섹션은 생성 중단으로 충분, **CardHand는 .ui에 잔존하므로 upsert 시 경로 매칭으로 명시 제거**.
|
||||
- `ApplyCardFace`(Lua): kind 틴트 분기 제거 → `self.CardFrames[self.ClassToFrame[c.class]][c.rarity]` 적용. `CardFrames`/`ClassToFrame`는 OnBeginPlay에서 Lua 테이블 주입 + `prop('any', …)` 선언(LIA 1114 예방).
|
||||
- 자식 레이아웃 공용 헬퍼 `cardFaceLayout(W)` 신설 — 중복 5곳(손패 523·조회 787·전체덱 928·보상 1443·상점 1660 부근) 일괄 적용. 180×250 기준값(스케일 s=W/180):
|
||||
- Cost: pos(-68, 103)·size 44·font 26 (현 위치와 거의 일치)
|
||||
- Name: pos(4, 97)·size 150×26·font 18 — 상단 배너로 이동
|
||||
- Art: pos(0, 16)·size 110 — 중앙 아트 영역 확대
|
||||
- Desc: pos(0, -85)·size 152×64·font 16 — 하단 박스
|
||||
- 초깃값이며 메이커 스크린샷으로 미세 튜닝.
|
||||
- 정적 프리뷰(Card1~5)도 동일 프레임 적용.
|
||||
|
||||
## 보상 가중 추첨
|
||||
|
||||
`OfferReward`(Lua): 풀을 rarity 버킷으로 분류 후 1~100 롤 — ≤70 normal / ≤95 unique / >95 legend. 해당 버킷이 비면 전체 풀 폴백. 상점·전투 계산은 변경 없음 (sim-balance 전투 미러 무관).
|
||||
|
||||
JS 미러: `tools/balance/sim-balance.mjs`에 `rarityForRoll(roll)` export + 경계 테스트(70/71/95/96).
|
||||
|
||||
## 검증
|
||||
|
||||
재생성 → `grep -c` 카운트(CardFrames·rarity) → 기존 테스트 40건 + 신규 통과 → 메이커 refresh·빌드 0에러 → 플레이 스크린샷(손패 프레임·등급 색 구분·보상·덱 조회) → 텍스트 위치 튜닝.
|
||||
|
||||
## 주의 (이번 세션 실측)
|
||||
|
||||
- maker_save 시 메이커가 산출물을 재직렬화(0→0.0 등)하고 `Mislocated/`로 엔티티를 옮길 수 있음 → 임포트 후 `.sprite`만 남기고 산출물은 `git restore`로 복원했음. 재발 시 동일 절차.
|
||||
- sprite RUID는 map01.map에 등록되지 않고 `.sprite` 디스크립터 자체가 등록 메커니즘.
|
||||
@@ -29,6 +29,13 @@ export function shuffle(arr, rng) {
|
||||
|
||||
// 공격 피해 공식 — Lua CalcPlayerAttack(힘·약화) + DealDamageToTarget(취약)과 동기화.
|
||||
// floor((base + str) * (weak>0 ? 0.75 : 1)) → floor(... * (vulnOnTarget>0 ? 1.5 : 1))
|
||||
// 보상 카드 등급 추첨 (Lua OfferReward 미러) — roll ∈ 1..100, normal 70 / unique 25 / legend 5
|
||||
export function rarityForRoll(roll) {
|
||||
if (roll > 95) return 'legend';
|
||||
if (roll > 70) return 'unique';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
export function calcAttack(base, str, weak, vulnOnTarget) {
|
||||
let dmg = base + str;
|
||||
if (weak > 0) dmg = Math.floor(dmg * 0.75);
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack,
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch, calcAttack, rarityForRoll,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('rarityForRoll: 70/25/5 경계 (Lua OfferReward 미러)', () => {
|
||||
assert.equal(rarityForRoll(1), 'normal');
|
||||
assert.equal(rarityForRoll(70), 'normal');
|
||||
assert.equal(rarityForRoll(71), 'unique');
|
||||
assert.equal(rarityForRoll(95), 'unique');
|
||||
assert.equal(rarityForRoll(96), 'legend');
|
||||
assert.equal(rarityForRoll(100), 'legend');
|
||||
});
|
||||
|
||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||
assert.deepEqual(applyDamage(80, 0, 10), { hp: 70, block: 0 });
|
||||
assert.deepEqual(applyDamage(80, 5, 10), { hp: 75, block: 0 });
|
||||
|
||||
@@ -36,6 +36,29 @@ if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
|
||||
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
|
||||
}
|
||||
|
||||
// 카드 프레임 (사용자 제작 이미지 — 로컬 임포트 .sprite RUID, 직업 3종 × 등급 3종)
|
||||
const CARDFRAMES = JSON.parse(readFileSync('data/cardframes.json', 'utf8'));
|
||||
const RARITIES = ['normal', 'unique', 'legend'];
|
||||
for (const [fid, fr] of Object.entries(CARDFRAMES.frames)) {
|
||||
for (const r of RARITIES) {
|
||||
if (!fr[r]) throw new Error(`[gen-slaydeck] cardframes.frames.${fid}.${r} RUID 없음`);
|
||||
}
|
||||
}
|
||||
for (const [id, c] of Object.entries(CARDS.cards)) {
|
||||
if (!RARITIES.includes(c.rarity)) throw new Error(`[gen-slaydeck] 카드 ${id} rarity 누락/오류: ${c.rarity}`);
|
||||
const fc = CARDFRAMES.classToFrame[c.class];
|
||||
if (!fc || !CARDFRAMES.frames[fc]) throw new Error(`[gen-slaydeck] 카드 ${id} class ${c.class} → 프레임 매핑 없음`);
|
||||
}
|
||||
function frameRuid(card) {
|
||||
return CARDFRAMES.frames[CARDFRAMES.classToFrame[card.class]][card.rarity];
|
||||
}
|
||||
function luaFramesTable() {
|
||||
const frames = Object.entries(CARDFRAMES.frames).map(([fid, fr]) =>
|
||||
`\t${fid} = { normal = ${luaStr(fr.normal)}, unique = ${luaStr(fr.unique)}, legend = ${luaStr(fr.legend)} },`).join('\n');
|
||||
const cls = Object.entries(CARDFRAMES.classToFrame).map(([c, f]) => `\t${c} = ${luaStr(f)},`).join('\n');
|
||||
return `self.CardFrames = {\n${frames}\n}\nself.ClassToFrame = {\n${cls}\n}`;
|
||||
}
|
||||
|
||||
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
|
||||
const MAP_ROWS = 7; // 걷는 행 1..7, 보스 row 8
|
||||
const MAP_COLS = 4;
|
||||
@@ -100,6 +123,7 @@ function luaCardsTable(cards) {
|
||||
if (c.value != null) fields.push(`value = ${c.value}`);
|
||||
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
|
||||
fields.push(`class = ${luaStr(c.class)}`);
|
||||
fields.push(`rarity = ${luaStr(c.rarity)}`);
|
||||
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}`);
|
||||
@@ -162,6 +186,21 @@ const MAX_MONSTERS = 4;
|
||||
const HEAD_OFFSET_Y = 1.4; // 몬스터 월드 원점 위로 띄울 높이(머리 위) — world→screen 변환 전 가산
|
||||
|
||||
const HP_BAR_W = 140;
|
||||
const WHITE = { r: 1, g: 1, b: 1, a: 1 };
|
||||
const INK = { r: 0.13, g: 0.11, b: 0.09, a: 1 }; // 밝은 배너·설명 박스 위 먹색 글자
|
||||
// 카드 프레임(263×366 원본, 0.72 비율) 슬롯 레이아웃 — 180×250 기준값을 폭 비례 스케일
|
||||
function cardFaceLayout(W) {
|
||||
const s = W / 180;
|
||||
const r = (v) => Math.round(v * s);
|
||||
return {
|
||||
texts: [
|
||||
['Cost', { size: { x: r(44), y: r(44) }, pos: { x: r(-68), y: r(103) }, fontSize: r(26), bold: true, color: WHITE }],
|
||||
['Name', { size: { x: r(150), y: r(26) }, pos: { x: r(4), y: r(97) }, fontSize: r(18), bold: true, color: INK }],
|
||||
['Desc', { size: { x: r(152), y: r(64) }, pos: { x: 0, y: r(-85) }, fontSize: r(16), bold: false, color: INK }],
|
||||
],
|
||||
art: { size: { x: r(110), y: r(110) }, pos: { x: 0, y: r(16) } },
|
||||
};
|
||||
}
|
||||
const CARD_W = 180;
|
||||
const CARD_H = 250;
|
||||
const CARD_SPACING = 200;
|
||||
@@ -172,7 +211,8 @@ const ALIGN_BOTTOM_CENTER = 6;
|
||||
|
||||
function guid(prefix, n) {
|
||||
// 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑.
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4 : 0xfe;
|
||||
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4
|
||||
: prefix === 'ins2' ? 0xe5 : prefix === 'all2' ? 0xe6 : prefix === 'rwd2' ? 0xe7 : prefix === 'shp2' ? 0xe8 : 0xfe;
|
||||
const v = (ns * 0x100000 + n) >>> 0;
|
||||
return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`;
|
||||
}
|
||||
@@ -417,7 +457,9 @@ function appendUiSection(ui, section, entities) {
|
||||
function upsertUi() {
|
||||
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
|
||||
const E = ui.ContentProto.Entities;
|
||||
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e));
|
||||
// CardHand는 스톡 섹션이라 과거 생성된 단색판(NamePlate/CostPlate)이 잔존 → 프레임 이미지 도입으로 제거
|
||||
const obsoletePlate = /^\/ui\/DefaultGroup\/CardHand\/Card\d+\/(NamePlate|CostPlate)$/;
|
||||
ui.ContentProto.Entities = E.filter((e) => !isGeneratedUiEntity(e) && !obsoletePlate.test(e.path));
|
||||
|
||||
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
|
||||
const uiSections = new Map();
|
||||
@@ -444,7 +486,7 @@ function upsertUi() {
|
||||
const previewIds = Object.keys(CARDS.cards);
|
||||
const cards = Array.from({ length: 5 }, (_, i) => {
|
||||
const c = CARDS.cards[previewIds[i % previewIds.length]];
|
||||
return { name: c.name, cost: String(c.cost), desc: c.desc, tint: c.kind === 'Attack' ? ATTACK : DEFEND };
|
||||
return { name: c.name, cost: String(c.cost), desc: c.desc, frame: frameRuid(c) };
|
||||
});
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
@@ -456,9 +498,9 @@ function upsertUi() {
|
||||
tr.anchoredPosition = { x: CARD_XS[i - 1], y: 0 };
|
||||
tr.OffsetMin = { x: CARD_XS[i - 1] - CARD_W / 2, y: -CARD_H / 2 };
|
||||
tr.OffsetMax = { x: CARD_XS[i - 1] + CARD_W / 2, y: CARD_H / 2 };
|
||||
sp.ImageRUID = { DataId: '' };
|
||||
sp.Type = 1;
|
||||
sp.Color = cards[i - 1].tint;
|
||||
sp.ImageRUID = { DataId: cards[i - 1].frame };
|
||||
sp.Type = 0;
|
||||
sp.Color = WHITE;
|
||||
sp.RaycastTarget = true;
|
||||
const comps = card.jsonString['@components'];
|
||||
if (!comps.some((c) => c['@type'] === 'MOD.Core.ButtonComponent')) {
|
||||
@@ -476,11 +518,9 @@ function upsertUi() {
|
||||
card.jsonString.enable = true;
|
||||
card.jsonString.visible = true;
|
||||
|
||||
const children = [
|
||||
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: cards[i - 1].cost, fontSize: 26, bold: true }],
|
||||
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: cards[i - 1].name, fontSize: 20, bold: true }],
|
||||
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: cards[i - 1].desc, fontSize: 18, bold: false }],
|
||||
];
|
||||
const handLayout = cardFaceLayout(CARD_W);
|
||||
const previewValues = { Cost: cards[i - 1].cost, Name: cards[i - 1].name, Desc: cards[i - 1].desc };
|
||||
const children = handLayout.texts.map(([suffix, cfg]) => [suffix, { ...cfg, value: previewValues[suffix] }]);
|
||||
for (const [suffix, cfg] of children) {
|
||||
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
@@ -496,7 +536,7 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||||
],
|
||||
});
|
||||
ui.ContentProto.Entities.push(child);
|
||||
@@ -517,18 +557,19 @@ function upsertUi() {
|
||||
child.jsonString['@components'][2].Text = cfg.value;
|
||||
child.jsonString['@components'][2].FontSize = cfg.fontSize;
|
||||
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
|
||||
child.jsonString['@components'][2].FontColor = cfg.color;
|
||||
}
|
||||
}
|
||||
|
||||
// 프레임 이미지가 이름판·코스트판을 내장하므로 Art만 유지 (잔존 NamePlate/CostPlate는 upsertUi 초입에서 제거)
|
||||
const frameKids = [
|
||||
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
|
||||
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
|
||||
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
|
||||
['Art', 5, handLayout.art, WHITE, 0],
|
||||
];
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
|
||||
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||
if (!byPath.get(fPath)) {
|
||||
const fe = entity({
|
||||
let fe = byPath.get(fPath);
|
||||
if (!fe) {
|
||||
fe = entity({
|
||||
id: guid('dck', 200 + i * 10 + dOrder),
|
||||
path: fPath,
|
||||
modelId: 'uisprite',
|
||||
@@ -542,6 +583,14 @@ function upsertUi() {
|
||||
});
|
||||
ui.ContentProto.Entities.push(fe);
|
||||
byPath.set(fPath, fe);
|
||||
} else {
|
||||
const ftr = fe.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||||
if (ftr) {
|
||||
ftr.RectSize = cfg.size;
|
||||
ftr.anchoredPosition = cfg.pos;
|
||||
ftr.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
|
||||
ftr.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -743,14 +792,16 @@ function upsertUi() {
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
let insN = 6;
|
||||
const INSPECT_CARD_COUNT = 60;
|
||||
const INSPECT_CARD_W = 158;
|
||||
const INSPECT_CARD_H = 214;
|
||||
// 카드 단위 엔티티는 v2 네임스페이스(ins2/all2/rwd2/shp2) — 자식 구성이 바뀌면 id를 통째로 새로 발급해야 함.
|
||||
// 구 id를 다른 path에 재사용하면 메이커 refresh의 id 기준 in-place 병합이 꼬여 자식이 소실됨 (P13 실측).
|
||||
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
||||
const insBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('ins', insN++),
|
||||
id: guid('ins2', insBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
@@ -758,19 +809,16 @@ function upsertUi() {
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 950, parentH: 610, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: INSPECT_CARD_W, y: INSPECT_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: ATTACK, type: 1 }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
inspect.push(card);
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
|
||||
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
|
||||
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
|
||||
]) {
|
||||
const inspectLayout = cardFaceLayout(INSPECT_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
inspect.push(entity({
|
||||
id: guid('ins', insN++),
|
||||
id: guid('ins2', insBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
@@ -779,29 +827,23 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of [
|
||||
['NamePlate', 3, { size: { x: 148, y: 30 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
|
||||
['CostPlate', 4, { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
|
||||
['Art', 5, { size: { x: 84, y: 84 }, pos: { x: 0, y: 44 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
|
||||
]) {
|
||||
inspect.push(entity({
|
||||
id: guid('ins', insN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
id: guid('ins2', insBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: dOrder,
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color, type: spriteType, raycast: false }),
|
||||
transform({ parentW: INSPECT_CARD_W, parentH: INSPECT_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: inspectLayout.art.size, pos: inspectLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
emit('DeckInspectHud', inspect);
|
||||
|
||||
const allDeck = [];
|
||||
@@ -884,14 +926,15 @@ function upsertUi() {
|
||||
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
|
||||
],
|
||||
}));
|
||||
let allN = 6;
|
||||
const ALL_DECK_CARD_COUNT = 120;
|
||||
const ALL_DECK_CARD_W = 158;
|
||||
const ALL_DECK_CARD_H = 214;
|
||||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||||
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
||||
const allBase = 6 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
||||
const card = entity({
|
||||
id: guid('all', allN++),
|
||||
id: guid('all2', allBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
@@ -899,19 +942,16 @@ function upsertUi() {
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 980, parentH: 620, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: ALL_DECK_CARD_W, y: ALL_DECK_CARD_H }, pos: { x: 0, y: 0 } }),
|
||||
sprite({ color: ATTACK, type: 1 }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0 }),
|
||||
],
|
||||
});
|
||||
card.jsonString.enable = false;
|
||||
allDeck.push(card);
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
|
||||
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
|
||||
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
|
||||
]) {
|
||||
const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
allDeck.push(entity({
|
||||
id: guid('all', allN++),
|
||||
id: guid('all2', allBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
@@ -920,29 +960,23 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of [
|
||||
['NamePlate', 3, { size: { x: 148, y: 30 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
|
||||
['CostPlate', 4, { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
|
||||
['Art', 5, { size: { x: 84, y: 84 }, pos: { x: 0, y: 44 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
|
||||
]) {
|
||||
allDeck.push(entity({
|
||||
id: guid('all', allN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
id: guid('all2', allBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: dOrder,
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color, type: spriteType, raycast: false }),
|
||||
transform({ parentW: ALL_DECK_CARD_W, parentH: ALL_DECK_CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: allDeckLayout.art.size, pos: allDeckLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
emit('DeckAllHud', allDeck);
|
||||
|
||||
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
|
||||
@@ -1402,12 +1436,13 @@ function upsertUi() {
|
||||
text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let rwdN = 2;
|
||||
const rewardXs = [-300, 0, 300];
|
||||
// 카드 단위 엔티티 v2 네임스페이스 — DeckInspectHud 주석 참조
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const rwdBase = 2 + (i - 1) * 7;
|
||||
const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`;
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
id: guid('rwd2', rwdBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
@@ -1415,18 +1450,15 @@ function upsertUi() {
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: rewardXs[i - 1], y: 0 } }),
|
||||
sprite({ color: ATTACK, type: 1, raycast: true }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true }],
|
||||
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true }],
|
||||
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: '', fontSize: 18, bold: false }],
|
||||
]) {
|
||||
const rewardLayout = cardFaceLayout(CARD_W);
|
||||
for (const [tIdx, [suffix, cfg]] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]).entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
id: guid('rwd2', rwdBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
@@ -1435,29 +1467,24 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }),
|
||||
text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold, color: cfg.color }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of [
|
||||
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
|
||||
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
|
||||
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
|
||||
]) {
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
id: guid('rwd2', rwdBase + 6),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: dOrder,
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color, type: spriteType, raycast: false }),
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: rewardLayout.art.size, pos: rewardLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
let rwdN = 2 + 3 * 7; // 구 시퀀스의 루프 종료 시점 값(23) 보존 — Skip 등 후속 id 불변
|
||||
reward.push(entity({
|
||||
id: guid('rwd', rwdN++),
|
||||
path: '/ui/DefaultGroup/RewardHud/Skip',
|
||||
@@ -1618,12 +1645,13 @@ function upsertUi() {
|
||||
text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let shpN = 3;
|
||||
const shopXs = [-300, 0, 300];
|
||||
// 카드 단위 엔티티 v2 네임스페이스 (stride 8: Price 포함) — DeckInspectHud 주석 참조
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const shpBase = 3 + (i - 1) * 8;
|
||||
const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`;
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
id: guid('shp2', shpBase),
|
||||
path: cardPath,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
@@ -1631,19 +1659,17 @@ function upsertUi() {
|
||||
displayOrder: i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: CARD_W, y: CARD_H }, pos: { x: shopXs[i - 1], y: 20 } }),
|
||||
sprite({ color: ATTACK, type: 1, raycast: true }),
|
||||
sprite({ dataId: CARDFRAMES.frames.warrior.normal, color: WHITE, type: 0, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
for (const [suffix, cfg] of [
|
||||
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Desc', { size: { x: 164, y: 56 }, pos: { x: 0, y: -58 }, value: '', fontSize: 18, bold: false, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -105 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }],
|
||||
]) {
|
||||
const shopLayout = cardFaceLayout(CARD_W);
|
||||
const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]);
|
||||
shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]);
|
||||
for (const [tIdx, [suffix, cfg]] of shopTexts.entries()) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
id: guid('shp2', shpBase + 1 + tIdx),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
@@ -1656,25 +1682,20 @@ function upsertUi() {
|
||||
],
|
||||
}));
|
||||
}
|
||||
for (const [suffix, dOrder, cfg, color, spriteType] of [
|
||||
['NamePlate', 3, { size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }, 1],
|
||||
['CostPlate', 4, { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }, 1],
|
||||
['Art', 5, { size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }, 0],
|
||||
]) {
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: `${cardPath}/${suffix}`,
|
||||
id: guid('shp2', shpBase + 7),
|
||||
path: `${cardPath}/Art`,
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: dOrder,
|
||||
displayOrder: 5,
|
||||
components: [
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }),
|
||||
sprite({ color, type: spriteType, raycast: false }),
|
||||
transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: shopLayout.art.size, pos: shopLayout.art.pos }),
|
||||
sprite({ color: WHITE, type: 0, raycast: false }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
}
|
||||
let shpN = 3 + 3 * 8; // 구 시퀀스의 루프 종료 시점 값(27) 보존 — Relic 등 후속 id 불변
|
||||
shop.push(entity({
|
||||
id: guid('shp', shpN++),
|
||||
path: '/ui/DefaultGroup/ShopHud/Relic',
|
||||
@@ -2329,6 +2350,14 @@ function upsertUi() {
|
||||
appendUiSection(ui, section, entities);
|
||||
}
|
||||
|
||||
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
|
||||
const seenIds = new Map();
|
||||
for (const e of ui.ContentProto.Entities) {
|
||||
const prev = seenIds.get(e.id);
|
||||
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev} ↔ ${e.path})`);
|
||||
seenIds.set(e.id, e.path);
|
||||
}
|
||||
|
||||
JSON.parse(JSON.stringify(ui));
|
||||
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
|
||||
}
|
||||
@@ -2418,6 +2447,8 @@ function writeCodeblocks() {
|
||||
prop('string', 'DeckInspectKind', '""'),
|
||||
prop('boolean', 'DeckAllOpen', 'false'),
|
||||
prop('any', 'Cards'),
|
||||
prop('any', 'CardFrames'),
|
||||
prop('any', 'ClassToFrame'),
|
||||
prop('number', 'PlayerHp', '0'),
|
||||
prop('number', 'PlayerMaxHp', '80'),
|
||||
prop('number', 'PlayerBlock', '0'),
|
||||
@@ -2667,6 +2698,7 @@ self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self.PlayerJob = ""
|
||||
${luaJobsTable(JOBS)}
|
||||
${luaFramesTable()}
|
||||
self:GenerateMap()
|
||||
self:BindButtons()
|
||||
self:AddRelic("${RELICS.startingRelic}")
|
||||
@@ -3148,16 +3180,18 @@ end
|
||||
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
||||
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||||
if c == nil then
|
||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill", class = "warrior", rarity = "normal" }
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if c.kind == "Attack" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
||||
elseif c.kind == "Power" then
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||
else
|
||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||
local frames = self.CardFrames[self.ClassToFrame[c.class] or "warrior"]
|
||||
local ruid = nil
|
||||
if frames ~= nil then
|
||||
ruid = frames[c.rarity or "normal"]
|
||||
end
|
||||
if ruid ~= nil then
|
||||
e.SpriteGUIRendererComponent.ImageRUID = ruid
|
||||
e.SpriteGUIRendererComponent.Color = Color(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||
@@ -3981,9 +4015,20 @@ return pool`, [], 0, 'any'),
|
||||
method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false)
|
||||
local pool = self:CardPool()
|
||||
local byRarity = {}
|
||||
for _, id in ipairs(pool) do
|
||||
local r = self.Cards[id].rarity or "normal"
|
||||
if byRarity[r] == nil then byRarity[r] = {} end
|
||||
table.insert(byRarity[r], id)
|
||||
end
|
||||
self.RewardChoices = {}
|
||||
for i = 1, 3 do
|
||||
self.RewardChoices[i] = pool[math.random(1, #pool)]
|
||||
local roll = math.random(1, 100)
|
||||
local want = "normal"
|
||||
if roll > 95 then want = "legend" elseif roll > 70 then want = "unique" end
|
||||
local bucket = byRarity[want]
|
||||
if bucket == nil or #bucket == 0 then bucket = pool end
|
||||
self.RewardChoices[i] = bucket[math.random(1, #bucket)]
|
||||
self:ApplyRewardVisual(i, self.RewardChoices[i])
|
||||
end
|
||||
local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
||||
|
||||
76414
ui/DefaultGroup.ui
76414
ui/DefaultGroup.ui
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user