Add slay deck controller UI #6

Merged
gahusb merged 1 commits from feature/slay-deck-controller into main 2026-06-08 00:37:55 +09:00
4 changed files with 4281 additions and 1183 deletions

View File

@@ -16,7 +16,7 @@
{ {
"id": "b447311c-6f03-4ba1-aad0-8f0fe58df5c1", "id": "b447311c-6f03-4ba1-aad0-8f0fe58df5c1",
"path": "/common", "path": "/common",
"componentNames": "", "componentNames": "script.SlayDeckController",
"jsonString": { "jsonString": {
"name": "common", "name": "common",
"path": "/common", "path": "/common",
@@ -28,7 +28,16 @@
"pathConstraints": "/", "pathConstraints": "/",
"revision": 1, "revision": 1,
"modelId": null, "modelId": null,
"@components": [], "@components": [
{
"@type": "script.SlayDeckController",
"Enable": true,
"Energy": 0,
"MaxEnergy": 3,
"Turn": 0,
"TweenEventId": 0
}
],
"@version": 1 "@version": 1
} }
} }

View File

@@ -0,0 +1,372 @@
{
"Id": "",
"GameId": "",
"EntryKey": "codeblock://SlayDeckController",
"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": "SlayDeckController",
"Language": 1,
"Name": "SlayDeckController",
"Type": 1,
"Source": 0,
"Target": null,
"Properties": [
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "DrawPile"
},
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "DiscardPile"
},
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "Hand"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "Energy"
},
{
"Type": "number",
"DefaultValue": "3",
"SyncDirection": 0,
"Attributes": [],
"Name": "MaxEnergy"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "Turn"
},
{
"Type": "number",
"DefaultValue": "0",
"SyncDirection": 0,
"Attributes": [],
"Name": "TweenEventId"
},
{
"Type": "any",
"DefaultValue": "nil",
"SyncDirection": 0,
"Attributes": [],
"Name": "EndTurnHandler"
}
],
"Methods": [
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self:StartCombat()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "OnBeginPlay"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.MaxEnergy = 3\nself.Turn = 0\nself.DiscardPile = {}\nself.Hand = {}\nself.DrawPile = { \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Strike\", \"Defend\", \"Defend\", \"Defend\", \"Defend\", \"Bash\" }\nself:Shuffle(self.DrawPile)\nself:BindButtons()\nself:StartPlayerTurn()",
"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": "list"
}
],
"Code": "if list == nil then\n\treturn\nend\nfor i = #list, 2, -1 do\n\tlocal j = math.random(1, i)\n\tlist[i], list[j] = list[j], list[i]\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "Shuffle"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "local buttonEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/DeckHud/EndTurnButton\")\nif buttonEntity == nil or buttonEntity.ButtonComponent == nil then\n\treturn\nend\nif self.EndTurnHandler ~= nil then\n\tbuttonEntity:DisconnectEvent(ButtonClickEvent, self.EndTurnHandler)\n\tself.EndTurnHandler = nil\nend\nself.EndTurnHandler = buttonEntity:ConnectEvent(ButtonClickEvent, self.EndPlayerTurn)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "BindButtons"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self.Turn = self.Turn + 1\nself.Energy = self.MaxEnergy\nself:DrawCards(5)\nself:RenderHand(true)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "StartPlayerTurn"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "for i = 1, #self.Hand do\n\ttable.insert(self.DiscardPile, self.Hand[i])\nend\nself.Hand = {}\nself:RenderHand(false)\nself:RenderPiles()\n_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "EndPlayerTurn"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "amount"
}
],
"Code": "for i = 1, amount do\n\tif #self.DrawPile <= 0 then\n\t\tself:RecycleDiscardIntoDraw()\n\tend\n\tif #self.DrawPile <= 0 then\n\t\tbreak\n\tend\n\tlocal cardId = table.remove(self.DrawPile)\n\ttable.insert(self.Hand, cardId)\nend\nself:RenderPiles()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "DrawCards"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "if self.DiscardPile == nil or #self.DiscardPile <= 0 then\n\treturn\nend\nself.DrawPile = {}\nfor i = 1, #self.DiscardPile do\n\tself.DrawPile[i] = self.DiscardPile[i]\nend\nself.DiscardPile = {}\nself:Shuffle(self.DrawPile)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "RecycleDiscardIntoDraw"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [],
"Code": "self:SetText(\"/ui/DefaultGroup/DeckHud/DrawPile/Count\", tostring(#self.DrawPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/DiscardPile/Count\", tostring(#self.DiscardPile))\nself:SetText(\"/ui/DefaultGroup/DeckHud/Energy\", \"에너지 \" .. tostring(self.Energy) .. \"/\" .. tostring(self.MaxEnergy))",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "RenderPiles"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "boolean",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "animate"
}
],
"Code": "local drawStart = Vector2(-590, 8)\nfor i = 1, 5 do\n\tlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(i))\n\tif cardEntity ~= nil then\n\t\tlocal cardId = self.Hand[i]\n\t\tif cardId == nil then\n\t\t\tcardEntity.Enable = false\n\t\telse\n\t\t\tcardEntity.Enable = true\n\t\t\tself:ApplyCardVisual(i, cardId)\n\t\t\tif animate == true then\n\t\t\t\tself:AnimateCardFrom(i, drawStart, Vector2((i - 3) * 200, 0), 0.16 + i * 0.045)\n\t\t\tend\n\t\tend\n\tend\nend\nself:RenderPiles()",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "RenderHand"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "number",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "slot"
},
{
"Type": "string",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "cardId"
}
],
"Code": "local name = cardId\nlocal cost = 0\nlocal desc = \"\"\nlocal kind = \"Skill\"\nif cardId == \"Strike\" then\n\tname = \"타격\"\n\tcost = 1\n\tdesc = \"피해 6\"\n\tkind = \"Attack\"\nelseif cardId == \"Defend\" then\n\tname = \"방어\"\n\tcost = 1\n\tdesc = \"방어도 5\"\n\tkind = \"Skill\"\nelseif cardId == \"Bash\" then\n\tname = \"강타\"\n\tcost = 2\n\tdesc = \"피해 10\"\n\tkind = \"Attack\"\nend\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Cost\", tostring(cost))\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Name\", name)\nself:SetText(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot) .. \"/Desc\", desc)\nlocal cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot))\nif cardEntity ~= nil and cardEntity.SpriteGUIRendererComponent ~= nil then\n\tlocal ok = false\n\tlocal color = nil\n\tif kind == \"Attack\" then\n\t\tok, color = pcall(function() return Color(0.86, 0.42, 0.38, 1) end)\n\telseif kind == \"Skill\" then\n\t\tok, color = pcall(function() return Color(0.42, 0.55, 0.85, 1) end)\n\telse\n\t\tok, color = pcall(function() return Color(0.46, 0.68, 0.52, 1) end)\n\tend\n\tif ok == true and color ~= nil then\n\t\tcardEntity.SpriteGUIRendererComponent.Color = color\n\tend\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "ApplyCardVisual"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"Type": "string",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "path"
},
{
"Type": "string",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": "value"
}
],
"Code": "local entity = _EntityService:GetEntityByPath(path)\nif entity ~= nil and entity.TextComponent ~= nil then\n\tentity.TextComponent.Text = value\nend",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "SetText"
},
{
"Return": {
"Type": "void",
"DefaultValue": null,
"SyncDirection": 0,
"Attributes": [],
"Name": null
},
"Arguments": [
{
"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"
}
],
"Code": "local cardEntity = _EntityService:GetEntityByPath(\"/ui/DefaultGroup/CardHand/Card\" .. tostring(slot))\nif cardEntity == nil or cardEntity.UITransformComponent == nil then\n\treturn\nend\nlocal tr = cardEntity.UITransformComponent\ntr.anchoredPosition = fromPos\nlocal elapsed = 0\nlocal eventId = 0\neventId = _TimerService:SetTimerRepeat(function()\n\telapsed = elapsed + 1 / 60\n\tlocal t = math.min(elapsed / duration, 1)\n\tlocal eased = _TweenLogic:Ease(0, 1, 1, EaseType.SineEaseOut, t)\n\ttr.anchoredPosition = Vector2(fromPos.x + (toPos.x - fromPos.x) * eased, fromPos.y + (toPos.y - fromPos.y) * eased)\n\tif t >= 1 then\n\t\t_TimerService:ClearTimer(eventId)\n\tend\nend, 1 / 60)",
"Scope": 2,
"ExecSpace": 6,
"Attributes": [],
"Name": "AnimateCardFrom"
}
],
"EntityEventHandlers": []
}
}
}

