'use strict'; /* ============================================================ GAME โ€” actions, progression, quests, economy, romance, endings ============================================================ */ const Game = { /* ---------------- new game ---------------- */ newGame(opts) { G.player = makePlayer(opts); G.loc = 'l_village'; G.discovered = { l_village: true }; G.flags = { mainChapter: 1, ach: {}, seen: {}, fishTotal: 0 }; if (opts.hard) G.flags.hardMode = true; G.aff = {}; G.rep = { fame: 0, infamy: 0, azure: 0, temple: 0, serpent: 0, fist: 0, garrison: 0, underground: 0 }; G.quests = {}; G.party = []; G.allies = []; G.time = { day: 1, hour: 8 }; G.log = []; G.daily = defaultDaily(); Game.rollBounties(); Log.clearDom(); Log.add(`Day 1 โ€” ${G.player.name} steps out of Willow Creek Village. The jianghu awaits.`, 'sys'); SaveSys.autosave(); UI.showScreen('world'); UI.setTab('scene'); UI.refresh(); UI.modal('Prologue', `

Willow Creek never had use for heroes \u2014 until the wolves came down off Cloudmist Ridge,\nbold as tax collectors.\n\nYou are ${Util.esc(G.player.name)}, ${G.player.gender === 'f' ? 'daughter' : 'son'} of this village, and this morning\nyou wrapped your hands, checked your blade, and decided the wide road was calling.\n\nSpeak with Elder Chen to begin.

`, [{ label: 'Begin the journey', fn: () => {} }]); }, /* ---------------- inventory / money ---------------- */ addItem(id, q) { q = q || 1; const it = DATA.ITEMS[id]; if (!it) return; const stackable = !['weapon', 'body', 'head', 'feet'].includes(it.type); const ex = stackable && G.player.inv.find(s => s.id === id); if (ex) ex.q += q; else G.player.inv.push({ id, q }); Log.add(`Obtained ${Util.esc(it.name)} ร—${q}.`); }, removeItem(id, q) { q = q || 1; const slot = G.player.inv.find(s => s.id === id); if (!slot) return false; slot.q -= q; if (slot.q <= 0) G.player.inv = G.player.inv.filter(s => s !== slot); return true; }, hasItem(id, q) { q = q || 1; const s = G.player.inv.find(x => x.id === id); return !!s && s.q >= q; }, countItem(id) { const s = G.player.inv.find(x => x.id === id); return s ? s.q : 0; }, addSilver(n) { G.player.silver += n; Sfx.play('coin'); Log.add(`${n >= 0 ? 'Gained' : 'Lost'} ${Math.abs(n)} silver.`, n >= 0 ? 'good' : 'bad'); if (G.player.silver >= 2000) Game.unlockAch('fat_purse'); }, spendSilver(n) { if (G.player.silver < n) { Toast.show('Not enough silver.'); return false; } G.player.silver -= n; Sfx.play('coin'); return true; }, equip(slot, invIndex) { const entry = G.player.inv[invIndex]; if (!entry) return; const it = DATA.ITEMS[entry.id]; const target = it.slot; if (!target) return; // weapons/armor are unique items; swap const prev = G.player.equip[target]; G.player.equip[target] = entry.id; Game.removeItem(entry.id, 1); if (prev) Game.addItem(prev, 1); Stats.recalc(G.player); G.player.hp = Math.min(G.player.hp, G.player.d.maxHp); G.player.mp = Math.min(G.player.mp, G.player.d.maxMp); Sfx.play('click'); UI.refresh(); }, unequip(slot) { const cur = G.player.equip[slot]; if (!cur) return; G.player.equip[slot] = null; Game.addItem(cur, 1); Stats.recalc(G.player); UI.refresh(); }, useItem(invIndex) { const entry = G.player.inv[invIndex]; if (!entry) return; const it = DATA.ITEMS[entry.id]; if (it.type === 'manual') { Game.learnManual(entry.id); return; } if (it.type !== 'use' || !it.use) { Toast.show('Cannot use that here.'); return; } const u = it.use; const p = G.player; if (u.hp) { p.hp = Math.min(p.d.maxHp, p.hp + u.hp); Log.add(`Restored ${u.hp} health.`, 'good'); } if (u.mp) { p.mp = Math.min(p.d.maxMp, p.mp + u.mp); Log.add(`Restored ${u.mp} inner energy.`, 'good'); } if (u.cure) { Toast.show('Use antidotes during battle.'); return; } if (u.permHp) { if ((G.flags.tonicsUsed || 0) >= 10) { Toast.show('Your body can absorb no more tonic.'); return; } G.flags.tonicsUsed = (G.flags.tonicsUsed || 0) + 1; p.permBonuses.hp += u.permHp; Stats.recalc(p); Log.add(`Maximum health permanently +${u.permHp}!`, 'sys'); } if (u.permWit) { if ((G.flags.fragsUsed || 0) >= 3) { Toast.show('The fragments blur together now.'); return; } G.flags.fragsUsed = (G.flags.fragsUsed || 0) + 1; p.attrs.wit += 1; Stats.recalc(p); Log.add('Insight deepens. Wit permanently +1!', 'sys'); } if (u.buff || u.coatPoison) { Toast.show('Save it for battle.'); return; } Game.removeItem(entry.id, 1); Sfx.play('heal'); UI.refresh(); }, learnManual(id) { const it = DATA.ITEMS[id]; const t = it.teach; if (TECHNIQUES[t]) { if (G.player.techs[t]) { Toast.show('You already know this art.'); return; } Game.learnTechnique(t); Game.removeItem(id, 1); } else if (INTERNALS[t]) { if (G.player.knownInternals.includes(t)) { Toast.show('Already cultivated.'); return; } G.player.knownInternals.push(t); G.player.internal = t; G.player.internalLv = Math.max(1, G.player.internalLv); Stats.recalc(G.player); Log.add(`You internalize the ${INTERNALS[t].name}.`, 'sys'); if (t === 'i_sun') Game.unlockAch('sun_dawn'); Game.removeItem(id, 1); UI.refresh(); } else if (LIGHTNESS[t]) { if (G.player.knownLights.includes(t)) { Toast.show('Already mastered.'); return; } G.player.knownLights.push(t); G.player.light = t; Stats.recalc(G.player); Log.add(`Footwork transforms: ${LIGHTNESS[t].name}.`, 'sys'); Game.removeItem(id, 1); UI.refresh(); } }, learnTechnique(tid, free) { if (G.player.techs[tid]) return; G.player.techs[tid] = 10; Log.add(`Learned ${TECHNIQUES[tid].name}!`, 'sys'); Sfx.play('levelup'); UI.refresh(); }, /* ---------------- learning from masters ---------------- */ teachListHas(npc, kind, id) { return (npc.teach || []).some(t => t[kind] === id); }, alreadyKnows(t) { if (t.tech) return !!G.player.techs[t.tech]; if (t.internal) return G.player.knownInternals.includes(t.internal); return false; }, canLearnFrom(npcId, t) { const n = NPCS[npcId]; const aff = G.aff[npcId] || 0; if (aff < (t.aff || 0)) return { ok: false, why: `needs affection ${t.aff}` }; if (t.reqRep && (G.rep[t.reqRep[0]] || 0) < t.reqRep[1]) return { ok: false, why: `needs ${t.reqRep[1]} ${t.reqRep[0]} rep` }; if (t.reqFlag && !G.flags[t.reqFlag]) return { ok: false, why: 'a secret must surface first' }; if (G.player.silver < (t.cost || 0)) return { ok: false, why: `${t.cost} silver` }; return { ok: true, why: '' }; }, learnFrom(npcId, t) { const ok = Game.canLearnFrom(npcId, t); if (!ok.ok) { Toast.show(ok.why); return; } if (!Game.spendSilver(t.cost || 0)) return; if (t.tech) { G.player.techs[t.tech] = 10; Log.add(`${Util.esc(NPCS[npcId].name)} teaches you ${TECHNIQUES[t.tech].name}.`, 'sys'); } if (t.internal) { G.player.knownInternals.push(t.internal); G.player.internal = t.internal; Log.add(`${Util.esc(NPCS[npcId].name)} transmits the ${INTERNALS[t.internal].name}.`, 'sys'); if (t.internal === 'i_sun') Game.unlockAch('sun_dawn'); } Game.addAffinity(npcId, 3); Sfx.play('levelup'); SaveSys.autosave(); UI.refresh(); }, learnHongArt() { G.player.techs['t_fist_4'] = 25; G.flags.learnedHong = true; Game.unlockAch('hong_heir'); Log.add('Hong presses his palms together \u2014 ten thousand rivers pour into your bones. Learned Ten Thousand Rivers Palms!', 'sys'); Sfx.play('levelup'); UI.refresh(); }, /* ---------------- affinity & gifts ---------------- */ addAffinity(npcId, n) { G.aff[npcId] = Util.clamp((G.aff[npcId] || 0) + n, 0, 100); UI.refreshTop && UI.refreshTop(); }, giveGift(npcId, itemId) { const it = DATA.ITEMS[itemId]; if (!Game.hasItem(itemId)) return; let val = (it.val || 2) + Math.floor(G.player.attrs.cha / 8); if (NPCS[npcId].giftTags && it.giftTags && it.giftTags.some(tg => NPCS[npcId].giftTags.includes(tg))) val += (it.likesVal || 3); const givenToday = (G.daily.giftsGiven[npcId] || 0); if (givenToday > 0) val = Math.ceil(val / (givenToday + 1)); G.daily.giftsGiven[npcId] = givenToday + 1; Game.removeItem(itemId, 1); Game.addAffinity(npcId, val); const reactions = [ [50, 'is visibly moved by the gift.'], [20, 'accepts with a warm smile.'], [0, 'thanks you politely.'] ]; const aff = G.aff[npcId]; const line = aff >= 80 ? 'treasures it like something irreplaceable.' : aff >= 50 ? reactions[0][1] : aff >= 20 ? reactions[1][1] : reactions[2][1]; Log.add(`${Util.esc(NPCS[npcId].name)} ${line} (${NPCS[npcId].name.split(' ')[0]} +${val} affection)`, 'good'); Sfx.play('coin'); UI.refresh(); }, useGiftOn(npcId, itemId) { Game.giveGift(npcId, itemId); }, addRep(fac, n) { G.rep[fac] = Util.clamp((G.rep[fac] || 0) + n, -100, 100); if (fac !== 'underground') G.rep.fame += Math.max(0, Math.round(n / 2)); else G.rep.infamy += Math.max(0, n); UI.refreshTop && UI.refreshTop(); }, addFame(n) { G.rep.fame += n; UI.refreshTop && UI.refreshTop(); }, addInfamy(n) { G.rep.infamy += n; UI.refreshTop && UI.refreshTop(); }, /* ---------------- achievements ---------------- */ unlockAch(id) { G.flags.ach = G.flags.ach || {}; if (!ACHIEVEMENTS[id] || G.flags.ach[id]) return; G.flags.ach[id] = true; Toast.show(`๐Ÿ† Achievement: ${ACHIEVEMENTS[id].name}`); Log.add(`๐Ÿ† Achievement unlocked: ${ACHIEVEMENTS[id].name}`, 'sys'); Sfx.play('levelup'); }, /* ---------------- EXP & levels ---------------- */ addExp(n) { const p = G.player; p.exp += n; Log.add(`Gained ${n} experience.`); let leveled = false; while (p.exp >= Stats.expNext(p.lvl) && p.lvl < 30) { p.exp -= Stats.expNext(p.lvl); p.lvl++; leveled = true; G.flags.pendingAttr = (G.flags.pendingAttr || 0) + 2; } if (leveled) { Stats.recalc(p); p.hp = p.d.maxHp; p.mp = p.d.maxMp; Log.add(`LEVEL UP! You are now level ${p.lvl}. (+2 attribute points)`, 'sys'); Sfx.play('levelup'); if (p.lvl >= 10) Game.unlockAch('journeyman'); if (p.lvl >= 20) Game.unlockAch('master_qi'); UI.openLevelUp && UI.openLevelUp(); } UI.refreshTop && UI.refreshTop(); }, spendAttrPoint(attr) { if ((G.flags.pendingAttr || 0) <= 0) return; G.player.attrs[attr]++; G.flags.pendingAttr--; Stats.recalc(G.player); UI.refresh(); if (!G.flags.pendingAttr) UI.closeModal && UI.closeModal(); }, /* ---------------- travel & encounters ---------------- */ travelTo(locId) { if (G.loc === locId) return; const edge = TRAVEL.find(e => (e[0] === G.loc && e[1] === locId) || (e[1] === G.loc && e[0] === locId)); if (!edge) { Toast.show('No road leads there directly.'); return; } const target = LOCS[locId]; if (target.nightOnly && !TimeSys.isNight()) { Toast.show('That place only opens after dusk.'); return; } if (target.hidden && !G.flags[target.hidden]) { Toast.show('You don\u2019t know how to get there yet.'); return; } const hours = edge[2]; TimeSys.advance(hours); G.loc = locId; G.discovered[locId] = true; Log.add(`Traveled to ${Util.esc(target.name)}. (${TimeSys.str()})`, 'sys'); SaveSys.autosave(); UI.setTab('scene'); Game.arriveEvents(locId); UI.refresh(); Game.rollEncounter(locId); }, arriveEvents(locId) { if (locId === 'l_market' && QSsafe('q_main_3') === 0) { Game.setStage('q_main_3', 1); } if (locId === 'l_market') Game.unlockAch('after_dark'); if (locId === 'l_city' && !G.flags.hongSeen && G.time.day >= 4 && Util.chance(0.5)) { G.flags.hongSeen = true; Toast.show('A drunk beggar sings badly by the wine shops\u2026'); } if (locId === 'l_mountain' && FVsafe('mainChapter') >= 4 && !G.flags.tombroute) { G.flags.tombroute = true; Toast.show('Behind the old shrine, buried stairs descend into darkness\u2026 (Ancient Tomb discovered)'); G.discovered['l_tomb'] = true; } if (locId === 'l_tomb' && QSsafe('q_main_5') === 0) Game.setStage('q_main_5', 1); }, rollEncounter(locId) { const L = LOCS[locId]; if (!L.danger) return; if (Util.chance(0.10 + L.danger * 0.08)) Game.wildEncounter(locId); }, wildEncounter(locId) { const L = LOCS[locId]; const plvl = G.player.lvl; const pool = plvl <= 4 ? (L.pools.low || L.pools.mid) : plvl <= 9 ? (L.pools.mid || L.pools.high) : (L.pools.high || L.pools.mid); if (!pool) return; const count = Util.chance(0.65) ? 2 : 1; const scale = 0.85 + Math.min(0.5, plvl * 0.045); const enemies = []; for (let i = 0; i < count; i++) enemies.push(Util.pick(pool)); Game.startBattle({ ids: enemies, scale, meta: { kind: 'wild', locName: L.name } }); }, /* ---------------- battles ---------------- */ startBattle(spec) { if (G.inBattle) return; const plvl = G.player.lvl; let scale = spec.scale || 1; if (G.flags.hardMode) scale *= 1.28; let enemies; if (spec.customEnemies && spec.customEnemies.length) { enemies = spec.customEnemies.map(u => { u.side = 'enemy'; return u; }); enemies.forEach(e => { e.expVal = e.expVal || 0; e.silver = null; }); } else { const ids = spec.ids || []; enemies = ids.map(id => makeEnemyUnit(id, scale)).filter(Boolean); enemies.forEach((e, i) => { e.enemyId = ids[i]; }); } if (!enemies.length) return; // bestiary tracking G.flags.seen = G.flags.seen || {}; enemies.forEach(e => { G.flags.seen[e.enemyId || e.name] = true; }); const allies = [makePlayerUnit(G.player)]; for (const cid of G.party.slice(0, 2)) { const cu = makeCompanionUnit(cid, plvl); if (cu) allies.push(cu); } const meta = Object.assign({ kind: 'wild' }, spec.meta || {}); if (meta.spar) { // remember pre-battle vitals to restore afterwards meta.preHp = G.player.hp; meta.preMp = G.player.mp; } G.battleMeta = meta; G.inBattle = true; const battle = new Battle({ allies, enemies, instant: !!spec.instant, spar: !!meta.spar, noDeath: !!meta.spar }); battle.on('log', (m) => Log.add(m, 'combat')); battle.on('end', (res) => Game.afterBattle(res, battle)); G.battle = battle; if (typeof document !== 'undefined' && UI.openBattle) UI.openBattle(battle); else battle.startRound(); return battle; }, registerKill(unit) { G.flags.totalKills = (G.flags.totalKills || 0) + 1; G.flags.kills = G.flags.kills || {}; const kid = unit.enemyId || unit.name; G.flags.kills[kid] = (G.flags.kills[kid] || 0) + 1; Game.unlockAch('first_blood'); const meta = G.battleMeta || {}; if (meta.kind === 'wolfhunt' && unit.enemyId === 'e_wolf') G.flags.wolfKills = (G.flags.wolfKills || 0) + 1; if (meta.kind === 'tomb' && unit.enemyId === 'e_sentinel') G.flags.sentinelKills = (G.flags.sentinelKills || 0) + 1; }, afterBattle(res, battle) { G._lastRes = res; const meta = G.battleMeta || {}; G.inBattle = false; const p = G.player; if (meta.spar) { // friendly: restore vitals, grant proficiency & affection p.hp = Math.min(meta.preHp, p.d.maxHp); p.mp = Math.min(meta.preMp, p.d.maxMp); if (res.win) { for (const tid of Object.keys(p.techs)) p.techs[tid] = Math.min(100, p.techs[tid] + 3); if (meta.npcId) { Game.addAffinity(meta.npcId, res.win ? 4 : 2); G.daily.sparred[meta.npcId] = true; } Log.add('A clean exchange! Techniques sharpen. (+3 proficiency, +4 affection)', 'good'); if (meta.npcId === 'n_beggar_hong') G.flags.hongBeats = (G.flags.hongBeats || 0) + 1; if (meta.npcId === 'n_monk_jing') { const qf = G.quests['q_trial_fl']; if (qf && !qf.done && qf.stage === 0 && !G.flags.trialFlSpar) { G.flags.trialFlSpar = true; Log.add('Monk Jing bows. \u201cYour staff speaks politely. Good.\u201d', 'sys'); Game.checkTrialFl(); } } if (meta.onWin) meta.onWin(); } else { if (meta.npcId) Game.addAffinity(meta.npcId, 2); Log.add('Defeated \u2014 but every bruise is a lesson.', 'bad'); } G.battleMeta = null; G.battle = null; UI.closeBattle && UI.closeBattle(); UI.refresh(); SaveSys.autosave(); return; } if (res.win) { let exp = 0, silver = 0; const loots = []; for (const e of battle.enemies) { exp += e.expVal || 20; if (e.silver) silver += Util.ri(e.silver[0], e.silver[1]); for (const l of (e.loot || [])) if (Util.chance(l.ch)) loots.push([l.id, l.q || 1]); } // luck bonus if (Util.chance(G.player.attrs.luk * 0.01)) { loots.push(['c_pill_hp', 1]); Log.add('Fortune smiles \u2014 extra loot!', 'good'); } Game.addSilver(silver); Game.addExp(exp); for (const [id, q] of loots) Game.addItem(id, q); // proficiency from techniques used for (const [tid, uses] of Object.entries(res.techUses || {})) { p.techs[tid] = Math.min(100, (p.techs[tid] || 0) + Math.min(6, uses * 2)); } if (meta.kind === 'wild') Game.addFame(1); } else { Game.defeat(meta); } // tear down battle state BEFORE onWin so chained battles open cleanly G.battleMeta = null; G.battle = null; G.inBattle = false; UI.closeBattle && UI.closeBattle(); if (res.win && meta.onWin) meta.onWin(); UI.refresh(); if (p.hp > 0) SaveSys.autosave(); }, defeat(meta) { const p = G.player; const loss = Math.round(p.silver * 0.1); p.silver -= loss; Log.add(`Darkness takes you\u2026 You awaken at an inn, lighter by ${loss} silver.`, 'bad'); Sfx.play('defeat'); p.hp = Math.max(1, Math.round(p.d.maxHp * 0.5)); p.mp = Math.max(0, Math.round(p.d.maxMp * 0.5)); // respawn at nearest safe town G.loc = ['l_village', 'l_city', 'l_town'].includes(G.loc) ? G.loc : 'l_city'; G.time.hour = 8; G.time.day += 1; Game.dailyReset(); }, /* ---------------- rest & cultivation ---------------- */ restInn(cost) { if (!Game.spendSilver(cost)) return; const p = G.player; p.hp = p.d.maxHp; p.mp = p.d.maxMp; const hoursToEight = ((24 - G.time.hour) + 8) % 24 || 24; TimeSys.advance(hoursToEight); Log.add('You sleep deeply and wake restored.', 'good'); SaveSys.autosave(); UI.closeModal && UI.closeModal(); UI.refresh(); }, campOut() { const L = LOCS[G.loc]; if (!L.danger) { Toast.show('Camp near town? Just sleep at the inn.'); return; } TimeSys.advance(6); const p = G.player; p.hp = Math.min(p.d.maxHp, p.hp + Math.round(p.d.maxHp * 0.55)); p.mp = Math.min(p.d.maxMp, p.mp + Math.round(p.d.maxMp * 0.55)); Log.add('You camp beneath cold stars.', 'sys'); if (Util.chance(0.18)) { Log.add('Something stalks the campfire light\u2026', 'bad'); Game.wildEncounter(G.loc); return; } UI.refresh(); }, meditate() { TimeSys.advance(2); const p = G.player; const gain = 8 + p.attrs.wit * 2 + (LOCS[G.loc].actions || []).some(a => a.id === 'meditate' && G.loc === 'l_temple' ? 4 : 0); p.internalExp += gain; Log.add(`You circulate qi quietly. (+${gain} internal insight)`); // temple trial progress const qf = G.quests['q_trial_fl']; if (qf && !qf.done && qf.stage === 0 && !G.flags.trialFlMed) { G.flags.trialFlMed = true; Log.add('The incense clock shows one full sitting. Patience, practiced.', 'sys'); Game.checkTrialFl(); } const need = p.internalLv * 55; if (p.internalExp >= need && p.internalLv < 9) { p.internalExp -= need; p.internalLv++; Stats.recalc(p); Log.add(`${UTIL_INT_NAME(p.internal)} reaches level ${p.internalLv}!`, 'sys'); Sfx.play('levelup'); } UI.refresh(); }, trainDummy() { if ((G.daily.dummyPts || 0) >= 15) { Toast.show('The posts have taught you all they can today.'); return; } UI.openTrainPick(); }, doTrainDummy(tid) { TimeSys.advance(1); const amt = Math.min(3, 15 - (G.daily.dummyPts || 0)); G.daily.dummyPts = (G.daily.dummyPts || 0) + amt; G.player.techs[tid] = Math.min(100, (G.player.techs[tid] || 0) + amt); Log.add(`Drilled ${TECHNIQUES[tid].name} against the posts. (+${amt} proficiency)`); UI.closeModal && UI.closeModal(); UI.refresh(); }, /* ---------------- location facilities ---------------- */ gatherHerbs() { const key = 'herb_' + G.loc; if (G.daily.gathered[key]) { Toast.show('Picked clean for today.'); return; } G.daily.gathered[key] = true; TimeSys.advance(1); const loc = G.loc; if (loc === 'l_valley') { Game.addItem('c_lotus', Util.ri(1, 2)); if (Util.chance(0.35)) Game.addItem('mat_gall', 1); if (Util.chance(0.2)) Game.addItem('c_herb_red', 1); } else if (loc === 'l_bamboo') { Game.addItem('c_herb_green', Util.ri(1, 2)); if (Util.chance(0.3)) Game.addItem('c_lotus', 1); } else { Game.addItem('c_herb_green', Util.ri(1, 2)); if (Util.chance(0.3)) Game.addItem('c_herb_red', 1); } UI.refresh(); }, prospectMine() { if (G.daily.mined) { Toast.show('The seam is picked over for today.'); return; } G.daily.mined = true; TimeSys.advance(2); G.flags.oreRuns = (G.flags.oreRuns || 0) + 1; Game.addItem('mat_ore_1', Util.ri(1, 2)); if (Util.chance(0.3 + G.player.attrs.luk * 0.01)) Game.addItem('mat_ore_2', 1); if (Util.chance(0.04 + G.player.attrs.luk * 0.004)) { Game.addItem('mat_ore_3', 1); Log.add('Star-metal glints in the rubble!', 'good'); } UI.refresh(); }, sectTasks() { if (!G.flags.azureMember) { Toast.show('Only Azure Cloud disciples may take missions.'); return; } const opts = [ { label: 'Patrol the cloud stairs (fight)', fn: () => { UI.closeModal(); Game.startBattle({ ids: ['e_bandit_vet'], scale: 1, meta: { kind: 'sect', onWin: () => { Game.addRep('azure', 8); Game.addSilver(40); } } }); } }, { label: 'Courier scrolls to the temple (2h)', fn: () => { TimeSys.advance(2); Game.addRep('azure', 6); Log.add('Scrolls delivered; the brothers nod approvingly. (+6 azure rep)', 'good'); UI.refresh(); } }, { label: 'Demonstrate forms for juniors (1h)', fn: () => { TimeSys.advance(1); Game.addRep('azure', 5); Game.addExp(20); UI.refresh(); } }, { label: 'Not today', fn: () => UI.closeModal() } ]; UI.modal('Mission Hall', '

A bored steward slides three wooden tallies across the desk.

', opts.map(o => ({ label: o.label, fn: o.fn }))); }, /* ---------------- economy facilities ---------------- */ buyItem(npcId, itemId) { const n = NPCS[npcId]; if (!n.shop) return; const it = DATA.ITEMS[itemId]; const price = Game.buyPrice(it); if (!Game.spendSilver(price)) return; Game.addItem(itemId, 1); UI.refreshModalIf && UI.refreshModalIf(); }, sellItem(itemId) { const it = DATA.ITEMS[itemId]; const price = Game.sellPrice(it); if (!Game.hasItem(itemId)) return; Game.removeItem(itemId, 1); Game.addSilver(price); UI.refreshModalIf && UI.refreshModalIf(); }, buyPrice(it) { const disc = Util.clamp(G.player.attrs.cha * 0.012, 0, 0.15) + ((G.rep.underground > 20 && LOCS[G.loc] === LOCS.l_market) ? 0.05 : 0); return Math.max(1, Math.round(it.price * (1 - disc))); }, sellPrice(it) { return Math.max(1, Math.floor(it.price * 0.42 * (1 + G.player.attrs.cha * 0.008))); }, smithUpgrade(fromId) { const up = SMITH_UPGRADES[fromId]; if (!up) return; if (!Game.hasItem(fromId) && !Object.values(G.player.equip).includes(fromId)) { Toast.show('You don\u2019t have that item.'); return; } for (const [m, q] of Object.entries(up.mats)) if (!Game.hasItem(m, q)) { Toast.show('Missing materials.'); return; } if (!Game.spendSilver(up.silver)) return; for (const [m, q] of Object.entries(up.mats)) Game.removeItem(m, q); // consume owned copy (equipped preferred) if (Object.values(G.player.equip).includes(fromId)) { for (const s of Object.keys(G.player.equip)) if (G.player.equip[s] === fromId) { G.player.equip[s] = null; break; } } else Game.removeItem(fromId, 1); Game.addItem(up.to, 1); Log.add(`The forge roars \u2014 crafted ${DATA.ITEMS[up.to].name}!`, 'sys'); Sfx.play('levelup'); Stats.recalc(G.player); UI.openSmith(); // refresh panel }, brew(recipeId) { const r = ALCHEMY_RECIPES.find(x => x.id === recipeId); if (!r) return; if (r.needFlag && !G.flags[r.needFlag]) { Toast.show('You don\u2019t know that recipe.'); return; } for (const [m, q] of Object.entries(r.mats)) if (!Game.hasItem(m, q)) { Toast.show('Missing ingredients.'); return; } if (!Game.spendSilver(r.silver)) return; for (const [m, q] of Object.entries(r.mats)) Game.removeItem(m, q); Game.addItem(r.out, r.qty); Log.add(`Brewed ${DATA.ITEMS[r.out].name} ร—${r.qty}.`, 'good'); UI.openAlchemy(); }, /* ---------------- gambling & crime ---------------- */ gambleBet(kind, amount) { if (!Game.spendSilver(amount)) return null; const dice = [Util.ri(1, 6), Util.ri(1, 6), Util.ri(1, 6)]; const sum = dice[0] + dice[1] + dice[2]; const triple = dice[0] === dice[1] && dice[1] === dice[2]; let win = 0; if (kind === 'triple') { if (triple) win = amount * 28; } else if (kind === 'big') { if (!triple && sum >= 11) win = Math.floor(amount * 1.9); } else if (kind === 'small') { if (!triple && sum <= 10) win = Math.floor(amount * 1.9); } if (win) Game.addSilver(win); return { dice, sum, triple, win }; }, pickpocket() { if (G.daily.pickpocket) { Toast.show('Guards remember faces. Tomorrow, perhaps.'); return; } G.daily.pickpocket = true; TimeSys.advance(1); const p = G.player; const chance = Util.clamp(0.32 + p.attrs.agi * 0.03 + p.attrs.luk * 0.02 - (G.loc === 'l_market' ? 0.06 : 0), 0.15, 0.75); if (Util.chance(chance)) { const take = Util.ri(15, 70); Game.addSilver(take); Log.add('Light fingers, lighter purse. Nobody notices.', 'good'); } else { Game.addInfamy(2); const fine = Math.min(p.silver, 30); p.silver -= fine; Log.add(`Caught! A guard confiscates ${fine} silver and your dignity. (+2 infamy)`, 'bad'); } UI.refresh(); }, /* ---------------- fishing ---------------- */ /* ---------------- side jobs (job board postings) ---------------- */ SIDE_JOBS: { q_herb_bai: { give: () => `Deliver 5 Red Herb to Alchemist Bai\u2019s shelf.`, prog: () => `${Math.min(Game.countItem('c_herb_red'), 5)}/5 Red Herb`, ready: () => Game.hasItem('c_herb_red', 5), take: () => Game.removeItem('c_herb_red', 5), rw: { exp: 120, silver: 140, items: [['c_pill_hp', 2]], aff: [['n_alchemist_bai', 8]] } }, q_ring_reeds: { give: () => `Cull 4 serpents in Serpent Veil Valley; one swallowed a ring.`, prog: () => `${Math.min((G.flags.kills || {}).e_snake || 0, 4)}/4 serpents`, ready: () => ((G.flags.kills || {}).e_snake || 0) >= 4, take: () => {}, rw: { exp: 130, silver: 180, fame: 3, items: [['ac_rope', 1]] } }, q_forge_coal: { give: () => `Prospect ore twice at Cloudmist Ridge for Smith Wang.`, prog: () => `${Math.min(G.flags.oreRuns || 0, 2)}/2 expeditions`, ready: () => (G.flags.oreRuns || 0) >= 2, take: () => {}, rw: { exp: 110, silver: 160, items: [['mat_ore_1', 3]] } }, q_night_watch: { give: () => `Cull 3 bandits on the roads. Wage on delivery.`, prog: () => `${Math.min(((G.flags.kills || {}).e_bandit || 0) + ((G.flags.kills || {}).e_bandit_vet || 0), 3)}/3 bandits`, ready: () => (((G.flags.kills || {}).e_bandit || 0) + ((G.flags.kills || {}).e_bandit_vet || 0)) >= 3, take: () => {}, rw: { exp: 150, silver: 220, rep: [['garrison', 15]] } }, q_alms_lotus: { give: () => `Donate 300 silver to the temple restoration fund.`, prog: () => `300 silver required`, ready: () => G.player.silver >= 300, take: () => { G.player.silver -= 300; Log.add('You seal 300 silver into the temple fund box.', 'bad'); }, rw: { exp: 140, silver: 0, rep: [['temple', 20]] } }, q_lantern_dead: { give: () => `Bring 2 Mulled Wine and 1 Lotus Herb for the river rite.`, prog: () => `${Math.min(Game.countItem('c_wine'), 2)}/2 wine ยท ${Math.min(Game.countItem('c_lotus'), 1)}/1 lotus`, ready: () => Game.hasItem('c_wine', 2) && Game.hasItem('c_lotus', 1), take: () => { Game.removeItem('c_wine', 2); Game.removeItem('c_lotus', 1); }, rw: { exp: 100, silver: 90, fame: 5, items: [['c_antidote', 2]] } } }, sideState(qid) { const q = G.quests[qid]; if (!q) return 'offer'; return q.done ? 'done' : (((this.SIDE_JOBS[qid] || {}).ready && this.SIDE_JOBS[qid].ready()) ? 'ready' : 'active'); }, sideStart(qid) { if (G.quests[qid]) return; Game.startQuest(qid); Toast.show(`Job accepted: ${QUESTS[qid].name}`); }, sideDeliver(qid) { const job = this.SIDE_JOBS[qid]; const q = G.quests[qid]; if (!job || !q || q.done || !job.ready()) return; job.take(); Game.completeQuest(qid, {}); const rw = job.rw || {}; if (rw.exp) Game.addExp(rw.exp); if (rw.silver) Game.addSilver(rw.silver); if (rw.fame) Game.addFame(rw.fame); if (rw.rep) rw.rep.forEach(([f, n]) => Game.addRep(f, n)); if (rw.aff) rw.aff.forEach(([n, v]) => Game.addAffinity(n, v)); if (rw.items) rw.items.forEach(([i, n2]) => Game.addItem(i, n2)); Sfx.play('coin'); UI.refresh(); }, canFish() { return (G.daily.fished || 0) < 10; }, fishResult(hit) { G.daily.fished = (G.daily.fished || 0) + 1; G.flags.fishTotal = (G.flags.fishTotal || 0) + 1; if ((G.flags.fishTotal || 0) >= 25) Game.unlockAch('water_patience'); if (hit) { if (Util.chance(0.15)) { Game.addItem('c_fish', 2); Log.add('A double catch thrashes in the basket!', 'good'); } else Game.addItem('c_fish', 1); Sfx.play('coin'); } else if (Util.chance(0.5)) { Game.addItem('j_boot', 1); Log.add('You reel in\u2026 a boot. The river keeps its secrets.', 'bad'); } else { Log.add('The carp mocks your reflexes and departs with dignity.'); } }, /* ---------------- bounties ---------------- */ rollBounties() { const plvl = G.player.lvl; const tier = plvl <= 4 ? 1 : plvl <= 8 ? 2 : 3; const pool = BOUNTY_POOLS[tier]; const list = []; for (let i = 0; i < 3; i++) { const id = Util.pick(pool); const e = ENEMIES[id]; const cnt = e.tier <= 1 ? Util.ri(2, 3) : 1; const silver = Util.ri(30, 60) * e.tier * cnt; list.push({ id, count: cnt, silver, exp: Math.round((e.exp || 30) * 1.4 * cnt), fame: 4 * e.tier }); } G.bounty.list = list; G.bounty.taken = null; }, takeBounty(i) { if (G.bounty.taken !== null) { Toast.show('Finish your current contract first.'); return; } G.bounty.taken = i; Toast.show('Contract accepted. Hunt them via the board.'); UI.refresh(); }, huntBounty() { const b = G.bounty.list[G.bounty.taken]; if (!b) return; const scale = 0.9 + G.player.lvl * 0.03; const ids = []; for (let i = 0; i < b.count; i++) ids.push(b.id); Game.startBattle({ ids, scale, meta: { kind: 'bounty', onWin: () => { Game.addSilver(b.silver); Game.addExp(b.exp); Game.addFame(b.fame); G.bounty.list.splice(G.bounty.taken, 1); if (!G.bounty.list.length) Game.rollBounties(); G.bounty.taken = null; Log.add('Bounty claimed. Justice, monetized.', 'sys'); } } }); }, /* ---------------- tournament ---------------- */ isTournamentDay() { return G.time.day % 7 === 5; }, enterTournament() { if (!Game.isTournamentDay()) return; G.tourney.lastDay = G.time.day; G.tourney.stage = 0; TimeSys.advance(1); Game.nextTourneyFight(); }, nextTourneyFight() { const st = G.tourney.stage || 0; if (st >= TOURNEY_LADDER.length) { Toast.show('You have already conquered this week\u2019s bracket.'); return; } const rung = TOURNEY_LADDER[st]; const scale = 0.9 + G.player.lvl * 0.05; Game.startBattle({ ids: [rung.base], scale, meta: { kind: 'arena', name: rung.name, onWin: () => Game.tourneyWin() } }); }, tourneyWin() { G.tourney.stage = (G.tourney.stage || 0) + 1; const p = G.player; p.hp = Math.min(p.d.maxHp, p.hp + Math.round(p.d.maxHp * 0.45)); p.mp = Math.min(p.d.maxMp, p.mp + Math.round(p.d.maxMp * 0.45)); const prize = [80, 150, 250, 500][G.tourney.stage - 1] || 100; Game.addSilver(prize); Game.addFame(6); if (G.tourney.stage >= TOURNEY_LADDER.length) { Game.addFame(20); G.flags.champion = true; Game.unlockAch('champion'); Log.add('CHAMPION! The crowd roars your name across Qingyun!', 'sys'); Sfx.play('victory'); if (QSsafe('q_main_3') === 2) { Game.setStage('q_main_3', 3); Toast.show('Host Qian waves you over urgently\u2026'); } } else { Log.add(`Victory in round ${G.tourney.stage}! Prize: ${prize} silver.`, 'good'); UI.modal('Tournament', `

Round ${G.tourney.stage} won! Your next opponent awaits: ${TOURNEY_LADDER[G.tourney.stage].name}.

`, [{ label: 'Fight on', fn: () => { UI.closeModal(); Game.nextTourneyFight(); } }, { label: 'Withdraw (keep winnings)', fn: () => UI.closeModal() }]); } }, dukangAmbush() { UI.modal('The Pavilion', '

Behind the arena, a man waits beneath a silver-paper umbrella. Du Kang smiles without warmth.

\u201cThe celebrated ridge-clearer! The Society offers glory, wealth\u2026 and long life. Refuse, and become an anecdote.\u201d

', [ { label: 'Refuse. Draw your weapon.', fn: () => { UI.closeModal(); Game.fightDukang(false); } }, { label: '"Tell me more\u2026" (feign interest)', fn: () => { UI.closeModal(); UI.modal('Du Kang leans in\u2026', '

\u201cWise. The Ledger in the old tomb \u2014 bring it to us, and names, ranks, silver follow. Fail, and\u2026 well.\u201d

You could play along\u2026

', [ { label: 'Accept the dark mark (villain path)', fn: () => { UI.closeModal(); G.flags.villainPath = true; Game.fightDukang(true); } }, { label: 'PSYCH \u2014 attack!', fn: () => { UI.closeModal(); Game.fightDukang(false); } } ]); } } ]); }, fightDukang(joined) { Game.startBattle({ ids: ['e_boss_dukang'], scale: 1, meta: { kind: 'boss', boss: 'dukang', joined, onWin: () => { Game.completeQuest('q_main_3', { exp: 350, silver: 300, fame: 30 }); G.flags.mainChapter = 3; Game.startQuest('q_main_4'); Log.add(joined ? 'Du Kang bleeds anyway \u2014 \u201cConsider\u2026 the mark earned.\u201d He vanishes.' : 'Du Kang flees, umbrella shattered, secrets spilled.', 'sys'); } } }); }, /* ---------------- companions ---------------- */ recruitCompanion(npcId) { if (!G.allies.includes(npcId)) G.allies.push(npcId); if (G.party.length < 2 && !G.party.includes(npcId)) G.party.push(npcId); Log.add(`${Util.esc(NPCS[npcId].name)} joins you!`, 'sys'); Sfx.play('victory'); if (G.allies.length >= 3) Game.unlockAch('band_three'); Game.checkQuestAuto(); SaveSys.autosave(); UI.refresh(); }, togglePartyMember(npcId) { if (G.party.includes(npcId)) { G.party = G.party.filter(x => x !== npcId); } else if (G.party.length < 2) G.party.push(npcId); else { Toast.show('Only two may walk beside you in battle.'); return; } UI.refresh(); }, confess(npcId) { G.partner = npcId; G.aff[npcId] = 100; Game.recruitCompanion(npcId); Game.unlockAch('two_rivers'); Log.add('\u2764 Your hearts align beneath the clouds. (Romance unlocked)', 'sys'); }, canSpar(npcId) { return !!NPCS[npcId].spar && !G.daily.sparred[npcId]; }, spar(npcId) { if (!Game.canSpar(npcId)) return; const n = NPCS[npcId]; TimeSys.advance(1); // build sparring opponent const plvl = G.player.lvl; let opp; if (n.companion) opp = makeCompanionUnit(npcId, plvl + 1); else { const wtMap = { 'n_abbot_lian': 'staff', 'n_master_yun': 'sword', 'n_lady_miao': 'hidden', 'n_master_guo': 'fist', 'n_monk_jing': 'staff', 'n_hunter_ren': 'blade', 'n_beggar_hong': 'fist' }; const wt = wtMap[npcId] || 'fist'; const hp = 90 + plvl * 16; opp = makeUnit({ name: n.name, side: 'enemy', hp, hpMax: hp, mp: 60, mpMax: 60, atk: 8 + plvl * 2.4, def: 4 + plvl * 0.8, spd: 9 + plvl * 0.5, crit: 8, dodge: 8, mv: 4, rng: wt === 'hidden' ? 4 : 1, wt, moves: [{ n: 'Measured Strike', mp: 0, p: 118, r: 1, sh: 'foe' }], glyph: n.char, color: n.hue, ai: 'duelist' }); } opp.side = 'enemy'; Game.startBattle({ alliesOverride: null, ids: [], customEnemies: [opp], meta: { kind: 'spar', spar: true, npcId } }); }, drinkingContest() { if (!Game.spendSilver(20)) return; TimeSys.advance(1); const roll = Util.ri(10, 90) + G.player.attrs.con * 5 + G.player.attrs.wit * 2; if (roll >= 85) { Game.addAffinity('n_tiehu', 8); Game.addSilver(40); Log.add('Tiehu slams his cup down, swaying. \u201cYou\u2019re\u2026 a monster. Remind me to never fight you sober.\u201d (+8 affection, +40 silver)', 'good'); } else { G.player.hp = Math.max(1, G.player.hp - Math.round(G.player.d.maxHp * 0.08)); Log.add('The room spins like a kicking horse. Tiehu laughs and steals your stake.', 'bad'); } UI.refresh(); }, linDuel() { const plvl = G.player.lvl; const hp = 110 + plvl * 17; const opp = makeUnit({ name: 'Lin Wan\u2019er', side: 'enemy', hp, hpMax: hp, mp: 80, mpMax: 80, atk: 10 + plvl * 2.7, def: 4 + plvl * 0.9, spd: 11 + plvl * 0.6, crit: 12, dodge: 10, mv: 5, rng: 1, wt: 'sword', moves: [{ n: 'Cloud Ripple Thrust', mp: 0, p: 128, r: 1, sh: 'foe' }, { n: 'Moonveil Counter', mp: 10, p: 150, r: 1, sh: 'foe' }], glyph: NPCS.n_lin.char, color: NPCS.n_lin.hue, ai: 'duelist' }); Game.startBattle({ ids: [], customEnemies: [opp], meta: { kind: 'spar', spar: true, npcId: 'n_lin', onWin: () => { Log.add('Wan\u2019er lowers her sword, breathing hard, eyes bright. \u201c\u2026Fine. The roads are boring alone anyway.\u201d', 'sys'); Game.recruitCompanion('n_lin'); Game.addAffinity('n_lin', 10); } } }); }, /* ---------------- story beats ---------------- */ startQuest(qid) { if (G.quests[qid] && !G.quests[qid].done) return; G.quests[qid] = { stage: 0, done: false }; Log.add(`New quest: ${QUESTS[qid].name}`, 'sys'); UI.toastSafe && UI.toastSafe(`Quest accepted: ${QUESTS[qid].name}`); SaveSys.autosave(); }, setStage(qid, st) { const q = G.quests[qid]; if (!q || q.done) return; q.stage = st; Log.add(`Journal updated: ${QUESTS[qid].name}`, 'sys'); UI.toastSafe && UI.toastSafe(`Journal updated: ${QUESTS[qid].name}`); SaveSys.autosave(); }, completeQuest(qid, rw) { const q = G.quests[qid]; if (!q || q.done) return; q.done = true; rw = rw || {}; if (qid === 'q_main_1') Game.unlockAch('ridge_guardian'); if (qid === 'q_main_6') Game.unlockAch('umbrellas_fall'); Log.add(`Quest complete: ${QUESTS[qid].name}`, 'sys'); if (rw.exp) Game.addExp(rw.exp); if (rw.silver) Game.addSilver(rw.silver); if (rw.fame) Game.addFame(rw.fame); for (const [id, n] of (rw.items || [])) Game.addItem(id, n); for (const [fid, n] of (rw.aff || [])) Game.addAffinity(fid, n); for (const [fac, n] of (rw.rep || [])) Game.addRep(fac, n); Sfx.play('victory'); SaveSys.autosave(); Game.checkQuestAuto(); UI.refresh(); }, questStageText(qid) { const q = G.quests[qid]; if (!q) return null; const def = QUESTS[qid]; if (q.done) return def.doneText || 'Complete.'; let txt = def.stages[q.stage] || ''; txt = txt.replace('%K/wolves', `${Math.min(G.flags.wolfKills || 0, 3)}/3`); txt = txt.replace('%K/sentinels', `${Math.min(G.flags.sentinelKills || 0, 2)}/2`); return txt; }, checkQuestAuto() { const q4 = G.quests['q_main_4']; if (q4 && !q4.done && q4.stage === 0 && G.allies.length >= 2) Game.setStage('q_main_4', 1); if (q4 && !q4.done && q4.stage === 1) { if (G.flags.azureMember || G.flags.fistMember || G.flags.serpentMember || G.flags.templeMember || (G.rep.garrison || 0) >= 30) Game.setStage('q_main_4', 2); } }, checkTrialFl() { const qf = G.quests['q_trial_fl']; if (!qf || qf.done || qf.stage !== 0) return; if (G.flags.trialFlMed && G.flags.trialFlSpar) { Game.setStage('q_trial_fl', 1); Log.add('Swept courtyard, stilled breath, friendly bruises โ€” the trial feels complete.', 'sys'); } }, searchDeepBamboo() { Game.startBattle({ ids: ['e_wolf', 'e_wolf', 'e_tiger'], scale: 0.95, meta: { kind: 'quest', onWin: () => { Log.add('Deep in the black bamboo you find Uncle Ping\u2019s camp \u2014 cold ashes, and a carved walking stick you recognize.', 'sys'); Game.setStage('q_missing', 2); } } }); }, banditCampAction() { const q = G.quests['q_main_2']; if (!q || q.done) return; if (q.stage === 1 || q.stage === 2) { Game.startBattle({ ids: ['e_bandit', 'e_bandit_vet', 'e_boss_qiang'], scale: 1, meta: { kind: 'boss', boss: 'qiang', onWin: () => { Game.setStage('q_main_2', 3); } } }); } else if (q.stage === 3) { Log.add('Under Qiang\u2019s cot: a lead token stamped with a black umbrella\u2026', 'sys'); Game.addItem('mat_token', 1); Game.setStage('q_main_2', 4); } }, tombAction(which) { const q = G.quests['q_main_5']; if (!q || q.done) return; if (which === 'descent') { if (q.stage === 1) { Game.startBattle({ ids: ['e_sentinel'], scale: 1, meta: { kind: 'tomb', onWin: () => { if ((G.flags.sentinelKills || 0) >= 2) Game.setStage('q_main_5', 2); else Toast.show(`One guardian silenced. (${G.flags.sentinelKills || 0}/2)`); } } }); } else if (q.stage === 2) { Game.startBattle({ ids: ['e_sentinel', 'e_assassin'], scale: 1.05, meta: { kind: 'boss', boss: 'door', onWin: () => { Log.add('Heihu crashes down among the broken guardians. \u201cThe Patriarch sends his regards.\u201d', 'sys'); Game.startBattle({ ids: ['e_boss_heihu'], scale: 1, meta: { kind: 'boss', boss: 'heihu', onWin: () => { Game.setStage('q_main_5', 3); Log.add('Beyond the fallen enforcer, the inner door grinds open\u2026', 'sys'); } } }); } } }); } } else if (which === 'claim') { if (q.stage === 3) { Game.addItem('mat_ledger', 1); Game.unlockAch('keeper_truth'); Game.completeQuest('q_main_5', { exp: 500, fame: 40 }); G.flags.mainChapter = 5; Game.startQuest('q_main_6'); } } }, finalAssault() { const q = G.quests['q_main_6']; if (!q || q.done) return; if (q.stage === 0) { Game.startBattle({ ids: ['e_cultist', 'e_cultist', 'e_assassin'], scale: 1.05, meta: { kind: 'boss', boss: 'wave', onWin: () => { Game.setStage('q_main_6', 1); Log.add('Through the parting umbrellas strides an old man whose shadow falls wrong\u2026', 'bad'); } } }); } else if (q.stage === 1) { Game.startBattle({ ids: ['e_boss_wu'], scale: 1, meta: { kind: 'boss', boss: 'wu', onWin: () => { Game.completeQuest('q_main_6', { exp: 800, fame: 100 }); G.flags.mainChapter = 6; setTimeout(() => Game.showEnding(), 400); } } }); } }, showEnding() { const f = G.rep.fame, inf = G.rep.infamy; const villain = !!G.flags.villainPath; let code, title, body = ''; if (villain) { code = 'iron'; title = 'THE NEW IRON PATRIARCH'; body = `Wu Zhaoshan falls \u2014 and the umbrellas do not close; they turn.\n\nThey kneel to YOU. The Heaven\u2019s Ledger burns in your brazier, name by name,\nand magistrates wake to find their masters changed.\nThe jianghu learns a new proverb: when it rains, carry the iron umbrella.\n\nHistory will argue forever whether you saved the rivers and lakes\nor merely became the next storm.`; } else if (f - inf * 2 >= 160) { code = 'legend'; title = 'GRAND MASTER OF THE AGE'; body = `The tale of the ridge-clearer who toppled an empire\u2019s shadow travels\nfarther than any caravan. Sects name training halls after you;\nbeggars toast you in wines you never drank.\n\nYet you still rise early, sweep the courtyard yourself,\nand send silver home to Willow Creek.\n\nSome legends wear crowns. Yours wears road dust.`; } else if (f - inf * 2 >= 70) { code = 'hero'; title = 'HERO OF THE RIVERS AND LAKES'; body = `The Iron Umbrella Society collapses; its ledgers hang in the Garrison hall\nlike a trophy made of paper thunder.\n\nYou stay long enough for the festivals, then slip away before they can\nbuild you a statue. There are always more roads than reputations,\nand yours still itches.`; } else { code = 'wander'; title = 'WANDERER OF THE OPEN ROAD'; body = `The conspiracy dies quietly, the way most evils do \u2014 unfunded, unnamed.\n\nYou take no credit and seek none. In taverns from here to the desert,\nsomeone swears they once shared a fire with a stranger\nwho fought like ten rivers.\n\nThe stranger always leaves before dawn.`; } const partnerLine = G.partner ? `\n\nAt your side, ${NPCS[G.partner].name} watches the horizon and smiles:\nwherever you wander, that is home enough.` : ''; const statsLine = `\n\nโ€” Journey\u2019s End โ€”\nDays on the road: ${G.time.day} ยท Level ${G.player.lvl}\nFame ${G.rep.fame} ยท Infamy ${G.rep.infamy} ยท Silver ${G.player.silver}\nFoes felled: ${G.flags.totalKills || 0}`; UI.showEnding(title, body + partnerLine + statsLine); try { localStorage.removeItem(SaveSys.KEY + 'auto'); } catch (e) {} }, /* ---------------- misc ---------------- */ dailyReset() { G.daily = defaultDaily(); if (typeof W2D !== 'undefined' && W2D.entsCache) W2D.entsCache = {}; // respawn world beasts Game.rollBounties(); // Hong appears randomly in the city each day G.flags.hongVisible = G.time.day >= 4 && Util.chance(0.4); }, npcsVisibleHere() { return (LOCS[G.loc].npcs || []).filter(id => { const n = NPCS[id]; if (n.hiddenNpc) return id === 'n_beggar_hong' ? !!G.flags.hongVisible && G.loc === 'l_city' : true; return true; }); } }; function UTIL_INT_NAME(id) { return INTERNALS[id] ? INTERNALS[id].name : id; } function QSsafe(q) { return ((G.quests[q] || {}).done ? 99 : ((G.quests[q] || {}).stage ?? -1)); }