crimsonland 0.1.0.dev3__tar.gz → 0.1.0.dev5__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.dev3 → crimsonland-0.1.0.dev5}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/game_world.py +3 -14
  4. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/gameplay.py +55 -38
  5. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/projectiles.py +93 -0
  6. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/render/world_renderer.py +420 -154
  7. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/sim/world_state.py +1 -0
  8. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/arsenal_debug.py +11 -0
  9. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/player.py +1 -0
  10. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/projectile_fx.py +1 -0
  11. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/projectile_render_debug.py +9 -2
  12. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/__init__.py +0 -0
  13. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/assets_fetch.py +0 -0
  14. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/atlas.py +0 -0
  15. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/audio_router.py +0 -0
  16. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/bonuses.py +0 -0
  17. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/camera.py +0 -0
  18. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/cli.py +0 -0
  19. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/__init__.py +0 -0
  20. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/ai.py +0 -0
  21. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/anim.py +0 -0
  22. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/damage.py +0 -0
  23. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/runtime.py +0 -0
  24. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/creatures/spawn.py +0 -0
  25. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/debug.py +0 -0
  26. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/demo.py +0 -0
  27. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/demo_trial.py +0 -0
  28. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/effects.py +0 -0
  29. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/effects_atlas.py +0 -0
  30. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/__init__.py +0 -0
  31. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/assets.py +0 -0
  32. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/boot.py +0 -0
  33. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/menu.py +0 -0
  34. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/__init__.py +0 -0
  35. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/base.py +0 -0
  36. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/controls.py +0 -0
  37. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/mods.py +0 -0
  38. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/options.py +0 -0
  39. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/play_game.py +0 -0
  40. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/panels/stats.py +0 -0
  41. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/frontend/transitions.py +0 -0
  42. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/game.py +0 -0
  43. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/game_modes.py +0 -0
  44. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/input_codes.py +0 -0
  45. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/__init__.py +0 -0
  46. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/base_gameplay_mode.py +0 -0
  47. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/quest_mode.py +0 -0
  48. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/rush_mode.py +0 -0
  49. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/survival_mode.py +0 -0
  50. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/tutorial_mode.py +0 -0
  51. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/modes/typo_mode.py +0 -0
  52. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/paths.py +0 -0
  53. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/perks.py +0 -0
  54. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/persistence/__init__.py +0 -0
  55. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/persistence/highscores.py +0 -0
  56. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/persistence/save_status.py +0 -0
  57. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/player_damage.py +0 -0
  58. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/__init__.py +0 -0
  59. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/helpers.py +0 -0
  60. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/registry.py +0 -0
  61. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/results.py +0 -0
  62. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/runtime.py +0 -0
  63. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/tier1.py +0 -0
  64. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/tier2.py +0 -0
  65. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/tier3.py +0 -0
  66. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/tier4.py +0 -0
  67. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/tier5.py +0 -0
  68. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/timeline.py +0 -0
  69. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/quests/types.py +0 -0
  70. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/render/__init__.py +0 -0
  71. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/render/terrain_fx.py +0 -0
  72. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/sim/__init__.py +0 -0
  73. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/sim/world_defs.py +0 -0
  74. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/terrain_assets.py +0 -0
  75. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/tutorial/__init__.py +0 -0
  76. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/tutorial/timeline.py +0 -0
  77. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/typo/__init__.py +0 -0
  78. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/typo/names.py +0 -0
  79. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/typo/player.py +0 -0
  80. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/typo/spawns.py +0 -0
  81. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/typo/typing.py +0 -0
  82. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/__init__.py +0 -0
  83. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/cursor.py +0 -0
  84. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/demo_trial_overlay.py +0 -0
  85. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/game_over.py +0 -0
  86. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/hud.py +0 -0
  87. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/ui/perk_menu.py +0 -0
  88. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/__init__.py +0 -0
  89. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/aim_debug.py +0 -0
  90. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/animations.py +0 -0
  91. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/audio_bootstrap.py +0 -0
  92. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/bonuses.py +0 -0
  93. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/camera_debug.py +0 -0
  94. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/camera_shake.py +0 -0
  95. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/corpse_stamp_debug.py +0 -0
  96. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/decals_debug.py +0 -0
  97. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/empty.py +0 -0
  98. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/fonts.py +0 -0
  99. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/game_over.py +0 -0
  100. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/ground.py +0 -0
  101. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/lighting_debug.py +0 -0
  102. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/particles.py +0 -0
  103. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/perk_menu_debug.py +0 -0
  104. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/perks.py +0 -0
  105. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/player_sprite_debug.py +0 -0
  106. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/projectiles.py +0 -0
  107. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/quest_title_overlay.py +0 -0
  108. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/registry.py +0 -0
  109. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/rush.py +0 -0
  110. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/small_font_debug.py +0 -0
  111. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/spawn_plan.py +0 -0
  112. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/sprites.py +0 -0
  113. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/survival.py +0 -0
  114. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/terrain.py +0 -0
  115. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/ui.py +0 -0
  116. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/views/wicons.py +0 -0
  117. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/weapon_sfx.py +0 -0
  118. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/crimson/weapons.py +0 -0
  119. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/__init__.py +0 -0
  120. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/app.py +0 -0
  121. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/assets.py +0 -0
  122. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/audio.py +0 -0
  123. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/config.py +0 -0
  124. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/console.py +0 -0
  125. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/fonts/__init__.py +0 -0
  126. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/fonts/grim_mono.py +0 -0
  127. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/fonts/small.py +0 -0
  128. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/input.py +0 -0
  129. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/jaz.py +0 -0
  130. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/math.py +0 -0
  131. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/music.py +0 -0
  132. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/paq.py +0 -0
  133. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/rand.py +0 -0
  134. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/sfx.py +0 -0
  135. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/sfx_map.py +0 -0
  136. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/src/grim/terrain_render.py +0 -0
  137. {crimsonland-0.1.0.dev3 → crimsonland-0.1.0.dev5}/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.dev3
