feat(card-visuals): 메이플 전사 스킬 카드 비주얼 (배포 퀄리티 P2) #35
@@ -551,7 +551,7 @@
|
|||||||
"Name": null
|
"Name": null
|
||||||
},
|
},
|
||||||
"Arguments": [],
|
"Arguments": [],
|
||||||
"Code": "self:ShowState(\"combat\")\nself:SetEntityEnabled(\"/ui/DefaultGroup/CombatHud/Result\", false)\nself.MaxEnergy = 3\nself.Turn = 0\nself.PlayerBlock = 0\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"타격\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6 },\n\tDefend = { name = \"방어\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5 },\n\tBash = { name = \"강타\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10 },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:BuildMonsters()\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()",
|
"Code": "self:ShowState(\"combat\")\nself:SetEntityEnabled(\"/ui/DefaultGroup/CombatHud/Result\", false)\nself.MaxEnergy = 3\nself.Turn = 0\nself.PlayerBlock = 0\nself.CombatOver = false\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"파워 스트라이크\", cost = 1, desc = \"피해 6\", kind = \"Attack\", damage = 6, image = \"a71b116807904ef2b38e1dc013e2f9a2\" },\n\tDefend = { name = \"아이언 바디\", cost = 1, desc = \"방어도 5\", kind = \"Skill\", block = 5, image = \"1ae9b6741c5947a8b528a0f515b50e3e\" },\n\tBash = { name = \"슬래시 블러스트\", cost = 2, desc = \"피해 10\", kind = \"Attack\", damage = 10, image = \"d5bc2953fcab4cfe9062af81c35aff86\" },\n}\nself.DrawPile = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:BuildMonsters()\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
@@ -845,7 +845,7 @@
|
|||||||
"Name": "cardId"
|
"Name": "cardId"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\tc = { name = cardId, cost = 0, desc = \"\", kind = \"Skill\" }\nend\nlocal base = \"/ui/DefaultGroup/DeckInspectHud/Grid/Card\" .. tostring(slot)\nself:SetText(base .. \"/Cost\", tostring(c.cost))\nself:SetText(base .. \"/Name\", c.name)\nself:SetText(base .. \"/Desc\", c.desc)\nlocal e = _EntityService:GetEntityByPath(base)\nif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
"Code": "self:ApplyCardFace(\"/ui/DefaultGroup/DeckInspectHud/Grid/Card\" .. tostring(slot), cardId)",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
@@ -920,7 +920,7 @@
|
|||||||
"Name": "cardId"
|
"Name": "cardId"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Code": "local cards = self.Cards or {}\nlocal c = cards[cardId]\nif c == nil then\n\tc = { name = cardId, cost = 0, desc = \"\", kind = \"Skill\" }\nend\nlocal base = \"/ui/DefaultGroup/DeckAllHud/Grid/Card\" .. tostring(slot)\nself:SetText(base .. \"/Cost\", tostring(c.cost))\nself:SetText(base .. \"/Name\", c.name)\nself:SetText(base .. \"/Desc\", c.desc)\nlocal e = _EntityService:GetEntityByPath(base)\nif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
"Code": "self:ApplyCardFace(\"/ui/DefaultGroup/DeckAllHud/Grid/Card\" .. tostring(slot), cardId)",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
@@ -949,6 +949,36 @@
|
|||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
"Name": "RenderHand"
|
"Name": "RenderHand"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Return": {
|
||||||
|
"Type": "void",
|
||||||
|
"DefaultValue": null,
|
||||||
|
"SyncDirection": 0,
|
||||||
|
"Attributes": [],
|
||||||
|
"Name": null
|
||||||
|
},
|
||||||
|
"Arguments": [
|
||||||
|
{
|
||||||
|
"Type": "string",
|
||||||
|
"DefaultValue": null,
|
||||||
|
"SyncDirection": 0,
|
||||||
|
"Attributes": [],
|
||||||
|
"Name": "base"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Type": "string",
|
||||||
|
"DefaultValue": null,
|
||||||
|
"SyncDirection": 0,
|
||||||
|
"Attributes": [],
|
||||||
|
"Name": "cardId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\tc = { name = cardId, cost = 0, desc = \"\", kind = \"Skill\" }\nend\nlocal e = _EntityService:GetEntityByPath(base)\nif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend\nself:SetText(base .. \"/Cost\", string.format(\"%d\", c.cost))\nself:SetText(base .. \"/Name\", c.name)\nself:SetText(base .. \"/Desc\", c.desc)\nlocal art = _EntityService:GetEntityByPath(base .. \"/Art\")\nif art ~= nil then\n\tif c.image ~= nil and c.image ~= \"\" then\n\t\tart.Enable = true\n\t\tif art.SpriteGUIRendererComponent ~= nil then\n\t\t\tart.SpriteGUIRendererComponent.ImageRUID = c.image\n\t\tend\n\telse\n\t\tart.Enable = false\n\tend\nend",
|
||||||
|
"Scope": 2,
|
||||||
|
"ExecSpace": 6,
|
||||||
|
"Attributes": [],
|
||||||
|
"Name": "ApplyCardFace"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Return": {
|
"Return": {
|
||||||
"Type": "void",
|
"Type": "void",
|
||||||
@@ -973,7 +1003,7 @@
|
|||||||
"Name": "cardId"
|
"Name": "cardId"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\tc = { name = cardId, cost = 0, desc = \"\", kind = \"Skill\" }\nend\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Cost\", tostring(c.cost))\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Name\", c.name)\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Desc\", c.desc)\nlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot))\nif cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\tcardEntity.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
"Code": "self:ApplyCardFace(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot), cardId)",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
@@ -1380,7 +1410,7 @@
|
|||||||
"Name": "cardId"
|
"Name": "cardId"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Code": "local c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nlocal base = \"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(slot)\nself:SetText(base .. \"/Name\", c.name)\nself:SetText(base .. \"/Cost\", tostring(c.cost))\nself:SetText(base .. \"/Desc\", c.desc)\nlocal e = _EntityService:GetEntityByPath(base)\nif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\tif c.kind == \"Attack\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\telseif c.kind == \"Skill\" then\n\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\telse\n\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\tend\nend",
|
"Code": "self:ApplyCardFace(\"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(slot), cardId)",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
@@ -1570,7 +1600,7 @@
|
|||||||
"Name": null
|
"Name": null
|
||||||
},
|
},
|
||||||
"Arguments": [],
|
"Arguments": [],
|
||||||
"Code": "self:SetText(\"/ui/DefaultGroup/ShopHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))\nfor i = 1, 3 do\n\tlocal cid = self.ShopChoices[i]\n\tlocal c = self.Cards[cid]\n\tlocal base = \"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i)\n\tif c ~= nil then\n\t\tself:SetText(base .. \"/Name\", c.name)\n\t\tself:SetText(base .. \"/Cost\", tostring(c.cost))\n\t\tself:SetText(base .. \"/Desc\", c.desc)\n\t\tself:SetText(base .. \"/Price\", string.format(\"%d\", 30) .. \" 골드\")\n\t\tlocal e = _EntityService:GetEntityByPath(base)\n\t\tif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\t\t\tif self.ShopBought[i] == true then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\t\telseif c.kind == \"Attack\" then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)\n\t\t\telseif c.kind == \"Skill\" then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)\n\t\t\telse\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)\n\t\t\tend\n\t\tend\n\tend\nend\nlocal rr = self.Relics[self.ShopRelic]\nif rr ~= nil then\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Label\", rr.name .. \" — \" .. rr.desc)\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Price\", string.format(\"%d\", 60) .. \" 골드\")\n\tlocal re = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\n\tif re ~= nil and re.SpriteGUIRendererComponent ~= nil then\n\t\tif self.ShopRelicBought == true then\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\telse\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)\n\t\tend\n\tend\nend",
|
"Code": "self:SetText(\"/ui/DefaultGroup/ShopHud/Gold\", \"골드 \" .. string.format(\"%d\", self.Gold))\nfor i = 1, 3 do\n\tlocal cid = self.ShopChoices[i]\n\tlocal c = self.Cards[cid]\n\tlocal base = \"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i)\n\tif c ~= nil then\n\t\tself:ApplyCardFace(base, cid)\n\t\tself:SetText(base .. \"/Price\", string.format(\"%d\", 30) .. \" 골드\")\n\t\tlocal e = _EntityService:GetEntityByPath(base)\n\t\tif e ~= nil and e.SpriteGUIRendererComponent ~= nil then\n\t\t\tif self.ShopBought[i] == true then\n\t\t\t\te.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\t\tend\n\t\tend\n\tend\nend\nlocal rr = self.Relics[self.ShopRelic]\nif rr ~= nil then\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Label\", rr.name .. \" — \" .. rr.desc)\n\tself:SetText(\"/ui/DefaultGroup/ShopHud/Relic/Price\", string.format(\"%d\", 60) .. \" 골드\")\n\tlocal re = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\n\tif re ~= nil and re.SpriteGUIRendererComponent ~= nil then\n\t\tif self.ShopRelicBought == true then\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)\n\t\telse\n\t\t\tre.SpriteGUIRendererComponent.Color = Color(0.7, 0.55, 0.85, 1)\n\t\tend\n\tend\nend",
|
||||||
"Scope": 2,
|
"Scope": 2,
|
||||||
"ExecSpace": 6,
|
"ExecSpace": 6,
|
||||||
"Attributes": [],
|
"Attributes": [],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"cards": {
|
"cards": {
|
||||||
"Strike": { "name": "타격", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6" },
|
"Strike": { "name": "파워 스트라이크", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6", "image": "a71b116807904ef2b38e1dc013e2f9a2" },
|
||||||
"Defend": { "name": "방어", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5" },
|
"Defend": { "name": "아이언 바디", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5", "image": "1ae9b6741c5947a8b528a0f515b50e3e" },
|
||||||
"Bash": { "name": "강타", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10" }
|
"Bash": { "name": "슬래시 블러스트", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10", "image": "d5bc2953fcab4cfe9062af81c35aff86" }
|
||||||
},
|
},
|
||||||
"starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"]
|
"starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"]
|
||||||
}
|
}
|
||||||
|
|||||||
212
docs/superpowers/plans/2026-06-11-card-visuals.md
Normal file
212
docs/superpowers/plans/2026-06-11-card-visuals.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# 메이플 스킬 카드 비주얼 (P2) 구현 계획
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Task 1·6은 메이커 MCP 인터랙티브 작업 — 컨트롤러가 직접 수행.**
|
||||||
|
|
||||||
|
**Goal:** 카드(손패/보상/상점/인스펙터/모든덱)에 메이플 스킬 이미지+프레임을 입히고 이름을 전사 스킬명으로 바꾼다 (효과·밸런스 불변).
|
||||||
|
|
||||||
|
**Architecture:** `data/cards.json`에 `image`(공식 RUID)·새 `name`. `gen-slaydeck.mjs`가 카드 자식 엔티티(Art/NamePlate/CostPlate)를 5표면에 생성하고, 런타임 렌더는 새 `ApplyCardFace(base, cardId)` 단일 헬퍼로 통일(기존 4개 Apply* 함수가 위임). 공식 RUID는 로컬 워크스페이스에서 렌더됨이 실측 검증됨(스펙 §1).
|
||||||
|
|
||||||
|
**Tech Stack:** Node.js ESM 생성기, MSW Lua codeblock, asset_search_resources MCP(수확), maker MCP(선별·검증).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 배경 (구현자용)
|
||||||
|
|
||||||
|
- 생성물 3종은 `tools/deck/gen-slaydeck.mjs` 단일 소스(직접 편집 금지). 루트에서 `node tools/deck/gen-slaydeck.mjs`. 각 Task 검증 후 산출물 복원(`git checkout -- ui/DefaultGroup.ui Global/common.gamelogic RootDesk/MyDesk/SlayDeckController.codeblock`), 산출물 커밋은 T5.
|
||||||
|
- 카드 렌더 함수 4종(현재 동일 모양): `ApplyCardVisual`(손패 `/ui/DefaultGroup/CardHand/Card{slot}`), `ApplyRewardVisual`(`/RewardHud/Reward{slot}`), `ApplyInspectCardVisual`(`/DeckInspectHud/Grid/Card{slot}`), `ApplyAllDeckCardVisual`(`/DeckAllHud/Grid/Card{slot}`). 각각 Cost/Name/Desc SetText + 루트 색.
|
||||||
|
- 카드 크기: 손패/보상/상점 180×250(`CARD_W`/`CARD_H`), 그리드(인스펙터/모든덱) 셀 158×214.
|
||||||
|
- 색: ATTACK {0.86,0.42,0.38} / DEFEND {0.42,0.55,0.85} / SKILL {0.46,0.68,0.52} (luaCardsTable의 kind 기반).
|
||||||
|
|
||||||
|
## 파일 구조
|
||||||
|
| 파일 | 책임 |
|
||||||
|
|---|---|
|
||||||
|
| `data/cards.json` | name 3종 변경 + image RUID 3종 (T1) |
|
||||||
|
| `tools/deck/gen-slaydeck.mjs` | luaCardsTable image·ApplyCardFace·4함수 통일(T2), 카드 프레임 엔티티 5표면(T3·T4) |
|
||||||
|
| 산출물 | T5 재생성·커밋 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 (컨트롤러 직접): RUID 수확 + cards.json
|
||||||
|
|
||||||
|
메이커 MCP 인터랙티브 — subagent 금지, 컨트롤러가 수행.
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `asset_search_resources`(cat=sprite, source=maplestory)로 후보 수집: 질의 "파워 스트라이크"(이미 10건 확보), "슬래시 블러스트", "아이언 바디". 후보 부족 시 보조 질의("슬래시", "강철", "워리어").
|
||||||
|
- [ ] **Step 2:** 메이커 Play→전투 진입 후, 후보 RUID를 Card1 Art 자리(`SpriteGUIRendererComponent.ImageRUID`, Type=0)에 순회 주입 + 스크린샷으로 스킬당 1개 선별(일러스트 적합성 기준: 식별 가능·단일 컷·과도한 투명 여백 없음).
|
||||||
|
- [ ] **Step 3:** `data/cards.json`을 다음 형태로 갱신(RUID는 선별값으로):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cards": {
|
||||||
|
"Strike": { "name": "파워 스트라이크", "cost": 1, "kind": "Attack", "damage": 6, "desc": "피해 6", "image": "<선별RUID>" },
|
||||||
|
"Defend": { "name": "아이언 바디", "cost": 1, "kind": "Skill", "block": 5, "desc": "방어도 5", "image": "<선별RUID>" },
|
||||||
|
"Bash": { "name": "슬래시 블러스트", "cost": 2, "kind": "Attack", "damage": 10, "desc": "피해 10", "image": "<선별RUID>" }
|
||||||
|
},
|
||||||
|
"starterDeck": ["Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] **Step 4:** `node -e "JSON.parse(require('fs').readFileSync('data/cards.json','utf8'));console.log('ok')"` → ok. sim 회귀: `node --test tools/balance/sim-balance.test.mjs` → 14/14 (fixture 자체 카드라 무관).
|
||||||
|
- [ ] **Step 5:** Commit: `git add data/cards.json && git commit -m "feat(card-visuals): 카드를 전사 스킬로 리네임 + 공식 스킬 이미지 RUID"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: ApplyCardFace 렌더 일원화
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: luaCardsTable에 image 직렬화.** `function luaCardsTable(cards)`의 fields 구성에 추가:
|
||||||
|
```js
|
||||||
|
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
|
||||||
|
```
|
||||||
|
(`if (c.block != null) ...` 줄 다음에.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: ApplyCardFace 메서드 추가** (`ApplyCardVisual` 메서드 정의 바로 앞에):
|
||||||
|
```js
|
||||||
|
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||||||
|
if c == nil then
|
||||||
|
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
||||||
|
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 == "Skill" then
|
||||||
|
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||||
|
else
|
||||||
|
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||||
|
self:SetText(base .. "/Name", c.name)
|
||||||
|
self:SetText(base .. "/Desc", c.desc)
|
||||||
|
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||||
|
if art ~= nil then
|
||||||
|
if c.image ~= nil and c.image ~= "" then
|
||||||
|
art.Enable = true
|
||||||
|
if art.SpriteGUIRendererComponent ~= nil then
|
||||||
|
art.SpriteGUIRendererComponent.ImageRUID = c.image
|
||||||
|
end
|
||||||
|
else
|
||||||
|
art.Enable = false
|
||||||
|
end
|
||||||
|
end`, [
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
]),
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 기존 4개 함수를 위임으로 교체.** 각 메서드 본문 전체를:
|
||||||
|
- `ApplyCardVisual`: `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`
|
||||||
|
- `ApplyRewardVisual`: `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`
|
||||||
|
- `ApplyInspectCardVisual`: `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`
|
||||||
|
- `ApplyAllDeckCardVisual`: 기존 본문에서 카드 면 설정부를 `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`로 교체(본문에 수량 배지 등 추가 로직이 있으면 그 부분은 유지 — 현재 본문을 읽고 카드면(Cost/Name/Desc/색) 설정부만 위임).
|
||||||
|
(인자 목록은 변경 없음.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: RenderShop 카드부 위임.** `RenderShop` 본문에서 상점 카드 면 설정부(Card{i}의 Cost/Name/Desc SetText + 색 설정)를 `self:ApplyCardFace(base, cid)` 호출로 교체(가격(Price) SetText·구매 상태 처리는 유지). 본문을 읽고 해당 부분만 정확히 치환.
|
||||||
|
|
||||||
|
- [ ] **Step 5: 검증.** `node --check` → OK. `node tools/deck/gen-slaydeck.mjs` 후:
|
||||||
|
`node -e "const cb=JSON.parse(require('fs').readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8'));const s=JSON.stringify(cb);const names=cb.ContentProto.Json.Methods.map(m=>m.Name);console.log('face:',names.includes('ApplyCardFace'),'| image in Cards:',s.includes('image = '),'| delegates:',(s.match(/ApplyCardFace\(/g)||[]).length>=5)"`
|
||||||
|
Expected: 모두 true. 산출물 복원.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit:** `git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(card-visuals): ApplyCardFace 렌더 일원화 + Cards image 직렬화"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 손패 카드 프레임 (Card1~5)
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs` (upsertUi의 손패 카드 갱신부)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 현재 손패 카드 빌드부 읽기.** upsertUi에서 `for (let i = 1; i <= 5; i++)`로 `/ui/DefaultGroup/CardHand/Card{i}`를 byPath 갱신하고 children(['Cost'...],['Name'...],['Desc'...])을 생성/갱신하는 블록을 찾는다.
|
||||||
|
|
||||||
|
- [ ] **Step 2: children 배열을 프레임 배치로 교체.** 기존 children 항목의 cfg를:
|
||||||
|
```js
|
||||||
|
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 }],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
로 교체(기존 child 갱신 분기에서도 cfg의 size/pos를 반영하도록 — 기존 갱신 분기가 Text/FontSize만 갱신한다면 transform도 cfg로 갱신하는 줄 추가:
|
||||||
|
```js
|
||||||
|
const tr0 = child.jsonString['@components'][0];
|
||||||
|
tr0.RectSize = cfg.size;
|
||||||
|
tr0.anchoredPosition = cfg.pos;
|
||||||
|
tr0.OffsetMin = { x: cfg.pos.x - cfg.size.x / 2, y: cfg.pos.y - cfg.size.y / 2 };
|
||||||
|
tr0.OffsetMax = { x: cfg.pos.x + cfg.size.x / 2, y: cfg.pos.y + cfg.size.y / 2 };
|
||||||
|
```
|
||||||
|
)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 프레임 자식 추가(없으면 생성, byPath 패턴 동일).** children 루프 뒤에 카드별로:
|
||||||
|
```js
|
||||||
|
const frameKids = [
|
||||||
|
['NamePlate', 'uisprite', 'UISprite', 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', 3,
|
||||||
|
{ size: { x: 168, y: 34 }, pos: { x: 0, y: -8 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.92 }],
|
||||||
|
['CostPlate', 'uisprite', 'UISprite', 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', 4,
|
||||||
|
{ size: { x: 44, y: 44 }, pos: { x: -68, y: 103 } }, { r: 0.07, g: 0.08, b: 0.1, a: 0.95 }],
|
||||||
|
['Art', 'uisprite', 'UISprite', 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', 5,
|
||||||
|
{ size: { x: 96, y: 96 }, pos: { x: 0, y: 52 } }, { r: 1, g: 1, b: 1, a: 1 }],
|
||||||
|
];
|
||||||
|
for (const [suffix, modelId, entryId, componentNames, dOrder, cfg, color] of frameKids) {
|
||||||
|
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||||
|
if (!byPath.get(fPath)) {
|
||||||
|
const fe = entity({
|
||||||
|
id: guid('dck', 100 + i * 10 + dOrder),
|
||||||
|
path: fPath, modelId, entryId, componentNames,
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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(suffix === 'Art' ? { color, type: 0, raycast: false } : { color, type: 1, raycast: false }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
ui.ContentProto.Entities.push(fe);
|
||||||
|
byPath.set(fPath, fe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
주의: `guid('dck', N)` 기존 사용 대역 확인(grep `guid('dck'`) 후 충돌 시 200+로 이동. Cost/Name/Desc 텍스트가 NamePlate/CostPlate **위에** 그려지도록 displayOrder 관계 확인(텍스트 0/1/2 < 플레이트 3/4면 플레이트가 위 — **플레이트가 텍스트를 가리면 안 되므로** 텍스트 displayOrder를 6/7/8로 올리고 플레이트 3/4·Art 5 유지: Cost→7, Name→6, Desc→8로 child 생성/갱신 분기에서 displayOrder 설정).
|
||||||
|
|
||||||
|
- [ ] **Step 4: 검증.** `node --check` → 실행 → 확인:
|
||||||
|
`node -e "const ui=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const p=ui.ContentProto.Entities.map(e=>e.path);console.log('art:',[1,2,3,4,5].every(i=>p.includes('/ui/DefaultGroup/CardHand/Card'+i+'/Art')),'| plates:',[1,2,3,4,5].every(i=>p.includes('/ui/DefaultGroup/CardHand/Card'+i+'/NamePlate')))"` → 모두 true. dup id 0 확인. 산출물 복원.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit:** `git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(card-visuals): 손패 카드 프레임(Art·NamePlate·CostPlate)"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 보상/상점/그리드 카드 프레임
|
||||||
|
|
||||||
|
**Files:** Modify `tools/deck/gen-slaydeck.mjs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 보상 카드(Reward1~3, 180×250).** 보상 카드 빌더(자식 Cost/Name/Desc 생성 루프)에서 자식 cfg를 Task 3 Step 2와 동일 좌표로 바꾸고(Cost 44×44@(-68,103) f26 / Name 168×30@(0,-8) f20 / Desc 164×70@(0,-62) f18), 동일한 NamePlate/CostPlate/Art 3종을 push(부모 RewardHud/Reward{i}, guid('rwd', 100+i*10+dOrder), displayOrder: 텍스트 6/7/8·플레이트 3/4·Art 5).
|
||||||
|
|
||||||
|
- [ ] **Step 2: 상점 카드(ShopHud/Card1~3, 180×250).** 동일 적용하되 Desc는 `{ size: { x: 164, y: 56 }, pos: { x: 0, y: -58 } }` (Price (0,-105)와 1px 간격 — Price·구매로직 불변). guid('shp', 100+i*10+dOrder).
|
||||||
|
|
||||||
|
- [ ] **Step 3: 그리드 카드(DeckInspectHud/Grid/Card{n}·DeckAllHud/Grid/Card{n}, 158×214).** 두 빌더(line≈661, 783)에 비례 축소 프레임: Art 84×84@(0,44) / NamePlate 148×30@(0,-8) / CostPlate 38×38@(-58,86); 텍스트 cfg: Cost 38×38@(-58,86) f22 / Name 148×26@(0,-8) f17 / Desc 144×60@(0,-54) f15. guid는 각 빌더의 기존 네임스페이스 시퀀스 이어쓰기(중복 검증으로 확인).
|
||||||
|
|
||||||
|
- [ ] **Step 4: 검증.** 실행 후:
|
||||||
|
`node -e "const ui=JSON.parse(require('fs').readFileSync('ui/DefaultGroup.ui','utf8'));const E=ui.ContentProto.Entities;const cnt=s=>E.filter(e=>e.path.includes(s)&&e.path.endsWith('/Art')).length;console.log('reward:',cnt('/RewardHud/Reward'),'shop:',cnt('/ShopHud/Card'),'inspect:',cnt('/DeckInspectHud/Grid/'),'alldeck:',cnt('/DeckAllHud/Grid/'));const ids=E.map(e=>e.id);console.log('dup:',ids.filter((x,i)=>ids.indexOf(x)!==i).length)"`
|
||||||
|
Expected: reward:3 shop:3 inspect:(그리드 수) alldeck:(그리드 수), dup:0. 산출물 복원.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit:** `git add tools/deck/gen-slaydeck.mjs && git commit -m "feat(card-visuals): 보상·상점·그리드 카드 프레임 적용"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: 재생성·검증·산출물 커밋
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `node tools/deck/gen-slaydeck.mjs` → exit 0.
|
||||||
|
- [ ] **Step 2:** JSON 3종 파스 + dup 0 + 손패/보상/상점/그리드 Art 존재 + codeblock에 ApplyCardFace·image 직렬화(`image = `)·새 카드명("파워 스트라이크") 포함 확인:
|
||||||
|
`node -e "const fs=require('fs');for(const f of ['ui/DefaultGroup.ui','Global/common.gamelogic','RootDesk/MyDesk/SlayDeckController.codeblock'])JSON.parse(fs.readFileSync(f,'utf8'));const cb=fs.readFileSync('RootDesk/MyDesk/SlayDeckController.codeblock','utf8');console.log('face+image+name:',cb.includes('ApplyCardFace')&&cb.includes('image = ')&&cb.includes('파워 스트라이크'))"` → true.
|
||||||
|
- [ ] **Step 3:** 결정성(재실행 빈 diff) + sim 14/14.
|
||||||
|
- [ ] **Step 4:** Commit: `git add ui/DefaultGroup.ui Global/common.gamelogic RootDesk/MyDesk/SlayDeckController.codeblock && git commit -m "feat(card-visuals): 산출물 재생성"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6 (컨트롤러 직접): 메이커 플레이테스트 + 푸시 + PR
|
||||||
|
|
||||||
|
- refresh → build 0 → play. 4표면 스크린샷: ①손패(전투, 스킬 이미지+프레임+새 이름) ②보상 ③상점 ④모든덱/인스펙터. 이미지 미적용/프레임 깨짐/그리드 셀 영향 발견 시 좌표·RUID 수정 → 재생성 → 재확인.
|
||||||
|
- 통과 후: `git push -u origin feature/p2-card-visuals`(인증 실패 시 1회 재시도) → PR 링크+메시지 제공.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review 결과
|
||||||
|
- **스펙 커버리지**: §2→T1, §3→T1, §4→T3·T4, §5→T2, §6→T1~T5, §8→T5·T6. 전부 매핑.
|
||||||
|
- **플레이스홀더**: T1의 `<선별RUID>`는 수확 절차의 산출물로 정의됨(절차 명시). 그 외 실제 코드.
|
||||||
|
- **일관성**: `ApplyCardFace(base, cardId)` 시그니처·자식명(Art/NamePlate/CostPlate)·displayOrder 규칙(플레이트3/4·Art5·텍스트6/7/8) Task 간 일치. guid 충돌은 각 Task 검증(dup 0)으로 강제.
|
||||||
|
- **주의**: 손패 byPath 갱신 분기·RenderShop/ApplyAllDeckCardVisual 본문은 구현 시 현재 코드를 읽고 지정 부분만 치환(앵커 명시됨).
|
||||||
77
docs/superpowers/specs/2026-06-11-card-visuals-design.md
Normal file
77
docs/superpowers/specs/2026-06-11-card-visuals-design.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# 메이플 스킬 카드 비주얼 (P2) — 설계
|
||||||
|
|
||||||
|
- 날짜: 2026-06-11
|
||||||
|
- 대상: `data/cards.json`, `tools/deck/gen-slaydeck.mjs`(카드 엔티티 구조·ApplyCardVisual류), 생성물
|
||||||
|
- 상태: 승인됨 (배포 퀄리티 로드맵 P2/5 — P1 UI 정비 머지됨 #34)
|
||||||
|
|
||||||
|
## 1. 배경 / 타당성 (검증 완료)
|
||||||
|
|
||||||
|
카드가 단색 사각형+텍스트(코스트/이름/설명)뿐이라 게임 정체성이 약하다. 메이플 스킬 이미지를 카드에 넣는다.
|
||||||
|
|
||||||
|
**타당성 검증 완료(메이커 실측)**: `asset_search_resources`(source=maplestory, cat=sprite)로 "파워 스트라이크" 검색 → 공식 RUID 10+개. 그중 `37ed94ffd1a64a22ad91a6ae14774718`를 Play 중 `SpriteGUIRendererComponent.ImageRUID`(Type=0)에 주입 → **로컬 워크스페이스에서 정상 렌더 확인**(흰 박스 아님 — 흰 박스 문제는 클라우드 '계정' 리소스에만 해당, 공식 리소스는 OK).
|
||||||
|
주의: 검색 결과는 아이콘이 아니라 **스킬 이펙트 컷**일 수 있음 → 후보 중 선별 단계 필요.
|
||||||
|
|
||||||
|
## 2. 카드 → 메이플 스킬 매핑 (데이터)
|
||||||
|
|
||||||
|
`data/cards.json`의 각 카드에 선택 필드 추가:
|
||||||
|
- `image`: 공식 스프라이트 RUID(string). 없으면 현행 단색 폴백.
|
||||||
|
- 카드 `name`을 메이플 스킬명으로 변경(효과·코스트·밸런스 불변):
|
||||||
|
|
||||||
|
| 카드 id | 기존 이름 | 새 이름(전사 스킬) | 효과 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Strike | 타격 | 파워 스트라이크 | 피해 6 |
|
||||||
|
| Bash | 강타 | 슬래시 블러스트 | 피해 10 |
|
||||||
|
| Defend | 방어 | 아이언 바디 | 방어도 5 |
|
||||||
|
|
||||||
|
(id는 기존 유지 — RunDeck/starterDeck 호환. 표시명만 변경.)
|
||||||
|
|
||||||
|
## 3. RUID 수확 워크플로 (구현 선행 태스크)
|
||||||
|
|
||||||
|
1. `asset_search_resources`(source=maplestory)로 스킬별 후보 수집: "파워 스트라이크", "슬래시 블러스트", "아이언 바디" (+필요시 "워리어", "스킬" 등 보조 질의).
|
||||||
|
2. 메이커 Play에서 후보 RUID를 카드 Art에 순회 주입 + 스크린샷으로 선별(§1에서 검증한 방법). 카드 일러스트로 보기 좋은 컷 1개/스킬 확정.
|
||||||
|
3. 확정 RUID를 `data/cards.json`에 기록. (RUID 문자열만 저장 — 공식 콘텐츠 정책 기존 관행과 동일)
|
||||||
|
4. 적합 컷이 없으면 폴백: 그 카드들은 이펙트 컷 중 베스트, 그래도 없으면 image 필드 생략(단색 유지).
|
||||||
|
|
||||||
|
## 4. 카드 프레임 구조 (upsertUi — 손패 Card1~5 기준)
|
||||||
|
|
||||||
|
기존: 루트(단색 패널) + Cost/Name/Desc 텍스트 3개.
|
||||||
|
변경(카드 180×250 기준):
|
||||||
|
```
|
||||||
|
Card{i} (루트: 종류색 패널 — 테두리 역할, 기존 ATTACK/DEFEND/SKILL 색 유지)
|
||||||
|
├─ Art 96×96 중앙상단(pos 0, 52): ImageRUID, Type=0, 흰색
|
||||||
|
├─ NamePlate 168×34 (pos 0, -8): 어두운 띠 {0.07,0.08,0.1,0.92}
|
||||||
|
│ └─ (Name 텍스트를 NamePlate 위치로 이동, fontSize 20)
|
||||||
|
├─ Cost 44×44 좌상(pos -68, 103): 어두운 원판 패널 + 숫자 26 (기존 위치 강조형)
|
||||||
|
└─ Desc (pos 0, -62, fontSize 18) 하단 효과 텍스트
|
||||||
|
```
|
||||||
|
- 구현은 기존 자식 텍스트(Cost/Name/Desc) 위치·크기 조정 + 신규 Art/NamePlate/CostPlate 스프라이트 추가.
|
||||||
|
- 동일 구조를 **RewardHud/Reward{1~3}**, **ShopHud/Card{1~3}**, **DeckInspectHud/Grid/Card{n}**, **DeckAllHud/Grid/Card{n}** 카드에도 적용(폭이 다른 그리드 카드(158×214)는 비례 축소 좌표).
|
||||||
|
|
||||||
|
## 5. 런타임 렌더 일원화
|
||||||
|
|
||||||
|
- 신규 헬퍼 `ApplyCardFace(basePath, cardId)`(Lua): Cards[cardId]에서 name/cost/desc/kind/image를 읽어 — 루트 색(kind), Art ImageRUID(있으면 표시·없으면 Art 숨김), Name/Cost/Desc 텍스트 설정.
|
||||||
|
- 기존 `ApplyCardVisual`(손패)·`ApplyRewardVisual`(보상)·상점 렌더(`RenderShop` 내 카드부)·`ApplyInspectCardVisual`(인스펙터)·모든덱 렌더가 **ApplyCardFace를 호출**하도록 통일(경로만 다름).
|
||||||
|
- Lua 카드 테이블(`luaCardsTable`)에 `image` 필드 직렬화 추가(없으면 생략).
|
||||||
|
|
||||||
|
## 6. 변경 파일
|
||||||
|
|
||||||
|
| 파일 | 변경 |
|
||||||
|
|---|---|
|
||||||
|
| `data/cards.json` | name 3종 변경 + image RUID 3종 추가 |
|
||||||
|
| `tools/deck/gen-slaydeck.mjs` | luaCardsTable image, 카드 엔티티 프레임(5표면), ApplyCardFace + 호출부 통일 |
|
||||||
|
| 생성물 | `ui/DefaultGroup.ui`·`SlayDeckController.codeblock` 재생성 |
|
||||||
|
| `tools/balance/sim-balance.mjs` | 불변(이름 표시는 data에서 읽음 — 리포트에 새 이름 자동 반영) |
|
||||||
|
|
||||||
|
## 7. 범위 제외 (후속)
|
||||||
|
|
||||||
|
의도 아이콘(P3), 카드 호버 확대·사용 연출(P3), 희귀도 프레임 색(P5), 도적/마법사 카드(후속 콘텐츠).
|
||||||
|
|
||||||
|
## 8. 검증
|
||||||
|
|
||||||
|
- 생성 결정성·dup id 0·sim 14/14(이름 변경이 테스트에 영향 없는지 확인 — 테스트는 자체 fixture 사용이라 무관).
|
||||||
|
- 메이커: 손패/보상/상점/덱보기 4표면 스크린샷 — 스킬 이미지·프레임·이름 일관 표시, image 누락 카드 폴백 정상.
|
||||||
|
|
||||||
|
## 9. 리스크
|
||||||
|
|
||||||
|
- 후보 RUID가 멀티프레임 애니 시트일 수 있음 → SpriteGUIRenderer가 첫 프레임 표시(기존 옵션 FrameColumn/Row 1) — 선별 단계에서 확인.
|
||||||
|
- 그리드 카드(인스펙터/모든덱)는 ScrollLayoutGroup 셀 — 자식 추가가 셀 레이아웃에 영향 없는지 메이커 확인.
|
||||||
@@ -65,6 +65,7 @@ function luaCardsTable(cards) {
|
|||||||
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
|
||||||
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
if (c.damage != null) fields.push(`damage = ${c.damage}`);
|
||||||
if (c.block != null) fields.push(`block = ${c.block}`);
|
if (c.block != null) fields.push(`block = ${c.block}`);
|
||||||
|
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
|
||||||
return `\t${id} = { ${fields.join(', ')} },`;
|
return `\t${id} = { ${fields.join(', ')} },`;
|
||||||
});
|
});
|
||||||
return `self.Cards = {\n${lines.join('\n')}\n}`;
|
return `self.Cards = {\n${lines.join('\n')}\n}`;
|
||||||
@@ -422,12 +423,13 @@ function upsertUi() {
|
|||||||
card.jsonString.visible = true;
|
card.jsonString.visible = true;
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: cards[i - 1].cost, fontSize: 34, bold: true }],
|
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: cards[i - 1].cost, fontSize: 26, bold: true }],
|
||||||
['Name', { size: { x: 160, y: 50 }, pos: { x: 0, y: 50 }, value: cards[i - 1].name, 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: 160, y: 82 }, pos: { x: 0, y: -80 }, value: cards[i - 1].desc, fontSize: 20, bold: false }],
|
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: cards[i - 1].desc, fontSize: 18, bold: false }],
|
||||||
];
|
];
|
||||||
for (const [suffix, cfg] of children) {
|
for (const [suffix, cfg] of children) {
|
||||||
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||||
|
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||||
let child = byPath.get(path);
|
let child = byPath.get(path);
|
||||||
if (!child) {
|
if (!child) {
|
||||||
child = entity({
|
child = entity({
|
||||||
@@ -436,7 +438,7 @@ function upsertUi() {
|
|||||||
modelId: 'uitext',
|
modelId: 'uitext',
|
||||||
entryId: 'UIText',
|
entryId: 'UIText',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
|
displayOrder: dOrder,
|
||||||
components: [
|
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 }),
|
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 }),
|
sprite({ color: TRANSPARENT }),
|
||||||
@@ -449,11 +451,45 @@ function upsertUi() {
|
|||||||
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
|
child.id = guid('dck', i * 10 + children.findIndex(([s]) => s === suffix));
|
||||||
child.jsonString.enable = true;
|
child.jsonString.enable = true;
|
||||||
child.jsonString.visible = true;
|
child.jsonString.visible = true;
|
||||||
|
child.jsonString.displayOrder = dOrder;
|
||||||
|
const ctr = child.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent');
|
||||||
|
if (ctr) {
|
||||||
|
const pivot = { x: 0.5, y: 0.5 };
|
||||||
|
ctr.RectSize = cfg.size;
|
||||||
|
ctr.anchoredPosition = cfg.pos;
|
||||||
|
ctr.OffsetMin = { x: cfg.pos.x - pivot.x * cfg.size.x, y: cfg.pos.y - pivot.y * cfg.size.y };
|
||||||
|
ctr.OffsetMax = { x: cfg.pos.x + (1 - pivot.x) * cfg.size.x, y: cfg.pos.y + (1 - pivot.y) * cfg.size.y };
|
||||||
|
}
|
||||||
child.jsonString['@components'][2].Text = cfg.value;
|
child.jsonString['@components'][2].Text = cfg.value;
|
||||||
child.jsonString['@components'][2].FontSize = cfg.fontSize;
|
child.jsonString['@components'][2].FontSize = cfg.fontSize;
|
||||||
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
|
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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],
|
||||||
|
];
|
||||||
|
for (const [suffix, dOrder, cfg, color, spriteType] of frameKids) {
|
||||||
|
const fPath = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
|
||||||
|
if (!byPath.get(fPath)) {
|
||||||
|
const fe = entity({
|
||||||
|
id: guid('dck', 200 + i * 10 + dOrder),
|
||||||
|
path: fPath,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
ui.ContentProto.Entities.push(fe);
|
||||||
|
byPath.set(fPath, fe);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hud = [];
|
const hud = [];
|
||||||
@@ -655,8 +691,8 @@ function upsertUi() {
|
|||||||
}));
|
}));
|
||||||
let insN = 6;
|
let insN = 6;
|
||||||
const INSPECT_CARD_COUNT = 60;
|
const INSPECT_CARD_COUNT = 60;
|
||||||
const INSPECT_CARD_W = 142;
|
const INSPECT_CARD_W = 158;
|
||||||
const INSPECT_CARD_H = 198;
|
const INSPECT_CARD_H = 214;
|
||||||
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
|
||||||
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
|
||||||
const card = entity({
|
const card = entity({
|
||||||
@@ -674,17 +710,18 @@ function upsertUi() {
|
|||||||
card.jsonString.enable = false;
|
card.jsonString.enable = false;
|
||||||
inspect.push(card);
|
inspect.push(card);
|
||||||
for (const [suffix, cfg] of [
|
for (const [suffix, cfg] of [
|
||||||
['Cost', { size: { x: 42, y: 38 }, pos: { x: -46, y: 73 }, value: '1', fontSize: 25, bold: true }],
|
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
|
||||||
['Name', { size: { x: 126, y: 42 }, pos: { x: 0, y: 34 }, value: '', fontSize: 21, bold: true }],
|
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
|
||||||
['Desc', { size: { x: 126, y: 70 }, pos: { x: 0, y: -58 }, value: '', fontSize: 16, bold: false }],
|
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
|
||||||
]) {
|
]) {
|
||||||
|
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||||
inspect.push(entity({
|
inspect.push(entity({
|
||||||
id: guid('ins', insN++),
|
id: guid('ins', insN++),
|
||||||
path: `${cardPath}/${suffix}`,
|
path: `${cardPath}/${suffix}`,
|
||||||
modelId: 'uitext',
|
modelId: 'uitext',
|
||||||
entryId: 'UIText',
|
entryId: 'UIText',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
|
displayOrder: dOrder,
|
||||||
components: [
|
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 }),
|
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 }),
|
sprite({ color: TRANSPARENT }),
|
||||||
@@ -692,6 +729,24 @@ function upsertUi() {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
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}`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit('DeckInspectHud', inspect);
|
emit('DeckInspectHud', inspect);
|
||||||
|
|
||||||
@@ -777,8 +832,8 @@ function upsertUi() {
|
|||||||
}));
|
}));
|
||||||
let allN = 6;
|
let allN = 6;
|
||||||
const ALL_DECK_CARD_COUNT = 120;
|
const ALL_DECK_CARD_COUNT = 120;
|
||||||
const ALL_DECK_CARD_W = 142;
|
const ALL_DECK_CARD_W = 158;
|
||||||
const ALL_DECK_CARD_H = 198;
|
const ALL_DECK_CARD_H = 214;
|
||||||
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
|
||||||
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
|
||||||
const card = entity({
|
const card = entity({
|
||||||
@@ -796,17 +851,18 @@ function upsertUi() {
|
|||||||
card.jsonString.enable = false;
|
card.jsonString.enable = false;
|
||||||
allDeck.push(card);
|
allDeck.push(card);
|
||||||
for (const [suffix, cfg] of [
|
for (const [suffix, cfg] of [
|
||||||
['Cost', { size: { x: 42, y: 38 }, pos: { x: -46, y: 73 }, value: '1', fontSize: 25, bold: true }],
|
['Cost', { size: { x: 38, y: 38 }, pos: { x: -58, y: 86 }, value: '1', fontSize: 22, bold: true }],
|
||||||
['Name', { size: { x: 126, y: 42 }, pos: { x: 0, y: 34 }, value: '', fontSize: 21, bold: true }],
|
['Name', { size: { x: 148, y: 26 }, pos: { x: 0, y: -8 }, value: '', fontSize: 17, bold: true }],
|
||||||
['Desc', { size: { x: 126, y: 70 }, pos: { x: 0, y: -58 }, value: '', fontSize: 16, bold: false }],
|
['Desc', { size: { x: 144, y: 60 }, pos: { x: 0, y: -54 }, value: '', fontSize: 15, bold: false }],
|
||||||
]) {
|
]) {
|
||||||
|
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||||
allDeck.push(entity({
|
allDeck.push(entity({
|
||||||
id: guid('all', allN++),
|
id: guid('all', allN++),
|
||||||
path: `${cardPath}/${suffix}`,
|
path: `${cardPath}/${suffix}`,
|
||||||
modelId: 'uitext',
|
modelId: 'uitext',
|
||||||
entryId: 'UIText',
|
entryId: 'UIText',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
|
displayOrder: dOrder,
|
||||||
components: [
|
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 }),
|
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 }),
|
sprite({ color: TRANSPARENT }),
|
||||||
@@ -814,6 +870,24 @@ function upsertUi() {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
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}`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit('DeckAllHud', allDeck);
|
emit('DeckAllHud', allDeck);
|
||||||
|
|
||||||
@@ -1083,17 +1157,18 @@ function upsertUi() {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
for (const [suffix, cfg] of [
|
for (const [suffix, cfg] of [
|
||||||
['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: '1', fontSize: 34, bold: true }],
|
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true }],
|
||||||
['Name', { size: { x: 160, y: 50 }, pos: { x: 0, y: 50 }, value: '카드', fontSize: 26, bold: true }],
|
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true }],
|
||||||
['Desc', { size: { x: 160, y: 82 }, pos: { x: 0, y: -80 }, value: '', fontSize: 20, bold: false }],
|
['Desc', { size: { x: 164, y: 70 }, pos: { x: 0, y: -62 }, value: '', fontSize: 18, bold: false }],
|
||||||
]) {
|
]) {
|
||||||
|
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
|
||||||
reward.push(entity({
|
reward.push(entity({
|
||||||
id: guid('rwd', rwdN++),
|
id: guid('rwd', rwdN++),
|
||||||
path: `${cardPath}/${suffix}`,
|
path: `${cardPath}/${suffix}`,
|
||||||
modelId: 'uitext',
|
modelId: 'uitext',
|
||||||
entryId: 'UIText',
|
entryId: 'UIText',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
|
displayOrder: dOrder,
|
||||||
components: [
|
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 }),
|
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 }),
|
sprite({ color: TRANSPARENT }),
|
||||||
@@ -1101,6 +1176,24 @@ 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],
|
||||||
|
]) {
|
||||||
|
reward.push(entity({
|
||||||
|
id: guid('rwd', rwdN++),
|
||||||
|
path: `${cardPath}/${suffix}`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reward.push(entity({
|
reward.push(entity({
|
||||||
id: guid('rwd', rwdN++),
|
id: guid('rwd', rwdN++),
|
||||||
@@ -1239,18 +1332,19 @@ function upsertUi() {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
for (const [suffix, cfg] of [
|
for (const [suffix, cfg] of [
|
||||||
['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: '1', fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
['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: 160, y: 50 }, pos: { x: 0, y: 50 }, value: '카드', 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: 160, y: 60 }, pos: { x: 0, y: -50 }, value: '', fontSize: 20, bold: false, 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 } }],
|
['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 dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
|
||||||
shop.push(entity({
|
shop.push(entity({
|
||||||
id: guid('shp', shpN++),
|
id: guid('shp', shpN++),
|
||||||
path: `${cardPath}/${suffix}`,
|
path: `${cardPath}/${suffix}`,
|
||||||
modelId: 'uitext',
|
modelId: 'uitext',
|
||||||
entryId: 'UIText',
|
entryId: 'UIText',
|
||||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||||
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : suffix === 'Desc' ? 2 : 3,
|
displayOrder: dOrder,
|
||||||
components: [
|
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 }),
|
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 }),
|
sprite({ color: TRANSPARENT }),
|
||||||
@@ -1258,6 +1352,24 @@ 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}`,
|
||||||
|
modelId: 'uisprite',
|
||||||
|
entryId: 'UISprite',
|
||||||
|
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||||
|
displayOrder: dOrder,
|
||||||
|
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 }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
shop.push(entity({
|
shop.push(entity({
|
||||||
id: guid('shp', shpN++),
|
id: guid('shp', shpN++),
|
||||||
@@ -2111,24 +2223,7 @@ end`, [
|
|||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pile' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'title' },
|
||||||
]),
|
]),
|
||||||
method('ApplyInspectCardVisual', `local c = self.Cards[cardId]
|
method('ApplyInspectCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||||
if c == nil then
|
|
||||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
|
||||||
end
|
|
||||||
local base = "/ui/DefaultGroup/DeckInspectHud/Grid/Card" .. tostring(slot)
|
|
||||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
|
||||||
self:SetText(base .. "/Name", c.name)
|
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
|
||||||
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 == "Skill" then
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
|
||||||
else
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
|
||||||
end
|
|
||||||
end`, [
|
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
@@ -2167,25 +2262,7 @@ for i = 1, 120 do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end`),
|
end`),
|
||||||
method('ApplyAllDeckCardVisual', `local cards = self.Cards or {}
|
method('ApplyAllDeckCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot), cardId)`, [
|
||||||
local c = cards[cardId]
|
|
||||||
if c == nil then
|
|
||||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
|
||||||
end
|
|
||||||
local base = "/ui/DefaultGroup/DeckAllHud/Grid/Card" .. tostring(slot)
|
|
||||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
|
||||||
self:SetText(base .. "/Name", c.name)
|
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
|
||||||
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 == "Skill" then
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
|
||||||
else
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
|
||||||
end
|
|
||||||
end`, [
|
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
@@ -2206,23 +2283,38 @@ for i = 1, 5 do
|
|||||||
\tend
|
\tend
|
||||||
end
|
end
|
||||||
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]),
|
||||||
method('ApplyCardVisual', `local c = self.Cards[cardId]
|
method('ApplyCardFace', `local c = self.Cards[cardId]
|
||||||
if c == nil then
|
if c == nil then
|
||||||
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
c = { name = cardId, cost = 0, desc = "", kind = "Skill" }
|
||||||
end
|
end
|
||||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Cost", tostring(c.cost))
|
local e = _EntityService:GetEntityByPath(base)
|
||||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Name", c.name)
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||||
self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Desc", c.desc)
|
|
||||||
local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
|
|
||||||
if cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then
|
|
||||||
if c.kind == "Attack" then
|
if c.kind == "Attack" then
|
||||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
||||||
elseif c.kind == "Skill" then
|
elseif c.kind == "Skill" then
|
||||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
||||||
else
|
else
|
||||||
cardEntity.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||||
|
self:SetText(base .. "/Name", c.name)
|
||||||
|
self:SetText(base .. "/Desc", c.desc)
|
||||||
|
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||||
|
if art ~= nil then
|
||||||
|
if c.image ~= nil and c.image ~= "" then
|
||||||
|
art.Enable = true
|
||||||
|
if art.SpriteGUIRendererComponent ~= nil then
|
||||||
|
art.SpriteGUIRendererComponent.ImageRUID = c.image
|
||||||
|
end
|
||||||
|
else
|
||||||
|
art.Enable = false
|
||||||
end
|
end
|
||||||
end`, [
|
end`, [
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' },
|
||||||
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
|
]),
|
||||||
|
method('ApplyCardVisual', `self:ApplyCardFace("/ui/DefaultGroup/CardHand/Card" .. tostring(slot), cardId)`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
@@ -2473,24 +2565,7 @@ local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud")
|
|||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = true
|
hud.Enable = true
|
||||||
end`),
|
end`),
|
||||||
method('ApplyRewardVisual', `local c = self.Cards[cardId]
|
method('ApplyRewardVisual', `self:ApplyCardFace("/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot), cardId)`, [
|
||||||
if c == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local base = "/ui/DefaultGroup/RewardHud/Reward" .. tostring(slot)
|
|
||||||
self:SetText(base .. "/Name", c.name)
|
|
||||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
|
||||||
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 == "Skill" then
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
|
||||||
else
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
|
||||||
end
|
|
||||||
end`, [
|
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
@@ -2623,20 +2698,12 @@ for i = 1, 3 do
|
|||||||
local c = self.Cards[cid]
|
local c = self.Cards[cid]
|
||||||
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||||||
if c ~= nil then
|
if c ~= nil then
|
||||||
self:SetText(base .. "/Name", c.name)
|
self:ApplyCardFace(base, cid)
|
||||||
self:SetText(base .. "/Cost", tostring(c.cost))
|
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
|
||||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 골드")
|
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 골드")
|
||||||
local e = _EntityService:GetEntityByPath(base)
|
local e = _EntityService:GetEntityByPath(base)
|
||||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||||
if self.ShopBought[i] == true then
|
if self.ShopBought[i] == true then
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
|
||||||
elseif c.kind == "Attack" then
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
|
|
||||||
elseif c.kind == "Skill" then
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
|
|
||||||
else
|
|
||||||
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
104585
ui/DefaultGroup.ui
104585
ui/DefaultGroup.ui
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user