132 lines
4.6 KiB
JavaScript
132 lines
4.6 KiB
JavaScript
// @ts-check
|
||
/**
|
||
* Plugin-System (#100): lädt JSON-Manifeste aus plugins/ zur Laufzeit.
|
||
* Stubs für skills / outfits / quests.
|
||
*/
|
||
(function (root) {
|
||
/** @type {{id:string,name:string,type:string,raw:any}[]} */
|
||
let registry = [];
|
||
const DEFAULT_PATHS = [
|
||
'plugins/sample-skill.json',
|
||
'plugins/sample-outfit.json',
|
||
'plugins/sample-quest.json'
|
||
];
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||
}
|
||
|
||
function validate(manifest) {
|
||
if (!manifest || typeof manifest !== 'object') return null;
|
||
if (!manifest.id || !manifest.name || !manifest.type) return null;
|
||
if (!['skill', 'outfit', 'quest', 'theme'].includes(manifest.type)) return null;
|
||
return {
|
||
id: String(manifest.id),
|
||
name: String(manifest.name),
|
||
type: String(manifest.type),
|
||
version: manifest.version || '0.1.0',
|
||
description: manifest.description || '',
|
||
raw: manifest
|
||
};
|
||
}
|
||
|
||
function register(manifest) {
|
||
const m = validate(manifest);
|
||
if (!m) return false;
|
||
registry = registry.filter((x) => x.id !== m.id);
|
||
registry.push(m);
|
||
applySideEffects(m);
|
||
return true;
|
||
}
|
||
|
||
function applySideEffects(m) {
|
||
const S = root.__CK_S;
|
||
if (!S) return;
|
||
if (m.type === 'outfit' && m.raw.costumeId) {
|
||
S.inventory = S.inventory || { costumes: [] };
|
||
S.inventory.costumes = S.inventory.costumes || [];
|
||
if (!S.inventory.costumes.includes(m.raw.costumeId)) S.inventory.costumes.push(m.raw.costumeId);
|
||
}
|
||
if (m.type === 'quest' && m.raw.quest && root.CodeKumpelQuests) {
|
||
// stash as plugin quest template
|
||
S.meta = S.meta || {};
|
||
S.meta.pluginQuests = S.meta.pluginQuests || [];
|
||
if (!S.meta.pluginQuests.some((q) => q.id === m.raw.quest.id)) {
|
||
S.meta.pluginQuests.push(m.raw.quest);
|
||
}
|
||
}
|
||
if (m.type === 'skill' && m.raw.skill) {
|
||
S.meta = S.meta || {};
|
||
S.meta.pluginSkills = S.meta.pluginSkills || [];
|
||
if (!S.meta.pluginSkills.some((s) => s.id === m.raw.skill.id)) {
|
||
S.meta.pluginSkills.push(m.raw.skill);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadFromUrl(url) {
|
||
try {
|
||
const r = await fetch(url);
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
const j = await r.json();
|
||
return register(j);
|
||
} catch (e) {
|
||
if (root.CodeKumpelStorage) root.CodeKumpelStorage.logErr('plugin:' + url, e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function loadDefaults() {
|
||
// file:// may block fetch – try anyway; also accept window.CK_PLUGINS_INLINE
|
||
if (Array.isArray(root.CK_PLUGINS_INLINE)) {
|
||
root.CK_PLUGINS_INLINE.forEach(register);
|
||
}
|
||
const results = await Promise.all(DEFAULT_PATHS.map(loadFromUrl));
|
||
return results.filter(Boolean).length;
|
||
}
|
||
|
||
function list() {
|
||
return registry.slice();
|
||
}
|
||
|
||
function renderPlugins(el, hooks) {
|
||
if (!el) return;
|
||
const rows =
|
||
registry
|
||
.map(
|
||
(p) =>
|
||
`<tr><td>${esc(p.type)}</td><td><b>${esc(p.name)}</b><div class="meta">${esc(p.id)} · v${esc(p.version)}</div><div class="hint">${esc(p.description)}</div></td></tr>`
|
||
)
|
||
.join('') || '<tr><td colspan="2">Noch keine Plugins geladen.</td></tr>';
|
||
el.innerHTML = `
|
||
<div class="plugins-panel">
|
||
<p class="hint">JSON-Manifeste unter <code>plugins/</code>. Typen: skill, outfit, quest, theme.</p>
|
||
<table class="priv-table"><thead><tr><th>Typ</th><th>Plugin</th></tr></thead><tbody>${rows}</tbody></table>
|
||
<div class="row">
|
||
<label class="filebtn">Plugin JSON laden<input type="file" id="pl-file" accept=".json" hidden></label>
|
||
<button type="button" class="ghost" id="pl-reload">Defaults neu laden</button>
|
||
</div>
|
||
</div>`;
|
||
el.querySelector('#pl-file')?.addEventListener('change', async (e) => {
|
||
try {
|
||
const j = JSON.parse(await e.target.files[0].text());
|
||
if (register(j)) {
|
||
if (hooks && hooks.save) hooks.save();
|
||
if (hooks && hooks.say) hooks.say('Plugin geladen: ' + j.name);
|
||
renderPlugins(el, hooks);
|
||
} else alert('Ungültiges Manifest');
|
||
} catch (err) {
|
||
alert('Ungültige JSON');
|
||
}
|
||
});
|
||
el.querySelector('#pl-reload')?.addEventListener('click', async () => {
|
||
await loadDefaults();
|
||
if (hooks && hooks.say) hooks.say('Plugin-Defaults geprüft.');
|
||
renderPlugins(el, hooks);
|
||
});
|
||
}
|
||
|
||
const api = { register, loadFromUrl, loadDefaults, list, renderPlugins, validate };
|
||
root.CodeKumpelPlugins = api;
|
||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|