crimsonland 0.1.0.dev2__tar.gz → 0.1.0.dev7__tar.gz

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 (137) hide show
  1. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/audio_router.py +15 -3
  4. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/demo.py +2 -2
  5. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/effects.py +1 -1
  6. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/game_world.py +3 -14
  7. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/gameplay.py +105 -55
  8. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/projectiles.py +318 -61
  9. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/render/world_renderer.py +789 -46
  10. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/sim/world_defs.py +16 -5
  11. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/sim/world_state.py +1 -0
  12. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/arsenal_debug.py +11 -0
  13. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/player.py +1 -0
  14. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/projectile_fx.py +1 -0
  15. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/projectile_render_debug.py +9 -2
  16. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/__init__.py +0 -0
  17. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/assets_fetch.py +0 -0
  18. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/atlas.py +0 -0
  19. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/bonuses.py +0 -0
  20. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/camera.py +0 -0
  21. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/cli.py +0 -0
  22. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/__init__.py +0 -0
  23. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/ai.py +0 -0
  24. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/anim.py +0 -0
  25. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/damage.py +0 -0
  26. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/runtime.py +0 -0
  27. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/creatures/spawn.py +0 -0
  28. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/debug.py +0 -0
  29. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/demo_trial.py +0 -0
  30. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/effects_atlas.py +0 -0
  31. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/__init__.py +0 -0
  32. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/assets.py +0 -0
  33. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/boot.py +0 -0
  34. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/menu.py +0 -0
  35. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/__init__.py +0 -0
  36. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/base.py +0 -0
  37. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/controls.py +0 -0
  38. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/mods.py +0 -0
  39. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/options.py +0 -0
  40. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/play_game.py +0 -0
  41. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/panels/stats.py +0 -0
  42. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/frontend/transitions.py +0 -0
  43. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/game.py +0 -0
  44. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/game_modes.py +0 -0
  45. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/input_codes.py +0 -0
  46. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/__init__.py +0 -0
  47. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/base_gameplay_mode.py +0 -0
  48. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/quest_mode.py +0 -0
  49. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/rush_mode.py +0 -0
  50. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/survival_mode.py +0 -0
  51. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/tutorial_mode.py +0 -0
  52. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/modes/typo_mode.py +0 -0
  53. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/paths.py +0 -0
  54. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/perks.py +0 -0
  55. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/persistence/__init__.py +0 -0
  56. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/persistence/highscores.py +0 -0
  57. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/persistence/save_status.py +0 -0
  58. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/player_damage.py +0 -0
  59. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/__init__.py +0 -0
  60. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/helpers.py +0 -0
  61. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/registry.py +0 -0
  62. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/results.py +0 -0
  63. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/runtime.py +0 -0
  64. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/tier1.py +0 -0
  65. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/tier2.py +0 -0
  66. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/tier3.py +0 -0
  67. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/tier4.py +0 -0
  68. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/tier5.py +0 -0
  69. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/timeline.py +0 -0
  70. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/quests/types.py +0 -0
  71. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/render/__init__.py +0 -0
  72. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/render/terrain_fx.py +0 -0
  73. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/sim/__init__.py +0 -0
  74. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/terrain_assets.py +0 -0
  75. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/tutorial/__init__.py +0 -0
  76. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/tutorial/timeline.py +0 -0
  77. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/typo/__init__.py +0 -0
  78. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/typo/names.py +0 -0
  79. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/typo/player.py +0 -0
  80. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/typo/spawns.py +0 -0
  81. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/typo/typing.py +0 -0
  82. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/__init__.py +0 -0
  83. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/cursor.py +0 -0
  84. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/demo_trial_overlay.py +0 -0
  85. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/game_over.py +0 -0
  86. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/hud.py +0 -0
  87. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/ui/perk_menu.py +0 -0
  88. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/__init__.py +0 -0
  89. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/aim_debug.py +0 -0
  90. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/animations.py +0 -0
  91. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/audio_bootstrap.py +0 -0
  92. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/bonuses.py +0 -0
  93. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/camera_debug.py +0 -0
  94. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/camera_shake.py +0 -0
  95. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/corpse_stamp_debug.py +0 -0
  96. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/decals_debug.py +0 -0
  97. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/empty.py +0 -0
  98. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/fonts.py +0 -0
  99. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/game_over.py +0 -0
  100. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/ground.py +0 -0
  101. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/lighting_debug.py +0 -0
  102. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/particles.py +0 -0
  103. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/perk_menu_debug.py +0 -0
  104. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/perks.py +0 -0
  105. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/player_sprite_debug.py +0 -0
  106. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/projectiles.py +0 -0
  107. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/quest_title_overlay.py +0 -0
  108. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/registry.py +0 -0
  109. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/rush.py +0 -0
  110. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/small_font_debug.py +0 -0
  111. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/spawn_plan.py +0 -0
  112. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/sprites.py +0 -0
  113. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/survival.py +0 -0
  114. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/terrain.py +0 -0
  115. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/ui.py +0 -0
  116. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/views/wicons.py +0 -0
  117. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/weapon_sfx.py +0 -0
  118. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/crimson/weapons.py +0 -0
  119. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/__init__.py +0 -0
  120. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/app.py +0 -0
  121. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/assets.py +0 -0
  122. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/audio.py +0 -0
  123. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/config.py +0 -0
  124. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/console.py +0 -0
  125. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/fonts/__init__.py +0 -0
  126. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/fonts/grim_mono.py +0 -0
  127. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/fonts/small.py +0 -0
  128. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/input.py +0 -0
  129. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/jaz.py +0 -0
  130. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/math.py +0 -0
  131. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/music.py +0 -0
  132. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/paq.py +0 -0
  133. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/rand.py +0 -0
  134. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/sfx.py +0 -0
  135. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/sfx_map.py +0 -0
  136. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/terrain_render.py +0 -0
  137. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev7}/src/grim/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crimsonland
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev7
4
4
  Requires-Dist: construct>=2.10.70
5
5
  Requires-Dist: pillow>=12.1.0
6
6
  Requires-Dist: platformdirs>=4.5.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crimsonland"
3
- version = "0.1.0.dev2"
3
+ version = "0.1.0.dev7"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -9,7 +9,7 @@ from grim.audio import AudioState, play_sfx, trigger_game_tune
9
9
  from .creatures.spawn import CreatureTypeId
10
10
  from .game_modes import GameMode
11
11
  from .weapon_sfx import resolve_weapon_sfx_ref
12
- from .weapons import WEAPON_BY_ID
12
+ from .weapons import WEAPON_BY_ID, WeaponId
13
13
 
14
14
  _MAX_HIT_SFX_PER_FRAME = 4
15
15
  _MAX_DEATH_SFX_PER_FRAME = 3
@@ -96,7 +96,17 @@ class AudioRouter:
96
96
  return
97
97
 
98
98
  if int(getattr(player, "shot_seq", 0)) > int(prev_shot_seq):
99
- self.play_sfx(resolve_weapon_sfx_ref(weapon.fire_sound))
99
+ if float(getattr(player, "fire_bullets_timer", 0.0)) > 0.0:
100
+ # player_update (crimsonland.exe): when Fire Bullets is active, the regular per-weapon
101
+ # shot sfx is suppressed and replaced by Fire Bullets + Plasma Minigun fire sfx.
102
+ fire_bullets = WEAPON_BY_ID.get(int(WeaponId.FIRE_BULLETS))
103
+ plasma_minigun = WEAPON_BY_ID.get(int(WeaponId.PLASMA_MINIGUN))
104
+ if fire_bullets is not None:
105
+ self.play_sfx(resolve_weapon_sfx_ref(fire_bullets.fire_sound))
106
+ if plasma_minigun is not None:
107
+ self.play_sfx(resolve_weapon_sfx_ref(plasma_minigun.fire_sound))
108
+ else:
109
+ self.play_sfx(resolve_weapon_sfx_ref(weapon.fire_sound))
100
110
 
101
111
  reload_active = bool(getattr(player, "reload_active", False))
102
112
  reload_timer = float(getattr(player, "reload_timer", 0.0))
@@ -111,7 +121,9 @@ class AudioRouter:
111
121
  beam_types: frozenset[int],
112
122
  rand: Callable[[], int],
113
123
  ) -> str | None:
114
- if type_id in beam_types:
124
+ weapon = WEAPON_BY_ID.get(int(type_id))
125
+ ammo_class = weapon.ammo_class if weapon is not None else None
126
+ if ammo_class == 4:
115
127
  return "sfx_shock_hit_01"
