xyvera/nakama/modules/village.ts
caoimhinr fb00954954 Initial scaffold: Nakama backend, Godot client, content schema
Sets up the Step 1 (Project Foundation) and Step 2 (Content Schemas) scaffold
from the implementation sequence. Includes Docker Compose stack, Nakama
TypeScript module stubs, PostgreSQL migration, Godot 4 project shell with
scene stubs and NakamaClient singleton, YAML content schemas for all entity
types, and example data for 3 characters, 2 jobs, and all 7 core resources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 23:33:06 +02:00

131 lines
4.8 KiB
TypeScript
Raw Permalink 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.

// 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 35x 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.",
});
}