"""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 `${esc(text || STATUS_TEXT[status] || status)}`; } 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 ? `` : `?`; } function itemLink(id) { const it = itemInfo(id); return `${esc(it.name)}`; } 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 `${sp.dex || ''}`; } function monLink(sid, extra) { const sp = FG.species[sid]; return `${esc(sp ? sp.name : sid)}${extra ? ' ' + esc(extra) + '' : ''}`; } function types(sid) { const sp = FG.species[sid]; return sp ? `${sp.types.map(t => `${t}`).join('')}` : ''; } function chips(list) { return `${list.map(t => `${esc(t)}`).join('')}`; } function bucket(b) { const pct = FG.meta.buckets[b]; return `${esc(b === 'boss' ? 'alpha' : b)}${pct !== undefined ? ` (${pct}%)` : ''}`; } // ---------- sources and uses ---------- function slot(ing, count) { if (!ing) return '
'; 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 `${id ? icon(id, '') : ''}${alts > 1 ? '+' : ''}${count > 1 ? `${count}` : ''}`; } function recipeHtml(rid) { const r = FG.recipes[rid]; if (!r) return `${esc(rid)}`; let left; if (r.grid) { const w = Math.max(...r.grid.map(row => row.length)); left = `
${r.grid.map(row => { const cells = []; for (let i = 0; i < w; i++) cells.push(slot(row[i])); return cells.join(''); }).join('')}
`; } else { const n = r.slots.length; const w = n > 3 ? 3 : n; left = `
${r.slots.map(s => slot(s)).join('')}
`; } return `
${pill(r.status, '')}${esc(r.label)}
${left}→${slot({items: [r.result]}, r.count)}
${esc(itemInfo(r.result).name)}
`; } function sourceHtml(s) { let body = esc(s.text); if (s.chance) body += ` ${esc(s.chance)}`; 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 += `
Needs ${s.blocked.map(esc).join('; ')}
`; return `
${esc(s.kind)}
${body}
${pill(s.status, '')}
`; } // ---------- 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 = `
`; s += `${Object.entries(FG.species).map(([id, sp]) => ``; for (const [k, label] of kinds) { const locs = FG.locations.filter(l => l.kind === k); if (!locs.length) continue; s += `

${label}

` + locs.map(l => `${esc(l.name)}${new Set(l.spawns.flatMap(i => FG.spawns[i].m.map(m => m[0]))).size}`).join(''); } const missing = Object.values(FG.species).filter(sp => sp.wild === 'unavailable').length; s += `

Elsewhere

Not on the island${missing}`; 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 '

Unknown location.

'; 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 = `

${esc(l.name)}

${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 · ${esc(l.id)}
`; if (l.status !== 'available') h += ``; h += `

Rarity: Cobblemon first picks a bucket (chance in brackets), then a spawn inside it by weight. Herds and alphas spawn as groups.

`; h += `
`; h += `
`; for (const r of rows) { const mons = r.m.map(m => `
${monDot(m[0])}
${monLink(m[0], m[1])}${r.m.length > 1 ? ` ${m[3]} · Lv ${m[2][0]}–${m[2][1]}` : ''}
`).join(''); const conds = r.c.map(c => c.length ? chips(c) : 'anywhere').join(' or '); const anti = r.a.length ? `
not: ${r.a.map(a => esc(a.join(', '))).join('; ')}
` : ''; const mult = r.mul.length ? `
${r.mul.map(x => `×${x.multiplier} when ${esc(x.when.join(', '))}`).join('; ')}
` : ''; h += ``; } return h + '
PokémonLevelRarity · weightWhereConditions
${mons}${r.t === 'pokemon-herd' ? `herd up to ${r.h}` : ''}${r.lv[0]}–${r.lv[1]}${bucket(r.b)}
weight ${r.w}
${esc(POS[r.p] || r.p)}${conds}${anti}${mult}${r.st !== 'available' ? pill(r.st) : ''}
'; } function speciesView(sid) { const sp = FG.species[sid]; if (!sp) return '

Unknown species.

'; let h = `
${monDot(sid)}

${esc(sp.name)} #${sp.dex}

${types(sid)} ${esc(sid)}
`; h += `
`; if (sp.locations.length) { h += `

Where to find it

`; h += sp.locations.map(l => ``).join(''); h += `
LocationLevelRarity
${esc(l.name)}${l.zones && l.zones.length ? ` ${esc(l.zones.join(', '))}` : ''}${l.special ? ' only near particular blocks' : ''}${l.level[0]}–${l.level[1]}${l.buckets.map(bucket).join(', ')}
All ${sp.spawns.length} spawn entries with conditions${sp.spawns.map(i => FG.spawns[i]).map(r => `
${esc(r.id)}
${bucket(r.b)} · weight ${r.w} · Lv ${r.lv[0]}–${r.lv[1]} · ${esc(POS[r.p] || r.p)}
${r.c.map(c => chips(c)).join(' or ')}
${r.loc.map(i => esc(FG.locations[i]?.name || '')).join(', ') || 'no island biome'}
${pill(r.st)}
`).join('')}
`; } else if (sp.why_not_wild.length) { h += `

Why it is not wild here

`; } else if (!sp.spawns.length) { h += `

Wild spawns

Cobblemon has no wild spawn for it.

`; } if (sp.sources.length) h += `

Other ways to get it

${sp.sources.map(sourceHtml).join('')}
`; if (sp.evolutions.length) h += `

Evolves into

${sp.evolutions.map(e => `
evolution
${monLink(e.to)}${e.form ? ` (${esc(e.form)} form)` : ''}: ${esc(e.how.join(', ') || 'level up')}
`).join('')}
`; if (sp.drops.length) h += `

Drops (when defeated in the wild)

${sp.drops.map(d => `${icon(d.item)}${esc(itemInfo(d.item).name)}${esc(d.chance)}`).join('')}
`; 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 = `

Not found in the wild on the island

${list.length} species. Green: obtainable another way (evolution, fossil, starter, trade). Red: no way on this island.

`; for (const [id, sp] of list) { const how = sp.sources.filter(s => s.status !== 'unavailable').slice(0, 3).map(s => esc(s.text)).join('
') || (sp.implemented ? '' : 'not implemented in Cobblemon'); h += ``; } return h + '
PokémonHow to get it hereWild spawns need
${monDot(id)}${monLink(id)}
${how} ${pill(sp.status, '')}${esc(sp.why_not_wild.slice(0, 3).join('; ')) || (sp.spawns.length ? '' : 'no wild spawn in Cobblemon')}
'; } // ---------- 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 = `

Items

Every item the pack's mods add, with how to get it on the island. Red items have no source here.

`; 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]) => `${icon(id)}${esc(it.name)}`).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 = `

← All items

${icon(id, 'ico big')}

${esc(it.name)}

${esc(id)} ${it.category ? `${esc(it.category)}` : ''}
`; if (it.tooltip && it.tooltip.length) h += `

${it.tooltip.map(esc).join('
')}

`; 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 += `
`; const srcs = it.sources || []; const recs = srcs.filter(s => s.recipe), other = srcs.filter(s => !s.recipe); h += `

How to get it

`; if (!srcs.length) h += `

No recipe, loot table, drop or worldgen feature produces it.

`; if (recs.length) h += `
${recs.map(s => recipeHtml(s.recipe)).join('')}
`; if (other.length) h += `
${other.map(sourceHtml).join('')}
`; h += `
`; 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 += `

What it is used for

`; if (!uses.length) h += `

No recipe or Cobblemon mechanic uses it.

`; if (uo.length) h += uo.map(u => `
${esc(u.kind)}
${u.species ? esc(u.text).replace(esc(FG.species[u.species]?.name || '\u0000'), monLink(u.species)) : esc(u.text)}
`).join(''); if (ur.length) h += `

Recipes that use it (${ur.length})

${ur.slice(0, 60).map(u => recipeHtml(u.recipe)).join('')}
${ur.length > 60 ? `

… and ${ur.length - 60} more.

` : ''}`; h += `
`; } if (it.tags && it.tags.length) h += `

Tags

${chips(it.tags)}
`; 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'{label}' for href, label in (("index.html", "Overview"), ("pokemon.html", "Pokémon"), ("items.html", "Items"))) return f""" {html.escape(title)}
Cobblemon Survival · Field Guide
{body} """ 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'{e(loc["name"])}
{kind} · {n} Pokémon' f'{" · conditional" if loc["status"] != "available" else ""}
') def item_row(i): it = items[i] img = f'' if it.get("icon") else '?' why = it.get("reason") or "no source" return (f'{img}{e(it["name"])}' f'
{e(why)}
') problems = "".join(f"
{e(k)} ({len(v)})

{e(', '.join(map(str, v[:400])))}

" for k, v in data["problems"].items() if v) struct = "".join(f"{e(s['name'])}{s['status']}{e(s['reason'])}" for s in data["island"]["structures"] if s["status"] != "unavailable") return f"""

Field guide

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 datapacks/core on top). Generated {e(data['meta']['generated'])}.

{wild}Pokémon in the wild here
{other}more by evolution, fossils, starters
{cond}only in the End
{none}Pokémon with no way to get them here
{len(items)}mod items
{len(nosrc)}items with no source here

Statuses: on the island conditional needs something that may not exist (the End, a stronghold inside the border) no source here ore, Nether or missing biome only.

Locations

{''.join(cards)}

Pokémon not found in the wild on the island →

Items with no source on the island ({len(nosrc)})

{len(used_nosrc)} of them are used by a recipe or a Cobblemon mechanic. Creative/admin items are left out.

{''.join(item_row(i) for i in nosrc)}

Structures that can generate

{struct}

What the generator could not resolve

{problems}

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.

""" 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", '
', "pokemon")) (out / "items.html").write_text(page("Items · Field Guide", "items.html", '
', "items"))