116
128
  return self._rand_choice(rand, _BULLET_HIT_SFX)
117
129
 
@@ -690,8 +690,8 @@ class DemoView:
690
690
  rl.draw_circle(int(sx), int(sy), radius, rl.Color(200, 120, 255, 255))
691
691
  continue
692
692
  if proj.type_id == 3:
693
- t = _clamp(proj.lifetime, 0.0, 1.0)
694
- radius = proj.speed * t * 80.0
693
+ t = _clamp(proj.vel_x, 0.0, 1.0)
694
+ radius = proj.vel_y * t * 80.0
695
695
  alpha = int((1.0 - t) * 180.0)
696
696
  color = rl.Color(200, 120, 255, alpha)
697
697
  rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
@@ -261,7 +261,7 @@ class ParticlePool:
261
261
  entry.age = alpha
262
262
  entry.scale_x = shade
263
263
  entry.scale_y = shade
264
- entry.scale_z = shade
264
+ # Native only updates scale_x/scale_y; scale_z stays at its spawn value (1.0).
265
265
 
266
266
  alive = entry.intensity > (0.0 if style == 0 else 0.8)
267
267
  if not alive:
@@ -31,7 +31,7 @@ from .render.world_renderer import WorldRenderer
31
31
  from .audio_router import AudioRouter
32
32
  from .perks import PerkId
33
33
  from .projectiles import ProjectileTypeId
34
- from .sim.world_defs import BEAM_TYPES, CREATURE_ASSET
34
+ from .sim.world_defs import BEAM_TYPES, CREATURE_ASSET, ION_TYPES
35
35
  from .sim.world_state import ProjectileHit, WorldState
36
36
  from .weapons import WEAPON_TABLE
37
37
  from .game_modes import GameMode
@@ -510,19 +510,8 @@ class GameWorld:
510
510
  pos_y=float(target_y) + dir_y * dist * 20.0,
511
511
  rand=rand,
512
512
  )
513
- elif type_id in BEAM_TYPES:
514
- if self.ground is not None and self.fx_textures is not None:
515
- size = float(int(rand()) % 18 + 18)
516
- rotation = float(int(rand()) % 628) * 0.01
517
- self.fx_queue.add(
518
- effect_id=0x01,
519
- pos_x=float(hit_x),
520
- pos_y=float(hit_y),
521
- width=size,
522
- height=size,
523
- rotation=rotation,
524
- rgba=(0.7, 0.9, 1.0, 1.0),
525
- )
513
+ elif type_id in ION_TYPES:
514
+ pass
526
515
  elif not freeze_active:
527
516
  for _ in range(3):
528
517
  spread = float(int(rand()) % 0x14 - 10) * 0.1
@@ -64,8 +64,8 @@ class PlayerState:
64
64
  health: float = 100.0
65
65
  size: float = 50.0
66
66
 
67
- move_speed_multiplier: float = 2.0
68
- long_distance_runner_timer: float = 0.0
67
+ speed_multiplier: float = 2.0
68
+ move_speed: float = 0.0
69
69
  move_phase: float = 0.0
70
70
  heading: float = 0.0
71
71
  death_timer: float = 16.0
@@ -358,11 +358,49 @@ class BonusPool:
358
358
  return None
359
359
 
360
360
  rng = state.rng
361
- if rng.rand() % 9 != 1:
362
- if not any(perk_active(player, PerkId.BONUS_MAGNET) for player in players):
363
- return None
364
- if rng.rand() % 10 != 2:
365
- return None
361
+ # Native special-case: while any player has Pistol, 3/4 chance to force a Weapon drop.
362
+ if players and any(int(player.weapon_id) == int(WeaponId.PISTOL) for player in players):
363
+ if (int(rng.rand()) & 3) < 3:
364
+ entry = self.spawn_at_pos(
365
+ pos_x,
366
+ pos_y,
367
+ state=state,
368
+ players=players,
369
+ world_width=world_width,
370
+ world_height=world_height,
371
+ )
372
+ if entry is None:
373
+ return None
374
+
375
+ entry.bonus_id = int(BonusId.WEAPON)
376
+ weapon_id = int(weapon_pick_random_available(state))
377
+ entry.amount = int(weapon_id)
378
+ if weapon_id == int(WeaponId.PISTOL):
379
+ weapon_id = int(weapon_pick_random_available(state))
380
+ entry.amount = int(weapon_id)
381
+
382
+ matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
383
+ if matches > 1:
384
+ self._clear_entry(entry)
385
+ return None
386
+
387
+ if entry.amount == int(WeaponId.PISTOL) or (players and perk_active(players[0], PerkId.MY_FAVOURITE_WEAPON)):
388
+ self._clear_entry(entry)
389
+ return None
390
+
391
+ return entry
392
+
393
+ base_roll = int(rng.rand())
394
+ if base_roll % 9 != 1:
395
+ allow_without_magnet = False
396
+ if players and int(players[0].weapon_id) == int(WeaponId.PISTOL):
397
+ allow_without_magnet = int(rng.rand()) % 5 == 1
398
+
399
+ if not allow_without_magnet:
400
+ if not (players and perk_active(players[0], PerkId.BONUS_MAGNET)):
401
+ return None
402
+ if int(rng.rand()) % 10 != 2:
403
+ return None
366
404
 
367
405
  entry = self.spawn_at_pos(
368
406
  pos_x,
@@ -377,11 +415,9 @@ class BonusPool:
377
415
 
378
416
  if entry.bonus_id == int(BonusId.WEAPON):
379
417
  near_sq = BONUS_WEAPON_NEAR_RADIUS * BONUS_WEAPON_NEAR_RADIUS
380
- for player in players:
381
- if _distance_sq(pos_x, pos_y, player.pos_x, player.pos_y) < near_sq:
382
- entry.bonus_id = int(BonusId.POINTS)
383
- entry.amount = 100
384
- break
418
+ if players and _distance_sq(pos_x, pos_y, players[0].pos_x, players[0].pos_y) < near_sq:
419
+ entry.bonus_id = int(BonusId.POINTS)
420
+ entry.amount = 100
385
421
 
386
422
  if entry.bonus_id != int(BonusId.POINTS):
387
423
  matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
@@ -390,10 +426,9 @@ class BonusPool:
390
426
  return None
391
427
 
392
428
  if entry.bonus_id == int(BonusId.WEAPON):
393
- for player in players:
394
- if entry.amount == player.weapon_id:
395
- self._clear_entry(entry)
396
- return None
429
+ if players and entry.amount == players[0].weapon_id:
430
+ self._clear_entry(entry)
431
+ return None
397
432
 
398
433
  return entry
399
434
 
@@ -1592,6 +1627,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1592
1627
 
1593
1628
  firing_during_reload = False
1594
1629
  ammo_cost = 1.0
1630
+ is_fire_bullets = float(player.fire_bullets_timer) > 0.0
1595
1631
  if player.reload_timer > 0.0:
1596
1632
  if player.ammo <= 0 and player.experience > 0:
1597
1633
  if perk_active(player, PerkId.REGRESSION_BULLETS):
@@ -1616,7 +1652,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1616
1652
  else:
1617
1653
  return
1618
1654
 
1619
- if player.ammo <= 0 and not firing_during_reload:
1655
+ if player.ammo <= 0 and not firing_during_reload and not is_fire_bullets:
1620
1656
  player_start_reload(player, state)
1621
1657
  return
1622
1658
 
@@ -1624,20 +1660,18 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1624
1660
  fire_bullets_weapon = weapon_entry_for_projectile_type_id(int(ProjectileTypeId.FIRE_BULLETS))
1625
1661
 
1626
1662
  shot_cooldown = float(weapon.shot_cooldown) if weapon.shot_cooldown is not None else 0.0
1627
- spread_inc = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1628
- spread_inc *= 1.3
1629
- if player.fire_bullets_timer > 0.0 and pellet_count == 1 and fire_bullets_weapon is not None:
1663
+ spread_heat_base = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1664
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None and fire_bullets_weapon.spread_heat_inc is not None:
1665
+ spread_heat_base = float(fire_bullets_weapon.spread_heat_inc)
1666
+
1667
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None:
1630
1668
  shot_cooldown = (
1631
1669
  float(fire_bullets_weapon.shot_cooldown)
1632
1670
  if fire_bullets_weapon.shot_cooldown is not None
1633
1671
  else 0.0
1634
1672
  )
1635
- spread_inc = (
1636
- float(fire_bullets_weapon.spread_heat_inc)
1637
- if fire_bullets_weapon.spread_heat_inc is not None
1638
- else 0.0
1639
- )
1640
- spread_inc *= 1.3
1673
+
1674
+ spread_inc = spread_heat_base * 1.3
1641
1675
 
1642
1676
  if perk_active(player, PerkId.FASTSHOT):
1643
1677
  shot_cooldown *= 0.88
@@ -1657,6 +1691,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1657
1691
  aim_jitter_x = aim_x + math.cos(dir_angle) * offset
1658
1692
  aim_jitter_y = aim_y + math.sin(dir_angle) * offset
1659
1693
  shot_angle = math.atan2(aim_jitter_y - float(player.pos_y), aim_jitter_x - float(player.pos_x)) + math.pi / 2.0
1694
+ particle_angle = shot_angle - math.pi / 2.0
1660
1695
 
1661
1696
  muzzle_x = player.pos_x + player.aim_dir_x * 16.0
1662
1697
  muzzle_y = player.pos_y + player.aim_dir_y * 16.0
@@ -1676,14 +1711,12 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1676
1711
  return 0.0013
1677
1712
  return 0.0015
1678
1713
 
1679
- if player.fire_bullets_timer > 0.0:
1714
+ if is_fire_bullets:
1680
1715
  pellets = max(1, int(pellet_count))
1681
1716
  shot_count = pellets
1682
1717
  meta = _projectile_meta_for_type_id(ProjectileTypeId.FIRE_BULLETS)
1683
1718
  for _ in range(pellets):
1684
- angle = shot_angle
1685
- if pellets > 1:
1686
- angle += float(int(state.rng.rand()) % 200 - 100) * 0.0015
1719
+ angle = shot_angle + float(int(state.rng.rand()) % 200 - 100) * 0.0015
1687
1720
  state.projectiles.spawn(
1688
1721
  pos_x=muzzle_x,
1689
1722
  pos_y=muzzle_y,
@@ -1713,16 +1746,16 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1713
1746
  state.secondary_projectiles.spawn(pos_x=muzzle_x, pos_y=muzzle_y, angle=shot_angle, type_id=4, owner_id=owner_id)
1714
1747
  elif weapon_id == WeaponId.FLAMETHROWER:
1715
1748
  # Flamethrower -> fast particle weapon (style 0), fractional ammo drain.
1716
- state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1749
+ state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1717
1750
  ammo_cost = 0.1
1718
1751
  elif weapon_id == WeaponId.BLOW_TORCH:
1719
1752
  # Blow Torch -> fast particle weapon (style 1), fractional ammo drain.
1720
- particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1753
+ particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1721
1754
  state.particles.entries[particle_id].style_id = 1
1722
1755
  ammo_cost = 0.05
1723
1756
  elif weapon_id == WeaponId.HR_FLAMER:
1724
1757
  # HR Flamer -> fast particle weapon (style 2), fractional ammo drain.
1725
- particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=dir_angle, intensity=1.0, owner_id=owner_id)
1758
+ particle_id = state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1726
1759
  state.particles.entries[particle_id].style_id = 2
1727
1760
  ammo_cost = 0.1
1728
1761
  elif weapon_id == WeaponId.BUBBLEGUN:
@@ -1833,15 +1866,17 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1833
1866
  player.spread_heat = min(0.48, max(0.0, player.spread_heat + spread_inc))
1834
1867
 
1835
1868
  muzzle_inc = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1869
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None and fire_bullets_weapon.spread_heat_inc is not None:
1870
+ muzzle_inc = float(fire_bullets_weapon.spread_heat_inc)
1836
1871
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha)
1837
1872
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha + muzzle_inc)
1838
1873
  player.muzzle_flash_alpha = min(0.8, player.muzzle_flash_alpha)
