Files
OBS-Overlays/pokemon-party/editor.html
T

402 lines
13 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pokémon Party Editor</title>
<style>
:root {
--bg: #07111f;
--panel: #101d33;
--panel-2: #152642;
--line: rgba(255,255,255,.16);
--text: #f8fbff;
--muted: #b8c7df;
--accent: #f6d35b;
--blue: #79d2ff;
--green: #80ffaa;
--red: #ff8e8e;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(121,210,255,.18), transparent 32rem),
radial-gradient(circle at bottom right, rgba(246,211,91,.14), transparent 30rem),
var(--bg);
color: var(--text);
font-family: "Segoe UI", Arial, sans-serif;
padding: 28px;
}
h1 { margin: 0 0 8px; font-size: 30px; }
p { color: var(--muted); line-height: 1.5; }
.wrap {
max-width: 1180px;
margin: 0 auto;
}
.topbar,
.slot,
.output {
background: linear-gradient(180deg, rgba(255,255,255,.06), transparent), var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: 0 18px 45px rgba(0,0,0,.28);
}
.topbar {
padding: 18px;
margin: 20px 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
button {
border: 0;
border-radius: 12px;
padding: 11px 14px;
background: var(--blue);
color: #06101f;
font-weight: 800;
cursor: pointer;
}
button.secondary { background: #273b5d; color: var(--text); border: 1px solid var(--line); }
button.good { background: var(--green); }
button.warn { background: var(--accent); }
button.danger { background: var(--red); }
button:hover { filter: brightness(1.08); }
.hint {
flex-basis: 100%;
margin: 4px 0 0;
font-size: 14px;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.slot {
padding: 16px;
display: grid;
grid-template-columns: 90px 1fr;
gap: 14px;
align-items: center;
}
.preview {
width: 90px;
height: 90px;
display: grid;
place-items: center;
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: 16px;
}
.preview img {
max-width: 78px;
max-height: 78px;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.ball {
width: 52px;
height: 52px;
border-radius: 50%;
background:
radial-gradient(circle at center, #fff 0 5px, #111 6px 9px, transparent 10px),
linear-gradient(#dc3144 0 46%, #161616 47% 53%, #f4f4f4 54% 100%);
border: 3px solid #111;
opacity: .55;
}
.fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
label {
display: grid;
gap: 5px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 10px;
background: #081426;
color: var(--text);
padding: 10px;
font: inherit;
}
.slot-title {
grid-column: 1 / -1;
color: var(--accent);
font-weight: 900;
letter-spacing: .5px;
}
.wide { grid-column: 1 / -1; }
.output {
margin-top: 18px;
padding: 16px;
}
textarea {
min-height: 210px;
font-family: Consolas, "Courier New", monospace;
font-size: 13px;
}
.note {
border-left: 4px solid var(--accent);
padding: 10px 12px;
background: rgba(246,211,91,.08);
border-radius: 8px;
}
@media (max-width: 860px) {
body { padding: 14px; }
.grid { grid-template-columns: 1fr; }
.slot { grid-template-columns: 1fr; }
.preview { width: 100%; }
.fields { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main class="wrap">
<h1>Pokémon Party Editor</h1>
<p>Use this page to build your six-slot party and save it directly to party.json for the OBS overlay.</p>
<section class="topbar">
<button id="loadJson">Load current party.json</button>
<button id="saveFile" class="good">Save party.json directly</button>
<button id="saveBrowser" class="secondary">Save to browser preview</button>
<button id="copyJson" class="warn">Copy JSON</button>
<button id="downloadJson" class="secondary">Download party.json</button>
<button id="clearParty" class="danger">Clear all slots</button>
<div class="hint">Direct save requires running this folder with server.py, not plain python -m http.server. If direct save fails, Copy JSON and Download party.json still work as backups.</div>
</section>
<p class="note">Sprite path examples: sprites/cyndaquil.png, sprites/hgss/155.png, or a full PokeAPI URL. If you enter a National Dex number and leave Sprite blank, the helper buttons can fill it for you.</p>
<section id="slots" class="grid" aria-label="Party slots"></section>
<section class="output">
<h2>Generated party.json</h2>
<textarea id="jsonOutput" spellcheck="false"></textarea>
</section>
</main>
<script>
const STORAGE_KEY = 'pokemonPartyOverlay.party';
const emptySlot = () => ({ species: '', nickname: '', sprite: '', status: 'empty' });
const sample = [
{ species: 'cyndaquil', nickname: 'Toast', sprite: 'sprites/cyndaquil.png', status: 'alive' },
{ species: 'pidgey', nickname: 'Gale', sprite: 'sprites/pidgey.png', status: 'alive' },
{ species: 'mareep', nickname: 'Static', sprite: 'sprites/mareep.png', status: 'alive' },
emptySlot(), emptySlot(), emptySlot()
];
function normalizeParty(data) {
const party = Array.isArray(data) ? data : data?.party;
return Array.from({ length: 6 }, (_, i) => ({ ...emptySlot(), ...(party?.[i] || {}) }));
}
function niceSpecies(value) {
return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
}
function currentParty() {
return Array.from(document.querySelectorAll('.slot')).map(slot => {
const species = niceSpecies(slot.querySelector('[data-field="species"]').value);
const nickname = slot.querySelector('[data-field="nickname"]').value.trim();
const dex = slot.querySelector('[data-field="dex"]').value.trim();
let sprite = slot.querySelector('[data-field="sprite"]').value.trim();
const status = slot.querySelector('[data-field="status"]').value;
if (!sprite && species) sprite = `sprites/${species}.png`;
if (!species && !nickname && !sprite) return emptySlot();
return { species, nickname, sprite, status: status || 'alive' };
});
}
function partyJson() {
return JSON.stringify({ party: currentParty() }, null, 2);
}
function updateOutput() {
document.getElementById('jsonOutput').value = partyJson();
updatePreviews();
}
function updatePreviews() {
document.querySelectorAll('.slot').forEach(slot => {
const sprite = slot.querySelector('[data-field="sprite"]').value.trim();
const species = slot.querySelector('[data-field="species"]').value.trim();
const preview = slot.querySelector('.preview');
preview.innerHTML = '';
if (sprite || species) {
const img = document.createElement('img');
img.alt = species || 'Pokémon sprite';
img.src = sprite || `sprites/${niceSpecies(species)}.png`;
img.onerror = () => {
preview.innerHTML = '<div class="ball" title="Sprite not found"></div>';
};
preview.appendChild(img);
} else {
preview.innerHTML = '<div class="ball" title="Empty slot"></div>';
}
});
}
function setParty(party) {
normalizeParty(party).forEach((mon, index) => {
const slot = document.querySelector(`.slot[data-index="${index}"]`);
slot.querySelector('[data-field="species"]').value = mon.species || '';
slot.querySelector('[data-field="nickname"]').value = mon.nickname || '';
slot.querySelector('[data-field="sprite"]').value = mon.sprite || '';
slot.querySelector('[data-field="status"]').value = mon.status || (mon.species ? 'alive' : 'empty');
});
updateOutput();
}
function makeSlot(index) {
const slot = document.createElement('article');
slot.className = 'slot';
slot.dataset.index = index;
slot.innerHTML = `
<div class="preview"><div class="ball"></div></div>
<div class="fields">
<div class="slot-title">Slot ${index + 1}</div>
<label>Species
<input data-field="species" placeholder="cyndaquil" />
</label>
<label>Nickname
<input data-field="nickname" placeholder="Toast" />
</label>
<label>National Dex #
<input data-field="dex" inputmode="numeric" placeholder="155" />
</label>
<label>Status
<select data-field="status">
<option value="alive">alive</option>
<option value="dead">dead</option>
<option value="boxed">boxed</option>
<option value="empty">empty</option>
</select>
</label>
<label class="wide">Sprite path or URL
<input data-field="sprite" placeholder="sprites/cyndaquil.png" />
</label>
<button type="button" class="secondary" data-action="localHgss">Use local HGSS path</button>
<button type="button" class="secondary" data-action="pokeapiHgss">Use PokeAPI HGSS URL</button>
</div>
`;
slot.addEventListener('input', updateOutput);
slot.addEventListener('change', updateOutput);
slot.addEventListener('click', event => {
const action = event.target.dataset.action;
if (!action) return;
const dex = slot.querySelector('[data-field="dex"]').value.trim();
const sprite = slot.querySelector('[data-field="sprite"]');
if (!dex) {
alert('Enter the National Dex number first. Example: Cyndaquil is 155.');
return;
}
if (action === 'localHgss') sprite.value = `sprites/hgss/${dex}.png`;
if (action === 'pokeapiHgss') sprite.value = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-iv/heartgold-soulsilver/${dex}.png`;
updateOutput();
});
return slot;
}
async function loadCurrentJson() {
try {
const response = await fetch(`party.json?cacheBust=${Date.now()}`, { cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setParty(await response.json());
} catch (error) {
alert(`Could not load party.json. Make sure this folder is being served with python -m http.server.\n\n${error.message}`);
}
}
async function saveFileDirectly() {
try {
const response = await fetch('/api/party', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: partyJson()
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.ok) throw new Error(result.error || `HTTP ${response.status}`);
alert('Saved directly to party.json. OBS should update within 2 seconds.');
} catch (error) {
alert(`Could not save directly. Start the overlay with server.py instead of python -m http.server.\n\nWindows command: py server.py\n\nBackup option: click Copy JSON or Download party.json.\n\n${error.message}`);
}
}
function saveBrowserPreview() {
localStorage.setItem(STORAGE_KEY, partyJson());
alert('Saved to this browser. To make OBS use this without replacing party.json, set the OBS URL to overlay.html?source=browser');
}
async function copyJson() {
await navigator.clipboard.writeText(partyJson());
alert('Copied. Paste this over the contents of party.json, then save.');
}
function downloadJson() {
const blob = new Blob([partyJson() + '\n'], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'party.json';
link.click();
URL.revokeObjectURL(url);
}
document.getElementById('loadJson').addEventListener('click', loadCurrentJson);
document.getElementById('saveFile').addEventListener('click', saveFileDirectly);
document.getElementById('saveBrowser').addEventListener('click', saveBrowserPreview);
document.getElementById('copyJson').addEventListener('click', copyJson);
document.getElementById('downloadJson').addEventListener('click', downloadJson);
document.getElementById('clearParty').addEventListener('click', () => setParty(Array.from({ length: 6 }, emptySlot)));
const slots = document.getElementById('slots');
for (let i = 0; i < 6; i++) slots.appendChild(makeSlot(i));
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try { setParty(JSON.parse(saved)); }
catch { setParty(sample); }
} else {
setParty(sample);
}
</script>
</body>
</html>