crimsonland 0.1.0.dev6__py3-none-any.whl → 0.1.0.dev8__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.
crimson/audio_router.py CHANGED
@@ -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))
crimson/creatures/anim.py CHANGED
@@ -28,6 +28,7 @@ _CREATURE_CORPSE_FRAMES: dict[int, int] = {
28
28
  3: 1, # spider sp1
29
29
  4: 2, # spider sp2
30
30
  5: 7, # trooper
31
+ 7: 6, # ping-pong strip corpse fallback
31
32
  }
32
33
 
33
34
 
crimson/game.py CHANGED
@@ -789,6 +789,7 @@ class SurvivalGameView:
789
789
  ViewContext(assets_dir=state.assets_dir),
790
790
  texture_cache=state.texture_cache,
791
791
  config=state.config,
792
+ console=state.console,
792
793
  audio=state.audio,
793
794
  audio_rng=state.rng,
794
795
  )
@@ -848,6 +849,7 @@ class RushGameView:
848
849
  ViewContext(assets_dir=state.assets_dir),
849
850
  texture_cache=state.texture_cache,
850
851
  config=state.config,
852
+ console=state.console,
851
853
  audio=state.audio,
852
854
  audio_rng=state.rng,
853
855
  )
@@ -905,6 +907,7 @@ class TypoShooterGameView:
905
907
  ViewContext(assets_dir=state.assets_dir),
906
908
  texture_cache=state.texture_cache,
907
909
  config=state.config,
910
+ console=state.console,
908
911
  audio=state.audio,
909
912
  audio_rng=state.rng,
910
913
  )
@@ -962,6 +965,7 @@ class TutorialGameView:
962
965
  ViewContext(assets_dir=state.assets_dir),
963
966
  texture_cache=state.texture_cache,
964
967
  config=state.config,
968
+ console=state.console,
965
969
  audio=state.audio,
966
970
  audio_rng=state.rng,
967
971
  demo_mode_active=state.demo_enabled,
@@ -1011,6 +1015,7 @@ class QuestGameView:
1011
1015
  ViewContext(assets_dir=state.assets_dir),
1012
1016
  texture_cache=state.texture_cache,
1013
1017
  config=state.config,
1018
+ console=state.console,
1014
1019
  audio=state.audio,
1015
1020
  audio_rng=state.rng,
1016
1021
  demo_mode_active=state.demo_enabled,
crimson/gameplay.py CHANGED
@@ -159,8 +159,11 @@ class BonusHudSlot:
159
159
  bonus_id: int = 0
160
160
  label: str = ""
161
161
  icon_id: int = -1
162
+ slide_x: float = -184.0
162
163
  timer_ref: _TimerRef | None = None
163
164
  timer_ref_alt: _TimerRef | None = None
165
+ timer_value: float = 0.0
166
+ timer_value_alt: float = 0.0
164
167
 
165
168
 
166
169
  BONUS_HUD_SLOT_COUNT = 16
@@ -199,8 +202,11 @@ class BonusHudState:
199
202
  slot.bonus_id = int(bonus_id)
200
203
  slot.label = label
201
204
  slot.icon_id = int(icon_id)
205
+ slot.slide_x = -184.0
202
206
  slot.timer_ref = timer_ref
203
207
  slot.timer_ref_alt = timer_ref_alt
208
+ slot.timer_value = 0.0
209
+ slot.timer_value_alt = 0.0
204
210
 
205
211
 
206
212
  @dataclass(slots=True)
@@ -358,11 +364,49 @@ class BonusPool:
358
364
  return None
359
365
 
360
366
  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
367
+ # Native special-case: while any player has Pistol, 3/4 chance to force a Weapon drop.
368
+ if players and any(int(player.weapon_id) == int(WeaponId.PISTOL) for player in players):
369
+ if (int(rng.rand()) & 3) < 3:
370
+ entry = self.spawn_at_pos(
371
+ pos_x,
372
+ pos_y,
373
+ state=state,
374
+ players=players,
375
+ world_width=world_width,
376
+ world_height=world_height,
377
+ )
378
+ if entry is None:
379
+ return None
380
+
381
+ entry.bonus_id = int(BonusId.WEAPON)
382
+ weapon_id = int(weapon_pick_random_available(state))
383
+ entry.amount = int(weapon_id)
384
+ if weapon_id == int(WeaponId.PISTOL):
385
+ weapon_id = int(weapon_pick_random_available(state))
386
+ entry.amount = int(weapon_id)
387
+
388
+ matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
389
+ if matches > 1:
390
+ self._clear_entry(entry)
391
+ return None
392
+
393
+ if entry.amount == int(WeaponId.PISTOL) or (players and perk_active(players[0], PerkId.MY_FAVOURITE_WEAPON)):
394
+ self._clear_entry(entry)
395
+ return None
396
+
397
+ return entry
398
+
399
+ base_roll = int(rng.rand())
400
+ if base_roll % 9 != 1:
401
+ allow_without_magnet = False
402
+ if players and int(players[0].weapon_id) == int(WeaponId.PISTOL):
403
+ allow_without_magnet = int(rng.rand()) % 5 == 1
404
+
405
+ if not allow_without_magnet:
406
+ if not (players and perk_active(players[0], PerkId.BONUS_MAGNET)):
407
+ return None
408
+ if int(rng.rand()) % 10 != 2:
409
+ return None
366
410
 