1839
1874
 
1840
1875
  player.shot_seq += 1
1841
- if not firing_during_reload and state.bonuses.reflex_boost <= 0.0:
1876
+ if (not firing_during_reload) and state.bonuses.reflex_boost <= 0.0 and not is_fire_bullets:
1842
1877
  player.ammo = max(0.0, float(player.ammo) - float(ammo_cost))
1843
- if player.ammo <= 0.0:
1844
- player_start_reload(player, state)
1878
+ if (not firing_during_reload) and player.ammo <= 0.0 and player.reload_timer <= 0.0:
1879
+ player_start_reload(player, state)
1845
1880
 
1846
1881
 
1847
1882
  def player_update(player: PlayerState, input_state: PlayerInput, dt: float, state: GameplayState, *, world_size: float = 1024.0) -> None:
@@ -1885,35 +1920,50 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
1885
1920
  player.aim_heading = math.atan2(aim_dir_y, aim_dir_x) + math.pi / 2.0
1886
1921
 
1887
1922
  # Movement.
1888
- move_x, move_y = _normalize(float(input_state.move_x), float(input_state.move_y))
1889
- moving = move_x != 0.0 or move_y != 0.0
1890
- runner_bonus = 0.0
1891
- if perk_active(player, PerkId.LONG_DISTANCE_RUNNER):
1892
- if moving:
1893
- player.long_distance_runner_timer = min(1.2, float(player.long_distance_runner_timer) + dt)
1923
+ raw_move_x = float(input_state.move_x)
1924
+ raw_move_y = float(input_state.move_y)
1925
+ raw_mag = math.hypot(raw_move_x, raw_move_y)
1926
+ moving_input = raw_mag > 0.2
1927
+
1928
+ if moving_input:
1929
+ inv = 1.0 / raw_mag if raw_mag > 1e-9 else 0.0
1930
+ move_x = raw_move_x * inv
1931
+ move_y = raw_move_y * inv
1932
+ player.heading = math.atan2(move_y, move_x) + math.pi / 2.0
1933
+ if perk_active(player, PerkId.LONG_DISTANCE_RUNNER):
1934
+ if player.move_speed < 2.0:
1935
+ player.move_speed = float(player.move_speed + dt * 4.0)
1936
+ player.move_speed = float(player.move_speed + dt)
1937
+ if player.move_speed > 2.8:
1938
+ player.move_speed = 2.8
1894
1939
  else:
