feat: add a Monaco-based Rune script editor
This commit is contained in:
parent
dd02a16d36
commit
1e7a5f911a
157
src/index.html
157
src/index.html
|
|
@ -25,16 +25,35 @@
|
||||||
width: 56px;
|
width: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#script {
|
#editorPanel {
|
||||||
width: 100%;
|
display: none;
|
||||||
height: 260px;
|
position: fixed;
|
||||||
box-sizing: border-box;
|
top: 0;
|
||||||
background: #181818;
|
right: 0;
|
||||||
color: #ddd;
|
bottom: 0;
|
||||||
font: 12px monospace;
|
width: min(760px, 70vw);
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-left: 1px solid #444;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editorBar {
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#monaco {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scriptErr {
|
#scriptErr {
|
||||||
|
padding: 6px 10px;
|
||||||
|
max-height: 25vh;
|
||||||
|
overflow: auto;
|
||||||
color: #f77;
|
color: #f77;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
@ -90,15 +109,25 @@
|
||||||
<h3>Stockpile</h3>
|
<h3>Stockpile</h3>
|
||||||
<div id="stockEdit"></div>
|
<div id="stockEdit"></div>
|
||||||
<h3>Behaviour script (Rune)</h3>
|
<h3>Behaviour script (Rune)</h3>
|
||||||
<textarea id="script" spellcheck="false"></textarea>
|
<div><button id="openEditor">Open script editor</button></div>
|
||||||
<div><button id="applyScript">Apply script</button></div>
|
|
||||||
<div id="scriptErr"></div>
|
|
||||||
<h3>Events</h3>
|
<h3>Events</h3>
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
<h3>Legend</h3>
|
<h3>Legend</h3>
|
||||||
<div>yellow: person, white: rabbit, red: wolf<br>green dot: tree, pink: berries, grey: stone<br>brown square: house
|
<div>yellow: person, white: rabbit, red: wolf<br>green dot: tree, pink: berries, grey: stone<br>brown square: house
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="editorPanel">
|
||||||
|
<div id="editorBar">
|
||||||
|
<b>behavior.rn</b>
|
||||||
|
<button id="applyScript">Apply (Ctrl+Enter)</button>
|
||||||
|
<button id="closeEditor">Close</button>
|
||||||
|
<label><input type="checkbox" id="vimToggle"> Vim</label>
|
||||||
|
<span id="scriptStatus"></span>
|
||||||
|
</div>
|
||||||
|
<div id="monaco"></div>
|
||||||
|
<div id="vimStatus" style="padding:2px 10px;font:12px monospace;min-height:1.4em"></div>
|
||||||
|
<div id="scriptErr"></div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const T = 10;
|
const T = 10;
|
||||||
const COLORS = ['#10305e', '#2a62a8', '#e0d08a', '#6aaa4a', '#3f7d3a', '#8a8a6a', '#aaaaaa'];
|
const COLORS = ['#10305e', '#2a62a8', '#e0d08a', '#6aaa4a', '#3f7d3a', '#8a8a6a', '#aaaaaa'];
|
||||||
|
|
@ -318,11 +347,111 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/api/script').then(r => r.text()).then(t => { $('script').value = t; });
|
// Monaco is fetched from a CDN on first use, so the browser needs internet access for the script editor.
|
||||||
$('applyScript').addEventListener('click', async () => {
|
const MONACO = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min';
|
||||||
const r = await post('/api/script', $('script').value);
|
let monacoReady = null, editor = null;
|
||||||
$('scriptErr').textContent = r.ok ? 'Script applied.' : await r.text();
|
|
||||||
|
function loadMonaco() {
|
||||||
|
monacoReady ??= new Promise((resolve, reject) => {
|
||||||
|
window.MonacoEnvironment = {
|
||||||
|
getWorkerUrl: () => URL.createObjectURL(new Blob(
|
||||||
|
[`self.MonacoEnvironment={baseUrl:'${MONACO}/'};importScripts('${MONACO}/vs/base/worker/workerMain.js');`],
|
||||||
|
{ type: 'text/javascript' })),
|
||||||
|
};
|
||||||
|
const s = document.createElement('script');
|
||||||
|
s.src = MONACO + '/vs/loader.js';
|
||||||
|
s.onload = () => {
|
||||||
|
require.config({ paths: { vs: MONACO + '/vs' } });
|
||||||
|
require(['vs/editor/editor.main'], () => resolve(window.monaco), reject);
|
||||||
|
};
|
||||||
|
s.onerror = () => { monacoReady = null; reject(new Error('could not load the editor from the CDN')); };
|
||||||
|
document.head.appendChild(s);
|
||||||
});
|
});
|
||||||
|
return monacoReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrors(text) {
|
||||||
|
const model = editor.getModel();
|
||||||
|
const markers = [];
|
||||||
|
for (const block of text.split(/\n(?=error:|warning:)/)) {
|
||||||
|
const m = /behavior:(\d+):(\d+)/.exec(block);
|
||||||
|
if (!m) continue;
|
||||||
|
const line = +m[1], col = +m[2];
|
||||||
|
const word = model.getWordAtPosition({ lineNumber: line, column: col });
|
||||||
|
markers.push({
|
||||||
|
severity: block.startsWith('warning') ? monaco.MarkerSeverity.Warning : monaco.MarkerSeverity.Error,
|
||||||
|
message: block.split('\n')[0].replace(/^(error|warning): /, ''),
|
||||||
|
startLineNumber: line, startColumn: col,
|
||||||
|
endLineNumber: line, endColumn: word ? word.endColumn : col + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
monaco.editor.setModelMarkers(model, 'rune', markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyScript() {
|
||||||
|
if (!editor) return;
|
||||||
|
const r = await post('/api/script', editor.getValue());
|
||||||
|
if (r.ok) {
|
||||||
|
monaco.editor.setModelMarkers(editor.getModel(), 'rune', []);
|
||||||
|
$('scriptErr').textContent = '';
|
||||||
|
$('scriptStatus').textContent = 'Applied.';
|
||||||
|
} else {
|
||||||
|
const text = await r.text();
|
||||||
|
$('scriptStatus').textContent = 'Rejected; the previous script is still running.';
|
||||||
|
$('scriptErr').textContent = text;
|
||||||
|
showErrors(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('openEditor').addEventListener('click', async () => {
|
||||||
|
$('editorPanel').style.display = 'flex';
|
||||||
|
try {
|
||||||
|
await loadMonaco();
|
||||||
|
if (!editor) {
|
||||||
|
const src = await (await fetch('/api/script')).text();
|
||||||
|
editor = monaco.editor.create($('monaco'), {
|
||||||
|
value: src, language: 'rust', theme: 'vs-dark',
|
||||||
|
automaticLayout: true, minimap: { enabled: false }, fontSize: 13, tabSize: 4,
|
||||||
|
});
|
||||||
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, applyScript);
|
||||||
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, applyScript);
|
||||||
|
if ($('vimToggle').checked) await setVim(true);
|
||||||
|
}
|
||||||
|
editor.focus();
|
||||||
|
} catch (e) {
|
||||||
|
$('scriptErr').textContent = e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('closeEditor').addEventListener('click', () => { $('editorPanel').style.display = 'none'; });
|
||||||
|
$('applyScript').addEventListener('click', applyScript);
|
||||||
|
|
||||||
|
const VIM = 'https://cdn.jsdelivr.net/npm/monaco-vim@0.4.2/dist/monaco-vim';
|
||||||
|
let vimMode = null, vimLib = null;
|
||||||
|
|
||||||
|
async function setVim(on) {
|
||||||
|
localStorage.setItem('vim', on ? '1' : '0');
|
||||||
|
if (!editor) return;
|
||||||
|
if (!on) {
|
||||||
|
vimMode?.dispose();
|
||||||
|
vimMode = null;
|
||||||
|
$('vimStatus').textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
vimLib ??= await new Promise((resolve, reject) => {
|
||||||
|
require.config({ paths: { 'monaco-vim': VIM } });
|
||||||
|
require(['monaco-vim'], resolve, reject);
|
||||||
|
});
|
||||||
|
if (vimMode || localStorage.getItem('vim') !== '1') return;
|
||||||
|
vimMode = vimLib.initVimMode(editor, $('vimStatus'));
|
||||||
|
vimLib.VimMode.Vim.defineEx('write', 'w', applyScript);
|
||||||
|
editor.focus();
|
||||||
|
} catch (e) {
|
||||||
|
$('scriptErr').textContent = 'Could not load vim mode: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('vimToggle').checked = localStorage.getItem('vim') !== '0';
|
||||||
|
$('vimToggle').addEventListener('change', e => setVim(e.target.checked));
|
||||||
|
|
||||||
loadTerrain().then(poll);
|
loadTerrain().then(poll);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue