feat: add thirst to Needs, drink/forage to GoalType, thirst constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-10 20:26:54 +00:00
parent 30b187b838
commit 5da03ebb69
10 changed files with 564 additions and 1 deletions

63
shared/dist/constants.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
export declare const TILE_SIZE = 48;
export declare const WORLD_WIDTH = 64;
export declare const WORLD_HEIGHT = 64;
export declare const TICK_RATE = 10;
export declare const MOVE_SPEED = 0.75;
export declare const BROADCAST_EVERY_N_TICKS = 3;
export declare const SPRITE_FRAME_WIDTH = 48;
export declare const SPRITE_FRAME_HEIGHT = 48;
export declare const SPRITE_COLS = 6;
export declare const SPRITE_ROWS = 4;
export declare const HUNGER_DECAY_PER_TICK = 0.05;
export declare const ENERGY_DECAY_PER_TICK = 0.03;
export declare const HUNGER_THRESHOLD = 30;
export declare const THIRST_DECAY_PER_TICK = 0.05;
export declare const THIRST_THRESHOLD = 30;
export declare const ENERGY_THRESHOLD = 20;
export declare const NEED_RECOVERY_RATE = 0.5;
export declare const SLEEP_ENERGY_RECOVERY_PER_TICK = 0.048;
export declare const SLEEP_HUNGER_DECAY_MULTIPLIER = 0.5;
export declare const SLEEP_WAKE_THRESHOLD = 85;
export declare const SLEEP_VOLUNTARY_ENERGY_THRESHOLD = 60;
export declare const SLEEP_NIGHT_START = 0.667;
export declare const PRODUCTIVITY_DECAY_PER_TICK = 0.02;
export declare const PRODUCTIVITY_THRESHOLD = 40;
export declare const PRODUCTIVITY_RECOVERY_RATE = 0.5;
export declare const DAY_NIGHT_RATIO = 2;
export declare const DAY_HOURS = 12;
export declare const NIGHT_HOURS: number;
export declare const TOTAL_HOURS: number;
export declare const SUNSET_DURATION_HOURS = 1;
export declare const SUNRISE_DURATION_HOURS = 1;
export declare const NIGHT_DARKNESS = 0.45;
export declare const Direction: {
readonly DOWN: 0;
readonly LEFT: 1;
readonly UP: 2;
readonly RIGHT: 3;
};
export type Direction = (typeof Direction)[keyof typeof Direction];
export declare const ACCESSORY_SLOTS: readonly ["bottom", "feet", "chest", "arm", "shoulder", "waist", "back", "facialHair", "haircut", "hat"];
export type AccessorySlot = (typeof ACCESSORY_SLOTS)[number];
export declare const PORTRAIT_SLOTS: readonly ["brows", "eyes", "mouths"];
export type PortraitSlot = (typeof PORTRAIT_SLOTS)[number];
export declare const SPRITE_TO_PORTRAIT_FOLDER: Record<string, string>;
export declare const AWARENESS_RADIUS = 5;
export declare const FACING_DURATION = 10;
export declare const PAUSING_DURATION = 15;
export declare const EMOTING_DURATION = 20;
export declare const SOCIAL_GLOBAL_COOLDOWN = 75;
export declare const SOCIAL_PAIR_COOLDOWN = 450;
export declare const PROPOSAL_EMOTING_DURATION = 30;
export declare const MAX_NPC_COUNT = 50;
export declare const Terrain: {
readonly GRASS: 0;
readonly WATER: 1;
readonly DIRT: 2;
readonly STONE: 3;
};
export type Terrain = (typeof Terrain)[keyof typeof Terrain];
export declare const TILESET_TILE_SIZE = 32;
export declare const LPC_COLS = 3;
export declare const TILESET_SCALE: number;
export declare const TREE_TILESET_COLS = 8;

86
shared/dist/constants.js vendored Normal file
View File

