Merge pull request 'Map monster combat' from feature/map-monster-combat
# Conflicts: # RootDesk/MyDesk/SlayDeckController.codeblock
This commit is contained in:
@@ -32,10 +32,10 @@
|
||||
{
|
||||
"@type": "script.SlayDeckController",
|
||||
"Enable": true,
|
||||
"Energy": 0.0,
|
||||
"MaxEnergy": 3.0,
|
||||
"Turn": 0.0,
|
||||
"TweenEventId": 0.0
|
||||
"Energy": 0,
|
||||
"MaxEnergy": 3,
|
||||
"Turn": 0,
|
||||
"TweenEventId": 0
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
67
RootDesk/MyDesk/CombatMonster.codeblock
Normal file
67
RootDesk/MyDesk/CombatMonster.codeblock
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"Id": "",
|
||||
"GameId": "",
|
||||
"EntryKey": "codeblock://combatmonster",
|
||||
"ContentType": "x-mod/codeblock",
|
||||
"Content": "",
|
||||
"Usage": 0,
|
||||
"UsePublish": 1,
|
||||
"UseService": 0,
|
||||
"CoreVersion": "26.5.0.0",
|
||||
"StudioVersion": "",
|
||||
"DynamicLoading": 0,
|
||||
"ContentProto": {
|
||||
"Use": "Json",
|
||||
"Json": {
|
||||
"CoreVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2
|
||||
},
|
||||
"ScriptVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0
|
||||
},
|
||||
"Description": "",
|
||||
"Id": "CombatMonster",
|
||||
"Language": 1,
|
||||
"Name": "CombatMonster",
|
||||
"Type": 1,
|
||||
"Source": 0,
|
||||
"Target": null,
|
||||
"Properties": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": "\"\"",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyId"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "0",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "RegTries"
|
||||
}
|
||||
],
|
||||
"Methods": [
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.RegTries = 0\nlocal eventId = 0\nlocal function reg()\n\tself.RegTries = self.RegTries + 1\n\tlocal c = _EntityService:GetEntityByPath(\"/common\")\n\tif c ~= nil and c.SlayDeckController ~= nil then\n\t\tc.SlayDeckController:RegisterMonster(self.Entity, self.EnemyId)\n\t\t_TimerService:ClearTimer(eventId)\n\telseif self.RegTries > 50 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend\neventId = _TimerService:SetTimerRepeat(reg, 0.1)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "OnBeginPlay"
|
||||
}
|
||||
],
|
||||
"EntityEventHandlers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,34 +169,6 @@
|
||||
"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",
|
||||
@@ -209,14 +181,28 @@
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyIntents"
|
||||
"Name": "Monsters"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "EnemyName"
|
||||
"Name": "Registered"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": "1",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "TargetIndex"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": "nil",
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "SlotPos"
|
||||
},
|
||||
{
|
||||
"Type": "any",
|
||||
@@ -445,7 +431,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 1\nself.RunLength = 3\nself.RunDeck = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself.RunActive = true\nself.RunRelics = {}\nself.Relics = {\n\tironHeart = { name = \"강철 심장\", desc = \"전투 시작 시 방어도 +6\", hook = \"combatStart\", effect = \"block\", value = 6 },\n\tenergyCore = { name = \"에너지 코어\", desc = \"턴 시작 시 에너지 +1\", hook = \"turnStart\", effect = \"energy\", value = 1 },\n\tvampire = { name = \"흡혈 송곳니\", desc = \"공격 카드 사용 시 HP +1\", hook = \"cardPlayed\", effect = \"healOnAttack\", value = 1 },\n\tgoldIdol = { name = \"황금 우상\", desc = \"전투 승리 시 골드 +10\", hook = \"combatReward\", effect = \"gold\", value = 10 },\n}\nself.RelicPool = { \"energyCore\", \"vampire\", \"goldIdol\" }\nself.Enemies = {\n\tslime = { name = \"슬라임\", maxHp = 45, intents = { { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 8 } } },\n\tslime_elite = { name = \"정예 슬라임\", maxHp = 70, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Attack\", value = 8 }, { kind = \"Defend\", value = 10 } } },\n\tslime_boss = { name = \"슬라임 킹\", maxHp = 120, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 12 }, { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 22 } } },\n}\nself.MapNodes = {\n\tA = { type = \"combat\", enemy = \"slime\", row = 1, col = -1, next = { \"C\", \"D\" } },\n\tB = { type = \"combat\", enemy = \"slime\", row = 1, col = 1, next = { \"C\", \"D\" } },\n\tC = { type = \"rest\", row = 2, col = -1, next = { \"E\", \"F\" } },\n\tD = { type = \"shop\", row = 2, col = 1, next = { \"E\", \"F\" } },\n\tE = { type = \"elite\", enemy = \"slime_elite\", row = 3, col = -1, next = { \"BOSS\" } },\n\tF = { type = \"combat\", enemy = \"slime\", row = 3, col = 1, next = { \"BOSS\" } },\n\tBOSS = { type = \"boss\", enemy = \"slime_boss\", row = 4, col = 0, next = { } },\n}\nself.MapStart = { \"A\", \"B\" }\nself.CurrentNodeId = \"\"\nself.CurrentEnemyId = \"\"\nself:BindButtons()\nself:AddRelic(\"ironHeart\")\nself:ShowMap()",
|
||||
"Code": "self.PlayerMaxHp = 80\nself.PlayerHp = self.PlayerMaxHp\nself.Gold = 0\nself.Floor = 1\nself.RunLength = 3\nself.RunDeck = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself.RunActive = true\nself.RunRelics = {}\nself.Relics = {\n\tironHeart = { name = \"강철 심장\", desc = \"전투 시작 시 방어도 +6\", hook = \"combatStart\", effect = \"block\", value = 6 },\n\tenergyCore = { name = \"에너지 코어\", desc = \"턴 시작 시 에너지 +1\", hook = \"turnStart\", effect = \"energy\", value = 1 },\n\tvampire = { name = \"흡혈 송곳니\", desc = \"공격 카드 사용 시 HP +1\", hook = \"cardPlayed\", effect = \"healOnAttack\", value = 1 },\n\tgoldIdol = { name = \"황금 우상\", desc = \"전투 승리 시 골드 +10\", hook = \"combatReward\", effect = \"gold\", value = 10 },\n}\nself.RelicPool = { \"energyCore\", \"vampire\", \"goldIdol\" }\nself.Enemies = {\n\tslime = { name = \"슬라임\", maxHp = 45, intents = { { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 6 }, { kind = \"Defend\", value = 8 } } },\n\tslime_elite = { name = \"정예 슬라임\", maxHp = 70, intents = { { kind = \"Attack\", value = 14 }, { kind = \"Attack\", value = 8 }, { kind = \"Defend\", value = 10 } } },\n\tslime_boss = { name = \"슬라임 킹\", maxHp = 120, intents = { { kind = \"Attack\", value = 18 }, { kind = \"Defend\", value = 12 }, { kind = \"Attack\", value = 10 }, { kind = \"Attack\", value = 22 } } },\n\torange_mushroom = { name = \"주황버섯\", maxHp = 16, intents = { { kind = \"Attack\", value = 5 }, { kind = \"Defend\", value = 4 }, { kind = \"Attack\", value = 7 } } },\n\tblue_mushroom = { name = \"파란버섯\", maxHp = 22, intents = { { kind = \"Attack\", value = 8 }, { kind = \"Attack\", value = 4 } } },\n}\nself.MapNodes = {\n\tA = { type = \"combat\", enemy = \"slime\", row = 1, col = -1, next = { \"C\", \"D\" } },\n\tB = { type = \"combat\", enemy = \"slime\", row = 1, col = 1, next = { \"C\", \"D\" } },\n\tC = { type = \"rest\", row = 2, col = -1, next = { \"E\", \"F\" } },\n\tD = { type = \"shop\", row = 2, col = 1, next = { \"E\", \"F\" } },\n\tE = { type = \"elite\", enemy = \"slime_elite\", row = 3, col = -1, next = { \"BOSS\" } },\n\tF = { type = \"combat\", enemy = \"slime\", row = 3, col = 1, next = { \"BOSS\" } },\n\tBOSS = { type = \"boss\", enemy = \"slime_boss\", row = 4, col = 0, next = { } },\n}\nself.MapStart = { \"A\", \"B\" }\nself.SlotPos = { { x = 430, y = 140 }, { x = 600, y = 140 }, { x = 770, y = 140 }, { x = 900, y = 140 } }\nself.CurrentNodeId = \"\"\nself.CurrentEnemyId = \"\"\nself:BindButtons()\nself:AddRelic(\"ironHeart\")\nself:ShowMap()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -460,12 +446,80 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nlocal enemy = self.Enemies[self.CurrentEnemyId]\nlocal mult = 1 + (self.Floor - 1) * 0.6\nself.PlayerBlock = 0\nself.EnemyName = enemy.name\nself.EnemyMaxHp = math.floor(enemy.maxHp * mult)\nself.EnemyHp = self.EnemyMaxHp\nself.EnemyBlock = 0\nself.EnemyIntents = {}\nfor i = 1, #enemy.intents do\n\tself.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }\nend\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 = {}\nfor i = 1, #self.RunDeck do\n\tself.DrawPile[i] = self.RunDeck[i]\nend\nself:Shuffle(self.DrawPile)\nself:RenderCombat()\nself:StartPlayerTurn()\nself:ApplyRelics(\"combatStart\")\nself:RenderCombat()",
|
||||
"Code": "self.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()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "StartCombat"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "monster"
|
||||
},
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "enemyId"
|
||||
}
|
||||
],
|
||||
"Code": "if self.Registered == nil then\n\tself.Registered = {}\nend\ntable.insert(self.Registered, { entity = monster, enemyId = enemyId })",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RegisterMonster"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self.Monsters = {}\nlocal reg = self.Registered or {}\nlocal list = {}\nfor i = 1, #reg do\n\tlocal r = reg[i]\n\tif r.entity ~= nil and isvalid(r.entity) then\n\t\tlocal x = 0\n\t\tif r.entity.TransformComponent ~= nil then\n\t\t\tx = r.entity.TransformComponent.WorldPosition.x\n\t\tend\n\t\ttable.insert(list, { entity = r.entity, enemyId = r.enemyId, x = x })\n\tend\nend\ntable.sort(list, function(a, b) return a.x < b.x end)\nlocal mult = 1 + (self.Floor - 1) * 0.6\nlocal n = #list\nif n > 4 then n = 4 end\nfor i = 1, n do\n\tlocal item = list[i]\n\tlocal e = self.Enemies[item.enemyId]\n\tif e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = \"Attack\", value = 5 } } } end\n\tlocal intents = {}\n\tfor k = 1, #e.intents do\n\t\tintents[k] = { kind = e.intents[k].kind, value = math.floor(e.intents[k].value * mult) }\n\tend\n\tlocal maxHp = math.floor(e.maxHp * mult)\n\tself.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,\n\t\thp = maxHp, maxHp = maxHp, block = 0, intents = intents, intentIdx = 1, alive = true, slot = i }\n\tself:ReviveMonsterEntity(item.entity)\n\tself:PositionMonsterSlot(i)\nend\nself.TargetIndex = 1",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "BuildMonsters"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "any",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "monster"
|
||||
}
|
||||
],
|
||||
"Code": "if monster == nil or not isvalid(monster) then\n\treturn\nend\nmonster:SetEnable(true)\nmonster:SetVisible(true)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "ReviveMonsterEntity"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
@@ -498,7 +552,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "local endTurn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/EndTurnButton\")\nif endTurn ~= nil and endTurn.ButtonComponent ~= nil then\n\tif self.EndTurnHandler ~= nil then\n\t\tendTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)\n\t\tself.EndTurnHandler = nil\n\tend\n\tself.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)\nend\nlocal drawPile = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/DrawPile\")\nif drawPile ~= nil and drawPile.ButtonComponent ~= nil then\n\tif self.DrawPileHandler ~= nil then\n\t\tdrawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)\n\t\tself.DrawPileHandler = nil\n\tend\n\tself.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect(\"draw\") end)\nend\nlocal discardPile = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/DiscardPile\")\nif discardPile ~= nil and discardPile.ButtonComponent ~= nil then\n\tif self.DiscardPileHandler ~= nil then\n\t\tdiscardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)\n\t\tself.DiscardPileHandler = nil\n\tend\n\tself.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect(\"discard\") end)\nend\nlocal inspectClose = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckInspectHud/Close\")\nif inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then\n\tif self.DeckInspectCloseHandler ~= nil then\n\t\tinspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)\n\t\tself.DeckInspectCloseHandler = nil\n\tend\n\tself.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)\nend\nlocal allDeckButton = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/AllDeckButton\")\nif allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then\n\tif self.AllDeckHandler ~= nil then\n\t\tallDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)\n\t\tself.AllDeckHandler = nil\n\tend\n\tself.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)\nend\nlocal allDeckClose = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckAllHud/Close\")\nif allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then\n\tif self.AllDeckCloseHandler ~= nil then\n\t\tallDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)\n\t\tself.AllDeckCloseHandler = nil\n\tend\n\tself.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)\nend\nfor i = 1, 5 do\n\tlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(i))\n\tif cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then\n\t\tcardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal rc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(i))\n\tif rc ~= nil and rc.ButtonComponent ~= nil then\n\t\trc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)\n\tend\nend\nlocal skip = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Skip\")\nif skip ~= nil and skip.ButtonComponent ~= nil then\n\tskip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)\nend\nlocal mapNodeIds = { \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"BOSS\" }\nfor i = 1, #mapNodeIds do\n\tlocal nid = mapNodeIds[i]\n\tlocal mn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud/Node_\" .. nid)\n\tif mn ~= nil and mn.ButtonComponent ~= nil then\n\t\tmn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal sc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i))\n\tif sc ~= nil and sc.ButtonComponent ~= nil then\n\t\tsc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)\n\tend\nend\nlocal shopLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Leave\")\nif shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then\n\tshopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend\nlocal shopRelic = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\nif shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then\n\tshopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)\nend\nlocal restLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RestHud/Leave\")\nif restLeave ~= nil and restLeave.ButtonComponent ~= nil then\n\trestLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend",
|
||||
"Code": "local endTurn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/EndTurnButton\")\nif endTurn ~= nil and endTurn.ButtonComponent ~= nil then\n\tif self.EndTurnHandler ~= nil then\n\t\tendTurn:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)\n\t\tself.EndTurnHandler = nil\n\tend\n\tself.EndTurnHandler = endTurn:ConnectEvent(ButtonClickEvent, function() self:EndPlayerTurn() end)\nend\nlocal drawPile = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/DrawPile\")\nif drawPile ~= nil and drawPile.ButtonComponent ~= nil then\n\tif self.DrawPileHandler ~= nil then\n\t\tdrawPile:DisconnectEvent(ButtonClickEvent, self.DrawPileHandler)\n\t\tself.DrawPileHandler = nil\n\tend\n\tself.DrawPileHandler = drawPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect(\"draw\") end)\nend\nlocal discardPile = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/DiscardPile\")\nif discardPile ~= nil and discardPile.ButtonComponent ~= nil then\n\tif self.DiscardPileHandler ~= nil then\n\t\tdiscardPile:DisconnectEvent(ButtonClickEvent, self.DiscardPileHandler)\n\t\tself.DiscardPileHandler = nil\n\tend\n\tself.DiscardPileHandler = discardPile:ConnectEvent(ButtonClickEvent, function() self:OpenDeckInspect(\"discard\") end)\nend\nlocal inspectClose = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckInspectHud/Close\")\nif inspectClose ~= nil and inspectClose.ButtonComponent ~= nil then\n\tif self.DeckInspectCloseHandler ~= nil then\n\t\tinspectClose:DisconnectEvent(ButtonClickEvent, self.DeckInspectCloseHandler)\n\t\tself.DeckInspectCloseHandler = nil\n\tend\n\tself.DeckInspectCloseHandler = inspectClose:ConnectEvent(ButtonClickEvent, function() self:CloseDeckInspect() end)\nend\nlocal allDeckButton = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/AllDeckButton\")\nif allDeckButton ~= nil and allDeckButton.ButtonComponent ~= nil then\n\tif self.AllDeckHandler ~= nil then\n\t\tallDeckButton:DisconnectEvent(ButtonClickEvent, self.AllDeckHandler)\n\t\tself.AllDeckHandler = nil\n\tend\n\tself.AllDeckHandler = allDeckButton:ConnectEvent(ButtonClickEvent, function() self:OpenAllDeck() end)\nend\nlocal allDeckClose = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckAllHud/Close\")\nif allDeckClose ~= nil and allDeckClose.ButtonComponent ~= nil then\n\tif self.AllDeckCloseHandler ~= nil then\n\t\tallDeckClose:DisconnectEvent(ButtonClickEvent, self.AllDeckCloseHandler)\n\t\tself.AllDeckCloseHandler = nil\n\tend\n\tself.AllDeckCloseHandler = allDeckClose:ConnectEvent(ButtonClickEvent, function() self:CloseAllDeck() end)\nend\nfor i = 1, 5 do\n\tlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(i))\n\tif cardEntity ~= nil and cardEntity.ButtonComponent ~= nil then\n\t\tcardEntity:ConnectEvent(ButtonClickEvent, function() self:PlayCard(i) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal rc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Reward\" .. tostring(i))\n\tif rc ~= nil and rc.ButtonComponent ~= nil then\n\t\trc:ConnectEvent(ButtonClickEvent, function() self:PickReward(i) end)\n\tend\nend\nlocal skip = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RewardHud/Skip\")\nif skip ~= nil and skip.ButtonComponent ~= nil then\n\tskip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end)\nend\nlocal mapNodeIds = { \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"BOSS\" }\nfor i = 1, #mapNodeIds do\n\tlocal nid = mapNodeIds[i]\n\tlocal mn = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/MapHud/Node_\" .. nid)\n\tif mn ~= nil and mn.ButtonComponent ~= nil then\n\t\tmn:ConnectEvent(ButtonClickEvent, function() self:PickNode(nid) end)\n\tend\nend\nfor i = 1, 3 do\n\tlocal sc = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Card\" .. tostring(i))\n\tif sc ~= nil and sc.ButtonComponent ~= nil then\n\t\tsc:ConnectEvent(ButtonClickEvent, function() self:BuyCard(i) end)\n\tend\nend\nlocal shopLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Leave\")\nif shopLeave ~= nil and shopLeave.ButtonComponent ~= nil then\n\tshopLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend\nlocal shopRelic = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/ShopHud/Relic\")\nif shopRelic ~= nil and shopRelic.ButtonComponent ~= nil then\n\tshopRelic:ConnectEvent(ButtonClickEvent, function() self:BuyRelic() end)\nend\nlocal restLeave = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/RestHud/Leave\")\nif restLeave ~= nil and restLeave.ButtonComponent ~= nil then\n\trestLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)\nend\nfor i = 1, 4 do\n\tlocal ms = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CombatHud/MonsterSlot\" .. tostring(i))\n\tif ms ~= nil and ms.ButtonComponent ~= nil then\n\t\tms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)\n\tend\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -904,7 +958,7 @@
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"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\n\tself:ApplyRelics(\"cardPlayed\")\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()",
|
||||
"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:DealDamageToTarget(c.damage)\n\tend\n\tself:ApplyRelics(\"cardPlayed\")\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": [],
|
||||
@@ -950,11 +1004,34 @@
|
||||
"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",
|
||||
"Code": "local m = self.Monsters[self.TargetIndex]\nif m == nil or m.alive ~= true then\n\tm = nil\n\tfor i = 1, #self.Monsters do\n\t\tif self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end\n\tend\nend\nif m == nil then\n\treturn\nend\nlocal dmg = amount\nif m.block > 0 then\n\tlocal absorbed = math.min(m.block, dmg)\n\tm.block = m.block - absorbed\n\tdmg = dmg - absorbed\nend\nm.hp = m.hp - dmg\nif m.hp <= 0 then\n\tm.hp = 0\n\tself:KillMonster(m.slot)\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "DealDamageToEnemy"
|
||||
"Name": "DealDamageToTarget"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "local m = self.Monsters[slot]\nif m == nil then\n\treturn\nend\nm.alive = false\nif m.entity ~= nil and isvalid(m.entity) then\n\tm.entity:SetVisible(false)\nend\nself:SetEntityEnabled(\"/ui/DefaultGroup/CombatHud/MonsterSlot\" .. tostring(slot), false)\nfor i = 1, #self.Monsters do\n\tif self.Monsters[i].alive == true then self.TargetIndex = i; break end\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "KillMonster"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
@@ -988,7 +1065,7 @@
|
||||
"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()",
|
||||
"Code": "for i = 1, #self.Monsters do\n\tlocal m = self.Monsters[i]\n\tif m.alive == true then\n\t\tm.block = 0\n\t\tlocal intent = m.intents[m.intentIdx]\n\t\tif intent ~= nil then\n\t\t\tif intent.kind == \"Attack\" then\n\t\t\t\tself:DealDamageToPlayer(intent.value)\n\t\t\telseif intent.kind == \"Defend\" then\n\t\t\t\tm.block = m.block + intent.value\n\t\t\tend\n\t\tend\n\t\tm.intentIdx = m.intentIdx + 1\n\t\tif m.intentIdx > #m.intents then\n\t\t\tm.intentIdx = 1\n\t\tend\n\tend\nend\nself:RenderCombat()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -1003,7 +1080,7 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "if self.EnemyHp <= 0 then\n\tself.CombatOver = true\n\tself.Gold = self.Gold + 15\n\tself:ApplyRelics(\"combatReward\")\n\tself:RenderRun()\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node ~= nil and node.type == \"elite\" then\n\t\tself:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])\n\tend\n\tif node ~= nil and node.type == \"boss\" then\n\t\tif self.Floor < self.RunLength then\n\t\t\tself.Floor = self.Floor + 1\n\t\t\tself.CurrentNodeId = \"\"\n\t\t\tself.CurrentEnemyId = \"\"\n\t\t\tself:RenderRun()\n\t\t\tself:ShowMap()\n\t\telse\n\t\t\tself:ShowResult(\"런 클리어!\")\n\t\t\tself.RunActive = false\n\t\tend\n\telse\n\t\tself:OfferReward()\n\tend\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"패배...\")\n\tself.RunActive = false\nend",
|
||||
"Code": "local anyAlive = false\nfor i = 1, #self.Monsters do\n\tif self.Monsters[i].alive == true then anyAlive = true; break end\nend\nif anyAlive == false then\n\tself.CombatOver = true\n\tself.Gold = self.Gold + 15\n\tself:ApplyRelics(\"combatReward\")\n\tself:RenderRun()\n\tlocal node = self.MapNodes[self.CurrentNodeId]\n\tif node ~= nil and node.type == \"elite\" then\n\t\tself:AddRelic(self.RelicPool[math.random(1, #self.RelicPool)])\n\tend\n\tif node ~= nil and node.type == \"boss\" then\n\t\tif self.Floor < self.RunLength then\n\t\t\tself.Floor = self.Floor + 1\n\t\t\tself.CurrentNodeId = \"\"\n\t\t\tself.CurrentEnemyId = \"\"\n\t\t\tself:RenderRun()\n\t\t\tself:ShowMap()\n\t\telse\n\t\t\tself:ShowResult(\"런 클리어!\")\n\t\t\tself.RunActive = false\n\t\tend\n\telse\n\t\tself:OfferReward()\n\tend\nelseif self.PlayerHp <= 0 then\n\tself.CombatOver = true\n\tself:ShowResult(\"패배...\")\n\tself.RunActive = false\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
@@ -1041,12 +1118,95 @@
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [],
|
||||
"Code": "self:SetText(\"/ui/DefaultGroup/CombatHud/EnemyName\", self.EnemyName)\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyHp\", \"HP \" .. string.format(\"%d\", self.EnemyHp) .. \"/\" .. string.format(\"%d\", self.EnemyMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/EnemyBlock\", \"방어 \" .. string.format(\"%d\", 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 \" .. string.format(\"%d\", self.PlayerHp) .. \"/\" .. string.format(\"%d\", self.PlayerMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerBlock\", \"방어 \" .. string.format(\"%d\", self.PlayerBlock))\nself:RenderRun()",
|
||||
"Code": "for i = 1, 4 do\n\tlocal base = \"/ui/DefaultGroup/CombatHud/MonsterSlot\" .. tostring(i)\n\tlocal m = self.Monsters[i]\n\tif m ~= nil and m.alive == true then\n\t\tself:SetEntityEnabled(base, true)\n\t\tself:SetText(base .. \"/Name\", m.name)\n\t\tself:SetText(base .. \"/Hp\", string.format(\"%d\", m.hp) .. \"/\" .. string.format(\"%d\", m.maxHp))\n\t\tlocal intent = m.intents[m.intentIdx]\n\t\tlocal t = \"\"\n\t\tif intent ~= nil then\n\t\t\tif intent.kind == \"Attack\" then t = \"공격 \" .. tostring(intent.value)\n\t\t\telseif intent.kind == \"Defend\" then t = \"방어 \" .. tostring(intent.value) end\n\t\tend\n\t\tif i == self.TargetIndex then t = \"[타겟] \" .. t end\n\t\tself:SetText(base .. \"/Intent\", t)\n\t\tself:SetHpBar(base .. \"/HpBarFill\", m.hp, m.maxHp)\n\telse\n\t\tself:SetEntityEnabled(base, false)\n\tend\nend\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerHp\", \"HP \" .. string.format(\"%d\", self.PlayerHp) .. \"/\" .. string.format(\"%d\", self.PlayerMaxHp))\nself:SetText(\"/ui/DefaultGroup/CombatHud/PlayerBlock\", \"방어 \" .. string.format(\"%d\", self.PlayerBlock))\nself:RenderRun()",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "RenderCombat"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "string",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "path"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "hp"
|
||||
},
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "maxHp"
|
||||
}
|
||||
],
|
||||
"Code": "local e = _EntityService:GetEntityByPath(path)\nif e == nil or e.UITransformComponent == nil then\n\treturn\nend\nlocal ratio = 0\nif maxHp > 0 then ratio = hp / maxHp end\nif ratio < 0 then ratio = 0 end\nlocal w = 120 * ratio\ne.UITransformComponent.RectSize = Vector2(w, 14)",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "SetHpBar"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "local sp = self.SlotPos\nif sp == nil or sp[slot] == nil then\n\treturn\nend\nlocal e = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CombatHud/MonsterSlot\" .. tostring(slot))\nif e ~= nil and e.UITransformComponent ~= nil then\n\te.UITransformComponent.anchoredPosition = Vector2(sp[slot].x, sp[slot].y)\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "PositionMonsterSlot"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": null
|
||||
},
|
||||
"Arguments": [
|
||||
{
|
||||
"Type": "number",
|
||||
"DefaultValue": null,
|
||||
"SyncDirection": 0,
|
||||
"Attributes": [],
|
||||
"Name": "slot"
|
||||
}
|
||||
],
|
||||
"Code": "if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then\n\tself.TargetIndex = slot\n\tself:RenderCombat()\nend",
|
||||
"Scope": 2,
|
||||
"ExecSpace": 6,
|
||||
"Attributes": [],
|
||||
"Name": "SetTarget"
|
||||
},
|
||||
{
|
||||
"Return": {
|
||||
"Type": "void",
|
||||
|
||||
@@ -27,7 +27,25 @@
|
||||
{ "kind": "Attack", "value": 10 },
|
||||
{ "kind": "Attack", "value": 22 }
|
||||
]
|
||||
},
|
||||
"orange_mushroom": {
|
||||
"name": "주황버섯",
|
||||
"maxHp": 16,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 5 },
|
||||
{ "kind": "Defend", "value": 4 },
|
||||
{ "kind": "Attack", "value": 7 }
|
||||
]
|
||||
},
|
||||
"blue_mushroom": {
|
||||
"name": "파란버섯",
|
||||
"maxHp": 22,
|
||||
"intents": [
|
||||
{ "kind": "Attack", "value": 8 },
|
||||
{ "kind": "Attack", "value": 4 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"activeEnemy": "slime"
|
||||
"activeEnemy": "slime",
|
||||
"simEncounter": ["orange_mushroom", "orange_mushroom", "blue_mushroom"]
|
||||
}
|
||||
|
||||
6
data/monster-slots.json
Normal file
6
data/monster-slots.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "x": 430, "y": 140 },
|
||||
{ "x": 600, "y": 140 },
|
||||
{ "x": 770, "y": 140 },
|
||||
{ "x": 900, "y": 140 }
|
||||
]
|
||||
1064
docs/superpowers/plans/2026-06-10-map-monster-combat.md
Normal file
1064
docs/superpowers/plans/2026-06-10-map-monster-combat.md
Normal file
File diff suppressed because it is too large
Load Diff
118
docs/superpowers/specs/2026-06-10-map-monster-combat-design.md
Normal file
118
docs/superpowers/specs/2026-06-10-map-monster-combat-design.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 맵 몬스터 카드 전투 — 설계
|
||||
|
||||
- 날짜: 2026-06-10
|
||||
- 대상: `tools/deck/gen-slaydeck.mjs`(SlayDeckController + UI), `data/enemies.json`, 신규 `tools/monster/gen-combat-monster.mjs`(+`CombatMonster.codeblock`), 맵 몬스터 엔티티, `tools/balance/sim-balance.mjs`
|
||||
- 상태: 승인됨 (브레인스토밍 → 본 스펙)
|
||||
|
||||
## 1. 배경
|
||||
|
||||
현재 카드 전투는 `SlayDeckController` 내부의 **추상 단일 적**으로 동작한다.
|
||||
|
||||
- 상태: `EnemyHp`/`EnemyMaxHp`/`EnemyBlock`/`EnemyName`/`EnemyIntents`/`EnemyIntentIndex` (단일 적 1체).
|
||||
- 공격 카드 → `DealDamageToEnemy(damage)` → `EnemyHp` 차감.
|
||||
- `CheckCombatEnd`: `EnemyHp<=0` → 승리(골드·보상·노드/막 진행), `PlayerHp<=0` → 패배.
|
||||
- 적 데이터는 `data/enemies.json`(slime/elite/boss), 노드의 `CurrentEnemyId`가 어떤 적인지 결정, `Floor` 배율 적용.
|
||||
- CombatHud는 단일 적 패널(EnemyName/EnemyHp/EnemyBlock/EnemyIntent)을 텍스트로 표시.
|
||||
|
||||
맵에는 실제 몬스터 엔티티(`script.Monster` + `script.MonsterAttack`, 예: map01의 주황버섯)가 있으나, 이는 물리 액션 전투용이라 **카드 전투와 완전히 분리**되어 있다.
|
||||
|
||||
## 2. 목표
|
||||
|
||||
카드 공격이 추상 슬라임이 아니라 **맵의 실제 몬스터**에 적용되고, **맵의 모든 몬스터가 쓰러지면 전투 승리**가 되도록 한다. 승리 이후 흐름(보상·노드·상점·휴식·막/보스)은 기존 런 시스템을 그대로 재사용한다.
|
||||
|
||||
## 3. 확정 요구사항 (브레인스토밍 결과)
|
||||
|
||||
1. **타겟팅**: 몬스터를 클릭하면 그 몬스터가 "현재 타겟"이 되고, 이후 공격 카드가 그 타겟에 적용된다.
|
||||
2. **전투원 모델**: 각 몬스터가 개별 HP와 의도(공격/방어)를 가지며, 적 턴에 생존 몬스터가 각자 플레이어를 공격한다(멀티 적).
|
||||
3. **런 연동**: 전투 노드 = 현재 물리 맵의 몬스터들. 전부 처치 시 노드 클리어 → 기존 보상/맵/상점/막 흐름 유지.
|
||||
4. **스탯 소스**: 각 맵 몬스터가 적 타입 id를 보유하고 HP/의도를 `data/enemies.json`에서 읽는다. 배율은 **막 배율(필수, 기존 `1+(Floor-1)*0.6` 재사용)** + **노드 타입 배율(선택, 기본 1; 엘리트/보스만 가산)**.
|
||||
5. **상태 표시**: 각 몬스터 머리 위에 월드(화면) HP바 + 의도 표시.
|
||||
6. **아키텍처**: 컨트롤러 중심. 전투 상태는 `SlayDeckController`가 단일 소유, 몬스터 엔티티는 타겟·시각(애니메이션) 역할만. 사망 연출은 기존 `script.Monster` 자산 재사용.
|
||||
7. **몬스터↔적ID**: 전용 경량 스크립트 `script.CombatMonster`의 `EnemyId`(string) 속성으로 명시.
|
||||
|
||||
## 4. 데이터 모델
|
||||
|
||||
### 4.1 `data/enemies.json`
|
||||
맵 몬스터용 적 타입을 추가한다(기존 slime/elite/boss는 유지). 예:
|
||||
|
||||
```json
|
||||
"orange_mushroom": {
|
||||
"name": "주황버섯",
|
||||
"maxHp": 16,
|
||||
"intents": [ { "kind": "Attack", "value": 5 }, { "kind": "Defend", "value": 4 } ]
|
||||
}
|
||||
```
|
||||
|
||||
스탯 수치는 placeholder이며 `sim-balance.mjs`로 추후 튜닝한다.
|
||||
|
||||
### 4.2 `script.CombatMonster` (신규 코드블록)
|
||||
맵 몬스터 엔티티에 부착하는 경량 마커. 속성 `EnemyId`(string) 1개. 런타임 로직 없음(컨트롤러가 읽기만). `script.Monster`를 보유한 엔티티에 함께 부착한다.
|
||||
|
||||
### 4.3 런타임 전투 상태 (SlayDeckController)
|
||||
단일 `Enemy*` 속성군을 제거하고 리스트로 교체한다.
|
||||
|
||||
- `Monsters`(any 리스트). 원소: `{ path, enemyId, name, hp, maxHp, block, intents, intentIndex, alive }`.
|
||||
- `TargetIndex`(number). 현재 선택된 타겟의 `Monsters` 인덱스.
|
||||
- 상수 `MAX_MONSTERS`(예: 4). UI 슬롯 수 = 이 값. 맵 몬스터가 더 많으면 앞에서 `MAX_MONSTERS`마리만 전투에 참여(초과분은 미지원, 로그 경고).
|
||||
|
||||
## 5. 전투 흐름 (SlayDeckController 메서드 변경)
|
||||
|
||||
- **StartCombat**:
|
||||
1. 현재 맵에서 `script.CombatMonster`를 가진 몬스터 엔티티를 스캔(맵 루트 하위, 최대 `MAX_MONSTERS`).
|
||||
2. 각 몬스터의 `EnemyId`로 `enemies.json` 스탯을 읽어 배율(막 배율 필수 + 노드 타입 배율 선택, §3-4)을 적용해 `Monsters` 리스트 구성.
|
||||
3. 몬스터 부활: 가시성 on, `StateComponent` IDLE, HP 리셋.
|
||||
4. 각 몬스터 화면 위치에 UI 슬롯(HP바·의도·타겟버튼) 배치·활성화.
|
||||
5. `TargetIndex` = 첫 생존자. 손패/턴 시작.
|
||||
- **SetTarget(i)**: `TargetIndex` 갱신 + 타겟 하이라이트 갱신.
|
||||
- **PlayCard(Attack)**: `Monsters[TargetIndex]`에 방어도→HP 차감. HP≤0 → 해당 몬스터 사망 처리(§6), UI 슬롯 숨김, 자동으로 다음 생존 타겟 선택. 이후 `CheckCombatEnd`.
|
||||
- **PlayCard(Skill)**: 기존대로 플레이어 방어도 증가(변경 없음).
|
||||
- **EnemyTurn**: 생존 몬스터 각자 `intentIndex` 진행 — Attack→`DealDamageToPlayer`, Defend→자기 `block` 증가. (각 몬스터는 독립 의도 사이클)
|
||||
- **CheckCombatEnd**: 생존 몬스터 0 → 승리(기존 보상/노드/막 분기 재사용). `PlayerHp<=0` → 패배.
|
||||
- **RenderCombat**: 각 생존 몬스터 UI 슬롯의 HP바·의도 갱신, 플레이어 패널 갱신. 기존 단일 적 패널(EnemyName/EnemyHp/EnemyIntent)은 제거 또는 숨김.
|
||||
|
||||
## 6. 사망 / 부활 연출
|
||||
|
||||
컨트롤러가 직접 관리하여 **노드 간 몬스터 영속**(엔티티 Destroy 안 함):
|
||||
|
||||
- 사망: 타겟 몬스터의 `StateComponent`를 DEAD로 전환(die 애니메이션) 후 짧은 지연 뒤 가시성 off. `alive=false`.
|
||||
- 부활: `StartCombat`에서 가시성 on, IDLE 상태, HP 리셋.
|
||||
|
||||
기존 `Monster.codeblock`의 hit/die 애니메이션 자산을 활용하되, Destroy/Respawn 타이머에 의존하지 않고 컨트롤러가 생사 시점을 통제한다.
|
||||
|
||||
## 7. UI — 몬스터 슬롯 (DefaultGroup.ui)
|
||||
|
||||
카메라가 고정(MapCamera)이라 몬스터의 화면상 위치가 불변 → UI 슬롯을 전투 시작 시 한 번 배치하면 된다.
|
||||
|
||||
- `gen-slaydeck.mjs`가 `CombatHud` 아래 **MonsterSlot ×MAX_MONSTERS**를 사전 생성(평소 비활성):
|
||||
- HP바 스프라이트(배경+채움), 의도 텍스트, 투명 타겟 버튼(클릭→`SetTarget`).
|
||||
- **위치 결정**: 런타임 world→screen 변환을 우선 시도(카메라 고정이므로 전투 시작 시 1회 계산). 변환 API가 여의치 않으면 **`data`에 슬롯 화면좌표를 명시**(현재 map01 몬스터 배치 기준)하는 폴백을 사용한다. → 구현 단계에서 변환 가용성 검증.
|
||||
|
||||
## 8. 변경 파일 요약
|
||||
|
||||
| 파일 | 변경 |
|
||||
|------|------|
|
||||
| `data/enemies.json` | 맵 몬스터 적 타입 추가(orange_mushroom 등) |
|
||||
| 신규 `tools/monster/gen-combat-monster.mjs` | `CombatMonster.codeblock` 생성 + 11개 맵 몬스터 엔티티에 `script.CombatMonster`(EnemyId) 부착(idempotent) |
|
||||
| `RootDesk/MyDesk/CombatMonster.codeblock` | 신규 생성물 |
|
||||
| `tools/deck/gen-slaydeck.mjs` | 전투 멀티 몬스터화(상태·PlayCard·EnemyTurn·CheckCombatEnd·RenderCombat·StartCombat·타겟 바인딩) + UI 몬스터 슬롯 생성 |
|
||||
| `tools/balance/sim-balance.mjs` | 멀티 몬스터 규칙으로 동기화 |
|
||||
| `map/map01.map`~`map11.map` | 몬스터 엔티티에 `script.CombatMonster` 부착(생성기 재실행 산출) |
|
||||
| `ui/DefaultGroup.ui` | 몬스터 슬롯 추가(생성기 산출) |
|
||||
|
||||
## 9. 알려진 한계 (MVP)
|
||||
|
||||
- 모든 전투 노드가 같은 물리 맵 몬스터를 재사용한다(막 배율로 난이도 차등). 노드별 다른 적 구성/맵 이동은 후속 과제.
|
||||
- `MAX_MONSTERS` 초과 몬스터는 전투에 미참여.
|
||||
- 보스 노드도 동일 맵 몬스터를 사용(테마 불일치)는 후속 콘텐츠 확장에서 해결.
|
||||
|
||||
## 10. 리스크
|
||||
|
||||
- **world→screen 변환 가용성**: 미지원 시 슬롯 좌표 데이터 폴백으로 대응(§7).
|
||||
- **외부 엔티티 스크립트 메서드/상태 접근**: 컨트롤러가 몬스터 엔티티의 `StateComponent`·가시성을 제어할 수 있어야 함(구현 단계 검증).
|
||||
- **생성물 단일 소스 유지**: 전투/HUD 산출물은 `gen-slaydeck.mjs`에서만 생성(직접 편집 금지) 규칙 유지.
|
||||
|
||||
## 11. 검증
|
||||
|
||||
- `node tools/balance/sim-balance.test.mjs` 통과 + 멀티 몬스터 규칙 반영.
|
||||
- 생성기 2회 실행 결과 동일(결정적).
|
||||
- 메이커 플레이: 카드로 특정 몬스터 타겟 공격 → HP 감소·사망 애니 → 전체 처치 시 승리 → 기존 보상/노드 흐름 진입. 적 턴에 생존 몬스터가 플레이어 공격.
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "595374e9-e49a-4733-b00f-77e8e61dad59",
|
||||
"path": "/maps/map01/주황버섯",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.DamageSkinSettingComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "주황버섯",
|
||||
"path": "/maps/map01/주황버섯",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"DataId": "02c22d93421b4038b3c413b3e40b57ec"
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "b1923aa6-7265-4f99-9ab6-1c45eae9af23",
|
||||
"path": "/maps/map01/돼지",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "돼지",
|
||||
"path": "/maps/map01/돼지",
|
||||
@@ -6642,6 +6647,11 @@
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6650,7 +6660,7 @@
|
||||
{
|
||||
"id": "566e94be-0efe-4536-9007-882dde030a95",
|
||||
"path": "/maps/map01/초록버섯",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.RigidbodyComponent,MOD.Core.MovementComponent,MOD.Core.StateComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,script.Monster,script.MonsterAttack,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "초록버섯",
|
||||
"path": "/maps/map01/초록버섯",
|
||||
@@ -6778,6 +6788,11 @@
|
||||
"y": 0
|
||||
},
|
||||
"Enable": true
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "000007d5-0000-4000-8000-0000000007d5",
|
||||
"path": "/maps/map02/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map02/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "000007d6-0000-4000-8000-0000000007d6",
|
||||
"path": "/maps/map02/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map02/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00000bbd-0000-4000-8000-000000000bbd",
|
||||
"path": "/maps/map03/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map03/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00000bbe-0000-4000-8000-000000000bbe",
|
||||
"path": "/maps/map03/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map03/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00000fa5-0000-4000-8000-000000000fa5",
|
||||
"path": "/maps/map04/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map04/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00000fa6-0000-4000-8000-000000000fa6",
|
||||
"path": "/maps/map04/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map04/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "0000138d-0000-4000-8000-00000000138d",
|
||||
"path": "/maps/map05/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map05/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "0000138e-0000-4000-8000-00000000138e",
|
||||
"path": "/maps/map05/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map05/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00001775-0000-4000-8000-000000001775",
|
||||
"path": "/maps/map06/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map06/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00001776-0000-4000-8000-000000001776",
|
||||
"path": "/maps/map06/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map06/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00001b5d-0000-4000-8000-000000001b5d",
|
||||
"path": "/maps/map07/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map07/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00001b5e-0000-4000-8000-000000001b5e",
|
||||
"path": "/maps/map07/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map07/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00001f45-0000-4000-8000-000000001f45",
|
||||
"path": "/maps/map08/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map08/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00001f46-0000-4000-8000-000000001f46",
|
||||
"path": "/maps/map08/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map08/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "0000232d-0000-4000-8000-00000000232d",
|
||||
"path": "/maps/map09/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map09/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "0000232e-0000-4000-8000-00000000232e",
|
||||
"path": "/maps/map09/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map09/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00002716-0000-4000-8000-000000002716",
|
||||
"path": "/maps/map10/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map10/Monster2",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00002715-0000-4000-8000-000000002715",
|
||||
"path": "/maps/map10/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map10/Monster1",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -6366,7 +6366,7 @@
|
||||
{
|
||||
"id": "00002afd-0000-4000-8000-000000002afd",
|
||||
"path": "/maps/map11/Monster1",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster1",
|
||||
"path": "/maps/map11/Monster1",
|
||||
@@ -6506,6 +6506,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
@@ -6514,7 +6519,7 @@
|
||||
{
|
||||
"id": "00002afe-0000-4000-8000-000000002afe",
|
||||
"path": "/maps/map11/Monster2",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack",
|
||||
"componentNames": "MOD.Core.TransformComponent,MOD.Core.StateAnimationComponent,MOD.Core.SpriteRendererComponent,MOD.Core.DamageSkinSettingComponent,MOD.Core.HitComponent,MOD.Core.DamageSkinSpawnerComponent,MOD.Core.StateComponent,MOD.Core.RigidbodyComponent,MOD.Core.KinematicbodyComponent,MOD.Core.SideviewbodyComponent,MOD.Core.MovementComponent,script.Monster,script.MonsterAttack,script.CombatMonster",
|
||||
"jsonString": {
|
||||
"name": "Monster2",
|
||||
"path": "/maps/map11/Monster2",
|
||||
@@ -6654,6 +6659,11 @@
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "script.CombatMonster",
|
||||
"Enable": true,
|
||||
"EnemyId": "orange_mushroom"
|
||||
}
|
||||
],
|
||||
"@version": 1
|
||||
|
||||
@@ -43,36 +43,38 @@ export function applyDamage(hp, block, amount) {
|
||||
export function loadData() {
|
||||
const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||
const enemy = enemiesData.enemies[enemiesData.activeEnemy];
|
||||
if (!enemy) throw new Error(`activeEnemy 없음: ${enemiesData.activeEnemy}`);
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, enemy };
|
||||
const ids = enemiesData.simEncounter || [enemiesData.activeEnemy];
|
||||
const monsters = ids.map((id) => {
|
||||
const e = enemiesData.enemies[id];
|
||||
if (!e) throw new Error(`simEncounter 적 없음: ${id}`);
|
||||
return { name: e.name, maxHp: e.maxHp, intents: e.intents };
|
||||
});
|
||||
return { cards: cardsData.cards, starterDeck: cardsData.starterDeck, monsters };
|
||||
}
|
||||
|
||||
// 손패에서 다음에 낼 카드의 인덱스 반환(-1=턴 종료). hand=카드 id 배열.
|
||||
export function chooseAction(hand, cards, energy, enemyHp, enemyBlock, enemyIntent) {
|
||||
// 주의: 인게임은 플레이어가 카드를 직접 선택한다. 이 chooseAction은 밸런스 추정용 자동 플레이 휴리스틱일 뿐
|
||||
// 이며, Lua에 대응 AI가 없다(동기화 대상은 데미지/방어/의도/승패 규칙이지 플레이어 선택이 아님).
|
||||
// 손패에서 낼 카드 인덱스(-1=종료). 공격 우선, 없으면 스킬.
|
||||
export function chooseAction(hand, cards, energy) {
|
||||
const entries = hand.map((id, i) => ({ id, i })).filter((x) => cards[x.id].cost <= energy);
|
||||
const attacks = entries.filter((x) => cards[x.id].kind === 'Attack');
|
||||
const skills = entries.filter((x) => cards[x.id].kind === 'Skill');
|
||||
const dmgEff = (x) => (cards[x.id].damage || 0) / cards[x.id].cost;
|
||||
const blkEff = (x) => (cards[x.id].block || 0) / cards[x.id].cost;
|
||||
const bestBy = (list, fn) => list.slice().sort((a, b) => fn(b) - fn(a))[0];
|
||||
|
||||
// 1) 치사: 에너지 한도 내 효율순 공격 데미지 합 >= 적 유효 hp?
|
||||
let e = energy, lethalDmg = 0;
|
||||
for (const x of attacks.slice().sort((a, b) => dmgEff(b) - dmgEff(a))) {
|
||||
if (cards[x.id].cost <= e) { e -= cards[x.id].cost; lethalDmg += cards[x.id].damage || 0; }
|
||||
}
|
||||
if (attacks.length && lethalDmg >= enemyHp + enemyBlock) return bestBy(attacks, dmgEff).i;
|
||||
|
||||
// 2) 적 공격 의도면 방어 우선
|
||||
if (enemyIntent && enemyIntent.kind === 'Attack' && skills.length) return bestBy(skills, blkEff).i;
|
||||
|
||||
// 3) 공격 우선, 없으면 스킬, 없으면 종료
|
||||
if (attacks.length) return bestBy(attacks, dmgEff).i;
|
||||
if (skills.length) return bestBy(skills, blkEff).i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 공격 타겟 선택: 이번 타격으로 처치 가능한 최소 유효체력, 없으면 유효체력 최소.
|
||||
export function chooseTarget(aliveMonsters, plannedDamage) {
|
||||
const eff = (m) => m.hp + m.block;
|
||||
const killable = aliveMonsters.filter((m) => eff(m) <= plannedDamage);
|
||||
const pool = killable.length ? killable : aliveMonsters;
|
||||
return pool.slice().sort((a, b) => eff(a) - eff(b) || pool.indexOf(a) - pool.indexOf(b))[0];
|
||||
}
|
||||
|
||||
function bump(s, cost, dmg, blk) {
|
||||
s = s || { plays: 0, energy: 0, damage: 0, block: 0 };
|
||||
s.plays++; s.energy += cost; s.damage += dmg; s.block += blk;
|
||||
@@ -82,12 +84,16 @@ function bump(s, cost, dmg, blk) {
|
||||
// 단일 전투 시뮬. stats(선택): {cardId: {plays,energy,damage,block}} 누적.
|
||||
// 반환: { win, turns, playerHpRemaining, draw? }
|
||||
export function simulateCombat(data, rng, stats) {
|
||||
const { cards, starterDeck, enemy } = data;
|
||||
const { cards, starterDeck, monsters } = data;
|
||||
if (monsters.length === 0) return { win: true, turns: 0, playerHpRemaining: PLAYER_HP };
|
||||
let drawPile = shuffle(starterDeck, rng);
|
||||
let discard = [];
|
||||
let hand = [];
|
||||
let pHp = PLAYER_HP, pBlock = 0;
|
||||
let eHp = enemy.maxHp, eBlock = 0, intentIdx = 0;
|
||||
const mob = monsters.map((m) => ({
|
||||
name: m.name, hp: m.maxHp, maxHp: m.maxHp, block: 0,
|
||||
intents: m.intents, intentIdx: 0, alive: true,
|
||||
}));
|
||||
let turns = 0;
|
||||
|
||||
function draw(n) {
|
||||
@@ -97,34 +103,44 @@ export function simulateCombat(data, rng, stats) {
|
||||
hand.push(drawPile.pop());
|
||||
}
|
||||
}
|
||||
const aliveList = () => mob.filter((m) => m.alive);
|
||||
|
||||
while (turns < MAX_TURNS) {
|
||||
turns++;
|
||||
let energy = ENERGY; pBlock = 0; hand = []; draw(HAND_SIZE);
|
||||
while (true) {
|
||||
const intent = enemy.intents[intentIdx];
|
||||
const idx = chooseAction(hand, cards, energy, eHp, eBlock, intent);
|
||||
const alive = aliveList();
|
||||
if (alive.length === 0) break;
|
||||
const idx = chooseAction(hand, cards, energy);
|
||||
if (idx < 0) break;
|
||||
const id = hand[idx], c = cards[id];
|
||||
energy -= c.cost;
|
||||
if (c.kind === 'Attack') {
|
||||
const r = applyDamage(eHp, eBlock, c.damage || 0); eHp = r.hp; eBlock = r.block;
|
||||
const target = chooseTarget(alive, c.damage || 0);
|
||||
const r = applyDamage(target.hp, target.block, c.damage || 0);
|
||||
target.hp = r.hp; target.block = r.block;
|
||||
if (target.hp <= 0) target.alive = false;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, 0);
|
||||
} else {
|
||||
pBlock += c.block || 0;
|
||||
if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0);
|
||||
}
|
||||
hand.splice(idx, 1); discard.push(id);
|
||||
if (eHp <= 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
||||
}
|
||||
discard.push(...hand); hand = [];
|
||||
eBlock = 0;
|
||||
const intent = enemy.intents[intentIdx];
|
||||
if (intent.kind === 'Attack') { const r = applyDamage(pHp, pBlock, intent.value); pHp = r.hp; pBlock = r.block; }
|
||||
else if (intent.kind === 'Defend') { eBlock += intent.value; }
|
||||
intentIdx = (intentIdx + 1) % enemy.intents.length;
|
||||
for (const m of mob) {
|
||||
if (!m.alive) continue;
|
||||
m.block = 0; // 매 턴 초기화 (이전 턴 블록 미이월)
|
||||
const it = m.intents[m.intentIdx];
|
||||
if (it) {
|
||||
if (it.kind === 'Attack') { const r = applyDamage(pHp, pBlock, it.value); pHp = r.hp; pBlock = r.block; }
|
||||
else if (it.kind === 'Defend') { m.block += it.value; }
|
||||
}
|
||||
m.intentIdx = (m.intentIdx + 1) % m.intents.length;
|
||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||
}
|
||||
}
|
||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
||||
}
|
||||
|
||||
@@ -152,13 +168,13 @@ export function runBatch(N, seed) {
|
||||
winRate: wins / N,
|
||||
avgTurns: mean(turnsArr), medianTurns: median(turnsArr),
|
||||
avgHpOnWin: mean(hpArr),
|
||||
cardStats, cards: data.cards, enemy: data.enemy, seed,
|
||||
cardStats, cards: data.cards, monsters: data.monsters, seed,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatReport(r) {
|
||||
const L = [];
|
||||
L.push(`=== 밸런스 시뮬레이션 (적: ${r.enemy.name} HP ${r.enemy.maxHp}) ===`);
|
||||
L.push(`=== 밸런스 시뮬레이션 (인카운터: ${r.monsters.map((m) => `${m.name}(${m.maxHp})`).join(', ')}) ===`);
|
||||
L.push(`시뮬 ${r.N}회 (seed=${r.seed})`);
|
||||
L.push(`승률: ${(r.winRate * 100).toFixed(1)}% (승 ${r.wins} / 패 ${r.losses}${r.draws ? ` / 무 ${r.draws}` : ''})`);
|
||||
L.push(`평균 턴: ${r.avgTurns.toFixed(2)} 중앙값 턴: ${r.medianTurns}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mulberry32, applyDamage, chooseAction, simulateCombat, runBatch,
|
||||
mulberry32, applyDamage, chooseAction, chooseTarget, simulateCombat, runBatch,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
test('applyDamage: 방어 우선 차감 후 hp', () => {
|
||||
@@ -23,34 +23,46 @@ const CARDS = {
|
||||
Bash: { name: '강타', cost: 2, kind: 'Attack', damage: 10 },
|
||||
};
|
||||
|
||||
test('chooseAction: 치사 가능하면 공격 선택', () => {
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 5, 0, { kind: 'Attack', value: 10 });
|
||||
test('chooseAction: 공격을 스킬보다 먼저 선택', () => {
|
||||
const idx = chooseAction(['Defend', 'Strike'], CARDS, 3);
|
||||
assert.equal(idx, 1); // Strike
|
||||
});
|
||||
|
||||
test('chooseAction: 공격 없으면 스킬 선택', () => {
|
||||
const idx = chooseAction(['Defend'], CARDS, 3);
|
||||
assert.equal(idx, 0);
|
||||
});
|
||||
|
||||
test('chooseAction: 치사 불가 + 적 공격 의도면 방어 선택', () => {
|
||||
const idx = chooseAction(['Strike', 'Defend'], CARDS, 3, 40, 0, { kind: 'Attack', value: 10 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 적 방어 의도면 공격 우선', () => {
|
||||
const idx = chooseAction(['Defend', 'Strike'], CARDS, 3, 40, 0, { kind: 'Defend', value: 8 });
|
||||
assert.equal(idx, 1);
|
||||
});
|
||||
|
||||
test('chooseAction: 사용 가능 카드 없으면 -1', () => {
|
||||
const idx = chooseAction(['Bash'], CARDS, 1, 40, 0, { kind: 'Attack', value: 10 });
|
||||
const idx = chooseAction(['Bash'], CARDS, 1);
|
||||
assert.equal(idx, -1);
|
||||
});
|
||||
|
||||
test('chooseTarget: 이번 타격으로 처치 가능한 최소 체력 우선', () => {
|
||||
const mob = [
|
||||
{ hp: 20, block: 0, alive: true },
|
||||
{ hp: 5, block: 0, alive: true },
|
||||
{ hp: 8, block: 0, alive: true },
|
||||
];
|
||||
assert.equal(chooseTarget(mob, 6), mob[1]); // 5<=6 처치 가능, 최소
|
||||
});
|
||||
|
||||
test('chooseTarget: 처치 불가면 유효체력 최소 선택', () => {
|
||||
const mob = [
|
||||
{ hp: 20, block: 0, alive: true },
|
||||
{ hp: 12, block: 5, alive: true },
|
||||
{ hp: 14, block: 0, alive: true },
|
||||
];
|
||||
assert.equal(chooseTarget(mob, 6), mob[2]); // 유효 14 < 17 < 20
|
||||
});
|
||||
|
||||
const DATA = {
|
||||
cards: CARDS,
|
||||
starterDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Bash'],
|
||||
enemy: {
|
||||
name: '슬라임', maxHp: 45, intents: [
|
||||
{ kind: 'Attack', value: 10 }, { kind: 'Attack', value: 6 }, { kind: 'Defend', value: 8 },
|
||||
monsters: [
|
||||
{ name: '주황버섯', maxHp: 16, intents: [{ kind: 'Attack', value: 5 }, { kind: 'Defend', value: 4 }] },
|
||||
{ name: '파란버섯', maxHp: 12, intents: [{ kind: 'Attack', value: 8 }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
test('simulateCombat: 결정적 결과(동일 시드)', () => {
|
||||
@@ -61,12 +73,40 @@ test('simulateCombat: 결정적 결과(동일 시드)', () => {
|
||||
assert.ok(r1.turns >= 1);
|
||||
});
|
||||
|
||||
test('simulateCombat: 약한 적이면 대체로 승리', () => {
|
||||
test('simulateCombat: 모든 몬스터 처치 시 승리', () => {
|
||||
let wins = 0;
|
||||
for (let i = 0; i < 50; i++) if (simulateCombat(DATA, mulberry32(i + 1)).win) wins++;
|
||||
assert.ok(wins >= 40, `예상 승리 다수, 실제 ${wins}/50`);
|
||||
});
|
||||
|
||||
test('simulateCombat: 강한 다수 적이면 패배 가능', () => {
|
||||
const hard = {
|
||||
cards: CARDS,
|
||||
starterDeck: DATA.starterDeck,
|
||||
monsters: Array.from({ length: 4 }, () => ({ name: '슬라임', maxHp: 60, intents: [{ kind: 'Attack', value: 12 }] })),
|
||||
};
|
||||
let losses = 0;
|
||||
for (let i = 0; i < 30; i++) if (!simulateCombat(hard, mulberry32(i + 1)).win) losses++;
|
||||
assert.ok(losses >= 1, `강한 적엔 패배가 나와야 함, 실제 패 ${losses}/30`);
|
||||
});
|
||||
|
||||
test('simulateCombat: 턴 상한 초과 시 draw 반환', () => {
|
||||
const immortal = {
|
||||
cards: { Defend: { name: '방어', cost: 1, kind: 'Skill', block: 5 } },
|
||||
starterDeck: Array(10).fill('Defend'),
|
||||
monsters: [{ name: '불사', maxHp: 9999, intents: [{ kind: 'Attack', value: 1 }] }],
|
||||
};
|
||||
const r = simulateCombat(immortal, mulberry32(1));
|
||||
assert.equal(r.draw, true);
|
||||
assert.equal(r.win, false);
|
||||
});
|
||||
|
||||
test('simulateCombat: 몬스터 없으면 즉시 승리', () => {
|
||||
const r = simulateCombat({ cards: {}, starterDeck: [], monsters: [] }, mulberry32(1));
|
||||
assert.equal(r.win, true);
|
||||
assert.equal(r.turns, 0);
|
||||
});
|
||||
|
||||
test('runBatch: 집계 필드·승률 범위', () => {
|
||||
const r = runBatch(100, 1);
|
||||
assert.equal(r.N, 100);
|
||||
|
||||
@@ -12,7 +12,6 @@ for (const id of CARDS.starterDeck) {
|
||||
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
|
||||
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
|
||||
}
|
||||
const ACTIVE_ENEMY = ENEMIES.enemies[ENEMIES.activeEnemy];
|
||||
|
||||
const MAP = JSON.parse(readFileSync('data/map.json', 'utf8'));
|
||||
for (const id of MAP.start) {
|
||||
@@ -27,6 +26,7 @@ for (const [id, n] of Object.entries(MAP.nodes)) {
|
||||
const MAX_ROW = Math.max(...Object.values(MAP.nodes).map((n) => n.row));
|
||||
|
||||
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||
const SLOTS = JSON.parse(readFileSync('data/monster-slots.json', 'utf8'));
|
||||
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
|
||||
for (const id of RELICS.relicPool) {
|
||||
if (!RELICS.relics[id]) throw new Error(`[gen-slaydeck] relicPool에 없는 유물 id: ${id}`);
|
||||
@@ -73,15 +73,6 @@ function luaCardsTable(cards) {
|
||||
function luaDeckTable(deck) {
|
||||
return `self.DrawPile = { ${deck.map(luaStr).join(', ')} }`;
|
||||
}
|
||||
function luaIntentsTable(intents) {
|
||||
const lines = intents.map((it) => `\t{ kind = ${luaStr(it.kind)}, value = ${it.value} },`);
|
||||
return `self.EnemyIntents = {\n${lines.join('\n')}\n}`;
|
||||
}
|
||||
function intentText(it) {
|
||||
if (it.kind === 'Attack') return `의도: 공격 ${it.value}`;
|
||||
if (it.kind === 'Defend') return `의도: 방어 ${it.value}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
const UI_FILE = 'ui/DefaultGroup.ui';
|
||||
const COMMON_FILE = 'Global/common.gamelogic';
|
||||
@@ -93,6 +84,9 @@ const ATTACK = { r: 0.86, g: 0.42, b: 0.38, a: 1 };
|
||||
const DEFEND = { r: 0.42, g: 0.55, b: 0.85, a: 1 };
|
||||
const SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 };
|
||||
|
||||
const MAX_MONSTERS = 4;
|
||||
|
||||
const HP_BAR_W = 120;
|
||||
const CARD_W = 180;
|
||||
const CARD_H = 250;
|
||||
const CARD_SPACING = 200;
|
||||
@@ -285,6 +279,9 @@ function entity({ id, path, modelId, entryId, componentNames, components, displa
|
||||
}
|
||||
|
||||
function upsertUi() {
|
||||
if (SLOTS.length < MAX_MONSTERS) {
|
||||
throw new Error(`[gen-slaydeck] monster-slots.json 항목(${SLOTS.length}) < MAX_MONSTERS(${MAX_MONSTERS})`);
|
||||
}
|
||||
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') && !e.path.startsWith('/ui/DefaultGroup/DeckInspectHud') && !e.path.startsWith('/ui/DefaultGroup/DeckAllHud') && !e.path.startsWith('/ui/DefaultGroup/CombatHud') && !e.path.startsWith('/ui/DefaultGroup/RewardHud') && !e.path.startsWith('/ui/DefaultGroup/MapHud') && !e.path.startsWith('/ui/DefaultGroup/ShopHud') && !e.path.startsWith('/ui/DefaultGroup/RestHud') && !e.path.startsWith('/ui/DefaultGroup/MainMenu'));
|
||||
@@ -721,40 +718,73 @@ function upsertUi() {
|
||||
sprite({ color: TRANSPARENT }),
|
||||
],
|
||||
}));
|
||||
const SLOT_W = 140, SLOT_H = 96;
|
||||
for (let i = 1; i <= MAX_MONSTERS; i++) {
|
||||
const base = `/ui/DefaultGroup/CombatHud/MonsterSlot${i}`;
|
||||
const slot = entity({
|
||||
id: guid('cmb', 40 + i),
|
||||
path: base,
|
||||
modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: 20 + i,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: SLOT_H }, pos: { x: (i - 2.5) * 320, y: 300 } }),
|
||||
sprite({ color: { r: 0, g: 0, b: 0, a: 0.0001 }, type: 1, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
});
|
||||
slot.jsonString.enable = false;
|
||||
combat.push(slot);
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 1),
|
||||
path: '/ui/DefaultGroup/CombatHud/EnemyBg',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
id: guid('cmb', 60 + i), path: `${base}/Name`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
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 }),
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 30 }, pos: { x: 0, y: 34 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 20, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const enemyTexts = [
|
||||
['EnemyName', { x: 0, y: 358 }, { x: 360, y: 44 }, ACTIVE_ENEMY.name, 28, true, GOLD],
|
||||
['EnemyHp', { x: 0, y: 316 }, { x: 360, y: 40 }, `HP ${ACTIVE_ENEMY.maxHp}/${ACTIVE_ENEMY.maxHp}`, 24, true, { r: 1, g: 1, b: 1, a: 1 }],
|
||||
['EnemyBlock', { x: 0, y: 280 }, { x: 360, y: 36 }, '방어 0', 20, false, { r: 0.6, g: 0.8, b: 1, a: 1 }],
|
||||
['EnemyIntent', { x: 0, y: 244 }, { x: 360, y: 38 }, intentText(ACTIVE_ENEMY.intents[0]), 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',
|
||||
id: guid('cmb', 80 + i), path: `${base}/Hp`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: enemyTexts.findIndex(([s]) => s === suffix) + 1,
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W, y: 26 }, pos: { x: 0, y: 6 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value, fontSize, bold, color }),
|
||||
text({ value: '', fontSize: 18, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 100 + i), path: `${base}/HpBarBg`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: 0, y: -14 } }),
|
||||
sprite({ color: { r: 0.18, g: 0.05, b: 0.06, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 120 + i), path: `${base}/HpBarFill`, modelId: 'uisprite', entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0, y: 0.5 }, size: { x: HP_BAR_W, y: 14 }, pos: { x: -HP_BAR_W / 2, y: -14 } }),
|
||||
sprite({ color: { r: 0.86, g: 0.35, b: 0.32, a: 1 }, type: 1 }),
|
||||
],
|
||||
}));
|
||||
combat.push(entity({
|
||||
id: guid('cmb', 140 + i), path: `${base}/Intent`, modelId: 'uitext', entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: SLOT_W, parentH: SLOT_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: SLOT_W + 40, y: 24 }, pos: { x: 0, y: -36 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 17, bold: true, color: { r: 1, g: 0.72, b: 0.5, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
}
|
||||
let cmbN = 2;
|
||||
combat.push(entity({
|
||||
id: guid('cmb', cmbN++),
|
||||
path: '/ui/DefaultGroup/CombatHud/PlayerBg',
|
||||
@@ -1318,13 +1348,11 @@ function writeCodeblocks() {
|
||||
prop('number', 'PlayerHp', '0'),
|
||||
prop('number', 'PlayerMaxHp', '80'),
|
||||
prop('number', 'PlayerBlock', '0'),
|
||||
prop('number', 'EnemyHp', '0'),
|
||||
prop('number', 'EnemyMaxHp', String(ACTIVE_ENEMY.maxHp)),
|
||||
prop('number', 'EnemyBlock', '0'),
|
||||
prop('number', 'EnemyIntentIndex', '1'),
|
||||
prop('boolean', 'CombatOver', 'false'),
|
||||
prop('any', 'EnemyIntents'),
|
||||
prop('any', 'EnemyName'),
|
||||
prop('any', 'Monsters'),
|
||||
prop('any', 'Registered'),
|
||||
prop('number', 'TargetIndex', '1'),
|
||||
prop('any', 'SlotPos'),
|
||||
prop('any', 'RunDeck'),
|
||||
prop('number', 'Gold', '0'),
|
||||
prop('number', 'Floor', '0'),
|
||||
@@ -1378,6 +1406,7 @@ self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
|
||||
${luaEnemiesTable(ENEMIES.enemies)}
|
||||
${luaMapNodesTable(MAP.nodes)}
|
||||
${luaStartArray(MAP.start)}
|
||||
self.SlotPos = { ${SLOTS.map((s) => `{ x = ${s.x}, y = ${s.y} }`).join(', ')} }
|
||||
self.CurrentNodeId = ""
|
||||
self.CurrentEnemyId = ""
|
||||
self:BindButtons()
|
||||
@@ -1385,18 +1414,7 @@ self:AddRelic("${RELICS.startingRelic}")
|
||||
self:ShowMap()`),
|
||||
method('StartCombat', `self.MaxEnergy = 3
|
||||
self.Turn = 0
|
||||
local enemy = self.Enemies[self.CurrentEnemyId]
|
||||
local mult = 1 + (self.Floor - 1) * 0.6
|
||||
self.PlayerBlock = 0
|
||||
self.EnemyName = enemy.name
|
||||
self.EnemyMaxHp = math.floor(enemy.maxHp * mult)
|
||||
self.EnemyHp = self.EnemyMaxHp
|
||||
self.EnemyBlock = 0
|
||||
self.EnemyIntents = {}
|
||||
for i = 1, #enemy.intents do
|
||||
self.EnemyIntents[i] = { kind = enemy.intents[i].kind, value = math.floor(enemy.intents[i].value * mult) }
|
||||
end
|
||||
self.EnemyIntentIndex = 1
|
||||
self.CombatOver = false
|
||||
self.DiscardPile = {}
|
||||
self.Hand = {}
|
||||
@@ -1406,10 +1424,55 @@ for i = 1, #self.RunDeck do
|
||||
self.DrawPile[i] = self.RunDeck[i]
|
||||
end
|
||||
self:Shuffle(self.DrawPile)
|
||||
self:BuildMonsters()
|
||||
self:RenderCombat()
|
||||
self:StartPlayerTurn()
|
||||
self:ApplyRelics("combatStart")
|
||||
self:RenderCombat()`),
|
||||
method('RegisterMonster', `if self.Registered == nil then
|
||||
self.Registered = {}
|
||||
end
|
||||
table.insert(self.Registered, { entity = monster, enemyId = enemyId })`, [
|
||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' },
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enemyId' },
|
||||
]),
|
||||
method('BuildMonsters', `self.Monsters = {}
|
||||
local reg = self.Registered or {}
|
||||
local list = {}
|
||||
for i = 1, #reg do
|
||||
local r = reg[i]
|
||||
if r.entity ~= nil and isvalid(r.entity) then
|
||||
local x = 0
|
||||
if r.entity.TransformComponent ~= nil then
|
||||
x = r.entity.TransformComponent.WorldPosition.x
|
||||
end
|
||||
table.insert(list, { entity = r.entity, enemyId = r.enemyId, x = x })
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return a.x < b.x end)
|
||||
local mult = 1 + (self.Floor - 1) * 0.6
|
||||
local n = #list
|
||||
if n > ${MAX_MONSTERS} then n = ${MAX_MONSTERS} end
|
||||
for i = 1, n do
|
||||
local item = list[i]
|
||||
local e = self.Enemies[item.enemyId]
|
||||
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
|
||||
local intents = {}
|
||||
for k = 1, #e.intents do
|
||||
intents[k] = { kind = e.intents[k].kind, value = math.floor(e.intents[k].value * mult) }
|
||||
end
|
||||
local maxHp = math.floor(e.maxHp * mult)
|
||||
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
|
||||
hp = maxHp, maxHp = maxHp, block = 0, intents = intents, intentIdx = 1, alive = true, slot = i }
|
||||
self:ReviveMonsterEntity(item.entity)
|
||||
self:PositionMonsterSlot(i)
|
||||
end
|
||||
self.TargetIndex = 1`),
|
||||
method('ReviveMonsterEntity', `if monster == nil or not isvalid(monster) then
|
||||
return
|
||||
end
|
||||
monster:SetEnable(true)
|
||||
monster:SetVisible(true)`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]),
|
||||
method('Shuffle', `if list == nil then
|
||||
\treturn
|
||||
end
|
||||
@@ -1506,6 +1569,12 @@ end
|
||||
local restLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud/Leave")
|
||||
if restLeave ~= nil and restLeave.ButtonComponent ~= nil then
|
||||
restLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end)
|
||||
end
|
||||
for i = 1, ${MAX_MONSTERS} do
|
||||
local ms = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i))
|
||||
if ms ~= nil and ms.ButtonComponent ~= nil then
|
||||
ms:ConnectEvent(ButtonClickEvent, function() self:SetTarget(i) end)
|
||||
end
|
||||
end`),
|
||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||
self.Energy = self.MaxEnergy
|
||||
@@ -1772,7 +1841,7 @@ end
|
||||
self.Energy = self.Energy - c.cost
|
||||
if c.kind == "Attack" then
|
||||
if c.damage ~= nil then
|
||||
self:DealDamageToEnemy(c.damage)
|
||||
self:DealDamageToTarget(c.damage)
|
||||
end
|
||||
self:ApplyRelics("cardPlayed")
|
||||
elseif c.kind == "Skill" then
|
||||
@@ -1787,16 +1856,39 @@ 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
|
||||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||
if m == nil or m.alive ~= true then
|
||||
m = nil
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
|
||||
end
|
||||
end
|
||||
if m == nil then
|
||||
return
|
||||
end
|
||||
local dmg = amount
|
||||
if m.block > 0 then
|
||||
local absorbed = math.min(m.block, dmg)
|
||||
m.block = m.block - absorbed
|
||||
dmg = dmg - absorbed
|
||||
end
|
||||
self.EnemyHp = self.EnemyHp - dmg
|
||||
if self.EnemyHp < 0 then
|
||||
self.EnemyHp = 0
|
||||
m.hp = m.hp - dmg
|
||||
if m.hp <= 0 then
|
||||
m.hp = 0
|
||||
self:KillMonster(m.slot)
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
|
||||
method('KillMonster', `local m = self.Monsters[slot]
|
||||
if m == nil then
|
||||
return
|
||||
end
|
||||
m.alive = false
|
||||
if m.entity ~= nil and isvalid(m.entity) then
|
||||
m.entity:SetVisible(false)
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('DealDamageToPlayer', `local dmg = amount
|
||||
if self.PlayerBlock > 0 then
|
||||
local absorbed = math.min(self.PlayerBlock, dmg)
|
||||
@@ -1807,21 +1899,30 @@ 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
|
||||
method('EnemyTurn', `for i = 1, #self.Monsters do
|
||||
local m = self.Monsters[i]
|
||||
if m.alive == true then
|
||||
m.block = 0
|
||||
local intent = m.intents[m.intentIdx]
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then
|
||||
self:DealDamageToPlayer(intent.value)
|
||||
elseif intent.kind == "Defend" then
|
||||
self.EnemyBlock = self.EnemyBlock + intent.value
|
||||
m.block = m.block + intent.value
|
||||
end
|
||||
end
|
||||
m.intentIdx = m.intentIdx + 1
|
||||
if m.intentIdx > #m.intents then
|
||||
m.intentIdx = 1
|
||||
end
|
||||
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
|
||||
method('CheckCombatEnd', `local anyAlive = false
|
||||
for i = 1, #self.Monsters do
|
||||
if self.Monsters[i].alive == true then anyAlive = true; break end
|
||||
end
|
||||
if anyAlive == false then
|
||||
self.CombatOver = true
|
||||
self.Gold = self.Gold + ${GOLD_PER_WIN}
|
||||
self:ApplyRelics("combatReward")
|
||||
@@ -1854,22 +1955,54 @@ 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 " .. string.format("%d", self.EnemyHp) .. "/" .. string.format("%d", self.EnemyMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyBlock", "방어 " .. string.format("%d", 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)
|
||||
method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do
|
||||
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i)
|
||||
local m = self.Monsters[i]
|
||||
if m ~= nil and m.alive == true then
|
||||
self:SetEntityEnabled(base, true)
|
||||
self:SetText(base .. "/Name", m.name)
|
||||
self:SetText(base .. "/Hp", string.format("%d", m.hp) .. "/" .. string.format("%d", m.maxHp))
|
||||
local intent = m.intents[m.intentIdx]
|
||||
local t = ""
|
||||
if intent ~= nil then
|
||||
if intent.kind == "Attack" then t = "공격 " .. tostring(intent.value)
|
||||
elseif intent.kind == "Defend" then t = "방어 " .. tostring(intent.value) end
|
||||
end
|
||||
if i == self.TargetIndex then t = "[타겟] " .. t end
|
||||
self:SetText(base .. "/Intent", t)
|
||||
self:SetHpBar(base .. "/HpBarFill", m.hp, m.maxHp)
|
||||
else
|
||||
self:SetEntityEnabled(base, false)
|
||||
end
|
||||
end
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/EnemyIntent", intentText)
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerHp", "HP " .. string.format("%d", self.PlayerHp) .. "/" .. string.format("%d", self.PlayerMaxHp))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/PlayerBlock", "방어 " .. string.format("%d", self.PlayerBlock))
|
||||
self:RenderRun()`),
|
||||
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
|
||||
if e == nil or e.UITransformComponent == nil then
|
||||
return
|
||||
end
|
||||
local ratio = 0
|
||||
if maxHp > 0 then ratio = hp / maxHp end
|
||||
if ratio < 0 then ratio = 0 end
|
||||
local w = ${HP_BAR_W} * ratio
|
||||
e.UITransformComponent.RectSize = Vector2(w, 14)`, [
|
||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hp' },
|
||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'maxHp' },
|
||||
]),
|
||||
method('PositionMonsterSlot', `local sp = self.SlotPos
|
||||
if sp == nil or sp[slot] == nil then
|
||||
return
|
||||
end
|
||||
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot))
|
||||
if e ~= nil and e.UITransformComponent ~= nil then
|
||||
e.UITransformComponent.anchoredPosition = Vector2(sp[slot].x, sp[slot].y)
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('SetTarget', `if self.Monsters[slot] ~= nil and self.Monsters[slot].alive == true then
|
||||
self.TargetIndex = slot
|
||||
self:RenderCombat()
|
||||
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
|
||||
method('RenderRun', `self:SetText("/ui/DefaultGroup/CombatHud/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength))
|
||||
self:SetText("/ui/DefaultGroup/CombatHud/Gold", "골드 " .. string.format("%d", self.Gold))`),
|
||||
method('OfferReward', `local pool = {}
|
||||
|
||||
76
tools/monster/gen-combat-monster.mjs
Normal file
76
tools/monster/gen-combat-monster.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
// 맵 몬스터에 적 타입(EnemyId)을 부여하고, BeginPlay 시 /common 컨트롤러에 자기등록하는 마커.
|
||||
// 카드 전투 시 컨트롤러가 등록 목록으로 인카운터를 구성한다.
|
||||
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
|
||||
const NAME_TO_ENEMY = { '주황버섯': 'orange_mushroom', '파란버섯': 'blue_mushroom' };
|
||||
const DEFAULT_ENEMY = 'orange_mushroom';
|
||||
|
||||
function prop(Type, Name, DefaultValue = 'nil') {
|
||||
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
|
||||
}
|
||||
function method(Name, Code, Arguments = [], ExecSpace = 6) {
|
||||
return {
|
||||
Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
|
||||
Arguments, Code, Scope: 2, ExecSpace, Attributes: [], Name,
|
||||
};
|
||||
}
|
||||
|
||||
function writeCodeblock() {
|
||||
const cb = {
|
||||
Id: '', GameId: '', EntryKey: 'codeblock://combatmonster', ContentType: 'x-mod/codeblock',
|
||||
Content: '', Usage: 0, UsePublish: 1, UseService: 0, CoreVersion: '26.5.0.0', StudioVersion: '', DynamicLoading: 0,
|
||||
ContentProto: { Use: 'Json', Json: {
|
||||
CoreVersion: { Major: 0, Minor: 2 }, ScriptVersion: { Major: 1, Minor: 0 },
|
||||
Description: '', Id: 'CombatMonster', Language: 1, Name: 'CombatMonster', Type: 1, Source: 0, Target: null,
|
||||
Properties: [prop('string', 'EnemyId', '""'), prop('number', 'RegTries', '0')],
|
||||
Methods: [
|
||||
method('OnBeginPlay', `self.RegTries = 0
|
||||
local eventId = 0
|
||||
local function reg()
|
||||
self.RegTries = self.RegTries + 1
|
||||
local c = _EntityService:GetEntityByPath("/common")
|
||||
if c ~= nil and c.SlayDeckController ~= nil then
|
||||
c.SlayDeckController:RegisterMonster(self.Entity, self.EnemyId)
|
||||
_TimerService:ClearTimer(eventId)
|
||||
elseif self.RegTries > 50 then
|
||||
_TimerService:ClearTimer(eventId)
|
||||
end
|
||||
end
|
||||
eventId = _TimerService:SetTimerRepeat(reg, 0.1)`),
|
||||
],
|
||||
EntityEventHandlers: [],
|
||||
} },
|
||||
};
|
||||
writeFileSync('RootDesk/MyDesk/CombatMonster.codeblock', JSON.stringify(cb, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
const isMonster = (e) => typeof e.componentNames === 'string' && e.componentNames.includes('script.Monster');
|
||||
|
||||
function patchMap(nn) {
|
||||
const tag = String(nn).padStart(2, '0');
|
||||
const file = `map/map${tag}.map`;
|
||||
const map = JSON.parse(readFileSync(file, 'utf8'));
|
||||
let count = 0;
|
||||
for (const e of map.ContentProto.Entities.filter(isMonster)) {
|
||||
const comps = e.jsonString && e.jsonString['@components'];
|
||||
if (!Array.isArray(comps)) {
|
||||
console.warn(`[gen-combat-monster] entity "${(e.jsonString && e.jsonString.name) || e.path}" has no @components — skipped`);
|
||||
continue;
|
||||
}
|
||||
const name = (e.jsonString && e.jsonString.name) || '';
|
||||
const enemyId = NAME_TO_ENEMY[name] || DEFAULT_ENEMY;
|
||||
e.jsonString['@components'] = comps.filter((c) => c['@type'] !== 'script.CombatMonster');
|
||||
e.jsonString['@components'].push({ '@type': 'script.CombatMonster', Enable: true, EnemyId: enemyId });
|
||||
const names = (e.componentNames || '').split(',').filter((s) => s && s !== 'script.CombatMonster');
|
||||
names.push('script.CombatMonster');
|
||||
e.componentNames = names.join(',');
|
||||
count++;
|
||||
}
|
||||
writeFileSync(file, JSON.stringify(map, null, 2), 'utf8');
|
||||
return `map${tag}(${count})`;
|
||||
}
|
||||
|
||||
writeCodeblock();
|
||||
const patched = MAP_NUMBERS.map(patchMap);
|
||||
console.log('CombatMonster codeblock written; patched maps:', patched.join(', '));
|
||||
15011
ui/DefaultGroup.ui
15011
ui/DefaultGroup.ui
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user