import { readFileSync, writeFileSync } from 'node:fs'; const UI_FILE = 'ui/DefaultGroup.ui'; const COMMON_FILE = 'Global/common.gamelogic'; const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 }; const DARK = { r: 0.08, g: 0.09, b: 0.11, a: 0.92 }; const GOLD = { r: 0.94, g: 0.74, b: 0.26, a: 1 }; 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 CARD_W = 180; const CARD_H = 250; const CARD_XS = [-400, -200, 0, 200, 400]; const ALIGN_CENTER = 0; const ALIGN_BOTTOM_CENTER = 6; function guid(prefix, n) { return `${prefix}${n.toString(16).padStart(4, '0')}-0000-4000-8000-${n.toString(16).padStart(12, '0')}`; } function transform({ parentW, parentH, anchor, pivot, size, pos, align = 0 }) { return { '@type': 'MOD.Core.UITransformComponent', ActivePlatform: 255, AlignmentOption: align, AnchorsMax: anchor, AnchorsMin: anchor, MobileOnly: false, OffsetMax: { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y }, OffsetMin: { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y }, Pivot: pivot, RectSize: size, UIMode: 1, UIScale: { x: 1, y: 1, z: 1 }, UIVersion: 2, anchoredPosition: pos, Position: { x: anchor.x * parentW - parentW / 2 + pos.x, y: anchor.y * parentH - parentH / 2 + pos.y, z: 0 }, QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 }, Scale: { x: 1, y: 1, z: 1 }, Enable: true, }; } function sprite({ dataId = '', color = TRANSPARENT, type = 1, raycast = false }) { return { '@type': 'MOD.Core.SpriteGUIRendererComponent', AnimClipPlayType: 0, EndFrameIndex: 2147483647, ImageRUID: { DataId: dataId }, LocalPosition: { x: 0, y: 0 }, LocalScale: { x: 1, y: 1 }, OverrideSorting: false, PlayRate: 1, PreserveSprite: 0, StartFrameIndex: 0, Color: color, DropShadow: false, DropShadowAngle: 30, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowDistance: 32, FillAmount: 1, FillCenter: true, FillClockWise: true, FillMethod: 0, FillOrigin: 0, FlipX: false, FlipY: false, FrameColumn: 1, FrameRate: 0, FrameRow: 1, Outline: false, OutlineColor: { r: 0, g: 0, b: 0, a: 1 }, OutlineWidth: 3, RaycastTarget: raycast, Type: type, Enable: true, }; } function button({ enabled = true } = {}) { return { '@type': 'MOD.Core.ButtonComponent', Colors: { NormalColor: { r: 1, g: 1, b: 1, a: 1 }, HighlightedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, PressedColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 1 }, SelectedColor: { r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1 }, DisabledColor: { r: 0.784313738, g: 0.784313738, b: 0.784313738, a: 0.5019608 }, ColorMultiplier: 1, FadeDuration: 0.1, }, ImageRUIDs: { HighlightedSprite: null, PressedSprite: null, SelectedSprite: null, DisabledSprite: null, }, KeyCode: 0, OverrideSorting: false, Transition: 1, Enable: enabled, }; } function text({ value, fontSize, bold = false, color = { r: 1, g: 1, b: 1, a: 1 }, alignment = 4 }) { return { '@type': 'MOD.Core.TextComponent', Alignment: alignment, Bold: bold, DropShadow: false, DropShadowAngle: 30, DropShadowColor: { r: 0, g: 0, b: 0, a: 0.72 }, DropShadowDistance: 32, Font: 0, FontColor: color, FontSize: fontSize, MaxSize: fontSize, MinSize: 8, OutlineColor: { r: 0.08, g: 0.08, b: 0.08, a: 1 }, OutlineDistance: { x: 1, y: -1 }, OutlineWidth: 1, Overflow: 0, OverrideSorting: false, Padding: { left: 0, right: 0, top: 0, bottom: 0 }, SizeFit: false, Text: value, UseOutLine: true, Enable: true, }; } function entity({ id, path, modelId, entryId, componentNames, components, displayOrder, enable = true }) { const parts = path.split('/'); const name = parts[parts.length - 1]; return { id, path, componentNames, jsonString: { name, path, nameEditable: true, enable, visible: true, localize: true, displayOrder, pathConstraints: '/'.repeat(parts.length - 1), revision: 1, origin: { type: 'Model', entry_id: entryId, sub_entity_id: null, root_entity_id: null, replaced_model_id: null, }, modelId, '@components': components, '@version': 1, }, }; } function upsertUi() { const ui = JSON.parse(readFileSync(UI_FILE, 'utf8')); ui.ContentProto.Entities = ui.ContentProto.Entities.filter( (e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud') && !e.path.startsWith('/ui/DefaultGroup/MainMenu') && !e.path.startsWith('/ui/DefaultGroup/CardHand/Card5/'), ); const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e])); const cardHand = byPath.get('/ui/DefaultGroup/CardHand'); if (cardHand) cardHand.jsonString.enable = false; const cards = [ { name: '타격', cost: '1', desc: '피해 6', tint: ATTACK }, { name: '타격', cost: '1', desc: '피해 6', tint: ATTACK }, { name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND }, { name: '방어', cost: '1', desc: '방어도 5', tint: DEFEND }, { name: '강타', cost: '2', desc: '피해 10', tint: ATTACK }, ]; for (let i = 1; i <= 5; i += 1) { const card = byPath.get(`/ui/DefaultGroup/CardHand/Card${i}`); if (!card) continue; const tr = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.UITransformComponent'); const sp = card.jsonString['@components'].find((c) => c['@type'] === 'MOD.Core.SpriteGUIRendererComponent'); tr.RectSize = { x: CARD_W, y: CARD_H }; tr.anchoredPosition = { x: CARD_XS[i - 1], y: 0 }; tr.OffsetMin = { x: CARD_XS[i - 1] - CARD_W / 2, y: -CARD_H / 2 }; tr.OffsetMax = { x: CARD_XS[i - 1] + CARD_W / 2, y: CARD_H / 2 }; sp.ImageRUID = { DataId: '' }; sp.Type = 1; sp.Color = cards[i - 1].tint; card.jsonString.enable = false; for (const [suffix, cfg] of [ ['Cost', { size: { x: 50, y: 50 }, pos: { x: -60, y: 95 }, value: cards[i - 1].cost, fontSize: 34, bold: true }], ['Name', { size: { x: 160, y: 50 }, pos: { x: 0, y: 50 }, value: cards[i - 1].name, fontSize: 26, bold: true }], ['Desc', { size: { x: 160, y: 82 }, pos: { x: 0, y: -80 }, value: cards[i - 1].desc, fontSize: 20, bold: false }], ]) { const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`; let child = byPath.get(path); if (!child) { child = entity({ id: guid('d00e', i * 10 + (suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2)), path, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2, enable: false, components: [ transform({ parentW: CARD_W, parentH: CARD_H, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: cfg.size, pos: cfg.pos }), sprite({ color: TRANSPARENT }), text({ value: cfg.value, fontSize: cfg.fontSize, bold: cfg.bold }), ], }); ui.ContentProto.Entities.push(child); byPath.set(path, child); } else { child.jsonString.enable = false; child.jsonString['@components'][2].Text = cfg.value; child.jsonString['@components'][2].FontSize = cfg.fontSize; child.jsonString['@components'][2].MaxSize = cfg.fontSize; } } } const ents = []; const add = (e) => ents.push(e); add(entity({ id: guid('f00d', 0), path: '/ui/DefaultGroup/MainMenu', modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 20, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.04, g: 0.05, b: 0.07, a: 0.96 }, type: 1, raycast: true }), ], })); add(entity({ id: guid('f00d', 1), path: '/ui/DefaultGroup/MainMenu/Title', 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: 720, y: 100 }, pos: { x: 0, y: 180 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '슬레이 메이플', fontSize: 64, bold: true, color: GOLD, alignment: 0 }), ], })); add(entity({ id: guid('f00d', 2), path: '/ui/DefaultGroup/MainMenu/Subtitle', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 48 }, pos: { x: 0, y: 104 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '카드를 뽑고, 덱을 만들고, 첨탑을 오른다', fontSize: 24, color: { r: 0.82, g: 0.86, b: 0.9, a: 1 }, alignment: 0 }), ], })); add(entity({ id: guid('f00d', 3), path: '/ui/DefaultGroup/MainMenu/NewGameButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 68 }, pos: { x: 0, y: -20 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.13, g: 0.15, b: 0.18, a: 1 }, type: 1, raycast: true }), button(), text({ value: '새 게임', fontSize: 30, bold: true, color: GOLD, alignment: 0 }), ], })); add(entity({ id: guid('f00d', 4), path: '/ui/DefaultGroup/MainMenu/ContinueButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 260, y: 58 }, pos: { x: 0, y: -100 }, align: ALIGN_CENTER }), sprite({ color: { r: 0.1, g: 0.11, b: 0.13, a: 0.78 }, type: 1, raycast: false }), button({ enabled: false }), text({ value: '이어하기', fontSize: 24, bold: true, color: { r: 0.55, g: 0.58, b: 0.62, a: 1 }, alignment: 0 }), ], })); add(entity({ id: guid('feed', 0), path: '/ui/DefaultGroup/DeckHud', modelId: 'uiempty', entryId: 'UIEmpty', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: 5, enable: false, components: [ transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1280, y: 330 }, pos: { x: 0, y: 180 }, align: ALIGN_BOTTOM_CENTER }), sprite({ color: TRANSPARENT }), ], })); for (const pile of [ { key: 'DrawPile', x: -590, label: '뽑을 덱', count: '10', color: { r: 0.17, g: 0.2, b: 0.25, a: 1 } }, { key: 'DiscardPile', x: 590, label: '버린 덱', count: '0', color: { r: 0.22, g: 0.18, b: 0.16, a: 1 } }, ]) { add(entity({ id: guid('feed', ents.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}`, modelId: 'uisprite', entryId: 'UISprite', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', displayOrder: pile.key === 'DrawPile' ? 0 : 1, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 132, y: 186 }, pos: { x: pile.x, y: 8 }, align: ALIGN_CENTER }), sprite({ color: pile.color, type: 1, raycast: true }), ], })); add(entity({ id: guid('feed', ents.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}/Label`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 0, components: [ transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 42 }, pos: { x: 0, y: 45 } }), sprite({ color: TRANSPARENT }), text({ value: pile.label, fontSize: 21, bold: true, color: GOLD }), ], })); add(entity({ id: guid('feed', ents.length), path: `/ui/DefaultGroup/DeckHud/${pile.key}/Count`, modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 1, components: [ transform({ parentW: 132, parentH: 186, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 120, y: 72 }, pos: { x: 0, y: -20 } }), sprite({ color: TRANSPARENT }), text({ value: pile.count, fontSize: 42, bold: true }), ], })); } add(entity({ id: guid('feed', ents.length), path: '/ui/DefaultGroup/DeckHud/EndTurnButton', modelId: 'uibutton', entryId: 'UIButton', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', displayOrder: 2, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 170, y: 58 }, pos: { x: 0, y: 135 }, align: ALIGN_CENTER }), sprite({ color: DARK, type: 1, raycast: true }), button(), text({ value: '턴 종료', fontSize: 25, bold: true, color: GOLD, alignment: 0 }), ], })); add(entity({ id: guid('feed', ents.length), path: '/ui/DefaultGroup/DeckHud/Energy', modelId: 'uitext', entryId: 'UIText', componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', displayOrder: 3, components: [ transform({ parentW: 1280, parentH: 330, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 220, y: 42 }, pos: { x: 0, y: 90 }, align: ALIGN_CENTER }), sprite({ color: TRANSPARENT }), text({ value: '에너지 3/3', fontSize: 24, bold: true, color: { r: 0.6, g: 0.9, b: 1, a: 1 }, alignment: 0 }), ], })); ui.ContentProto.Entities.push(...ents); JSON.parse(JSON.stringify(ui)); writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8'); } function prop(Type, Name, DefaultValue = 'nil') { return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name }; } function method(Name, Code, Arguments = []) { return { Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null }, Arguments, Code, Scope: 2, ExecSpace: 6, Attributes: [], Name, }; } function codeblock(id, name, properties, methods) { return { Id: '', GameId: '', EntryKey: `codeblock://${id}`, 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: id, Language: 1, Name: name, Type: 1, Source: 0, Target: null, Properties: properties, Methods: methods, EntityEventHandlers: [], }, }, }; } function writeCodeblocks() { const combat = codeblock('SlayDeckController', 'SlayDeckController', [ prop('any', 'DrawPile'), prop('any', 'DiscardPile'), prop('any', 'Hand'), prop('number', 'Energy', '0'), prop('number', 'MaxEnergy', '3'), prop('number', 'Turn', '0'), prop('number', 'TweenEventId', '0'), prop('any', 'EndTurnHandler'), prop('any', 'NewGameHandler'), ], [ method('OnBeginPlay', `self:ShowMainMenu()`), method('ShowMainMenu', `self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", true) self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) self:BindMenuButtons()`), method('BindMenuButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/MainMenu/NewGameButton") if buttonEntity == nil or buttonEntity.ButtonComponent == nil then \treturn end if self.NewGameHandler ~= nil then \tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.NewGameHandler) \tself.NewGameHandler = nil end self.NewGameHandler = buttonEntity:ConnectEvent(ButtonClickEvent, self.StartNewGame)`), method('StartNewGame', `self:SetEntityEnabled("/ui/DefaultGroup/MainMenu", false) self:SetEntityEnabled("/ui/DefaultGroup/CardHand", true) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", true) self:ConfigureTurnBasedMonsters() self:ConfigureTurnBasedPlayer() self:StartCombat()`), method('StartCombat', `self.MaxEnergy = 3 self.Turn = 0 self.DiscardPile = {} self.Hand = {} self.DrawPile = { "Strike", "Strike", "Strike", "Strike", "Strike", "Defend", "Defend", "Defend", "Defend", "Bash" } self:Shuffle(self.DrawPile) self:BindButtons() self:StartPlayerTurn()`), method('ConfigureTurnBasedMonsters', `for mapIndex = 1, 11 do \tlocal mapName = "map" .. string.format("%02d", mapIndex) \tfor i = 1, 6 do \t\tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath("/maps/" .. mapName .. "/Monster" .. tostring(i))) \tend \tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath("/maps/" .. mapName .. "/StaticMonsterTemplate")) \tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath("/maps/" .. mapName .. "/MoveMonsterTemplate")) \tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath("/maps/" .. mapName .. "/ChaseMonsterTemplate")) \tself:ConfigureMonsterForTurnCombat(_EntityService:GetEntityByPath("/maps/" .. mapName .. "/monster-43")) end`), method('ConfigureMonsterForTurnCombat', `if monster == nil then \treturn end if monster.AIWanderComponent ~= nil then \tmonster.AIWanderComponent.Enable = false end if monster.AIChaseComponent ~= nil then \tmonster.AIChaseComponent.Enable = false end if monster.MovementComponent ~= nil then \tmonster.MovementComponent.Enable = false end if monster.RigidbodyComponent ~= nil then \tmonster.RigidbodyComponent.MoveVelocity = Vector2.zero \tmonster.RigidbodyComponent.RealMoveVelocity = Vector2.zero end if monster.TransformComponent ~= nil then \tlocal scale = monster.TransformComponent.Scale \tmonster.TransformComponent.Scale = Vector3(math.abs(scale.x), math.abs(scale.y), scale.z) end if monster.StateAnimationComponent ~= nil and monster.SpriteRendererComponent ~= nil then \tlocal stand = monster.StateAnimationComponent.ActionSheet["stand"] \tif stand ~= nil and stand ~= "" then \t\tmonster.SpriteRendererComponent.SpriteRUID = stand \tend end`, [{ Type: 'Entity', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'monster' }]), method('ConfigureTurnBasedPlayer', `local player = nil pcall(function() \tif _UserService ~= nil and _UserService.LocalPlayer ~= nil then \t\tplayer = _UserService.LocalPlayer \tend end) pcall(function() \tif player == nil and _UserService ~= nil and _UserService.LocalPlayerEntity ~= nil then \t\tplayer = _UserService.LocalPlayerEntity \tend end) pcall(function() \tif player == nil and _UserService ~= nil and _UserService.GetLocalPlayer ~= nil then \t\tplayer = _UserService:GetLocalPlayer() \tend end) if player ~= nil and player.Entity ~= nil then \tplayer = player.Entity end if player == nil then \treturn end if player.PlayerControllerComponent ~= nil then \tplayer.PlayerControllerComponent.Enable = false \tpcall(function() player.PlayerControllerComponent.LookDirectionX = 1 end) end if player.MovementComponent ~= nil then \tplayer.MovementComponent.Enable = false \tpcall(function() player.MovementComponent.InputSpeed = 0 end) \tpcall(function() player.MovementComponent.JumpForce = 0 end) end if player.RigidbodyComponent ~= nil then \tplayer.RigidbodyComponent.MoveVelocity = Vector2.zero \tplayer.RigidbodyComponent.RealMoveVelocity = Vector2.zero end if player.TransformComponent ~= nil then \tlocal scale = player.TransformComponent.Scale \tplayer.TransformComponent.Scale = Vector3(math.abs(scale.x), math.abs(scale.y), scale.z) end`), method('Shuffle', `if list == nil then \treturn end for i = #list, 2, -1 do \tlocal j = math.random(1, i) \tlist[i], list[j] = list[j], list[i] end`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'list' }]), method('BindButtons', `local buttonEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/DeckHud/EndTurnButton") if buttonEntity == nil or buttonEntity.ButtonComponent == nil then \treturn end if self.EndTurnHandler ~= nil then \tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler) \tself.EndTurnHandler = nil end self.EndTurnHandler = buttonEntity:ConnectEvent(ButtonClickEvent, self.EndPlayerTurn)`), method('StartPlayerTurn', `self.Turn = self.Turn + 1 self.Energy = self.MaxEnergy self:DrawCards(5) self:RenderHand(true)`), method('EndPlayerTurn', `for i = 1, #self.Hand do \ttable.insert(self.DiscardPile, self.Hand[i]) end self.Hand = {} self:RenderHand(false) self:RenderPiles() _TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`), method('DrawCards', `for i = 1, amount do \tif #self.DrawPile <= 0 then \t\tself:RecycleDiscardIntoDraw() \tend \tif #self.DrawPile <= 0 then \t\tbreak \tend \tlocal cardId = table.remove(self.DrawPile) \ttable.insert(self.Hand, cardId) end self:RenderPiles()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), method('RecycleDiscardIntoDraw', `if self.DiscardPile == nil or #self.DiscardPile <= 0 then \treturn end self.DrawPile = {} for i = 1, #self.DiscardPile do \tself.DrawPile[i] = self.DiscardPile[i] end self.DiscardPile = {} self:Shuffle(self.DrawPile)`), method('RenderPiles', `self:SetText("/ui/DefaultGroup/DeckHud/DrawPile/Count", tostring(#self.DrawPile)) self:SetText("/ui/DefaultGroup/DeckHud/DiscardPile/Count", tostring(#self.DiscardPile)) self:SetText("/ui/DefaultGroup/DeckHud/Energy", "에너지 " .. tostring(self.Energy) .. "/" .. tostring(self.MaxEnergy))`), method('RenderHand', `local drawStart = Vector2(-590, 8) for i = 1, 5 do \tlocal cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i)) \tif cardEntity ~= nil then \t\tlocal cardId = self.Hand[i] \t\tif cardId == nil then \t\t\tcardEntity.Enable = false \t\telse \t\t\tcardEntity.Enable = true \t\t\tself:ApplyCardVisual(i, cardId) \t\t\tif animate == true then \t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2((i - 3) * 200, 0), 0.16 + i * 0.045) \t\t\tend \t\tend \tend end self:RenderPiles()`, [{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' }]), method('ApplyCardVisual', `local name = cardId local cost = 0 local desc = "" local kind = "Skill" if cardId == "Strike" then \tname = "타격" \tcost = 1 \tdesc = "피해 6" \tkind = "Attack" elseif cardId == "Defend" then \tname = "방어" \tcost = 1 \tdesc = "방어도 5" \tkind = "Skill" elseif cardId == "Bash" then \tname = "강타" \tcost = 2 \tdesc = "피해 10" \tkind = "Attack" end self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Cost", tostring(cost)) self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Name", name) self:SetText("/ui/DefaultGroup/CardHand/Card" .. tostring(slot) .. "/Desc", desc) local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then \tlocal ok = false \tlocal color = nil \tif kind == "Attack" then \t\tok, color = pcall(function() return Color(0.86, 0.42, 0.38, 1) end) \telseif kind == "Skill" then \t\tok, color = pcall(function() return Color(0.42, 0.55, 0.85, 1) end) \tend \tif ok == true and color ~= nil then \t\tcardEntity.SpriteGUIRendererComponent.Color = color \tend end`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' }, ]), method('SetText', `local entity = _EntityService:GetEntityByPath(path) if entity ~= nil and entity.TextComponent ~= nil then \tentity.TextComponent.Text = value end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'value' }, ]), method('SetEntityEnabled', `local entity = _EntityService:GetEntityByPath(path) if entity ~= nil then \tentity.Enable = enabled end`, [ { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'path' }, { Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'enabled' }, ]), method('AnimateCardFrom', `local cardEntity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot)) if cardEntity == nil or cardEntity.UITransformComponent == nil then \treturn end local tr = cardEntity.UITransformComponent tr.anchoredPosition = fromPos local elapsed = 0 local eventId = 0 eventId = _TimerService:SetTimerRepeat(function() \telapsed = elapsed + 1 / 60 \tlocal t = math.min(elapsed / duration, 1) \tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t) \ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased) \tif t >= 1 then \t\t_TimerService:ClearTimer(eventId) \tend end, 1 / 60)`, [ { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromPos' }, { Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' }, ]), ]); writeFileSync('RootDesk/MyDesk/SlayDeckController.codeblock', JSON.stringify(combat, null, 2), 'utf8'); } function patchCommon() { const common = JSON.parse(readFileSync(COMMON_FILE, 'utf8')); const entity = common.ContentProto.Entities.find((e) => e.path === '/common'); entity.componentNames = 'script.SlayDeckController'; entity.jsonString['@components'] = [ { '@type': 'script.SlayDeckController', Enable: true, Energy: 0, MaxEnergy: 3, Turn: 0, TweenEventId: 0 }, ]; JSON.parse(JSON.stringify(common)); writeFileSync(COMMON_FILE, JSON.stringify(common, null, 2), 'utf8'); } upsertUi(); writeCodeblocks(); patchCommon(); console.log('Slay deck UI and main menu generated.');