diff --git a/GDD.md b/GDD.md index 321e140..3f18bc4 100644 --- a/GDD.md +++ b/GDD.md @@ -963,7 +963,17 @@ export function registerUserScripts() { Визуально это oldschool кнопки с F-клавишами в стиле Norton Commander: F1 Game F2 Save F3 Load F4 New F5 Sprite Edit F9 Settings -При нажатии Shift: F2 Save As... +Нижнее меню является **динамическим smart-меню**: + +- в обычном состоянии оно показывает базовые F-клавиши редактора; +- при удержании модификаторов (`Shift`, `Alt`, `Ctrl`) оно временно перестраивается и показывает набор горячих клавиш, доступных с этим модификатором; +- подписи в таком режиме могут зависеть от текущего состояния редактора. Например, при удержании `Alt` для выделенного объекта может показываться `D Disable` или `D Enable` в зависимости от того, отключён объект или нет. + +Пример: + +- при нажатии `Shift`: `F2 Save As...`, а также `1 Save Sel. 1` и `2 Save Sel. 2` для сохранения текущего выделения в два быстрых selection slots +- при нажатии `Alt`: `F2 Save As...`, а также контекстные действия вроде `L Lock/Unlock` и `D Disable/Enable` +- при нажатии `Ctrl`: операции редактирования вроде `Undo`, `Redo`, `Duplicate`, `Copy`, `Paste`, `Save Prefab`, `Load Prefab` Пользователь может нажимать на них мышкой или использовать клавиатурные сочетания. @@ -1078,10 +1088,16 @@ Prefab можно загрузить в текущую сцену из файл | **Ctrl + V** | Paste | Вставить объект/группу из буфера в позицию курсора (или в центр вида) | | **Ctrl + S** | Save Prefab | Сохранить текущее выделение (single/group) в файл Prefab | | **Ctrl + O** | Load Prefab | Загрузить prefab и вставить в позицию курсора (или в центр вида) | +| **Shift + 1** | Save Sel. 1 | Сохранить текущее single/group выделение в быстрый слот 1, если курсор над канвасом или Hierarchy | +| **Shift + 2** | Save Sel. 2 | Сохранить текущее single/group выделение в быстрый слот 2, если курсор над канвасом или Hierarchy | +| **1** | Restore Sel. 1 | Восстановить сохранённое выделение из быстрого слота 1, если курсор над канвасом или Hierarchy | +| **2** | Restore Sel. 2 | Восстановить сохранённое выделение из быстрого слота 2, если курсор над канвасом или Hierarchy | | **Insert** | Assign Sprite | Назначить спрайт выбранному объекту | | **Space** | Select Scene | Выбрать настройки сцены (и снять выделение с объектов), если курсор над канвасом | | **LMB Drag (empty area)** | Marquee Select | Выделить несколько объектов прямоугольной областью | +Быстрые selection slots запоминают именно текущий набор выделенных объектов по их ID в текущей сцене. Если слот пуст, или сохранённые объекты больше не существуют, редактор показывает пользователю Toast Notification. + ### 3. Создание объектов (Creation) | Клавиша | Действие | Описание | diff --git a/progress.md b/progress.md index 1cf1d40..5865e6a 100644 --- a/progress.md +++ b/progress.md @@ -24,3 +24,8 @@ Original prompt: Давай временно переключимся с пар - Budgets scene cache подняты в 3 раза; image cache получил отдельный budget по device class. - В `GDD.md` в конец раздела `Техническая реализация` добавлено описание profiler и примеры вызова через `window.__QUEST_DEBUG__`. - Проверка browser loop: skill client не запустился, потому что в окружении нет пакета 'playwright'; для smoke test использован встроенный browser MCP. + +- New task: make editor bottom menu modifier-aware (Ctrl/Alt/Shift), with dynamic labels such as D ENABLE/DISABLE based on selected object state. +- Implemented state-driven EditorBottomMenu with modifier modes; added Alt/Shift Save As path and Ctrl action set; pending browser smoke verification. + +- Verified in browser: bottom menu now switches between base / Alt / Ctrl / Shift layouts, and Alt mode updates D from Disable to Enable after toggling selected object disabled state.\n diff --git a/public/scenes/logo.json b/public/scenes/logo.json new file mode 100644 index 0000000..f6efb28 --- /dev/null +++ b/public/scenes/logo.json @@ -0,0 +1,513 @@ +{ + "id": "logo", + "name": "New Scene", + "description": "You are in New Scene.", + "textRedirects": {}, + "filename": "logo", + "walkbox": [], + "triggerboxes": [], + "scaling": { + "enabled": true, + "min": 0.4, + "max": 1, + "horizon": 78, + "front": 421 + }, + "entities": [ + { + "name": "Quad_201", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": "#g,#a", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "3d-parallax" + } + ], + "layer": -1, + "visible": true, + "x": 334.7566652973353, + "y": 147.45349435034427, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_204", + "type": "vertex", + "index": 3 + } + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_204", + "type": "vertex", + "index": 2 + } + }, + { + "x": 3915.4050968588786, + "y": 457.44748933663504, + "p": 1 + }, + { + "x": -3349.5949031411214, + "y": 457.44748933663504, + "p": 1 + } + ], + "color": "#000219", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", + "isGrid": true, + "gridLinesX": 29, + "gridLinesY": 7, + "lineWidth": 7.4, + "gridColor": "#136382", + "filled": true, + "blur": 0 + }, + { + "name": "Quad_1", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": "#a", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "x": 363.1753332849686, + "y": -1342.0347499102, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad", + "type": "vertex", + "index": 3 + } + }, + { + "x": 1652, + "y": -488, + "p": 0.2, + "binding": { + "targetName": "Quad", + "type": "vertex", + "index": 2 + } + }, + { + "x": 3933.8237648465147, + "y": -1042.0407549239112, + "p": 1 + }, + { + "x": -3331.1762351534853, + "y": -1042.0407549239112, + "p": 1 + } + ], + "color": "#000219", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", + "isGrid": true, + "gridLinesX": 29, + "gridLinesY": 7, + "lineWidth": 7.4, + "gridColor": "#136382", + "filled": true, + "blur": 0 + }, + { + "name": "miles_ds", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Shadow", + "shadowQuadId": "shadow", + "offsetX": -80, + "offsetY": -0.79232867459731, + "triggerId": "#g" + } + ], + "layer": 0, + "visible": true, + "x": -11.507373210367716, + "y": 437.34328111928414, + "width": 66.7529345238604, + "height": 272.57448263909663, + "baseWidth": 96, + "baseHeight": 392, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "miles_ds-idle-right.json", + "color": "#e27fa5", + "scale": 0.6953430679568792, + "modelScale": 1.03, + "parallax": 0.4538289363137056, + "ignoreScaling": false, + "animationSpeed": 30, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": true, + "speed": 0.35, + "direction": "left", + "animSets": { + "idle": { + "id": "idle", + "up": "miles_ds-idle-up.json", + "down": "miles_ds-idle-down.json", + "left": null, + "right": "miles_ds-idle-right.json" + }, + "walk": { + "id": "walk", + "up": "miles_ds-walk-up.json", + "down": "miles_ds-walk-down.json", + "left": null, + "right": "miles_ds-walk-right.json" + } + } + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -67.17651736805739, + "y": 437.33802751880216, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -67.17651736805739, + "y": 437.33802751880216, + "p": 0.4523541075273739 + }, + { + "x": 5.443172833624047, + "y": 437.2945106625292, + "p": 0.452503989220326 + }, + { + "x": -5.703139435760795, + "y": 438.52482078346884, + "p": 0.48592782666534984 + }, + { + "x": -73.48973431343629, + "y": 438.4581791995139, + "p": 0.48411737462461735 + } + ], + "color": "#000000", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 4 + }, + { + "name": "logo_1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -71, + "y": 237, + "width": 439.65753424657544, + "height": 438.3150684931505, + "baseWidth": 392.5513698630138, + "baseHeight": 391.35273972602727, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "scanline_logo", + "color": "#AAAAAA", + "scale": 1.1199999999999999, + "modelScale": 2.8, + "parallax": 0.2, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0.6, + "blendMode": "lighter", + "blur": 0 + }, + { + "name": "logo_2", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -64, + "y": 346, + "width": 655.1999999999999, + "height": 668.64, + "baseWidth": 585, + "baseHeight": 597, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "scanline_logo", + "color": "#00ca4c", + "scale": 1.1199999999999999, + "modelScale": 2.8, + "parallax": 0.2, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0.35, + "blendMode": "screen", + "blur": 32 + }, + { + "name": "Quad_204", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": -306.3073195313695, + "y": 194.6463347631958, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1132, + "y": 408, + "p": 0.2 + }, + { + "x": 1152, + "y": 388, + "p": 0.2 + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#ee00ff", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "difference", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 8 + }, + { + "name": "Quad", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 31.056518423854072, + "y": -469.3074715804131, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -914, + "y": -424, + "p": 0.2 + }, + { + "x": 1249, + "y": -424, + "p": 0.2 + }, + { + "x": 1652, + "y": -488, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#ee00ff", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "difference", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 8 + }, + { + "name": "Quad_646", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -1359.1857676091972, + "y": -530.9597165247759, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 0 + } + }, + { + "x": 1550.7, + "y": -487.6, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "grid", + "gridU": 0.9666666666666667, + "gridV": 0 + } + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#020440", + "sortMode": "ignore", + "opacity": 0.55, + "blendMode": "source-over", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 20 + } + ], + "camera": { + "x": 165.57094204403754, + "y": -263.4392820164094, + "zoom": 0.1500946352969992 + }, + "autoCenter": true, + "cameraSpeed": 5, + "camDeadzoneX": 50, + "camDeadzoneY": 30, + "camMaxY": -370 +} diff --git a/public/scenes/test_room (10).json b/public/scenes/test_room (10).json index 8256b5f..6151972 100644 --- a/public/scenes/test_room (10).json +++ b/public/scenes/test_room (10).json @@ -17,7 +17,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "poly": [ { "x": -79, @@ -50,7 +49,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "poly": [ { "x": 82, @@ -117,13 +115,11 @@ "components": [ { "type": "Subscene", - "targetGroupId": "#D", - "name": "" + "targetGroupId": "#D" } ], "layer": 0, "visible": true, - "spatial": {}, "poly": [ { "x": 27, @@ -165,7 +161,7 @@ "type": "Triggerbox", "locked": false, "disabled": true, - "groupID": "", + "groupID": null, "customName": "", "textRedirects": {}, "interactions": {}, @@ -180,7 +176,7 @@ "sound2": "drawer_close.wav" } ], - "layer": 1, + "layer": 0, "visible": true, "spatial": { "parentNodeId": "Trig_sub_D", @@ -218,7 +214,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "poly": [], "script": "" }, @@ -242,9 +237,12 @@ "sound2": "drawer_close.wav" } ], - "layer": 1, + "layer": 0, "visible": true, - "spatial": {}, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, "poly": [ { "x": -156.29411764705878, @@ -266,44 +264,93 @@ "script": "" }, { - "name": "Drawer3", + "name": "Desk", "type": "Triggerbox", "locked": false, - "disabled": true, + "disabled": false, "groupID": null, "customName": "", "textRedirects": {}, "interactions": {}, - "components": [ - { - "type": "Subscene", - "targetGroupId": "", - "title": "", - "description": "" - } - ], + "components": [], "layer": 0, "visible": true, - "spatial": { - "parentNodeId": "Trig_sub_D", - "relation": "in" - }, "poly": [ { - "x": -106, - "y": 4 + "x": -210, + "y": 211 + }, + { + "x": -192, + "y": 213 + }, + { + "x": -192, + "y": 197 + }, + { + "x": -171, + "y": 189 + }, + { + "x": -159, + "y": 211 + }, + { + "x": -136, + "y": 217 }, { - "x": 389, - "y": 3 + "x": -131, + "y": 242 }, { - "x": 375, + "x": -109, + "y": 243 + }, + { + "x": -110, + "y": 146 + }, + { + "x": -18, + "y": 129 + }, + { + "x": -16, + "y": 201 + }, + { + "x": 26, + "y": 207 + }, + { + "x": 82, + "y": 193 + }, + { + "x": 87, "y": 95 }, { - "x": -107, - "y": 91 + "x": -70, + "y": 78 + }, + { + "x": -236, + "y": 115 + }, + { + "x": -235, + "y": 173 + }, + { + "x": -218, + "y": 182 + }, + { + "x": -212, + "y": 211 } ], "script": "" @@ -329,7 +376,10 @@ "components": [], "layer": -2, "visible": true, - "spatial": {}, + "spatial": { + "parentNodeId": "window1", + "relation": "in" + }, "x": 119.67896209456934, "y": 234, "width": 821.6, @@ -361,7 +411,6 @@ "components": [], "layer": -1, "visible": true, - "spatial": {}, "x": 199, "y": 297, "width": 884.8, @@ -393,7 +442,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "x": 100, "y": 249, "width": 116.89999999999999, @@ -422,24 +470,31 @@ "customName": "", "textRedirects": {}, "interactions": {}, - "components": [], + "components": [ + { + "type": "Shadow", + "shadowQuadId": "shadow", + "offsetX": -70, + "offsetY": -25, + "triggerId": "room" + } + ], "layer": 0, "visible": true, - "spatial": {}, - "x": 251.49445909180304, - "y": 256.5381551800198, - "width": 69.81706443264875, - "height": 285.0863464333157, + "x": 232.86186143649797, + "y": 223.85074125381902, + "width": 71.03999999999999, + "height": 290.08, "baseWidth": 96, "baseHeight": 392, "colliderWidth": 88, "colliderHeight": 4, "spriteName": "miles_ds-idle-right.json", "color": "#00ffff", - "scale": 0.7272610878400911, + "scale": 0.74, "modelScale": 0.74, - "parallax": 1.046112265312777, - "ignoreScaling": false, + "parallax": 1.024795357575403, + "ignoreScaling": true, "animationSpeed": 30, "opacity": 1, "blendMode": "source-over", @@ -476,7 +531,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "x": -328, "y": 307, "width": 171.2340644206598, @@ -508,7 +562,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "x": 135, "y": 310, "width": 614.4, @@ -545,7 +598,6 @@ ], "layer": 3, "visible": true, - "spatial": {}, "x": 134, "y": 311, "width": 614.4, @@ -577,7 +629,6 @@ "components": [], "layer": 4, "visible": true, - "spatial": {}, "x": 136, "y": -4, "width": 614.4, @@ -684,7 +735,6 @@ "components": [], "layer": 6, "visible": true, - "spatial": {}, "x": 135, "y": -176, "width": 614.4, @@ -721,7 +771,6 @@ ], "layer": 6, "visible": true, - "spatial": {}, "x": 135, "y": 66, "width": 614.4, @@ -757,7 +806,6 @@ ], "layer": 0, "visible": true, - "spatial": {}, "x": -146.13725490196006, "y": 289.60784313725503, "parallax": 1, @@ -808,9 +856,8 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, - "x": 222.90433731748038, - "y": 306.9284515529542, + "x": 222.9043373174802, + "y": 306.9284515529539, "width": 1008.8000000000001, "height": 90.39999999999999, "baseWidth": 1261, @@ -821,7 +868,7 @@ "color": "#36d87fff", "scale": 0.8, "modelScale": 0.8, - "parallax": 1.079038203629382, + "parallax": 1.0790382036293817, "ignoreScaling": false, "animationSpeed": 150, "opacity": 1, @@ -844,7 +891,6 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, "x": -152, "y": -11, "width": 112, @@ -942,7 +988,7 @@ "name": "Drawer1", "type": "Entity", "locked": false, - "disabled": true, + "disabled": false, "groupID": null, "customName": "", "textRedirects": {}, @@ -951,7 +997,7 @@ "layer": 0, "visible": true, "spatial": { - "parentNodeId": "Trig_sub_D", + "parentNodeId": "Desk", "relation": "in" }, "x": 90.66834500947778, @@ -972,6 +1018,87 @@ "opacity": 0, "blendMode": "source-over", "blur": 0 + }, + { + "name": "window1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 56, + "y": -192, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 177.40625891347847, + "y": 204.99379689971948, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": 177.40625891347847, + "y": 204.99379689971948, + "p": 1.0124869315719702 + }, + { + "x": 273.3546785024226, + "y": 203.45494893614816, + "p": 1.0114617012261051 + }, + { + "x": 262.5537137492114, + "y": 249.54350071101484, + "p": 1.0415316998111208 + }, + { + "x": 212.71215117336098, + "y": 247.59187519264492, + "p": 1.0402645533908195 + } + ], + "color": "#000975", + "sortMode": "ignore", + "opacity": 0.6, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 7 } ], "camera": { @@ -979,7 +1106,7 @@ "y": 29, "zoom": 0.51 }, - "autoCenter": true, + "autoCenter": false, "cameraSpeed": 1.5, "camDeadzoneX": 200, "camDeadzoneY": -21, diff --git a/public/scenes/test_room.json b/public/scenes/test_room.json index bc3afb8..cf76365 100644 --- a/public/scenes/test_room.json +++ b/public/scenes/test_room.json @@ -1,127 +1,468 @@ { "id": "test_room", - "name": "New Scene", - "description": "You are in New Scene.", + "name": "Test Room", + "description": "You are in Test Room.", "textRedirects": {}, "filename": "test_room", - "walkbox": [], - "triggerboxes": [], - "scaling": { - "enabled": true, - "min": 0.4, - "max": 1, - "horizon": 78, - "front": 421 - }, - "entities": [ + "walkbox": [ { - "name": "Quad_201", - "type": "Quad", + "name": "Walk_997", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -79, + "y": 346 + }, + { + "x": -81, + "y": 272 + }, + { + "x": 153, + "y": 272 + }, + { + "x": 153, + "y": 346 + } + ], + "mode": "Add" + }, + { + "name": "Walk_176", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": 82, + "y": 192 + }, + { + "x": 407, + "y": 195 + }, + { + "x": 408, + "y": 203 + }, + { + "x": 470, + "y": 207 + }, + { + "x": 596, + "y": 220 + }, + { + "x": 624, + "y": 211 + }, + { + "x": 680, + "y": 211 + }, + { + "x": 680, + "y": 297 + }, + { + "x": -234, + "y": 291 + }, + { + "x": -210, + "y": 247 + }, + { + "x": -111, + "y": 249 + }, + { + "x": 77, + "y": 194 + } + ], + "mode": "Add" + } + ], + "triggerboxes": [ + { + "name": "Trig_sub_D", + "type": "Triggerbox", "locked": false, "disabled": false, - "groupID": "#g,#a", + "groupID": null, "customName": "", "textRedirects": {}, "interactions": {}, "components": [ { - "type": "3d-parallax" + "type": "Subscene", + "targetGroupId": "#D" } ], - "layer": -1, + "layer": 0, "visible": true, - "spatial": {}, - "x": 334.7566652973353, - "y": 147.45349435034427, - "parallax": 1, - "ignoreScaling": false, - "vertices": [ + "poly": [ { - "x": -1431.0063050419365, - "y": 419.98108487419046, - "p": 0.2 + "x": 27, + "y": 210 }, { - "x": 1588.9936949580635, - "y": 415.98108487419046, - "p": 0.2 + "x": 28, + "y": 104 }, { - "x": 3915.4050968588786, - "y": 457.44748933663504, - "p": 1 + "x": 84, + "y": 94 }, { - "x": -3349.5949031411214, - "y": 457.44748933663504, - "p": 1 + "x": 83, + "y": 136 + }, + { + "x": 54, + "y": 145 + }, + { + "x": 50, + "y": 160 + }, + { + "x": 76, + "y": 170 + }, + { + "x": 79, + "y": 194 } ], - "color": "#888888", - "sortMode": "ignore", - "opacity": 1, - "blendMode": "source-over", - "isGrid": true, - "gridLinesX": 29, - "gridLinesY": 7, - "lineWidth": 4, - "gridColor": "#0f6719", - "filled": false, - "blur": 0 + "script": "" }, { - "name": "Quad_1", - "type": "Quad", + "name": "sub_sw_d1", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D1", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ + { + "x": -156.29411764705887, + "y": -171.3529411764706 + }, + { + "x": 425, + "y": -171 + }, + { + "x": 414, + "y": -95 + }, + { + "x": -143, + "y": -95 + } + ], + "script": "" + }, + { + "name": "Trig_834", + "type": "Triggerbox", "locked": false, "disabled": false, - "groupID": "#a", + "groupID": null, "customName": "", "textRedirects": {}, "interactions": {}, "components": [], - "layer": -1, + "layer": 0, "visible": true, - "spatial": {}, - "x": 363.1753332849686, - "y": -1342.0347499102, - "parallax": 1, - "ignoreScaling": false, - "vertices": [ + "poly": [], + "script": "" + }, + { + "name": "sub_sw_d2", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": "#D ", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D2", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ { - "x": -1382.5263247292278, - "y": -605.2106827614945, - "p": 0.2 + "x": -156.29411764705878, + "y": -87.03921568627447 }, { - "x": 1692.4736752707722, - "y": -565.2106827614945, - "p": 0.2 + "x": 424.9999999999999, + "y": -86.68627450980387 }, { - "x": 3933.8237648465147, - "y": -1042.0407549239112, - "p": 1 + "x": 413.9999999999999, + "y": -10.68627450980393 }, { - "x": -3331.1762351534853, - "y": -1042.0407549239112, - "p": 1 + "x": -142.99999999999994, + "y": -10.68627450980393 } ], + "script": "" + }, + { + "name": "Desk", + "type": "Triggerbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -210, + "y": 211 + }, + { + "x": -192, + "y": 213 + }, + { + "x": -192, + "y": 197 + }, + { + "x": -171, + "y": 189 + }, + { + "x": -159, + "y": 211 + }, + { + "x": -136, + "y": 217 + }, + { + "x": -131, + "y": 242 + }, + { + "x": -109, + "y": 243 + }, + { + "x": -110, + "y": 146 + }, + { + "x": -18, + "y": 129 + }, + { + "x": -16, + "y": 201 + }, + { + "x": 26, + "y": 207 + }, + { + "x": 82, + "y": 193 + }, + { + "x": 87, + "y": 95 + }, + { + "x": -70, + "y": 78 + }, + { + "x": -236, + "y": 115 + }, + { + "x": -235, + "y": 173 + }, + { + "x": -218, + "y": 182 + }, + { + "x": -212, + "y": 211 + } + ], + "script": "" + } + ], + "scaling": { + "enabled": true, + "min": 0.91, + "max": 1, + "horizon": 193, + "front": 269 + }, + "entities": [ + { + "name": "CityView", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "spatial": { + "parentNodeId": "window1", + "relation": "in" + }, + "x": 119.67896209456934, + "y": 234, + "width": 821.6, + "height": 551.2, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "window-view.json", + "color": "#00ff00", + "scale": 0.65, + "modelScale": 0.65, + "parallax": 0.4, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "room", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "x": 199, + "y": 297, + "width": 884.8, + "height": 593.5999999999999, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "room2", "color": "#888888", - "sortMode": "ignore", + "scale": 0.7, + "modelScale": 0.7, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "Chair", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 100, + "y": 249, + "width": 116.89999999999999, + "height": 198.79999999999998, + "baseWidth": 167, + "baseHeight": 284, + "colliderWidth": 78, + "colliderHeight": 18, + "spriteName": "chair.json", + "color": "#00ff00", + "scale": 0.7, + "modelScale": 0.7, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, "opacity": 1, "blendMode": "source-over", - "isGrid": true, - "gridLinesX": 29, - "gridLinesY": 7, - "lineWidth": 4, - "gridColor": "#0f6719", - "filled": false, "blur": 0 }, { - "name": "miles_ds", + "name": "Hero_1", "type": "Actor", "locked": false, "disabled": false, @@ -133,34 +474,33 @@ { "type": "Shadow", "shadowQuadId": "shadow", - "offsetX": -80, - "offsetY": -0.79232867459731, - "triggerId": "#g" + "offsetX": -70, + "offsetY": -25, + "triggerId": "room" } ], "layer": 0, "visible": true, - "spatial": {}, - "x": 265.79289206085633, - "y": 449.8636667262623, - "width": 159.48352288795112, - "height": 384.9262805505487, + "x": 306.351468358703, + "y": 227.84447142883158, + "width": 119.88, + "height": 289.34, "baseWidth": 162, "baseHeight": 391, - "colliderWidth": 0, - "colliderHeight": 0, + "colliderWidth": 88, + "colliderHeight": 4, "spriteName": "miles_ds-idle-down.json", - "color": "#e27fa5", - "scale": 0.984466190666365, - "modelScale": 1.03, - "parallax": 0.8536873845960513, - "ignoreScaling": false, + "color": "#00ffff", + "scale": 0.74, + "modelScale": 0.74, + "parallax": 1.0273839007520702, + "ignoreScaling": true, "animationSpeed": 30, "opacity": 1, "blendMode": "source-over", "blur": 0, "isPlayer": true, - "speed": 0.35, + "speed": 0.24, "direction": "down", "animSets": { "idle": { @@ -180,7 +520,278 @@ } }, { - "name": "shadow", + "name": "Black_1", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -328, + "y": 307, + "width": 171.2340644206598, + "height": 637.1050459736764, + "baseWidth": 155.6673312915089, + "baseHeight": 579.1864054306149, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#000000", + "scale": 1.1, + "modelScale": 1.1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 135, + "y": 310, + "width": 614.4, + "height": 484.79999999999995, + "baseWidth": 1024, + "baseHeight": 808, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_main", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D2_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D2", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d2" + } + ], + "layer": 3, + "visible": true, + "x": 134, + "y": 311, + "width": 614.4, + "height": 407.4, + "baseWidth": 1024, + "baseHeight": 679, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d2.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 4, + "visible": true, + "x": 136, + "y": -4, + "width": 614.4, + "height": 177.6, + "baseWidth": 1024, + "baseHeight": 296, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_body.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "miles_id", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "your ID card", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item", + "ignoreDistance": true + } + ], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 93, + "y": 0, + "width": 150.4436263347707, + "height": 93.2343137254902, + "baseWidth": 165.32266630194582, + "baseHeight": 102.45528980823099, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_id.json", + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_stuff", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 320, + "y": 184, + "width": 979, + "height": 412, + "baseWidth": 979, + "baseHeight": 412, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_items.json", + "color": "#00ff00", + "scale": 1, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main_top", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 6, + "visible": true, + "x": 135, + "y": -176, + "width": 614.4, + "height": 129.6, + "baseWidth": 1024, + "baseHeight": 216, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_top.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_fasade", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d1" + } + ], + "layer": 6, + "visible": true, + "x": 135, + "y": 66, + "width": 614.4, + "height": 105.6, + "baseWidth": 1024, + "baseHeight": 176, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_facade.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "floor-parallax", "type": "Quad", "locked": false, "disabled": false, @@ -188,50 +799,88 @@ "customName": "", "textRedirects": {}, "interactions": {}, - "components": [], + "components": [ + { + "type": "3d-parallax" + } + ], "layer": 0, "visible": true, - "spatial": {}, - "x": 186.54416364247743, - "y": 449.7722685807698, + "x": -146.13725490196006, + "y": 289.60784313725503, "parallax": 1, "ignoreScaling": false, "vertices": [ { - "x": 186.54416364247743, - "y": 449.7722685807698, - "p": 0.8518262423080945 + "x": -205.9411764705876, + "y": 185.68627450980406, + "p": 1 }, { - "x": 297.6163127073407, - "y": 449.77439467798024, - "p": 0.8518934071256167 + "x": 708, + "y": 186, + "p": 1 }, { - "x": 300.5132416470328, - "y": 453.4957983159374, - "p": 0.9237611059473142 + "x": 766, + "y": 339, + "p": 1.1 }, { - "x": 195.7653597390785, - "y": 453.2940530119322, - "p": 0.919868888975613 + "x": -196, + "y": 339, + "p": 1.1 } ], - "color": "#000000", + "color": "#52779aff", "sortMode": "ignore", - "opacity": 1, - "blendMode": "multiply", + "opacity": 0, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", "filled": true, - "blur": 4 + "blur": 0 + }, + { + "name": "Actor_562", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 222.9043373174802, + "y": 306.9284515529539, + "width": 1008.8000000000001, + "height": 90.39999999999999, + "baseWidth": 1261, + "baseHeight": 112.99999999999999, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sofa", + "color": "#36d87fff", + "scale": 0.8, + "modelScale": 0.8, + "parallax": 1.0790382036293817, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": false, + "speed": 0.1, + "direction": "down", + "animSets": {} }, { - "name": "logo_1", + "name": "boombox", "type": "Entity", "locked": false, "disabled": false, @@ -242,28 +891,101 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, - "x": 91, - "y": 392, - "width": 706.7755102040817, - "height": 721.2734693877551, - "baseWidth": 585, - "baseHeight": 597, + "x": -152, + "y": -11, + "width": 112, + "height": 47, + "baseWidth": 123.07692307692307, + "baseHeight": 51.64835164835165, "colliderWidth": 0, "colliderHeight": 0, - "spriteName": "scanline_logo", + "spriteName": null, "color": "#AAAAAA", - "scale": 1.2081632653061225, - "modelScale": 2.8, - "parallax": 0.2, + "scale": 0.91, + "modelScale": 1, + "parallax": 1, "ignoreScaling": false, "animationSpeed": 150, - "opacity": 0.6, - "blendMode": "lighter", + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "test", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Chair", + "relation": "under" + }, + "x": 98, + "y": 243, + "width": 31.114048235454423, + "height": 35.128764136803376, + "baseWidth": 32.10246627605941, + "baseHeight": 36.24471998909933, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.9692105263157895, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "Static_649", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 15, + "visible": true, + "spatial": { + "parentNodeId": "Drawer3", + "relation": "in" + }, + "x": 138, + "y": 101, + "width": 492, + "height": 58, + "baseWidth": 540.6593406593406, + "baseHeight": 63.73626373626374, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#b83ed0", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", "blur": 0 }, { - "name": "logo_2", + "name": "Drawer1", "type": "Entity", "locked": false, "disabled": false, @@ -274,35 +996,120 @@ "components": [], "layer": 0, "visible": true, - "spatial": {}, - "x": 79, - "y": 417, - "width": 778.4081632653061, - "height": 794.3755102040816, - "baseWidth": 585, - "baseHeight": 597, + "spatial": { + "parentNodeId": "Desk", + "relation": "in" + }, + "x": 90.66834500947778, + "y": 120, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "window1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 56, + "y": -192, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, "colliderWidth": 0, "colliderHeight": 0, - "spriteName": "scanline_logo", - "color": "#00ca4c", - "scale": 1.3306122448979592, - "modelScale": 2.8, - "parallax": 0.2, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, "ignoreScaling": false, "animationSpeed": 150, "opacity": 1, - "blendMode": "lighter", - "blur": 32 + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 251.56334401080537, + "y": 208.4319364013629, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": 251.56334401080537, + "y": 208.4319364013629, + "p": 1.014733918039131 + }, + { + "x": 347.5117635997495, + "y": 206.89308843779156, + "p": 1.013708687693266 + }, + { + "x": 336.71084835449676, + "y": 252.9816450467687, + "p": 1.0437788529717444 + }, + { + "x": 286.86923627068785, + "y": 251.03001469428833, + "p": 1.0425115398579803 + } + ], + "color": "#000975", + "sortMode": "ignore", + "opacity": 0.6, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 7 } ], "camera": { - "x": 165.57094204403754, - "y": -263.4392820164094, - "zoom": 0.1500946352969992 + "x": 297, + "y": 29, + "zoom": 0.51 }, - "autoCenter": true, - "cameraSpeed": 5, - "camDeadzoneX": 50, - "camDeadzoneY": 30, - "camMaxY": -370 + "autoCenter": false, + "cameraSpeed": 1.5, + "camDeadzoneX": 200, + "camDeadzoneY": -21, + "camMinX": 143, + "camMaxY": 45 } diff --git a/public/text/objects/CityView.json b/public/text/objects/CityView.json new file mode 100644 index 0000000..aa81c71 --- /dev/null +++ b/public/text/objects/CityView.json @@ -0,0 +1,6 @@ +{ + "title": "City view", + "description": "You look out over San Vesper as it comes awake by slow degrees. In the distance, the towers of Civic Crown catch the first pale gold of dawn, their edges softened by bay fog and the city's lingering smog. \rFrom here, the skyline feels strangely remote: clean lines, cold light, a promise that belongs to someone else. Morning spills through the bare window glass without obstruction, and for a moment the city looks almost gentle, though never quite harmless.", + "details": "", + "synonyms": ["view", "landscape", "city"] +} diff --git a/public/text/objects/Desk.json b/public/text/objects/Desk.json new file mode 100644 index 0000000..807d07c --- /dev/null +++ b/public/text/objects/Desk.json @@ -0,0 +1,6 @@ +{ + "title": "Desk", + "description": "A large desk where the computer and everything connected to it reigns supreme. It is crowded in the way only a real working desk can be. The desk has three drawers.", + "details": "", + "synonyms": ["table", "workplace"] +} diff --git a/public/text/objects/DeskDrawer-1.json b/public/text/objects/DeskDrawer-1.json new file mode 100644 index 0000000..b559281 --- /dev/null +++ b/public/text/objects/DeskDrawer-1.json @@ -0,0 +1,6 @@ +{ + "title": "upper drawer", + "description": "Yoy see upper drawer of the desk", + "details": "", + "synonyms": ["drawer 1", "first drawer"] +} diff --git a/public/text/objects/Drawer1.json b/public/text/objects/Drawer1.json new file mode 100644 index 0000000..31bfa38 --- /dev/null +++ b/public/text/objects/Drawer1.json @@ -0,0 +1,6 @@ +{ + "title": "upper drawer", + "description": "", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/Trig_sub_D.json b/public/text/objects/Trig_sub_D.json new file mode 100644 index 0000000..59c2c02 --- /dev/null +++ b/public/text/objects/Trig_sub_D.json @@ -0,0 +1,6 @@ +{ + "title": "Desk drawers", + "description": "The desk have 3 drawers.", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/room.json b/public/text/objects/room.json new file mode 100644 index 0000000..da6fbc4 --- /dev/null +++ b/public/text/objects/room.json @@ -0,0 +1,6 @@ +{ + "title": "room", + "description": "You see nothing special.", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/test.json b/public/text/objects/test.json new file mode 100644 index 0000000..5d77306 --- /dev/null +++ b/public/text/objects/test.json @@ -0,0 +1,5 @@ +{ + "title": "Someone ID card", + "description": "You see someone's ID", + "synonyms": [] +} diff --git a/public/text/objects/window1.json b/public/text/objects/window1.json new file mode 100644 index 0000000..4f2f056 --- /dev/null +++ b/public/text/objects/window1.json @@ -0,0 +1,6 @@ +{ + "title": "Window", + "description": "The windows in this building are large, as was fashionable in the mid-century. They are rarely opened due to smog, noise, and the heavy steel frames that often jam.", + "details": "", + "synonyms": [] +} diff --git a/public/text/scenes/logo.json b/public/text/scenes/logo.json new file mode 100644 index 0000000..faa8b8b --- /dev/null +++ b/public/text/scenes/logo.json @@ -0,0 +1,4 @@ +{ + "title": "New Scene", + "description": "You are in New Scene." +} diff --git a/public/text/scenes/quad4.json b/public/text/scenes/quad4.json new file mode 100644 index 0000000..fa67f05 --- /dev/null +++ b/public/text/scenes/quad4.json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} \ No newline at end of file diff --git a/public/text/scenes/test_room (10).json b/public/text/scenes/test_room (10).json new file mode 100644 index 0000000..fa67f05 --- /dev/null +++ b/public/text/scenes/test_room (10).json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} \ No newline at end of file diff --git a/public/text/scenes/test_room.json b/public/text/scenes/test_room.json index faa8b8b..8d9cc7f 100644 --- a/public/text/scenes/test_room.json +++ b/public/text/scenes/test_room.json @@ -1,4 +1,4 @@ { - "title": "New Scene", - "description": "You are in New Scene." + "title": "Mile's Home", + "description": "You look toward the working corner of the apartment. The desk stands against the wall perpendicular to the window, with a shelf of books above it that makes the space feel close, personal, and lived in. It is cozy, but not tidy. There is the usual kind of programmer's clutter everywhere: cables coiled and tangled across the floor, Post-it notes and scraps of paper with old reminders, crumpled printouts of BASIC and assembler code, five-inch floppy disks, and the small debris of long hours spent thinking at a machine. Across the room, against the wall opposite the window, sits the sofa, set so you could watch the television or turn and look out at the city. The whole room feels warm, familiar, and comfortably disordered, like a place you genuinely likes to work in." } diff --git a/public/text/system/parser-lexicon.json b/public/text/system/parser-lexicon.json index 7b759df..609985a 100644 --- a/public/text/system/parser-lexicon.json +++ b/public/text/system/parser-lexicon.json @@ -50,7 +50,7 @@ "relationMarkers": { "on": ["on"], "under": ["under", "beneath"], - "in": ["in", "inside"], + "in": ["in", "inside", "into"], "behind": ["behind"], "near": ["near", "next to", "by"] } diff --git a/public/text/system/parser.json b/public/text/system/parser.json index c61a802..0613008 100644 --- a/public/text/system/parser.json +++ b/public/text/system/parser.json @@ -10,6 +10,7 @@ "examine_relation_prompt": "Examine what area?", "relation_empty": "You see nothing {relation} the {target}.", "relation_contents": "{Relation} the {target} you see: {items}.", + "relation_location": "It is {relation} the {target}.", "relation_not_supported": "You can't determine what is {relation} the {target} from here.", "take_prompt": "Take what?", "take_which_one": "Which item do you mean: {options}?", diff --git a/src/components/FileBrowser.tsx b/src/components/FileBrowser.tsx index d54627b..dd3157e 100644 --- a/src/components/FileBrowser.tsx +++ b/src/components/FileBrowser.tsx @@ -281,52 +281,12 @@ export const FileBrowser: React.FC = ({ }, [onCancel]); return ( -
-
-
-
+
+
+
+

{title || (mode === 'save' ? 'Save File' : 'Load File')}

- {currentPath} + {currentPath}
-
+
{isLoading &&
Loading...
} - {error &&
Error: {error}
} + {error &&
Error: {error}
} {!isLoading && !error && @@ -380,32 +327,20 @@ export const FileBrowser: React.FC = ({ if (item.isUp) handleUp(); else handleDoubleClick(item); }} - style={{ - padding: '5px', - cursor: 'pointer', - backgroundColor: isSelected ? 'var(--ui-selection-bg)' : 'transparent', - color: item.isDir - ? '#ffff00' - : isSelected - ? 'var(--ui-selection-text)' - : 'var(--ui-main-color)', - border: isSelected - ? '1px solid var(--ui-selection-bg)' - : '1px solid transparent', - }} + className={`file-browser-item ${item.isDir ? 'is-dir' : ''} ${isSelected ? 'is-selected' : ''}`} > {item.isDir ? '📁' : '📄'} {item.name}
); })} {!isLoading && displayItems.length === 0 && ( -
Directory is empty
+
Directory is empty
)}
-
- +
+ = ({ autoFocus />
-
- -
diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx index 0263c7c..5ade7a6 100644 --- a/src/components/GameCanvas.tsx +++ b/src/components/GameCanvas.tsx @@ -8,15 +8,16 @@ interface GameCanvasProps { export const GameCanvas: React.FC = ({ onGameInit }) => { const canvasRef = useRef(null); const uiCanvasRef = useRef(null); + const editorOverlayCanvasRef = useRef(null); const gameRef = useRef(null); const containerRef = useRef(null); useEffect(() => { - if (canvasRef.current && uiCanvasRef.current && !gameRef.current) { + if (canvasRef.current && uiCanvasRef.current && editorOverlayCanvasRef.current && !gameRef.current) { // Initialize Game with BOTH canvases // canvasRef -> WebGL (CRT) // uiCanvasRef -> 2D (UI/Input) - const game = new Game(canvasRef.current, uiCanvasRef.current); + const game = new Game(canvasRef.current, uiCanvasRef.current, editorOverlayCanvasRef.current); gameRef.current = game; // Start Game Loop @@ -44,6 +45,11 @@ export const GameCanvas: React.FC = ({ onGameInit }) => { canvasRef.current.width = clientWidth * dpr; canvasRef.current.height = clientHeight * dpr; + if (editorOverlayCanvasRef.current) { + editorOverlayCanvasRef.current.width = clientWidth * dpr; + editorOverlayCanvasRef.current.height = clientHeight * dpr; + } + // Notify game of resize gameRef.current.resize(canvasRef.current.width, canvasRef.current.height); } @@ -127,6 +133,22 @@ export const GameCanvas: React.FC = ({ onGameInit }) => { pointerEvents: 'auto', }} /> + +
); }; diff --git a/src/components/UIOverlay.tsx b/src/components/UIOverlay.tsx index f2d2aaf..8627245 100644 --- a/src/components/UIOverlay.tsx +++ b/src/components/UIOverlay.tsx @@ -17,6 +17,13 @@ export const UIOverlay: React.FC = ({ game }) => { onConfirm: (f: string) => void; extension?: string; title?: string; + onCancel?: () => void; + } | null>(null); + const [choiceDialog, setChoiceDialog] = useState<{ + title: string; + message: string; + options: Array<{ id: string; label: string; variant?: 'primary' | 'danger' | 'neutral' }>; + onResolve: (choiceId: string | null) => void; } | null>(null); // Console History State @@ -35,8 +42,11 @@ export const UIOverlay: React.FC = ({ game }) => { game.onMessage = (text) => setMessage({ id: Date.now(), text }); // Bind File Browser Request - game.onRequestFileBrowser = (mode, dir, onConfirm, extension, title) => { - setFileBrowser({ open: true, mode, dir, onConfirm, extension, title }); + game.onRequestFileBrowser = (mode, dir, onConfirm, extension, title, onCancel) => { + setFileBrowser({ open: true, mode, dir, onConfirm, extension, title, onCancel }); + }; + game.onRequestChoiceDialog = (title, dialogMessage, options, onResolve) => { + setChoiceDialog({ title, message: dialogMessage, options, onResolve }); }; // Initialize UI bindings @@ -62,6 +72,21 @@ export const UIOverlay: React.FC = ({ game }) => { } }, [game]); + useEffect(() => { + if (!game) return; + if (editorEnabled || isConsoleOpen) return; + + const timer = window.setTimeout(() => { + const input = parserInputRef.current; + if (!input || input.disabled) return; + input.focus(); + const len = input.value.length; + input.setSelectionRange(len, len); + }, 0); + + return () => window.clearTimeout(timer); + }, [game, editorEnabled, isConsoleOpen]); + useEffect(() => { if (message) { const timer = setTimeout(() => { @@ -78,6 +103,36 @@ export const UIOverlay: React.FC = ({ game }) => { } }; + const handleBrowserCancel = () => { + if (fileBrowser?.onCancel) { + fileBrowser.onCancel(); + } + setFileBrowser(null); + }; + + const handleChoiceResolve = React.useCallback((choiceId: string | null) => { + setChoiceDialog((currentDialog) => { + if (currentDialog) { + currentDialog.onResolve(choiceId); + } + return null; + }); + }, []); + + useEffect(() => { + if (!choiceDialog) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + handleChoiceResolve(null); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [choiceDialog, handleChoiceResolve]); + return ( <>
@@ -120,10 +175,10 @@ export const UIOverlay: React.FC = ({ game }) => { const preprocessed = game.console.preprocessGameplayInput(val); // 1. Log Command to Buffer - game.console.log(val, 'command'); + game.console.log(preprocessed, 'command'); // 2. Add to History - game.console.addHistory(val); + game.console.addHistory(preprocessed); // 3. Send to gameplay parser void game.parser.parse(preprocessed); @@ -205,6 +260,26 @@ export const UIOverlay: React.FC = ({ game }) => {
)} + {choiceDialog && ( +
+
+
{choiceDialog.title}
+
{choiceDialog.message}
+
+ {choiceDialog.options.map((option) => ( + + ))} +
+
+
+ )} + {/* Virtual Console Overlay (High Res, Open State) */} {game && } @@ -225,7 +300,7 @@ export const UIOverlay: React.FC = ({ game }) => { mode={fileBrowser.mode} directory={fileBrowser.dir} onConfirm={handleBrowserConfirm} - onCancel={() => setFileBrowser(null)} + onCancel={handleBrowserCancel} extension={fileBrowser.extension} title={fileBrowser.title} /> diff --git a/src/components/editor/EditorBottomMenu.tsx b/src/components/editor/EditorBottomMenu.tsx index 06b17f2..26f78bf 100644 --- a/src/components/editor/EditorBottomMenu.tsx +++ b/src/components/editor/EditorBottomMenu.tsx @@ -2,13 +2,31 @@ import React from 'react'; import { useGame } from '../../hooks/useGame'; import { useEditorStore } from '../../store/editorStore'; +type ModifierMode = 'base' | 'shift' | 'alt' | 'ctrl'; + +type MenuEntry = { + hotkey: string; + text: string; + action: string; + enabled?: boolean; +}; + export const EditorBottomMenu: React.FC = () => { const game = useGame(); - const { toggle, toggleSpriteEditor } = useEditorStore(); + const { + enabled: editorEnabled, + toggle, + toggleSpriteEditor, + selectedObjectId, + selectedObjectType, + selectedObjectKeys, + objectVersion, + } = useEditorStore(); const [fps, setFps] = React.useState(0); const [sceneMem, setSceneMem] = React.useState(0); const [sceneCount, setSceneCount] = React.useState(0); + const [modifierMode, setModifierMode] = React.useState('base'); React.useEffect(() => { const interval = setInterval(() => { @@ -20,6 +38,74 @@ export const EditorBottomMenu: React.FC = () => { return () => clearInterval(interval); }, [game]); + React.useEffect(() => { + const getModifierMode = (e?: KeyboardEvent): ModifierMode => { + const altGraph = !!e?.getModifierState?.('AltGraph') || e?.key === 'AltGraph'; + const ctrl = !!e?.ctrlKey; + const alt = !!e?.altKey; + const shift = !!e?.shiftKey; + if (altGraph) return 'alt'; + if (ctrl && !alt) return 'ctrl'; + if (alt) return 'alt'; + if (shift) return 'shift'; + return 'base'; + }; + + const syncModifierMode = (e?: KeyboardEvent) => { + setModifierMode(getModifierMode(e)); + }; + + const isMenuModifierKey = (e: KeyboardEvent) => + e.key === 'Alt' || e.key === 'AltGraph' || e.code === 'AltLeft' || e.code === 'AltRight'; + + const handleKeyDown = (e: KeyboardEvent) => { + if (editorEnabled && isMenuModifierKey(e)) { + e.preventDefault(); + } + syncModifierMode(e); + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (editorEnabled && isMenuModifierKey(e)) { + e.preventDefault(); + } + syncModifierMode(e); + }; + + const handleBlur = () => setModifierMode('base'); + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + }; + }, [editorEnabled]); + + const selectedObject = game.editor?.selectedObject || null; + const hasObjectSelection = + (selectedObjectType === 'MULTI' && + Array.isArray(selectedObjectKeys) && + selectedObjectKeys.length > 0) || + (!!selectedObject && selectedObjectType !== 'SCENE' && selectedObjectType !== 'SETTINGS'); + const hasSingleSelectedObject = + !!selectedObject && selectedObjectType !== 'SCENE' && selectedObjectType !== 'SETTINGS'; + void selectedObjectId; + void objectVersion; + + const lockActionText = hasSingleSelectedObject + ? selectedObject.locked + ? 'Unlock' + : 'Lock' + : 'Lock'; + const disableActionText = hasSingleSelectedObject + ? selectedObject.disabled + ? 'Enable' + : 'Disable' + : 'Disable'; + const handleAction = (action: string) => { const editor = game.editor; @@ -30,9 +116,15 @@ export const EditorBottomMenu: React.FC = () => { case 'F2': editor.saveScene(false); break; - case 'ShiftF2': + case 'SaveAs': editor.saveScene(true); break; + case 'SaveSelectionSlot1': + editor.saveSelectionSlot(0); + break; + case 'SaveSelectionSlot2': + editor.saveSelectionSlot(1); + break; case 'F3': editor.promptLoadScene(); break; @@ -45,25 +137,101 @@ export const EditorBottomMenu: React.FC = () => { case 'F9': editor.selectObject('SETTINGS'); break; + case 'Undo': + editor.undo(); + break; + case 'Redo': + editor.redo(); + break; + case 'Duplicate': + editor.duplicateSelectedObject(); + break; + case 'Copy': + editor.copySelectedObjectToClipboard(); + break; + case 'Paste': + void editor.pasteObjectFromClipboard(); + break; + case 'SaveSelection': + editor.persistenceManager.saveObject(); + break; + case 'LoadSelection': + editor.persistenceManager.loadObject('cursor'); + break; + case 'ToggleLock': + if (editor.selectedObject) { + editor.selectedObject.locked = !editor.selectedObject.locked; + useEditorStore.getState().incrementObjectVersion(); + useEditorStore.getState().incrementHierarchyVersion(); + } + break; + case 'ToggleDisabled': + if (editor.selectedObject) { + editor.selectedObject.disabled = !editor.selectedObject.disabled; + useEditorStore.getState().incrementObjectVersion(); + useEditorStore.getState().incrementHierarchyVersion(); + } + break; } }; - const keys = [ - { label: 'F1 Game', action: 'F1' }, - { label: 'F2 Save', action: 'F2' }, - { label: 'F3 Load', action: 'F3' }, - { label: 'F4 New', action: 'F4' }, - { label: 'F5 Sprite', action: 'F5' }, - { label: 'F9 Settings', action: 'F9' }, + const baseKeys: MenuEntry[] = [ + { hotkey: 'F1', text: 'Game', action: 'F1' }, + { hotkey: 'F2', text: 'Save', action: 'F2' }, + { hotkey: 'F3', text: 'Load', action: 'F3' }, + { hotkey: 'F4', text: 'New', action: 'F4' }, + { hotkey: 'F5', text: 'Sprite', action: 'F5' }, + { hotkey: 'F9', text: 'Settings', action: 'F9' }, ]; + const shiftKeys: MenuEntry[] = [ + { hotkey: 'F2', text: 'Save As', action: 'SaveAs' }, + { hotkey: '1', text: 'Save Sel. 1', action: 'SaveSelectionSlot1', enabled: hasObjectSelection }, + { hotkey: '2', text: 'Save Sel. 2', action: 'SaveSelectionSlot2', enabled: hasObjectSelection }, + ]; + + const altKeys: MenuEntry[] = [ + { hotkey: 'F2', text: 'Save As', action: 'SaveAs' }, + { hotkey: 'L', text: lockActionText, action: 'ToggleLock', enabled: hasSingleSelectedObject }, + { + hotkey: 'D', + text: disableActionText, + action: 'ToggleDisabled', + enabled: hasSingleSelectedObject, + }, + ]; + + const ctrlKeys: MenuEntry[] = [ + { hotkey: 'Z', text: 'Undo', action: 'Undo' }, + { hotkey: 'Y', text: 'Redo', action: 'Redo' }, + { hotkey: 'D', text: 'Duplicate', action: 'Duplicate', enabled: hasObjectSelection }, + { hotkey: 'C', text: 'Copy', action: 'Copy', enabled: hasObjectSelection }, + { hotkey: 'V', text: 'Paste', action: 'Paste', enabled: true }, + { hotkey: 'S', text: 'Save Prefab', action: 'SaveSelection', enabled: hasObjectSelection }, + { hotkey: 'O', text: 'Load Prefab', action: 'LoadSelection', enabled: true }, + ]; + + const keys = + modifierMode === 'ctrl' + ? ctrlKeys + : modifierMode === 'alt' + ? altKeys + : modifierMode === 'shift' + ? shiftKeys + : baseKeys; + return (
{`MEM ${sceneMem} | ${sceneCount}`}
{keys.map((k) => ( - ))}
FPS: {fps}
diff --git a/src/components/editor/HierarchyPanel.tsx b/src/components/editor/HierarchyPanel.tsx index 29f1a29..7e9a2ff 100644 --- a/src/components/editor/HierarchyPanel.tsx +++ b/src/components/editor/HierarchyPanel.tsx @@ -416,7 +416,7 @@ export const HierarchyPanel: React.FC = () => { > {icon} - {label} + {label}
{item.locked && 🔒}
diff --git a/src/components/editor/PropertiesPanel.tsx b/src/components/editor/PropertiesPanel.tsx index b177bdf..62c24bd 100644 --- a/src/components/editor/PropertiesPanel.tsx +++ b/src/components/editor/PropertiesPanel.tsx @@ -4,6 +4,140 @@ import { useGame } from '../../hooks/useGame'; import { Select } from '../../components/common/Select'; import { QuadObject } from '../../entities/QuadObject'; import { Entity } from '../../entities/Entity'; +import { Triggerbox } from '../../entities/Triggerbox'; + +const PROPERTIES_LABEL_TOOLTIPS: Record = { + 'Group #ID': + 'Comma-separated tags used to address this object or selection from triggers, switches, scripts, and subscenes.', + Parent: + 'Chooses the direct spatial parent of this object. The object will be treated as attached to that parent instead of the root scene.', + Relation: + 'Defines how this object is attached to its parent in the spatial hierarchy: in, on, under, or behind.', + 'Group X': + 'Moves the whole selected group horizontally while preserving the relative layout between the selected objects.', + 'Group Y': + 'Moves the whole selected group vertically while preserving the relative layout between the selected objects.', + 'Group Scale': + 'Scales the whole selected group around its shared center while keeping the objects aligned with each other.', + X: 'Horizontal position in scene space.', + Y: 'Vertical position in scene space.', + H: 'Visible height of the object rectangle.', + W: 'Visible width of the object rectangle.', + Scale: + 'Overall size multiplier. For polygon objects it scales the current shape around its center; for sprite objects it changes their model scale.', + Layer: + 'Render and interaction layer. Higher layers are treated as being in front of lower ones.', + Parallax: + 'Camera parallax factor. Values around 1 move with the scene, while lower or higher values create foreground or background depth drift.', + 'Collider H': + 'Collision height used for walkbox and obstacle interaction. Set to 0 to make the object non-blocking.', + 'Collider W': + 'Collision width used for walkbox and obstacle interaction. Set to 0 to make the object non-blocking.', + 'Disable Depth-scaling': + 'Keeps the object at a fixed visual size instead of letting the scene depth-scaling system resize it by Y position.', + 'Fill Color': + 'Base fill color for the object when no sprite is used, or the tint/fill color used by this visual mode.', + 'Blend Mode': + 'Canvas blend mode used to combine this object with the scene behind it.', + Opacity: + 'Visual transparency. 0% keeps the object fully opaque; 100% makes it invisible and excluded from rendering.', + Blur: 'Blur radius in pixels. 0 px is sharp; higher values make the object softer.', + Sprite: + 'Sprite asset used to render this object. Leave empty to keep the plain filled rectangle look.', + Mode: + 'Selects the behavior mode for this object or component.', + 'Depth Sort mode': + 'Chooses which quad rule is used for Y sorting, or disables Y sorting so layer order stays fully manual.', + 'Grid X': + 'Number of vertical subdivisions in the retro grid effect.', + 'Grid Y': + 'Number of horizontal subdivisions in the retro grid effect.', + Width: 'Line width or stroke width used by the current visual effect.', + 'Grid Color': 'Color used to draw the retro grid lines.', + ID: 'Unique identifier used by the engine, scripts, references, and file operations.', + 'ID/File': + 'Unique scene identifier and file path key. Slashes create subfolders when the scene is saved.', + Title: + 'Text-asset title shown to the player and used by the text layer as the friendly name for this object or scene.', + 'Key Item ID': + 'Inventory item ID required to unlock or activate this interaction.', + Description: 'Player-facing short description used by text interactions and subscene presentation.', + 'Target ID(s)': + 'One or more target group IDs or object IDs affected by this component or interaction.', + 'Target ID(s) (Optional)': + 'Optional target IDs affected by this component. Leave empty when the component should only provide auxiliary behavior.', + 'Target Trigger (Name/ID)': + 'Name or ID of the triggerbox that this helper area should activate as if it were clicked directly.', + 'Target(s) 1': + 'Targets used when the switch is in state 1, usually the closed or default state.', + 'Target(s) 2': + 'Targets used when the switch is in state 2, usually the open or alternate state.', + 'Sound 1': 'Sound played when the switch moves into state 1.', + 'Sound 2': 'Sound played when the switch moves into state 2.', + State: 'Current switch state used as the starting state in the editor and at runtime.', + 'Shadow Quad ID': + 'Quad that receives or shapes this shadow effect.', + 'Offset X': 'Horizontal offset applied by the component or effect.', + 'Offset Y': 'Vertical offset applied by the component or effect.', + 'Trigger ID(s) (Zone)': + 'Trigger IDs that enable, disable, or otherwise gate this component in specific zones.', + Axis: 'Axis constrained by the component or comparison rule.', + Op: 'Comparison operator used by the current component or condition.', + 'Culling Type': + 'Chooses how the object is culled or hidden when it falls outside the active visibility rule.', + 'Vert A (0-3)': 'First quad vertex index used by this link or rule.', + 'Vert B (0-3)': 'Second quad vertex index used by this link or rule.', + Direction: 'Default facing direction for the actor.', + 'Move Speed': 'Actor movement speed in scene units per step.', + 'Anim Speed (ms)': 'Frame duration for sprite animation playback, in milliseconds.', + 'Cam X': 'Current camera X position in scene space.', + 'Cam Y': 'Current camera Y position in scene space.', + Zoom: 'Current scene camera zoom.', + 'Auto-Center on Player': + 'Automatically keeps the camera centered on the player instead of relying on manual camera values.', + 'Cam Spd': 'Camera follow speed when auto-centering or camera tracking is active.', + 'Dead X': 'Horizontal deadzone before camera follow begins.', + 'Dead Y': 'Vertical deadzone before camera follow begins.', + 'Min X': 'Minimum allowed X value for this camera range.', + 'Max X': 'Maximum allowed X value for this camera range.', + 'Min Y': 'Minimum allowed Y value for this camera range.', + 'Max Y': 'Maximum allowed Y value for this camera range.', + 'Def X': 'Default camera X used when the scene loads or resets.', + 'Def Y': 'Default camera Y used when the scene loads or resets.', + 'Def Zoom': 'Default camera zoom used when the scene loads or resets.', + 'Enable Depth Scaling': + 'Turns scene depth scaling on or off so objects can grow or shrink according to their Y position.', + Min: 'Minimum depth-scaling factor used at the horizon end of the scene.', + Max: 'Maximum depth-scaling factor used at the front end of the scene.', + 'Horizon Y': 'Y coordinate treated as the horizon for depth scaling.', + 'Front Y': 'Y coordinate treated as the foreground limit for depth scaling.', + 'UI Scale': 'Editor interface scale multiplier.', + Curvature: 'Strength of the CRT screen curvature effect.', + Vignette: 'Darkening applied toward the screen edges.', + 'Scanline Count': 'Number of scanlines used by the CRT filter.', + 'Scanline Intensity': 'Visibility strength of the CRT scanlines.', + 'RGB Split': 'Amount of RGB channel separation in the CRT effect.', + Bloom: 'Glow intensity added by the CRT effect.', + 'Phosphor / Grain': 'Amount of phosphor persistence and grain noise.', + 'Enable CRT Filter': 'Turns the CRT post-processing effect on or off.', + 'Bezel Glow': 'Adds a glow around the virtual CRT bezel.', + 'Lock Object': 'Prevents accidental editing of this object in the editor. Hotkey: Alt-L.', + Disabled: 'Disables the object so it does not participate in the scene. Hotkey: Alt-D.', + 'Retro Grid': + 'Enables the retro grid line overlay for this quad. It is also useful for alignment, because objects can snap to grid nodes while Alt is held.', +}; + +const normalizeTooltipLabelText = (rawText: string): string => { + const text = rawText.replace(/\s+/g, ' ').trim(); + if (!text) return ''; + if (text.startsWith('Opacity')) return 'Opacity'; + if (text.startsWith('Blur')) return 'Blur'; + if (text.startsWith('UI Scale')) return 'UI Scale'; + if (text.startsWith('State')) return 'State'; + if (text.startsWith('Mode:')) return 'Mode'; + if (text === 'Disable Depth Scaling') return 'Disable Depth-scaling'; + return text; +}; export const PropertiesPanel: React.FC = () => { const game = useGame(); @@ -12,18 +146,31 @@ export const PropertiesPanel: React.FC = () => { selectedObjectType, incrementHierarchyVersion, incrementObjectVersion, + objectVersion, mode, selectedVertexIndex, } = useEditorStore(); const [groupIdDraft, setGroupIdDraft] = React.useState(''); + const [multiSpatialParentDraft, setMultiSpatialParentDraft] = React.useState(''); + const [multiSpatialRelationDraft, setMultiSpatialRelationDraft] = React.useState(''); const [resolvedTitle, setResolvedTitle] = React.useState(''); const [textAssetPath, setTextAssetPath] = React.useState(''); const [isReadingTA, setIsReadingTA] = React.useState(false); const [hasTextAsset, setHasTextAsset] = React.useState(false); + const [polygonScaleDraft, setPolygonScaleDraft] = React.useState('1'); + const lastUndoObjectKeyRef = React.useRef(null); + const lastUndoMultiKeyRef = React.useRef(null); + const lastPolygonScaleObjectKeyRef = React.useRef(null); + const polygonScaleSnapshotRef = React.useRef(null); + const panelRef = React.useRef(null); + const contentRef = React.useRef(null); + const sectionRefs = React.useRef>({}); + const isPanelHoveredRef = React.useRef(false); // Derived Object Binding (Source of Truth) - // We re-render whenever objectVersion changes (subscribed via store hook) + // We re-render whenever objectVersion changes. let obj: any = null; + void objectVersion; if (game) { if (selectedObjectId === 'SETTINGS') { obj = game.settings; @@ -56,22 +203,76 @@ export const PropertiesPanel: React.FC = () => { [spatialRelationOptions] ); + const getSpatialDescendantNames = React.useCallback((rootNames: string[]) => { + const scene = game?.sceneManager?.currentScene; + if (!scene || !rootNames.length) return new Set(); + + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; + const childrenByParent = new Map(); + + allObjects.forEach((item: any) => { + const parentId = typeof item?.spatial?.parentNodeId === 'string' ? item.spatial.parentNodeId.trim() : ''; + const name = typeof item?.name === 'string' ? item.name.trim() : ''; + if (!parentId || !name) return; + const children = childrenByParent.get(parentId) || []; + children.push(name); + childrenByParent.set(parentId, children); + }); + + const visited = new Set(); + const stack = [...rootNames.filter(Boolean)]; + while (stack.length) { + const current = stack.pop()!; + const children = childrenByParent.get(current) || []; + children.forEach((child) => { + if (visited.has(child)) return; + visited.add(child); + stack.push(child); + }); + } + + return visited; + }, [game]); + const getSceneSpatialParentOptions = React.useCallback(() => { const scene = game?.sceneManager?.currentScene; if (!scene || !obj) { return [{ value: '', label: '(None)' }]; } + const excludedNames = new Set([obj?.name].filter(Boolean)); + getSpatialDescendantNames([obj?.name]).forEach((name) => excludedNames.add(name)); + + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; + const options = allObjects + .filter((item: any) => item && !excludedNames.has(item.name)) + .map((item: any) => ({ + value: item.name, + label: item.customName?.trim() || item.name, + })); + + return [{ value: '', label: '(None)' }, ...options]; + }, [game, obj, getSpatialDescendantNames]); + + const getMultiSpatialParentOptions = React.useCallback(() => { + const scene = game?.sceneManager?.currentScene; + if (!scene || !multiObjects.length) { + return [{ value: '', label: '(None)' }]; + } + + const selectedNames = new Set(multiObjects.map((item: any) => item?.name).filter(Boolean)); + getSpatialDescendantNames(Array.from(selectedNames)).forEach((name) => selectedNames.add(name)); + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; const options = allObjects - .filter((item) => item !== obj) + .filter((item: any) => item && !selectedNames.has(item.name)) .map((item: any) => ({ value: item.name, label: item.customName?.trim() || item.name, })); return [{ value: '', label: '(None)' }, ...options]; - }, [game, obj]); + }, [game, multiObjects, getSpatialDescendantNames]); const getSharedValue = (arr: any[], getter: (o: any) => any) => { if (!arr.length) return ''; @@ -91,12 +292,271 @@ export const PropertiesPanel: React.FC = () => { return first ? 'on' : 'off'; }; + const formatPanelNumber = React.useCallback((value: any): number | string => { + if (value === '' || value === null || value === undefined) return ''; + const n = Number(value); + if (!Number.isFinite(n)) return value; + return Number(n.toFixed(3)); + }, []); + + const setSectionRef = React.useCallback( + (section: number) => (node: HTMLDivElement | null) => { + sectionRefs.current[section] = node; + }, + [] + ); + + const scrollToSection = React.useCallback((section: number) => { + const container = contentRef.current; + const node = sectionRefs.current[section]; + if (!container || !node) return; + const containerRect = container.getBoundingClientRect(); + const nodeRect = node.getBoundingClientRect(); + const targetTop = container.scrollTop + (nodeRect.top - containerRect.top) - 8; + container.scrollTo({ + top: Math.max(0, targetTop), + behavior: 'smooth', + }); + }, []); + + const isPanelTextEntryFocused = React.useCallback(() => { + const active = document.activeElement as HTMLElement | null; + if (!active || !panelRef.current || !panelRef.current.contains(active)) return false; + if (active.matches('input, textarea, select, [contenteditable="true"]')) return true; + if (active.closest('.custom-select-container')) return true; + return false; + }, []); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isPanelHoveredRef.current) return; + if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return; + if (isPanelTextEntryFocused()) return; + + const key = e.key; + if (!/^[0-6]$/.test(key)) return; + + e.preventDefault(); + scrollToSection(parseInt(key, 10)); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isPanelTextEntryFocused, scrollToSection]); + + React.useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + + const labels = panel.querySelectorAll('label.e-label'); + labels.forEach((node) => { + const label = node as HTMLLabelElement; + if (label.dataset.tooltipFixed === 'true') { + label.classList.add('e-tooltip-label'); + return; + } + + const normalized = normalizeTooltipLabelText(label.textContent || ''); + const tooltip = PROPERTIES_LABEL_TOOLTIPS[normalized]; + if (!tooltip) { + label.removeAttribute('title'); + label.classList.remove('e-tooltip-label'); + return; + } + + label.title = tooltip; + label.classList.add('e-tooltip-label'); + }); + }); + + const getPolyCentroid = React.useCallback((poly: { x: number; y: number }[] = []) => { + if (!poly.length) return { x: 0, y: 0 }; + const sum = poly.reduce((acc, pt) => ({ x: acc.x + pt.x, y: acc.y + pt.y }), { x: 0, y: 0 }); + return { x: sum.x / poly.length, y: sum.y / poly.length }; + }, []); + + const translatePolyTo = React.useCallback( + (targetX: number, targetY: number) => { + if (!obj?.poly?.length) return; + const centroid = getPolyCentroid(obj.poly); + const dx = targetX - centroid.x; + const dy = targetY - centroid.y; + obj.poly = obj.poly.map((pt: any) => ({ + x: Math.round(pt.x + dx), + y: Math.round(pt.y + dy), + })); + incrementObjectVersion(); + }, + [getPolyCentroid, incrementObjectVersion, obj] + ); + + const getQuadCentroid = React.useCallback((quad: any) => { + const verts = quad?.vertices || []; + if (!verts.length) return { x: quad?.x || 0, y: quad?.y || 0 }; + const sum = verts.reduce((acc: any, v: any) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 }); + return { x: sum.x / verts.length, y: sum.y / verts.length }; + }, []); + + const translateQuadTo = React.useCallback( + (targetX: number, targetY: number) => { + if (!obj?.vertices?.length) return; + const centroid = getQuadCentroid(obj); + const dx = targetX - centroid.x; + const dy = targetY - centroid.y; + obj.vertices = obj.vertices.map((v: any) => ({ + ...v, + x: v.x + dx, + y: v.y + dy, + })); + obj.x = targetX; + obj.y = targetY; + incrementObjectVersion(); + }, + [getQuadCentroid, incrementObjectVersion, obj] + ); + + const scalePolyByFactor = React.useCallback( + (poly: { x: number; y: number }[], factor: number, originX: number, originY: number) => + poly.map((pt) => ({ + x: Math.round(originX + (pt.x - originX) * factor), + y: Math.round(originY + (pt.y - originY) * factor), + })), + [] + ); + + const scaleQuadVerticesByFactor = React.useCallback( + (vertices: any[], factor: number, originX: number, originY: number) => + vertices.map((v) => ({ + ...v, + x: originX + (v.x - originX) * factor, + y: originY + (v.y - originY) * factor, + })), + [] + ); + + const applyPolygonScaleDraft = React.useCallback( + (nextScaleRaw: string) => { + if (!obj || !(selectedObjectType === 'Triggerbox' || selectedObjectType === 'Quad')) return; + const nextScale = parseFloat(nextScaleRaw); + if (!Number.isFinite(nextScale) || nextScale <= 0) return; + + const objectKey = `${selectedObjectType || 'Object'}:${obj.name || ''}`; + if (lastPolygonScaleObjectKeyRef.current !== objectKey) { + game?.editor?.saveUndoState(); + if (selectedObjectType === 'Quad' && obj.vertices?.length) { + polygonScaleSnapshotRef.current = { + key: objectKey, + kind: 'quad', + vertices: obj.vertices.map((v: any) => ({ ...v })), + }; + } else if (obj.poly?.length) { + polygonScaleSnapshotRef.current = { + key: objectKey, + kind: 'poly', + poly: obj.poly.map((pt: any) => ({ x: pt.x, y: pt.y })), + }; + } else { + polygonScaleSnapshotRef.current = null; + } + } + + const snapshot = polygonScaleSnapshotRef.current; + if (!snapshot || snapshot.key !== objectKey) { + setPolygonScaleDraft(nextScaleRaw); + lastPolygonScaleObjectKeyRef.current = objectKey; + return; + } + + if (snapshot.kind === 'quad' && selectedObjectType === 'Quad' && snapshot.vertices?.length) { + const sourceVertices = snapshot.vertices.map((v: any) => ({ ...v })); + const sourceCentroid = { + x: sourceVertices.reduce((acc: number, v: any) => acc + v.x, 0) / sourceVertices.length, + y: sourceVertices.reduce((acc: number, v: any) => acc + v.y, 0) / sourceVertices.length, + }; + obj.vertices = scaleQuadVerticesByFactor( + sourceVertices, + nextScale, + sourceCentroid.x, + sourceCentroid.y + ); + obj.x = Math.round( + obj.vertices.reduce((acc: number, v: any) => acc + v.x, 0) / obj.vertices.length + ); + obj.y = Math.round( + obj.vertices.reduce((acc: number, v: any) => acc + v.y, 0) / obj.vertices.length + ); + } else if (snapshot.kind === 'poly' && snapshot.poly?.length) { + const sourcePoly = snapshot.poly.map((pt: any) => ({ x: pt.x, y: pt.y })); + const sourceCentroid = getPolyCentroid(sourcePoly); + obj.poly = scalePolyByFactor(sourcePoly, nextScale, sourceCentroid.x, sourceCentroid.y); + } + + setPolygonScaleDraft(nextScaleRaw); + lastPolygonScaleObjectKeyRef.current = objectKey; + incrementObjectVersion(); + }, + [ + game, + getPolyCentroid, + getQuadCentroid, + incrementObjectVersion, + obj, + scalePolyByFactor, + scaleQuadVerticesByFactor, + selectedObjectType, + ] + ); + const applyToMulti = (fn: (o: any) => void) => { + const multiKey = `MULTI:${multiObjects.map((item: any) => item?.name || '').filter(Boolean).join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } multiObjects.forEach(fn); incrementObjectVersion(); incrementHierarchyVersion(); }; + const applyToMultiRoots = (fn: (o: any) => void) => { + const multiKey = `MULTI:${multiObjects.map((item: any) => item?.name || '').filter(Boolean).join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const selectedNames = new Set(multiObjects.map((item: any) => item?.name).filter(Boolean)); + multiObjects.forEach((o: any) => { + const parentId = typeof o?.spatial?.parentNodeId === 'string' ? o.spatial.parentNodeId.trim() : ''; + if (parentId && selectedNames.has(parentId)) return; + fn(o); + }); + incrementObjectVersion(); + incrementHierarchyVersion(); + }; + + React.useEffect(() => { + if (selectedObjectType !== 'MULTI' || multiObjects.length <= 1) { + setMultiSpatialParentDraft(''); + setMultiSpatialRelationDraft(''); + return; + } + + const sharedParent = getSharedValue(multiObjects, (o: any) => o.spatial?.parentNodeId || ''); + const sharedRelation = getSharedValue( + multiObjects, + (o: any) => (o.spatial?.parentNodeId ? o.spatial?.relation || 'in' : o.spatial?.relation || '') + ); + + setMultiSpatialParentDraft(sharedParent === '' ? '' : sharedParent); + setMultiSpatialRelationDraft(sharedRelation === '' ? '' : sharedRelation); + }, [selectedObjectType, selectedObjectId, objectVersion, multiObjects.length]); + + React.useEffect(() => { + setPolygonScaleDraft('1'); + lastPolygonScaleObjectKeyRef.current = null; + polygonScaleSnapshotRef.current = null; + }, [selectedObjectType, selectedObjectId]); + const loadResolvedTitle = React.useCallback( async (forceReload: boolean = false) => { if (!game || !obj || selectedObjectType === 'MULTI' || selectedObjectType === 'SETTINGS') { @@ -224,7 +684,74 @@ export const PropertiesPanel: React.FC = () => { } }; + const renderSection = ( + section: number, + title: string | null, + color: 'blue' | 'red' | 'yellow' | 'purple' | 'neutral', + children: React.ReactNode + ) => ( +
+ {title !== null && ( +
+
+ {section} + {title} +
+
+ )} + {children} +
+ ); + + const renderOpacityBlurControls = ( + opacityValue: number | '', + blurValue: number | '', + onOpacityChange: (nextOpacity: number) => void, + onBlurChange: (nextBlur: number) => void + ) => { + const normalizedOpacity = opacityValue === '' ? 1 : Number(opacityValue); + const normalizedBlur = blurValue === '' ? 0 : Number(blurValue); + const opacityUi = Math.round((1 - normalizedOpacity) * 100); + const blurUi = Math.max(0, Math.min(50, Math.round(normalizedBlur))); + + return ( +
+
+ + onOpacityChange(1 - parseInt(e.target.value, 10) / 100)} + /> +
+
+ + onBlurChange(parseInt(e.target.value, 10))} + /> +
+
+ ); + }; + React.useEffect(() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; if (selectedObjectType !== 'MULTI') { setGroupIdDraft(''); } @@ -233,20 +760,27 @@ export const PropertiesPanel: React.FC = () => { if (!obj || !game) { return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} + onBlurCapture={() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; + }} style={{ fontSize: `${12 * uiScale}px` }} >
{selectedObjectId === 'SETTINGS' ? 'SETTINGS (Loading...)' : 'PROPERTIES'}
-
+
{selectedObjectId === 'SETTINGS' ? 'Loading Settings...' : 'No Selection'}
@@ -256,9 +790,12 @@ export const PropertiesPanel: React.FC = () => { if (selectedObjectType === 'MULTI' && multiObjects.length > 1) { const group = game.editor.selectionManager.getGroupTransform(); const entitiesAndQuads = multiObjects.filter((o: any) => o instanceof Entity); + const parallaxObjects = multiObjects.filter( + (o: any) => o instanceof Entity || o instanceof Triggerbox || (o as any).type === 'Quad' + ); const quads = multiObjects.filter((o: any) => (o as any).type === 'Quad'); const sharedLayer = getSharedValue(multiObjects, (o) => o.layer ?? 0); - const sharedParallax = getSharedValue(entitiesAndQuads, (o: any) => o.parallax ?? 1); + const sharedParallax = getSharedValue(parallaxObjects, (o: any) => o.parallax ?? 1); const sharedBlendMode = getSharedValue( entitiesAndQuads, (o: any) => o.blendMode || 'source-over' @@ -278,19 +815,27 @@ export const PropertiesPanel: React.FC = () => { entitiesAndQuads, (o: any) => !!o.ignoreScaling ); + const sharedParentNodeId = getSharedValue(multiObjects, (o: any) => o.spatial?.parentNodeId || ''); const sharedLocked = getSharedBooleanState(multiObjects, (o: any) => !!o.locked); const sharedDisabled = getSharedBooleanState(multiObjects, (o: any) => !!o.disabled); return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} + onBlurCapture={() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; + }} style={{ fontSize: `${12 * uiScale}px` }} >
@@ -299,462 +844,545 @@ export const PropertiesPanel: React.FC = () => { X
-
-
- -
- (<Enter> = append, <Ctrl+Enter> = remove) -
- setGroupIdDraft(e.target.value)} - onKeyDown={(e) => { - if (e.key !== 'Enter') return; - e.preventDefault(); - const raw = groupIdDraft.trim(); - if (!raw) return; - const prepared = raw - .split(',') - .map((x) => x.trim()) - .filter(Boolean) - .map((x) => (x.startsWith('#') ? x : `#${x}`)); - - let changedCount = 0; - multiObjects.forEach((o: any) => { - const existing = (o.groupID || '') - .split(',') - .map((x: string) => x.trim()) - .filter(Boolean); - - if (e.ctrlKey) { - const filtered = existing.filter((x: string) => !prepared.includes(x)); - if (filtered.length !== existing.length) { - changedCount++; - } - o.groupID = filtered.join(','); - return; - } - - const merged = [...new Set([...existing, ...prepared])]; - if (merged.length !== existing.length) { - changedCount++; - } - o.groupID = merged.join(','); - }); - - if (changedCount > 0) { - incrementObjectVersion(); - incrementHierarchyVersion(); - const tagsText = prepared.join(', '); - if (e.ctrlKey) { - game.showNotification( - `Removed ${tagsText} from ${changedCount} object${changedCount === 1 ? '' : 's'}` - ); - } else { - game.showNotification( - `Appended ${tagsText} to ${changedCount} object${changedCount === 1 ? '' : 's'}` - ); - } - } - setGroupIdDraft(''); - }} - /> -
- -
-
- - { - const x = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - isNaN(x) ? 0 : x, - group.offsetY, - group.scale - ); - incrementObjectVersion(); - }} - /> -
-
- - { - const y = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - group.offsetX, - isNaN(y) ? 0 : y, - group.scale - ); - incrementObjectVersion(); - }} - /> -
-
- -
- - { - const s = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - group.offsetX, - group.offsetY, - isNaN(s) ? 1 : s - ); - incrementObjectVersion(); - }} - /> -
- -
-
- - { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o) => { - o.layer = v; - }); - }} - /> -
- {entitiesAndQuads.length > 0 ? ( -
- +
+ {renderSection( + 0, + null, + 'neutral', + <> +
+ +
+ (<Enter> = append, <Ctrl+Enter> = remove) +
{ - const v = parseFloat(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.parallax = v; - }); - }} - /> -
- ) : ( -
- )} -
+ value={groupIdDraft} + placeholder="#group" + onChange={(e) => setGroupIdDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + const raw = groupIdDraft.trim(); + if (!raw) return; + const prepared = raw + .split(',') + .map((x) => x.trim()) + .filter(Boolean) + .map((x) => (x.startsWith('#') ? x : `#${x}`)); + + let changedCount = 0; + multiObjects.forEach((o: any) => { + const existing = (o.groupID || '') + .split(',') + .map((x: string) => x.trim()) + .filter(Boolean); + + if (e.ctrlKey) { + const filtered = existing.filter((x: string) => !prepared.includes(x)); + if (filtered.length !== existing.length) changedCount++; + o.groupID = filtered.join(','); + return; + } - {entitiesAndQuads.length > 0 && ( -
-
- - { - const v = parseFloat(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.opacity = v; + const merged = [...new Set([...existing, ...prepared])]; + if (merged.length !== existing.length) changedCount++; + o.groupID = merged.join(','); }); + + if (changedCount > 0) { + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + incrementObjectVersion(); + incrementHierarchyVersion(); + const tagsText = prepared.join(', '); + if (e.ctrlKey) { + game.showNotification( + `Removed ${tagsText} from ${changedCount} object${changedCount === 1 ? '' : 's'}` + ); + } else { + game.showNotification( + `Appended ${tagsText} to ${changedCount} object${changedCount === 1 ? '' : 's'}` + ); + } + } + setGroupIdDraft(''); }} />
-
- - { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.blur = v; + +
+ + { - const v = e.target.value; - applyToMulti((o: any) => { - if (o.color !== undefined) o.color = v; - }); - }} - /> - { - const v = e.target.value; - applyToMulti((o: any) => { - if (o.color !== undefined) o.color = v; - }); - }} - /> -
-
-
-
- + {sharedParentNodeId && ( +
+ { - if (el) el.indeterminate = sharedIsGrid === 'mixed'; - }} - onChange={(e) => { - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).isGrid = e.target.checked; - }); - }} - /> - Retro Grid - -
+ )} + )} - {quads.length > 0 && sharedIsGrid !== 'off' && ( + {renderSection( + 1, + 'Transform', + 'blue', <>
- + { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).gridLinesX = v; - }); + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const x = parseFloat(e.target.value); + game.editor.selectionManager.applyGroupTransform( + isNaN(x) ? 0 : x, + group.offsetY, + group.scale + ); + incrementObjectVersion(); }} - min={1} - max={50} />
- + { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).gridLinesY = v; - }); + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const y = parseFloat(e.target.value); + game.editor.selectionManager.applyGroupTransform( + group.offsetX, + isNaN(y) ? 0 : y, + group.scale + ); + incrementObjectVersion(); + }} + /> +
+
+ +
+
+ + { + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const s = parseFloat(e.target.value); + if (isNaN(s) || s <= 0) return; + game.editor.selectionManager.applyGroupTransform(group.offsetX, group.offsetY, s); + incrementObjectVersion(); }} - min={1} - max={50} />
- + { - const v = parseFloat(e.target.value); + const v = parseInt(e.target.value); if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).lineWidth = v; + applyToMulti((o) => { + o.layer = v; }); }} - step={0.1} - min={0.1} - max={10} />
+
+ {parallaxObjects.length > 0 ? ( + <> + + { + const v = parseFloat(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if (o instanceof Entity || o instanceof Triggerbox || (o as any).type === 'Quad') { + o.parallax = v; + } + }); + }} + /> + + ) : ( +
+ )} +
-
- -
+ + {entitiesAndQuads.length > 0 && ( +
+ +
+ )} + + )} + + {renderSection( + 2, + 'Visual', + 'yellow', + <> + {entitiesAndQuads.length > 0 && ( + renderOpacityBlurControls( + sharedOpacity, + sharedBlur, + (nextOpacity) => { + applyToMulti((o: any) => { + if (o instanceof Entity) o.opacity = nextOpacity; + }); + }, + (nextBlur) => { + applyToMulti((o: any) => { + if (o instanceof Entity) o.blur = nextBlur; + }); + } + ) + )} + + {entitiesAndQuads.length > 0 && ( +
+
+ +
+ { + const v = e.target.value; + applyToMulti((o: any) => { + if (o.color !== undefined) o.color = v; + }); + }} + /> + { + const v = e.target.value; + applyToMulti((o: any) => { + if (o.color !== undefined) o.color = v; + }); + }} + /> +
+
+
+
+ + { + if (el) el.indeterminate = sharedIsGrid === 'mixed'; + }} + onChange={(e) => { + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).isGrid = e.target.checked; + }); + }} + /> + Retro Grid + +
+ )} + + {quads.length > 0 && sharedIsGrid !== 'off' && ( + <> +
+
+ + { + const v = parseInt(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridLinesX = v; + }); + }} + min={1} + max={50} + /> +
+
+ + { + const v = parseInt(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridLinesY = v; + }); + }} + min={1} + max={50} + /> +
+
+ + { + const v = parseFloat(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).lineWidth = v; + }); + }} + step={0.1} + min={0.1} + max={10} + /> +
+
+
+ +
+ { + const v = e.target.value; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridColor = v; + }); + }} + /> + { + const v = e.target.value; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridColor = v; + }); + }} + /> +
+
+ + )} + + )} + + {renderSection( + 6, + null, + 'neutral', + <> +
+ + +
+ Disabled +
)} - -
-
- -
- -
- -
- -
); @@ -763,6 +1391,16 @@ export const PropertiesPanel: React.FC = () => { const handleChange = (field: string, value: any, enforceNumber = false) => { if (!obj) return; + if (selectedObjectType !== 'SETTINGS' && game?.editor) { + const objectKey = selectedObjectType === 'SCENE' + ? `SCENE:${obj.id || ''}` + : `${selectedObjectType || 'Object'}:${obj.name || ''}`; + if (lastUndoObjectKeyRef.current !== objectKey) { + game.editor.saveUndoState(); + lastUndoObjectKeyRef.current = objectKey; + } + } + let finalVal = value; if (enforceNumber) { finalVal = parseFloat(value); @@ -831,14 +1469,25 @@ export const PropertiesPanel: React.FC = () => { } }; + const isEntityLike = + selectedObjectType === 'Entity' || selectedObjectType === 'Actor' || selectedObjectType === 'Static'; + const isTriggerbox = selectedObjectType === 'Triggerbox'; + const isWalkbox = selectedObjectType === 'Walkbox'; + const isScene = selectedObjectType === 'SCENE'; + const isSettings = selectedObjectType === 'SETTINGS'; + const isObjectWithScriptEvents = !isSettings && !isScene && !isWalkbox && selectedObjectType !== 'MULTI'; + return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} style={{ fontSize: `${12 * uiScale}px` }} @@ -852,865 +1501,584 @@ export const PropertiesPanel: React.FC = () => {
-
- {selectedObjectType !== 'SETTINGS' && ( - <> - {/* Common: Name -> ID */} -
- - { - // Local update only - const val = e.target.value; - if (selectedObjectType === 'SCENE') obj.id = val; - else obj.name = val; - incrementObjectVersion(); - }} - onBlur={(e) => { - // Commit with Validation - const rawVal = e.target.value; - const finalVal = rawVal.trim(); - const field = selectedObjectType === 'SCENE' ? 'id' : 'name'; - - // Validation (Only for Name/ID) - let isValid = true; - const scene = game?.sceneManager?.currentScene; - - if (selectedObjectType !== 'SCENE' && scene) { - // Check duplicates - // Check Entities - const dupEntity = scene.entities.find( - (ent) => ent.name === finalVal && ent !== game?.editor?.selectedObject - ); - // Check Triggerboxes - const dupTrigger = scene.triggerboxes - ? scene.triggerboxes.find( - (tb) => tb.name === finalVal && tb !== game?.editor?.selectedObject - ) - : null; - - if (dupEntity || dupTrigger) { - console.warn(`[PropertiesPanel] Duplicate Name '${finalVal}' rejected.`); - // @ts-ignore - if (game.showMessage) game.showMessage(`Name '${finalVal}' already exists!`); - isValid = false; - } - } - - if (isValid) { - handleChange(field, finalVal); - } else { - // Revert to original from real object - let realObj: any = null; - if (game?.editor) realObj = game.editor.selectedObject; - - if (realObj) { - if (selectedObjectType === 'SCENE') obj.id = realObj.id; - else obj.name = realObj.name; - incrementObjectVersion(); - } - } - }} - /> -
- {supportsTextAsset && ( +
+ {!isSettings && + renderSection( + 0, + null, + 'neutral', + <>
- + e.currentTarget.blur()} - style={{ pointerEvents: 'none', color: '#888' }} - /> - {textAssetPath && ( - <> -
- - - {hasTextAsset && ( - - )} -
-
- {textAssetPath} -
- - )} -
- )} - - )} + value={isScene ? obj.id || '' : obj.name || ''} + onChange={(e) => { + const val = e.target.value; + if (isScene) obj.id = val; + else obj.name = val; + incrementObjectVersion(); + }} + onBlur={(e) => { + const rawVal = e.target.value; + const finalVal = rawVal.trim(); + const field = isScene ? 'id' : 'name'; + + let isValid = true; + const scene = game?.sceneManager?.currentScene; + + if (!isScene && scene) { + const dupEntity = scene.entities.find( + (ent) => ent.name === finalVal && ent !== game?.editor?.selectedObject + ); + const dupTrigger = scene.triggerboxes + ? scene.triggerboxes.find( + (tb) => tb.name === finalVal && tb !== game?.editor?.selectedObject + ) + : null; + + if (dupEntity || dupTrigger) { + console.warn(`[PropertiesPanel] Duplicate Name '${finalVal}' rejected.`); + // @ts-ignore + if (game.showMessage) game.showMessage(`Name '${finalVal}' already exists!`); + isValid = false; + } + } - {selectedObjectType !== 'SETTINGS' && selectedObjectType !== 'SCENE' && ( -
- - { - const val = e.target.value; - // Auto-format: Ensure every token starts with # - // 1. Split by comma - const tokens = val.split(','); - const newTokens = tokens.map((t) => { - // Don't auto-add to the very last token if it's empty (user just typed comma) - if (t.length === 0) return ''; - - let clean = t; - // If this is a new char entry (not just backspace), check prefix - const trimmed = t.trimStart(); - if (trimmed.length > 0 && !trimmed.startsWith('#')) { - // Find where the white space ends to insert # - const firstCharIdx = t.length - trimmed.length; - clean = t.substring(0, firstCharIdx) + '#' + trimmed; - } - return clean; - }); + if (isValid) { + handleChange(field, finalVal); + } else { + let realObj: any = null; + if (game?.editor) realObj = game.editor.selectedObject; - handleChange('groupID', newTokens.join(',')); - }} - /> -
- )} + if (realObj) { + if (isScene) obj.id = realObj.id; + else obj.name = realObj.name; + incrementObjectVersion(); + } + } + }} + /> +
- {/* Entity Properties (Static, Actor, Entity) - Moved & Compacted */} - {(selectedObjectType === 'Entity' || - selectedObjectType === 'Actor' || - selectedObjectType === 'Static') && ( - <> - {/* Transform: X, Y, W, H */} -
-
- - handleChange('x', e.target.value, true)} - /> -
-
- - handleChange('y', e.target.value, true)} - /> -
-
- - handleChange('width', e.target.value, true)} - /> -
-
- - handleChange('height', e.target.value, true)} - /> -
-
- - {/* Scale, Layer, Parallax */} -
-
- - handleChange('modelScale', e.target.value, true)} - /> -
-
- - handleChange('layer', e.target.value, true)} - /> -
-
- - { - const val = parseFloat(e.target.value); - const newP = isNaN(val) ? 1.0 : val; - const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; - - // Auto-Correct Position to prevent visual jump - // NewPos = OldPos + Cam * (NewP - OldP) - const scene = game.sceneManager.currentScene; - if (scene && game.editor.selectedObject) { - const camX = scene.camera.x; - const camY = scene.camera.y; - - const dx = camX * (newP - oldP); - const dy = camY * (newP - oldP); - - // Apply to Local - obj.x += dx; - obj.y += dy; - - // Apply to Real (Must do this manually as handleChange only does the targeting field) - if ( - game.editor && - game.editor.selectedObject && - 'x' in game.editor.selectedObject - ) { - (game.editor.selectedObject as any).x = obj.x; - (game.editor.selectedObject as any).y = obj.y; - } - } - - handleChange('parallax', newP, true); - }} - /> -
-
- - {(selectedObjectType === 'Entity' || - selectedObjectType === 'Actor' || - selectedObjectType === 'Static') && ( -
-
- - { - obj.spatial = { - ...(obj.spatial || {}), - parentNodeId: obj.spatial?.parentNodeId || null, - relation: value || (obj.spatial?.parentNodeId ? 'in' : null), - }; - incrementObjectVersion(); - }} - options={getSpatialRelationOptions(!!obj.spatial?.parentNodeId)} - style={{ width: '100%' }} - /> -
-
- )} - - {/* Color & Blend Mode */} -
-
- -
- handleChange('color', e.target.value)} - /> - handleChange('color', e.target.value)} - /> -
-
-
- - handleChange('opacity', e.target.value, true)} - /> -
-
- - handleChange('blur', 50 - parseInt(e.target.value))} - /> -
-
- - {/* Sprite */} -
- -
- handleChange('spriteName', e.target.value)} - /> - -
-
- - {/* Colliders + Flags */} -
-
- - handleChange('colliderWidth', e.target.value, true)} - /> -
-
- - handleChange('colliderHeight', e.target.value, true)} - /> -
-
- -
- -
- -
- -
-
- -
- - {/* Interactions */} -
-
- SCRIPT EVENTS - { - obj.interactions[verb] = e.target.value; - // Sync to real object - if (game.editor.selectedObject) { - (game.editor.selectedObject as any).interactions[verb] = e.target.value; - } - incrementObjectVersion(); - }} - /> - -
- ))} -
- - )} - - {/* Walkbox/Triggerbox Properties */} - {(selectedObjectType === 'Walkbox' || selectedObjectType === 'Triggerbox') && ( -
- {selectedObjectType === 'Walkbox' && ( -
- - e.currentTarget.blur()} + style={{ pointerEvents: 'none' }} + /> + {textAssetPath && ( + <> +
+ + + {hasTextAsset && ( + + )} +
+
{textAssetPath}
+ + )} +
+ )} - {selectedObjectType === 'Triggerbox' && ( - <> + {!isScene && !isSettings && (
- + handleChange('layer', e.target.value, true)} + value={obj.groupID || ''} + onChange={(e) => { + const val = e.target.value; + const tokens = val.split(','); + const newTokens = tokens.map((t) => { + if (t.length === 0) return ''; + + let clean = t; + const trimmed = t.trimStart(); + if (trimmed.length > 0 && !trimmed.startsWith('#')) { + const firstCharIdx = t.length - trimmed.length; + clean = t.substring(0, firstCharIdx) + '#' + trimmed; + } + return clean; + }); + + handleChange('groupID', newTokens.join(',')); + }} />
+ )} + + {!isScene && !isSettings && !isWalkbox && (
{ + game.editor.saveUndoState(); + obj.spatial = { + ...(obj.spatial || {}), + parentNodeId: obj.spatial?.parentNodeId || null, + relation: value || (obj.spatial?.parentNodeId ? 'in' : null), + }; + incrementObjectVersion(); + incrementHierarchyVersion(); + }} + options={getSpatialRelationOptions(true)} + style={{ width: '100%' }} + /> +
+ )} +
+ )} + + )} + + {isEntityLike && ( + <> + {renderSection( + 1, + 'Transform', + 'blue', + <> +
+
+ + handleChange('x', e.target.value, true)} + /> +
- - handleChange('y', e.target.value, true)} + /> +
+
+ + handleChange('height', e.target.value, true)} + /> +
+
+ + handleChange('width', e.target.value, true)} + /> +
+
+ +
+
+ + handleChange('modelScale', e.target.value, true)} + /> +
+
+ + handleChange('layer', e.target.value, true)} + /> +
+
+ + { + const val = parseFloat(e.target.value); + const newP = isNaN(val) ? 1.0 : val; + const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; + const scene = game.sceneManager.currentScene; + if (scene && game.editor.selectedObject) { + const camX = scene.camera.x; + const camY = scene.camera.y; + obj.x += camX * (newP - oldP); + obj.y += camY * (newP - oldP); + if (game.editor && game.editor.selectedObject && 'x' in game.editor.selectedObject) { + (game.editor.selectedObject as any).x = obj.x; + (game.editor.selectedObject as any).y = obj.y; + } + } + handleChange('parallax', newP, true); }} - options={getSpatialRelationOptions(!!obj.spatial?.parentNodeId)} + /> +
+
+ +
+
+ + handleChange('colliderHeight', e.target.value, true)} + /> +
+
+ + handleChange('colliderWidth', e.target.value, true)} + /> +
+
+ +
+ +
+ + )} + + {renderSection( + 2, + 'Visual', + 'yellow', + <> +
+
+ +
+ handleChange('color', e.target.value)} + /> + handleChange('color', e.target.value)} + /> +
+
+
+ + handleChange('spriteName', e.target.value)} + /> + +
+
)} -
- -
-
- -
-
+ )} - {/* Quad Properties */} - {selectedObjectType === 'Quad' && ( + {selectedObjectType === 'Walkbox' && (
- {/* Layer */}
- - handleChange('layer', e.target.value, true)} - /> -
- - {/* Opacity / Blur */} -
-
- - handleChange('opacity', e.target.value, true)} - /> -
-
- - handleChange('blur', 50 - parseInt(e.target.value))} - /> -
-
- - {/* Fill Color */} -
- - {obj.filled !== false && ( -
- handleChange('color', e.target.value)} - /> - handleChange('color', e.target.value)} - /> -
- )} + + handleChange('isGrid', e.target.checked)} - /> - Retro Grid - + +
+ {mode && mode.includes('DRAW') + ? 'Click to add points. Press ENTER to finish. Hold Shift for 22.5° snap.' + : 'To edit, drag vertices on screen. Hold Shift for 22.5° snap.'}
+
+ )} - {obj.isGrid && ( + {isTriggerbox && ( + <> + {renderSection( + 1, + 'Transform', + 'blue', <> -
+
- + handleChange('gridLinesX', parseInt(e.target.value))} - min={1} - max={50} + value={formatPanelNumber(getPolyCentroid(obj.poly).x)} + onChange={(e) => translatePolyTo(parseFloat(e.target.value) || 0, getPolyCentroid(obj.poly).y)} />
- + handleChange('gridLinesY', parseInt(e.target.value))} - min={1} - max={50} + value={formatPanelNumber(getPolyCentroid(obj.poly).y)} + onChange={(e) => translatePolyTo(getPolyCentroid(obj.poly).x, parseFloat(e.target.value) || 0)} />
+
+
- + handleChange('lineWidth', parseFloat(e.target.value))} - step={0.1} - min={0.1} - max={10} + value={polygonScaleDraft} + onChange={(e) => applyPolygonScaleDraft(e.target.value)} />
-
-
- -
+
+ handleChange('gridColor', e.target.value)} + value={formatPanelNumber(obj.layer || 0)} + onChange={(e) => handleChange('layer', e.target.value, true)} /> +
+
+ handleChange('gridColor', e.target.value)} + value={formatPanelNumber(obj.parallax ?? 1)} + onChange={(e) => { + const val = parseFloat(e.target.value); + const newP = isNaN(val) ? 1.0 : val; + const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; + const scene = game.sceneManager.currentScene; + if (scene && obj.poly?.length) { + const dx = scene.camera.x * (newP - oldP); + const dy = scene.camera.y * (newP - oldP); + obj.poly = obj.poly.map((pt: any) => ({ + x: Math.round(pt.x + dx), + y: Math.round(pt.y + dy), + })); + } + handleChange('parallax', newP, true); + }} />
)} - {/* Blend & Sort (Extras) */} + + )} + + {/* Quad Properties */} + {selectedObjectType === 'Quad' && ( +
+
+
+
+ 1 + Transform +
+
+
+
- - translateQuadTo(parseFloat(e.target.value) || 0, getQuadCentroid(obj).y)} + /> +
+
+ + translateQuadTo(getQuadCentroid(obj).x, parseFloat(e.target.value) || 0)} + /> +
+
+ + handleChange('layer', e.target.value, true)} + /> +
+
+ +
+
+ + handleChange('parallax', e.target.value, true)} + /> +
+
+ + applyPolygonScaleDraft(e.target.value)} />
- + handleChange('filled', e.target.checked)} + /> + Fill Color + + {obj.filled !== false && ( +
+ handleChange('color', e.target.value)} + /> + handleChange('color', e.target.value)} + /> +
+ )} +
+ + {/* Retro Grid */} +
+ +
+ + {obj.isGrid && ( + <> +
+
+ + handleChange('gridLinesX', parseInt(e.target.value))} + min={1} + max={50} + /> +
+
+ + handleChange('gridLinesY', parseInt(e.target.value))} + min={1} + max={50} + /> +
+
+ + handleChange('lineWidth', parseFloat(e.target.value))} + step={0.1} + min={0.1} + max={10} + /> +
+
+
+ +
+ handleChange('gridColor', e.target.value)} + /> + handleChange('gridColor', e.target.value)} + /> +
+
+ + )} -
- -
-
-
@@ -2594,7 +3065,7 @@ export const PropertiesPanel: React.FC = () => { { comp.offsetX = parseFloat(e.target.value); incrementObjectVersion(); @@ -2608,7 +3079,7 @@ export const PropertiesPanel: React.FC = () => { { comp.offsetY = parseFloat(e.target.value); incrementObjectVersion(); @@ -2637,38 +3108,20 @@ export const PropertiesPanel: React.FC = () => {
)} - {selectedObjectType === 'Quad' && ( -
- Drag VERTEX: Hold ALT to snap to vertices/grid. -
- Hold SHIFT for angle snap. -
- )} - {selectedObjectType === 'Actor' && ( <> -
-
- ACTOR PROPERTIES +
+
+
+ 4 + ACTOR PROP. +
{/* Is Player */}
-
@@ -2722,7 +3175,7 @@ export const PropertiesPanel: React.FC = () => { type="number" step="10" className="e-input" - value={obj.animationSpeed !== undefined ? obj.animationSpeed : 150} + value={formatPanelNumber(obj.animationSpeed !== undefined ? obj.animationSpeed : 150)} onChange={(e) => handleChange('animationSpeed', e.target.value, true)} />
@@ -2730,13 +3183,8 @@ export const PropertiesPanel: React.FC = () => { {/* Animation Sets */}
ANIMATION SETS + > + x + +
+ + {/* Directions */} + {['down', 'up', 'left', 'right'].map((dir) => ( +
+
+ {dir.toUpperCase()} +
+ + +
+ ))} +
+ ); + })} + + )} + + {isObjectWithScriptEvents && ( +
+
+
+ 5 + SCRIPT EVENTS +
+
+ { + obj.interactions[verb] = e.target.value; + if (game.editor.selectedObject) { + (game.editor.selectedObject as any).interactions[verb] = e.target.value; + } + incrementObjectVersion(); + }} + /> + +
+ ))} +
+ )} + + {!isSettings && !isScene && !isWalkbox && ( +
+ {isTriggerbox && ( + <> + +
+ {mode && mode.includes('DRAW') + ? 'Click to add points. Press ENTER to finish. Hold Shift for 22.5° snap.' + : 'To edit, drag vertices on screen. Hold Shift for 22.5° snap.'} +
+ + )} + + {selectedObjectType === 'Quad' && ( +
+ Drag VERTEX: Hold ALT to snap to vertices/grid. +
+ Hold SHIFT for angle snap. +
+ )} + +
+ + + +
+
+ )} + + {/* SCENE Properties */} + {selectedObjectType === 'SCENE' && ( + <> + {(obj.camera || obj.defaultCamera) && + renderSection( + 1, + 'Camera', + 'blue', + <> +
+
+ + { + if (obj.camera) { + obj.camera.x = parseFloat(e.target.value); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + if (obj.camera) { + obj.camera.y = parseFloat(e.target.value); + incrementObjectVersion(); + } + }} + />
- - {/* Directions */} - {['down', 'up', 'left', 'right'].map((dir) => ( -
+ + { + if (obj.camera) { + obj.camera.zoom = parseFloat(e.target.value); + incrementObjectVersion(); + } }} - > -
- {dir.toUpperCase()} -
- - -
- ))} + /> +
- ); - })} - - )} - {/* SCENE Properties */} - {selectedObjectType === 'SCENE' && ( - <> - {/* Camera properties */} - {(obj.camera || obj.defaultCamera) && ( -
-
- CAMERA -
-
-
- - { - if (obj.camera) { - obj.camera.x = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> -
-
- - { - if (obj.camera) { - obj.camera.y = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> -
-
- - { - if (obj.camera) { - obj.camera.zoom = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> +
+
-
-
- -
-
-
- - - handleChange('cameraSpeed', parseFloat(e.target.value), true) - } - /> -
-
- - - handleChange('camDeadzoneX', parseFloat(e.target.value), true) - } - /> -
-
- - - handleChange('camDeadzoneY', parseFloat(e.target.value), true) - } - /> +
+
+ + handleChange('cameraSpeed', parseFloat(e.target.value), true)} + /> +
+
+ + handleChange('camDeadzoneX', parseFloat(e.target.value), true)} + /> +
+
+ + handleChange('camDeadzoneY', parseFloat(e.target.value), true)} + /> +
-
- <> +
-
- Camera Bounds (Min/Max) -
+
Camera Bounds (Min/Max)
@@ -3012,7 +3611,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMinX !== undefined ? obj.camMinX : ''} + value={obj.camMinX !== undefined ? formatPanelNumber(obj.camMinX) : ''} onChange={(e) => handleChange( 'camMinX', @@ -3028,7 +3627,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMaxX !== undefined ? obj.camMaxX : ''} + value={obj.camMaxX !== undefined ? formatPanelNumber(obj.camMaxX) : ''} onChange={(e) => handleChange( 'camMaxX', @@ -3044,7 +3643,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMinY !== undefined ? obj.camMinY : ''} + value={obj.camMinY !== undefined ? formatPanelNumber(obj.camMinY) : ''} onChange={(e) => handleChange( 'camMinY', @@ -3060,7 +3659,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMaxY !== undefined ? obj.camMaxY : ''} + value={obj.camMaxY !== undefined ? formatPanelNumber(obj.camMaxY) : ''} onChange={(e) => handleChange( 'camMaxY', @@ -3072,89 +3671,81 @@ export const PropertiesPanel: React.FC = () => {
- -
- )} - {/* Default Camera (Start Position) */} - {obj.defaultCamera && ( -
-
- DEFAULT CAMERA -
-
-
- - { - obj.defaultCamera.x = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
- - { - obj.defaultCamera.y = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
- - { - obj.defaultCamera.zoom = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
-
- -
-
- )} + {obj.defaultCamera && ( +
+
Default Camera
+
+
+ + { + obj.defaultCamera.x = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+ + { + obj.defaultCamera.y = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+ + { + obj.defaultCamera.zoom = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+
+ +
+
+ )} + + )} - {/* Scaling Settings */} - {game.sceneManager.currentScene && ( -
-
- SCALING -
- {(() => { + {game.sceneManager.currentScene && + renderSection( + 2, + 'Scaling', + 'yellow', + (() => { const s = game.sceneManager.currentScene.scaling; return ( <>
-
{s.enabled && ( -
+
{ s.min = parseFloat(e.target.value); incrementObjectVersion(); @@ -3190,7 +3779,7 @@ export const PropertiesPanel: React.FC = () => { type="number" step="0.1" className="e-input" - value={s.max} + value={formatPanelNumber(s.max)} onChange={(e) => { s.max = parseFloat(e.target.value); incrementObjectVersion(); @@ -3202,7 +3791,7 @@ export const PropertiesPanel: React.FC = () => { { s.horizon = parseFloat(e.target.value); incrementObjectVersion(); @@ -3214,7 +3803,7 @@ export const PropertiesPanel: React.FC = () => { { s.front = parseFloat(e.target.value); incrementObjectVersion(); @@ -3225,9 +3814,8 @@ export const PropertiesPanel: React.FC = () => { )} ); - })()} -
- )} + })() + )} )} @@ -3236,8 +3824,8 @@ export const PropertiesPanel: React.FC = () => { <>
@@ -3249,7 +3837,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - UI Scale {(obj.editor?.uiScale || 1.0).toFixed(1)}x + UI Scale {formatPanelNumber(obj.editor?.uiScale || 1.0)}x { min="0.5" max="2.0" step="0.1" - value={obj.editor?.uiScale || 1.0} + value={formatPanelNumber(obj.editor?.uiScale || 1.0)} onChange={(e) => { if (!obj.editor) obj.editor = { uiScale: 1.0 }; obj.editor.uiScale = parseFloat(e.target.value); @@ -3272,8 +3860,8 @@ export const PropertiesPanel: React.FC = () => {
@@ -3305,7 +3893,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Curvature {obj.crt.curvature.toFixed(2)} + Curvature {formatPanelNumber(obj.crt.curvature)} { min="0" max="0.5" step="0.01" - value={obj.crt.curvature} + value={formatPanelNumber(obj.crt.curvature)} onChange={(e) => { obj.crt.curvature = parseFloat(e.target.value); incrementObjectVersion(); @@ -3325,7 +3913,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Vignette {obj.crt.vignette.toFixed(2)} + Vignette {formatPanelNumber(obj.crt.vignette)} { min="0" max="1" step="0.05" - value={obj.crt.vignette} + value={formatPanelNumber(obj.crt.vignette)} onChange={(e) => { obj.crt.vignette = parseFloat(e.target.value); incrementObjectVersion(); @@ -3345,7 +3933,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Scanline Count {Math.round(obj.crt.scanlineCount)} + Scanline Count {formatPanelNumber(obj.crt.scanlineCount)} { min="100" max="2000" step="50" - value={obj.crt.scanlineCount} + value={formatPanelNumber(obj.crt.scanlineCount)} onChange={(e) => { obj.crt.scanlineCount = parseFloat(e.target.value); incrementObjectVersion(); @@ -3365,7 +3953,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Scanline Intensity {obj.crt.scanlineIntensity.toFixed(2)} + Scanline Intensity {formatPanelNumber(obj.crt.scanlineIntensity)} { min="0" max="1" step="0.05" - value={obj.crt.scanlineIntensity} + value={formatPanelNumber(obj.crt.scanlineIntensity)} onChange={(e) => { obj.crt.scanlineIntensity = parseFloat(e.target.value); incrementObjectVersion(); @@ -3385,7 +3973,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - RGB Split {obj.crt.aberration.toFixed(1)} + RGB Split {formatPanelNumber(obj.crt.aberration)} { min="0" max="5" step="0.1" - value={obj.crt.aberration} + value={formatPanelNumber(obj.crt.aberration)} onChange={(e) => { obj.crt.aberration = parseFloat(e.target.value); incrementObjectVersion(); @@ -3405,7 +3993,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Bloom {obj.crt.bloom.toFixed(2)} + Bloom {formatPanelNumber(obj.crt.bloom)} { min="0" max="1" step="0.05" - value={obj.crt.bloom} + value={formatPanelNumber(obj.crt.bloom)} onChange={(e) => { obj.crt.bloom = parseFloat(e.target.value); incrementObjectVersion(); @@ -3426,7 +4014,7 @@ export const PropertiesPanel: React.FC = () => { style={{ display: 'flex', justifyContent: 'space-between' }} > Phosphor / Grain{' '} - {obj.crt.phosphor ? obj.crt.phosphor.toFixed(2) : '0.00'} + {formatPanelNumber(obj.crt.phosphor || 0)} { min="0" max="1" step="0.05" - value={obj.crt.phosphor || 0} + value={formatPanelNumber(obj.crt.phosphor || 0)} onChange={(e) => { obj.crt.phosphor = parseFloat(e.target.value); incrementObjectVersion(); @@ -3458,10 +4046,7 @@ export const PropertiesPanel: React.FC = () => { )} -
+