// village.ts — village state, assignment loop, and manual micro-management // // This is the highest-priority system to validate in the PoC. The core design // question: does the manual work loop feel meaningfully better than passive // waiting, without becoming mandatory misery? // // Passive resource generation rules: // - Each job slot generates resources while characters are assigned. // - Output is calculated at collection time (lazy evaluation), not on a tick. // - Passive output caps after MAX_PASSIVE_HOURS hours to discourage long // check-in skips from breaking the economy. // - A character's profession suitability bonus multiplies their slot output. // // Manual micro-management rules: // - The player can spend MANUAL_WORK_COST_MANA to trigger a burst gain on // one scarce resource (default: wood — defined as the first bottleneck). // - Burst gain is 3–5x the passive rate per action but subject to a daily cap. // - Each successive manual action within the same day gives diminishing returns // (50% yield per extra action after the third). // - Server tracks manual_work_count_today and manual_work_reset_at per player. // // These constants are PoC defaults. Production values must come from content. const MAX_PASSIVE_HOURS = 8; // cap passive accumulation at 8 hours const MANUAL_WORK_COST_MANA = 5; const MANUAL_WORK_DAILY_FULL_CAP = 3; // first 3 actions give full burst yield const MANUAL_WORK_BURST_MULTIPLIER = 4; // 4x passive rate per burst action const MANUAL_WORK_DIMINISHING_FACTOR = 0.5; // 50% per extra action after cap export function rpcGetVillageState( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, _payload: string ): string { // TODO PR 8: read village state, assignment slots, and pending passive yield logger.info(`village/state stub called by ${ctx.userId}`); return JSON.stringify({ slots: [], pending_resources: {}, message: "Village state is stubbed. Implement in PR 8.", }); } export function rpcAssignCharacter( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string ): string { // TODO PR 8: validate character ownership, job compatibility, write assignment const req = JSON.parse(payload || "{}"); logger.info(`village/assign stub: char=${req.character_id} job=${req.job_id} user=${ctx.userId}`); return JSON.stringify({ ok: false, error: "not_implemented", message: "Assignment is stubbed. Implement in PR 8.", }); } export function rpcUnassignCharacter( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string ): string { const req = JSON.parse(payload || "{}"); logger.info(`village/unassign stub: char=${req.character_id} user=${ctx.userId}`); return JSON.stringify({ ok: false, error: "not_implemented", message: "Unassignment is stubbed. Implement in PR 8.", }); } export function rpcCollectPassiveResources( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, _payload: string ): string { // TODO PR 8: // 1. Read assignment state and last_collected_at timestamp. // 2. Calculate elapsed time (capped at MAX_PASSIVE_HOURS). // 3. Sum per-slot output adjusted by suitability bonuses. // 4. Grant resources via wallet update with ledger entry. // 5. Write new last_collected_at. logger.info(`village/collect stub called by ${ctx.userId}`); return JSON.stringify({ ok: false, granted: {}, error: "not_implemented", message: "Passive collection is stubbed. Implement in PR 8.", }); } export function rpcManualWork( ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string ): string { // TODO PR 5 (priority — this is the core PoC differentiator): // 1. Read player manual_work_count_today; reset if a new day. // 2. Check mana balance >= MANUAL_WORK_COST_MANA. // 3. Deduct mana. // 4. Calculate burst yield: // - If count < MANUAL_WORK_DAILY_FULL_CAP: full burst // - Else: apply MANUAL_WORK_DIMINISHING_FACTOR per extra action // 5. Grant scarce resource (wood by default in PoC). // 6. Increment manual_work_count_today. // 7. Return new balance and yield so the client can show the comparison. const req = JSON.parse(payload || "{}"); const targetResource: string = req.resource ?? "wood"; logger.info(`village/manual_work stub: resource=${targetResource} user=${ctx.userId}`); return JSON.stringify({ ok: false, error: "not_implemented", resource: targetResource, burst_yield: 0, passive_equivalent: 0, daily_actions_used: 0, daily_actions_cap: MANUAL_WORK_DAILY_FULL_CAP, message: "Manual work is stubbed. This is the highest-priority PoC feature — implement in PR 5.", }); }