Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards
Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus the Kotlin companion mod (cobblesurvival) and its built-in data pack. - Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and export tooling, Prism instance sync, test server with smoke checks. - Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26), Adrian's biome list, no natural ore, structures without ore or vanilla mobs, no Nether portals, world template builder and world report. - Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap fixes), Revival Charm, expensive Healing Machine, Lootr refreshing loot, in-game self-tests on Minecraft's GameTest framework. - Phase 3 so far: 14 zones with their own spawn pools and level bands (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition), Cold Sweat heat and cold as gates with boss-drop insulation recipes, spawn survey and hazard self-tests in tools/quick_check.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
398301f0be
232 files changed
+72665
No files matched your search
@@ -0,0 +1,375 @@
|
||||
"""HTML field guide from fieldguide.json: index.html, pokemon.html, items.html.
|
||||
|
||||
Pages share assets/guide.css, assets/guide.js and data.js (the JSON, slimmed,
|
||||
as a script so the guide also works when opened from disk). No network access
|
||||
and no external libraries; light and dark mode follow the system setting and
|
||||
can be switched by hand.
|
||||
"""
|
||||
import html
|
||||
import json
|
||||
|
||||
TYPE_COLORS = {
|
||||
"normal": "#9fa19f", "fire": "#e62829", "water": "#2980ef", "electric": "#fac000", "grass": "#3fa129",
|
||||
"ice": "#3dcef3", "fighting": "#ff8000", "poison": "#9141cb", "ground": "#915121", "flying": "#81b9ef",
|
||||
"psychic": "#ef4179", "bug": "#91a119", "rock": "#afa981", "ghost": "#704170", "dragon": "#5060e1",
|
||||
"dark": "#624d4e", "steel": "#60a1b8", "fairy": "#ef70ef",
|
||||
}
|
||||
|
||||
CSS = r"""
|
||||
:root{--bg:#f6f5f1;--panel:#fff;--panel2:#f0eee8;--text:#1d1d1b;--muted:#6b6a64;--line:#dcd9d0;--accent:#c0392b;
|
||||
--ok:#2e7d32;--ok-bg:#e3f1e4;--warn:#9a6700;--warn-bg:#fdf1d6;--bad:#b3261e;--bad-bg:#fbe3e1;--link:#1f5fa8;--slot:#c6c6c6;--slot-line:#8b8b8b}
|
||||
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#17181a;--panel:#212226;--panel2:#2a2b30;--text:#e9e7e1;
|
||||
--muted:#a3a19a;--line:#3a3b41;--accent:#ff6b5b;--ok:#7bd88f;--ok-bg:#1e3323;--warn:#f2c14e;--warn-bg:#3a3018;--bad:#ff8a80;
|
||||
--bad-bg:#3d1f1d;--link:#8ab4f8;--slot:#3b3c42;--slot-line:#5a5b62}}
|
||||
:root[data-theme="dark"]{--bg:#17181a;--panel:#212226;--panel2:#2a2b30;--text:#e9e7e1;--muted:#a3a19a;--line:#3a3b41;--accent:#ff6b5b;
|
||||
--ok:#7bd88f;--ok-bg:#1e3323;--warn:#f2c14e;--warn-bg:#3a3018;--bad:#ff8a80;--bad-bg:#3d1f1d;--link:#8ab4f8;--slot:#3b3c42;--slot-line:#5a5b62}
|
||||
*{box-sizing:border-box}html{-webkit-text-size-adjust:100%}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font:15px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
||||
a{color:var(--link);text-decoration:none}a:hover{text-decoration:underline}
|
||||
header.top{position:sticky;top:0;z-index:5;display:flex;gap:16px;align-items:center;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--line)}
|
||||
header.top .brand{font-weight:700;letter-spacing:.2px;color:var(--text)}header.top nav{display:flex;gap:4px;flex-wrap:wrap}
|
||||
header.top nav a{padding:4px 10px;border-radius:6px;color:var(--muted)}header.top nav a.on{background:var(--panel2);color:var(--text)}
|
||||
header.top .sp{flex:1}button.theme{background:none;border:1px solid var(--line);color:var(--muted);border-radius:6px;padding:3px 8px;cursor:pointer}
|
||||
main{max-width:1280px;margin:0 auto;padding:16px}
|
||||
.layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:16px}
|
||||
@media (max-width:820px){.layout{grid-template-columns:minmax(0,1fr)}aside.side{max-height:none;position:static;order:2}
|
||||
header.top{flex-wrap:wrap;gap:6px 12px}header.top .brand{flex:1 1 100%}header.top .sp{display:none}main{padding:12px}}
|
||||
aside.side{position:sticky;top:62px;align-self:start;max-height:calc(100vh - 80px);overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px}
|
||||
aside.side h4{margin:12px 6px 4px;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
|
||||
aside.side a{display:flex;justify-content:space-between;gap:8px;padding:4px 8px;border-radius:6px;color:var(--text)}
|
||||
aside.side a.on,aside.side a:hover{background:var(--panel2);text-decoration:none}aside.side a small{color:var(--muted)}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:14px}
|
||||
h1{font-size:24px;margin:4px 0 8px}h2{font-size:18px;margin:4px 0 10px}h3{font-size:15px;margin:14px 0 6px}
|
||||
.muted{color:var(--muted)}.small{font-size:13px}
|
||||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}
|
||||
.stat{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px}.stat b{display:block;font-size:26px}
|
||||
.grid-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
|
||||
.loc{display:block;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 12px;color:var(--text)}
|
||||
.loc:hover{border-color:var(--accent);text-decoration:none}.loc .k{font-size:12px;color:var(--muted)}
|
||||
.pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:12px;border:1px solid var(--line);background:var(--panel2);white-space:nowrap}
|
||||
.st-available{color:var(--ok);background:var(--ok-bg);border-color:transparent}.st-conditional{color:var(--warn);background:var(--warn-bg);border-color:transparent}
|
||||
.st-unavailable{color:var(--bad);background:var(--bad-bg);border-color:transparent}
|
||||
.b-common{}.b-uncommon{color:#2e7d9a}.b-rare{color:#7b4bb7}.b-ultra-rare{color:#c2185b}.b-boss{color:var(--accent)}
|
||||
.tbl{width:100%;border-collapse:collapse}.tbl th,.tbl td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.tbl th{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);background:var(--panel)}
|
||||
.tbl td.nw{white-space:nowrap}
|
||||
.tblwrap{overflow-x:auto}
|
||||
.mon{display:flex;align-items:center;gap:8px;min-width:150px}.dot{width:22px;height:22px;border-radius:50%;flex:none;display:grid;place-items:center;font-size:9px;font-weight:700;color:#fff;text-shadow:0 0 2px #0008}
|
||||
.types span{display:inline-block;font-size:11px;padding:0 6px;border-radius:4px;color:#fff;margin-right:3px;text-shadow:0 0 2px #0006}
|
||||
.chips span{display:inline-block;font-size:12px;padding:1px 6px;margin:1px 2px 1px 0;border-radius:4px;background:var(--panel2);border:1px solid var(--line)}
|
||||
.chips .or{background:none;border:none;color:var(--muted)}
|
||||
.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px}
|
||||
.toolbar input,.toolbar select{background:var(--panel);color:var(--text);border:1px solid var(--line);border-radius:6px;padding:6px 8px;font:inherit}
|
||||
.toolbar input{min-width:0;flex:1 1 200px}.toolbar input[type=checkbox]{flex:none}.toolbar label{font-size:13px;color:var(--muted);display:flex;gap:4px;align-items:center}
|
||||
.items{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px}
|
||||
.item{display:flex;gap:8px;align-items:center;padding:6px 8px;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--text)}
|
||||
.item:hover{border-color:var(--accent);text-decoration:none}.item .n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.ico{width:32px;height:32px;image-rendering:pixelated;flex:none}.ico.big{width:64px;height:64px}
|
||||
.noico{width:32px;height:32px;border-radius:6px;background:var(--panel2);border:1px dashed var(--line);flex:none;display:grid;place-items:center;font-size:10px;color:var(--muted)}
|
||||
.sdot{width:9px;height:9px;border-radius:50%;flex:none}.sdot.available{background:var(--ok)}.sdot.conditional{background:var(--warn)}.sdot.unavailable{background:var(--bad)}
|
||||
.recipes{display:flex;flex-wrap:wrap;gap:10px}
|
||||
.recipe{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 10px}
|
||||
.recipe .lbl{font-size:12px;color:var(--muted);margin-bottom:6px;display:flex;gap:6px;align-items:center}
|
||||
.craft{display:flex;align-items:center;gap:10px}
|
||||
.cgrid{display:grid;gap:2px}.slot{width:38px;height:38px;background:var(--slot);border:1px solid var(--slot-line);display:grid;place-items:center;position:relative}
|
||||
.slot img{width:32px;height:32px;image-rendering:pixelated}.slot .cnt{position:absolute;right:1px;bottom:-1px;font-size:11px;font-weight:700;color:#fff;text-shadow:1px 1px 0 #000}
|
||||
.slot .alt{position:absolute;left:1px;top:-2px;font-size:10px;color:var(--muted)}
|
||||
.arrow{font-size:22px;color:var(--muted)}
|
||||
.src{display:flex;gap:10px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--line)}.src:last-child{border-bottom:none}
|
||||
.src .kind{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--muted);width:92px;flex:none;padding-top:2px}
|
||||
.src .body{flex:1;min-width:0}.blocked{color:var(--bad);font-size:13px}
|
||||
.banner{padding:10px 12px;border-radius:8px;margin:10px 0}.banner.unavailable{background:var(--bad-bg);color:var(--bad)}
|
||||
.banner.conditional{background:var(--warn-bg);color:var(--warn)}.banner.available{background:var(--ok-bg);color:var(--ok)}
|
||||
.head{display:flex;gap:14px;align-items:center}.head .id{font-family:ui-monospace,monospace;font-size:12px;color:var(--muted)}
|
||||
details summary{cursor:pointer;color:var(--muted)}code{font-family:ui-monospace,monospace;font-size:12px}
|
||||
.foot{color:var(--muted);font-size:12px;margin:24px 0 8px}
|
||||
"""
|
||||
|
||||
JS = r"""
|
||||
const FG = window.FG;
|
||||
const $ = (s, el = document) => el.querySelector(s);
|
||||
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c]));
|
||||
const TYPE = %TYPES%;
|
||||
const BUCKET_ORDER = {'common': 0, 'uncommon': 1, 'rare': 2, 'ultra-rare': 3, 'boss': 4};
|
||||
const POS = {grounded: 'land', submerged: 'underwater', surface: 'water surface', seafloor: 'sea floor', fishing: 'fishing', lava_surface: 'lava'};
|
||||
const STATUS_TEXT = {available: 'on the island', conditional: 'conditional', unavailable: 'no source here'};
|
||||
function store(k, v) { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) { return null; } }
|
||||
function initTheme() {
|
||||
const t = store('fg-theme'); if (t) document.documentElement.dataset.theme = t;
|
||||
const b = $('button.theme'); if (!b) return;
|
||||
b.onclick = () => { const cur = document.documentElement.dataset.theme ||
|
||||
(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
const n = cur === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = n; store('fg-theme', n); };
|
||||
}
|
||||
function hashArgs() { const o = {}; location.hash.slice(1).split('&').forEach(p => { const [k, v] = p.split('='); if (k) o[k] = decodeURIComponent(v || ''); }); return o; }
|
||||
function pill(status, text) { return `<span class="pill st-${status}">${esc(text || STATUS_TEXT[status] || status)}</span>`; }
|
||||
function itemInfo(id) { return FG.items[id] || FG.vanilla[id] || {name: id.split(':')[1].replace(/_/g, ' '), status: 'available'}; }
|
||||
function icon(id, cls = 'ico') { const it = itemInfo(id); return it.icon ? `<img class="${cls}" src="${esc(it.icon)}" alt="">` : `<span class="noico">?</span>`; }
|
||||
function itemLink(id) { const it = itemInfo(id); return `<a href="items.html#item=${encodeURIComponent(id)}" title="${esc(id)}">${esc(it.name)}</a>`; }
|
||||
function monDot(sid) {
|
||||
const sp = FG.species[sid] || {types: []}; const c = sp.types.map(t => TYPE[t] || '#888');
|
||||
const bg = c.length > 1 ? `linear-gradient(135deg,${c[0]} 50%,${c[1]} 50%)` : (c[0] || '#888');
|
||||
return `<span class="dot" style="background:${bg}">${sp.dex || ''}</span>`;
|
||||
}
|
||||
function monLink(sid, extra) { const sp = FG.species[sid]; return `<a href="pokemon.html#species=${encodeURIComponent(sid)}">${esc(sp ? sp.name : sid)}</a>${extra ? ' <span class="muted small">' + esc(extra) + '</span>' : ''}`; }
|
||||
function types(sid) { const sp = FG.species[sid]; return sp ? `<span class="types">${sp.types.map(t => `<span style="background:${TYPE[t]}">${t}</span>`).join('')}</span>` : ''; }
|
||||
function chips(list) { return `<span class="chips">${list.map(t => `<span>${esc(t)}</span>`).join('')}</span>`; }
|
||||
function bucket(b) { const pct = FG.meta.buckets[b]; return `<span class="b-${b}">${esc(b === 'boss' ? 'alpha' : b)}</span>${pct !== undefined ? `<span class="muted small"> (${pct}%)</span>` : ''}`; }
|
||||
|
||||
// ---------- sources and uses ----------
|
||||
function slot(ing, count) {
|
||||
if (!ing) return '<div class="slot"></div>';
|
||||
const id = ing.items[0]; const alts = ing.items.length;
|
||||
const title = (ing.tag ? ing.tag + ': ' : '') + ing.items.slice(0, 12).map(i => itemInfo(i).name).join(', ') + (alts > 12 ? ' …' : '');
|
||||
return `<a class="slot" href="items.html#item=${encodeURIComponent(id || '')}" title="${esc(title)}">${id ? icon(id, '') : ''}${alts > 1 ? '<span class="alt">+</span>' : ''}${count > 1 ? `<span class="cnt">${count}</span>` : ''}</a>`;
|
||||
}
|
||||
function recipeHtml(rid) {
|
||||
const r = FG.recipes[rid]; if (!r) return `<code>${esc(rid)}</code>`;
|
||||
let left;
|
||||
if (r.grid) {
|
||||
const w = Math.max(...r.grid.map(row => row.length));
|
||||
left = `<div class="cgrid" style="grid-template-columns:repeat(${w},38px)">${r.grid.map(row => { const cells = []; for (let i = 0; i < w; i++) cells.push(slot(row[i])); return cells.join(''); }).join('')}</div>`;
|
||||
} else {
|
||||
const n = r.slots.length; const w = n > 3 ? 3 : n;
|
||||
left = `<div class="cgrid" style="grid-template-columns:repeat(${w},38px)">${r.slots.map(s => slot(s)).join('')}</div>`;
|
||||
}
|
||||
return `<div class="recipe"><div class="lbl">${pill(r.status, '')}<span>${esc(r.label)}</span></div><div class="craft">${left}<span class="arrow">→</span>${slot({items: [r.result]}, r.count)}</div><div class="muted small" title="${esc(rid)}">${esc(itemInfo(r.result).name)}</div></div>`;
|
||||
}
|
||||
function sourceHtml(s) {
|
||||
let body = esc(s.text);
|
||||
if (s.chance) body += ` <span class="muted small">${esc(s.chance)}</span>`;
|
||||
if (s.species) body = body.replace(esc(FG.species[s.species]?.name || '\u0000'), monLink(s.species));
|
||||
if (s.blocked && s.blocked.length && s.status !== 'available') body += `<div class="blocked">Needs ${s.blocked.map(esc).join('; ')}</div>`;
|
||||
return `<div class="src"><div class="kind">${esc(s.kind)}</div><div class="body">${body}</div>${pill(s.status, '')}</div>`;
|
||||
}
|
||||
|
||||
// ---------- pokemon page ----------
|
||||
function pokemonPage() {
|
||||
const side = $('#side'); const main = $('#main');
|
||||
const kinds = [['surface', 'Surface'], ['cave', 'Caves'], ['water', 'Water & coast'], ['end', 'The End (conditional)']];
|
||||
let s = `<div class="toolbar"><input id="q" placeholder="Find a Pokémon…" list="monlist"></div>`;
|
||||
s += `<datalist id="monlist">${Object.entries(FG.species).map(([id, sp]) => `<option value="${esc(sp.name)}">`).join('')}</datalist>`;
|
||||
for (const [k, label] of kinds) {
|
||||
const locs = FG.locations.filter(l => l.kind === k); if (!locs.length) continue;
|
||||
s += `<h4>${label}</h4>` + locs.map(l => `<a href="#loc=${encodeURIComponent(l.id)}" data-loc="${esc(l.id)}">${esc(l.name)}<small>${new Set(l.spawns.flatMap(i => FG.spawns[i].m.map(m => m[0]))).size}</small></a>`).join('');
|
||||
}
|
||||
const missing = Object.values(FG.species).filter(sp => sp.wild === 'unavailable').length;
|
||||
s += `<h4>Elsewhere</h4><a href="#missing">Not on the island<small>${missing}</small></a>`;
|
||||
side.innerHTML = s;
|
||||
$('#q').addEventListener('change', e => { const v = e.target.value.toLowerCase(); const hit = Object.entries(FG.species).find(([id, sp]) => sp.name.toLowerCase() === v || id === v); if (hit) location.hash = 'species=' + encodeURIComponent(hit[0]); });
|
||||
const route = () => {
|
||||
const a = hashArgs();
|
||||
side.querySelectorAll('a').forEach(x => x.classList.toggle('on', x.getAttribute('href') === location.hash));
|
||||
if (a.species) main.innerHTML = speciesView(a.species);
|
||||
else if ('missing' in a) main.innerHTML = missingView();
|
||||
else main.innerHTML = locationView(a.loc || FG.locations[0].id, a);
|
||||
window.scrollTo(0, 0);
|
||||
main.querySelectorAll('[data-filter]').forEach(el => el.addEventListener('input', () => filterRows(main)));
|
||||
filterRows(main);
|
||||
};
|
||||
addEventListener('hashchange', route); route();
|
||||
}
|
||||
function filterRows(main) {
|
||||
const b = $('#fb', main), p = $('#fp', main), q = $('#fq', main), x = $('#fx', main); if (!b) return;
|
||||
main.querySelectorAll('tbody tr').forEach(tr => {
|
||||
const ok = (!b.value || tr.dataset.b === b.value) && (!p.value || tr.dataset.p === p.value) && (!q.value || tr.dataset.n.includes(q.value.toLowerCase())) && !(x.checked && tr.dataset.x === 'true');
|
||||
tr.style.display = ok ? '' : 'none';
|
||||
});
|
||||
}
|
||||
function locationView(id) {
|
||||
const l = FG.locations.find(x => x.id === id); if (!l) return '<p>Unknown location.</p>';
|
||||
const rows = l.spawns.map(i => FG.spawns[i]).sort((a, b) => (a.x - b.x) || (BUCKET_ORDER[a.b] - BUCKET_ORDER[b.b]) || (b.w - a.w));
|
||||
const species = new Set(rows.flatMap(r => r.m.map(m => m[0])));
|
||||
let h = `<div class="card"><h1>${esc(l.name)}</h1><div class="muted">${esc(l.kind === 'cave' ? 'Cave biome' : l.kind === 'water' ? 'Water / coast biome' : l.kind === 'end' ? 'The End' : 'Surface biome')} · ${species.size} Pokémon · ${rows.length} spawn entries · <code>${esc(l.id)}</code></div>`;
|
||||
if (l.status !== 'available') h += `<div class="banner ${l.status}">${esc(l.reason || STATUS_TEXT[l.status])}</div>`;
|
||||
h += `<p class="small muted">Rarity: Cobblemon first picks a bucket (chance in brackets), then a spawn inside it by weight. Herds and alphas spawn as groups.</p></div>`;
|
||||
h += `<div class="card"><div class="toolbar"><input id="fq" data-filter placeholder="Filter by name"><label>Rarity <select id="fb" data-filter><option value="">all</option>${Object.keys(BUCKET_ORDER).map(b => `<option value="${b}">${b === 'boss' ? 'alpha' : b}</option>`).join('')}</select></label><label><input type="checkbox" id="fx" data-filter> hide spawns that need special blocks or structures</label><label>Where <select id="fp" data-filter><option value="">all</option>${Object.entries(POS).map(([k, v]) => `<option value="${k}">${v}</option>`).join('')}</select></label></div>`;
|
||||
h += `<div class="tblwrap"><table class="tbl"><thead><tr><th>Pokémon</th><th>Level</th><th>Rarity · weight</th><th>Where</th><th>Conditions</th></tr></thead><tbody>`;
|
||||
for (const r of rows) {
|
||||
const mons = r.m.map(m => `<div class="mon">${monDot(m[0])}<div>${monLink(m[0], m[1])}${r.m.length > 1 ? ` <span class="muted small">${m[3]} · Lv ${m[2][0]}–${m[2][1]}</span>` : ''}</div></div>`).join('');
|
||||
const conds = r.c.map(c => c.length ? chips(c) : '<span class="muted small">anywhere</span>').join('<span class="muted small"> or </span>');
|
||||
const anti = r.a.length ? `<div class="small muted">not: ${r.a.map(a => esc(a.join(', '))).join('; ')}</div>` : '';
|
||||
const mult = r.mul.length ? `<div class="small muted">${r.mul.map(x => `×${x.multiplier} when ${esc(x.when.join(', '))}`).join('; ')}</div>` : '';
|
||||
h += `<tr data-x="${r.x}" data-b="${r.b}" data-p="${r.p}" data-n="${esc(r.m.map(m => (FG.species[m[0]]?.name || m[0]).toLowerCase()).join(' '))}"><td>${mons}${r.t === 'pokemon-herd' ? `<span class="pill">herd up to ${r.h}</span>` : ''}</td><td class="nw">${r.lv[0]}–${r.lv[1]}</td><td>${bucket(r.b)}<div class="muted small">weight ${r.w}</div></td><td>${esc(POS[r.p] || r.p)}</td><td>${conds}${anti}${mult}${r.st !== 'available' ? pill(r.st) : ''}</td></tr>`;
|
||||
}
|
||||
return h + '</tbody></table></div></div>';
|
||||
}
|
||||
function speciesView(sid) {
|
||||
const sp = FG.species[sid]; if (!sp) return '<p>Unknown species.</p>';
|
||||
let h = `<div class="card"><div class="head">${monDot(sid)}<div><h1>${esc(sp.name)} <span class="muted">#${sp.dex}</span></h1>${types(sid)} <span class="id">${esc(sid)}</span></div></div>`;
|
||||
h += `<div class="banner ${sp.status}">${sp.wild !== 'unavailable' ? 'Found in the wild on the island.' : sp.status !== 'unavailable' ? 'Not found in the wild here, but obtainable another way (below).' : sp.implemented ? 'No way to get it on this island.' : 'Not implemented in Cobblemon (no model), so it cannot be obtained.'}</div></div>`;
|
||||
if (sp.locations.length) {
|
||||
h += `<div class="card"><h2>Where to find it</h2><table class="tbl"><thead><tr><th>Location</th><th>Level</th><th>Rarity</th></tr></thead><tbody>`;
|
||||
h += sp.locations.map(l => `<tr><td><a href="#loc=${encodeURIComponent(l.biome)}">${esc(l.name)}</a>${l.zones && l.zones.length ? ` <span class="muted small">${esc(l.zones.join(', '))}</span>` : ''}${l.special ? ' <span class="muted small">only near particular blocks</span>' : ''}</td><td>${l.level[0]}–${l.level[1]}</td><td>${l.buckets.map(bucket).join(', ')}</td></tr>`).join('');
|
||||
h += `</tbody></table><details><summary>All ${sp.spawns.length} spawn entries with conditions</summary>${sp.spawns.map(i => FG.spawns[i]).map(r => `<div class="src"><div class="kind">${esc(r.id)}</div><div class="body">${bucket(r.b)} · weight ${r.w} · Lv ${r.lv[0]}–${r.lv[1]} · ${esc(POS[r.p] || r.p)}<br>${r.c.map(c => chips(c)).join(' or ')}<div class="muted small">${r.loc.map(i => esc(FG.locations[i]?.name || '')).join(', ') || 'no island biome'}</div></div>${pill(r.st)}</div>`).join('')}</details></div>`;
|
||||
} else if (sp.why_not_wild.length) {
|
||||
h += `<div class="card"><h2>Why it is not wild here</h2><ul>${sp.why_not_wild.map(w => `<li>${esc(w)}</li>`).join('')}</ul></div>`;
|
||||
} else if (!sp.spawns.length) {
|
||||
h += `<div class="card"><h2>Wild spawns</h2><p class="muted">Cobblemon has no wild spawn for it.</p></div>`;
|
||||
}
|
||||
if (sp.sources.length) h += `<div class="card"><h2>Other ways to get it</h2>${sp.sources.map(sourceHtml).join('')}</div>`;
|
||||
if (sp.evolutions.length) h += `<div class="card"><h2>Evolves into</h2>${sp.evolutions.map(e => `<div class="src"><div class="kind">evolution</div><div class="body">${monLink(e.to)}${e.form ? ` <span class="muted small">(${esc(e.form)} form)</span>` : ''}: ${esc(e.how.join(', ') || 'level up')}</div></div>`).join('')}</div>`;
|
||||
if (sp.drops.length) h += `<div class="card"><h2>Drops (when defeated in the wild)</h2><div class="items">${sp.drops.map(d => `<a class="item" href="items.html#item=${encodeURIComponent(d.item)}">${icon(d.item)}<span class="n">${esc(itemInfo(d.item).name)}</span><span class="muted small">${esc(d.chance)}</span></a>`).join('')}</div></div>`;
|
||||
return h;
|
||||
}
|
||||
function missingView() {
|
||||
const list = Object.entries(FG.species).filter(([id, sp]) => sp.wild === 'unavailable').sort((a, b) => a[1].dex - b[1].dex);
|
||||
let h = `<div class="card"><h1>Not found in the wild on the island</h1><p class="muted">${list.length} species. Green: obtainable another way (evolution, fossil, starter, trade). Red: no way on this island.</p></div><div class="card"><div class="tblwrap"><table class="tbl"><thead><tr><th>Pokémon</th><th>How to get it here</th><th>Wild spawns need</th></tr></thead><tbody>`;
|
||||
for (const [id, sp] of list) {
|
||||
const how = sp.sources.filter(s => s.status !== 'unavailable').slice(0, 3).map(s => esc(s.text)).join('<br>') || (sp.implemented ? '' : '<span class="muted">not implemented in Cobblemon</span>');
|
||||
h += `<tr><td><div class="mon">${monDot(id)}${monLink(id)}</div></td><td>${how} ${pill(sp.status, '')}</td><td class="small muted">${esc(sp.why_not_wild.slice(0, 3).join('; ')) || (sp.spawns.length ? '' : 'no wild spawn in Cobblemon')}</td></tr>`;
|
||||
}
|
||||
return h + '</tbody></table></div></div>';
|
||||
}
|
||||
|
||||
// ---------- items page ----------
|
||||
function itemsPage() {
|
||||
const main = $('#main');
|
||||
const cats = [...new Set(Object.values(FG.items).map(i => i.category))].sort();
|
||||
const mods = [...new Set(Object.values(FG.items).map(i => i.mod))].sort();
|
||||
const route = () => {
|
||||
const a = hashArgs();
|
||||
if (a.item) { main.innerHTML = itemView(a.item); window.scrollTo(0, 0); return; }
|
||||
main.innerHTML = `<div class="card"><h1>Items</h1><p class="muted">Every item the pack's mods add, with how to get it on the island. Red items have no source here.</p><div class="toolbar"><input id="iq" placeholder="Search items…" value="${esc(a.q || '')}"><select id="ic"><option value="">All categories</option>${cats.map(c => `<option${a.cat === c ? ' selected' : ''}>${esc(c)}</option>`).join('')}</select><select id="im"><option value="">All mods</option>${mods.map(c => `<option${a.mod === c ? ' selected' : ''}>${esc(c)}</option>`).join('')}</select><select id="is"><option value="">Any status</option><option value="available">On the island</option><option value="conditional">Conditional</option><option value="unavailable"${a.status === 'unavailable' ? ' selected' : ''}>No source here</option></select></div><div id="count" class="muted small"></div></div><div class="items" id="list"></div>`;
|
||||
const draw = () => {
|
||||
const q = $('#iq').value.toLowerCase(), c = $('#ic').value, m = $('#im').value, s = $('#is').value;
|
||||
const list = Object.entries(FG.items).filter(([id, it]) => (!q || it.name.toLowerCase().includes(q) || id.includes(q)) && (!c || it.category === c) && (!m || it.mod === m) && (!s || (it.status === s && !(s === 'unavailable' && it.technical))))
|
||||
.sort((x, y) => x[1].category.localeCompare(y[1].category) || x[1].name.localeCompare(y[1].name));
|
||||
$('#count').textContent = `${list.length} items`;
|
||||
$('#list').innerHTML = list.map(([id, it]) => `<a class="item" href="#item=${encodeURIComponent(id)}" title="${esc(id)}">${icon(id)}<span class="n">${esc(it.name)}</span><span class="sdot ${it.technical ? '' : it.status}" title="${esc(it.technical ? 'creative/admin item' : STATUS_TEXT[it.status])}"></span></a>`).join('');
|
||||
};
|
||||
['#iq', '#ic', '#im', '#is'].forEach(sel => $(sel).addEventListener('input', draw)); draw();
|
||||
};
|
||||
addEventListener('hashchange', route); route();
|
||||
}
|
||||
function itemView(id) {
|
||||
const it = itemInfo(id); const isMod = !!FG.items[id];
|
||||
let h = `<p><a href="items.html">← All items</a></p><div class="card"><div class="head">${icon(id, 'ico big')}<div><h1>${esc(it.name)}</h1><span class="id">${esc(id)}</span> ${it.category ? `<span class="pill">${esc(it.category)}</span>` : ''}</div></div>`;
|
||||
if (it.tooltip && it.tooltip.length) h += `<p>${it.tooltip.map(esc).join('<br>')}</p>`;
|
||||
const msg = it.technical ? 'Creative or admin item; not meant to be obtained in survival.' : it.status === 'available' ? 'Obtainable on the island.' : it.status === 'conditional' ? 'Only obtainable under a condition (e.g. reaching the End).' : 'No source on this island' + (it.reason ? ': ' + it.reason + '.' : '.');
|
||||
h += `<div class="banner ${it.technical ? 'conditional' : it.status}">${msg}</div></div>`;
|
||||
const srcs = it.sources || [];
|
||||
const recs = srcs.filter(s => s.recipe), other = srcs.filter(s => !s.recipe);
|
||||
h += `<div class="card"><h2>How to get it</h2>`;
|
||||
if (!srcs.length) h += `<p class="muted">No recipe, loot table, drop or worldgen feature produces it.</p>`;
|
||||
if (recs.length) h += `<div class="recipes">${recs.map(s => recipeHtml(s.recipe)).join('')}</div>`;
|
||||
if (other.length) h += `<div style="margin-top:8px">${other.map(sourceHtml).join('')}</div>`;
|
||||
h += `</div>`;
|
||||
const uses = (FG.items[id] && FG.items[id].uses) || [];
|
||||
if (isMod || uses.length) {
|
||||
const ur = uses.filter(u => u.recipe), uo = uses.filter(u => !u.recipe);
|
||||
h += `<div class="card"><h2>What it is used for</h2>`;
|
||||
if (!uses.length) h += `<p class="muted">No recipe or Cobblemon mechanic uses it.</p>`;
|
||||
if (uo.length) h += uo.map(u => `<div class="src"><div class="kind">${esc(u.kind)}</div><div class="body">${u.species ? esc(u.text).replace(esc(FG.species[u.species]?.name || '\u0000'), monLink(u.species)) : esc(u.text)}</div></div>`).join('');
|
||||
if (ur.length) h += `<h3>Recipes that use it (${ur.length})</h3><div class="recipes">${ur.slice(0, 60).map(u => recipeHtml(u.recipe)).join('')}</div>${ur.length > 60 ? `<p class="muted">… and ${ur.length - 60} more.</p>` : ''}`;
|
||||
h += `</div>`;
|
||||
}
|
||||
if (it.tags && it.tags.length) h += `<div class="card"><h3>Tags</h3>${chips(it.tags)}</div>`;
|
||||
return h;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTheme();
|
||||
const page = document.body.dataset.page;
|
||||
if (page === 'pokemon') pokemonPage();
|
||||
if (page === 'items') itemsPage();
|
||||
});
|
||||
"""
|
||||
|
||||
|
||||
def slim(data):
|
||||
"""The JSON as the pages need it (spawns as short records, location indices)."""
|
||||
loc_index = {loc["id"]: i for i, loc in enumerate(data["locations"])}
|
||||
spawns = []
|
||||
for s in data["spawns"]:
|
||||
spawns.append({
|
||||
"id": s["id"], "t": s["type"], "b": s["bucket"], "w": s["weight"], "lv": s["level"], "p": s["position"],
|
||||
"h": s["max_herd"], "st": s["status"],
|
||||
"m": [[m["species"], " ".join(p for p in m["props"] if not p.startswith(("level", "levelRange"))),
|
||||
m["level"], m["role"]] for m in s["members"]],
|
||||
"c": [c["text"] for c in s["conditions"] if c["biomes"]] or [c["text"] for c in s["conditions"]],
|
||||
"a": s["anticonditions"], "mul": s["multipliers"],
|
||||
"loc": [loc_index[b] for b in s["biomes"] if b in loc_index],
|
||||
# needs particular blocks or a structure nearby: listed after the plain spawns
|
||||
"x": s["needs_blocks"],
|
||||
})
|
||||
species = {k: {kk: v[kk] for kk in ("name", "dex", "types", "implemented", "status", "wild", "locations", "spawns",
|
||||
"sources", "evolutions", "drops", "why_not_wild")}
|
||||
for k, v in data["species"].items()}
|
||||
return {"meta": data["meta"], "locations": data["locations"], "spawns": spawns, "species": species,
|
||||
"items": data["items"], "vanilla": data["vanilla"], "recipes": data["recipes"]}
|
||||
|
||||
|
||||
def page(title, active, body, page_id=""):
|
||||
nav = "".join(f'<a href="{href}"{" class=on" if active == href else ""}>{label}</a>'
|
||||
for href, label in (("index.html", "Overview"), ("pokemon.html", "Pokémon"), ("items.html", "Items")))
|
||||
return f"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{html.escape(title)}</title><link rel="stylesheet" href="assets/guide.css"></head>
|
||||
<body data-page="{page_id}"><header class="top"><a class="brand" href="index.html">Cobblemon Survival · Field Guide</a><nav>{nav}</nav>
|
||||
<span class="sp"></span><button class="theme" type="button" title="Switch light/dark">◐</button></header>
|
||||
{body}
|
||||
<script src="data.js"></script><script src="assets/guide.js"></script></body></html>
|
||||
"""
|
||||
|
||||
|
||||
def index_body(data):
|
||||
e = html.escape
|
||||
species = data["species"]
|
||||
wild = sum(1 for s in species.values() if s["wild"] == "available")
|
||||
other = sum(1 for s in species.values() if s["wild"] != "available" and s["status"] == "available")
|
||||
cond = sum(1 for s in species.values() if s["status"] == "conditional")
|
||||
none = sum(1 for s in species.values() if s["status"] == "unavailable")
|
||||
items = data["items"]
|
||||
nosrc = sorted((i for i, v in items.items() if v["status"] == "unavailable" and not v["technical"]),
|
||||
key=lambda i: (items[i]["category"], items[i]["name"]))
|
||||
used_nosrc = [i for i in nosrc if items[i]["uses"]]
|
||||
cards = []
|
||||
for loc in data["locations"]:
|
||||
n = len({m["species"] for i in loc["spawns"] for m in data["spawns"][i]["members"]})
|
||||
kind = {"surface": "Surface", "cave": "Cave", "water": "Water / coast", "end": "The End"}.get(loc["kind"], loc["kind"])
|
||||
cards.append(f'<a class="loc" href="pokemon.html#loc={e(loc["id"])}"><b>{e(loc["name"])}</b><div class="k">{kind} · {n} Pokémon'
|
||||
f'{" · conditional" if loc["status"] != "available" else ""}</div></a>')
|
||||
|
||||
def item_row(i):
|
||||
it = items[i]
|
||||
img = f'<img class="ico" src="{e(it["icon"])}" alt="">' if it.get("icon") else '<span class="noico">?</span>'
|
||||
why = it.get("reason") or "no source"
|
||||
return (f'<a class="item" href="items.html#item={e(i)}" title="{e(i)}">{img}<span class="n">{e(it["name"])}'
|
||||
f'<br><span class="muted small">{e(why)}</span></span></a>')
|
||||
|
||||
problems = "".join(f"<details><summary>{e(k)} ({len(v)})</summary><p class='small'><code>{e(', '.join(map(str, v[:400])))}</code></p></details>"
|
||||
for k, v in data["problems"].items() if v)
|
||||
struct = "".join(f"<tr><td>{e(s['name'])}</td><td><span class='pill st-{s['status']}'>{s['status']}</span></td><td class='small muted'>{e(s['reason'])}</td></tr>"
|
||||
for s in data["island"]["structures"] if s["status"] != "unavailable")
|
||||
return f"""<main>
|
||||
<div class="card"><h1>Field guide</h1><p>Where to find every Pokémon on the island, how to get every item the mods add, and what each item is for,
|
||||
generated from the pack's own data (Cobblemon spawn pools, species, recipes and loot tables, with <code>datapacks/core</code> on top).
|
||||
Generated {e(data['meta']['generated'])}.</p>
|
||||
<div class="stats"><div class="stat"><b>{wild}</b>Pokémon in the wild here</div><div class="stat"><b>{other}</b>more by evolution, fossils, starters</div>
|
||||
<div class="stat"><b>{cond}</b>only in the End</div>
|
||||
<div class="stat"><b>{none}</b>Pokémon with no way to get them here</div><div class="stat"><b>{len(items)}</b>mod items</div>
|
||||
<div class="stat"><b>{len(nosrc)}</b>items with no source here</div></div>
|
||||
<p class="small muted">Statuses: <span class="pill st-available">on the island</span> <span class="pill st-conditional">conditional</span> needs something that may not exist (the End, a stronghold inside the border) <span class="pill st-unavailable">no source here</span> ore, Nether or missing biome only.</p></div>
|
||||
<div class="card"><h2>Locations</h2><div class="grid-cards">{''.join(cards)}</div>
|
||||
<p><a href="pokemon.html#missing">Pokémon not found in the wild on the island →</a></p></div>
|
||||
<div class="card"><h2>Items with no source on the island ({len(nosrc)})</h2>
|
||||
<p class="muted small">{len(used_nosrc)} of them are used by a recipe or a Cobblemon mechanic. Creative/admin items are left out.</p>
|
||||
<div class="items">{''.join(item_row(i) for i in nosrc)}</div></div>
|
||||
<div class="card"><h2>Structures that can generate</h2><div class="tblwrap"><table class="tbl"><tbody>{struct}</tbody></table></div></div>
|
||||
<div class="card"><h2>What the generator could not resolve</h2>{problems}</div>
|
||||
<p class="foot">Unofficial fan project, not affiliated with Nintendo, Game Freak, Creatures Inc., The Pokémon Company, the Cobblemon team, Mojang Studios or Microsoft.
|
||||
Icons are taken from the installed mods at build time and are their authors' work.</p>
|
||||
</main>"""
|
||||
|
||||
|
||||
def write(data, out):
|
||||
(out / "assets").mkdir(parents=True, exist_ok=True)
|
||||
(out / "assets/guide.css").write_text(CSS)
|
||||
(out / "assets/guide.js").write_text(JS.replace("%TYPES%", json.dumps(TYPE_COLORS)))
|
||||
(out / "data.js").write_text("window.FG=" + json.dumps(slim(data), ensure_ascii=False, separators=(",", ":")) + ";\n")
|
||||
(out / "index.html").write_text(page("Field Guide", "index.html", index_body(data)))
|
||||
(out / "pokemon.html").write_text(page("Pokémon by location · Field Guide", "pokemon.html",
|
||||
'<main><div class="layout"><aside class="side" id="side"></aside><section id="main"></section></div></main>',
|
||||
"pokemon"))
|
||||
(out / "items.html").write_text(page("Items · Field Guide", "items.html", '<main id="main"></main>', "items"))
|
||||
Reference in new issue
Block a user