code-kumpel/plugins.js
2026-10-24 04:51:23 +02:00

132 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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);