3
+ Version: 0.1.0.dev5
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.dev3"
3
+ version = "0.1.0.dev5"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -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
@@ -1592,6 +1592,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1592
1592
 
1593
1593
  firing_during_reload = False
1594
1594
  ammo_cost = 1.0
1595
+ is_fire_bullets = float(player.fire_bullets_timer) > 0.0
1595
1596
  if player.reload_timer > 0.0:
1596
1597
  if player.ammo <= 0 and player.experience > 0:
1597
1598
  if perk_active(player, PerkId.REGRESSION_BULLETS):
@@ -1616,7 +1617,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1616
1617
  else:
1617
1618
  return
1618
1619
 
1619
- if player.ammo <= 0 and not firing_during_reload:
1620
+ if player.ammo <= 0 and not firing_during_reload and not is_fire_bullets:
1620
1621
  player_start_reload(player, state)
1621
1622
  return
1622
1623
 
@@ -1624,20 +1625,18 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1624
1625
  fire_bullets_weapon = weapon_entry_for_projectile_type_id(int(ProjectileTypeId.FIRE_BULLETS))
1625
1626
 
1626
1627
  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:
1628
+ spread_heat_base = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1629
+ if is_fire_bullets and fire_bullets_weapon is not None and fire_bullets_weapon.spread_heat_inc is not None:
1630
+ spread_heat_base = float(fire_bullets_weapon.spread_heat_inc)
1631
+
1632
+ if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None:
1630
1633
  shot_cooldown = (
1631
1634
  float(fire_bullets_weapon.shot_cooldown)
1632
1635
  if fire_bullets_weapon.shot_cooldown is not None
1633
1636
  else 0.0
1634
1637
  )
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
1638
+
1639
+ spread_inc = spread_heat_base * 1.3
1641
1640
 
1642
1641
  if perk_active(player, PerkId.FASTSHOT):
1643
1642
  shot_cooldown *= 0.88
@@ -1657,6 +1656,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1657
1656
  aim_jitter_x = aim_x + math.cos(dir_angle) * offset