1895
- player.long_distance_runner_timer = max(0.0, float(player.long_distance_runner_timer) - dt * 15.0)
1896
- runner_bonus = max(0.0, min(0.8, float(player.long_distance_runner_timer) - 0.4))
1940
+ player.move_speed = float(player.move_speed + dt * 5.0)
1941
+ if player.move_speed > 2.0:
1942
+ player.move_speed = 2.0
1897
1943
  else:
1898
- player.long_distance_runner_timer = 0.0
1944
+ player.move_speed = float(player.move_speed - dt * 15.0)
1945
+ if player.move_speed < 0.0:
1946
+ player.move_speed = 0.0
1947
+ move_x = math.cos(player.heading - math.pi / 2.0)
1948
+ move_y = math.sin(player.heading - math.pi / 2.0)
1949
+
1950
+ if player.weapon_id == WeaponId.MEAN_MINIGUN and player.move_speed > 0.8:
1951
+ player.move_speed = 0.8
1899
1952
 
1900
- speed_multiplier = float(player.move_speed_multiplier + runner_bonus)
1953
+ speed_multiplier = float(player.speed_multiplier)
1901
1954
  if player.speed_bonus_timer > 0.0:
1902
1955
  speed_multiplier += 1.0
1903
- speed = 120.0 * speed_multiplier
1956
+
1957
+ speed = player.move_speed * speed_multiplier * 25.0
1958
+ if moving_input:
1959
+ speed *= min(1.0, raw_mag)
1904
1960
  if perk_active(player, PerkId.ALTERNATE_WEAPON):
1905
1961
  speed *= 0.8
1962
+
1906
1963
  player.pos_x = _clamp(player.pos_x + move_x * speed * dt, 0.0, float(world_size))
1907
1964
  player.pos_y = _clamp(player.pos_y + move_y * speed * dt, 0.0, float(world_size))
1908
1965
 
1909
- if moving:
1910
- player.heading = math.atan2(move_y, move_x) + math.pi / 2.0
1911
-
1912
- move_dist = math.hypot(player.pos_x - prev_x, player.pos_y - prev_y)
1913
- if move_dist > 1e-9:
1914
- # Port of `move_phase += frame_dt * move_speed * 19.0` (player_update).
1915
- move_speed = move_dist / dt / 120.0
1916
- player.move_phase += dt * move_speed * 19.0
1966
+ player.move_phase += dt * player.move_speed * 19.0
1917
1967
 
1918
1968
  stationary = abs(player.pos_x - prev_x) <= 1e-9 and abs(player.pos_y - prev_y) <= 1e-9
1919
1969
  reload_scale = 1.0