dino-ge-playground 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # dino-ge-playground
2
+
3
+ Interactive, live-editing playground and inspector for the **Dino GE** game engine.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g dino-ge-playground
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Run the playground in any directory where you'd like to develop game scripts:
14
+
15
+ ```bash
16
+ dino-ge-playground
17
+ ```
18
+
19
+ Once running, access the editor and inspector at `http://localhost:3000`.
20
+
21
+ ### Features
22
+
23
+ - **Live Code Editor:** Modify your game logic in real-time with an integrated Ace editor.
24
+ - **Property Inspector:** Inspect and adjust game object properties while the engine is running.
25
+ - **Hot Refresh:** Automatically apply changes to your scripts.
26
+ - **Integrated Console:** Debug output directly in the playground UI.
27
+ - **Snippet Library:** Quick access to standard Dino GE object templates.
28
+
29
+ ### Script Storage
30
+
31
+ The playground saves your scripts (e.g., `script.js`) directly in the directory where the command is executed.
32
+
33
+ ## License
34
+
35
+ MIT
@@ -0,0 +1,71 @@
1
+ .console {
2
+ font-family: 'Courier New', Courier, monospace;
3
+ display: flex;
4
+ flex-direction: column;
5
+ flex: 1;
6
+ min-height: 150px;
7
+ background-color: #011627;
8
+ border-radius: 0.5rem;
9
+ overflow: hidden;
10
+ margin: 0 1rem 1rem 1rem;
11
+ border: 1px solid rgba(255, 255, 255, 0.1);
12
+ }
13
+
14
+ .console-header {
15
+ display: flex;
16
+ justify-content: space-between;
17
+ align-items: center;
18
+ padding: 0.5rem 1rem;
19
+ background-color: rgba(255, 255, 255, 0.05);
20
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
21
+ }
22
+
23
+ .title {
24
+ font-size: 0.8rem;
25
+ color: #43aa8b;
26
+ text-transform: uppercase;
27
+ letter-spacing: 1px;
28
+ }
29
+
30
+ .clear-btn {
31
+ color: rgba(255, 255, 255, 0.5);
32
+ cursor: pointer;
33
+ font-size: 0.9rem;
34
+ transition: color 0.2s;
35
+ }
36
+
37
+ .clear-btn:hover {
38
+ color: #F94144;
39
+ }
40
+
41
+ .console-text {
42
+ flex: 1;
43
+ overflow-y: auto;
44
+ padding: 0.5rem;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.25rem;
48
+ }
49
+
50
+ .console-line {
51
+ display: flex;
52
+ align-items: flex-start;
53
+ font-size: 0.85rem;
54
+ border-bottom: 1px solid rgba(255, 255, 255, 0.02);
55
+ padding-bottom: 2px;
56
+ }
57
+
58
+ .console-line pre {
59
+ margin: 0;
60
+ white-space: pre-wrap;
61
+ word-break: break-all;
62
+ color: #d1d1d1;
63
+ }
64
+
65
+ .console-line span.bash {
66
+ color: #43aa8b;
67
+ margin-right: 0.75rem;
68
+ user-select: none;
69
+ font-weight: bold;
70
+ }
71
+
@@ -0,0 +1,43 @@
1
+ export default () => {
2
+ const container = document.createElement('div');
3
+ container.className = 'console';
4
+
5
+ const header = document.createElement('div');
6
+ header.className = 'console-header';
7
+
8
+ const title = document.createElement('b');
9
+ title.className = 'title';
10
+ title.innerHTML = 'Console';
11
+
12
+ const clearBtn = document.createElement('i');
13
+ clearBtn.className = 'fa-solid fa-trash-can clear-btn';
14
+ clearBtn.title = 'Clear Console';
15
+ clearBtn.onclick = () => {
16
+ consoleText.innerHTML = '';
17
+ };
18
+
19
+ const consoleText = document.createElement('div');
20
+ consoleText.innerHTML = '';
21
+ consoleText.className = 'console-text';
22
+
23
+ const originalLog = window.console.log;
24
+ window.console.log = (...args) => {
25
+ originalLog(...args);
26
+ const message = args
27
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg))
28
+ .join(' ');
29
+
30
+ const line = document.createElement('div');
31
+ line.className = 'console-line';
32
+ line.innerHTML = '<span class="bash">$</span><pre>' + message + '</pre>';
33
+ consoleText.appendChild(line);
34
+ consoleText.scrollTop = consoleText.scrollHeight;
35
+ };
36
+
37
+ header.appendChild(title);
38
+ header.appendChild(clearBtn);
39
+ container.appendChild(header);
40
+ container.appendChild(consoleText);
41
+
42
+ return container;
43
+ };
@@ -0,0 +1,94 @@
1
+ .editor-container {
2
+ position: fixed;
3
+ right: 0;
4
+ top: 0;
5
+ bottom: 0;
6
+ display: flex;
7
+ flex-direction: column;
8
+ width: 40%;
9
+ max-width: 600px;
10
+ min-width: 300px;
11
+ z-index: 100;
12
+ background: rgba(0, 0, 0, 0.3);
13
+ backdrop-filter: blur(10px);
14
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
15
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
16
+ }
17
+
18
+ .editor-container.collapsed {
19
+ transform: translateX(100%);
20
+ }
21
+
22
+ .toggle-sidebar {
23
+ position: absolute;
24
+ left: -2.5rem;
25
+ top: 50%;
26
+ transform: translateY(-50%);
27
+ width: 2.5rem;
28
+ height: 5rem;
29
+ background: rgba(0, 0, 0, 0.3);
30
+ backdrop-filter: blur(10px);
31
+ border: 1px solid rgba(255, 255, 255, 0.1);
32
+ border-right: none;
33
+ border-radius: 1rem 0 0 1rem;
34
+ display: flex;
35
+ justify-content: center;
36
+ align-items: center;
37
+ cursor: pointer;
38
+ color: white;
39
+ transition: background 0.2s;
40
+ }
41
+
42
+ .toggle-sidebar:hover {
43
+ background: rgba(67, 170, 139, 0.4);
44
+ }
45
+
46
+ .editor {
47
+ flex: 3;
48
+ display: flex;
49
+ flex-direction: column;
50
+ background-color: #011627;
51
+ margin: 1rem;
52
+ border-radius: 0.5rem;
53
+ color: white;
54
+ overflow: hidden;
55
+ border: 1px solid rgba(255, 255, 255, 0.1);
56
+ }
57
+
58
+ .editor-textbox {
59
+ flex: 1;
60
+ width: 100%;
61
+ height: 100%;
62
+ }
63
+
64
+ .editor h2 {
65
+ font-family: Arial, Helvetica, sans-serif;
66
+ margin: 0;
67
+ font-size: 1rem;
68
+ color: #43aa8b;
69
+ text-transform: uppercase;
70
+ letter-spacing: 1px;
71
+ }
72
+
73
+ .banner {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ padding: 0.75rem 1rem;
78
+ background-color: rgba(255, 255, 255, 0.05);
79
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
80
+ position: static;
81
+ width: auto;
82
+ }
83
+
84
+ .icons-container {
85
+ display: flex;
86
+ flex-direction: row;
87
+ gap: 1rem;
88
+ }
89
+
90
+ .icons-container i {
91
+ cursor: pointer;
92
+ }
93
+
94
+
@@ -0,0 +1,159 @@
1
+ import sidebar from '../sidebar/index.js';
2
+ import { getScript, updateScript } from '../helpers.js';
3
+ import createConsole from '../console/index.js';
4
+ import { updatePlayground } from '../index.js';
5
+
6
+ let editorInstance = null;
7
+
8
+ export const createEditor = () => {
9
+ const container = document.createElement('div');
10
+ container.className = 'editor-container collapsed';
11
+ container.appendChild(sidebar());
12
+
13
+ // Toggle Sidebar Button
14
+ const toggleBtn = document.createElement('div');
15
+ toggleBtn.className = 'toggle-sidebar';
16
+ toggleBtn.innerHTML = '<i class="fa-solid fa-chevron-left"></i>';
17
+ toggleBtn.onclick = () => {
18
+ container.classList.toggle('collapsed');
19
+ const icon = toggleBtn.querySelector('i');
20
+ if (container.classList.contains('collapsed')) {
21
+ icon.className = 'fa-solid fa-chevron-left';
22
+ } else {
23
+ icon.className = 'fa-solid fa-chevron-right';
24
+ }
25
+ };
26
+ container.appendChild(toggleBtn);
27
+
28
+ const div = document.createElement('div');
29
+ div.id = 'editor';
30
+ div.className = 'editor';
31
+
32
+ const banner = document.createElement('div');
33
+ banner.className = 'banner';
34
+
35
+ const title = document.createElement('h2');
36
+ title.innerText = 'Editor';
37
+ title.onclick = () => {
38
+ if (div.classList.contains('hidden')) {
39
+ div.classList.remove('hidden');
40
+ }
41
+ };
42
+
43
+ const iconsContainer = document.createElement('div');
44
+ iconsContainer.className = 'icons-container';
45
+
46
+ const save = document.createElement('i');
47
+ save.id = 'saveIcon';
48
+ save.className = 'fa-solid fa-cloud save';
49
+ save.onclick = async () => {
50
+ if (editorInstance) {
51
+ await updateScript(editorInstance.getValue(), false);
52
+ await updatePlayground();
53
+ await updateEditor(true);
54
+ }
55
+ };
56
+
57
+ const editorDiv = document.createElement('div');
58
+ editorDiv.id = 'editor-textbox';
59
+ editorDiv.className = 'editor-textbox';
60
+ editorDiv.style.width = '100%';
61
+ editorDiv.style.height = 'calc(100% - 40px)'; // Adjust for banner height
62
+
63
+ banner.appendChild(title);
64
+ iconsContainer.appendChild(save);
65
+ banner.appendChild(iconsContainer);
66
+ div.appendChild(banner);
67
+ div.appendChild(editorDiv);
68
+ container.appendChild(div);
69
+ container.appendChild(createConsole());
70
+
71
+ // Initialize Ace Editor after a short delay to ensure DOM attachment
72
+ setTimeout(() => {
73
+ /* global ace */
74
+ editorInstance = ace.edit('editor-textbox');
75
+ editorInstance.setTheme('ace/theme/tomorrow_night_eighties');
76
+ editorInstance.session.setMode('ace/mode/javascript');
77
+ editorInstance.setOptions({
78
+ fontSize: '10pt',
79
+ tabSize: 2,
80
+ useSoftTabs: true,
81
+ showPrintMargin: false,
82
+ wrap: true
83
+ });
84
+
85
+ // Save Command
86
+ editorInstance.commands.addCommand({
87
+ name: 'save',
88
+ bindKey: { win: 'Ctrl-S', mac: 'Command-S' },
89
+ exec: function () {
90
+ save.click();
91
+ }
92
+ });
93
+ }, 0);
94
+
95
+ return container;
96
+ };
97
+
98
+ export const updateEditor = async (shouldFetchScript = true) => {
99
+ const saveIcon = document.getElementById('saveIcon');
100
+
101
+ if (saveIcon) {
102
+ saveIcon.classList.remove('save');
103
+ saveIcon.classList.add('saving');
104
+ }
105
+
106
+ let script = '';
107
+
108
+ if (shouldFetchScript) {
109
+ script = await getScript();
110
+ } else if (editorInstance) {
111
+ script = editorInstance.getValue();
112
+ }
113
+
114
+ const options = {
115
+ indent_size: 2,
116
+ space_in_empty_paren: false,
117
+ preserve_newlines: true
118
+ };
119
+
120
+ // Only beautify if fetched
121
+ if (shouldFetchScript && window.js_beautify) {
122
+ /* global js_beautify */
123
+ script = js_beautify(script, options) + '\n\n';
124
+ }
125
+
126
+ if (editorInstance && shouldFetchScript) {
127
+ // Preserve cursor/scroll position if possible
128
+ const pos = editorInstance.getCursorPosition();
129
+ editorInstance.setValue(script, -1);
130
+ editorInstance.moveCursorToPosition(pos);
131
+ }
132
+
133
+ if (saveIcon) {
134
+ saveIcon.classList.remove('saving');
135
+ saveIcon.classList.add('save');
136
+ }
137
+ };
138
+
139
+ export const insertTextToEditor = (text) => {
140
+ if (editorInstance) {
141
+ editorInstance.insert(text);
142
+ editorInstance.focus();
143
+ }
144
+ };
145
+
146
+ export const getEditorValue = () => {
147
+ if (editorInstance) {
148
+ return editorInstance.getValue();
149
+ }
150
+ return '';
151
+ };
152
+
153
+ export const setEditorValue = (text) => {
154
+ if (editorInstance) {
155
+ const pos = editorInstance.getCursorPosition();
156
+ editorInstance.setValue(text, -1);
157
+ editorInstance.moveCursorToPosition(pos);
158
+ }
159
+ };
package/helpers.js ADDED
@@ -0,0 +1,75 @@
1
+ export const createTextButton = (text, onclick) => {
2
+ const btn = document.createElement('button');
3
+ btn.textContent = text || 'Text';
4
+ btn.onclick = onclick;
5
+
6
+ document.body.appendChild(btn);
7
+ };
8
+
9
+ export const openDialog = ({ labelText = '' }) => {
10
+ const div = document.createElement('div');
11
+ const label = document.createElement('label');
12
+ label.textContent = labelText;
13
+ const input = document.createElement('input');
14
+ input.type = 'text';
15
+ input.name = Date.now().toString();
16
+ input.id = Date.now().toString();
17
+ input.placeholder = labelText;
18
+
19
+ label.htmlFor = input.name;
20
+ label.hidden = true;
21
+
22
+ div.appendChild(label);
23
+ div.appendChild(input);
24
+
25
+ return div;
26
+ };
27
+
28
+ const createInitialFile = async (id) => {
29
+ const defaultContent = `import { Engine, Scene, Sprite, Vector2, Text } from 'dino-ge';
30
+
31
+ // Your playground script starts here!
32
+ `;
33
+
34
+ await fetch(`/file/${id}`, {
35
+ method: 'PUT',
36
+ body: JSON.stringify({
37
+ data: defaultContent,
38
+ }),
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ });
43
+
44
+ return defaultContent;
45
+ };
46
+
47
+ export const getScript = async (id = 'script') => {
48
+ const res = await fetch(`/file/${id}`);
49
+
50
+ if (res.status === 404) {
51
+ return await createInitialFile(id);
52
+ } else if (res.status === 200) {
53
+ return await res.json();
54
+ } else {
55
+ window.alert('Error loading script');
56
+ return '';
57
+ }
58
+ };
59
+
60
+ export const updateScript = async (data, upsert = true, id = 'script') => {
61
+ const res = await fetch(`/file/${id}`, {
62
+ method: 'PUT',
63
+ body: JSON.stringify({
64
+ data,
65
+ upsert,
66
+ }),
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ });
71
+
72
+ if (!res.ok) {
73
+ window.alert('Error updating script.');
74
+ }
75
+ };
package/index.css ADDED
@@ -0,0 +1,66 @@
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ height: 100%;
5
+ width: 100%;
6
+ display: flex;
7
+ flex-direction: row;
8
+ background-color: black;
9
+ }
10
+
11
+ #canvas-container {
12
+ position: fixed;
13
+ left: 0;
14
+ top: 0;
15
+ bottom: 0;
16
+ right: 0;
17
+ background-color: #1a1a1a;
18
+ z-index: 0;
19
+ }
20
+
21
+ .logo-container {
22
+ position: fixed;
23
+ z-index: 1000;
24
+ left: 1rem;
25
+ top: 1rem;
26
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
27
+ padding: 0.5rem 1rem;
28
+ background: rgba(67, 170, 139, 0.2);
29
+ backdrop-filter: blur(5px);
30
+ border: 1px solid rgba(67, 170, 139, 0.4);
31
+ border-radius: 0.5rem;
32
+ color: #43aa8b;
33
+ -webkit-user-select: none;
34
+ user-select: none;
35
+ }
36
+
37
+ .logo-container h1 {
38
+ margin: 0;
39
+ }
40
+
41
+ .action-buttons {
42
+ display: flex;
43
+ flex-direction: row;
44
+ gap: 1.5rem;
45
+ position: fixed;
46
+ bottom: 2rem;
47
+ left: 2rem;
48
+ z-index: 1000;
49
+ background: rgba(255, 255, 255, 0.15);
50
+ backdrop-filter: blur(5px);
51
+ padding: 0.75rem 1.25rem;
52
+ border-radius: 2rem;
53
+ border: 1px solid rgba(255, 255, 255, 0.2);
54
+ }
55
+
56
+ .action-buttons i {
57
+ font-size: 1.5rem;
58
+ color: white;
59
+ cursor: pointer;
60
+ transition: transform 0.1s ease, color 0.2s ease;
61
+ }
62
+
63
+ .action-buttons i:hover {
64
+ transform: scale(1.2);
65
+ color: #43aa8b;
66
+ }
package/index.html ADDED
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify.js"></script>
5
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify-css.js"></script>
6
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify-html.js"></script>
7
+
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify-css.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.11/beautify-html.min.js"></script>
11
+
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js" type="text/javascript" charset="utf-8"></script>
13
+
14
+ <script src="https://kit.fontawesome.com/d4796734cb.js" crossorigin="anonymous"></script>
15
+ <script type="importmap">
16
+ {
17
+ "imports": {
18
+ "dino-ge": "/built/index.js"
19
+ }
20
+ }
21
+ </script>
22
+ <link rel="stylesheet" href="/sidebar/index.css">
23
+ <link rel="stylesheet" href="/editor/index.css">
24
+ <link rel="stylesheet" href="/console/index.css">
25
+ <link rel="stylesheet" href="/inspector/index.css">
26
+ <link rel="stylesheet" href="/index.css">
27
+ <script type="module" src="/index.js"></script>
28
+ </head>
29
+ <body>
30
+ </body>
31
+ </html>
package/index.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createEditor, updateEditor, getEditorValue } from './editor/index.js';
2
+ import { createInspector } from './inspector/index.js';
3
+ import { getScript, updateScript } from './helpers.js';
4
+ import * as Dino from 'dino-ge';
5
+
6
+ // Expose all dino-ge classes to window for snippets and user scripts
7
+ Object.assign(window, Dino);
8
+
9
+ const { Engine } = Dino;
10
+
11
+ let updateInspectorToggleState;
12
+
13
+ window.onload = async () => {
14
+ const logo = makeLogo();
15
+ const editor = createEditor();
16
+ const inspector = createInspector(() => {
17
+ if (updateInspectorToggleState) updateInspectorToggleState(inspector, logo);
18
+ });
19
+
20
+ document.body.appendChild(logo);
21
+ document.body.appendChild(editor);
22
+ document.body.appendChild(inspector);
23
+
24
+ setupActionButtons(inspector, logo);
25
+
26
+ await updatePlayground();
27
+ await updateEditor();
28
+
29
+ Engine.paused = true;
30
+ };
31
+
32
+ export const updatePlayground = async () => {
33
+ Engine.destroyAll();
34
+ document.getElementById('script.js')?.remove();
35
+ document.getElementById('canvas-container')?.remove();
36
+
37
+ const script = document.createElement('script');
38
+ script.type = 'module';
39
+ script.innerHTML = await getScript();
40
+ script.id = 'script.js';
41
+ document.body.appendChild(script);
42
+ };
43
+
44
+ const makeLogo = () => {
45
+ const container = document.createElement('div');
46
+ container.className = 'logo-container';
47
+
48
+ const logo = document.createElement('h1');
49
+ logo.innerHTML = 'DINO-GE';
50
+
51
+ container.appendChild(logo);
52
+
53
+ return container;
54
+ };
55
+
56
+ const setupActionButtons = (inspector, logo) => {
57
+ const actionButtons = document.createElement('div');
58
+ actionButtons.className = 'action-buttons';
59
+
60
+ const inspectorToggle = document.createElement('i');
61
+ inspectorToggle.className = 'fa-solid fa-sliders';
62
+ inspectorToggle.title = 'Toggle Property Inspector';
63
+
64
+ updateInspectorToggleState = (insp, lg) => {
65
+ const isHidden = insp.classList.contains('hidden');
66
+ inspectorToggle.style.color = isHidden ? 'white' : '#43aa8b';
67
+ lg.style.display = isHidden ? 'block' : 'none';
68
+ };
69
+
70
+ const play = document.createElement('i');
71
+ play.className = 'fa-solid fa-play';
72
+ play.onclick = async () => {
73
+ Engine.paused = false;
74
+ };
75
+
76
+ const pause = document.createElement('i');
77
+ pause.className = 'fa-solid fa-pause';
78
+ pause.onclick = () => {
79
+ Engine.paused = true;
80
+ document.getElementById('canvas').style = 'cursor: default';
81
+ };
82
+
83
+ const refresh = document.createElement('i');
84
+ refresh.className = 'fa-solid fa-rotate';
85
+ refresh.title = 'Refresh (Ctrl+Enter)';
86
+ refresh.onclick = async () => {
87
+ await updateScript(getEditorValue(), false);
88
+ await updatePlayground();
89
+ await updateEditor(true);
90
+
91
+ if (Engine.paused) {
92
+ Engine.paused = false;
93
+ }
94
+ };
95
+
96
+ const debug = document.createElement('i');
97
+ debug.className = 'fa-solid fa-bug';
98
+ debug.title = 'Toggle Debug Mode';
99
+ debug.onclick = () => {
100
+ Engine.debug = !Engine.debug;
101
+ debug.style.color = Engine.debug ? '#F94144' : 'white';
102
+
103
+ // Auto-open inspector if turning debug ON and inspector is hidden
104
+ if (Engine.debug && inspector.classList.contains('hidden')) {
105
+ inspectorToggle.click();
106
+ }
107
+ };
108
+
109
+ inspectorToggle.onclick = () => {
110
+ inspector.classList.toggle('hidden');
111
+ updateInspectorToggleState(inspector, logo);
112
+ };
113
+
114
+ actionButtons.appendChild(play);
115
+ actionButtons.appendChild(pause);
116
+ actionButtons.appendChild(refresh);
117
+ actionButtons.appendChild(debug);
118
+ actionButtons.appendChild(inspectorToggle);
119
+
120
+ document.body.appendChild(actionButtons);
121
+
122
+ // Keyboard Shortcuts
123
+ document.addEventListener('keydown', (e) => {
124
+ if (e.ctrlKey && e.key === 'Enter') {
125
+ refresh.click();
126
+ }
127
+ });
128
+ };
@@ -0,0 +1,108 @@
1
+ .inspector-container {
2
+ position: fixed;
3
+ left: 1rem;
4
+ top: 1rem;
5
+ bottom: 1rem;
6
+ width: 300px;
7
+ background: rgba(1, 22, 39, 0.95);
8
+ backdrop-filter: blur(10px);
9
+ border: 1px solid rgba(255, 255, 255, 0.1);
10
+ border-radius: 0.5rem;
11
+ color: white;
12
+ display: flex;
13
+ flex-direction: column;
14
+ z-index: 200;
15
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
16
+ overflow: hidden;
17
+ font-family: Arial, Helvetica, sans-serif;
18
+ }
19
+
20
+ .inspector-container.hidden {
21
+ transform: translateX(-120%);
22
+ opacity: 0;
23
+ pointer-events: none;
24
+ }
25
+
26
+ .inspector-header {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: 0.75rem 1rem;
31
+ background-color: rgba(255, 255, 255, 0.05);
32
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
33
+ }
34
+
35
+ .inspector-header h2 {
36
+ margin: 0;
37
+ font-size: 1rem;
38
+ color: #43aa8b;
39
+ text-transform: uppercase;
40
+ letter-spacing: 1px;
41
+ }
42
+
43
+ .inspector-body {
44
+ padding: 1rem;
45
+ overflow-y: auto;
46
+ flex: 1;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 1rem;
50
+ }
51
+
52
+ .inspector-section {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 0.5rem;
56
+ }
57
+
58
+ .inspector-section h3 {
59
+ margin: 0;
60
+ font-size: 0.8rem;
61
+ color: rgba(255, 255, 255, 0.5);
62
+ text-transform: uppercase;
63
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
64
+ padding-bottom: 0.25rem;
65
+ }
66
+
67
+ .prop-row {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ gap: 0.5rem;
72
+ }
73
+
74
+ .prop-row label {
75
+ font-size: 0.85rem;
76
+ flex: 1;
77
+ }
78
+
79
+ .prop-row input[type="number"],
80
+ .prop-row input[type="text"] {
81
+ flex: 2;
82
+ background: rgba(0, 0, 0, 0.3);
83
+ border: 1px solid rgba(255, 255, 255, 0.2);
84
+ color: white;
85
+ padding: 0.25rem 0.5rem;
86
+ border-radius: 0.25rem;
87
+ font-family: 'Fira Code', monospace;
88
+ font-size: 0.85rem;
89
+ outline: none;
90
+ width: 100%;
91
+ }
92
+
93
+ .prop-row input:focus {
94
+ border-color: #43aa8b;
95
+ }
96
+
97
+ .prop-row input[type="checkbox"] {
98
+ accent-color: #43aa8b;
99
+ width: 1.2rem;
100
+ height: 1.2rem;
101
+ }
102
+
103
+ .no-selection {
104
+ color: rgba(255, 255, 255, 0.5);
105
+ text-align: center;
106
+ margin-top: 2rem;
107
+ font-size: 0.9rem;
108
+ }
@@ -0,0 +1,214 @@
1
+ import Engine from '../../built/Engine.js';
2
+ import { getEditorValue, setEditorValue } from '../editor/index.js';
3
+
4
+ export const createInspector = (onClose) => {
5
+ const container = document.createElement('div');
6
+ container.className = 'inspector-container hidden'; // Hidden by default, toggleable
7
+
8
+ // Prevent clicks from falling through to the game canvas
9
+ container.addEventListener('mousedown', (e) => e.stopPropagation());
10
+ container.addEventListener('mouseup', (e) => e.stopPropagation());
11
+ container.addEventListener('click', (e) => e.stopPropagation());
12
+
13
+ const header = document.createElement('div');
14
+ header.className = 'inspector-header';
15
+ const title = document.createElement('h2');
16
+ title.innerText = 'Inspector';
17
+
18
+ const closeBtn = document.createElement('i');
19
+ closeBtn.className = 'fa-solid fa-xmark';
20
+ closeBtn.style.cursor = 'pointer';
21
+ closeBtn.onclick = () => {
22
+ container.classList.add('hidden');
23
+ if (onClose) onClose();
24
+ };
25
+
26
+ header.appendChild(title);
27
+ header.appendChild(closeBtn);
28
+
29
+ const body = document.createElement('div');
30
+ body.className = 'inspector-body';
31
+
32
+ const noSelection = document.createElement('div');
33
+ noSelection.className = 'no-selection';
34
+ noSelection.innerText = 'Toggle Debug Mode and click an object to inspect.';
35
+
36
+ const formContainer = document.createElement('div');
37
+ formContainer.className = 'inspector-form';
38
+ formContainer.style.display = 'none';
39
+ formContainer.style.flexDirection = 'column';
40
+ formContainer.style.gap = '1rem';
41
+
42
+ // Helper to create rows
43
+ const createRow = (labelText, type, propertyPath) => {
44
+ const row = document.createElement('div');
45
+ row.className = 'prop-row';
46
+ const label = document.createElement('label');
47
+ label.innerText = labelText;
48
+ const input = document.createElement('input');
49
+ input.type = type;
50
+ input.step = 'any';
51
+
52
+ // Update Engine when UI changes
53
+ input.addEventListener('input', (e) => {
54
+ const obj = Engine.selectedObject;
55
+ if (!obj) return;
56
+
57
+ let val = type === 'checkbox' ? e.target.checked : e.target.value;
58
+ if (type === 'number') val = parseFloat(val);
59
+
60
+ // Handle nested paths (e.g., 'position.x')
61
+ const paths = propertyPath.split('.');
62
+ if (paths.length === 2) {
63
+ obj[paths[0]][paths[1]] = val;
64
+ } else {
65
+ obj[propertyPath] = val;
66
+ }
67
+ });
68
+
69
+ // Update Editor when UI value is committed
70
+ input.addEventListener('change', (e) => {
71
+ const obj = Engine.selectedObject;
72
+ if (!obj || !obj.tag) return;
73
+
74
+ let val = type === 'checkbox' ? e.target.checked : e.target.value;
75
+ if (type === 'number') val = parseFloat(val);
76
+
77
+ try {
78
+ const code = getEditorValue();
79
+
80
+ // Find the tag: 'obj.tag' definition
81
+ const tagRegex = new RegExp(`tag:\\s*['"\`]${obj.tag}['"\`]`, 'g');
82
+ const match = tagRegex.exec(code);
83
+
84
+ if (match) {
85
+ // Look 500 characters around the tag to find the object instantiation
86
+ const searchWindowStart = Math.max(0, match.index - 500);
87
+ const searchWindowEnd = Math.min(code.length, match.index + 500);
88
+ let snippet = code.substring(searchWindowStart, searchWindowEnd);
89
+
90
+ let updatedSnippet = snippet;
91
+ const paths = propertyPath.split('.');
92
+
93
+ if (paths.length === 2 && paths[0] === 'position') {
94
+ // Find position: new Vector2(x, y)
95
+ const posRegex = /position:\s*new\s*Vector2\s*\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/;
96
+ const posMatch = posRegex.exec(snippet);
97
+ if (posMatch) {
98
+ const newX = paths[1] === 'x' ? val : posMatch[1].trim();
99
+ const newY = paths[1] === 'y' ? val : posMatch[2].trim();
100
+ updatedSnippet = snippet.replace(posRegex, `position: new Vector2(${newX}, ${newY})`);
101
+ }
102
+ } else {
103
+ // Find property: value
104
+ // Be careful to match the exact property name
105
+ const propRegex = new RegExp(`(\\b${propertyPath}\\s*:\\s*)([^,\\n}]+)`);
106
+ const propMatch = propRegex.exec(snippet);
107
+ if (propMatch) {
108
+ const formattedVal = typeof val === 'string' ? `'${val}'` : val;
109
+ updatedSnippet = snippet.replace(propRegex, `$1${formattedVal}`);
110
+ }
111
+ }
112
+
113
+ if (snippet !== updatedSnippet) {
114
+ const newCode = code.substring(0, searchWindowStart) + updatedSnippet + code.substring(searchWindowEnd);
115
+ setEditorValue(newCode);
116
+ }
117
+ }
118
+ } catch(err) {
119
+ console.error('Failed to sync inspector changes to editor:', err);
120
+ }
121
+ });
122
+
123
+ row.appendChild(label);
124
+ row.appendChild(input);
125
+ return { row, input, propertyPath, type };
126
+ };
127
+
128
+ const sections = {
129
+ General: [
130
+ createRow('Tag', 'text', 'tag'),
131
+ createRow('Visible', 'checkbox', 'visible'),
132
+ createRow('Z-Index', 'number', 'zIndex')
133
+ ],
134
+ Transform: [
135
+ createRow('X', 'number', 'position.x'),
136
+ createRow('Y', 'number', 'position.y')
137
+ // Note: width/height are often getters depending on the shape, so we might skip editing them globally here, or handle specific subclasses if needed.
138
+ ],
139
+ Physics: [
140
+ createRow('Velocity X', 'number', 'velocity.x'),
141
+ createRow('Velocity Y', 'number', 'velocity.y'),
142
+ createRow('Accel X', 'number', 'acceleration.x'),
143
+ createRow('Accel Y', 'number', 'acceleration.y'),
144
+ createRow('Mass', 'number', 'mass'),
145
+ createRow('Is Static', 'checkbox', 'isStatic')
146
+ ]
147
+ };
148
+
149
+ const inputs = [];
150
+
151
+ for (const [sectionName, rows] of Object.entries(sections)) {
152
+ const section = document.createElement('div');
153
+ section.className = 'inspector-section';
154
+ const sectionTitle = document.createElement('h3');
155
+ sectionTitle.innerText = sectionName;
156
+ section.appendChild(sectionTitle);
157
+
158
+ rows.forEach(({ row, input, propertyPath, type }) => {
159
+ section.appendChild(row);
160
+ inputs.push({ input, propertyPath, type });
161
+ });
162
+
163
+ formContainer.appendChild(section);
164
+ }
165
+
166
+ body.appendChild(noSelection);
167
+ body.appendChild(formContainer);
168
+
169
+ container.appendChild(header);
170
+ container.appendChild(body);
171
+
172
+ // Update Loop
173
+ const updateLoop = () => {
174
+ if (!container.classList.contains('hidden')) {
175
+ const obj = Engine.selectedObject;
176
+
177
+ if (!Engine.debug || !obj) {
178
+ noSelection.style.display = 'block';
179
+ formContainer.style.display = 'none';
180
+ title.innerText = 'Inspector';
181
+ } else {
182
+ noSelection.style.display = 'none';
183
+ formContainer.style.display = 'flex';
184
+ title.innerText = `Inspector: ${obj.constructor.name}`;
185
+
186
+ inputs.forEach(({ input, propertyPath, type }) => {
187
+ // Skip updating if user is currently typing in this field
188
+ if (document.activeElement === input) return;
189
+
190
+ let val;
191
+ const paths = propertyPath.split('.');
192
+ if (paths.length === 2) {
193
+ val = obj[paths[0]][paths[1]];
194
+ } else {
195
+ val = obj[propertyPath];
196
+ }
197
+
198
+ if (type === 'checkbox') {
199
+ input.checked = !!val;
200
+ } else if (type === 'number') {
201
+ input.value = typeof val === 'number' ? val.toFixed(2) : 0;
202
+ } else {
203
+ input.value = val !== undefined ? val : '';
204
+ }
205
+ });
206
+ }
207
+ }
208
+ requestAnimationFrame(updateLoop);
209
+ };
210
+
211
+ requestAnimationFrame(updateLoop);
212
+
213
+ return container;
214
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "dino-ge-playground",
3
+ "version": "1.0.3",
4
+ "description": "Interactive, live-editing playground and inspector for Dino GE",
5
+ "bin": {
6
+ "dino-ge-playground": "server.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "nodemon server.js",
12
+ "build": "echo \"Nothing to build for playground\""
13
+ },
14
+ "files": [
15
+ "server.js",
16
+ "index.html",
17
+ "index.js",
18
+ "index.css",
19
+ "helpers.js",
20
+ "script.js",
21
+ "console/",
22
+ "editor/",
23
+ "inspector/",
24
+ "sidebar/",
25
+ "sprites/"
26
+ ],
27
+ "dependencies": {
28
+ "express": "^4.21.2",
29
+ "dino-ge": "^1.0.3"
30
+ },
31
+ "devDependencies": {
32
+ "nodemon": "^3.1.9"
33
+ },
34
+ "author": "Rich <richgled25@gmail.com>",
35
+ "license": "ISC"
36
+ }
package/server.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import express from 'express';
3
+ import path from 'path';
4
+ import fs from 'fs/promises';
5
+ import { fileURLToPath } from 'url';
6
+ import { createRequire } from 'module';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const app = express();
12
+ const PLAYGROUND_DIR = __dirname;
13
+
14
+ let DINO_GE_DIST;
15
+ try {
16
+ DINO_GE_DIST = path.join(path.dirname(require.resolve('dino-ge/package.json')), 'dist');
17
+ } catch {
18
+ DINO_GE_DIST = path.join(__dirname, '../dino-ge/dist');
19
+ }
20
+
21
+ app.use(express.json());
22
+ app.use('/', express.static(PLAYGROUND_DIR));
23
+ app.use('/built', express.static(DINO_GE_DIST));
24
+
25
+ const getFilePath = (id) => path.join(process.cwd(), `${id || 'script'}.js`);
26
+
27
+ app.get('/file/:id', async (req, res) => {
28
+ try {
29
+ const file = await fs.readFile(getFilePath(req.params.id));
30
+ res.status(200).json(file.toString());
31
+ } catch (err) {
32
+ const status = err.code === 'ENOENT' ? 404 : 500;
33
+ res.status(status).json({ error: err.message });
34
+ }
35
+ });
36
+
37
+ app.put('/file/:id', async (req, res) => {
38
+ try {
39
+ const filePath = getFilePath(req.params.id);
40
+ const data = String(req.body.data);
41
+ if (req.body.upsert) {
42
+ await fs.appendFile(filePath, data);
43
+ } else {
44
+ await fs.writeFile(filePath, data);
45
+ }
46
+ res.status(200).json({ success: true });
47
+ } catch (err) {
48
+ res.status(500).json({ error: err.message });
49
+ }
50
+ });
51
+
52
+ const PORT = process.env.PORT || 3000;
53
+ app.listen(PORT, () => {
54
+ console.log(`Dino GE Playground: http://localhost:${PORT}`);
55
+ console.log(`Scripts directory: ${process.cwd()}`);
56
+ });
@@ -0,0 +1,86 @@
1
+ .sidebar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 0.5rem;
5
+ padding: 1rem;
6
+ background-color: rgba(255, 255, 255, 0.05);
7
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
8
+ min-height: fit-content;
9
+ transition: all 0.3s ease;
10
+ overflow: hidden;
11
+ }
12
+
13
+ .sidebar.minimized {
14
+ height: 2.5rem;
15
+ min-height: 2.5rem;
16
+ padding-top: 0.5rem;
17
+ padding-bottom: 0.5rem;
18
+ }
19
+
20
+ .sidebar-header {
21
+ display: flex;
22
+ justify-content: space-between;
23
+ align-items: center;
24
+ cursor: pointer;
25
+ user-select: none;
26
+ }
27
+
28
+ .sidebar-title {
29
+ font-family: Arial, Helvetica, sans-serif;
30
+ font-size: 0.75rem;
31
+ color: #43aa8b;
32
+ text-transform: uppercase;
33
+ letter-spacing: 1px;
34
+ margin-bottom: 0;
35
+ font-weight: bold;
36
+ }
37
+
38
+ .sidebar-toggle-icon {
39
+ color: rgba(255, 255, 255, 0.3);
40
+ font-size: 0.8rem;
41
+ transition: transform 0.3s ease;
42
+ }
43
+
44
+ .sidebar.minimized .sidebar-toggle-icon {
45
+ transform: rotate(-90deg);
46
+ }
47
+
48
+ .sidebar.minimized .snippet-btn {
49
+ opacity: 0;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .snippet-btn {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 0.75rem;
57
+ background: rgba(255, 255, 255, 0.05);
58
+ border: 1px solid rgba(255, 255, 255, 0.1);
59
+ color: #d1d1d1;
60
+ padding: 0.5rem 0.75rem;
61
+ border-radius: 0.25rem;
62
+ cursor: pointer;
63
+ transition: all 0.2s;
64
+ font-family: Arial, Helvetica, sans-serif;
65
+ text-align: left;
66
+ }
67
+
68
+ .snippet-btn i {
69
+ width: 1rem;
70
+ text-align: center;
71
+ color: #43aa8b;
72
+ }
73
+
74
+ .snippet-btn span {
75
+ font-size: 0.85rem;
76
+ }
77
+
78
+ .snippet-btn:hover {
79
+ background: rgba(67, 170, 139, 0.2);
80
+ border-color: rgba(67, 170, 139, 0.4);
81
+ color: white;
82
+ }
83
+
84
+ .snippet-btn:active {
85
+ transform: translateY(1px);
86
+ }
@@ -0,0 +1,87 @@
1
+ import { insertTextToEditor } from '../editor/index.js';
2
+
3
+ export default () => {
4
+ const sidebarDiv = document.createElement('div');
5
+ sidebarDiv.className = 'sidebar minimized';
6
+
7
+ const header = document.createElement('div');
8
+ header.className = 'sidebar-header';
9
+ header.onclick = () => {
10
+ sidebarDiv.classList.toggle('minimized');
11
+ };
12
+
13
+ const title = document.createElement('div');
14
+ title.className = 'sidebar-title';
15
+ title.innerText = 'Snippets';
16
+
17
+ const toggleIcon = document.createElement('i');
18
+ toggleIcon.className = 'fa-solid fa-chevron-down sidebar-toggle-icon';
19
+
20
+ header.appendChild(title);
21
+ header.appendChild(toggleIcon);
22
+ sidebarDiv.appendChild(header);
23
+
24
+ const snippets = [
25
+ { icon: 'fa-arrows-up-down-left-right', label: 'Vector2', code: 'new Vector2(0, 0)' },
26
+ { icon: 'fa-font', label: 'Text', code: `new Text({
27
+ tag: 'textObj',
28
+ text: 'Hello World',
29
+ fontSize: 30,
30
+ colour: 'white',
31
+ position: new Vector2(100, 100),
32
+ width: 200,
33
+ zIndex: 10
34
+ });` },
35
+ { icon: 'fa-square', label: 'Rectangle', code: `new Rectangle({
36
+ tag: 'rectObj',
37
+ position: new Vector2(100, 100),
38
+ width: 50,
39
+ height: 50,
40
+ colour: '#43aa8b',
41
+ zIndex: 5
42
+ });` },
43
+ { icon: 'fa-circle', label: 'Circle', code: `new Circle({
44
+ tag: 'circleObj',
45
+ position: new Vector2(100, 100),
46
+ radius: 25,
47
+ colour: '#F94144',
48
+ zIndex: 5
49
+ });` },
50
+ { icon: 'fa-image', label: 'Sprite', code: `new Sprite({
51
+ tag: 'spriteObj',
52
+ img: dinoImg,
53
+ rows: 1,
54
+ cols: 24,
55
+ position: new Vector2(100, 100),
56
+ startCol: 0,
57
+ endCol: 4,
58
+ zIndex: 5
59
+ });` },
60
+ { icon: 'fa-minus', label: 'Line', code: `new Line({
61
+ tag: 'lineObj',
62
+ width: 2,
63
+ p1: new Vector2(0, 0),
64
+ p2: new Vector2(100, 100),
65
+ zIndex: 1
66
+ });` }
67
+ ];
68
+
69
+ snippets.forEach(s => {
70
+ sidebarDiv.appendChild(snippetFactory(s.icon, s.label, s.code));
71
+ });
72
+
73
+ return sidebarDiv;
74
+ };
75
+
76
+ function snippetFactory(iconClass, label, code) {
77
+ const btn = document.createElement('button');
78
+ btn.className = 'snippet-btn';
79
+ btn.title = label;
80
+ btn.innerHTML = `<i class="fa-solid ${iconClass}"></i><span>${label}</span>`;
81
+
82
+ btn.onclick = () => {
83
+ insertTextToEditor(code);
84
+ };
85
+
86
+ return btn;
87
+ }
Binary file