herd / commit
Add 10-level system with thematic level configs
src/levels/LevelConfig.ts | 301 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/src/levels/LevelConfig.ts b/src/levels/LevelConfig.ts new file mode 100644 index 0000000..7a95ea2 --- /dev/null +++ b/src/levels/LevelConfig.ts @@ -0,0 +1,301 @@ +import type { GateSide } from '../entities/Pen.ts' +import type { SheepProps } from '../entities/Sheep.ts' + +export interface SpawnCluster { + cx: number // fraction 0–1 of canvas width + cy: number // fraction 0–1 of canvas height + r: number // spread radius in fraction of min(w,h) + count: number +} + +export interface ObstacleDef { + x: number; y: number; r: number // fractions of canvas size; r fraction of min(w,h) +} + +export interface DynamicObstacleDef extends ObstacleDef { + appearsAtSeconds: number +} + +export interface SheepOverride extends SheepProps { + index: number +} + +export interface LevelConfig { + id: number + title: string + subtitle: string + + sheepCount: number + penRect: { x: number; y: number; w: number; h: number } // fractions of canvas + penGateSide: GateSide + timeLimitSeconds: number | null + winThreshold: number + + spawnClusters: SpawnCluster[] + obstacles: ObstacleDef[] + dynamicObstacles: DynamicObstacleDef[] + sheepOverrides: SheepOverride[] + + darkMode: boolean + penShrinksEverySeconds: number | null + + // per-level dog speed override (null = default 160 px/s) + dogSpeed: number | null + // per-level sheep max speed multiplier (null = 1.0) + sheepSpeedMult: number | null +} + +export const LEVELS: LevelConfig[] = [ + { + id: 1, + title: 'First Lesson', + subtitle: '', + sheepCount: 6, + penRect: { x: 0.68, y: 0.05, w: 0.25, h: 0.25 }, + penGateSide: 'bottom', + timeLimitSeconds: 120, + winThreshold: 0.8, + spawnClusters: [{ cx: 0.5, cy: 0.55, r: 0.08, count: 6 }], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: [], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 2, + title: 'The Scatter', + subtitle: 'Rural depopulation — communities spread too thin to self-organize.', + sheepCount: 12, + penRect: { x: 0.38, y: 0.78, w: 0.22, h: 0.18 }, + penGateSide: 'top', + timeLimitSeconds: 150, + winThreshold: 1.0, + spawnClusters: [ + { cx: 0.08, cy: 0.1, r: 0.04, count: 3 }, + { cx: 0.92, cy: 0.1, r: 0.04, count: 3 }, + { cx: 0.08, cy: 0.82, r: 0.04, count: 3 }, + { cx: 0.92, cy: 0.82, r: 0.04, count: 3 }, + ], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: [], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 3, + title: 'The Holdouts', + subtitle: 'Herd immunity — the last few require disproportionate effort.', + sheepCount: 15, + penRect: { x: 0.05, y: 0.35, w: 0.22, h: 0.28 }, + penGateSide: 'right', + timeLimitSeconds: 180, + winThreshold: 1.0, + spawnClusters: [ + { cx: 0.55, cy: 0.5, r: 0.12, count: 12 }, + { cx: 0.88, cy: 0.12, r: 0.03, count: 1 }, + { cx: 0.88, cy: 0.88, r: 0.03, count: 1 }, + { cx: 0.5, cy: 0.12, r: 0.03, count: 1 }, + ], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: [ + { index: 12, stubborn: true }, + { index: 13, stubborn: true }, + { index: 14, stubborn: true }, + ], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 4, + title: 'Two Tribes', + subtitle: 'Political polarization — two groups that actively repel each other.', + sheepCount: 20, + penRect: { x: 0.37, y: 0.35, w: 0.26, h: 0.28 }, + penGateSide: 'left', + timeLimitSeconds: 180, + winThreshold: 1.0, + spawnClusters: [ + { cx: 0.12, cy: 0.5, r: 0.08, count: 10 }, + { cx: 0.88, cy: 0.5, r: 0.08, count: 10 }, + ], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: [ + ...Array.from({ length: 10 }, (_, i) => ({ index: i, subFlock: 0 })), + ...Array.from({ length: 10 }, (_, i) => ({ index: i + 10, subFlock: 1 })), + ], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 5, + title: 'Shrinking Pasture', + subtitle: 'Habitat loss — the environment erodes in real time.', + sheepCount: 15, + penRect: { x: 0.05, y: 0.05, w: 0.22, h: 0.22 }, + penGateSide: 'right', + timeLimitSeconds: 210, + winThreshold: 0.85, + spawnClusters: [{ cx: 0.55, cy: 0.55, r: 0.15, count: 15 }], + obstacles: [ + { x: 0.88, y: 0.3, r: 0.04 }, + { x: 0.88, y: 0.6, r: 0.04 }, + { x: 0.88, y: 0.88, r: 0.04 }, + ], + dynamicObstacles: [ + { x: 0.75, y: 0.15, r: 0.04, appearsAtSeconds: 30 }, + { x: 0.75, y: 0.85, r: 0.04, appearsAtSeconds: 60 }, + { x: 0.6, y: 0.88, r: 0.04, appearsAtSeconds: 90 }, + { x: 0.62, y: 0.12, r: 0.04, appearsAtSeconds: 120 }, + { x: 0.5, y: 0.82, r: 0.04, appearsAtSeconds: 150 }, + { x: 0.5, y: 0.18, r: 0.04, appearsAtSeconds: 180 }, + ], + sheepOverrides: [], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 6, + title: 'Already Spooked', + subtitle: 'Displaced populations — heavy-handed intervention makes things worse.', + sheepCount: 18, + penRect: { x: 0.7, y: 0.7, w: 0.26, h: 0.24 }, + penGateSide: 'top', + timeLimitSeconds: 240, + winThreshold: 0.8, + spawnClusters: [ + { cx: 0.15, cy: 0.2, r: 0.05, count: 3 }, + { cx: 0.5, cy: 0.15, r: 0.05, count: 3 }, + { cx: 0.82, cy: 0.2, r: 0.05, count: 3 }, + { cx: 0.15, cy: 0.75, r: 0.05, count: 3 }, + { cx: 0.5, cy: 0.8, r: 0.05, count: 3 }, + { cx: 0.82, cy: 0.4, r: 0.05, count: 3 }, + ], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: Array.from({ length: 18 }, (_, i) => ({ index: i, startFear: 1.0 })), + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: 110, + sheepSpeedMult: null, + }, + { + id: 7, + title: 'The Influencers', + subtitle: 'Opinion dynamics — redirect the few and the many follow.', + sheepCount: 22, + penRect: { x: 0.7, y: 0.3, w: 0.24, h: 0.38 }, + penGateSide: 'left', + timeLimitSeconds: 180, + winThreshold: 0.9, + spawnClusters: [{ cx: 0.25, cy: 0.5, r: 0.18, count: 22 }], + obstacles: [ + { x: 0.5, y: 0.25, r: 0.04 }, + { x: 0.5, y: 0.75, r: 0.04 }, + ], + dynamicObstacles: [], + sheepOverrides: [ + { index: 0, leader: true }, + { index: 1, leader: true }, + { index: 2, leader: true }, + { index: 3, leader: true }, + ], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 8, + title: 'Race to Market', + subtitle: 'Industrial agriculture — volume, speed, efficiency above all else.', + sheepCount: 30, + penRect: { x: 0.38, y: 0.05, w: 0.24, h: 0.3 }, + penGateSide: 'bottom', + timeLimitSeconds: 75, + winThreshold: 0.8, + spawnClusters: [ + { cx: 0.2, cy: 0.65, r: 0.12, count: 15 }, + { cx: 0.8, cy: 0.65, r: 0.12, count: 15 }, + ], + obstacles: [], + dynamicObstacles: [], + sheepOverrides: [], + darkMode: false, + penShrinksEverySeconds: null, + dogSpeed: 200, + sheepSpeedMult: 1.2, + }, + { + id: 9, + title: 'Lights Out', + subtitle: 'Information asymmetry — you can only see what is directly in front of you.', + sheepCount: 16, + penRect: { x: 0.38, y: 0.38, w: 0.24, h: 0.24 }, + penGateSide: 'bottom', + timeLimitSeconds: 270, + winThreshold: 1.0, + spawnClusters: [ + { cx: 0.1, cy: 0.1, r: 0.06, count: 2 }, + { cx: 0.5, cy: 0.05, r: 0.06, count: 2 }, + { cx: 0.9, cy: 0.1, r: 0.06, count: 2 }, + { cx: 0.1, cy: 0.9, r: 0.06, count: 2 }, + { cx: 0.5, cy: 0.9, r: 0.06, count: 2 }, + { cx: 0.9, cy: 0.9, r: 0.06, count: 2 }, + { cx: 0.1, cy: 0.5, r: 0.06, count: 1 }, + { cx: 0.9, cy: 0.5, r: 0.06, count: 1 }, + ], + obstacles: [ + { x: 0.25, y: 0.5, r: 0.03 }, + { x: 0.75, y: 0.5, r: 0.03 }, + ], + dynamicObstacles: [], + sheepOverrides: [], + darkMode: true, + penShrinksEverySeconds: null, + dogSpeed: null, + sheepSpeedMult: null, + }, + { + id: 10, + title: 'The Last Pasture', + subtitle: 'Climate crisis — the window closes slowly, then all at once.', + sheepCount: 25, + penRect: { x: 0.65, y: 0.05, w: 0.3, h: 0.35 }, + penGateSide: 'bottom', + timeLimitSeconds: null, + winThreshold: 1.0, + spawnClusters: [ + { cx: 0.2, cy: 0.45, r: 0.12, count: 13 }, + { cx: 0.45, cy: 0.8, r: 0.12, count: 12 }, + ], + obstacles: [ + { x: 0.5, y: 0.3, r: 0.1 }, // dried-out lake blocking direct route + ], + dynamicObstacles: [], + sheepOverrides: [ + { index: 0, stubborn: true }, + { index: 1, stubborn: true }, + { index: 2, stubborn: true }, + { index: 23, leader: true }, + { index: 24, leader: true }, + ], + darkMode: false, + penShrinksEverySeconds: 25, + dogSpeed: null, + sheepSpeedMult: null, + }, +]