crimsonland 0.1.0.dev5__py3-none-any.whl → 0.1.0.dev11__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 +12 -2
- crimson/creatures/anim.py +1 -0
- crimson/demo.py +79 -37
- crimson/effects.py +1 -1
- crimson/game.py +5 -0
- crimson/gameplay.py +91 -31
- crimson/modes/base_gameplay_mode.py +75 -1
- crimson/modes/quest_mode.py +471 -44
- crimson/modes/rush_mode.py +12 -3
- crimson/modes/survival_mode.py +13 -2
- crimson/modes/tutorial_mode.py +12 -1
- crimson/modes/typo_mode.py +12 -4
- crimson/projectiles.py +224 -61
- crimson/render/world_renderer.py +172 -36
- crimson/ui/hud.py +274 -51
- crimson/views/arsenal_debug.py +46 -1
- crimson/views/player.py +2 -2
- {crimsonland-0.1.0.dev5.dist-info → crimsonland-0.1.0.dev11.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev5.dist-info → crimsonland-0.1.0.dev11.dist-info}/RECORD +22 -22
- grim/console.py +14 -0
- {crimsonland-0.1.0.dev5.dist-info → crimsonland-0.1.0.dev11.dist-info}/WHEEL +0 -0
- {crimsonland-0.1.0.dev5.dist-info → crimsonland-0.1.0.dev11.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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
crimson/demo.py
CHANGED
|
@@ -475,6 +475,9 @@ class DemoView:
|
|
|
475
475
|
player = self._world.players[idx]
|
|
476
476
|
player.pos_x = float(x)
|
|
477
477
|
player.pos_y = float(y)
|
|
478
|
+
# Keep aim anchored to the spawn position so demo aim starts stable.
|
|
479
|
+
player.aim_x = float(x)
|
|
480
|
+
player.aim_y = float(y)
|
|
478
481
|
weapon_assign_player(player, int(weapon_id))
|
|
479
482
|
self._demo_targets = [None] * len(self._world.players)
|
|
480
483
|
|
|
@@ -549,7 +552,8 @@ class DemoView:
|
|
|
549
552
|
|
|
550
553
|
def _setup_variant_0(self) -> None:
|
|
551
554
|
self._demo_time_limit_ms = 4000
|
|
552
|
-
weapon_id
|
|
555
|
+
# demo_setup_variant_0 uses weapon_id=0x0B.
|
|
556
|
+
weapon_id = 11
|
|
553
557
|
self._setup_world_players(
|
|
554
558
|
[
|
|
555
559
|
(448.0, 384.0, weapon_id),
|
|
@@ -567,7 +571,8 @@ class DemoView:
|
|
|
567
571
|
|
|
568
572
|
def _setup_variant_1(self) -> None:
|
|
569
573
|
self._demo_time_limit_ms = 5000
|
|
570
|
-
weapon_id
|
|
574
|
+
# demo_setup_variant_1 uses weapon_id=0x05.
|
|
575
|
+
weapon_id = 5
|
|
571
576
|
self._setup_world_players(
|
|
572
577
|
[
|
|
573
578
|
(490.0, 448.0, weapon_id),
|
|
@@ -586,7 +591,8 @@ class DemoView:
|
|
|
586
591
|
|
|
587
592
|
def _setup_variant_2(self) -> None:
|
|
588
593
|
self._demo_time_limit_ms = 5000
|
|
589
|
-
weapon_id
|
|
594
|
+
# demo_setup_variant_2 uses weapon_id=0x15.
|
|
595
|
+
weapon_id = 21
|
|
590
596
|
self._setup_world_players([(512.0, 512.0, weapon_id)])
|
|
591
597
|
y = 128
|
|
592
598
|
i = 0
|
|
@@ -601,7 +607,8 @@ class DemoView:
|
|
|
601
607
|
|
|
602
608
|
def _setup_variant_3(self) -> None:
|
|
603
609
|
self._demo_time_limit_ms = 4000
|
|
604
|
-
weapon_id
|
|
610
|
+
# demo_setup_variant_3 uses weapon_id=0x12.
|
|
611
|
+
weapon_id = 18
|
|
605
612
|
self._setup_world_players([(512.0, 512.0, weapon_id)])
|
|
606
613
|
for idx in range(20):
|
|
607
614
|
x = float(self._crand_mod(200) + 32)
|
|
@@ -690,8 +697,8 @@ class DemoView:
|
|
|
690
697
|
rl.draw_circle(int(sx), int(sy), radius, rl.Color(200, 120, 255, 255))
|
|
691
698
|
continue
|
|
692
699
|
if proj.type_id == 3:
|
|
693
|
-
t = _clamp(proj.
|
|
694
|
-
radius = proj.
|
|
700
|
+
t = _clamp(proj.vel_x, 0.0, 1.0)
|
|
701
|
+
radius = proj.vel_y * t * 80.0
|
|
695
702
|
alpha = int((1.0 - t) * 180.0)
|
|
696
703
|
color = rl.Color(200, 120, 255, alpha)
|
|
697
704
|
rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
|
|
@@ -881,10 +888,10 @@ class DemoView:
|
|
|
881
888
|
def _update_world(self, dt: float) -> None:
|
|
882
889
|
if not self._world.players:
|
|
883
890
|
return
|
|
884
|
-
inputs = self._build_demo_inputs()
|
|
891
|
+
inputs = self._build_demo_inputs(dt)
|
|
885
892
|
self._world.update(dt, inputs=inputs, auto_pick_perks=False, game_mode=0, perk_progression_enabled=False)
|
|
886
893
|
|
|
887
|
-
def _build_demo_inputs(self) -> list[PlayerInput]:
|
|
894
|
+
def _build_demo_inputs(self, dt: float) -> list[PlayerInput]:
|
|
888
895
|
players = self._world.players
|
|
889
896
|
creatures = self._world.creatures.entries
|
|
890
897
|
if len(self._demo_targets) != len(players):
|
|
@@ -892,42 +899,77 @@ class DemoView:
|
|
|
892
899
|
center_x = float(self._world.world_size) * 0.5
|
|
893
900
|
center_y = float(self._world.world_size) * 0.5
|
|
894
901
|
|
|
902
|
+
dt = float(dt)
|
|
903
|
+
|
|
904
|
+
def _turn_towards_heading(cur: float, target: float) -> tuple[float, float]:
|
|
905
|
+
cur = cur % math.tau
|
|
906
|
+
target = target % math.tau
|
|
907
|
+
delta = (target - cur + math.pi) % math.tau - math.pi
|
|
908
|
+
diff = abs(delta)
|
|
909
|
+
if diff <= 1e-9:
|
|
910
|
+
return cur, 0.0
|
|
911
|
+
step = dt * diff * 5.0
|
|
912
|
+
cur = (cur + step) % math.tau if delta > 0.0 else (cur - step) % math.tau
|
|
913
|
+
return cur, diff
|
|
914
|
+
|
|
895
915
|
inputs: list[PlayerInput] = []
|
|
896
916
|
for idx, player in enumerate(players):
|
|
897
917
|
target_idx = self._select_demo_target(idx, player, creatures)
|
|
898
|
-
aim_x = center_x
|
|
899
|
-
aim_y = center_y
|
|
900
918
|
target = None
|
|
901
919
|
if target_idx is not None and 0 <= target_idx < len(creatures):
|
|
902
920
|
candidate = creatures[target_idx]
|
|
903
|
-
if candidate.hp > 0.0:
|
|
921
|
+
if candidate.active and candidate.hp > 0.0:
|
|
904
922
|
target = candidate
|
|
905
|
-
aim_x = candidate.x
|
|
906
|
-
aim_y = candidate.y
|
|
907
|
-
|
|
908
|
-
move_x, move_y = 0.0, 0.0
|
|
909
|
-
to_cx = center_x - player.pos_x
|
|
910
|
-
to_cy = center_y - player.pos_y
|
|
911
|
-
nx, ny, d = _normalize(to_cx, to_cy)
|
|
912
|
-
if d > 120.0:
|
|
913
|
-
move_x += nx
|
|
914
|
-
move_y += ny
|
|
915
923
|
|
|
924
|
+
# Aim: ease the aim point toward the target.
|
|
925
|
+
aim_x = float(player.aim_x)
|
|
926
|
+
aim_y = float(player.aim_y)
|
|
927
|
+
auto_fire = False
|
|
916
928
|
if target is not None:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
929
|
+
aim_dx = float(target.x) - aim_x
|
|
930
|
+
aim_dy = float(target.y) - aim_y
|
|
931
|
+
aim_dir_x, aim_dir_y, aim_dist = _normalize(aim_dx, aim_dy)
|
|
932
|
+
if aim_dist >= 4.0:
|
|
933
|
+
step = aim_dist * 6.0 * dt
|
|
934
|
+
aim_x += aim_dir_x * step
|
|
935
|
+
aim_y += aim_dir_y * step
|
|
936
|
+
else:
|
|
937
|
+
aim_x = float(target.x)
|
|
938
|
+
aim_y = float(target.y)
|
|
939
|
+
auto_fire = aim_dist < 128.0
|
|
940
|
+
else:
|
|
941
|
+
ax, ay, amag = _normalize(float(player.pos_x) - center_x, float(player.pos_y) - center_y)
|
|
942
|
+
if amag <= 1e-6:
|
|
943
|
+
ax, ay = 0.0, -1.0
|
|
944
|
+
aim_x = float(player.pos_x) + ax * 60.0
|
|
945
|
+
aim_y = float(player.pos_y) + ay * 60.0
|
|
946
|
+
|
|
947
|
+
# Movement:
|
|
948
|
+
# - orbit center if no target
|
|
949
|
+
# - chase target when near center
|
|
950
|
+
# - return to center when too far
|
|
951
|
+
if target is None:
|
|
952
|
+
move_dx = -(float(player.pos_y) - center_y)
|
|
953
|
+
move_dy = float(player.pos_x) - center_x
|
|
954
|
+
else:
|
|
955
|
+
center_dist = math.hypot(float(player.pos_x) - center_x, float(player.pos_y) - center_y)
|
|
956
|
+
if center_dist <= 300.0:
|
|
957
|
+
move_dx = float(target.x) - float(player.pos_x)
|
|
958
|
+
move_dy = float(target.y) - float(player.pos_y)
|
|
959
|
+
else:
|
|
960
|
+
move_dx = center_x - float(player.pos_x)
|
|
961
|
+
move_dy = center_y - float(player.pos_y)
|
|
962
|
+
|
|
963
|
+
desired_x, desired_y, desired_mag = _normalize(move_dx, move_dy)
|
|
964
|
+
if desired_mag <= 1e-6:
|
|
965
|
+
move_x = 0.0
|
|
966
|
+
move_y = 0.0
|
|
967
|
+
else:
|
|
968
|
+
desired_heading = math.atan2(desired_y, desired_x) + math.pi / 2.0
|
|
969
|
+
smoothed_heading, angle_diff = _turn_towards_heading(float(player.heading), desired_heading)
|
|
970
|
+
move_mag = max(0.001, (math.pi - angle_diff) / math.pi)
|
|
971
|
+
move_x = math.cos(smoothed_heading - math.pi / 2.0) * move_mag
|
|
972
|
+
move_y = math.sin(smoothed_heading - math.pi / 2.0) * move_mag
|
|
931
973
|
|
|
932
974
|
inputs.append(
|
|
933
975
|
PlayerInput(
|
|
@@ -935,8 +977,8 @@ class DemoView:
|
|
|
935
977
|
move_y=move_y,
|
|
936
978
|
aim_x=aim_x,
|
|
937
979
|
aim_y=aim_y,
|
|
938
|
-
fire_down=
|
|
939
|
-
fire_pressed=
|
|
980
|
+
fire_down=auto_fire,
|
|
981
|
+
fire_pressed=auto_fire,
|
|
940
982
|
reload_pressed=False,
|
|
941
983
|
)
|
|
942
984
|
)
|
crimson/effects.py
CHANGED
|
@@ -261,7 +261,7 @@ class ParticlePool:
|
|
|
261
261
|
entry.age = alpha
|
|
262
262
|
entry.scale_x = shade
|
|
263
263
|
entry.scale_y = shade
|
|
264
|
-
|
|
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:
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
|
@@ -431,7 +472,7 @@ class BonusPool:
|
|
|
431
472
|
player,
|
|
432
473
|
BonusId(entry.bonus_id),
|
|
433
474
|
amount=entry.amount,
|
|
434
|
-
origin=
|
|
475
|
+
origin=entry,
|
|
435
476
|
creatures=creatures,
|
|
436
477
|
players=players,
|
|
437
478
|
apply_creature_damage=apply_creature_damage,
|
|
@@ -933,8 +974,8 @@ def perk_apply(
|
|
|
933
974
|
weapon_id = int(current)
|
|
934
975
|
for _ in range(100):
|
|
935
976
|
candidate = int(weapon_pick_random_available(state))
|
|
936
|
-
|
|
937
|
-
|
|
977
|
+
weapon_id = candidate
|
|
978
|
+
if candidate != int(WeaponId.PISTOL) and candidate != current:
|
|
938
979
|
break
|
|
939
980
|
weapon_assign_player(owner, weapon_id, state=state)
|
|
940
981
|
return
|
|
@@ -1626,7 +1667,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
|
|
|
1626
1667
|
|
|
1627
1668
|
shot_cooldown = float(weapon.shot_cooldown) if weapon.shot_cooldown is not None else 0.0
|
|
1628
1669
|
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:
|
|
1670
|
+
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:
|
|
1630
1671
|
spread_heat_base = float(fire_bullets_weapon.spread_heat_inc)
|
|
1631
1672
|
|
|
1632
1673
|
if is_fire_bullets and pellet_count == 1 and fire_bullets_weapon is not None:
|
|
@@ -1681,9 +1722,7 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
|
|
|
1681
1722
|
shot_count = pellets
|
|
1682
1723
|
meta = _projectile_meta_for_type_id(ProjectileTypeId.FIRE_BULLETS)
|
|
1683
1724
|
for _ in range(pellets):
|
|
1684
|
-
angle = shot_angle
|
|
1685
|
-
if pellets > 1:
|
|
1686
|
-
angle += float(int(state.rng.rand()) % 200 - 100) * 0.0015
|
|
1725
|
+
angle = shot_angle + float(int(state.rng.rand()) % 200 - 100) * 0.0015
|
|
1687
1726
|
state.projectiles.spawn(
|
|
1688
1727
|
pos_x=muzzle_x,
|
|
1689
1728
|
pos_y=muzzle_y,
|
|
@@ -1890,7 +1929,9 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
|
|
|
1890
1929
|
raw_move_x = float(input_state.move_x)
|
|
1891
1930
|
raw_move_y = float(input_state.move_y)
|
|
1892
1931
|
raw_mag = math.hypot(raw_move_x, raw_move_y)
|
|
1893
|
-
|
|
1932
|
+
# Demo/autoplay uses very small analog magnitudes to represent turn-in-place and
|
|
1933
|
+
# heading alignment slowdown; don't apply a deadzone there.
|
|
1934
|
+
moving_input = raw_mag > (0.0 if state.demo_mode_active else 0.2)
|
|
1894
1935
|
|
|
1895
1936
|
if moving_input:
|
|
1896
1937
|
inv = 1.0 / raw_mag if raw_mag > 1e-9 else 0.0
|
|
@@ -2323,8 +2364,8 @@ def bonus_apply(
|
|
|
2323
2364
|
return
|
|
2324
2365
|
|
|
2325
2366
|
|
|
2326
|
-
def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
|
|
2327
|
-
"""Refresh HUD slots based on current timer values."""
|
|
2367
|
+
def bonus_hud_update(state: GameplayState, players: list[PlayerState], *, dt: float = 0.0) -> None:
|
|
2368
|
+
"""Refresh HUD slots based on current timer values + advance slide animation."""
|
|
2328
2369
|
|
|
2329
2370
|
def _timer_value(ref: _TimerRef | None) -> float:
|
|
2330
2371
|
if ref is None:
|
|
@@ -2338,16 +2379,35 @@ def bonus_hud_update(state: GameplayState, players: list[PlayerState]) -> None:
|
|
|
2338
2379
|
return float(getattr(players[idx], ref.key, 0.0) or 0.0)
|
|
2339
2380
|
return 0.0
|
|
2340
2381
|
|
|
2341
|
-
|
|
2382
|
+
player_count = len(players)
|
|
2383
|
+
dt = max(0.0, float(dt))
|
|
2384
|
+
|
|
2385
|
+
for slot_index, slot in enumerate(state.bonus_hud.slots):
|
|
2342
2386
|
if not slot.active:
|
|
2343
2387
|
continue
|
|
2344
|
-
timer = _timer_value(slot.timer_ref)
|
|
2345
|
-
if slot.timer_ref_alt is not None
|
|
2346
|
-
|
|
2347
|
-
|
|
2388
|
+
timer = max(0.0, _timer_value(slot.timer_ref))
|
|
2389
|
+
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
|
|
2390
|
+
slot.timer_value = float(timer)
|
|
2391
|
+
slot.timer_value_alt = float(timer_alt)
|
|
2392
|
+
|
|
2393
|
+
if timer > 0.0 or timer_alt > 0.0:
|
|
2394
|
+
slot.slide_x += dt * 350.0
|
|
2395
|
+
else:
|
|
2396
|
+
slot.slide_x -= dt * 320.0
|
|
2397
|
+
|
|
2398
|
+
if slot.slide_x > -2.0:
|
|
2399
|
+
slot.slide_x = -2.0
|
|
2400
|
+
|
|
2401
|
+
if slot.slide_x < -184.0 and not any(other.active for other in state.bonus_hud.slots[slot_index + 1 :]):
|
|
2348
2402
|
slot.active = False
|
|
2403
|
+
slot.bonus_id = 0
|
|
2404
|
+
slot.label = ""
|
|
2405
|
+
slot.icon_id = -1
|
|
2406
|
+
slot.slide_x = -184.0
|
|
2349
2407
|
slot.timer_ref = None
|
|
2350
2408
|
slot.timer_ref_alt = None
|
|
2409
|
+
slot.timer_value = 0.0
|
|
2410
|
+
slot.timer_value_alt = 0.0
|
|
2351
2411
|
|
|
2352
2412
|
|
|
2353
2413
|
def bonus_telekinetic_update(
|
|
@@ -2398,7 +2458,7 @@ def bonus_telekinetic_update(
|
|
|
2398
2458
|
player,
|
|
2399
2459
|
BonusId(int(entry.bonus_id)),
|
|
2400
2460
|
amount=int(entry.amount),
|
|
2401
|
-
origin=
|
|
2461
|
+
origin=entry,
|
|
2402
2462
|
creatures=creatures,
|
|
2403
2463
|
players=players,
|
|
2404
2464
|
apply_creature_damage=apply_creature_damage,
|
|
@@ -2462,6 +2522,6 @@ def bonus_update(
|
|
|
2462
2522
|
state.bonuses.freeze = max(0.0, state.bonuses.freeze - dt)
|
|
2463
2523
|
|
|
2464
2524
|
if update_hud:
|
|
2465
|
-
bonus_hud_update(state, players)
|
|
2525
|
+
bonus_hud_update(state, players, dt=dt)
|
|
2466
2526
|
|
|
2467
2527
|
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
|