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 +35 -0
- package/console/index.css +71 -0
- package/console/index.js +43 -0
- package/editor/index.css +94 -0
- package/editor/index.js +159 -0
- package/helpers.js +75 -0
- package/index.css +66 -0
- package/index.html +31 -0
- package/index.js +128 -0
- package/inspector/index.css +108 -0
- package/inspector/index.js +214 -0
- package/package.json +36 -0
- package/server.js +56 -0
- package/sidebar/index.css +86 -0
- package/sidebar/index.js +87 -0
- package/sprites/DinoSprites - doux.png +0 -0
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
|
+
|
package/console/index.js
ADDED
|
@@ -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
|
+
};
|
package/editor/index.css
ADDED
|
@@ -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
|
+
|
package/editor/index.js
ADDED
|
@@ -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
|
+
}
|
package/sidebar/index.js
ADDED
|
@@ -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
|