1658
1657
  aim_jitter_y = aim_y + math.sin(dir_angle) * offset
1659
1658
  shot_angle = math.atan2(aim_jitter_y - float(player.pos_y), aim_jitter_x - float(player.pos_x)) + math.pi / 2.0
1659
+ particle_angle = shot_angle - math.pi / 2.0
1660
1660
 
1661
1661
  muzzle_x = player.pos_x + player.aim_dir_x * 16.0
1662
1662
  muzzle_y = player.pos_y + player.aim_dir_y * 16.0
@@ -1676,7 +1676,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1676
1676
  return 0.0013
1677
1677
  return 0.0015
1678
1678
 
1679
- if player.fire_bullets_timer > 0.0:
1679
+ if is_fire_bullets:
1680
1680
  pellets = max(1, int(pellet_count))
1681
1681
  shot_count = pellets
1682
1682
  meta = _projectile_meta_for_type_id(ProjectileTypeId.FIRE_BULLETS)
@@ -1713,16 +1713,16 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1713
1713
  state.secondary_projectiles.spawn(pos_x=muzzle_x, pos_y=muzzle_y, angle=shot_angle, type_id=4, owner_id=owner_id)
1714
1714
  elif weapon_id == WeaponId.FLAMETHROWER:
1715
1715
  # 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)
1716
+ state.particles.spawn_particle(pos_x=muzzle_x, pos_y=muzzle_y, angle=particle_angle, intensity=1.0, owner_id=owner_id)
1717
1717
  ammo_cost = 0.1
1718
1718
  elif weapon_id == WeaponId.BLOW_TORCH:
1719
1719
  # 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)
1720
+ 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
1721
  state.particles.entries[particle_id].style_id = 1
1722
1722
  ammo_cost = 0.05
1723
1723
  elif weapon_id == WeaponId.HR_FLAMER:
1724
1724
  # 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)
1725
+ 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
1726
  state.particles.entries[particle_id].style_id = 2
1727
1727
  ammo_cost = 0.1
1728
1728
  elif weapon_id == WeaponId.BUBBLEGUN:
@@ -1833,15 +1833,17 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1833
1833
  player.spread_heat = min(0.48, max(0.0, player.spread_heat + spread_inc))
1834
1834
 
1835
1835
  muzzle_inc = float(weapon.spread_heat_inc) if weapon.spread_heat_inc is not None else 0.0
1836
+ 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:
1837
+ muzzle_inc = float(fire_bullets_weapon.spread_heat_inc)
1836
1838
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha)
1837
1839
  player.muzzle_flash_alpha = min(1.0, player.muzzle_flash_alpha + muzzle_inc)
1838
1840
  player.muzzle_flash_alpha = min(0.8, player.muzzle_flash_alpha)
1839
1841
 
1840
1842
  player.shot_seq += 1
1841
- if not firing_during_reload and state.bonuses.reflex_boost <= 0.0:
1843
+ if (not firing_during_reload) and state.bonuses.reflex_boost <= 0.0 and not is_fire_bullets:
1842
1844
  player.ammo = max(0.0, float(player.ammo) - float(ammo_cost))
1843
- if player.ammo <= 0.0:
1844
- player_start_reload(player, state)
1845
+ if (not firing_during_reload) and player.ammo <= 0.0 and player.reload_timer <= 0.0:
1846
+ player_start_reload(player, state)
1845
1847
 
1846
1848
 
1847
1849
  def player_update(player: PlayerState, input_state: PlayerInput, dt: float, state: GameplayState, *, world_size: float = 1024.0) -> None:
@@ -1885,35 +1887,50 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
1885
1887
  player.aim_heading = math.atan2(aim_dir_y, aim_dir_x) + math.pi / 2.0
1886
1888
 
1887
1889
  # 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)
1890
+ raw_move_x = float(input_state.move_x)
1891
+ raw_move_y = float(input_state.move_y)
1892
+ raw_mag = math.hypot(raw_move_x, raw_move_y)
1893
+ moving_input = raw_mag > 0.2
1894
+
1895
+ if moving_input:
1896
+ inv = 1.0 / raw_mag if raw_mag > 1e-9 else 0.0
1897
+ move_x = raw_move_x * inv
1898
+ move_y = raw_move_y * inv
1899
+ player.heading = math.atan2(move_y, move_x) + math.pi / 2.0
1900
+ if perk_active(player, PerkId.LONG_DISTANCE_RUNNER):
1901
+ if player.move_speed < 2.0:
1902
+ player.move_speed = float(player.move_speed + dt * 4.0)
1903
+ player.move_speed = float(player.move_speed + dt)
1904
+ if player.move_speed > 2.8:
1905
+ player.move_speed = 2.8
1894
1906
  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))
1907
+ player.move_speed = float(player.move_speed + dt * 5.0)
1908
+ if player.move_speed > 2.0:
1909
+ player.move_speed = 2.0
1897
1910
  else:
1898
- player.long_distance_runner_timer = 0.0
1911
+ player.move_speed = float(player.move_speed - dt * 15.0)
1912
+ if player.move_speed < 0.0:
1913
+ player.move_speed = 0.0
1914
+ move_x = math.cos(player.heading - math.pi / 2.0)
1915
+ move_y = math.sin(player.heading - math.pi / 2.0)
1916
+
1917
+ if player.weapon_id == WeaponId.MEAN_MINIGUN and player.move_speed > 0.8:
1918
+ player.move_speed = 0.8
1899
1919
 
1900
- speed_multiplier = float(player.move_speed_multiplier + runner_bonus)
1920
+ speed_multiplier = float(player.speed_multiplier)
1901
1921
  if player.speed_bonus_timer > 0.0:
1902
1922
  speed_multiplier += 1.0
1903
- speed = 120.0 * speed_multiplier
1923
+
1924
+ speed = player.move_speed * speed_multiplier * 25.0
1925
+ if moving_input:
1926
+ speed *= min(1.0, raw_mag)
1904
1927
  if perk_active(player, PerkId.ALTERNATE_WEAPON):
1905
1928
  speed *= 0.8
1929
+
1906
1930
  player.pos_x = _clamp(player.pos_x + move_x * speed * dt, 0.0, float(world_size))
1907
1931
  player.pos_y = _clamp(player.pos_y + move_y * speed * dt, 0.0, float(world_size))
1908
1932
 
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
1933
+ player.move_phase += dt * player.move_speed * 19.0
1917
1934
 
1918
1935
  stationary = abs(player.pos_x - prev_x) <= 1e-9 and abs(player.pos_y - prev_y) <= 1e-9
1919
1936
  reload_scale = 1.0
@@ -220,6 +220,7 @@ class ProjectilePool:
220
220
  damage_scale_by_type: dict[int, float] | None = None,
221
221
  damage_scale_default: float = 1.0,
222
222
  ion_aoe_scale: float = 1.0,
223
+ detail_preset: int = 5,
223
224
  rng: Callable[[], int] | None = None,
224
225
  runtime_state: object | None = None,
225
226
  players: list[PlayerDamageable] | None = None,
@@ -280,6 +281,12 @@ class ProjectilePool:
280
281
  if rng is None:
281
282
  rng = _rng_zero
282
283
 
284
+ effects = None
285
+ sfx_queue = None
286
+ if runtime_state is not None:
287
+ effects = getattr(runtime_state, "effects", None)
288
+ sfx_queue = getattr(runtime_state, "sfx_queue", None)
289
+
283
290
  hits: list[tuple[int, float, float, float, float, float, float]] = []
284
291
  margin = 64.0
285
292
 
@@ -292,6 +299,90 @@ class ProjectilePool:
292
299
  def _damage_type_for() -> int:
293
300
  return 1
294
301
 
302
+ def _spawn_ion_hit_effects(type_id: int, pos_x: float, pos_y: float) -> None:
303
+ if effects is None or not hasattr(effects, "spawn"):
304
+ return
305
+
306
+ ring_scale = 0.0
307
+ ring_strength = 0.0
308
+ burst_scale = 0.0
309
+ if type_id == int(ProjectileTypeId.ION_MINIGUN):
310
+ ring_scale = 1.5
311
+ ring_strength = 0.1
312
+ burst_scale = 0.8
313
+ elif type_id == int(ProjectileTypeId.ION_RIFLE):
314
+ ring_scale = 1.2
315
+ ring_strength = 0.4
316
+ burst_scale = 1.2
317
+ elif type_id == int(ProjectileTypeId.ION_CANNON):
318
+ ring_scale = 1.0
319
+ ring_strength = 1.0
320
+ burst_scale = 2.2
321
+ if isinstance(sfx_queue, list):
322
+ sfx_queue.append("sfx_shockwave")
323
+ else:
324
+ return
325
+
326
+ detail = int(detail_preset)
327
+
328
+ # Port of `FUN_0042f270(pos, ring_scale, ring_strength)`: ring burst (effect_id=1).
329
+ effects.spawn(
330
+ effect_id=1,
331
+ pos_x=float(pos_x),
332
+ pos_y=float(pos_y),
333
+ vel_x=0.0,
334
+ vel_y=0.0,
335
+ rotation=0.0,
336
+ scale=1.0,
337
+ half_width=4.0,
338
+ half_height=4.0,
339
+ age=0.0,
340
+ lifetime=float(ring_strength) * 0.8,
341
+ flags=0x19,
342
+ color_r=0.6,
343
+ color_g=0.6,
344
+ color_b=0.9,
345
+ color_a=1.0,
346
+ rotation_step=0.0,
347
+ scale_step=float(ring_scale) * 45.0,
348
+ detail_preset=detail,
349
+ )
350
+
351
+ # Port of `FUN_0042f540(pos, burst_scale)`: burst cloud (effect_id=0).
352
+ burst = float(burst_scale) * 0.8
353
+ lifetime = min(burst * 0.7, 1.1)
354
+ half = burst * 32.0
355
+ count = int(half)
356
+ if detail < 3:
357
+ count //= 2
358
+
359
+ for _ in range(max(0, count)):
360
+ rotation = float(int(rng()) & 0x7F) * 0.049087387
361
+ vel_x = float((int(rng()) & 0x7F) - 0x40) * burst * 1.4
362
+ vel_y = float((int(rng()) & 0x7F) - 0x40) * burst * 1.4
363
+ scale_step = (float(int(rng()) % 100) * 0.01 + 0.1) * burst
364
+ effects.spawn(
365
+ effect_id=0,
366
+ pos_x=float(pos_x),
367
+ pos_y=float(pos_y),
368
+ vel_x=vel_x,
369
+ vel_y=vel_y,
370
+ rotation=rotation,
371
+ scale=1.0,
372
+ half_width=half,
373
+ half_height=half,
374
+ age=0.0,
375
+ lifetime=float(lifetime),
376
+ flags=0x1D,
377
+ color_r=0.4,
378
+ color_g=0.5,
379
+ color_b=1.0,
380
+ color_a=0.5,
381
+ rotation_step=0.0,
382
+ scale_step=scale_step,
383
+ detail_preset=detail,
384
+ )
385
+
295
386
  def _apply_damage_to_creature(
296
387
  creature_index: int,
297
388
  damage: float,
@@ -598,6 +689,8 @@ class ProjectilePool:
598
689
  if type_id == ProjectileTypeId.ION_RIFLE:
599
690
  if runtime_state is not None and getattr(runtime_state, "shock_chain_projectile_id", -1) == proj_index:
600
691
  proj.reserved = float(int(hit_idx) + 1)
692
+ if type_id in (ProjectileTypeId.ION_MINIGUN, ProjectileTypeId.ION_RIFLE, ProjectileTypeId.ION_CANNON):
693
+ _spawn_ion_hit_effects(int(type_id), target_x, target_y)
601
694
  elif type_id == ProjectileTypeId.PLASMA_CANNON:
602
695
  size = float(getattr(creature, "size", 50.0) or 50.0)
603
696
  ring_radius = size * 0.5 + 1.0