555
tools/gen-slaydeck.mjs Normal file
View File

@@ -0,0 +1,555 @@
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 SKILL = { r: 0.46, g: 0.68, b: 0.52, a: 1 };
const CARD_W = 180;
const CARD_H = 250;
const CARD_SPACING = 200;
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 }) {
const offMin = { x: pos.x - pivot.x * size.x, y: pos.y - pivot.y * size.y };
const offMax = { x: pos.x + (1 - pivot.x) * size.x, y: pos.y + (1 - pivot.y) * size.y };
return {
'@type': 'MOD.Core.UITransformComponent',
ActivePlatform: 255,
AlignmentOption: align,
AnchorsMax: anchor,
AnchorsMin: anchor,
MobileOnly: false,
OffsetMax: offMax,
OffsetMin: offMin,
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() {
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: true,
};
}
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 }) {
const parts = path.split('/');
const name = parts[parts.length - 1];
return {
id,
path,
componentNames,
jsonString: {
name,
path,
nameEditable: true,
enable: true,
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'));
const E = ui.ContentProto.Entities;
ui.ContentProto.Entities = E.filter((e) => !e.path.startsWith('/ui/DefaultGroup/DeckHud'));
const byPath = new Map(ui.ContentProto.Entities.map((e) => [e.path, e]));
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++) {
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 = true;
card.jsonString.visible = true;
const children = [
['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 }],
];
for (const [suffix, cfg] of children) {
const path = `/ui/DefaultGroup/CardHand/Card${i}/${suffix}`;
let child = byPath.get(path);
if (!child) {
child = entity({
id: guid('dck', i * 10 + children.findIndex(([s]) => s === suffix)),
path,
modelId: 'uitext',
entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: suffix === 'Cost' ? 0 : suffix === 'Name' ? 1 : 2,
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 = true;
child.jsonString.visible = true;
child.jsonString['@components'][2].Text = cfg.value;
child.jsonString['@components'][2].FontSize = cfg.fontSize;
child.jsonString['@components'][2].MaxSize = cfg.fontSize;
}
}
}
const hud = [];
const add = (e) => hud.push(e);
add(entity({
id: guid('hud', 0),
path: '/ui/DefaultGroup/DeckHud',
modelId: 'uiempty',
entryId: 'UIEmpty',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 5,
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.20, 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('hud', hud.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('hud', hud.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('hud', hud.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('hud', hud.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('hud', hud.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(...hud);
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 = [], ExecSpace = 0) {
return {
Return: { Type: 'void', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: null },
Arguments,
Code,
Scope: 2,
ExecSpace,
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'),
], [
method('OnBeginPlay', `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('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)
\telse
\t\tok, color = pcall(function() return Color(0.46, 0.68, 0.52, 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('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' },
]),
]);
for (const m of combat.ContentProto.Json.Methods) {
m.ExecSpace = 6;
}
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 combat codeblocks generated.');

File diff suppressed because it is too large Load Diff