crimsonland 0.1.0.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. crimson/__init__.py +24 -0
  2. crimson/assets_fetch.py +60 -0
  3. crimson/atlas.py +92 -0
  4. crimson/audio_router.py +155 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +380 -0
  8. crimson/creatures/__init__.py +8 -0
  9. crimson/creatures/ai.py +186 -0
  10. crimson/creatures/anim.py +173 -0
  11. crimson/creatures/damage.py +103 -0
  12. crimson/creatures/runtime.py +1019 -0
  13. crimson/creatures/spawn.py +2871 -0
  14. crimson/debug.py +7 -0
  15. crimson/demo.py +1360 -0
  16. crimson/demo_trial.py +140 -0
  17. crimson/effects.py +1086 -0
  18. crimson/effects_atlas.py +73 -0
  19. crimson/frontend/__init__.py +1 -0
  20. crimson/frontend/assets.py +43 -0
  21. crimson/frontend/boot.py +424 -0
  22. crimson/frontend/menu.py +700 -0
  23. crimson/frontend/panels/__init__.py +1 -0
  24. crimson/frontend/panels/base.py +410 -0
  25. crimson/frontend/panels/controls.py +132 -0
  26. crimson/frontend/panels/mods.py +128 -0
  27. crimson/frontend/panels/options.py +409 -0
  28. crimson/frontend/panels/play_game.py +627 -0
  29. crimson/frontend/panels/stats.py +351 -0
  30. crimson/frontend/transitions.py +31 -0
  31. crimson/game.py +2533 -0
  32. crimson/game_modes.py +15 -0
  33. crimson/game_world.py +652 -0
  34. crimson/gameplay.py +2467 -0
  35. crimson/input_codes.py +176 -0
  36. crimson/modes/__init__.py +1 -0
  37. crimson/modes/base_gameplay_mode.py +219 -0
  38. crimson/modes/quest_mode.py +502 -0
  39. crimson/modes/rush_mode.py +300 -0
  40. crimson/modes/survival_mode.py +792 -0
  41. crimson/modes/tutorial_mode.py +648 -0
  42. crimson/modes/typo_mode.py +472 -0
  43. crimson/paths.py +23 -0
  44. crimson/perks.py +828 -0
  45. crimson/persistence/__init__.py +1 -0
  46. crimson/persistence/highscores.py +385 -0
  47. crimson/persistence/save_status.py +245 -0
  48. crimson/player_damage.py +77 -0
  49. crimson/projectiles.py +1133 -0
  50. crimson/quests/__init__.py +18 -0
  51. crimson/quests/helpers.py +147 -0
  52. crimson/quests/registry.py +49 -0
  53. crimson/quests/results.py +164 -0
  54. crimson/quests/runtime.py +91 -0
  55. crimson/quests/tier1.py +620 -0
  56. crimson/quests/tier2.py +652 -0
  57. crimson/quests/tier3.py +579 -0
  58. crimson/quests/tier4.py +721 -0
  59. crimson/quests/tier5.py +886 -0
  60. crimson/quests/timeline.py +115 -0
  61. crimson/quests/types.py +70 -0
  62. crimson/render/__init__.py +1 -0
  63. crimson/render/terrain_fx.py +88 -0
  64. crimson/render/world_renderer.py +1941 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +67 -0
  67. crimson/sim/world_state.py +422 -0
  68. crimson/terrain_assets.py +19 -0
  69. crimson/tutorial/__init__.py +12 -0
  70. crimson/tutorial/timeline.py +291 -0
  71. crimson/typo/__init__.py +2 -0
  72. crimson/typo/names.py +233 -0
  73. crimson/typo/player.py +43 -0
  74. crimson/typo/spawns.py +73 -0
  75. crimson/typo/typing.py +52 -0
  76. crimson/ui/__init__.py +3 -0
  77. crimson/ui/cursor.py +95 -0
  78. crimson/ui/demo_trial_overlay.py +235 -0
  79. crimson/ui/game_over.py +660 -0
  80. crimson/ui/hud.py +601 -0
  81. crimson/ui/perk_menu.py +388 -0
  82. crimson/views/__init__.py +40 -0
  83. crimson/views/aim_debug.py +276 -0
  84. crimson/views/animations.py +274 -0
  85. crimson/views/arsenal_debug.py +404 -0
  86. crimson/views/audio_bootstrap.py +47 -0
  87. crimson/views/bonuses.py +201 -0
  88. crimson/views/camera_debug.py +359 -0
  89. crimson/views/camera_shake.py +229 -0
  90. crimson/views/corpse_stamp_debug.py +324 -0
  91. crimson/views/decals_debug.py +739 -0
  92. crimson/views/empty.py +19 -0
  93. crimson/views/fonts.py +114 -0
  94. crimson/views/game_over.py +117 -0
  95. crimson/views/ground.py +259 -0
  96. crimson/views/lighting_debug.py +1166 -0
  97. crimson/views/particles.py +293 -0
  98. crimson/views/perk_menu_debug.py +430 -0
  99. crimson/views/perks.py +398 -0
  100. crimson/views/player.py +434 -0
  101. crimson/views/player_sprite_debug.py +314 -0
  102. crimson/views/projectile_fx.py +609 -0
  103. crimson/views/projectile_render_debug.py +393 -0
  104. crimson/views/projectiles.py +221 -0
  105. crimson/views/quest_title_overlay.py +108 -0
  106. crimson/views/registry.py +34 -0
  107. crimson/views/rush.py +16 -0
  108. crimson/views/small_font_debug.py +204 -0
  109. crimson/views/spawn_plan.py +363 -0
  110. crimson/views/sprites.py +214 -0
  111. crimson/views/survival.py +15 -0
  112. crimson/views/terrain.py +132 -0
  113. crimson/views/ui.py +123 -0
  114. crimson/views/wicons.py +166 -0
  115. crimson/weapon_sfx.py +63 -0
  116. crimson/weapons.py +860 -0
  117. crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
  118. crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
  119. crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
  120. crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
  121. grim/__init__.py +20 -0
  122. grim/app.py +92 -0
  123. grim/assets.py +231 -0
  124. grim/audio.py +106 -0
  125. grim/config.py +294 -0
  126. grim/console.py +737 -0
  127. grim/fonts/__init__.py +7 -0
  128. grim/fonts/grim_mono.py +111 -0
  129. grim/fonts/small.py +120 -0
  130. grim/input.py +44 -0
  131. grim/jaz.py +103 -0
  132. grim/math.py +17 -0
  133. grim/music.py +403 -0
  134. grim/paq.py +76 -0
  135. grim/rand.py +37 -0
  136. grim/sfx.py +276 -0
  137. grim/sfx_map.py +103 -0
  138. grim/terrain_render.py +840 -0
  139. grim/view.py +16 -0
