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;
|
||||
}
|
||||
|
||||
#script {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
box-sizing: border-box;
|
||||
background: #181818;
|
||||
color: #ddd;
|
||||
font: 12px monospace;
|
||||
#editorPanel {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
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 {
|
||||
padding: 6px 10px;
|
||||
max-height: 25vh;
|
||||
overflow: auto;
|
||||
color: #f77;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
@ -90,15 +109,25 @@
|
|||
<h3>Stockpile</h3>
|
||||
<div id="stockEdit"></div>
|
||||
<h3>Behaviour script (Rune)</h3>
|
||||
<textarea id="script" spellcheck="false"></textarea>
|
||||
<div><button id="applyScript">Apply script</button></div>
|
||||
<div id="scriptErr"></div>
|
||||
<div><button id="openEditor">Open script editor</button></div>
|
||||
<h3>Events</h3>
|
||||
<div id="log"></div>
|
||||
<h3>Legend</h3>
|
||||
<div>yellow: person, white: rabbit, red: wolf<br>green dot: tree, pink: berries, grey: stone<br>brown square: house
|
||||
</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>
|
||||
const T = 10;
|
||||
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; });
|
||||
$('applyScript').addEventListener('click', async () => {
|
||||
const r = await post('/api/script', $('script').value);
|
||||
$('scriptErr').textContent = r.ok ? 'Script applied.' : await r.text();
|
||||
// Monaco is fetched from a CDN on first use, so the browser needs internet access for the script editor.
|
||||
const MONACO = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min';
|
||||
let monacoReady = null, editor = null;
|
||||
|
||||
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);
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue