crimsonland 0.1.0.dev5__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/__init__.py +24 -0
- crimson/assets_fetch.py +60 -0
- crimson/atlas.py +92 -0
- crimson/audio_router.py +155 -0
- crimson/bonuses.py +167 -0
- crimson/camera.py +75 -0
- crimson/cli.py +380 -0
- crimson/creatures/__init__.py +8 -0
- crimson/creatures/ai.py +186 -0
- crimson/creatures/anim.py +173 -0
- crimson/creatures/damage.py +103 -0
- crimson/creatures/runtime.py +1019 -0
- crimson/creatures/spawn.py +2871 -0
- crimson/debug.py +7 -0
- crimson/demo.py +1360 -0
- crimson/demo_trial.py +140 -0
- crimson/effects.py +1086 -0
- crimson/effects_atlas.py +73 -0
- crimson/frontend/__init__.py +1 -0
- crimson/frontend/assets.py +43 -0
- crimson/frontend/boot.py +424 -0
- crimson/frontend/menu.py +700 -0
- crimson/frontend/panels/__init__.py +1 -0
- crimson/frontend/panels/base.py +410 -0
- crimson/frontend/panels/controls.py +132 -0
- crimson/frontend/panels/mods.py +128 -0
- crimson/frontend/panels/options.py +409 -0
- crimson/frontend/panels/play_game.py +627 -0
- crimson/frontend/panels/stats.py +351 -0
- crimson/frontend/transitions.py +31 -0
- crimson/game.py +2533 -0
- crimson/game_modes.py +15 -0
- crimson/game_world.py +652 -0
- crimson/gameplay.py +2467 -0
- crimson/input_codes.py +176 -0
- crimson/modes/__init__.py +1 -0
- crimson/modes/base_gameplay_mode.py +219 -0
- crimson/modes/quest_mode.py +502 -0
- crimson/modes/rush_mode.py +300 -0
- crimson/modes/survival_mode.py +792 -0
- crimson/modes/tutorial_mode.py +648 -0
- crimson/modes/typo_mode.py +472 -0
- crimson/paths.py +23 -0
- crimson/perks.py +828 -0
- crimson/persistence/__init__.py +1 -0
- crimson/persistence/highscores.py +385 -0
- crimson/persistence/save_status.py +245 -0
- crimson/player_damage.py +77 -0
- crimson/projectiles.py +1133 -0
- crimson/quests/__init__.py +18 -0
- crimson/quests/helpers.py +147 -0
- crimson/quests/registry.py +49 -0
- crimson/quests/results.py +164 -0
- crimson/quests/runtime.py +91 -0
- crimson/quests/tier1.py +620 -0
- crimson/quests/tier2.py +652 -0
- crimson/quests/tier3.py +579 -0
- crimson/quests/tier4.py +721 -0
- crimson/quests/tier5.py +886 -0
- crimson/quests/timeline.py +115 -0
- crimson/quests/types.py +70 -0
- crimson/render/__init__.py +1 -0
- crimson/render/terrain_fx.py +88 -0
- crimson/render/world_renderer.py +1941 -0
- crimson/sim/__init__.py +1 -0
- crimson/sim/world_defs.py +67 -0
- crimson/sim/world_state.py +422 -0
- crimson/terrain_assets.py +19 -0
- crimson/tutorial/__init__.py +12 -0
- crimson/tutorial/timeline.py +291 -0
- crimson/typo/__init__.py +2 -0
- crimson/typo/names.py +233 -0
- crimson/typo/player.py +43 -0
- crimson/typo/spawns.py +73 -0
- crimson/typo/typing.py +52 -0
- crimson/ui/__init__.py +3 -0
- crimson/ui/cursor.py +95 -0
- crimson/ui/demo_trial_overlay.py +235 -0
- crimson/ui/game_over.py +660 -0
- crimson/ui/hud.py +601 -0
- crimson/ui/perk_menu.py +388 -0
- crimson/views/__init__.py +40 -0
- crimson/views/aim_debug.py +276 -0
- crimson/views/animations.py +274 -0
- crimson/views/arsenal_debug.py +404 -0
- crimson/views/audio_bootstrap.py +47 -0
- crimson/views/bonuses.py +201 -0
- crimson/views/camera_debug.py +359 -0
- crimson/views/camera_shake.py +229 -0
- crimson/views/corpse_stamp_debug.py +324 -0
- crimson/views/decals_debug.py +739 -0
- crimson/views/empty.py +19 -0
- crimson/views/fonts.py +114 -0
- crimson/views/game_over.py +117 -0
- crimson/views/ground.py +259 -0
- crimson/views/lighting_debug.py +1166 -0
- crimson/views/particles.py +293 -0
- crimson/views/perk_menu_debug.py +430 -0
- crimson/views/perks.py +398 -0
- crimson/views/player.py +434 -0
- crimson/views/player_sprite_debug.py +314 -0
- crimson/views/projectile_fx.py +609 -0
- crimson/views/projectile_render_debug.py +393 -0
- crimson/views/projectiles.py +221 -0
- crimson/views/quest_title_overlay.py +108 -0
- crimson/views/registry.py +34 -0
- crimson/views/rush.py +16 -0
- crimson/views/small_font_debug.py +204 -0
- crimson/views/spawn_plan.py +363 -0
- crimson/views/sprites.py +214 -0
- crimson/views/survival.py +15 -0
- crimson/views/terrain.py +132 -0
- crimson/views/ui.py +123 -0
- crimson/views/wicons.py +166 -0
- crimson/weapon_sfx.py +63 -0
- crimson/weapons.py +860 -0
- crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
- crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
- crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
- crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
- grim/__init__.py +20 -0
- grim/app.py +92 -0
- grim/assets.py +231 -0
- grim/audio.py +106 -0
- grim/config.py +294 -0
- grim/console.py +737 -0
- grim/fonts/__init__.py +7 -0
- grim/fonts/grim_mono.py +111 -0
- grim/fonts/small.py +120 -0
- grim/input.py +44 -0
- grim/jaz.py +103 -0
- grim/math.py +17 -0
- grim/music.py +403 -0
- grim/paq.py +76 -0
- grim/rand.py +37 -0
- grim/sfx.py +276 -0
- grim/sfx_map.py +103 -0
- grim/terrain_render.py +840 -0
- grim/view.py +16 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .types import QuestContext, QuestDefinition, SpawnEntry
|
|
4
|
+
from .registry import all_quests, quest_by_level
|
|
5
|
+
from . import tier1, tier2, tier3, tier4, tier5
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"QuestContext",
|
|
9
|
+
"QuestDefinition",
|
|
10
|
+
"SpawnEntry",
|
|
11
|
+
"all_quests",
|
|
12
|
+
"quest_by_level",
|
|
13
|
+
"tier1",
|
|
14
|
+
"tier2",
|
|
15
|
+
"tier3",
|
|
16
|
+
"tier4",
|
|
17
|
+
"tier5",
|
|
18
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
|
|
8
|
+
from ..creatures.spawn import SpawnId
|
|
9
|
+
from .types import SpawnEntry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class EdgePoints:
|
|
14
|
+
left: tuple[float, float]
|
|
15
|
+
right: tuple[float, float]
|
|
16
|
+
top: tuple[float, float]
|
|
17
|
+
bottom: tuple[float, float]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def center_point(width: float, height: float | None = None) -> tuple[float, float]:
|
|
21
|
+
if height is None:
|
|
22
|
+
height = width
|
|
23
|
+
return float(width) / 2.0, float(height) / 2.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def edge_midpoints(width: float, height: float | None = None, offset: float = 64.0) -> EdgePoints:
|
|
27
|
+
if height is None:
|
|
28
|
+
height = width
|
|
29
|
+
cx, cy = center_point(width, height)
|
|
30
|
+
return EdgePoints(
|
|
31
|
+
left=(-offset, cy),
|
|
32
|
+
right=(float(width) + offset, cy),
|
|
33
|
+
top=(cx, -offset),
|
|
34
|
+
bottom=(cx, float(height) + offset),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def corner_points(width: float, height: float | None = None, offset: float = 64.0) -> tuple[tuple[float, float], ...]:
|
|
39
|
+
if height is None:
|
|
40
|
+
height = width
|
|
41
|
+
return (
|
|
42
|
+
(-offset, -offset),
|
|
43
|
+
(float(width) + offset, -offset),
|
|
44
|
+
(-offset, float(height) + offset),
|
|
45
|
+
(float(width) + offset, float(height) + offset),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def iter_angles(count: int, *, step: float | None = None, start: float = 0.0) -> Iterator[float]:
|
|
50
|
+
if count <= 0:
|
|
51
|
+
return iter(())
|
|
52
|
+
if step is None:
|
|
53
|
+
step = math.tau / float(count)
|
|
54
|
+
for idx in range(count):
|
|
55
|
+
yield start + float(idx) * step
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ring_points(
|
|
59
|
+
center_x: float,
|
|
60
|
+
center_y: float,
|
|
61
|
+
radius: float,
|
|
62
|
+
count: int,
|
|
63
|
+
*,
|
|
64
|
+
step: float | None = None,
|
|
65
|
+
start: float = 0.0,
|
|
66
|
+
) -> Iterator[tuple[float, float, float]]:
|
|
67
|
+
for angle in iter_angles(count, step=step, start=start):
|
|
68
|
+
yield (
|
|
69
|
+
math.cos(angle) * radius + center_x,
|
|
70
|
+
math.sin(angle) * radius + center_y,
|
|
71
|
+
angle,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def random_angle(rng: random.Random) -> float:
|
|
76
|
+
return float(rng.randrange(0x264)) * 0.01
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def radial_points(
|
|
80
|
+
center_x: float,
|
|
81
|
+
center_y: float,
|
|
82
|
+
angle: float,
|
|
83
|
+
radius_start: float,
|
|
84
|
+
radius_end: float,
|
|
85
|
+
radius_step: float,
|
|
86
|
+
) -> Iterator[tuple[float, float]]:
|
|
87
|
+
cos_a = math.cos(angle)
|
|
88
|
+
sin_a = math.sin(angle)
|
|
89
|
+
radius = radius_start
|
|
90
|
+
while radius < radius_end:
|
|
91
|
+
yield (
|
|
92
|
+
cos_a * radius + center_x,
|
|
93
|
+
sin_a * radius + center_y,
|
|
94
|
+
)
|
|
95
|
+
radius += radius_step
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def heading_from_center(x: float, y: float, center_x: float, center_y: float) -> float:
|
|
99
|
+
return math.atan2(y - center_y, x - center_x) - (math.pi / 2.0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def line_points_x(start: float, step: float, count: int, y: float) -> Iterator[tuple[float, float]]:
|
|
103
|
+
for idx in range(count):
|
|
104
|
+
yield start + float(idx) * step, y
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def line_points_y(start: float, step: float, count: int, x: float) -> Iterator[tuple[float, float]]:
|
|
108
|
+
for idx in range(count):
|
|
109
|
+
yield x, start + float(idx) * step
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def spawn(
|
|
113
|
+
*,
|
|
114
|
+
x: float,
|
|
115
|
+
y: float,
|
|
116
|
+
heading: float = 0.0,
|
|
117
|
+
spawn_id: SpawnId,
|
|
118
|
+
trigger_ms: int,
|
|
119
|
+
count: int,
|
|
120
|
+
) -> SpawnEntry:
|
|
121
|
+
return SpawnEntry(
|
|
122
|
+
x=x,
|
|
123
|
+
y=y,
|
|
124
|
+
heading=heading,
|
|
125
|
+
spawn_id=spawn_id,
|
|
126
|
+
trigger_ms=trigger_ms,
|
|
127
|
+
count=count,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def spawn_at(
|
|
132
|
+
point: tuple[float, float],
|
|
133
|
+
*,
|
|
134
|
+
heading: float = 0.0,
|
|
135
|
+
spawn_id: SpawnId,
|
|
136
|
+
trigger_ms: int,
|
|
137
|
+
count: int,
|
|
138
|
+
) -> SpawnEntry:
|
|
139
|
+
x, y = point
|
|
140
|
+
return spawn(
|
|
141
|
+
x=x,
|
|
142
|
+
y=y,
|
|
143
|
+
heading=heading,
|
|
144
|
+
spawn_id=spawn_id,
|
|
145
|
+
trigger_ms=trigger_ms,
|
|
146
|
+
count=count,
|
|
147
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from .types import QuestBuilder, QuestDefinition
|
|
6
|
+
|
|
7
|
+
_QUESTS: dict[str, QuestDefinition] = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_quest(
|
|
11
|
+
*,
|
|
12
|
+
level: str,
|
|
13
|
+
title: str,
|
|
14
|
+
time_limit_ms: int,
|
|
15
|
+
start_weapon_id: int,
|
|
16
|
+
unlock_perk_id: int | None = None,
|
|
17
|
+
unlock_weapon_id: int | None = None,
|
|
18
|
+
terrain_id: int | None = None,
|
|
19
|
+
terrain_ids: tuple[int, int, int] | None = None,
|
|
20
|
+
builder_address: int | None = None,
|
|
21
|
+
) -> Callable[[QuestBuilder], QuestBuilder]:
|
|
22
|
+
def decorator(builder: QuestBuilder) -> QuestBuilder:
|
|
23
|
+
quest = QuestDefinition(
|
|
24
|
+
level=level,
|
|
25
|
+
title=title,
|
|
26
|
+
builder=builder,
|
|
27
|
+
time_limit_ms=time_limit_ms,
|
|
28
|
+
start_weapon_id=start_weapon_id,
|
|
29
|
+
unlock_perk_id=unlock_perk_id,
|
|
30
|
+
unlock_weapon_id=unlock_weapon_id,
|
|
31
|
+
terrain_id=terrain_id,
|
|
32
|
+
terrain_ids=terrain_ids,
|
|
33
|
+
builder_address=builder_address,
|
|
34
|
+
)
|
|
35
|
+
existing = _QUESTS.get(quest.level)
|
|
36
|
+
if existing is not None:
|
|
37
|
+
raise ValueError(f"duplicate quest level {quest.level}: {existing.builder.__name__} vs {builder.__name__}")
|
|
38
|
+
_QUESTS[quest.level] = quest
|
|
39
|
+
return builder
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def all_quests() -> list[QuestDefinition]:
|
|
45
|
+
return sorted(_QUESTS.values(), key=lambda quest: quest.level_key)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def quest_by_level(level: str) -> QuestDefinition | None:
|
|
49
|
+
return _QUESTS.get(level)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class QuestFinalTime:
|
|
8
|
+
base_time_ms: int
|
|
9
|
+
life_bonus_ms: int
|
|
10
|
+
unpicked_perk_bonus_ms: int
|
|
11
|
+
final_time_ms: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class QuestResultsBreakdownAnim:
|
|
16
|
+
"""Phase-based breakdown animation modeled after `quest_results_screen_update`.
|
|
17
|
+
|
|
18
|
+
The native flow animates the breakdown in four steps:
|
|
19
|
+
0) base time counts up to `base_time_ms`
|
|
20
|
+
1) life bonus counts up to `life_bonus_ms`
|
|
21
|
+
2) perk bonus counts up (in 1s steps) to `unpicked_perk_bonus_ms`
|
|
22
|
+
3) final-time highlight blink then completes
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
step: int = 0 # 0=base,1=life,2=perk,3=final blink,4=done
|
|
26
|
+
step_timer_ms: int = 700
|
|
27
|
+
|
|
28
|
+
base_time_ms: int = 0
|
|
29
|
+
life_bonus_ms: int = 0
|
|
30
|
+
unpicked_perk_bonus_s: int = 0
|
|
31
|
+
final_time_ms: int = 0
|
|
32
|
+
|
|
33
|
+
blink_ticks: int = 0
|
|
34
|
+
done: bool = False
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def start(cls) -> "QuestResultsBreakdownAnim":
|
|
38
|
+
return cls()
|
|
39
|
+
|
|
40
|
+
def set_final(self, target: QuestFinalTime) -> None:
|
|
41
|
+
self.step = 4
|
|
42
|
+
self.done = True
|
|
43
|
+
self.step_timer_ms = 0
|
|
44
|
+
self.base_time_ms = int(target.base_time_ms)
|
|
45
|
+
self.life_bonus_ms = int(target.life_bonus_ms)
|
|
46
|
+
self.unpicked_perk_bonus_s = max(0, int(target.unpicked_perk_bonus_ms) // 1000)
|
|
47
|
+
self.final_time_ms = int(target.final_time_ms)
|
|
48
|
+
self.blink_ticks = 0
|
|
49
|
+
|
|
50
|
+
def highlight_alpha(self) -> float:
|
|
51
|
+
if self.step != 3:
|
|
52
|
+
return 1.0
|
|
53
|
+
return max(0.0, min(1.0, 1.0 - float(self.blink_ticks) * 0.1))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compute_quest_final_time(
|
|
57
|
+
*,
|
|
58
|
+
base_time_ms: int,
|
|
59
|
+
player_health: float,
|
|
60
|
+
pending_perk_count: int,
|
|
61
|
+
player2_health: float | None = None,
|
|
62
|
+
) -> QuestFinalTime:
|
|
63
|
+
"""Compute quest final time (ms) and breakdown.
|
|
64
|
+
|
|
65
|
+
Modeled after `quest_results_screen_update`:
|
|
66
|
+
final_time_ms = base_time_ms - round(player_health) - (pending_perk_count * 1000)
|
|
67
|
+
clamped to at least 1ms.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
base_ms = int(base_time_ms)
|
|
71
|
+
life_bonus_ms = int(round(float(player_health)))
|
|
72
|
+
if player2_health is not None:
|
|
73
|
+
life_bonus_ms += int(round(float(player2_health)))
|
|
74
|
+
|
|
75
|
+
unpicked_perk_bonus_ms = max(0, int(pending_perk_count)) * 1000
|
|
76
|
+
final_ms = base_ms - int(life_bonus_ms) - int(unpicked_perk_bonus_ms)
|
|
77
|
+
if final_ms < 1:
|
|
78
|
+
final_ms = 1
|
|
79
|
+
|
|
80
|
+
return QuestFinalTime(
|
|
81
|
+
base_time_ms=base_ms,
|
|
82
|
+
life_bonus_ms=int(life_bonus_ms),
|
|
83
|
+
unpicked_perk_bonus_ms=int(unpicked_perk_bonus_ms),
|
|
84
|
+
final_time_ms=int(final_ms),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tick_quest_results_breakdown_anim(
|
|
89
|
+
anim: QuestResultsBreakdownAnim,
|
|
90
|
+
*,
|
|
91
|
+
frame_dt_ms: int,
|
|
92
|
+
target: QuestFinalTime,
|
|
93
|
+
) -> int:
|
|
94
|
+
"""Advance quest results breakdown animation.
|
|
95
|
+
|
|
96
|
+
Returns the number of "clink" ticks to play this frame.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if anim.done:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
clinks = 0
|
|
103
|
+
remaining = max(0, int(frame_dt_ms))
|
|
104
|
+
if remaining <= 0:
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
base_target_ms = max(0, int(target.base_time_ms))
|
|
108
|
+
life_target_ms = max(0, int(target.life_bonus_ms))
|
|
109
|
+
perk_target_s = max(0, int(target.unpicked_perk_bonus_ms) // 1000)
|
|
110
|
+
|
|
111
|
+
while remaining > 0 and not anim.done:
|
|
112
|
+
step_timer = int(anim.step_timer_ms)
|
|
113
|
+
take = remaining if step_timer <= 0 else min(remaining, step_timer)
|
|
114
|
+
anim.step_timer_ms = int(anim.step_timer_ms) - int(take)
|
|
115
|
+
remaining -= int(take)
|
|
116
|
+
|
|
117
|
+
while anim.step_timer_ms <= 0 and not anim.done:
|
|
118
|
+
step = int(anim.step)
|
|
119
|
+
if step == 0:
|
|
120
|
+
anim.base_time_ms = min(base_target_ms, int(anim.base_time_ms) + 2000)
|
|
121
|
+
anim.final_time_ms = int(anim.base_time_ms)
|
|
122
|
+
anim.step_timer_ms += 40
|
|
123
|
+
clinks += 1
|
|
124
|
+
if int(anim.base_time_ms) >= base_target_ms:
|
|
125
|
+
anim.step = 1
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if step == 1:
|
|
129
|
+
anim.life_bonus_ms = min(life_target_ms, int(anim.life_bonus_ms) + 1000)
|
|
130
|
+
anim.final_time_ms = max(
|
|
131
|
+
1,
|
|
132
|
+
base_target_ms - int(anim.life_bonus_ms) - int(anim.unpicked_perk_bonus_s) * 1000,
|
|
133
|
+
)
|
|
134
|
+
anim.step_timer_ms += 150
|
|
135
|
+
clinks += 1
|
|
136
|
+
if int(anim.life_bonus_ms) >= life_target_ms:
|
|
137
|
+
anim.step = 2
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if step == 2:
|
|
141
|
+
anim.unpicked_perk_bonus_s = min(perk_target_s, int(anim.unpicked_perk_bonus_s) + 1)
|
|
142
|
+
anim.final_time_ms = max(
|
|
143
|
+
1,
|
|
144
|
+
base_target_ms - int(anim.life_bonus_ms) - int(anim.unpicked_perk_bonus_s) * 1000,
|
|
145
|
+
)
|
|
146
|
+
clinks += 1
|
|
147
|
+
if int(anim.unpicked_perk_bonus_s) >= perk_target_s:
|
|
148
|
+
anim.final_time_ms = int(target.final_time_ms)
|
|
149
|
+
anim.step_timer_ms += 1000
|
|
150
|
+
anim.step = 3
|
|
151
|
+
else:
|
|
152
|
+
anim.step_timer_ms += 300
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if step == 3:
|
|
156
|
+
anim.blink_ticks += 1
|
|
157
|
+
anim.step_timer_ms += 50
|
|
158
|
+
if int(anim.blink_ticks) > 10:
|
|
159
|
+
anim.set_final(target)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
anim.set_final(target)
|
|
163
|
+
|
|
164
|
+
return int(clinks)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
import inspect
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from .types import QuestContext, QuestDefinition, SpawnEntry
|
|
8
|
+
|
|
9
|
+
QUEST_COMPLETION_TRANSITION_MS = 1000.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _call_builder(
|
|
13
|
+
builder,
|
|
14
|
+
ctx: QuestContext,
|
|
15
|
+
*,
|
|
16
|
+
rng: random.Random | None,
|
|
17
|
+
full_version: bool,
|
|
18
|
+
) -> list[SpawnEntry]:
|
|
19
|
+
params = inspect.signature(builder).parameters
|
|
20
|
+
kwargs: dict[str, object] = {}
|
|
21
|
+
if "rng" in params:
|
|
22
|
+
kwargs["rng"] = rng
|
|
23
|
+
if "full_version" in params:
|
|
24
|
+
kwargs["full_version"] = bool(full_version)
|
|
25
|
+
return builder(ctx, **kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def apply_hardcore_spawn_table_adjustment(entries: list[SpawnEntry]) -> list[SpawnEntry]:
|
|
29
|
+
"""Apply quest hardcore spawn-table count adjustment.
|
|
30
|
+
|
|
31
|
+
Modeled after the quest start logic in the classic game, which bumps `SpawnEntry.count`
|
|
32
|
+
for most multi-spawn entries in hardcore mode.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
adjusted: list[SpawnEntry] = []
|
|
36
|
+
for entry in entries:
|
|
37
|
+
spawn_id = int(entry.spawn_id)
|
|
38
|
+
count = int(entry.count)
|
|
39
|
+
if count > 1 and spawn_id != 0x3C:
|
|
40
|
+
if spawn_id == 0x2B:
|
|
41
|
+
count += 2
|
|
42
|
+
else:
|
|
43
|
+
count += 8
|
|
44
|
+
adjusted.append(entry if count == entry.count else replace(entry, count=count))
|
|
45
|
+
return adjusted
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_quest_spawn_table(
|
|
49
|
+
quest: QuestDefinition,
|
|
50
|
+
ctx: QuestContext,
|
|
51
|
+
*,
|
|
52
|
+
seed: int | None = None,
|
|
53
|
+
hardcore: bool = False,
|
|
54
|
+
full_version: bool = True,
|
|
55
|
+
) -> tuple[SpawnEntry, ...]:
|
|
56
|
+
"""Build the quest spawn script (with optional hardcore modifications)."""
|
|
57
|
+
|
|
58
|
+
rng = random.Random(seed) if seed is not None else random.Random()
|
|
59
|
+
entries = _call_builder(quest.builder, ctx, rng=rng, full_version=full_version)
|
|
60
|
+
if hardcore:
|
|
61
|
+
entries = apply_hardcore_spawn_table_adjustment(list(entries))
|
|
62
|
+
return tuple(entries)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def tick_quest_completion_transition(
|
|
66
|
+
completion_transition_ms: float,
|
|
67
|
+
frame_dt_ms: float,
|
|
68
|
+
*,
|
|
69
|
+
creatures_none_active: bool,
|
|
70
|
+
spawn_table_empty: bool,
|
|
71
|
+
) -> tuple[float, bool]:
|
|
72
|
+
"""Advance quest completion transition timer.
|
|
73
|
+
|
|
74
|
+
The quest-mode update loop waits for a short delay after the quest is "idle complete"
|
|
75
|
+
(no active creatures + no remaining spawn table entries) before transitioning to the
|
|
76
|
+
results screen.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
(completion_transition_ms, completed)
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
dt_ms = float(frame_dt_ms)
|
|
83
|
+
timer_ms = float(completion_transition_ms)
|
|
84
|
+
|
|
85
|
+
if creatures_none_active and spawn_table_empty:
|
|
86
|
+
if timer_ms < 0.0:
|
|
87
|
+
timer_ms = 0.0
|
|
88
|
+
timer_ms += dt_ms
|
|
89
|
+
return timer_ms, bool(timer_ms >= QUEST_COMPLETION_TRANSITION_MS)
|
|
90
|
+
|
|
91
|
+
return -1.0, False
|