feat: add localStorage helpers and wire team editor pre-fill
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
const STORAGE_PREFIX = 'scenario:';
|
||||
const MATCH_KEY = 'match:current';
|
||||
|
||||
function load(id) {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_PREFIX + id);
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function save(id, scenario) {
|
||||
const stamped = { ...scenario, lastModified: Date.now() };
|
||||
localStorage.setItem(STORAGE_PREFIX + id, JSON.stringify(stamped));
|
||||
}
|
||||
|
||||
function remove(id) {
|
||||
localStorage.removeItem(STORAGE_PREFIX + id);
|
||||
}
|
||||
|
||||
function listAll() {
|
||||
const items = [];
|
||||
for (let i = 0; i < localStorage.length; i += 1) {
|
||||
const key = localStorage.key(i);
|
||||
if (key !== null && key.startsWith(STORAGE_PREFIX)) {
|
||||
const id = key.slice(STORAGE_PREFIX.length);
|
||||
const scenario = load(id);
|
||||
if (scenario !== null) {
|
||||
items.push({
|
||||
id,
|
||||
name: typeof scenario.name === 'string' ? scenario.name : id,
|
||||
lastModified: typeof scenario.lastModified === 'number' ? scenario.lastModified : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
items.sort((a, b) => b.lastModified - a.lastModified);
|
||||
return items;
|
||||
}
|
||||
|
||||
function currentMatch() {
|
||||
try {
|
||||
const raw = localStorage.getItem(MATCH_KEY);
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentMatch(match) {
|
||||
localStorage.setItem(MATCH_KEY, JSON.stringify(match));
|
||||
}
|
||||
|
||||
function removeCurrentMatch() {
|
||||
localStorage.removeItem(MATCH_KEY);
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
function parsePath(name) {
|
||||
const match = name.match(/^(teamA|teamB)\[units\]\[(\d+)\]\[(\w+)\]$/);
|
||||
if (match) {
|
||||
const [, team, indexStr, key] = match;
|
||||
const teamKey = team === 'teamA' ? 'teamA' : 'teamB';
|
||||
return [teamKey, 'units', Number(indexStr), key];
|
||||
}
|
||||
const simple = [
|
||||
'id',
|
||||
'name',
|
||||
'battlefieldWidth',
|
||||
'battlefieldHeight',
|
||||
'victoryCondition',
|
||||
'holdRoundsRequired',
|
||||
];
|
||||
if (simple.includes(name)) {
|
||||
return [name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveField(scenario, path) {
|
||||
const parts = parsePath(path);
|
||||
if (parts === null) {
|
||||
return undefined;
|
||||
}
|
||||
let cursor = scenario;
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
if (cursor === null || cursor === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
cursor = cursor[parts[i]];
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
function populateRecentScenarios() {
|
||||
const root = document.getElementById('recent');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
while (root.firstChild) {
|
||||
root.removeChild(root.firstChild);
|
||||
}
|
||||
const items = listAll();
|
||||
if (items.length === 0) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'bf-empty';
|
||||
p.appendChild(document.createTextNode('No saved scenarios yet.'));
|
||||
root.appendChild(p);
|
||||
return;
|
||||
}
|
||||
const ul = document.createElement('ul');
|
||||
items.forEach((item) => {
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', `/scenarios/${encodeURIComponent(item.id)}/edit/team`);
|
||||
a.appendChild(document.createTextNode(`${item.name} (${item.id})`));
|
||||
li.appendChild(a);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
root.appendChild(ul);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function fetchArchetypes() {
|
||||
const res = await fetch('/assets/archetypes.json');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function prefillTeamEditor() {
|
||||
const form = document.querySelector('form[action*="/edit/team"]');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const id = decodeURIComponent(window.location.pathname.split('/').slice(-3, -2)[0] || 'new');
|
||||
if (id === 'new') {
|
||||
return;
|
||||
}
|
||||
const scenario = load(id);
|
||||
if (scenario === null) {
|
||||
return;
|
||||
}
|
||||
const fields = form.querySelectorAll('input[name], select[name]');
|
||||
fields.forEach((field) => {
|
||||
const name = field.getAttribute('name');
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const value = resolveField(scenario, name);
|
||||
if (value !== undefined) {
|
||||
Object.assign(field, { value: String(value) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function startMatch() {
|
||||
const id = decodeURIComponent(window.location.pathname.split('/').slice(-3, -2)[0] || 'new');
|
||||
if (id === 'new') {
|
||||
return;
|
||||
}
|
||||
const scenario = load(id);
|
||||
if (scenario === null) {
|
||||
return;
|
||||
}
|
||||
const csrf = getCsrfToken();
|
||||
const res = await fetch(`/scenarios/${encodeURIComponent(id)}/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrf,
|
||||
},
|
||||
body: JSON.stringify(scenario),
|
||||
});
|
||||
if (!res.ok) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`Start match failed: HTTP ${res.status}`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.match) {
|
||||
setCurrentMatch(data.match);
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`Start match failed: ${data.errors ? data.errors.join(', ') : 'unknown'}`);
|
||||
}
|
||||
}
|
||||
|
||||
window.bfStorage = {
|
||||
load,
|
||||
save,
|
||||
delete: remove,
|
||||
listAll,
|
||||
currentMatch,
|
||||
setCurrentMatch,
|
||||
removeCurrentMatch,
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
populateRecentScenarios();
|
||||
prefillTeamEditor();
|
||||
const startBtn = document.getElementById('bf-start-match');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
startMatch();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -82,6 +82,8 @@ render_layout(static function () use ($csrf, $e, $a, $archetypes, $error, $old,
|
||||
<label>Hold rounds: <input type="number" name="holdRoundsRequired" min="1" max="10" value="<?= $e($old['holdRoundsRequired'] ?? '1') ?>"></label>
|
||||
</fieldset>
|
||||
<button type="submit">Save</button>
|
||||
<button type="button" id="bf-start-match">Start match</button>
|
||||
</form>
|
||||
<script type="module" src="<?= $a('/js/storage.js') ?>"></script>
|
||||
<?php
|
||||
}, $csrf, 'Team editor');
|
||||
|
||||
Reference in New Issue
Block a user