fuzzyworld

herd / files / src / levels / LevelConfig.ts

LevelConfig.ts

8.7 KB · history · blame

1import type { GateSide } from '../entities/Pen.ts'
2import type { SheepProps } from '../entities/Sheep.ts'
3
4export interface SpawnCluster {
5 cx: number // fraction 0–1 of canvas width
6 cy: number // fraction 0–1 of canvas height
7 r: number // spread radius in fraction of min(w,h)
8 count: number
9}
10
11export interface ObstacleDef {
12 x: number; y: number; r: number // fractions of canvas size; r fraction of min(w,h)
13}
14
15export interface DynamicObstacleDef extends ObstacleDef {
16 appearsAtSeconds: number
17}
18
19export interface SheepOverride extends SheepProps {
20 index: number
21}
22
23export interface LevelConfig {
24 id: number
25 title: string
26 subtitle: string
27
28 sheepCount: number
29 penRect: { x: number; y: number; w: number; h: number } // fractions of canvas
30 penGateSide: GateSide
31 timeLimitSeconds: number | null
32 winThreshold: number // integer count of sheep that must enter the pen (≤ sheepCount)
33
34 spawnClusters: SpawnCluster[]
35 obstacles: ObstacleDef[]
36 dynamicObstacles: DynamicObstacleDef[]
37 sheepOverrides: SheepOverride[]
38
39 darkMode: boolean
40 penShrinksEverySeconds: number | null
41
42 // per-level dog speed override (null = default 160 px/s)
43 dogSpeed: number | null
44 // per-level sheep max speed multiplier (null = 1.0)
45 sheepSpeedMult: number | null
46}
47
48export const LEVELS: LevelConfig[] = [
49 {
50 id: 1,
51 title: 'First Lesson',
52 subtitle: '',
53 sheepCount: 6,
54 penRect: { x: 0.68, y: 0.05, w: 0.25, h: 0.25 },
55 penGateSide: 'bottom',
56 timeLimitSeconds: 120,
57 winThreshold: 5,
58 spawnClusters: [{ cx: 0.5, cy: 0.55, r: 0.08, count: 6 }],
59 obstacles: [],
60 dynamicObstacles: [],
61 sheepOverrides: [],
62 darkMode: false,
63 penShrinksEverySeconds: null,
64 dogSpeed: null,
65 sheepSpeedMult: null,
66 },
67 {
68 id: 2,
69 title: 'The Scatter',
70 subtitle: 'Rural depopulation — communities spread too thin to self-organize.',
71 sheepCount: 12,
72 penRect: { x: 0.38, y: 0.78, w: 0.22, h: 0.18 },
73 penGateSide: 'top',
74 timeLimitSeconds: 150,
75 winThreshold: 12,
76 spawnClusters: [
77 { cx: 0.08, cy: 0.1, r: 0.04, count: 3 },
78 { cx: 0.92, cy: 0.1, r: 0.04, count: 3 },
79 { cx: 0.08, cy: 0.82, r: 0.04, count: 3 },
80 { cx: 0.92, cy: 0.82, r: 0.04, count: 3 },
81 ],
82 obstacles: [],
83 dynamicObstacles: [],
84 sheepOverrides: [],
85 darkMode: false,
86 penShrinksEverySeconds: null,
87 dogSpeed: null,
88 sheepSpeedMult: null,
89 },
90 {
91 id: 3,
92 title: 'The Holdouts',
93 subtitle: 'Herd immunity — the last few require disproportionate effort.',
94 sheepCount: 15,
95 penRect: { x: 0.05, y: 0.35, w: 0.22, h: 0.28 },
96 penGateSide: 'right',
97 timeLimitSeconds: 180,
98 winThreshold: 15,
99 spawnClusters: [
100 { cx: 0.55, cy: 0.5, r: 0.12, count: 12 },
101 { cx: 0.88, cy: 0.12, r: 0.03, count: 1 },
102 { cx: 0.88, cy: 0.88, r: 0.03, count: 1 },
103 { cx: 0.5, cy: 0.12, r: 0.03, count: 1 },
104 ],
105 obstacles: [],
106 dynamicObstacles: [],
107 sheepOverrides: [
108 { index: 12, stubborn: true },
109 { index: 13, stubborn: true },
110 { index: 14, stubborn: true },
111 ],
112 darkMode: false,
113 penShrinksEverySeconds: null,
114 dogSpeed: null,
115 sheepSpeedMult: null,
116 },
117 {
118 id: 4,
119 title: 'Two Tribes',
120 subtitle: 'Political polarization — two groups that actively repel each other.',
121 sheepCount: 20,
122 penRect: { x: 0.37, y: 0.35, w: 0.26, h: 0.28 },
123 penGateSide: 'left',
124 timeLimitSeconds: 180,
125 winThreshold: 20,
126 spawnClusters: [
127 { cx: 0.12, cy: 0.5, r: 0.08, count: 10 },
128 { cx: 0.88, cy: 0.5, r: 0.08, count: 10 },
129 ],
130 obstacles: [],
131 dynamicObstacles: [],
132 sheepOverrides: [
133 ...Array.from({ length: 10 }, (_, i) => ({ index: i, subFlock: 0 })),
134 ...Array.from({ length: 10 }, (_, i) => ({ index: i + 10, subFlock: 1 })),
135 ],
136 darkMode: false,
137 penShrinksEverySeconds: null,
138 dogSpeed: null,
139 sheepSpeedMult: null,
140 },
141 {
142 id: 5,
143 title: 'Shrinking Pasture',
144 subtitle: 'Habitat loss — the environment erodes in real time.',
145 sheepCount: 15,
146 penRect: { x: 0.05, y: 0.05, w: 0.22, h: 0.22 },
147 penGateSide: 'right',
148 timeLimitSeconds: 210,
149 winThreshold: 13,
150 spawnClusters: [{ cx: 0.55, cy: 0.55, r: 0.15, count: 15 }],
151 obstacles: [
152 { x: 0.88, y: 0.3, r: 0.04 },
153 { x: 0.88, y: 0.6, r: 0.04 },
154 { x: 0.88, y: 0.88, r: 0.04 },
155 ],
156 dynamicObstacles: [
157 { x: 0.75, y: 0.15, r: 0.04, appearsAtSeconds: 30 },
158 { x: 0.75, y: 0.85, r: 0.04, appearsAtSeconds: 60 },
159 { x: 0.6, y: 0.88, r: 0.04, appearsAtSeconds: 90 },
160 { x: 0.62, y: 0.12, r: 0.04, appearsAtSeconds: 120 },
161 { x: 0.5, y: 0.82, r: 0.04, appearsAtSeconds: 150 },
162 { x: 0.5, y: 0.18, r: 0.04, appearsAtSeconds: 180 },
163 ],
164 sheepOverrides: [],
165 darkMode: false,
166 penShrinksEverySeconds: null,
167 dogSpeed: null,
168 sheepSpeedMult: null,
169 },
170 {
171 id: 6,
172 title: 'Already Spooked',
173 subtitle: 'Displaced populations — heavy-handed intervention makes things worse.',
174 sheepCount: 18,
175 penRect: { x: 0.7, y: 0.7, w: 0.26, h: 0.24 },
176 penGateSide: 'top',
177 timeLimitSeconds: 240,
178 winThreshold: 15,
179 spawnClusters: [
180 { cx: 0.15, cy: 0.2, r: 0.05, count: 3 },
181 { cx: 0.5, cy: 0.15, r: 0.05, count: 3 },
182 { cx: 0.82, cy: 0.2, r: 0.05, count: 3 },
183 { cx: 0.15, cy: 0.75, r: 0.05, count: 3 },
184 { cx: 0.5, cy: 0.8, r: 0.05, count: 3 },
185 { cx: 0.82, cy: 0.4, r: 0.05, count: 3 },
186 ],
187 obstacles: [],
188 dynamicObstacles: [],
189 sheepOverrides: Array.from({ length: 18 }, (_, i) => ({ index: i, startFear: 1.0 })),
190 darkMode: false,
191 penShrinksEverySeconds: null,
192 dogSpeed: 110,
193 sheepSpeedMult: null,
194 },
195 {
196 id: 7,
197 title: 'The Influencers',
198 subtitle: 'Opinion dynamics — redirect the few and the many follow.',
199 sheepCount: 22,
200 penRect: { x: 0.7, y: 0.3, w: 0.24, h: 0.38 },
201 penGateSide: 'left',
202 timeLimitSeconds: 180,
203 winThreshold: 20,
204 spawnClusters: [{ cx: 0.25, cy: 0.5, r: 0.18, count: 22 }],
205 obstacles: [
206 { x: 0.5, y: 0.25, r: 0.04 },
207 { x: 0.5, y: 0.75, r: 0.04 },
208 ],
209 dynamicObstacles: [],
210 sheepOverrides: [
211 { index: 0, leader: true },
212 { index: 1, leader: true },
213 { index: 2, leader: true },
214 { index: 3, leader: true },
215 ],
216 darkMode: false,
217 penShrinksEverySeconds: null,
218 dogSpeed: null,
219 sheepSpeedMult: null,
220 },
221 {
222 id: 8,
223 title: 'Race to Market',
224 subtitle: 'Industrial agriculture — volume, speed, efficiency above all else.',
225 sheepCount: 30,
226 penRect: { x: 0.38, y: 0.05, w: 0.24, h: 0.3 },
227 penGateSide: 'bottom',
228 timeLimitSeconds: 75,
229 winThreshold: 24,
230 spawnClusters: [
231 { cx: 0.2, cy: 0.65, r: 0.12, count: 15 },
232 { cx: 0.8, cy: 0.65, r: 0.12, count: 15 },
233 ],
234 obstacles: [],
235 dynamicObstacles: [],
236 sheepOverrides: [],
237 darkMode: false,
238 penShrinksEverySeconds: null,
239 dogSpeed: 200,
240 sheepSpeedMult: 1.2,
241 },
242 {
243 id: 9,
244 title: 'Lights Out',
245 subtitle: 'Information asymmetry — you can only see what is directly in front of you.',
246 sheepCount: 16,
247 penRect: { x: 0.38, y: 0.38, w: 0.24, h: 0.24 },
248 penGateSide: 'bottom',
249 timeLimitSeconds: 270,
250 winThreshold: 16,
251 spawnClusters: [
252 { cx: 0.1, cy: 0.1, r: 0.06, count: 2 },
253 { cx: 0.5, cy: 0.05, r: 0.06, count: 2 },
254 { cx: 0.9, cy: 0.1, r: 0.06, count: 2 },
255 { cx: 0.1, cy: 0.9, r: 0.06, count: 2 },
256 { cx: 0.5, cy: 0.9, r: 0.06, count: 2 },
257 { cx: 0.9, cy: 0.9, r: 0.06, count: 2 },
258 { cx: 0.1, cy: 0.5, r: 0.06, count: 1 },
259 { cx: 0.9, cy: 0.5, r: 0.06, count: 1 },
260 ],
261 obstacles: [
262 { x: 0.25, y: 0.5, r: 0.03 },
263 { x: 0.75, y: 0.5, r: 0.03 },
264 ],
265 dynamicObstacles: [],
266 sheepOverrides: [],
267 darkMode: true,
268 penShrinksEverySeconds: null,
269 dogSpeed: null,
270 sheepSpeedMult: null,
271 },
272 {
273 id: 10,
274 title: 'The Last Pasture',
275 subtitle: 'Climate crisis — the window closes slowly, then all at once.',
276 sheepCount: 25,
277 penRect: { x: 0.65, y: 0.05, w: 0.3, h: 0.35 },
278 penGateSide: 'bottom',
279 timeLimitSeconds: null,
280 winThreshold: 25,
281 spawnClusters: [
282 { cx: 0.2, cy: 0.45, r: 0.12, count: 13 },
283 { cx: 0.45, cy: 0.8, r: 0.12, count: 12 },
284 ],
285 obstacles: [
286 { x: 0.5, y: 0.3, r: 0.1 }, // dried-out lake blocking direct route
287 ],
288 dynamicObstacles: [],
289 sheepOverrides: [
290 { index: 0, stubborn: true },
291 { index: 1, stubborn: true },
292 { index: 2, stubborn: true },
293 { index: 23, leader: true },
294 { index: 24, leader: true },
295 ],
296 darkMode: false,
297 penShrinksEverySeconds: 25,
298 dogSpeed: null,
299 sheepSpeedMult: null,
300 },
301]