@@ -0,0 +1,86 @@
export const TILE_SIZE = 48;
export const WORLD_WIDTH = 64;
export const WORLD_HEIGHT = 64;
export const TICK_RATE = 10; // server ticks per second
export const MOVE_SPEED = 0.75; // tiles per tick (NPC movement speed)
export const BROADCAST_EVERY_N_TICKS = 3; // state broadcast frequency
export const SPRITE_FRAME_WIDTH = 48;
export const SPRITE_FRAME_HEIGHT = 48;
export const SPRITE_COLS = 6;
export const SPRITE_ROWS = 4;
// NPC needs
export const HUNGER_DECAY_PER_TICK = 0.05;
export const ENERGY_DECAY_PER_TICK = 0.03;
export const HUNGER_THRESHOLD = 30;
export const THIRST_DECAY_PER_TICK = 0.05;
export const THIRST_THRESHOLD = 30;
export const ENERGY_THRESHOLD = 20;
export const NEED_RECOVERY_RATE = 0.5;
// Sleep system
export const SLEEP_ENERGY_RECOVERY_PER_TICK = 0.048; // ~80 energy over one night (~1667 ticks)
export const SLEEP_HUNGER_DECAY_MULTIPLIER = 0.5; // hunger decays at half rate while sleeping
export const SLEEP_WAKE_THRESHOLD = 85; // energy level where waking chance begins
export const SLEEP_VOLUNTARY_ENERGY_THRESHOLD = 60; // nighttime voluntary sleep threshold
export const SLEEP_NIGHT_START = 0.667; // gameTime when night begins (DAY_HOURS/TOTAL_HOURS)
// Productivity need
export const PRODUCTIVITY_DECAY_PER_TICK = 0.02;
export const PRODUCTIVITY_THRESHOLD = 40;
export const PRODUCTIVITY_RECOVERY_RATE = 0.5;
// Day/night cycle
export const DAY_NIGHT_RATIO = 2; // day is 2x as long as night
export const DAY_HOURS = 12; // game hours of daytime
export const NIGHT_HOURS = DAY_HOURS / DAY_NIGHT_RATIO; // 6 hours of night
export const TOTAL_HOURS = DAY_HOURS + NIGHT_HOURS; // 18 hours total cycle
export const SUNSET_DURATION_HOURS = 1; // game hours for sunset transition
export const SUNRISE_DURATION_HOURS = 1; // game hours for sunrise transition
export const NIGHT_DARKNESS = 0.45; // max overlay opacity (0-1)
// Directions (row index in spritesheet)
export const Direction = {
DOWN: 0,
LEFT: 1,
UP: 2,
RIGHT: 3,
};
// Accessory slot names in z-order (bottom to top rendering)
export const ACCESSORY_SLOTS = [
'bottom', 'feet', 'chest', 'arm', 'shoulder',
'waist', 'back', 'facialHair', 'haircut', 'hat',
];
// Portrait-only slots (facial features for close-up portraits)
export const PORTRAIT_SLOTS = ['brows', 'eyes', 'mouths'];
// Mapping from sprite accessory slot names to portrait folder names
// (some folders are pluralized in the portrait assets)
export const SPRITE_TO_PORTRAIT_FOLDER = {
arm: 'arms',
back: 'back',
bottom: 'bottom',
chest: 'chest',
facialHair: 'facialHair',
// feet: no portrait equivalent
haircut: 'haircuts',
hat: 'hats',
shoulder: 'shoulders',
waist: 'waist',
};
// Social interactions
export const AWARENESS_RADIUS = 5; // Manhattan distance in tiles
export const FACING_DURATION = 10; // ticks (1s)
export const PAUSING_DURATION = 15; // ticks (1.5s)
export const EMOTING_DURATION = 20; // ticks (2s)
export const SOCIAL_GLOBAL_COOLDOWN = 75; // ticks (7.5s)
export const SOCIAL_PAIR_COOLDOWN = 450; // ticks (45s)
export const PROPOSAL_EMOTING_DURATION = 30; // 3 seconds - longer for dramatic effect
// Camera mode commands
export const MAX_NPC_COUNT = 50;
// Terrain types for procedural map
export const Terrain = {
GRASS: 0,
WATER: 1,
DIRT: 2,
STONE: 3,
};
// LPC tileset constants (32x32 tiles scaled 1.5x to match TILE_SIZE=48)
export const TILESET_TILE_SIZE = 32;
export const LPC_COLS = 3; // terrain tilesets are 3 cols wide (96px / 32px)
export const TILESET_SCALE = TILE_SIZE / TILESET_TILE_SIZE; // 1.5
export const TREE_TILESET_COLS = 8; // treesnstone.png is 8 cols wide (256px / 32px)

3
shared/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export * from './constants.js';
export * from './narration.js';
export * from './types.js';

3
shared/dist/index.js vendored Normal file
View File

@@ -0,0 +1,3 @@
export * from './constants.js';
export * from './narration.js';
export * from './types.js';

