Files
deepgrain-foundry/src/ui/claims.js
T

147 lines
10 KiB
JavaScript
Raw 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.
// Shared player state; the world save remains a complete standalone fallback.
let career = null, claimBusy = false;
function careerState() {
const carriedBag = Array.from(bag);
for (const f of flyers) if (f.bag) carriedBag[f.m]++;
return { money, earned, bag: carriedBag, player: { ...player }, seen: Array.from(seen), ledger: { ...ledger }, stats: { ...stats } };
}
function validCareer(c, id, worldId) {
const obj = v => v && typeof v === 'object' && !Array.isArray(v);
const count = v => Number.isFinite(v) && v >= 0;
const counts = v => obj(v) && Object.values(v).every(count);
return obj(c) && c.id === id && Array.isArray(c.claims) && c.claims.length > 0 &&
c.claims.every(v => typeof v === 'string' && v.length > 0) && new Set(c.claims).size === c.claims.length && c.claims.includes(worldId) &&
Number.isSafeInteger(c.claimNo) && c.claimNo >= c.claims.length && count(c.money) && count(c.earned) &&
Array.isArray(c.bag) && c.bag.length > 0 && c.bag.every(v => Number.isInteger(v) && v >= 0 && v <= 2147483647) &&
Array.isArray(c.seen) && c.seen.length > 0 && c.seen.every(v => v === 0 || v === 1) &&
obj(c.player) && Number.isInteger(c.player.bitTier) && !!BIT_TIERS[c.player.bitTier] &&
Number.isInteger(c.player.tankTier) && !!TANK_TIERS[c.player.tankTier] &&
typeof c.player.lens === 'boolean' && typeof c.player.flask === 'boolean' &&
counts(c.ledger) && counts(c.stats) && ['dug', 'moved', 'placed', 'ignitions', 'cast', 'bars'].every(k => count(c.stats[k]));
}
function applyCareerState(c) {
money = c.money; earned = c.earned;
bag = new Int32Array(NMAT); bag.set(c.bag.slice(0, NMAT));
bagTotal = bag.reduce((n, v) => n + v, 0);
// These grains are already included in the shared bag, even on an older claim.
flyers = flyers.filter(f => !f.bag);
for (const k of Object.keys(player)) player[k] = c.player[k];
seen = new Uint8Array(NMAT); seen.set(c.seen.slice(0, NMAT));
ledger = { ...c.ledger };
for (const k of Object.keys(stats)) delete stats[k];
Object.assign(stats, c.stats);
}
async function loadCareer() {
career = null; GAME.careerId = ''; GAME.claimNo = 1;
try {
if (!GAME.worldId) GAME.worldId = newWorldId();
const entry = (await listWorlds()).find(w => w.id === GAME.worldId);
const c = entry && entry.career ? await DB.get('career:' + entry.career) : null;
if (entry && validCareer(c, entry.career, GAME.worldId)) {
career = c; applyCareerState(c); GAME.claimNo = Number.isSafeInteger(entry.claimNo) && entry.claimNo > 0 ? entry.claimNo : 1;
} else {
// Preserve surviving claim membership when repairing a missing or damaged record.
const claims = entry && entry.career ? (await listWorlds()).filter(w => w.career === entry.career) : [];
career = { id: entry && entry.career || newWorldId(), claims: claims.length ? claims.map(w => w.id) : [GAME.worldId],
claimNo: Math.max(1, ...claims.map(w => Number.isSafeInteger(w.claimNo) && w.claimNo > 0 ? w.claimNo : 1)), ...careerState() };
GAME.claimNo = entry && Number.isSafeInteger(entry.claimNo) && entry.claimNo > 0 ? entry.claimNo : 1;
await worldWrite(async () => {
const list = await listWorlds();
let w = list.find(w => w.id === GAME.worldId);
if (!w) { w = { id: GAME.worldId, name: GAME.worldName || 'World', seed: worldSeed, w: W, h: H, savedAt: 0 }; list.push(w); }
Object.assign(w, { career: career.id, claimNo: GAME.claimNo, debt });
await DB.op('readwrite', st => { st.put(career, 'career:' + career.id); st.put(list, 'worlds'); });
});
}
GAME.careerId = career.id;
} catch (e) { career = null; toast('Career could not be loaded. Using this world’s own money and bag.'); }
}
// Seeded camp names stay the same unless an existing world already uses one.
function claimName(seed, taken = []) {
const first = ['Copper', 'Iron', 'Red', 'Black', 'Grey', 'Silver', 'Deep', 'Cinder', 'Ash', 'Granite', 'Basalt', 'Slate', 'Rust', 'Ember', 'Flint', 'Cold', 'Hollow', 'Lone', 'Twin', 'Broken'];
const last = ['Hollow', 'Ridge', 'Flats', 'Gulch', 'Mesa', 'Seam', 'Reach', 'Bluff', 'Basin', 'Crag', 'Shelf', 'Draw', 'Cut', 'Rise', 'Pit', 'Vein', 'Wash', 'Fork', 'Run', 'Bench'];
const count = first.length * last.length, start = Math.floor(mulberry32(seed)() * count);
for (let offset = 0; offset < count; offset++) {
const i = (start + offset) % count, a = first[Math.floor(i / last.length)], b = last[i % last.length];
const name = a + ' ' + b;
if (a !== b && !taken.includes(name)) return name;
}
// every camp name is taken: fall back to numbered names
let n = 1;
while (taken.includes('Mining Camp ' + n)) n++;
return 'Mining Camp ' + n;
}
async function openNewClaim() {
if (claimBusy || !career || debt > 0 || money < 5000) return false;
claimBusy = true; const paused = GAME.paused; GAME.paused = true;
let previous;
try {
if (!await saveWorld('main', true)) throw new Error('Save failed');
previous = { save: serializeWorld(), id: GAME.worldId, name: GAME.worldName, time: GAME.playTime, career, claimNo: GAME.claimNo };
const carried = careerState(), n = career.claimNo + 1, w = W, h = H;
carried.money -= 5000;
let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed);
const name = claimName(seed, (await listWorlds()).map(w => w.name));
await setupNewWorld(seed, w, h, name, carried, 'Opening a new claim');
debt = CLAIM_DEBT * 2 ** (n - 1);
career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n };
GAME.claimNo = n;
showLoading(true, 0.9, 'Saving');
if (!await saveWorld('auto', true)) throw new Error('Save failed');
showLoading(true, 1, 'Saving');
await gpuWarmWorld();
showLoading(false); closeAllModals(); enterGame(true);
showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
return true;
} catch (e) {
if (previous) {
deserializeWorld(previous.save.meta, previous.save.buffers);
GAME.worldId = previous.id; GAME.worldName = previous.name; GAME.playTime = previous.time;
career = previous.career; GAME.claimNo = previous.claimNo;
}
showLoading(false); GAME.paused = paused; toast('Could not open a claim. Your old claim has been kept.'); return false;
} finally { claimBusy = false; }
}
async function travel(worldId) {
if (claimBusy || !career || worldId === GAME.worldId || !career.claims.includes(worldId)) return false;
claimBusy = true; const paused = GAME.paused; let arrived = false; GAME.paused = true;
try {
const entry = (await listWorlds()).find(w => w.id === worldId);
if (!entry || entry.career !== GAME.careerId) return false;
const progress = (f, s) => showLoading(true, f, s, 'Travelling to ' + entry.name);
progress(0, 'Saving');
if (!await saveWorld('main', true)) throw new Error('Save failed');
const sv = await worldNewest(worldId, progress);
if (!sv || !await startFromSave(sv, progress)) return false;
closeAllModals(); arrived = true; return true;
} catch (e) { toast('Could not travel. Check browser storage and try again.'); return false; }
finally { showLoading(false); claimBusy = false; if (!arrived) GAME.paused = paused; }
}
async function renderClaimsTab(body) {
body.innerHTML = '<p class=note>Loading claims…</p>';
try {
const list = (await listWorlds()).filter(w => career && w.career === GAME.careerId && career.claims.includes(w.id));
if (nbTab !== 'Claims') return;
body.innerHTML = list.map(w => `<div class="world-row"><div class="world-info"><strong>${esc(w.name)}${w.id === GAME.worldId ? ' · here' : ''}</strong> <small class="note">Claim ${esc(w.claimNo)}</small><div class="note">${esc(w.w)}×${esc(w.h)} grains · ${(w.id === GAME.worldId ? debt : w.debt) <= 0 ? 'paid off ✓' : fmtMoney(w.id === GAME.worldId ? debt : w.debt || 0) + ' owed'} · Last played: ${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Just opened'}</div></div><button class="btn" data-rename="${esc(w.id)}">Rename</button>${w.id === GAME.worldId ? '' : `<button class="btn" data-travel="${esc(w.id)}">Travel</button>`}</div>`).join('') +
(debt <= 0 ? `<button class="btn primary" id="claim-open" ${money < 5000 || !career ? 'disabled' : ''}>Open a new claim: $5,000 down, then ${fmtMoney(CLAIM_DEBT * 2 ** (career ? career.claimNo : 1))} owed</button>${money < 5000 ? '<p class=note>You need $5,000 for the down payment.</p>' : ''}` : '<p class=note>A new claim opens once this one is paid off.</p>') +
'<p class=note>Machines on other claims wait until you come back.</p>';
body.querySelectorAll('[data-rename]').forEach(b => b.onclick = () => {
const w = list.find(w => w.id === b.dataset.rename);
dialog('Rename claim', `<input class="text" type="text" id="claim-name" aria-label="Claim name" maxlength="40" value="${esc(w.name)}">`, [
{ label: 'Cancel', cls: 'ghost' }, { label: 'Save', cls: 'primary', fn: async () => {
const name = cleanWorldName($('claim-name').value, w.name);
try { await renameWorld(w.id, name); closeModal('m-dialog'); renderClaimsTab(body); }
catch (e) { toast('Could not rename this claim. Try again.'); }
} },
]);
});
const button = $('claim-open');
if (button) button.onclick = () => dialog('Open a new claim?', `<p class="note">Pay $5,000 down and owe ${fmtMoney(CLAIM_DEBT * 2 ** career.claimNo)} on fresh land. Your buildings stay here.</p>`, [
{ label: 'Cancel', cls: 'ghost' }, { label: 'Open claim', cls: 'primary', fn: () => { closeModal('m-dialog'); openNewClaim(); } },
]);
body.querySelectorAll('[data-travel]').forEach(b => b.onclick = () => dialog('Travel to this claim?', '<p class=note>Your current claim will be saved. Money, bag, upgrades and knowledge come with you.</p>', [
{ label: 'Cancel', cls: 'ghost' }, { label: 'Travel', cls: 'primary', fn: () => { closeModal('m-dialog'); travel(b.dataset.travel); } },
]));
} catch (e) { if (nbTab === 'Claims') body.innerHTML = '<p class=note>Could not load claims from browser storage.</p>'; }
}