carddeck(B): 카드 전투 통합 — 적/플레이어 전투 상태·의도·승패

PlayCard가 토스트 대신 실제 효과를 적용하도록 통합.

- 카드 데이터에 damage/block 수치 필드 추가 (desc 파싱 폐기)
- 전투 상태: 플레이어 HP/Block, 적 HP/Block/의도(결정적 사이클)
- PlayCard: Attack→적 HP 감소(방어 우선 차감), Skill→플레이어 Block 증가
- EndPlayerTurn→적 턴(의도 실행)→다음 플레이어 턴, 승패 판정
- CombatHud UI: 적 패널(이름/HP/방어/의도)·플레이어 패널(HP/방어)·결과 텍스트
- 수치(플레이어80/적45/의도10·6·방8)는 임시 placeholder (D에서 캐릭터/몬스터별 외부화)

생성기 단일 소스(tools/gen-slaydeck.mjs)에서 생성. 결정적 출력 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 00:04:26 +09:00
parent 8f08e67e4c
commit bd02865f4f
3 changed files with 2144 additions and 14 deletions

View File

@@ -91,6 +91,76 @@
"SyncDirection": 0,
"Attributes": [],
"Name": "Cards"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "PlayerHp"
},
{
"Type": "number",
"DefaultValue": "80",
"SyncDirection": 0,
"Attributes": [],
"Name": "PlayerMaxHp"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "PlayerBlock"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyHp"
},
{
"Type": "number",
"DefaultValue": "45",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyMaxHp"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyBlock"
},
{
"Type": "number",
"DefaultValue": "1",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyIntentIndex"
},
{
"Type": "boolean",
"DefaultValue": "false",
"SyncDirection": 0,
"Attributes": [],
"Name": "CombatOver"
},
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyIntents"
},
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "EnemyName"
}
],
"Methods": [
@@ -118,7 +188,7 @@
"Name": null
},
"Arguments": [],
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nself.DiscardPile = {}\nself.Hand = {}\nself.Cards = {\n\tStrike = { name = \"타격\", cost = 1, desc = \"피해 6\", kind = \"Attack\" },\n\tDefend = { name = \"방어\", cost = 1, desc = \"방어도 5\", kind = \"Skill\" },\n\tBash = { name = \"강타\", cost = 2, desc = \"피해 10\", kind = \"Attack\" },\n}\nself.DrawPile = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself:Shuffle(self.DrawPile)\nself:BindButtons()\nself:StartPlayerTurn()",
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nself.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.PlayerBlock = 0\nself.EnemyName = \"슬라임\"\nself.EnemyMaxHp = 45\nself.EnemyHp = self.EnemyMaxHp\nself.EnemyBlock = 0\nself.EnemyIntents = {\n\t{ kind = \"Attack\", value = 10 },\n\t{ kind = \"Attack\", value = 6 },\n\t{ kind = \"Defend\", value = 8 },\n}\nself.EnemyIntentIndex = 1\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 = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself:Shuffle(self.DrawPile)\nself:BindButtons()\nself:RenderCombat()\nself:StartPlayerTurn()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
@@ -171,7 +241,7 @@
"Name": null
},
"Arguments": [],
"Code": "self.Turn = self.Turn + 1\nself.Energy = self.MaxEnergy\nself:DrawCards(5)\nself:RenderHand(true)",
"Code": "self.Turn = self.Turn + 1\nself.Energy = self.MaxEnergy\nself.PlayerBlock = 0\nself:DrawCards(5)\nself:RenderHand(true)\nself:RenderCombat()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
@@ -186,7 +256,7 @@
"Name": null
},
"Arguments": [],
"Code": "for i = 1, #self.Hand do\n\ttable.insert(self.DiscardPile, self.Hand[i])\nend\nself.Hand = {}\nself:RenderHand(false)\nself:RenderPiles()\n_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)",
"Code": "if self.CombatOver == true then\n\treturn\nend\nfor i = 1, #self.Hand do\n\ttable.insert(self.DiscardPile, self.Hand[i])\nend\nself.Hand = {}\nself:RenderHand(false)\nself:RenderPiles()\nself:EnemyTurn()\nself:CheckCombatEnd()\nif self.CombatOver == true then\n\treturn\nend\n_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
@@ -389,7 +459,7 @@
"Name": "slot"
}
],
"Code": "if self.Hand == nil then\n\treturn\nend\nlocal cardId = self.Hand[slot]\nif cardId == nil then\n\treturn\nend\nlocal c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nif self.Energy < c.cost then\n\tself:Toast(\"에너지가 부족합니다\")\n\treturn\nend\nself.Energy = self.Energy - c.cost\nself:Toast(c.name .. \" — \" .. c.desc)\ntable.remove(self.Hand, slot)\ntable.insert(self.DiscardPile, cardId)\nself:RenderHand(false)\nself:RenderPiles()",
"Code": "if self.CombatOver == true then\n\treturn\nend\nif self.Hand == nil then\n\treturn\nend\nlocal cardId = self.Hand[slot]\nif cardId == nil then\n\treturn\nend\nlocal c = self.Cards[cardId]\nif c == nil then\n\treturn\nend\nif self.Energy < c.cost then\n\tself:Toast(\"에너지가 부족합니다\")\n\treturn\nend\nself.Energy = self.Energy - c.cost\nif c.kind == \"Attack\" then\n\tif c.damage ~= nil then\n\t\tself:DealDamageToEnemy(c.damage)\n\tend\nelseif c.kind == \"Skill\" then\n\tif c.block ~= nil then\n\t\tself.PlayerBlock = self.PlayerBlock + c.block\n\tend\nend\ntable.remove(self.Hand, slot)\ntable.insert(self.DiscardPile, cardId)\nself:RenderHand(false)\nself:RenderPiles()\nself:RenderCombat()\nself:CheckCombatEnd()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
@@ -417,6 +487,120 @@
"ExecSpace": 6,
"Attributes": [],
"Name": "Toast"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "amount"
}
],
"Code": "local dmg = amount\nif self.EnemyBlock > 0 then\n\tlocal absorbed = math.min(self.EnemyBlock, dmg)\n\tself.EnemyBlock = self.EnemyBlock - absorbed\n\tdmg = dmg - absorbed\nend\nself.EnemyHp = self.EnemyHp - dmg\nif self.EnemyHp < 0 then\n\tself.EnemyHp = 0\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "DealDamageToEnemy"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "amount"
}
],
"Code": "local dmg = amount\nif self.PlayerBlock > 0 then\n\tlocal absorbed = math.min(self.PlayerBlock, dmg)\n\tself.PlayerBlock = self.PlayerBlock - absorbed\n\tdmg = dmg - absorbed\nend\nself.PlayerHp = self.PlayerHp - dmg\nif self.PlayerHp < 0 then\n\tself.PlayerHp = 0\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "DealDamageToPlayer"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.EnemyBlock = 0\nlocal intent = self.EnemyIntents[self.EnemyIntentIndex]\nif intent ~= nil then\n\tif intent.kind == \"Attack\" then\n\t\tself:DealDamageToPlayer(intent.value)\n\telseif intent.kind == \"Defend\" then\n\t\tself.EnemyBlock = self.EnemyBlock + intent.value\n\tend\nend\nself.EnemyIntentIndex = self.EnemyIntentIndex + 1\nif self.EnemyIntentIndex > #self.EnemyIntents then\n\tself.EnemyIntentIndex = 1\nend\nself:RenderCombat()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "EnemyTurn"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "if self.EnemyHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"승리!\")\n\t-- TODO(E): 전투 보상 훅 — 카드 보상/골드/유물 선택 진입점\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"패배...\")\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "CheckCombatEnd"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "string",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "text"
}
],
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/Result\", text)\nlocal entity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CombatHud/Result\")\nif entity ~= nil then\n\tentity.Enable = true\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "ShowResult"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/EnemyName\", self.EnemyName)\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyHp\", \"HP \" .. tostring(self.EnemyHp) .. \"/\" .. tostring(self.EnemyMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyBlock\", \"방어 \" .. tostring(self.EnemyBlock))\nlocal intent = self.EnemyIntents[self.EnemyIntentIndex]\nlocal intentText = \"\"\nif intent ~= nil then\n\tif intent.kind == \"Attack\" then\n\t\tintentText = \"의도: 공격 \" .. tostring(intent.value)\n\telseif intent.kind == \"Defend\" then\n\t\tintentText = \"의도: 방어 \" .. tostring(intent.value)\n\tend\nend\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyIntent\", intentText)\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerHp\", \"HP \" .. tostring(self.PlayerHp) .. \"/\" .. tostring(self.PlayerMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerBlock\", \"방어 \" .. tostring(self.PlayerBlock))",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "RenderCombat"
}
],
"EntityEventHandlers": []