12
shared/dist/narration.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import type { EntityId, InteractionOutcome } from './types.js';
export type NarrationOutcome = InteractionOutcome | 'proposal_accepted' | 'proposal_rejected';
export interface NarrationEvent {
id: number;
tick: number;
type: 'social' | 'proposal' | 'invention';
entityIds: [EntityId, EntityId];
names: [string, string];
outcome: NarrationOutcome;
narration: string;
isLlmGenerated: boolean;
}

1
shared/dist/narration.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

389
shared/dist/types.d.ts vendored Normal file
View File

@@ -0,0 +1,389 @@
import type { AccessorySlot, PortraitSlot } from './constants.js';
import type { NarrationEvent } from './narration.js';
export type EntityId = number;
export type MemoryEventType = 'social_positive' | 'social_negative' | 'proposal_accepted' | 'proposal_rejected' | 'tier_change' | 'need_crisis' | 'need_recovery' | 'goal_change' | 'bond_formed' | 'bond_dissolved' | 'spawned' | 'invention' | 'desire_fulfilled' | 'desire_added';
export interface MemoryEvent {
id: number;
type: MemoryEventType;
tick: number;
otherEntityId?: EntityId;
otherName?: string;
detail: string;
oldTier?: string;
newTier?: string;
need?: 'hunger' | 'energy' | 'productivity';
oldGoal?: string;
newGoal?: string;
}
export interface Position {
x: number;
y: number;
}
export interface Velocity {
dx: number;
dy: number;
}
export interface Appearance {
skinId: string;
accessories: Partial<Record<AccessorySlot, string>>;
portraitFeatures?: Partial<Record<PortraitSlot, string>>;
}
export interface Needs {
hunger: number;
thirst: number;
energy: number;
productivity: number;
}
export interface Stats {
strength: number;
dexterity: number;
constitution: number;
intelligence: number;
perception: number;
sociability: number;
courage: number;
curiosity: number;
empathy: number;
temperament: number;
}
export type StatName = keyof Stats;
export interface StatModifier {
stat: StatName;
value: number;
remaining: number;
}
export interface StatModifiers {
modifiers: StatModifier[];
}
export type MovementState = 'idle' | 'walking';
export type GoalType = 'wander' | 'eat' | 'drink' | 'forage' | 'sleep' | 'gather' | 'craft' | 'build' | 'dropoff' | 'pickup';
export interface Movement {
state: MovementState;
target: Position | null;
path: Position[];
direction: number;
moveProgress: number;
}
export interface PlayerControlled {
playerId: string;
mode: 'avatar' | 'camera';
}
export interface NPCBrain {
currentGoal: GoalType | null;
goalQueue: GoalType[];
gatherTarget?: {
x: number;
y: number;
resourceType: string;
} | null;
}
export type InteractionPhase = 'none' | 'facing' | 'pausing' | 'emoting' | 'proposing';
export type InteractionOutcome = 'positive' | 'negative';
export interface SocialState {
phase: InteractionPhase;
partnerId: EntityId | null;
phaseTimer: number;
outcome: InteractionOutcome | null;
globalCooldown: number;
pairCooldowns: Map<EntityId, number>;
lastOutcome: LastOutcome | null;
proposalCooldown: number;
pendingProposal: {
targetId: EntityId;
type: string;
} | null;
isProposalInteraction: boolean;
}
export interface RelationshipData {
value: number;
interactions: number;
lastInteractionTick: number;
status: 'active' | 'memory';
}
export type Relationships = Map<EntityId, RelationshipData>;
export interface LastOutcome {
partnerId: EntityId;
outcome: InteractionOutcome;
tick: number;
}
export interface EntityState {
id: EntityId;
position: Position;
movement: Movement;
appearance: Appearance;
needs?: Needs;
stats?: Stats;
npcBrain?: NPCBrain;
playerControlled?: PlayerControlled;
name?: string;
backstory?: string;
inventory?: Record<string, number>;
desires?: {
description: string;
category: DesireCategory;
}[];
socialState?: {
phase: InteractionPhase;
partnerId: EntityId | null;
outcome: InteractionOutcome | null;
};
relationships?: Array<{
entityId: EntityId;
name: string;
value: number;
classification: string;
status: 'active' | 'memory';
bond: string | null;
}>;
}
export interface WorldState {
entities: EntityState[];
worldWidth: number;
worldHeight: number;
tileSize: number;
obstacles: Position[];
pointsOfInterest: {
type: 'food';
position: Position;
}[];
terrain: number[];
decorations: number[];
trunkDecorations: number[];
resourceTiles: Array<{
x: number;
y: number;
resourceType: string;
}>;
}
export interface StateUpdate {
entities: EntityState[];
tick: number;
gameTime: number;
dayNumber: number;
}
export interface PlayerJoined {
playerId: string;
entityId: EntityId;
}
export interface PlayerLeft {
playerId: string;
}
export interface PlayerInput {
type: 'move' | 'toggle-mode' | 'follow' | 'interact';
direction?: {
dx: number;
dy: number;
};
targetPlayerId?: string;
targetEntityId?: EntityId;
}
export interface SuperlativeEntry {
entityId: EntityId;
name: string;
value: number;
}
export interface SuperlativesData {
mostLoved: SuperlativeEntry | null;
mostPopular: SuperlativeEntry | null;
mostReviled: SuperlativeEntry | null;
mostAnnoying: SuperlativeEntry | null;
shyest: SuperlativeEntry | null;
mostOutgoing: SuperlativeEntry | null;
biggestHeartbreaker: SuperlativeEntry | null;
mostDevoted: SuperlativeEntry | null;
loneliest: SuperlativeEntry | null;
mostPolarizing: SuperlativeEntry | null;
socialButterfly: SuperlativeEntry | null;
mostInventive: SuperlativeEntry | null;
}
export interface InventionSummary {
itemId: string;
name: string;
category: 'resource' | 'tool' | 'material' | 'structure';
inputs: {
itemId: string;
quantity: number;
}[];
workshopType: string | null;
toolRequired: string | null;
inventorName: string;
day: number;
}
export interface StockpileLogEntry {
npcName: string;
action: 'dropoff' | 'pickup';
itemId: string;
quantity: number;
tick: number;
}
export type DesireCategory = 'material' | 'social' | 'shelter' | 'comfort' | 'community' | 'creative';
export type FulfillmentCriteria = {
type: 'own_item';
itemId: string;
quantity: number;
} | {
type: 'own_item_category';
category: string;
quantity: number;
} | {
type: 'structure_exists';
structureType: string;
} | {
type: 'building_exists';
buildingType: string;
} | {
type: 'relationship_tier';
tier: string;
count: number;
} | {
type: 'recipe_exists';
tag: string;
} | {
type: 'custom';
check: string;
};
export interface Desire {
id: string;
description: string;
category: DesireCategory;
fulfillment: FulfillmentCriteria;
priority: number;
source: 'spawn' | 'event' | 'periodic';
sourceDetail?: string;
createdAtTick: number;
cooldownTicks?: number;
}
export interface TunableConstants {
TICK_RATE: number;
BROADCAST_EVERY_N_TICKS: number;
HUNGER_DECAY_PER_TICK: number;
ENERGY_DECAY_PER_TICK: number;
HUNGER_THRESHOLD: number;
THIRST_DECAY_PER_TICK: number;
THIRST_THRESHOLD: number;
ENERGY_THRESHOLD: number;
NEED_RECOVERY_RATE: number;
SLEEP_ENERGY_RECOVERY_PER_TICK: number;
SLEEP_HUNGER_DECAY_MULTIPLIER: number;
SLEEP_WAKE_THRESHOLD: number;
SLEEP_VOLUNTARY_ENERGY_THRESHOLD: number;
PRODUCTIVITY_DECAY_PER_TICK: number;
PRODUCTIVITY_THRESHOLD: number;
PRODUCTIVITY_RECOVERY_RATE: number;
DAY_NIGHT_RATIO: number;
DAY_HOURS: number;
SUNSET_DURATION_HOURS: number;
SUNRISE_DURATION_HOURS: number;
NIGHT_DARKNESS: number;
AWARENESS_RADIUS: number;
FACING_DURATION: number;
PAUSING_DURATION: number;
EMOTING_DURATION: number;
SOCIAL_GLOBAL_COOLDOWN: number;
SOCIAL_PAIR_COOLDOWN: number;
PROPOSAL_EMOTING_DURATION: number;
MAX_NPC_COUNT: number;
MOVE_SPEED: number;
}
export type TunableKey = keyof TunableConstants;
export type LogSeverity = 'error' | 'warning' | 'info';
export type LogCategory = 'LLM' | 'Save' | 'Network' | 'Performance' | 'Game';
export interface LogEntry {
timestamp: number;
severity: LogSeverity;
category: LogCategory;
message: string;
}
export interface LlmCallTypeStats {
templateName: string;
label: string;
count: number;
totalCost: number;
minCost: number;
maxCost: number;
avgCost: number;
retryCount: number;
failCount: number;
retryPct: number;
failPct: number;
}
export interface LlmStatsData {
types: LlmCallTypeStats[];
totalCount: number;
totalCost: number;
totalRetries: number;
totalFailures: number;
trackingEnabled: boolean;
}
export interface ServerEvents {
'world-state': (data: WorldState) => void;
'state-update': (data: StateUpdate) => void;
'player-joined': (data: PlayerJoined) => void;
'player-left': (data: PlayerLeft) => void;
'npc-recomposed': (data: {
entityId: EntityId;
appearance: Appearance;
}) => void;
'superlatives-update': (data: SuperlativesData) => void;
'narration-event': (data: NarrationEvent) => void;
'narration-update': (data: {
id: number;
narration: string;
}) => void;
'narration-history': (data: NarrationEvent[]) => void;
'npc-thought': (data: {
entityId: EntityId;
text: string;
emoji: string;
}) => void;
'memory-event': (data: {
entityId: EntityId;
event: MemoryEvent;
}) => void;
'memory-history': (data: {
entityId: EntityId;
events: MemoryEvent[];
}) => void;
'invention-event': (data: InventionSummary) => void;
'invention-history': (data: InventionSummary[]) => void;
'stockpile-event': (data: StockpileLogEntry) => void;
'stockpile-history': (data: StockpileLogEntry[]) => void;
'stockpile-summary': (data: Record<string, number>) => void;
'admin-auth-result': (data: {
success: boolean;
constants?: TunableConstants;
}) => void;
'admin-constants-updated': (data: TunableConstants) => void;
'log-history': (data: LogEntry[]) => void;
'log-entry': (data: LogEntry) => void;
'admin-llm-stats-result': (data: LlmStatsData) => void;
'admin-llm-tracking-toggled': (data: {
enabled: boolean;
}) => void;
}
export interface ClientEvents {
'player-input': (data: PlayerInput) => void;
'spawn-npc': (data: {
x: number;
y: number;
}) => void;
'superlatives-subscribe': () => void;
'superlatives-unsubscribe': () => void;
'follow-npc': (data: {
entityId: EntityId | null;
}) => void;
'admin-auth': (data: {
password: string;
}) => void;
'admin-update-constant': (data: {
key: TunableKey;
value: number;
}) => void;
'admin-reset-defaults': () => void;
'log-subscribe': () => void;
'log-unsubscribe': () => void;
'admin-llm-stats': () => void;
'admin-toggle-llm-tracking': (data: {
enabled: boolean;
}) => void;
}