367
411
  entry = self.spawn_at_pos(
368
412
  pos_x,
@@ -377,11 +421,9 @@ class BonusPool:
377
421
 
378
422
  if entry.bonus_id == int(BonusId.WEAPON):
379
423
  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
424
+ if players and _distance_sq(pos_x, pos_y, players[0].pos_x, players[0].pos_y) < near_sq:
425
+ entry.bonus_id = int(BonusId.POINTS)
426
+ entry.amount = 100
385
427
 
386
428
  if entry.bonus_id != int(BonusId.POINTS):
387
429
  matches = sum(1 for bonus in self._entries if bonus.bonus_id == entry.bonus_id)
@@ -390,10 +432,9 @@ class BonusPool:
390
432
  return None
391
433
 
392
434
  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
435
+ if players and entry.amount == players[0].weapon_id:
436
+ self._clear_entry(entry)
437
+ return None
397
438
 
398
439
  return entry
399
440
 
@@ -2321,8 +2362,8 @@ def bonus_apply(
2321
2362
  return
2322
2363
 
2323
2364
 
2324
- def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2325
- """Refresh HUD slots based on current timer values."""
2365
+ def bonus_hud_update(state: GameplayState, players: list[PlayerState], *, dt: float = 0.0) -> None:
2366
+ """Refresh HUD slots based on current timer values + advance slide animation."""
2326
2367
 
2327
2368
  def _timer_value(ref: _TimerRef | None) -> float:
2328
2369
  if ref is None:
@@ -2336,16 +2377,35 @@ def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
2336
2377
  return float(getattr(players[idx], ref.key, 0.0) or 0.0)
2337
2378
  return 0.0
2338
2379
 
2339
- for slot in state.bonus_hud.slots:
2380
+ player_count = len(players)
2381
+ dt = max(0.0, float(dt))
2382
+
2383
+ for slot_index, slot in enumerate(state.bonus_hud.slots):
2340
2384
  if not slot.active:
2341
2385
  continue
2342
- timer = _timer_value(slot.timer_ref)
2343
- if slot.timer_ref_alt is not None:
2344
- timer = max(timer, _timer_value(slot.timer_ref_alt))
2345
- if timer <= 0.0:
2386
+ timer = max(0.0, _timer_value(slot.timer_ref))
2387
+ timer_alt = max(0.0, _timer_value(slot.timer_ref_alt)) if (slot.timer_ref_alt is not None and player_count > 1) else 0.0
2388
+ slot.timer_value = float(timer)
2389
+ slot.timer_value_alt = float(timer_alt)
2390
+
2391
+ if timer > 0.0 or timer_alt > 0.0:
2392
+ slot.slide_x += dt * 350.0
2393
+ else:
2394
+ slot.slide_x -= dt * 320.0
2395
+
2396
+ if slot.slide_x > -2.0:
2397
+ slot.slide_x = -2.0
2398
+
2399
+ if slot.slide_x < -184.0 and not any(other.active for other in state.bonus_hud.slots[slot_index + 1 :]):
2346
2400
  slot.active = False
2401
+ slot.bonus_id = 0
2402
+ slot.label = ""
2403
+ slot.icon_id = -1
2404
+ slot.slide_x = -184.0
2347
2405
  slot.timer_ref = None
2348
2406
  slot.timer_ref_alt = None
2407
+ slot.timer_value = 0.0
2408
+ slot.timer_value_alt = 0.0
2349
2409
 
2350
2410
 
2351
2411
  def bonus_telekinetic_update(
@@ -2460,6 +2520,6 @@ def bonus_update(
2460
2520
  state.bonuses.freeze = max(0.0, state.bonuses.freeze - dt)
2461
2521
 
2462
2522
  if update_hud:
2463
- bonus_hud_update(state, players)
2523
+ bonus_hud_update(state, players, dt=dt)
2464
2524
 
2465
2525
  return pickups
@@ -8,14 +8,17 @@ import pyray as rl
8
8
 
9
9
  from grim.assets import PaqTextureCache
10
10
  from grim.audio import AudioState, update_audio
11
+ from grim.console import ConsoleState
11
12
  from grim.config import CrimsonConfig
12
13
  from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
13
14
  from grim.view import ViewContext
14
15
 
16
+ from ..gameplay import _creature_find_in_radius, perk_count_get
15
17
  from ..game_world import GameWorld
16
18
  from ..persistence.highscores import HighScoreRecord
19
+ from ..perks import PerkId
17
20
  from ..ui.game_over import GameOverUi
18
- from ..ui.hud import HudAssets, load_hud_assets
21
+ from ..ui.hud import HudAssets, draw_target_health_bar, load_hud_assets
19
22
 
20
23
  if TYPE_CHECKING:
21
24
  from ..persistence.save_status import GameStatus
@@ -45,6 +48,7 @@ class BaseGameplayMode:
45
48
  hardcore: bool = False,
46
49
  texture_cache: PaqTextureCache | None = None,
47
50
  config: CrimsonConfig | None = None,
51
+ console: ConsoleState | None = None,
48
52
  audio: AudioState | None = None,
49
53
  audio_rng: random.Random | None = None,
50
54
  ) -> None:
@@ -54,7 +58,9 @@ class BaseGameplayMode:
54
58
  self._small: SmallFontData | None = None
55
59
  self._hud_assets: HudAssets | None = None
56
60
 
61
+ self._default_game_mode_id = int(default_game_mode_id)
57
62
  self._config = config
63
+ self._console = console
58
64
  self._base_dir = config.path.parent if config is not None else Path.cwd()
59
65
 
60
66
  self.close_requested = False
@@ -90,6 +96,74 @@ class BaseGameplayMode:
90
96
  self._last_dt_ms = 0.0
91
97
  self._screen_fade: _ScreenFade | None = None
92
98
 
99
+ def _cvar_float(self, name: str, default: float = 0.0) -> float:
100
+ console = self._console
101
+ if console is None:
102
+ return float(default)
103
+ cvar = console.cvars.get(name)
104
+ if cvar is None:
105
+ return float(default)
106
+ return float(cvar.value_f)
107
+
108
+ def _hud_small_indicators(self) -> bool:
109
+ return self._cvar_float("cv_uiSmallIndicators", 0.0) != 0.0
110
+
111
+ def _config_game_mode_id(self) -> int:
112
+ config = self._config
113
+ if config is None:
114
+ return int(self._default_game_mode_id)
115
+ try:
116
+ value = config.data.get("game_mode", self._default_game_mode_id)
117
+ return int(value or self._default_game_mode_id)
118
+ except Exception:
119
+ return int(self._default_game_mode_id)
120
+
121
+ def _draw_target_health_bar(self, *, alpha: float = 1.0) -> None:
122
+ creatures = getattr(self._creatures, "entries", [])
123
+ if not creatures:
124
+ return
125
+
126
+ if perk_count_get(self._player, PerkId.DOCTOR) <= 0:
127
+ return
128
+
129
+ target_idx = _creature_find_in_radius(
130
+ creatures,
131
+ pos_x=float(getattr(self._player, "aim_x", 0.0)),
132
+ pos_y=float(getattr(self._player, "aim_y", 0.0)),
133
+ radius=12.0,
134
+ start_index=0,
135
+ )
136
+ if target_idx == -1:
137
+ return
138
+
139
+ creature = creatures[target_idx]
140
+ if not bool(getattr(creature, "active", False)):
141
+ return
142
+ hp = float(getattr(creature, "hp", 0.0))
143
+ max_hp = float(getattr(creature, "max_hp", 0.0))
144
+ if hp <= 0.0 or max_hp <= 0.0:
145
+ return
146
+
147
+ ratio = hp / max_hp
148
+ if ratio < 0.0:
149
+ ratio = 0.0
150
+ if ratio > 1.0:
151
+ ratio = 1.0
152
+
153
+ x0, y0 = self._world.world_to_screen(float(creature.x) - 32.0, float(creature.y) + 32.0)
154
+ x1, _y1 = self._world.world_to_screen(float(creature.x) + 32.0, float(creature.y) + 32.0)
155
+ width = float(x1) - float(x0)
156
+ if width <= 1e-3:
157
+ return
158
+ draw_target_health_bar(
159
+ x=float(x0),
160
+ y=float(y0),
161
+ width=width,
162
+ ratio=ratio,
163
+ alpha=float(alpha),
164
+ scale=width / 64.0,
165
+ )
166
+
93
167
  def _bind_world(self) -> None:
94
168
  self._state = self._world.state
95
169
  self._creatures = self._world.creatures