@@ -0,0 +1,579 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from ..perks import PerkId
6
+ from ..creatures.spawn import SpawnId
7
+ from .helpers import (
8
+ center_point,
9
+ edge_midpoints,
10
+ line_points_x,
11
+ line_points_y,
12
+ radial_points,
13
+ random_angle,
14
+ ring_points,
15
+ spawn,
16
+ spawn_at,
17
+ )
18
+ from .registry import register_quest
19
+ from .types import QuestContext, SpawnEntry
20
+
21
+
22
+ @register_quest(
23
+ level="3.1",
24
+ title="The Blighting",
25
+ time_limit_ms=300000,
26
+ start_weapon_id=1,
27
+ unlock_perk_id=PerkId.TOXIC_AVENGER,
28
+ builder_address=0x00438050,
29
+ )
30
+ def build_3_1_the_blighting(ctx: QuestContext) -> list[SpawnEntry]:
31
+ edges = edge_midpoints(ctx.width)
32
+ edges_wide = edge_midpoints(ctx.width, offset=128.0)
33
+ entries = [
34
+ spawn_at(
35
+ edges_wide.right,
36
+ heading=0.0,
37
+ spawn_id=SpawnId.ALIEN_CONST_RED_FAST_2B,
38
+ trigger_ms=1500,
39
+ count=2,
40
+ ),
41
+ spawn_at(edges_wide.left, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_RED_FAST_2B, trigger_ms=1500, count=2),
42
+ spawn(x=896.0, y=128.0, heading=0.0, spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_1D_FAST_07, trigger_ms=2000, count=1),
43
+ spawn(x=128.0, y=128.0, heading=0.0, spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_1D_FAST_07, trigger_ms=2000, count=1),
44
+ spawn(x=128.0, y=896.0, heading=0.0, spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_1D_FAST_07, trigger_ms=2000, count=1),
45
+ spawn(x=896.0, y=896.0, heading=0.0, spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_1D_FAST_07, trigger_ms=2000, count=1),
46
+ ]
47
+
48
+ trigger = 4000
49
+ for wave in range(8):
50
+ if wave in (2, 4):
51
+ entries.append(
52
+ spawn_at(
53
+ edges_wide.left,
54
+ heading=0.0,
55
+ spawn_id=SpawnId.ALIEN_CONST_RED_FAST_2B,
56
+ trigger_ms=trigger,
57
+ count=4,
58
+ )
59
+ )
60
+ if wave in (3, 5):
61
+ entries.append(
62
+ spawn_at(
63
+ edges_wide.right,
64
+ heading=0.0,
65
+ spawn_id=SpawnId.ALIEN_CONST_RED_FAST_2B,
66
+ trigger_ms=trigger,
67
+ count=4,
68
+ )
69
+ )
70
+ spawn_id = SpawnId.AI1_ALIEN_BLUE_TINT_1A if wave % 2 == 0 else SpawnId.AI1_LIZARD_BLUE_TINT_1C
71
+ edge = wave % 5
72
+ if edge == 0:
73
+ entries.append(
74
+ spawn_at(
75
+ edges.right,
76
+ heading=0.0,
77
+ spawn_id=spawn_id,
78
+ trigger_ms=trigger,
79
+ count=12,
80
+ )
81
+ )
82
+ trigger += 15000
83
+ elif edge == 1:
84
+ entries.append(
85
+ spawn_at(
86
+ edges.left,
87
+ heading=0.0,
88
+ spawn_id=spawn_id,
89
+ trigger_ms=trigger,
90
+ count=12,
91
+ )
92
+ )
93
+ trigger += 15000
94
+ elif edge == 2:
95
+ entries.append(
96
+ spawn_at(
97
+ edges.bottom,
98
+ heading=0.0,
99
+ spawn_id=spawn_id,
100
+ trigger_ms=trigger,
101
+ count=12,
102
+ )
103
+ )
104
+ trigger += 15000
105
+ elif edge == 3:
106
+ entries.append(
107
+ spawn_at(
108
+ edges.top,
109
+ heading=0.0,
110
+ spawn_id=spawn_id,
111
+ trigger_ms=trigger,
112
+ count=12,
113
+ )
114
+ )
115
+ trigger += 15000
116
+ trigger += 1000
117
+ return entries
118
+
119
+
120
+ @register_quest(
121
+ level="3.2",
122
+ title="Lizard Kings",
123
+ time_limit_ms=180000,
124
+ start_weapon_id=1,
125
+ unlock_weapon_id=0x0A,
126
+ builder_address=0x00437710,
127
+ )
128
+ def build_3_2_lizard_kings(ctx: QuestContext) -> list[SpawnEntry]:
129
+ center_x, center_y = center_point(ctx.width, ctx.height)
130
+ entries = [
131
+ spawn(
132
+ x=1152.0,
133
+ y=512.0,
134
+ heading=0.0,
135
+ spawn_id=SpawnId.FORMATION_CHAIN_LIZARD_4_11,
136
+ trigger_ms=1500,
137
+ count=1,
138
+ ),
139
+ spawn(
140
+ x=-128.0,
141
+ y=512.0,
142
+ heading=0.0,
143
+ spawn_id=SpawnId.FORMATION_CHAIN_LIZARD_4_11,
144
+ trigger_ms=1500,
145
+ count=1,
146
+ ),
147
+ spawn(
148
+ x=1152.0,
149
+ y=896.0,
150
+ heading=0.0,
151
+ spawn_id=SpawnId.FORMATION_CHAIN_LIZARD_4_11,
152
+ trigger_ms=1500,
153
+ count=1,
154
+ ),
155
+ ]
156
+ trigger = 1500
157
+ for x, y, angle in ring_points(center_x, center_y, 256.0, 28, step=0.34906587):
158
+ entries.append(
159
+ spawn(
160
+ x=x,
161
+ y=y,
162
+ heading=-angle,
163
+ spawn_id=SpawnId.LIZARD_RANDOM_31,
164
+ trigger_ms=trigger,
165
+ count=1,
166
+ )
167
+ )
168
+ trigger += 900
169
+ return entries
170
+
171
+
172
+ @register_quest(
173
+ level="3.3",
174
+ title="The Killing",
175
+ time_limit_ms=300000,
176
+ start_weapon_id=1,
177
+ unlock_perk_id=PerkId.REGENERATION,
178
+ builder_address=0x004384A0,
179
+ )
180
+ def build_3_3_the_killing(ctx: QuestContext, rng: random.Random | None = None) -> list[SpawnEntry]:
181
+ rng = rng or random.Random()
182
+ edges = edge_midpoints(ctx.width)
183
+ entries: list[SpawnEntry] = []
184
+ trigger = 2000
185
+ for wave in range(10):
186
+ rng.randrange(0x8000)
187
+ rng.randrange(0x8000)
188
+ spawn_cycle = wave % 3
189
+ if spawn_cycle == 0:
190
+ spawn_id = SpawnId.AI1_ALIEN_BLUE_TINT_1A
191
+ elif spawn_cycle == 1:
192
+ spawn_id = SpawnId.AI1_SPIDER_SP1_BLUE_TINT_1B
193
+ else:
194
+ spawn_id = SpawnId.AI1_LIZARD_BLUE_TINT_1C
195
+
196
+ edge = wave % 5
197
+ if edge == 0:
198
+ entries.append(
199
+ spawn_at(
200
+ edges.right,
201
+ heading=0.0,
202
+ spawn_id=spawn_id,
203
+ trigger_ms=trigger,
204
+ count=12,
205
+ )
206
+ )
207
+ elif edge == 1:
208
+ entries.append(
209
+ spawn_at(
210
+ edges.left,
211
+ heading=0.0,
212
+ spawn_id=spawn_id,
213
+ trigger_ms=trigger,
214
+ count=12,
215
+ )
216
+ )
217
+ elif edge == 2:
218
+ entries.append(
219
+ spawn_at(
220
+ edges.bottom,
221
+ heading=0.0,
222
+ spawn_id=spawn_id,
223
+ trigger_ms=trigger,
224
+ count=12,
225
+ )
226
+ )
227
+ elif edge == 3:
228
+ entries.append(
229
+ spawn_at(
230
+ edges.top,
231
+ heading=0.0,
232
+ spawn_id=spawn_id,
233
+ trigger_ms=trigger,
234
+ count=12,
235
+ )
236
+ )
237
+ else:
238
+ for offset in (0, 1000, 2000):
239
+ x = rng.randrange(0x300) + 0x80
240
+ y = rng.randrange(0x300) + 0x80
241
+ entries.append(
242
+ spawn(
243
+ x=float(x),
244
+ y=float(y),
245
+ heading=0.0,
246
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_1D_FAST_07,
247
+ trigger_ms=trigger + offset,
248
+ count=3,
249
+ )
250
+ )
251
+
252
+ trigger += 6000
253
+ return entries
254
+
255
+
256
+ @register_quest(
257
+ level="3.4",
258
+ title="Hidden Evil",
259
+ time_limit_ms=300000,
260
+ start_weapon_id=1,
261
+ unlock_weapon_id=0x0D,
262
+ builder_address=0x00435A30,
263
+ )
264
+ def build_3_4_hidden_evil(ctx: QuestContext) -> list[SpawnEntry]:
265
+ edges = edge_midpoints(ctx.width, ctx.height)
266
+ return [
267
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_PURPLE_GHOST_21, trigger_ms=500, count=50),
268
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_GREEN_GHOST_22, trigger_ms=15000, count=30),
269
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_GREEN_GHOST_SMALL_23, trigger_ms=25000, count=20),
270
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_GREEN_GHOST_SMALL_23, trigger_ms=30000, count=30),
271
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.ALIEN_CONST_GREEN_GHOST_22, trigger_ms=35000, count=30),
272
+ ]
273
+
274
+
275
+ @register_quest(
276
+ level="3.5",
277
+ title="Surrounded By Reptiles",
278
+ time_limit_ms=300000,
279
+ start_weapon_id=1,
280
+ unlock_perk_id=PerkId.PYROMANIAC,
281
+ builder_address=0x00438940,
282
+ )
283
+ def build_3_5_surrounded_by_reptiles(ctx: QuestContext) -> list[SpawnEntry]:
284
+ entries: list[SpawnEntry] = []
285
+ trigger = 1000
286
+ for _x, y in line_points_y(256.0, 102.4, 5, 256.0):
287
+ entries.append(
288
+ spawn(
289
+ x=256.0,
290
+ y=y,
291
+ heading=0.0,
292
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_SLOW_0D,
293
+ trigger_ms=trigger,
294
+ count=1,
295
+ )
296
+ )
297
+ entries.append(
298
+ spawn(
299
+ x=768.0,
300
+ y=y,
301
+ heading=0.0,
302
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_SLOW_0D,
303
+ trigger_ms=trigger,
304
+ count=1,
305
+ )
306
+ )
307
+ trigger += 800
308
+
309
+ trigger = 8000
310
+ for x, _y in line_points_x(256.0, 102.4, 5, 256.0):
311
+ entries.append(
312
+ spawn(
313
+ x=x,
314
+ y=256.0,
315
+ heading=0.0,
316
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_SLOW_0D,
317
+ trigger_ms=trigger,
318
+ count=1,
319
+ )
320
+ )
321
+ entries.append(
322
+ spawn(
323
+ x=x,
324
+ y=768.0,
325
+ heading=0.0,
326
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_SLOW_0D,
327
+ trigger_ms=trigger,
328
+ count=1,
329
+ )
330
+ )
331
+ trigger += 800
332
+ return entries
333
+
334
+
335
+ @register_quest(
336
+ level="3.6",
337
+ title="The Lizquidation",
338
+ time_limit_ms=300000,
339
+ start_weapon_id=1,
340
+ unlock_weapon_id=0x0F,
341
+ builder_address=0x00437C70,
342
+ )
343
+ def build_3_6_the_lizquidation(ctx: QuestContext) -> list[SpawnEntry]:
344
+ entries: list[SpawnEntry] = []
345
+ edges = edge_midpoints(ctx.width)
346
+ trigger = 1500
347
+ for wave in range(10):
348
+ count = wave + 6
349
+ entries.append(
350
+ spawn_at(
351
+ edges.right,
352
+ heading=0.0,
353
+ spawn_id=SpawnId.LIZARD_RANDOM_2E,
354
+ trigger_ms=trigger,
355
+ count=count,
356
+ )
357
+ )
358
+ entries.append(
359
+ spawn_at(
360
+ edges.left,
361
+ heading=0.0,
362
+ spawn_id=SpawnId.LIZARD_RANDOM_2E,
363
+ trigger_ms=trigger,
364
+ count=count,
365
+ )
366
+ )
367
+ if wave == 4:
368
+ entries.append(
369
+ spawn(
370
+ x=ctx.width + 128.0,
371
+ y=edges.right[1],
372
+ heading=0.0,
373
+ spawn_id=SpawnId.ALIEN_CONST_RED_FAST_2B,
374
+ trigger_ms=1500,
375
+ count=2,
376
+ )
377
+ )
378
+ trigger += 8000
379
+ return entries
380
+
381
+
382
+ @register_quest(
383
+ level="3.7",
384
+ title="Spiders Inc.",
385
+ time_limit_ms=300000,
386
+ start_weapon_id=11,
387
+ unlock_perk_id=PerkId.NINJA,
388
+ builder_address=0x004390D0,
389
+ )
390
+ def build_3_7_spiders_inc(ctx: QuestContext) -> list[SpawnEntry]:
391
+ edges = edge_midpoints(ctx.width)
392
+ center_x, _center_y = center_point(ctx.width, ctx.height)
393
+ entries = [
394
+ spawn_at(edges.bottom, heading=0.0, spawn_id=SpawnId.SPIDER_SP1_AI7_TIMER_38, trigger_ms=500, count=1),
395
+ spawn(
396
+ x=center_x + 64.0,
397
+ y=edges.bottom[1],
398
+ heading=0.0,
399
+ spawn_id=SpawnId.SPIDER_SP1_AI7_TIMER_38,
400
+ trigger_ms=500,
401
+ count=1,
402
+ ),
403
+ spawn_at(edges.top, heading=0.0, spawn_id=SpawnId.SPIDER_SP1_CONST_BLUE_40, trigger_ms=500, count=4),
404
+ ]
405
+
406
+ trigger = 17000
407
+ step_count = 0
408
+ while trigger < 107000:
409
+ count = step_count // 2 + 3
410
+ entries.append(
411
+ spawn_at(
412
+ edges.bottom,
413
+ heading=0.0,
414
+ spawn_id=SpawnId.SPIDER_SP1_AI7_TIMER_38,
415
+ trigger_ms=trigger,
416
+ count=count,
417
+ )
418
+ )
419
+ entries.append(
420
+ spawn_at(
421
+ edges.top,
422
+ heading=0.0,
423
+ spawn_id=SpawnId.SPIDER_SP1_AI7_TIMER_38,
424
+ trigger_ms=trigger,
425
+ count=count,
426
+ )
427
+ )
428
+ trigger += 6000
429
+ step_count += 1
430
+ return entries
431
+
432
+
433
+ @register_quest(
434
+ level="3.8",
435
+ title="Lizard Raze",
436
+ time_limit_ms=300000,
437
+ start_weapon_id=1,
438
+ unlock_weapon_id=0x12,
439
+ builder_address=0x00438840,
440
+ )
441
+ def build_3_8_lizard_raze(ctx: QuestContext) -> list[SpawnEntry]:
442
+ entries: list[SpawnEntry] = []
443
+ edges = edge_midpoints(ctx.width)
444
+ trigger = 1500
445
+ while trigger < 91500:
446
+ entries.append(
447
+ spawn_at(
448
+ edges.right,
449
+ heading=0.0,
450
+ spawn_id=SpawnId.LIZARD_RANDOM_2E,
451
+ trigger_ms=trigger,
452
+ count=6,
453
+ )
454
+ )
455
+ entries.append(
456
+ spawn_at(
457
+ edges.left,
458
+ heading=0.0,
459
+ spawn_id=SpawnId.LIZARD_RANDOM_2E,
460
+ trigger_ms=trigger,
461
+ count=6,
462
+ )
463
+ )
464
+ trigger += 6000
465
+ entries.extend(
466
+ [
467
+ spawn(
468
+ x=128.0,
469
+ y=256.0,
470
+ heading=0.0,
471
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_FAST_0C,
472
+ trigger_ms=10000,
473
+ count=1,
474
+ ),
475
+ spawn(
476
+ x=128.0,
477
+ y=384.0,
478
+ heading=0.0,
479
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_FAST_0C,
480
+ trigger_ms=10000,
481
+ count=1,
482
+ ),
483
+ spawn(
484
+ x=128.0,
485
+ y=512.0,
486
+ heading=0.0,
487
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_FAST_0C,
488
+ trigger_ms=10000,
489
+ count=1,
490
+ ),
491
+ ]
492
+ )
493
+ return entries
494
+
495
+
496
+ @register_quest(
497
+ level="3.9",
498
+ title="Deja vu",
499
+ time_limit_ms=120000,
500
+ start_weapon_id=6,
501
+ unlock_perk_id=PerkId.HIGHLANDER,
502
+ builder_address=0x00437920,
503
+ )
504
+ def build_3_9_deja_vu(ctx: QuestContext, rng: random.Random | None = None) -> list[SpawnEntry]:
505
+ rng = rng or random.Random()
506
+ entries: list[SpawnEntry] = []
507
+ center_x, center_y = center_point(ctx.width, ctx.height)
508
+ trigger = 2000
509
+ step = 2000
510
+ while step > 560:
511
+ angle = random_angle(rng)
512
+ for x, y in radial_points(center_x, center_y, angle, 0x54, 0xFC, 0x2A):
513
+ entries.append(
514
+ spawn(
515
+ x=x,
516
+ y=y,
517
+ heading=0.0,
518
+ spawn_id=SpawnId.ALIEN_SPAWNER_CHILD_31_SLOW_0D,
519
+ trigger_ms=trigger,
520
+ count=1,
521
+ )
522
+ )
523
+ trigger += step
524
+ step -= 0x50
525
+ return entries
526
+
527
+
528
+ @register_quest(
529
+ level="3.10",
530
+ title="Zombie Masters",
531
+ time_limit_ms=300000,
532
+ start_weapon_id=1,
533
+ unlock_weapon_id=0x14,
534
+ builder_address=0x004360A0,
535
+ )
536
+ def build_3_10_zombie_masters(ctx: QuestContext) -> list[SpawnEntry]:
537
+ return [
538
+ spawn(
539
+ x=256.0,
540
+ y=256.0,
541
+ heading=0.0,
542
+ spawn_id=SpawnId.ZOMBIE_BOSS_SPAWNER_00,
543
+ trigger_ms=1000,
544
+ count=ctx.player_count,
545
+ ),
546
+ spawn(x=512.0, y=256.0, heading=0.0, spawn_id=SpawnId.ZOMBIE_BOSS_SPAWNER_00, trigger_ms=6000, count=1),
547
+ spawn(
548
+ x=768.0,
549
+ y=256.0,
550
+ heading=0.0,
551
+ spawn_id=SpawnId.ZOMBIE_BOSS_SPAWNER_00,
552
+ trigger_ms=14000,
553
+ count=ctx.player_count,
554
+ ),
555
+ spawn(
556
+ x=768.0,
557
+ y=768.0,
558
+ heading=0.0,
559
+ spawn_id=SpawnId.ZOMBIE_BOSS_SPAWNER_00,
560
+ trigger_ms=18000,
561
+ count=1,
562
+ ),
563
+ ]
564
+
565
+
566
+ __all__ = [
567
+ "QuestContext",
568
+ "SpawnEntry",
569
+ "build_3_1_the_blighting",
570
+ "build_3_2_lizard_kings",
571
+ "build_3_3_the_killing",
572
+ "build_3_4_hidden_evil",
573
+ "build_3_5_surrounded_by_reptiles",
574
+ "build_3_6_the_lizquidation",
575
+ "build_3_7_spiders_inc",
576
+ "build_3_8_lizard_raze",
577
+ "build_3_9_deja_vu",
578
+ "build_3_10_zombie_masters",
579
+ ]