1
shared/dist/types.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

View File

@@ -13,6 +13,8 @@ export const SPRITE_ROWS = 4;
export const HUNGER_DECAY_PER_TICK = 0.05;
export const ENERGY_DECAY_PER_TICK = 0.03;
export const HUNGER_THRESHOLD = 30;
export const THIRST_DECAY_PER_TICK = 0.05;
export const THIRST_THRESHOLD = 30;
export const ENERGY_THRESHOLD = 20;
export const NEED_RECOVERY_RATE = 0.5;

View File

@@ -49,6 +49,7 @@ export interface Appearance {
export interface Needs {
hunger: number; // 0-100
thirst: number; // 0-100
energy: number; // 0-100
productivity: number; // 0-100
}
@@ -81,7 +82,7 @@ export interface StatModifiers {
}
export type MovementState = 'idle' | 'walking';
export type GoalType = 'wander' | 'eat' | 'sleep' | 'gather' | 'craft' | 'build' | 'dropoff' | 'pickup';
export type GoalType = 'wander' | 'eat' | 'drink' | 'forage' | 'sleep' | 'gather' | 'craft' | 'build' | 'dropoff' | 'pickup';
export interface Movement {
state: MovementState;
@@ -275,6 +276,8 @@ export interface TunableConstants {
HUNGER_DECAY_PER_TICK: number;
ENERGY_DECAY_PER_TICK: number;
HUNGER_THRESHOLD: number;
THIRST_DECAY_PER_TICK: number;
THIRST_THRESHOLD: number;
ENERGY_THRESHOLD: number;
NEED_RECOVERY_RATE: number;
SLEEP_ENERGY_RECOVERY_PER_TICK: number;