crimsonland 0.1.0.dev14__py3-none-any.whl → 0.1.0.dev16__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/cli.py +63 -0
- crimson/creatures/damage.py +111 -36
- crimson/creatures/runtime.py +246 -156
- crimson/creatures/spawn.py +7 -3
- crimson/debug.py +9 -0
- crimson/demo.py +38 -45
- crimson/effects.py +7 -13
- crimson/frontend/high_scores_layout.py +81 -0
- crimson/frontend/panels/base.py +4 -1
- crimson/frontend/panels/controls.py +0 -15
- crimson/frontend/panels/databases.py +291 -3
- crimson/frontend/panels/mods.py +0 -15
- crimson/frontend/panels/play_game.py +0 -16
- crimson/game.py +689 -3
- crimson/gameplay.py +921 -569
- crimson/modes/base_gameplay_mode.py +33 -12
- crimson/modes/components/__init__.py +2 -0
- crimson/modes/components/highscore_record_builder.py +58 -0
- crimson/modes/components/perk_menu_controller.py +325 -0
- crimson/modes/quest_mode.py +94 -272
- crimson/modes/rush_mode.py +12 -43
- crimson/modes/survival_mode.py +109 -330
- crimson/modes/tutorial_mode.py +46 -247
- crimson/modes/typo_mode.py +11 -38
- crimson/oracle.py +396 -0
- crimson/perks.py +5 -2
- crimson/player_damage.py +95 -36
- crimson/projectiles.py +539 -320
- crimson/render/projectile_draw_registry.py +637 -0
- crimson/render/projectile_render_registry.py +110 -0
- crimson/render/secondary_projectile_draw_registry.py +206 -0
- crimson/render/world_renderer.py +58 -707
- crimson/sim/world_state.py +118 -61
- crimson/typo/spawns.py +5 -12
- crimson/ui/demo_trial_overlay.py +3 -11
- crimson/ui/formatting.py +24 -0
- crimson/ui/game_over.py +12 -58
- crimson/ui/hud.py +72 -39
- crimson/ui/layout.py +20 -0
- crimson/ui/perk_menu.py +9 -34
- crimson/ui/quest_results.py +28 -70
- crimson/ui/text_input.py +20 -0
- crimson/views/_ui_helpers.py +27 -0
- crimson/views/aim_debug.py +15 -32
- crimson/views/animations.py +18 -28
- crimson/views/arsenal_debug.py +22 -32
- crimson/views/bonuses.py +23 -36
- crimson/views/camera_debug.py +16 -29
- crimson/views/camera_shake.py +9 -33
- crimson/views/corpse_stamp_debug.py +13 -21
- crimson/views/decals_debug.py +36 -23
- crimson/views/fonts.py +8 -25
- crimson/views/ground.py +4 -21
- crimson/views/lighting_debug.py +42 -45
- crimson/views/particles.py +33 -42
- crimson/views/perk_menu_debug.py +3 -10
- crimson/views/player.py +50 -44
- crimson/views/player_sprite_debug.py +24 -31
- crimson/views/projectile_fx.py +57 -52
- crimson/views/projectile_render_debug.py +24 -33
- crimson/views/projectiles.py +24 -37
- crimson/views/spawn_plan.py +13 -29
- crimson/views/sprites.py +14 -29
- crimson/views/terrain.py +6 -23
- crimson/views/ui.py +7 -24
- crimson/views/wicons.py +28 -33
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
- grim/config.py +29 -1
- grim/console.py +7 -10
- grim/math.py +12 -0
- {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
crimson/gameplay.py
CHANGED
|
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
import math
|
|
5
|
-
from typing import TYPE_CHECKING, Protocol
|
|
5
|
+
from typing import TYPE_CHECKING, Callable, Protocol
|
|
6
6
|
|
|
7
|
+
from grim.math import clamp, distance_sq
|
|
7
8
|
from .bonuses import BONUS_BY_ID, BonusId
|
|
8
9
|
from grim.rand import Crand
|
|
9
10
|
from .effects import EffectPool, FxQueue, ParticlePool, SpriteEffectPool
|
|
@@ -277,8 +278,8 @@ class BonusPool:
|
|
|
277
278
|
if entry is None:
|
|
278
279
|
return None
|
|
279
280
|
|
|
280
|
-
x =
|
|
281
|
-
y =
|
|
281
|
+
x = clamp(float(pos_x), BONUS_SPAWN_MARGIN, float(world_width) - BONUS_SPAWN_MARGIN)
|
|
282
|
+
y = clamp(float(pos_y), BONUS_SPAWN_MARGIN, float(world_height) - BONUS_SPAWN_MARGIN)
|
|
282
283
|
|
|
283
284
|
entry.bonus_id = int(bonus_id)
|
|
284
285
|
entry.picked = False
|
|
@@ -316,7 +317,7 @@ class BonusPool:
|
|
|
316
317
|
for entry in self._entries:
|
|
317
318
|
if entry.bonus_id == 0:
|
|
318
319
|
continue
|
|
319
|
-
if
|
|
320
|
+
if distance_sq(pos_x, pos_y, entry.pos_x, entry.pos_y) < min_dist_sq:
|
|
320
321
|
return None
|
|
321
322
|
|
|
322
323
|
entry = self._alloc_slot()
|
|
@@ -421,7 +422,7 @@ class BonusPool:
|
|
|
421
422
|
|
|
422
423
|
if entry.bonus_id == int(BonusId.WEAPON):
|
|
423
424
|
near_sq = BONUS_WEAPON_NEAR_RADIUS * BONUS_WEAPON_NEAR_RADIUS
|
|
424
|
-
if players and
|
|
425
|
+
if players and distance_sq(pos_x, pos_y, players[0].pos_x, players[0].pos_y) < near_sq:
|
|
425
426
|
entry.bonus_id = int(BonusId.POINTS)
|
|
426
427
|
entry.amount = 100
|
|
427
428
|
|
|
@@ -466,7 +467,7 @@ class BonusPool:
|
|
|
466
467
|
continue
|
|
467
468
|
|
|
468
469
|
for player in players:
|
|
469
|
-
if
|
|
470
|
+
if distance_sq(entry.pos_x, entry.pos_y, player.pos_x, player.pos_y) < BONUS_PICKUP_RADIUS * BONUS_PICKUP_RADIUS:
|
|
470
471
|
bonus_apply(
|
|
471
472
|
state,
|
|
472
473
|
player,
|
|
@@ -503,7 +504,7 @@ def bonus_find_aim_hover_entry(player: PlayerState, bonus_pool: BonusPool) -> tu
|
|
|
503
504
|
for idx, entry in enumerate(bonus_pool.entries):
|
|
504
505
|
if entry.bonus_id == 0 or entry.picked:
|
|
505
506
|
continue
|
|
506
|
-
if
|
|
507
|
+
if distance_sq(aim_x, aim_y, entry.pos_x, entry.pos_y) < radius_sq:
|
|
507
508
|
return idx, entry
|
|
508
509
|
return None
|
|
509
510
|
|
|
@@ -534,6 +535,7 @@ class GameplayState:
|
|
|
534
535
|
weapon_available: list[bool] = field(default_factory=lambda: [False] * WEAPON_COUNT_SIZE)
|
|
535
536
|
_weapon_available_game_mode: int = -1
|
|
536
537
|
_weapon_available_unlock_index: int = -1
|
|
538
|
+
_weapon_available_unlock_index_full: int = -1
|
|
537
539
|
friendly_fire_enabled: bool = False
|
|
538
540
|
bonus_spawn_guard: bool = False
|
|
539
541
|
bonus_hud: BonusHudState = field(default_factory=BonusHudState)
|
|
@@ -547,6 +549,7 @@ class GameplayState:
|
|
|
547
549
|
shots_fired: list[int] = field(default_factory=lambda: [0] * 4)
|
|
548
550
|
shots_hit: list[int] = field(default_factory=lambda: [0] * 4)
|
|
549
551
|
weapon_shots_fired: list[list[int]] = field(default_factory=lambda: [[0] * WEAPON_COUNT_SIZE for _ in range(4)])
|
|
552
|
+
debug_god_mode: bool = False
|
|
550
553
|
|
|
551
554
|
def __post_init__(self) -> None:
|
|
552
555
|
rand = self.rng.rand
|
|
@@ -594,6 +597,155 @@ def _creature_find_in_radius(creatures: list[_CreatureForPerks], *, pos_x: float
|
|
|
594
597
|
return -1
|
|
595
598
|
|
|
596
599
|
|
|
600
|
+
@dataclass(slots=True)
|
|
601
|
+
class _PerksUpdateEffectsCtx:
|
|
602
|
+
state: GameplayState
|
|
603
|
+
players: list[PlayerState]
|
|
604
|
+
dt: float
|
|
605
|
+
creatures: list[_CreatureForPerks] | None
|
|
606
|
+
fx_queue: FxQueue | None
|
|
607
|
+
_aim_target: int | None = None
|
|
608
|
+
|
|
609
|
+
def aim_target(self) -> int:
|
|
610
|
+
if self._aim_target is not None:
|
|
611
|
+
return int(self._aim_target)
|
|
612
|
+
|
|
613
|
+
target = -1
|
|
614
|
+
if self.players and self.creatures is not None and (
|
|
615
|
+
perk_active(self.players[0], PerkId.PYROKINETIC) or perk_active(self.players[0], PerkId.EVIL_EYES)
|
|
616
|
+
):
|
|
617
|
+
target = _creature_find_in_radius(
|
|
618
|
+
self.creatures,
|
|
619
|
+
pos_x=self.players[0].aim_x,
|
|
620
|
+
pos_y=self.players[0].aim_y,
|
|
621
|
+
radius=12.0,
|
|
622
|
+
start_index=0,
|
|
623
|
+
)
|
|
624
|
+
self._aim_target = int(target)
|
|
625
|
+
return int(target)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
_PerksUpdateEffectsStep = Callable[[_PerksUpdateEffectsCtx], None]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _perks_update_regeneration(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
632
|
+
if ctx.players and perk_active(ctx.players[0], PerkId.REGENERATION) and (ctx.state.rng.rand() & 1):
|
|
633
|
+
for player in ctx.players:
|
|
634
|
+
if not (0.0 < float(player.health) < 100.0):
|
|
635
|
+
continue
|
|
636
|
+
player.health = float(player.health) + ctx.dt
|
|
637
|
+
if player.health > 100.0:
|
|
638
|
+
player.health = 100.0
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _perks_update_lean_mean_exp_machine(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
642
|
+
ctx.state.lean_mean_exp_timer -= ctx.dt
|
|
643
|
+
if ctx.state.lean_mean_exp_timer < 0.0:
|
|
644
|
+
ctx.state.lean_mean_exp_timer = 0.25
|
|
645
|
+
for player in ctx.players:
|
|
646
|
+
perk_count = perk_count_get(player, PerkId.LEAN_MEAN_EXP_MACHINE)
|
|
647
|
+
if perk_count > 0:
|
|
648
|
+
player.experience += perk_count * 10
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _perks_update_death_clock(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
652
|
+
for player in ctx.players:
|
|
653
|
+
if not perk_active(player, PerkId.DEATH_CLOCK):
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
if float(player.health) <= 0.0:
|
|
657
|
+
player.health = 0.0
|
|
658
|
+
else:
|
|
659
|
+
player.health = float(player.health) - ctx.dt * 3.3333333
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _perks_update_evil_eyes_target(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
663
|
+
if not ctx.players:
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
target = ctx.aim_target()
|
|
667
|
+
player0 = ctx.players[0]
|
|
668
|
+
player0.evil_eyes_target_creature = target if perk_active(player0, PerkId.EVIL_EYES) else -1
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _perks_update_pyrokinetic(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
672
|
+
if not ctx.players:
|
|
673
|
+
return
|
|
674
|
+
if ctx.creatures is None:
|
|
675
|
+
return
|
|
676
|
+
if not perk_active(ctx.players[0], PerkId.PYROKINETIC):
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
target = ctx.aim_target()
|
|
680
|
+
if target == -1:
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
creature = ctx.creatures[target]
|
|
684
|
+
creature.collision_timer = float(creature.collision_timer) - ctx.dt
|
|
685
|
+
if creature.collision_timer < 0.0:
|
|
686
|
+
creature.collision_timer = 0.5
|
|
687
|
+
pos_x = float(creature.x)
|
|
688
|
+
pos_y = float(creature.y)
|
|
689
|
+
for intensity in (0.8, 0.6, 0.4, 0.3, 0.2):
|
|
690
|
+
angle = float(int(ctx.state.rng.rand()) % 0x274) * 0.01
|
|
691
|
+
ctx.state.particles.spawn_particle(pos_x=pos_x, pos_y=pos_y, angle=angle, intensity=float(intensity))
|
|
692
|
+
if ctx.fx_queue is not None:
|
|
693
|
+
ctx.fx_queue.add_random(pos_x=pos_x, pos_y=pos_y, rand=ctx.state.rng.rand)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _perks_update_jinxed_timer(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
697
|
+
if ctx.state.jinxed_timer >= 0.0:
|
|
698
|
+
ctx.state.jinxed_timer -= ctx.dt
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _perks_update_jinxed(ctx: _PerksUpdateEffectsCtx) -> None:
|
|
702
|
+
if ctx.state.jinxed_timer >= 0.0:
|
|
703
|
+
return
|
|
704
|
+
if not ctx.players:
|
|
705
|
+
return
|
|
706
|
+
if not perk_active(ctx.players[0], PerkId.JINXED):
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
player = ctx.players[0]
|
|
710
|
+
if int(ctx.state.rng.rand()) % 10 == 3:
|
|
711
|
+
player.health = float(player.health) - 5.0
|
|
712
|
+
if ctx.fx_queue is not None:
|
|
713
|
+
ctx.fx_queue.add_random(pos_x=player.pos_x, pos_y=player.pos_y, rand=ctx.state.rng.rand)
|
|
714
|
+
ctx.fx_queue.add_random(pos_x=player.pos_x, pos_y=player.pos_y, rand=ctx.state.rng.rand)
|
|
715
|
+
|
|
716
|
+
ctx.state.jinxed_timer = float(int(ctx.state.rng.rand()) % 0x14) * 0.1 + float(ctx.state.jinxed_timer) + 2.0
|
|
717
|
+
|
|
718
|
+
if float(ctx.state.bonuses.freeze) <= 0.0 and ctx.creatures is not None:
|
|
719
|
+
pool_mod = min(0x17F, len(ctx.creatures))
|
|
720
|
+
if pool_mod <= 0:
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
idx = int(ctx.state.rng.rand()) % pool_mod
|
|
724
|
+
attempts = 0
|
|
725
|
+
while attempts < 10 and not ctx.creatures[idx].active:
|
|
726
|
+
idx = int(ctx.state.rng.rand()) % pool_mod
|
|
727
|
+
attempts += 1
|
|
728
|
+
if not ctx.creatures[idx].active:
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
creature = ctx.creatures[idx]
|
|
732
|
+
creature.hp = -1.0
|
|
733
|
+
creature.hitbox_size = float(creature.hitbox_size) - ctx.dt * 20.0
|
|
734
|
+
player.experience = int(float(player.experience) + float(creature.reward_value))
|
|
735
|
+
ctx.state.sfx_queue.append("sfx_trooper_inpain_01")
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
_PERKS_UPDATE_EFFECT_STEPS: tuple[_PerksUpdateEffectsStep, ...] = (
|
|
739
|
+
_perks_update_regeneration,
|
|
740
|
+
_perks_update_lean_mean_exp_machine,
|
|
741
|
+
_perks_update_death_clock,
|
|
742
|
+
_perks_update_evil_eyes_target,
|
|
743
|
+
_perks_update_pyrokinetic,
|
|
744
|
+
_perks_update_jinxed_timer,
|
|
745
|
+
_perks_update_jinxed,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
597
749
|
def perks_update_effects(
|
|
598
750
|
state: GameplayState,
|
|
599
751
|
players: list[PlayerState],
|
|
@@ -607,83 +759,15 @@ def perks_update_effects(
|
|
|
607
759
|
dt = float(dt)
|
|
608
760
|
if dt <= 0.0:
|
|
609
761
|
return
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
state.lean_mean_exp_timer -= dt
|
|
620
|
-
if state.lean_mean_exp_timer < 0.0:
|
|
621
|
-
state.lean_mean_exp_timer = 0.25
|
|
622
|
-
for player in players:
|
|
623
|
-
perk_count = perk_count_get(player, PerkId.LEAN_MEAN_EXP_MACHINE)
|
|
624
|
-
if perk_count > 0:
|
|
625
|
-
player.experience += perk_count * 10
|
|
626
|
-
|
|
627
|
-
target = -1
|
|
628
|
-
if players and creatures is not None and (
|
|
629
|
-
perk_active(players[0], PerkId.PYROKINETIC) or perk_active(players[0], PerkId.EVIL_EYES)
|
|
630
|
-
):
|
|
631
|
-
target = _creature_find_in_radius(
|
|
632
|
-
creatures,
|
|
633
|
-
pos_x=players[0].aim_x,
|
|
634
|
-
pos_y=players[0].aim_y,
|
|
635
|
-
radius=12.0,
|
|
636
|
-
start_index=0,
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
if players:
|
|
640
|
-
player0 = players[0]
|
|
641
|
-
player0.evil_eyes_target_creature = target if perk_active(player0, PerkId.EVIL_EYES) else -1
|
|
642
|
-
|
|
643
|
-
if players and creatures is not None and perk_active(players[0], PerkId.PYROKINETIC) and target != -1:
|
|
644
|
-
creature = creatures[target]
|
|
645
|
-
creature.collision_timer = float(creature.collision_timer) - dt
|
|
646
|
-
if creature.collision_timer < 0.0:
|
|
647
|
-
creature.collision_timer = 0.5
|
|
648
|
-
pos_x = float(creature.x)
|
|
649
|
-
pos_y = float(creature.y)
|
|
650
|
-
for intensity in (0.8, 0.6, 0.4, 0.3, 0.2):
|
|
651
|
-
angle = float(int(state.rng.rand()) % 0x274) * 0.01
|
|
652
|
-
state.particles.spawn_particle(pos_x=pos_x, pos_y=pos_y, angle=angle, intensity=float(intensity))
|
|
653
|
-
if fx_queue is not None:
|
|
654
|
-
fx_queue.add_random(pos_x=pos_x, pos_y=pos_y, rand=state.rng.rand)
|
|
655
|
-
|
|
656
|
-
if state.jinxed_timer >= 0.0:
|
|
657
|
-
state.jinxed_timer -= dt
|
|
658
|
-
|
|
659
|
-
if state.jinxed_timer < 0.0 and players and perk_active(players[0], PerkId.JINXED):
|
|
660
|
-
player = players[0]
|
|
661
|
-
if int(state.rng.rand()) % 10 == 3:
|
|
662
|
-
player.health = float(player.health) - 5.0
|
|
663
|
-
if fx_queue is not None:
|
|
664
|
-
fx_queue.add_random(pos_x=player.pos_x, pos_y=player.pos_y, rand=state.rng.rand)
|
|
665
|
-
fx_queue.add_random(pos_x=player.pos_x, pos_y=player.pos_y, rand=state.rng.rand)
|
|
666
|
-
|
|
667
|
-
state.jinxed_timer = float(int(state.rng.rand()) % 0x14) * 0.1 + float(state.jinxed_timer) + 2.0
|
|
668
|
-
|
|
669
|
-
if float(state.bonuses.freeze) <= 0.0 and creatures is not None:
|
|
670
|
-
pool_mod = min(0x17F, len(creatures))
|
|
671
|
-
if pool_mod <= 0:
|
|
672
|
-
return
|
|
673
|
-
|
|
674
|
-
idx = int(state.rng.rand()) % pool_mod
|
|
675
|
-
attempts = 0
|
|
676
|
-
while attempts < 10 and not creatures[idx].active:
|
|
677
|
-
idx = int(state.rng.rand()) % pool_mod
|
|
678
|
-
attempts += 1
|
|
679
|
-
if not creatures[idx].active:
|
|
680
|
-
return
|
|
681
|
-
|
|
682
|
-
creature = creatures[idx]
|
|
683
|
-
creature.hp = -1.0
|
|
684
|
-
creature.hitbox_size = float(creature.hitbox_size) - dt * 20.0
|
|
685
|
-
player.experience = int(float(player.experience) + float(creature.reward_value))
|
|
686
|
-
state.sfx_queue.append("sfx_trooper_inpain_01")
|
|
762
|
+
ctx = _PerksUpdateEffectsCtx(
|
|
763
|
+
state=state,
|
|
764
|
+
players=players,
|
|
765
|
+
dt=dt,
|
|
766
|
+
creatures=creatures,
|
|
767
|
+
fx_queue=fx_queue,
|
|
768
|
+
)
|
|
769
|
+
for step in _PERKS_UPDATE_EFFECT_STEPS:
|
|
770
|
+
step(ctx)
|
|
687
771
|
|
|
688
772
|
|
|
689
773
|
def award_experience(state: GameplayState, player: PlayerState, amount: int) -> int:
|
|
@@ -941,139 +1025,193 @@ def _increment_perk_count(player: PlayerState, perk_id: PerkId, *, amount: int =
|
|
|
941
1025
|
player.perk_counts[idx] += int(amount)
|
|
942
1026
|
|
|
943
1027
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
"""Apply immediate perk effects and increment the perk counter."""
|
|
1028
|
+
@dataclass(slots=True)
|
|
1029
|
+
class _PerkApplyCtx:
|
|
1030
|
+
state: GameplayState
|
|
1031
|
+
players: list[PlayerState]
|
|
1032
|
+
owner: PlayerState
|
|
1033
|
+
perk_id: PerkId
|
|
1034
|
+
perk_state: PerkSelectionState | None
|
|
1035
|
+
dt: float | None
|
|
1036
|
+
creatures: list[_CreatureForPerks] | None
|
|
954
1037
|
|
|
955
|
-
|
|
1038
|
+
def frame_dt(self) -> float:
|
|
1039
|
+
return float(self.dt) if self.dt is not None else 0.0
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
_PerkApplyHandler = Callable[[_PerkApplyCtx], None]
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def _perk_apply_instant_winner(ctx: _PerkApplyCtx) -> None:
|
|
1046
|
+
ctx.owner.experience += 2500
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _perk_apply_fatal_lottery(ctx: _PerkApplyCtx) -> None:
|
|
1050
|
+
if ctx.state.rng.rand() & 1:
|
|
1051
|
+
ctx.owner.health = -1.0
|
|
1052
|
+
else:
|
|
1053
|
+
ctx.owner.experience += 10000
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _perk_apply_random_weapon(ctx: _PerkApplyCtx) -> None:
|
|
1057
|
+
current = int(ctx.owner.weapon_id)
|
|
1058
|
+
weapon_id = int(current)
|
|
1059
|
+
for _ in range(100):
|
|
1060
|
+
candidate = int(weapon_pick_random_available(ctx.state))
|
|
1061
|
+
weapon_id = candidate
|
|
1062
|
+
if candidate != int(WeaponId.PISTOL) and candidate != current:
|
|
1063
|
+
break
|
|
1064
|
+
weapon_assign_player(ctx.owner, weapon_id, state=ctx.state)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _perk_apply_lifeline_50_50(ctx: _PerkApplyCtx) -> None:
|
|
1068
|
+
creatures = ctx.creatures
|
|
1069
|
+
if creatures is None:
|
|
956
1070
|
return
|
|
957
|
-
owner = players[0]
|
|
958
|
-
try:
|
|
959
|
-
_increment_perk_count(owner, perk_id)
|
|
960
1071
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1072
|
+
kill_toggle = False
|
|
1073
|
+
for creature in creatures:
|
|
1074
|
+
if kill_toggle and creature.active and float(creature.hp) <= 500.0 and (int(creature.flags) & 0x04) == 0:
|
|
1075
|
+
creature.active = False
|
|
1076
|
+
ctx.state.effects.spawn_burst(
|
|
1077
|
+
pos_x=float(creature.x),
|
|
1078
|
+
pos_y=float(creature.y),
|
|
1079
|
+
count=4,
|
|
1080
|
+
rand=ctx.state.rng.rand,
|
|
1081
|
+
detail_preset=5,
|
|
1082
|
+
)
|
|
1083
|
+
kill_toggle = not kill_toggle
|
|
964
1084
|
|
|
965
|
-
if perk_id == PerkId.FATAL_LOTTERY:
|
|
966
|
-
if state.rng.rand() & 1:
|
|
967
|
-
owner.health = -1.0
|
|
968
|
-
else:
|
|
969
|
-
owner.experience += 10000
|
|
970
|
-
return
|
|
971
1085
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
candidate = int(weapon_pick_random_available(state))
|
|
977
|
-
weapon_id = candidate
|
|
978
|
-
if candidate != int(WeaponId.PISTOL) and candidate != current:
|
|
979
|
-
break
|
|
980
|
-
weapon_assign_player(owner, weapon_id, state=state)
|
|
981
|
-
return
|
|
1086
|
+
def _perk_apply_thick_skinned(ctx: _PerkApplyCtx) -> None:
|
|
1087
|
+
for player in ctx.players:
|
|
1088
|
+
if player.health > 0.0:
|
|
1089
|
+
player.health = max(1.0, player.health * (2.0 / 3.0))
|
|
982
1090
|
|
|
983
|
-
if perk_id == PerkId.LIFELINE_50_50:
|
|
984
|
-
if creatures is None:
|
|
985
|
-
return
|
|
986
|
-
|
|
987
|
-
kill_toggle = False
|
|
988
|
-
for creature in creatures:
|
|
989
|
-
if (
|
|
990
|
-
kill_toggle
|
|
991
|
-
and creature.active
|
|
992
|
-
and float(creature.hp) <= 500.0
|
|
993
|
-
and (int(creature.flags) & 0x04) == 0
|
|
994
|
-
):
|
|
995
|
-
creature.active = False
|
|
996
|
-
state.effects.spawn_burst(
|
|
997
|
-
pos_x=float(creature.x),
|
|
998
|
-
pos_y=float(creature.y),
|
|
999
|
-
count=4,
|
|
1000
|
-
rand=state.rng.rand,
|
|
1001
|
-
detail_preset=5,
|
|
1002
|
-
)
|
|
1003
|
-
kill_toggle = not kill_toggle
|
|
1004
|
-
return
|
|
1005
1091
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
player.health = max(1.0, player.health * (2.0 / 3.0))
|
|
1010
|
-
return
|
|
1092
|
+
def _perk_apply_breathing_room(ctx: _PerkApplyCtx) -> None:
|
|
1093
|
+
for player in ctx.players:
|
|
1094
|
+
player.health -= player.health * (2.0 / 3.0)
|
|
1011
1095
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1096
|
+
frame_dt = ctx.frame_dt()
|
|
1097
|
+
creatures = ctx.creatures
|
|
1098
|
+
if creatures is not None:
|
|
1099
|
+
for creature in creatures:
|
|
1100
|
+
if creature.active:
|
|
1101
|
+
creature.hitbox_size = float(creature.hitbox_size) - frame_dt
|
|
1015
1102
|
|
|
1016
|
-
|
|
1017
|
-
if creatures is not None:
|
|
1018
|
-
for creature in creatures:
|
|
1019
|
-
if creature.active:
|
|
1020
|
-
creature.hitbox_size = float(creature.hitbox_size) - frame_dt
|
|
1103
|
+
ctx.state.bonus_spawn_guard = False
|
|
1021
1104
|
|
|
1022
|
-
state.bonus_spawn_guard = False
|
|
1023
|
-
return
|
|
1024
1105
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
return
|
|
1106
|
+
def _perk_apply_infernal_contract(ctx: _PerkApplyCtx) -> None:
|
|
1107
|
+
ctx.owner.level += 3
|
|
1108
|
+
if ctx.perk_state is not None:
|
|
1109
|
+
ctx.perk_state.pending_count += 3
|
|
1110
|
+
ctx.perk_state.choices_dirty = True
|
|
1111
|
+
for player in ctx.players:
|
|
1112
|
+
if player.health > 0.0:
|
|
1113
|
+
player.health = 0.1
|
|
1034
1114
|
|
|
1035
|
-
if perk_id == PerkId.GRIM_DEAL:
|
|
1036
|
-
owner.health = -1.0
|
|
1037
|
-
owner.experience += int(owner.experience * 0.18)
|
|
1038
|
-
return
|
|
1039
1115
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
player.perk_counts[:] = owner.perk_counts
|
|
1044
|
-
for player in players:
|
|
1045
|
-
weapon_assign_player(player, int(player.weapon_id), state=state)
|
|
1046
|
-
return
|
|
1116
|
+
def _perk_apply_grim_deal(ctx: _PerkApplyCtx) -> None:
|
|
1117
|
+
ctx.owner.health = -1.0
|
|
1118
|
+
ctx.owner.experience += int(ctx.owner.experience * 0.18)
|
|
1047
1119
|
|
|
1048
|
-
if perk_id == PerkId.DEATH_CLOCK:
|
|
1049
|
-
_increment_perk_count(owner, PerkId.REGENERATION, amount=-perk_count_get(owner, PerkId.REGENERATION))
|
|
1050
|
-
_increment_perk_count(owner, PerkId.GREATER_REGENERATION, amount=-perk_count_get(owner, PerkId.GREATER_REGENERATION))
|
|
1051
|
-
for player in players:
|
|
1052
|
-
if player.health > 0.0:
|
|
1053
|
-
player.health = 100.0
|
|
1054
|
-
return
|
|
1055
1120
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
pos_x=float(player.pos_x),
|
|
1063
|
-
pos_y=float(player.pos_y),
|
|
1064
|
-
count=8,
|
|
1065
|
-
rand=state.rng.rand,
|
|
1066
|
-
detail_preset=5,
|
|
1067
|
-
)
|
|
1068
|
-
return
|
|
1121
|
+
def _perk_apply_ammo_maniac(ctx: _PerkApplyCtx) -> None:
|
|
1122
|
+
if len(ctx.players) > 1:
|
|
1123
|
+
for player in ctx.players[1:]:
|
|
1124
|
+
player.perk_counts[:] = ctx.owner.perk_counts
|
|
1125
|
+
for player in ctx.players:
|
|
1126
|
+
weapon_assign_player(player, int(player.weapon_id), state=ctx.state)
|
|
1069
1127
|
|
|
1070
|
-
if perk_id == PerkId.MY_FAVOURITE_WEAPON:
|
|
1071
|
-
for player in players:
|
|
1072
|
-
player.clip_size += 2
|
|
1073
|
-
return
|
|
1074
1128
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1129
|
+
def _perk_apply_death_clock(ctx: _PerkApplyCtx) -> None:
|
|
1130
|
+
_increment_perk_count(
|
|
1131
|
+
ctx.owner,
|
|
1132
|
+
PerkId.REGENERATION,
|
|
1133
|
+
amount=-perk_count_get(ctx.owner, PerkId.REGENERATION),
|
|
1134
|
+
)
|
|
1135
|
+
_increment_perk_count(
|
|
1136
|
+
ctx.owner,
|
|
1137
|
+
PerkId.GREATER_REGENERATION,
|
|
1138
|
+
amount=-perk_count_get(ctx.owner, PerkId.GREATER_REGENERATION),
|
|
1139
|
+
)
|
|
1140
|
+
for player in ctx.players:
|
|
1141
|
+
if player.health > 0.0:
|
|
1142
|
+
player.health = 100.0
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _perk_apply_bandage(ctx: _PerkApplyCtx) -> None:
|
|
1146
|
+
for player in ctx.players:
|
|
1147
|
+
if player.health > 0.0:
|
|
1148
|
+
scale = float(ctx.state.rng.rand() % 50 + 1)
|
|
1149
|
+
player.health = min(100.0, player.health * scale)
|
|
1150
|
+
ctx.state.effects.spawn_burst(
|
|
1151
|
+
pos_x=float(player.pos_x),
|
|
1152
|
+
pos_y=float(player.pos_y),
|
|
1153
|
+
count=8,
|
|
1154
|
+
rand=ctx.state.rng.rand,
|
|
1155
|
+
detail_preset=5,
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _perk_apply_my_favourite_weapon(ctx: _PerkApplyCtx) -> None:
|
|
1160
|
+
for player in ctx.players:
|
|
1161
|
+
player.clip_size += 2
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def _perk_apply_plaguebearer(ctx: _PerkApplyCtx) -> None:
|
|
1165
|
+
for player in ctx.players:
|
|
1166
|
+
player.plaguebearer_active = True
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
_PERK_APPLY_HANDLERS: dict[PerkId, _PerkApplyHandler] = {
|
|
1170
|
+
PerkId.INSTANT_WINNER: _perk_apply_instant_winner,
|
|
1171
|
+
PerkId.FATAL_LOTTERY: _perk_apply_fatal_lottery,
|
|
1172
|
+
PerkId.RANDOM_WEAPON: _perk_apply_random_weapon,
|
|
1173
|
+
PerkId.LIFELINE_50_50: _perk_apply_lifeline_50_50,
|
|
1174
|
+
PerkId.THICK_SKINNED: _perk_apply_thick_skinned,
|
|
1175
|
+
PerkId.BREATHING_ROOM: _perk_apply_breathing_room,
|
|
1176
|
+
PerkId.INFERNAL_CONTRACT: _perk_apply_infernal_contract,
|
|
1177
|
+
PerkId.GRIM_DEAL: _perk_apply_grim_deal,
|
|
1178
|
+
PerkId.AMMO_MANIAC: _perk_apply_ammo_maniac,
|
|
1179
|
+
PerkId.DEATH_CLOCK: _perk_apply_death_clock,
|
|
1180
|
+
PerkId.BANDAGE: _perk_apply_bandage,
|
|
1181
|
+
PerkId.MY_FAVOURITE_WEAPON: _perk_apply_my_favourite_weapon,
|
|
1182
|
+
PerkId.PLAGUEBEARER: _perk_apply_plaguebearer,
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def perk_apply(
|
|
1187
|
+
state: GameplayState,
|
|
1188
|
+
players: list[PlayerState],
|
|
1189
|
+
perk_id: PerkId,
|
|
1190
|
+
*,
|
|
1191
|
+
perk_state: PerkSelectionState | None = None,
|
|
1192
|
+
dt: float | None = None,
|
|
1193
|
+
creatures: list[_CreatureForPerks] | None = None,
|
|
1194
|
+
) -> None:
|
|
1195
|
+
"""Apply immediate perk effects and increment the perk counter."""
|
|
1196
|
+
|
|
1197
|
+
if not players:
|
|
1198
|
+
return
|
|
1199
|
+
owner = players[0]
|
|
1200
|
+
try:
|
|
1201
|
+
_increment_perk_count(owner, perk_id)
|
|
1202
|
+
handler = _PERK_APPLY_HANDLERS.get(perk_id)
|
|
1203
|
+
if handler is not None:
|
|
1204
|
+
handler(
|
|
1205
|
+
_PerkApplyCtx(
|
|
1206
|
+
state=state,
|
|
1207
|
+
players=players,
|
|
1208
|
+
owner=owner,
|
|
1209
|
+
perk_id=perk_id,
|
|
1210
|
+
perk_state=perk_state,
|
|
1211
|
+
dt=dt,
|
|
1212
|
+
creatures=creatures,
|
|
1213
|
+
)
|
|
1214
|
+
)
|
|
1077
1215
|
finally:
|
|
1078
1216
|
if len(players) > 1:
|
|
1079
1217
|
for player in players[1:]:
|
|
@@ -1198,14 +1336,6 @@ def survival_progression_update(
|
|
|
1198
1336
|
return []
|
|
1199
1337
|
|
|
1200
1338
|
|
|
1201
|
-
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
1202
|
-
if value < lo:
|
|
1203
|
-
return lo
|
|
1204
|
-
if value > hi:
|
|
1205
|
-
return hi
|
|
1206
|
-
return value
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
1339
|
def _normalize(x: float, y: float) -> tuple[float, float]:
|
|
1210
1340
|
mag = math.hypot(x, y)
|
|
1211
1341
|
if mag <= 1e-9:
|
|
@@ -1214,17 +1344,17 @@ def _normalize(x: float, y: float) -> tuple[float, float]:
|
|
|
1214
1344
|
return x * inv, y * inv
|
|
1215
1345
|
|
|
1216
1346
|
|
|
1217
|
-
def _distance_sq(x0: float, y0: float, x1: float, y1: float) -> float:
|
|
1218
|
-
dx = x1 - x0
|
|
1219
|
-
dy = y1 - y0
|
|
1220
|
-
return dx * dx + dy * dy
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
1347
|
def _owner_id_for_player(player_index: int) -> int:
|
|
1224
1348
|
# crimsonland.exe uses -1/-2/-3 for players (and sometimes -100 in demo paths).
|
|
1225
1349
|
return -1 - int(player_index)
|
|
1226
1350
|
|
|
1227
1351
|
|
|
1352
|
+
def _owner_id_for_player_projectiles(state: "GameplayState", player_index: int) -> int:
|
|
1353
|
+
if not state.friendly_fire_enabled:
|
|
1354
|
+
return -100
|
|
1355
|
+
return _owner_id_for_player(player_index)
|
|
1356
|
+
|
|
1357
|
+
|
|
1228
1358
|
def _weapon_entry(weapon_id: int) -> Weapon | None:
|
|
1229
1359
|
return WEAPON_BY_ID.get(int(weapon_id))
|
|
1230
1360
|
|
|
@@ -1236,17 +1366,23 @@ def weapon_refresh_available(state: "GameplayState") -> None:
|
|
|
1236
1366
|
"""
|
|
1237
1367
|
|
|
1238
1368
|
unlock_index = 0
|
|
1369
|
+
unlock_index_full = 0
|
|
1239
1370
|
status = state.status
|
|
1240
1371
|
if status is not None:
|
|
1241
1372
|
try:
|
|
1242
1373
|
unlock_index = int(status.quest_unlock_index)
|
|
1243
1374
|
except Exception:
|
|
1244
1375
|
unlock_index = 0
|
|
1376
|
+
try:
|
|
1377
|
+
unlock_index_full = int(status.quest_unlock_index_full)
|
|
1378
|
+
except Exception:
|
|
1379
|
+
unlock_index_full = 0
|
|
1245
1380
|
|
|
1246
1381
|
game_mode = int(state.game_mode)
|
|
1247
1382
|
if (
|
|
1248
1383
|
int(state._weapon_available_game_mode) == game_mode
|
|
1249
1384
|
and int(state._weapon_available_unlock_index) == unlock_index
|
|
1385
|
+
and int(state._weapon_available_unlock_index_full) == unlock_index_full
|
|
1250
1386
|
):
|
|
1251
1387
|
return
|
|
1252
1388
|
|
|
@@ -1281,8 +1417,16 @@ def weapon_refresh_available(state: "GameplayState") -> None:
|
|
|
1281
1417
|
if 0 <= idx < len(available):
|
|
1282
1418
|
available[idx] = True
|
|
1283
1419
|
|
|
1420
|
+
# Secret unlock: Splitter Gun (weapon id 29) becomes available once the hardcore
|
|
1421
|
+
# unlock track reaches stage 5 (quest_unlock_index_full >= 40).
|
|
1422
|
+
if (not state.demo_mode_active) and unlock_index_full >= 0x28:
|
|
1423
|
+
splitter_id = int(WeaponId.SPLITTER_GUN)
|
|
1424
|
+
if 0 <= splitter_id < len(available):
|
|
1425
|
+
available[splitter_id] = True
|
|
1426
|
+
|
|
1284
1427
|
state._weapon_available_game_mode = game_mode
|
|
1285
1428
|
state._weapon_available_unlock_index = unlock_index
|
|
1429
|
+
state._weapon_available_unlock_index_full = unlock_index_full
|
|
1286
1430
|
|
|
1287
1431
|
|
|
1288
1432
|
def weapon_pick_random_available(state: "GameplayState") -> int:
|
|
@@ -1368,6 +1512,85 @@ def _bonus_id_from_roll(roll: int, rng: Crand) -> int:
|
|
|
1368
1512
|
return int(v6)
|
|
1369
1513
|
|
|
1370
1514
|
|
|
1515
|
+
@dataclass(slots=True)
|
|
1516
|
+
class _BonusPickCtx:
|
|
1517
|
+
pool: BonusPool
|
|
1518
|
+
state: GameplayState
|
|
1519
|
+
players: list[PlayerState]
|
|
1520
|
+
bonus_id: int
|
|
1521
|
+
has_fire_bullets_drop: bool
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
_BonusPickSuppressRule = Callable[[_BonusPickCtx], bool]
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _bonus_pick_suppress_active_shock_chain(ctx: _BonusPickCtx) -> bool:
|
|
1528
|
+
return ctx.state.shock_chain_links_left > 0 and int(ctx.bonus_id) == int(BonusId.SHOCK_CHAIN)
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def _bonus_pick_suppress_quest_minor10_nuke(ctx: _BonusPickCtx) -> bool:
|
|
1532
|
+
if not (int(ctx.state.game_mode) == int(GameMode.QUESTS) and int(ctx.state.quest_stage_minor) == 10):
|
|
1533
|
+
return False
|
|
1534
|
+
if int(ctx.bonus_id) != int(BonusId.NUKE):
|
|
1535
|
+
return False
|
|
1536
|
+
major = int(ctx.state.quest_stage_major)
|
|
1537
|
+
if major in (2, 4, 5):
|
|
1538
|
+
return True
|
|
1539
|
+
return bool(ctx.state.hardcore) and major == 3
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def _bonus_pick_suppress_quest_minor10_freeze(ctx: _BonusPickCtx) -> bool:
|
|
1543
|
+
if not (int(ctx.state.game_mode) == int(GameMode.QUESTS) and int(ctx.state.quest_stage_minor) == 10):
|
|
1544
|
+
return False
|
|
1545
|
+
if int(ctx.bonus_id) != int(BonusId.FREEZE):
|
|
1546
|
+
return False
|
|
1547
|
+
major = int(ctx.state.quest_stage_major)
|
|
1548
|
+
return major == 4 or (bool(ctx.state.hardcore) and major == 2)
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _bonus_pick_suppress_freeze_active(ctx: _BonusPickCtx) -> bool:
|
|
1552
|
+
return int(ctx.bonus_id) == int(BonusId.FREEZE) and float(ctx.state.bonuses.freeze) > 0.0
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def _bonus_pick_suppress_shield_active(ctx: _BonusPickCtx) -> bool:
|
|
1556
|
+
if int(ctx.bonus_id) != int(BonusId.SHIELD):
|
|
1557
|
+
return False
|
|
1558
|
+
return any(player.shield_timer > 0.0 for player in ctx.players)
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _bonus_pick_suppress_weapon_when_fire_bullets_drop(ctx: _BonusPickCtx) -> bool:
|
|
1562
|
+
return int(ctx.bonus_id) == int(BonusId.WEAPON) and bool(ctx.has_fire_bullets_drop)
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _bonus_pick_suppress_weapon_when_favourite_weapon(ctx: _BonusPickCtx) -> bool:
|
|
1566
|
+
if int(ctx.bonus_id) != int(BonusId.WEAPON):
|
|
1567
|
+
return False
|
|
1568
|
+
return any(perk_active(player, PerkId.MY_FAVOURITE_WEAPON) for player in ctx.players)
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _bonus_pick_suppress_medikit_when_death_clock(ctx: _BonusPickCtx) -> bool:
|
|
1572
|
+
if int(ctx.bonus_id) != int(BonusId.MEDIKIT):
|
|
1573
|
+
return False
|
|
1574
|
+
return any(perk_active(player, PerkId.DEATH_CLOCK) for player in ctx.players)
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def _bonus_pick_suppress_disabled(ctx: _BonusPickCtx) -> bool:
|
|
1578
|
+
return not _bonus_enabled(int(ctx.bonus_id))
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
_BONUS_PICK_SUPPRESS_RULES: tuple[_BonusPickSuppressRule, ...] = (
|
|
1582
|
+
_bonus_pick_suppress_active_shock_chain,
|
|
1583
|
+
_bonus_pick_suppress_quest_minor10_nuke,
|
|
1584
|
+
_bonus_pick_suppress_quest_minor10_freeze,
|
|
1585
|
+
_bonus_pick_suppress_freeze_active,
|
|
1586
|
+
_bonus_pick_suppress_shield_active,
|
|
1587
|
+
_bonus_pick_suppress_weapon_when_fire_bullets_drop,
|
|
1588
|
+
_bonus_pick_suppress_weapon_when_favourite_weapon,
|
|
1589
|
+
_bonus_pick_suppress_medikit_when_death_clock,
|
|
1590
|
+
_bonus_pick_suppress_disabled,
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
|
|
1371
1594
|
def bonus_pick_random_type(pool: BonusPool, state: "GameplayState", players: list["PlayerState"]) -> int:
|
|
1372
1595
|
has_fire_bullets_drop = any(
|
|
1373
1596
|
entry.bonus_id == int(BonusId.FIRE_BULLETS) and not entry.picked
|
|
@@ -1379,33 +1602,49 @@ def bonus_pick_random_type(pool: BonusPool, state: "GameplayState", players: lis
|
|
|
1379
1602
|
bonus_id = _bonus_id_from_roll(roll, state.rng)
|
|
1380
1603
|
if bonus_id <= 0:
|
|
1381
1604
|
continue
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
)
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
):
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if bonus_id == int(BonusId.SHIELD) and any(player.shield_timer > 0.0 for player in players):
|
|
1396
|
-
continue
|
|
1397
|
-
if bonus_id == int(BonusId.WEAPON) and has_fire_bullets_drop:
|
|
1398
|
-
continue
|
|
1399
|
-
if bonus_id == int(BonusId.WEAPON) and any(perk_active(player, PerkId.MY_FAVOURITE_WEAPON) for player in players):
|
|
1400
|
-
continue
|
|
1401
|
-
if bonus_id == int(BonusId.MEDIKIT) and any(perk_active(player, PerkId.DEATH_CLOCK) for player in players):
|
|
1402
|
-
continue
|
|
1403
|
-
if not _bonus_enabled(bonus_id):
|
|
1605
|
+
ctx = _BonusPickCtx(
|
|
1606
|
+
pool=pool,
|
|
1607
|
+
state=state,
|
|
1608
|
+
players=players,
|
|
1609
|
+
bonus_id=int(bonus_id),
|
|
1610
|
+
has_fire_bullets_drop=bool(has_fire_bullets_drop),
|
|
1611
|
+
)
|
|
1612
|
+
suppressed = False
|
|
1613
|
+
for rule in _BONUS_PICK_SUPPRESS_RULES:
|
|
1614
|
+
if rule(ctx):
|
|
1615
|
+
suppressed = True
|
|
1616
|
+
break
|
|
1617
|
+
if suppressed:
|
|
1404
1618
|
continue
|
|
1405
1619
|
return bonus_id
|
|
1406
1620
|
return int(BonusId.POINTS)
|
|
1407
1621
|
|
|
1408
1622
|
|
|
1623
|
+
@dataclass(slots=True)
|
|
1624
|
+
class _WeaponAssignCtx:
|
|
1625
|
+
player: PlayerState
|
|
1626
|
+
clip_size: int
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
_WeaponAssignClipModifier = Callable[[_WeaponAssignCtx], None]
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def _weapon_assign_clip_ammo_maniac(ctx: _WeaponAssignCtx) -> None:
|
|
1633
|
+
if perk_active(ctx.player, PerkId.AMMO_MANIAC):
|
|
1634
|
+
ctx.clip_size += max(1, int(float(ctx.clip_size) * 0.25))
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
def _weapon_assign_clip_my_favourite_weapon(ctx: _WeaponAssignCtx) -> None:
|
|
1638
|
+
if perk_active(ctx.player, PerkId.MY_FAVOURITE_WEAPON):
|
|
1639
|
+
ctx.clip_size += 2
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
_WEAPON_ASSIGN_CLIP_MODIFIERS: tuple[_WeaponAssignClipModifier, ...] = (
|
|
1643
|
+
_weapon_assign_clip_ammo_maniac,
|
|
1644
|
+
_weapon_assign_clip_my_favourite_weapon,
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
|
|
1409
1648
|
def weapon_assign_player(player: PlayerState, weapon_id: int, *, state: GameplayState | None = None) -> None:
|
|
1410
1649
|
"""Assign weapon and reset per-weapon runtime state (ammo/cooldowns)."""
|
|
1411
1650
|
|
|
@@ -1420,15 +1659,10 @@ def weapon_assign_player(player: PlayerState, weapon_id: int, *, state: Gameplay
|
|
|
1420
1659
|
player.weapon_id = weapon_id
|
|
1421
1660
|
|
|
1422
1661
|
clip_size = int(weapon.clip_size) if weapon is not None and weapon.clip_size is not None else 0
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
clip_size += max(1, int(float(clip_size) * 0.25))
|
|
1428
|
-
if perk_active(player, PerkId.MY_FAVOURITE_WEAPON):
|
|
1429
|
-
clip_size += 2
|
|
1430
|
-
|
|
1431
|
-
player.clip_size = max(0, int(clip_size))
|
|
1662
|
+
clip_ctx = _WeaponAssignCtx(player=player, clip_size=max(0, clip_size))
|
|
1663
|
+
for modifier in _WEAPON_ASSIGN_CLIP_MODIFIERS:
|
|
1664
|
+
modifier(clip_ctx)
|
|
1665
|
+
player.clip_size = max(0, int(clip_ctx.clip_size))
|
|
1432
1666
|
player.ammo = float(player.clip_size)
|
|
1433
1667
|
player.weapon_reset_latch = 0
|
|
1434
1668
|
player.reload_active = False
|
|
@@ -1548,8 +1782,7 @@ def _perk_update_man_bomb(player: PlayerState, dt: float, state: GameplayState)
|
|
|
1548
1782
|
if player.man_bomb_timer <= state.perk_intervals.man_bomb:
|
|
1549
1783
|
return
|
|
1550
1784
|
|
|
1551
|
-
owner_id =
|
|
1552
|
-
state.bonus_spawn_guard = True
|
|
1785
|
+
owner_id = _owner_id_for_player_projectiles(state, player.index)
|
|
1553
1786
|
for idx in range(8):
|
|
1554
1787
|
type_id = ProjectileTypeId.ION_MINIGUN if ((idx & 1) == 0) else ProjectileTypeId.ION_RIFLE
|
|
1555
1788
|
angle = (float(state.rng.rand() % 50) * 0.01) + float(idx) * (math.pi / 4.0) - 0.25
|
|
@@ -1561,7 +1794,6 @@ def _perk_update_man_bomb(player: PlayerState, dt: float, state: GameplayState)
|
|
|
1561
1794
|
owner_id=owner_id,
|
|
1562
1795
|
base_damage=_projectile_meta_for_type_id(type_id),
|
|
1563
1796
|
)
|
|
1564
|
-
state.bonus_spawn_guard = False
|
|
1565
1797
|
state.sfx_queue.append("sfx_explosion_small")
|
|
1566
1798
|
|
|
1567
1799
|
player.man_bomb_timer -= state.perk_intervals.man_bomb
|
|
@@ -1573,7 +1805,7 @@ def _perk_update_hot_tempered(player: PlayerState, dt: float, state: GameplaySta
|
|
|
1573
1805
|
if player.hot_tempered_timer <= state.perk_intervals.hot_tempered:
|
|
1574
1806
|
return
|
|
1575
1807
|
|
|
1576
|
-
owner_id = _owner_id_for_player(player.index)
|
|
1808
|
+
owner_id = _owner_id_for_player(player.index) if state.friendly_fire_enabled else -100
|
|
1577
1809
|
state.bonus_spawn_guard = True
|
|
1578
1810
|
for idx in range(8):
|
|
1579
1811
|
type_id = ProjectileTypeId.PLASMA_MINIGUN if ((idx & 1) == 0) else ProjectileTypeId.PLASMA_RIFLE
|
|
@@ -1598,13 +1830,27 @@ def _perk_update_fire_cough(player: PlayerState, dt: float, state: GameplayState
|
|
|
1598
1830
|
if player.fire_cough_timer <= state.perk_intervals.fire_cough:
|
|
1599
1831
|
return
|
|
1600
1832
|
|
|
1601
|
-
owner_id =
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1833
|
+
owner_id = _owner_id_for_player_projectiles(state, player.index)
|
|
1834
|
+
state.sfx_queue.append("sfx_autorifle_fire")
|
|
1835
|
+
state.sfx_queue.append("sfx_plasmaminigun_fire")
|
|
1836
|
+
|
|
1837
|
+
aim_heading = float(player.aim_heading)
|
|
1838
|
+
muzzle_dir = (aim_heading - math.pi / 2.0) - 0.150915
|
|
1839
|
+
muzzle_x = player.pos_x + math.cos(muzzle_dir) * 16.0
|
|
1840
|
+
muzzle_y = player.pos_y + math.sin(muzzle_dir) * 16.0
|
|
1841
|
+
|
|
1842
|
+
aim_x = float(player.aim_x)
|
|
1843
|
+
aim_y = float(player.aim_y)
|
|
1844
|
+
dx = aim_x - float(player.pos_x)
|
|
1845
|
+
dy = aim_y - float(player.pos_y)
|
|
1846
|
+
dist = math.hypot(dx, dy)
|
|
1847
|
+
max_offset = dist * float(player.spread_heat) * 0.5
|
|
1848
|
+
dir_angle = float(int(state.rng.rand()) & 0x1FF) * (math.tau / 512.0)
|
|
1849
|
+
mag = float(int(state.rng.rand()) & 0x1FF) * (1.0 / 512.0)
|
|
1850
|
+
offset = max_offset * mag
|
|
1851
|
+
jitter_x = aim_x + math.cos(dir_angle) * offset
|
|
1852
|
+
jitter_y = aim_y + math.sin(dir_angle) * offset
|
|
1853
|
+
angle = math.atan2(jitter_y - float(player.pos_y), jitter_x - float(player.pos_x)) + math.pi / 2.0
|
|
1608
1854
|
state.projectiles.spawn(
|
|
1609
1855
|
pos_x=muzzle_x,
|
|
1610
1856
|
pos_y=muzzle_y,
|
|
@@ -1614,10 +1860,66 @@ def _perk_update_fire_cough(player: PlayerState, dt: float, state: GameplayState
|
|
|
1614
1860
|
base_damage=_projectile_meta_for_type_id(ProjectileTypeId.FIRE_BULLETS),
|
|
1615
1861
|
)
|
|
1616
1862
|
|
|
1863
|
+
vel_x = math.cos(aim_heading) * 25.0
|
|
1864
|
+
vel_y = math.sin(aim_heading) * 25.0
|
|
1865
|
+
sprite_id = state.sprite_effects.spawn(pos_x=muzzle_x, pos_y=muzzle_y, vel_x=vel_x, vel_y=vel_y, scale=1.0)
|
|
1866
|
+
sprite = state.sprite_effects.entries[int(sprite_id)]
|
|
1867
|
+
sprite.color_r = 0.5
|
|
1868
|
+
sprite.color_g = 0.5
|
|
1869
|
+
sprite.color_b = 0.5
|
|
1870
|
+
sprite.color_a = 0.413
|
|
1871
|
+
|
|
1617
1872
|
player.fire_cough_timer -= state.perk_intervals.fire_cough
|
|
1618
1873
|
state.perk_intervals.fire_cough = float(state.rng.rand() % 4) + 2.0
|
|
1619
1874
|
|
|
1620
1875
|
|
|
1876
|
+
@dataclass(slots=True)
|
|
1877
|
+
class _PlayerPerkTickCtx:
|
|
1878
|
+
state: GameplayState
|
|
1879
|
+
player: PlayerState
|
|
1880
|
+
dt: float
|
|
1881
|
+
stationary: bool
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
_PlayerPerkTickStep = Callable[[_PlayerPerkTickCtx], None]
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def _player_perk_tick_man_bomb(ctx: _PlayerPerkTickCtx) -> None:
|
|
1888
|
+
if ctx.stationary and perk_active(ctx.player, PerkId.MAN_BOMB):
|
|
1889
|
+
_perk_update_man_bomb(ctx.player, ctx.dt, ctx.state)
|
|
1890
|
+
else:
|
|
1891
|
+
ctx.player.man_bomb_timer = 0.0
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def _player_perk_tick_living_fortress(ctx: _PlayerPerkTickCtx) -> None:
|
|
1895
|
+
if ctx.stationary and perk_active(ctx.player, PerkId.LIVING_FORTRESS):
|
|
1896
|
+
ctx.player.living_fortress_timer = min(30.0, ctx.player.living_fortress_timer + ctx.dt)
|
|
1897
|
+
else:
|
|
1898
|
+
ctx.player.living_fortress_timer = 0.0
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def _player_perk_tick_fire_cough(ctx: _PlayerPerkTickCtx) -> None:
|
|
1902
|
+
if perk_active(ctx.player, PerkId.FIRE_CAUGH):
|
|
1903
|
+
_perk_update_fire_cough(ctx.player, ctx.dt, ctx.state)
|
|
1904
|
+
else:
|
|
1905
|
+
ctx.player.fire_cough_timer = 0.0
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def _player_perk_tick_hot_tempered(ctx: _PlayerPerkTickCtx) -> None:
|
|
1909
|
+
if perk_active(ctx.player, PerkId.HOT_TEMPERED):
|
|
1910
|
+
_perk_update_hot_tempered(ctx.player, ctx.dt, ctx.state)
|
|
1911
|
+
else:
|
|
1912
|
+
ctx.player.hot_tempered_timer = 0.0
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
_PLAYER_PERK_TICK_STEPS: tuple[_PlayerPerkTickStep, ...] = (
|
|
1916
|
+
_player_perk_tick_man_bomb,
|
|
1917
|
+
_player_perk_tick_living_fortress,
|
|
1918
|
+
_player_perk_tick_fire_cough,
|
|
1919
|
+
_player_perk_tick_hot_tempered,
|
|
1920
|
+
)
|
|
1921
|
+
|
|
1922
|
+
|
|
1621
1923
|
def player_fire_weapon(
|
|
1622
1924
|
player: PlayerState,
|
|
1623
1925
|
input_state: PlayerInput,
|
|
@@ -1642,26 +1944,25 @@ def player_fire_weapon(
|
|
|
1642
1944
|
ammo_cost = 1.0
|
|
1643
1945
|
is_fire_bullets = float(player.fire_bullets_timer) > 0.0
|
|
1644
1946
|
if player.reload_timer > 0.0:
|
|
1645
|
-
if player.
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
return
|
|
1947
|
+
if player.experience <= 0:
|
|
1948
|
+
return
|
|
1949
|
+
if perk_active(player, PerkId.REGRESSION_BULLETS):
|
|
1950
|
+
firing_during_reload = True
|
|
1951
|
+
ammo_class = int(weapon.ammo_class) if weapon.ammo_class is not None else 0
|
|
1952
|
+
|
|
1953
|
+
reload_time = float(weapon.reload_time) if weapon.reload_time is not None else 0.0
|
|
1954
|
+
factor = 4.0 if ammo_class == 1 else 200.0
|
|
1955
|
+
player.experience = int(float(player.experience) - reload_time * factor)
|
|
1956
|
+
if player.experience < 0:
|
|
1957
|
+
player.experience = 0
|
|
1958
|
+
elif perk_active(player, PerkId.AMMUNITION_WITHIN):
|
|
1959
|
+
firing_during_reload = True
|
|
1960
|
+
ammo_class = int(weapon.ammo_class) if weapon.ammo_class is not None else 0
|
|
1961
|
+
|
|
1962
|
+
from .player_damage import player_take_damage
|
|
1963
|
+
|
|
1964
|
+
cost = 0.15 if ammo_class == 1 else 1.0
|
|
1965
|
+
player_take_damage(state, player, cost, dt=dt, rand=state.rng.rand)
|
|
1665
1966
|
else:
|
|
1666
1967
|
return
|
|
1667
1968
|
|
|
@@ -1997,8 +2298,8 @@ def player_update(
|
|
|
1997
2298
|
if perk_active(player, PerkId.ALTERNATE_WEAPON):
|
|
1998
2299
|
speed *= 0.8
|
|
1999
2300
|
|
|
2000
|
-
player.pos_x =
|
|
2001
|
-
player.pos_y =
|
|
2301
|
+
player.pos_x = clamp(player.pos_x + move_x * speed * dt, 0.0, float(world_size))
|
|
2302
|
+
player.pos_y = clamp(player.pos_y + move_y * speed * dt, 0.0, float(world_size))
|
|
2002
2303
|
|
|
2003
2304
|
player.move_phase += dt * player.move_speed * 19.0
|
|
2004
2305
|
|
|
@@ -2007,25 +2308,9 @@ def player_update(
|
|
|
2007
2308
|
if stationary and perk_active(player, PerkId.STATIONARY_RELOADER):
|
|
2008
2309
|
reload_scale = 3.0
|
|
2009
2310
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
player.man_bomb_timer = 0.0
|
|
2014
|
-
|
|
2015
|
-
if stationary and perk_active(player, PerkId.LIVING_FORTRESS):
|
|
2016
|
-
player.living_fortress_timer = min(30.0, player.living_fortress_timer + dt)
|
|
2017
|
-
else:
|
|
2018
|
-
player.living_fortress_timer = 0.0
|
|
2019
|
-
|
|
2020
|
-
if perk_active(player, PerkId.FIRE_CAUGH):
|
|
2021
|
-
_perk_update_fire_cough(player, dt, state)
|
|
2022
|
-
else:
|
|
2023
|
-
player.fire_cough_timer = 0.0
|
|
2024
|
-
|
|
2025
|
-
if perk_active(player, PerkId.HOT_TEMPERED):
|
|
2026
|
-
_perk_update_hot_tempered(player, dt, state)
|
|
2027
|
-
else:
|
|
2028
|
-
player.hot_tempered_timer = 0.0
|
|
2311
|
+
perk_ctx = _PlayerPerkTickCtx(state=state, player=player, dt=dt, stationary=stationary)
|
|
2312
|
+
for step in _PLAYER_PERK_TICK_STEPS:
|
|
2313
|
+
step(perk_ctx)
|
|
2029
2314
|
|
|
2030
2315
|
# Reload + reload perks.
|
|
2031
2316
|
if perk_active(player, PerkId.ANXIOUS_LOADER) and input_state.fire_pressed and player.reload_timer > 0.0:
|
|
@@ -2049,7 +2334,7 @@ def player_update(
|
|
|
2049
2334
|
count=count,
|
|
2050
2335
|
angle_offset=0.1,
|
|
2051
2336
|
type_id=ProjectileTypeId.PLASMA_MINIGUN,
|
|
2052
|
-
owner_id=
|
|
2337
|
+
owner_id=_owner_id_for_player_projectiles(state, player.index),
|
|
2053
2338
|
)
|
|
2054
2339
|
state.bonus_spawn_guard = False
|
|
2055
2340
|
state.sfx_queue.append("sfx_explosion_small")
|
|
@@ -2085,309 +2370,376 @@ def player_update(
|
|
|
2085
2370
|
player.move_phase += 14.0
|
|
2086
2371
|
|
|
2087
2372
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
amount: int
|
|
2094
|
-
origin: _HasPos | None
|
|
2095
|
-
creatures: list[Damageable] | None
|
|
2096
|
-
players: list[PlayerState] | None
|
|
2097
|
-
apply_creature_damage: CreatureDamageApplier | None
|
|
2098
|
-
detail_preset: int
|
|
2099
|
-
|
|
2100
|
-
|
|
2373
|
+
@dataclass(slots=True)
|
|
2374
|
+
class _BonusApplyCtx:
|
|
2375
|
+
state: GameplayState
|
|
2376
|
+
player: PlayerState
|
|
2377
|
+
bonus_id: BonusId
|
|
2378
|
+
amount: int
|
|
2379
|
+
origin: _HasPos | None
|
|
2380
|
+
creatures: list[Damageable] | None
|
|
2381
|
+
players: list[PlayerState] | None
|
|
2382
|
+
apply_creature_damage: CreatureDamageApplier | None
|
|
2383
|
+
detail_preset: int
|
|
2384
|
+
economist_multiplier: float
|
|
2385
|
+
label: str
|
|
2386
|
+
icon_id: int
|
|
2387
|
+
|
|
2388
|
+
def register_global(self, timer_key: str) -> None:
|
|
2389
|
+
self.state.bonus_hud.register(
|
|
2390
|
+
self.bonus_id,
|
|
2391
|
+
label=self.label,
|
|
2392
|
+
icon_id=self.icon_id,
|
|
2393
|
+
timer_ref=_TimerRef("global", str(timer_key)),
|
|
2394
|
+
)
|
|
2101
2395
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2396
|
+
def register_player(self, timer_key: str) -> None:
|
|
2397
|
+
if self.players is not None and len(self.players) > 1:
|
|
2398
|
+
self.state.bonus_hud.register(
|
|
2399
|
+
self.bonus_id,
|
|
2400
|
+
label=self.label,
|
|
2401
|
+
icon_id=self.icon_id,
|
|
2402
|
+
timer_ref=_TimerRef("player", str(timer_key), player_index=0),
|
|
2403
|
+
timer_ref_alt=_TimerRef("player", str(timer_key), player_index=1),
|
|
2404
|
+
)
|
|
2405
|
+
else:
|
|
2406
|
+
self.state.bonus_hud.register(
|
|
2407
|
+
self.bonus_id,
|
|
2408
|
+
label=self.label,
|
|
2409
|
+
icon_id=self.icon_id,
|
|
2410
|
+
timer_ref=_TimerRef("player", str(timer_key), player_index=int(self.player.index)),
|
|
2411
|
+
)
|
|
2107
2412
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
return
|
|
2413
|
+
def origin_pos(self) -> _HasPos:
|
|
2414
|
+
return self.origin or self.player
|
|
2111
2415
|
|
|
2112
|
-
economist_multiplier = 1.0 + 0.5 * float(perk_count_get(player, PerkId.BONUS_ECONOMIST))
|
|
2113
2416
|
|
|
2114
|
-
|
|
2115
|
-
label = meta.name
|
|
2417
|
+
_BonusApplyHandler = Callable[[_BonusApplyCtx], None]
|
|
2116
2418
|
|
|
2117
|
-
def _register_global(timer_key: str) -> None:
|
|
2118
|
-
state.bonus_hud.register(
|
|
2119
|
-
bonus_id,
|
|
2120
|
-
label=label,
|
|
2121
|
-
icon_id=icon_id,
|
|
2122
|
-
timer_ref=_TimerRef("global", timer_key),
|
|
2123
|
-
)
|
|
2124
2419
|
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
state.bonus_hud.register(
|
|
2128
|
-
bonus_id,
|
|
2129
|
-
label=label,
|
|
2130
|
-
icon_id=icon_id,
|
|
2131
|
-
timer_ref=_TimerRef("player", timer_key, player_index=0),
|
|
2132
|
-
timer_ref_alt=_TimerRef("player", timer_key, player_index=1),
|
|
2133
|
-
)
|
|
2134
|
-
else:
|
|
2135
|
-
state.bonus_hud.register(
|
|
2136
|
-
bonus_id,
|
|
2137
|
-
label=label,
|
|
2138
|
-
icon_id=icon_id,
|
|
2139
|
-
timer_ref=_TimerRef("player", timer_key, player_index=int(player.index)),
|
|
2140
|
-
)
|
|
2420
|
+
def _bonus_apply_points(ctx: _BonusApplyCtx) -> None:
|
|
2421
|
+
award_experience(ctx.state, ctx.player, int(ctx.amount))
|
|
2141
2422
|
|
|
2142
|
-
if bonus_id == BonusId.ENERGIZER:
|
|
2143
|
-
old = float(state.bonuses.energizer)
|
|
2144
|
-
if old <= 0.0:
|
|
2145
|
-
_register_global("energizer")
|
|
2146
|
-
state.bonuses.energizer = float(old + float(amount) * economist_multiplier)
|
|
2147
|
-
return
|
|
2148
2423
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
player.weapon_reset_latch = 0
|
|
2155
|
-
player.shot_cooldown = 0.0
|
|
2156
|
-
player.reload_active = False
|
|
2157
|
-
player.reload_timer = 0.0
|
|
2158
|
-
player.reload_timer_max = 0.0
|
|
2159
|
-
player.ammo = float(player.clip_size)
|
|
2160
|
-
return
|
|
2424
|
+
def _bonus_apply_energizer(ctx: _BonusApplyCtx) -> None:
|
|
2425
|
+
old = float(ctx.state.bonuses.energizer)
|
|
2426
|
+
if old <= 0.0:
|
|
2427
|
+
ctx.register_global("energizer")
|
|
2428
|
+
ctx.state.bonuses.energizer = float(old + float(ctx.amount) * ctx.economist_multiplier)
|
|
2161
2429
|
|
|
2162
|
-
if bonus_id == BonusId.DOUBLE_EXPERIENCE:
|
|
2163
|
-
old = float(state.bonuses.double_experience)
|
|
2164
|
-
if old <= 0.0:
|
|
2165
|
-
_register_global("double_experience")
|
|
2166
|
-
state.bonuses.double_experience = float(old + float(amount) * economist_multiplier)
|
|
2167
|
-
return
|
|
2168
2430
|
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
target.reload_timer_max = 0.0
|
|
2181
|
-
return
|
|
2431
|
+
def _bonus_apply_weapon_power_up(ctx: _BonusApplyCtx) -> None:
|
|
2432
|
+
old = float(ctx.state.bonuses.weapon_power_up)
|
|
2433
|
+
if old <= 0.0:
|
|
2434
|
+
ctx.register_global("weapon_power_up")
|
|
2435
|
+
ctx.state.bonuses.weapon_power_up = float(old + float(ctx.amount) * ctx.economist_multiplier)
|
|
2436
|
+
ctx.player.weapon_reset_latch = 0
|
|
2437
|
+
ctx.player.shot_cooldown = 0.0
|
|
2438
|
+
ctx.player.reload_active = False
|
|
2439
|
+
ctx.player.reload_timer = 0.0
|
|
2440
|
+
ctx.player.reload_timer_max = 0.0
|
|
2441
|
+
ctx.player.ammo = float(ctx.player.clip_size)
|
|
2182
2442
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2443
|
+
|
|
2444
|
+
def _bonus_apply_double_experience(ctx: _BonusApplyCtx) -> None:
|
|
2445
|
+
old = float(ctx.state.bonuses.double_experience)
|
|
2446
|
+
if old <= 0.0:
|
|
2447
|
+
ctx.register_global("double_experience")
|
|
2448
|
+
ctx.state.bonuses.double_experience = float(old + float(ctx.amount) * ctx.economist_multiplier)
|
|
2449
|
+
|
|
2450
|
+
|
|
2451
|
+
def _bonus_apply_reflex_boost(ctx: _BonusApplyCtx) -> None:
|
|
2452
|
+
old = float(ctx.state.bonuses.reflex_boost)
|
|
2453
|
+
if old <= 0.0:
|
|
2454
|
+
ctx.register_global("reflex_boost")
|
|
2455
|
+
ctx.state.bonuses.reflex_boost = float(old + float(ctx.amount) * ctx.economist_multiplier)
|
|
2456
|
+
|
|
2457
|
+
targets = ctx.players if ctx.players is not None else [ctx.player]
|
|
2458
|
+
for target in targets:
|
|
2459
|
+
target.ammo = float(target.clip_size)
|
|
2460
|
+
target.reload_active = False
|
|
2461
|
+
target.reload_timer = 0.0
|
|
2462
|
+
target.reload_timer_max = 0.0
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
def _bonus_apply_freeze(ctx: _BonusApplyCtx) -> None:
|
|
2466
|
+
old = float(ctx.state.bonuses.freeze)
|
|
2467
|
+
if old <= 0.0:
|
|
2468
|
+
ctx.register_global("freeze")
|
|
2469
|
+
ctx.state.bonuses.freeze = float(old + float(ctx.amount) * ctx.economist_multiplier)
|
|
2470
|
+
|
|
2471
|
+
creatures = ctx.creatures
|
|
2472
|
+
if creatures:
|
|
2473
|
+
rand = ctx.state.rng.rand
|
|
2474
|
+
for creature in creatures:
|
|
2475
|
+
active = getattr(creature, "active", True)
|
|
2476
|
+
if not bool(active):
|
|
2477
|
+
continue
|
|
2478
|
+
if float(getattr(creature, "hp", 0.0)) > 0.0:
|
|
2479
|
+
continue
|
|
2480
|
+
pos_x = float(getattr(creature, "x", 0.0))
|
|
2481
|
+
pos_y = float(getattr(creature, "y", 0.0))
|
|
2482
|
+
for _ in range(8):
|
|
2207
2483
|
angle = float(int(rand()) % 0x264) * 0.01
|
|
2208
|
-
state.effects.
|
|
2484
|
+
ctx.state.effects.spawn_freeze_shard(
|
|
2209
2485
|
pos_x=pos_x,
|
|
2210
2486
|
pos_y=pos_y,
|
|
2211
2487
|
angle=angle,
|
|
2212
2488
|
rand=rand,
|
|
2213
|
-
detail_preset=int(detail_preset),
|
|
2489
|
+
detail_preset=int(ctx.detail_preset),
|
|
2214
2490
|
)
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2491
|
+
angle = float(int(rand()) % 0x264) * 0.01
|
|
2492
|
+
ctx.state.effects.spawn_freeze_shatter(
|
|
2493
|
+
pos_x=pos_x,
|
|
2494
|
+
pos_y=pos_y,
|
|
2495
|
+
angle=angle,
|
|
2496
|
+
rand=rand,
|
|
2497
|
+
detail_preset=int(ctx.detail_preset),
|
|
2498
|
+
)
|
|
2499
|
+
if hasattr(creature, "active"):
|
|
2500
|
+
setattr(creature, "active", False)
|
|
2219
2501
|
|
|
2220
|
-
|
|
2221
|
-
should_register = float(player.shield_timer) <= 0.0
|
|
2222
|
-
if players is not None and len(players) > 1:
|
|
2223
|
-
should_register = float(players[0].shield_timer) <= 0.0 and float(players[1].shield_timer) <= 0.0
|
|
2224
|
-
if should_register:
|
|
2225
|
-
_register_player("shield_timer")
|
|
2226
|
-
player.shield_timer = float(player.shield_timer + float(amount) * economist_multiplier)
|
|
2227
|
-
return
|
|
2502
|
+
ctx.state.sfx_queue.append("sfx_shockwave")
|
|
2228
2503
|
|
|
2229
|
-
if bonus_id == BonusId.SPEED:
|
|
2230
|
-
should_register = float(player.speed_bonus_timer) <= 0.0
|
|
2231
|
-
if players is not None and len(players) > 1:
|
|
2232
|
-
should_register = float(players[0].speed_bonus_timer) <= 0.0 and float(players[1].speed_bonus_timer) <= 0.0
|
|
2233
|
-
if should_register:
|
|
2234
|
-
_register_player("speed_bonus_timer")
|
|
2235
|
-
player.speed_bonus_timer = float(player.speed_bonus_timer + float(amount) * economist_multiplier)
|
|
2236
|
-
return
|
|
2237
2504
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
player.weapon_reset_latch = 0
|
|
2246
|
-
player.shot_cooldown = 0.0
|
|
2247
|
-
player.reload_active = False
|
|
2248
|
-
player.reload_timer = 0.0
|
|
2249
|
-
player.reload_timer_max = 0.0
|
|
2250
|
-
player.ammo = float(player.clip_size)
|
|
2251
|
-
return
|
|
2505
|
+
def _bonus_apply_shield(ctx: _BonusApplyCtx) -> None:
|
|
2506
|
+
should_register = float(ctx.player.shield_timer) <= 0.0
|
|
2507
|
+
if ctx.players is not None and len(ctx.players) > 1:
|
|
2508
|
+
should_register = float(ctx.players[0].shield_timer) <= 0.0 and float(ctx.players[1].shield_timer) <= 0.0
|
|
2509
|
+
if should_register:
|
|
2510
|
+
ctx.register_player("shield_timer")
|
|
2511
|
+
ctx.player.shield_timer = float(ctx.player.shield_timer + float(ctx.amount) * ctx.economist_multiplier)
|
|
2252
2512
|
|
|
2253
|
-
if bonus_id == BonusId.SHOCK_CHAIN:
|
|
2254
|
-
if creatures:
|
|
2255
|
-
origin_pos = origin or player
|
|
2256
|
-
best_idx: int | None = None
|
|
2257
|
-
best_dist = 0.0
|
|
2258
|
-
for idx, creature in enumerate(creatures):
|
|
2259
|
-
if creature.hp <= 0.0:
|
|
2260
|
-
continue
|
|
2261
|
-
d = _distance_sq(float(origin_pos.pos_x), float(origin_pos.pos_y), creature.x, creature.y)
|
|
2262
|
-
if best_idx is None or d < best_dist:
|
|
2263
|
-
best_idx = idx
|
|
2264
|
-
best_dist = d
|
|
2265
|
-
if best_idx is not None:
|
|
2266
|
-
target = creatures[best_idx]
|
|
2267
|
-
dx = target.x - float(origin_pos.pos_x)
|
|
2268
|
-
dy = target.y - float(origin_pos.pos_y)
|
|
2269
|
-
angle = math.atan2(dy, dx) + math.pi / 2.0
|
|
2270
|
-
owner_id = _owner_id_for_player(player.index) if state.friendly_fire_enabled else -100
|
|
2271
2513
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2514
|
+
def _bonus_apply_speed(ctx: _BonusApplyCtx) -> None:
|
|
2515
|
+
should_register = float(ctx.player.speed_bonus_timer) <= 0.0
|
|
2516
|
+
if ctx.players is not None and len(ctx.players) > 1:
|
|
2517
|
+
should_register = (
|
|
2518
|
+
float(ctx.players[0].speed_bonus_timer) <= 0.0 and float(ctx.players[1].speed_bonus_timer) <= 0.0
|
|
2519
|
+
)
|
|
2520
|
+
if should_register:
|
|
2521
|
+
ctx.register_player("speed_bonus_timer")
|
|
2522
|
+
ctx.player.speed_bonus_timer = float(ctx.player.speed_bonus_timer + float(ctx.amount) * ctx.economist_multiplier)
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
def _bonus_apply_fire_bullets(ctx: _BonusApplyCtx) -> None:
|
|
2526
|
+
should_register = float(ctx.player.fire_bullets_timer) <= 0.0
|
|
2527
|
+
if ctx.players is not None and len(ctx.players) > 1:
|
|
2528
|
+
should_register = float(ctx.players[0].fire_bullets_timer) <= 0.0 and float(ctx.players[1].fire_bullets_timer) <= 0.0
|
|
2529
|
+
if should_register:
|
|
2530
|
+
ctx.register_player("fire_bullets_timer")
|
|
2531
|
+
ctx.player.fire_bullets_timer = float(ctx.player.fire_bullets_timer + float(ctx.amount) * ctx.economist_multiplier)
|
|
2532
|
+
ctx.player.weapon_reset_latch = 0
|
|
2533
|
+
ctx.player.shot_cooldown = 0.0
|
|
2534
|
+
ctx.player.reload_active = False
|
|
2535
|
+
ctx.player.reload_timer = 0.0
|
|
2536
|
+
ctx.player.reload_timer_max = 0.0
|
|
2537
|
+
ctx.player.ammo = float(ctx.player.clip_size)
|
|
2538
|
+
|
|
2539
|
+
|
|
2540
|
+
def _bonus_apply_shock_chain(ctx: _BonusApplyCtx) -> None:
|
|
2541
|
+
creatures = ctx.creatures
|
|
2542
|
+
if not creatures:
|
|
2283
2543
|
return
|
|
2284
2544
|
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2545
|
+
origin_pos = ctx.origin_pos()
|
|
2546
|
+
best_idx: int | None = None
|
|
2547
|
+
best_dist = 0.0
|
|
2548
|
+
for idx, creature in enumerate(creatures):
|
|
2549
|
+
if creature.hp <= 0.0:
|
|
2550
|
+
continue
|
|
2551
|
+
d = distance_sq(float(origin_pos.pos_x), float(origin_pos.pos_y), creature.x, creature.y)
|
|
2552
|
+
if best_idx is None or d < best_dist:
|
|
2553
|
+
best_idx = idx
|
|
2554
|
+
best_dist = d
|
|
2555
|
+
if best_idx is None:
|
|
2296
2556
|
return
|
|
2297
2557
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2558
|
+
target = creatures[best_idx]
|
|
2559
|
+
dx = target.x - float(origin_pos.pos_x)
|
|
2560
|
+
dy = target.y - float(origin_pos.pos_y)
|
|
2561
|
+
angle = math.atan2(dy, dx) + math.pi / 2.0
|
|
2562
|
+
owner_id = _owner_id_for_player(ctx.player.index) if ctx.state.friendly_fire_enabled else -100
|
|
2563
|
+
|
|
2564
|
+
ctx.state.bonus_spawn_guard = True
|
|
2565
|
+
ctx.state.shock_chain_links_left = 0x20
|
|
2566
|
+
ctx.state.shock_chain_projectile_id = ctx.state.projectiles.spawn(
|
|
2567
|
+
pos_x=float(origin_pos.pos_x),
|
|
2568
|
+
pos_y=float(origin_pos.pos_y),
|
|
2569
|
+
angle=angle,
|
|
2570
|
+
type_id=int(ProjectileTypeId.ION_RIFLE),
|
|
2571
|
+
owner_id=int(owner_id),
|
|
2572
|
+
base_damage=_projectile_meta_for_type_id(int(ProjectileTypeId.ION_RIFLE)),
|
|
2573
|
+
)
|
|
2574
|
+
ctx.state.bonus_spawn_guard = False
|
|
2575
|
+
|
|
2576
|
+
|
|
2577
|
+
def _bonus_apply_weapon(ctx: _BonusApplyCtx) -> None:
|
|
2578
|
+
weapon_id = int(ctx.amount)
|
|
2579
|
+
if perk_active(ctx.player, PerkId.ALTERNATE_WEAPON) and ctx.player.alt_weapon_id is None:
|
|
2580
|
+
ctx.player.alt_weapon_id = int(ctx.player.weapon_id)
|
|
2581
|
+
ctx.player.alt_clip_size = int(ctx.player.clip_size)
|
|
2582
|
+
ctx.player.alt_ammo = float(ctx.player.ammo)
|
|
2583
|
+
ctx.player.alt_reload_active = bool(ctx.player.reload_active)
|
|
2584
|
+
ctx.player.alt_reload_timer = float(ctx.player.reload_timer)
|
|
2585
|
+
ctx.player.alt_shot_cooldown = float(ctx.player.shot_cooldown)
|
|
2586
|
+
ctx.player.alt_reload_timer_max = float(ctx.player.reload_timer_max)
|
|
2587
|
+
weapon_assign_player(ctx.player, weapon_id, state=ctx.state)
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
def _bonus_apply_fireblast(ctx: _BonusApplyCtx) -> None:
|
|
2591
|
+
origin_pos = ctx.origin_pos()
|
|
2592
|
+
owner_id = _owner_id_for_player(ctx.player.index) if ctx.state.friendly_fire_enabled else -100
|
|
2593
|
+
ctx.state.bonus_spawn_guard = True
|
|
2594
|
+
_spawn_projectile_ring(
|
|
2595
|
+
ctx.state,
|
|
2596
|
+
origin_pos,
|
|
2597
|
+
count=16,
|
|
2598
|
+
angle_offset=0.0,
|
|
2599
|
+
type_id=ProjectileTypeId.PLASMA_RIFLE,
|
|
2600
|
+
owner_id=int(owner_id),
|
|
2601
|
+
)
|
|
2602
|
+
ctx.state.bonus_spawn_guard = False
|
|
2603
|
+
ctx.state.sfx_queue.append("sfx_explosion_medium")
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
def _bonus_apply_nuke(ctx: _BonusApplyCtx) -> None:
|
|
2607
|
+
# `bonus_apply` (crimsonland.exe @ 0x00409890) starts screen shake via:
|
|
2608
|
+
# camera_shake_pulses = 0x14;
|
|
2609
|
+
# camera_shake_timer = 0.2f;
|
|
2610
|
+
ctx.state.camera_shake_pulses = 0x14
|
|
2611
|
+
ctx.state.camera_shake_timer = 0.2
|
|
2612
|
+
|
|
2613
|
+
origin_pos = ctx.origin_pos()
|
|
2614
|
+
ox = float(origin_pos.pos_x)
|
|
2615
|
+
oy = float(origin_pos.pos_y)
|
|
2616
|
+
rand = ctx.state.rng.rand
|
|
2617
|
+
|
|
2618
|
+
bullet_count = int(rand()) & 3
|
|
2619
|
+
bullet_count += 4
|
|
2620
|
+
assault_meta = _projectile_meta_for_type_id(int(ProjectileTypeId.ASSAULT_RIFLE))
|
|
2621
|
+
for _ in range(bullet_count):
|
|
2622
|
+
angle = float(int(rand()) % 0x274) * 0.01
|
|
2623
|
+
proj_id = ctx.state.projectiles.spawn(
|
|
2624
|
+
pos_x=ox,
|
|
2625
|
+
pos_y=oy,
|
|
2626
|
+
angle=float(angle),
|
|
2627
|
+
type_id=int(ProjectileTypeId.ASSAULT_RIFLE),
|
|
2628
|
+
owner_id=-100,
|
|
2629
|
+
base_damage=assault_meta,
|
|
2309
2630
|
)
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
state.camera_shake_pulses = 0x14
|
|
2319
|
-
state.camera_shake_timer = 0.2
|
|
2320
|
-
|
|
2321
|
-
origin_pos = origin or player
|
|
2322
|
-
ox = float(origin_pos.pos_x)
|
|
2323
|
-
oy = float(origin_pos.pos_y)
|
|
2324
|
-
rand = state.rng.rand
|
|
2325
|
-
|
|
2326
|
-
bullet_count = int(rand()) & 3
|
|
2327
|
-
bullet_count += 4
|
|
2328
|
-
assault_meta = _projectile_meta_for_type_id(int(ProjectileTypeId.ASSAULT_RIFLE))
|
|
2329
|
-
for _ in range(bullet_count):
|
|
2330
|
-
angle = float(int(rand()) % 0x274) * 0.01
|
|
2331
|
-
proj_id = state.projectiles.spawn(
|
|
2332
|
-
pos_x=ox,
|
|
2333
|
-
pos_y=oy,
|
|
2334
|
-
angle=float(angle),
|
|
2335
|
-
type_id=int(ProjectileTypeId.ASSAULT_RIFLE),
|
|
2336
|
-
owner_id=-100,
|
|
2337
|
-
base_damage=assault_meta,
|
|
2338
|
-
)
|
|
2339
|
-
if proj_id != -1:
|
|
2340
|
-
speed_scale = float(int(rand()) % 0x32) * 0.01 + 0.5
|
|
2341
|
-
state.projectiles.entries[proj_id].speed_scale *= float(speed_scale)
|
|
2342
|
-
|
|
2343
|
-
minigun_meta = _projectile_meta_for_type_id(int(ProjectileTypeId.MEAN_MINIGUN))
|
|
2344
|
-
for _ in range(2):
|
|
2345
|
-
angle = float(int(rand()) % 0x274) * 0.01
|
|
2346
|
-
state.projectiles.spawn(
|
|
2347
|
-
pos_x=ox,
|
|
2348
|
-
pos_y=oy,
|
|
2349
|
-
angle=float(angle),
|
|
2350
|
-
type_id=int(ProjectileTypeId.MEAN_MINIGUN),
|
|
2351
|
-
owner_id=-100,
|
|
2352
|
-
base_damage=minigun_meta,
|
|
2353
|
-
)
|
|
2354
|
-
|
|
2355
|
-
state.effects.spawn_explosion_burst(
|
|
2631
|
+
if proj_id != -1:
|
|
2632
|
+
speed_scale = float(int(rand()) % 0x32) * 0.01 + 0.5
|
|
2633
|
+
ctx.state.projectiles.entries[proj_id].speed_scale *= float(speed_scale)
|
|
2634
|
+
|
|
2635
|
+
minigun_meta = _projectile_meta_for_type_id(int(ProjectileTypeId.MEAN_MINIGUN))
|
|
2636
|
+
for _ in range(2):
|
|
2637
|
+
angle = float(int(rand()) % 0x274) * 0.01
|
|
2638
|
+
ctx.state.projectiles.spawn(
|
|
2356
2639
|
pos_x=ox,
|
|
2357
2640
|
pos_y=oy,
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2641
|
+
angle=float(angle),
|
|
2642
|
+
type_id=int(ProjectileTypeId.MEAN_MINIGUN),
|
|
2643
|
+
owner_id=-100,
|
|
2644
|
+
base_damage=minigun_meta,
|
|
2361
2645
|
)
|
|
2362
2646
|
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2647
|
+
ctx.state.effects.spawn_explosion_burst(
|
|
2648
|
+
pos_x=ox,
|
|
2649
|
+
pos_y=oy,
|
|
2650
|
+
scale=1.0,
|
|
2651
|
+
rand=rand,
|
|
2652
|
+
detail_preset=int(ctx.detail_preset),
|
|
2653
|
+
)
|
|
2654
|
+
|
|
2655
|
+
creatures = ctx.creatures
|
|
2656
|
+
if creatures:
|
|
2657
|
+
prev_guard = bool(ctx.state.bonus_spawn_guard)
|
|
2658
|
+
ctx.state.bonus_spawn_guard = True
|
|
2659
|
+
for idx, creature in enumerate(creatures):
|
|
2660
|
+
if creature.hp <= 0.0:
|
|
2661
|
+
continue
|
|
2662
|
+
dx = float(creature.x) - ox
|
|
2663
|
+
dy = float(creature.y) - oy
|
|
2664
|
+
if abs(dx) > 256.0 or abs(dy) > 256.0:
|
|
2665
|
+
continue
|
|
2666
|
+
dist = math.hypot(dx, dy)
|
|
2667
|
+
if dist < 256.0:
|
|
2668
|
+
damage = (256.0 - dist) * 5.0
|
|
2669
|
+
if ctx.apply_creature_damage is not None:
|
|
2670
|
+
ctx.apply_creature_damage(
|
|
2671
|
+
int(idx),
|
|
2672
|
+
float(damage),
|
|
2673
|
+
3,
|
|
2674
|
+
0.0,
|
|
2675
|
+
0.0,
|
|
2676
|
+
_owner_id_for_player(ctx.player.index),
|
|
2677
|
+
)
|
|
2678
|
+
else:
|
|
2679
|
+
creature.hp -= float(damage)
|
|
2680
|
+
ctx.state.bonus_spawn_guard = prev_guard
|
|
2681
|
+
|
|
2682
|
+
ctx.state.sfx_queue.append("sfx_explosion_large")
|
|
2683
|
+
ctx.state.sfx_queue.append("sfx_shockwave")
|
|
2684
|
+
|
|
2685
|
+
|
|
2686
|
+
_BONUS_APPLY_HANDLERS: dict[BonusId, _BonusApplyHandler] = {
|
|
2687
|
+
BonusId.POINTS: _bonus_apply_points,
|
|
2688
|
+
BonusId.ENERGIZER: _bonus_apply_energizer,
|
|
2689
|
+
BonusId.WEAPON_POWER_UP: _bonus_apply_weapon_power_up,
|
|
2690
|
+
BonusId.DOUBLE_EXPERIENCE: _bonus_apply_double_experience,
|
|
2691
|
+
BonusId.REFLEX_BOOST: _bonus_apply_reflex_boost,
|
|
2692
|
+
BonusId.FREEZE: _bonus_apply_freeze,
|
|
2693
|
+
BonusId.SHIELD: _bonus_apply_shield,
|
|
2694
|
+
BonusId.SPEED: _bonus_apply_speed,
|
|
2695
|
+
BonusId.FIRE_BULLETS: _bonus_apply_fire_bullets,
|
|
2696
|
+
BonusId.SHOCK_CHAIN: _bonus_apply_shock_chain,
|
|
2697
|
+
BonusId.WEAPON: _bonus_apply_weapon,
|
|
2698
|
+
BonusId.FIREBLAST: _bonus_apply_fireblast,
|
|
2699
|
+
BonusId.NUKE: _bonus_apply_nuke,
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
def bonus_apply(
|
|
2704
|
+
state: GameplayState,
|
|
2705
|
+
player: PlayerState,
|
|
2706
|
+
bonus_id: BonusId,
|
|
2707
|
+
*,
|
|
2708
|
+
amount: int | None = None,
|
|
2709
|
+
origin: _HasPos | None = None,
|
|
2710
|
+
creatures: list[Damageable] | None = None,
|
|
2711
|
+
players: list[PlayerState] | None = None,
|
|
2712
|
+
apply_creature_damage: CreatureDamageApplier | None = None,
|
|
2713
|
+
detail_preset: int = 5,
|
|
2714
|
+
) -> None:
|
|
2715
|
+
"""Apply a bonus to player + global timers (subset of `bonus_apply`)."""
|
|
2716
|
+
|
|
2717
|
+
meta = BONUS_BY_ID.get(int(bonus_id))
|
|
2718
|
+
if meta is None:
|
|
2390
2719
|
return
|
|
2720
|
+
if amount is None:
|
|
2721
|
+
amount = int(meta.default_amount or 0)
|
|
2722
|
+
|
|
2723
|
+
economist_multiplier = 1.0 + 0.5 * float(perk_count_get(player, PerkId.BONUS_ECONOMIST))
|
|
2724
|
+
icon_id = int(meta.icon_id) if meta.icon_id is not None else -1
|
|
2725
|
+
label = meta.name
|
|
2726
|
+
ctx = _BonusApplyCtx(
|
|
2727
|
+
state=state,
|
|
2728
|
+
player=player,
|
|
2729
|
+
bonus_id=bonus_id,
|
|
2730
|
+
amount=int(amount),
|
|
2731
|
+
origin=origin,
|
|
2732
|
+
creatures=creatures,
|
|
2733
|
+
players=players,
|
|
2734
|
+
apply_creature_damage=apply_creature_damage,
|
|
2735
|
+
detail_preset=int(detail_preset),
|
|
2736
|
+
economist_multiplier=float(economist_multiplier),
|
|
2737
|
+
label=str(label),
|
|
2738
|
+
icon_id=int(icon_id),
|
|
2739
|
+
)
|
|
2740
|
+
handler = _BONUS_APPLY_HANDLERS.get(bonus_id)
|
|
2741
|
+
if handler is not None:
|
|
2742
|
+
handler(ctx)
|
|
2391
2743
|
|
|
2392
2744
|
# Bonus types not modeled yet.
|
|
2393
2745
|
return
|