View File

@@ -20,7 +20,7 @@ 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 : 0xfe;
const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : 0xfe;
const v = (ns * 0x100000 + n) >>> 0;
return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`;
}
@@ -172,7 +172,7 @@ function entity({ id, path, modelId, entryId, componentNames, components, displa
function upsertUi() {
const ui = JSON.parse(readFileSync(UI_FILE, 'utf8'));
const E = ui.ContentProto.Entities;
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud'));
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud'));
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
@@ -332,6 +332,103 @@ function upsertUi() {
}));
ui.ContentProto.Entities.push(...hud);
const PANEL_BG = { r: 0.08, g: 0.09, b: 0.11, a: 0.78 };
const combat = [];
combat.push(entity({
id: guid('cmb', 0),
path: '/ui/DefaultGroup/CombatHud',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 4,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }),
sprite({ color: TRANSPARENT }),
],
}));
combat.push(entity({
id: guid('cmb', 1),
path: '/ui/DefaultGroup/CombatHud/EnemyBg',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 0,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 170 }, pos: { x: 0, y: 300 }, align: ALIGN_CENTER }),
sprite({ color: PANEL_BG, type: 1 }),
],
}));
const enemyTexts = [
['EnemyName', { x: 0, y: 58 }, { x: 360, y: 44 }, '슬라임', 28, true, GOLD],
['EnemyHp', { x: 0, y: 16 }, { x: 360, y: 40 }, 'HP 45/45', 24, true, { r: 1, g: 1, b: 1, a: 1 }],
['EnemyBlock', { x: 0, y: -20 }, { x: 360, y: 36 }, '방어 0', 20, false, { r: 0.6, g: 0.8, b: 1, a: 1 }],
['EnemyIntent', { x: 0, y: -56 }, { x: 360, y: 38 }, '의도: 공격 10', 22, true, { r: 1, g: 0.72, b: 0.5, a: 1 }],
];
let cmbN = 2;
for (const [suffix, pos, size, value, fontSize, bold, color] of enemyTexts) {
combat.push(entity({
id: guid('cmb', cmbN++),
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: enemyTexts.findIndex(([s]) => s === suffix) + 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
sprite({ color: TRANSPARENT }),
text({ value, fontSize, bold, color }),
],
}));
}
combat.push(entity({
id: guid('cmb', cmbN++),
path: '/ui/DefaultGroup/CombatHud/PlayerBg',
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 5,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 110 }, pos: { x: -760, y: -260 }, align: ALIGN_CENTER }),
sprite({ color: PANEL_BG, type: 1 }),
],
}));
const playerTexts = [
['PlayerHp', { x: -760, y: -238 }, { x: 280, y: 44 }, 'HP 80/80', 26, true, { r: 1, g: 1, b: 1, a: 1 }],
['PlayerBlock', { x: -760, y: -284 }, { x: 280, y: 38 }, '방어 0', 22, false, { r: 0.6, g: 0.8, b: 1, a: 1 }],
];
for (const [suffix, pos, size, value, fontSize, bold, color] of playerTexts) {
combat.push(entity({
id: guid('cmb', cmbN++),
path: `/ui/DefaultGroup/CombatHud/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 6 + playerTexts.findIndex(([s]) => s === suffix),
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
sprite({ color: TRANSPARENT }),
text({ value, fontSize, bold, color }),
],
}));
}
const result = entity({
id: guid('cmb', cmbN++),
path: '/ui/DefaultGroup/CombatHud/Result',
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 8,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 700, y: 140 }, pos: { x: 0, y: 120 } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: 64, bold: true, color: GOLD, alignment: 4 }),
],
});
result.jsonString.enable = false;
combat.push(result);
ui.ContentProto.Entities.push(...combat);
JSON.parse(JSON.stringify(ui));
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
}
@@ -396,20 +493,45 @@ function writeCodeblocks() {
prop('number', 'TweenEventId', '0'),
prop('any', 'EndTurnHandler'),
prop('any', 'Cards'),
prop('number', 'PlayerHp', '0'),
prop('number', 'PlayerMaxHp', '80'),
prop('number', 'PlayerBlock', '0'),
prop('number', 'EnemyHp', '0'),
prop('number', 'EnemyMaxHp', '45'),
prop('number', 'EnemyBlock', '0'),
prop('number', 'EnemyIntentIndex', '1'),
prop('boolean', 'CombatOver', 'false'),
prop('any', 'EnemyIntents'),
prop('any', 'EnemyName'),
], [
method('OnBeginPlay', `self:StartCombat()`),
method('StartCombat', `self.MaxEnergy = 3
self.Turn = 0
self.PlayerMaxHp = 80
self.PlayerHp = self.PlayerMaxHp
self.PlayerBlock = 0
self.EnemyName = "슬라임"
self.EnemyMaxHp = 45
self.EnemyHp = self.EnemyMaxHp
self.EnemyBlock = 0
self.EnemyIntents = {
{ kind = "Attack", value = 10 },
{ kind = "Attack", value = 6 },
{ kind = "Defend", value = 8 },
}
self.EnemyIntentIndex = 1
self.CombatOver = false
self.DiscardPile = {}
self.Hand = {}
self.Cards = {
Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack" },
Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill" },
Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack" },
Strike = { name = "타격", cost = 1, desc = "피해 6", kind = "Attack", damage = 6 },
Defend = { name = "방어", cost = 1, desc = "방어도 5", kind = "Skill", block = 5 },
Bash = { name = "강타", cost = 2, desc = "피해 10", kind = "Attack", damage = 10 },
}
self.DrawPile = { "Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash" }
self:Shuffle(self.DrawPile)
self:BindButtons()
self:RenderCombat()
self:StartPlayerTurn()`),
method('Shuffle', `if list == nil then
\treturn
@@ -434,14 +556,24 @@ for i = 1, 5 do
end`),
method('StartPlayerTurn', `self.Turn = self.Turn + 1
self.Energy = self.MaxEnergy
self.PlayerBlock = 0
self:DrawCards(5)
self:RenderHand(true)`),
method('EndPlayerTurn', `for i = 1, #self.Hand do
self:RenderHand(true)
self:RenderCombat()`),
method('EndPlayerTurn', `if self.CombatOver == true then
return
end
for i = 1, #self.Hand do
\ttable.insert(self.DiscardPile, self.Hand[i])
end
self.Hand = {}
self:RenderHand(false)
self:RenderPiles()
self:EnemyTurn()
self:CheckCombatEnd()
if self.CombatOver == true then
return
end
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
method('DrawCards', `for i = 1, amount do
\tif #self.DrawPile <= 0 then
@@ -532,7 +664,10 @@ end, 1 / 60)`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
]),
method('PlayCard', `if self.Hand == nil then
method('PlayCard', `if self.CombatOver == true then
return
end
if self.Hand == nil then
return
end
local cardId = self.Hand[slot]
@@ -548,12 +683,84 @@ if self.Energy < c.cost then
return
end
self.Energy = self.Energy - c.cost
self:Toast(c.name .. " — " .. c.desc)
if c.kind == "Attack" then
if c.damage ~= nil then
self:DealDamageToEnemy(c.damage)
end
elseif c.kind == "Skill" then
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
end
table.remove(self.Hand, slot)
table.insert(self.DiscardPile, cardId)
self:RenderHand(false)
self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
method('DealDamageToEnemy', `local dmg = amount
if self.EnemyBlock > 0 then
local absorbed = math.min(self.EnemyBlock, dmg)
self.EnemyBlock = self.EnemyBlock - absorbed
dmg = dmg - absorbed
end
self.EnemyHp = self.EnemyHp - dmg
if self.EnemyHp < 0 then
self.EnemyHp = 0
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('DealDamageToPlayer', `local dmg = amount
if self.PlayerBlock > 0 then
local absorbed = math.min(self.PlayerBlock, dmg)
self.PlayerBlock = self.PlayerBlock - absorbed
dmg = dmg - absorbed
end
self.PlayerHp = self.PlayerHp - dmg
if self.PlayerHp < 0 then
self.PlayerHp = 0
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('EnemyTurn', `self.EnemyBlock = 0
local intent = self.EnemyIntents[self.EnemyIntentIndex]
if intent ~= nil then
if intent.kind == "Attack" then
self:DealDamageToPlayer(intent.value)
elseif intent.kind == "Defend" then
self.EnemyBlock = self.EnemyBlock + intent.value
end
end
self.EnemyIntentIndex = self.EnemyIntentIndex + 1
if self.EnemyIntentIndex > #self.EnemyIntents then
self.EnemyIntentIndex = 1
end
self:RenderCombat()`),
method('CheckCombatEnd', `if self.EnemyHp <= 0 then
self.CombatOver = true
self:ShowResult("승리!")
-- TODO(E): 전투 보상 훅 — 카드 보상/골드/유물 선택 진입점
elseif self.PlayerHp <= 0 then
self.CombatOver = true
self:ShowResult("패배...")
end`),
method('ShowResult', `self:SetText("/ui/DefaultGroup/CombatHud/Result", text)
local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result")
if entity ~= nil then
entity.Enable = true
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]),
method('RenderCombat', `self:SetText("/ui/DefaultGroup/CombatHud/EnemyName", self.EnemyName)
self:SetText("/ui/DefaultGroup/CombatHud/EnemyHp", "HP " .. tostring(self.EnemyHp) .. "/" .. tostring(self.EnemyMaxHp))
self:SetText("/ui/DefaultGroup/CombatHud/EnemyBlock", "방어 " .. tostring(self.EnemyBlock))
local intent = self.EnemyIntents[self.EnemyIntentIndex]
local intentText = ""
if intent ~= nil then
if intent.kind == "Attack" then
intentText = "의도: 공격 " .. tostring(intent.value)
elseif intent.kind == "Defend" then
intentText = "의도: 방어 " .. tostring(intent.value)
end
end
self:SetText("/ui/DefaultGroup/CombatHud/EnemyIntent", intentText)
self:SetText("/ui/DefaultGroup/CombatHud/PlayerHp", "HP " .. tostring(self.PlayerHp) .. "/" .. tostring(self.PlayerMaxHp))
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. tostring(self.PlayerBlock))`),
]);
for (const m of combat.ContentProto.Json.Methods) {
m.ExecSpace = 6;

File diff suppressed because it is too large Load Diff