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/sim/world_state.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
import math
|
|
5
|
+
from typing import Callable
|
|
5
6
|
|
|
6
7
|
from ..bonuses import BonusId
|
|
7
8
|
from ..creatures.damage import creature_apply_damage
|
|
@@ -35,6 +36,107 @@ class WorldEvents:
|
|
|
35
36
|
sfx: list[str]
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class _WorldDtCtx:
|
|
41
|
+
dt: float
|
|
42
|
+
players: list[PlayerState]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_WorldDtStep = Callable[[_WorldDtCtx], None]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _world_dt_reflex_boosted(ctx: _WorldDtCtx) -> None:
|
|
49
|
+
if ctx.dt > 0.0 and ctx.players and perk_active(ctx.players[0], PerkId.REFLEX_BOOSTED):
|
|
50
|
+
ctx.dt *= 0.9
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_WORLD_DT_STEPS: tuple[_WorldDtStep, ...] = (_world_dt_reflex_boosted,)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class _PlayerDeathCtx:
|
|
58
|
+
state: GameplayState
|
|
59
|
+
creatures: CreaturePool
|
|
60
|
+
players: list[PlayerState]
|
|
61
|
+
player_index: int
|
|
62
|
+
player: PlayerState
|
|
63
|
+
dt: float
|
|
64
|
+
world_size: float
|
|
65
|
+
detail_preset: int
|
|
66
|
+
fx_queue: FxQueue
|
|
67
|
+
deaths: list[object]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_PlayerDeathHook = Callable[[_PlayerDeathCtx], None]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _player_death_final_revenge(ctx: _PlayerDeathCtx) -> None:
|
|
74
|
+
player = ctx.player
|
|
75
|
+
if not perk_active(player, PerkId.FINAL_REVENGE):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
px = float(player.pos_x)
|
|
79
|
+
py = float(player.pos_y)
|
|
80
|
+
rand = ctx.state.rng.rand
|
|
81
|
+
ctx.state.effects.spawn_explosion_burst(
|
|
82
|
+
pos_x=px,
|
|
83
|
+
pos_y=py,
|
|
84
|
+
scale=1.8,
|
|
85
|
+
rand=rand,
|
|
86
|
+
detail_preset=int(ctx.detail_preset),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
prev_guard = bool(ctx.state.bonus_spawn_guard)
|
|
90
|
+
ctx.state.bonus_spawn_guard = True
|
|
91
|
+
for creature_idx, creature in enumerate(ctx.creatures.entries):
|
|
92
|
+
if not creature.active:
|
|
93
|
+
continue
|
|
94
|
+
if float(creature.hp) <= 0.0:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
dx = float(creature.x) - px
|
|
98
|
+
dy = float(creature.y) - py
|
|
99
|
+
if abs(dx) > 512.0 or abs(dy) > 512.0:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
remaining = 512.0 - math.hypot(dx, dy)
|
|
103
|
+
if remaining <= 0.0:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
damage = remaining * 5.0
|
|
107
|
+
death_start_needed = float(creature.hp) > 0.0 and float(creature.hitbox_size) == CREATURE_HITBOX_ALIVE
|
|
108
|
+
killed = creature_apply_damage(
|
|
109
|
+
creature,
|
|
110
|
+
damage_amount=damage,
|
|
111
|
+
damage_type=3,
|
|
112
|
+
impulse_x=0.0,
|
|
113
|
+
impulse_y=0.0,
|
|
114
|
+
owner_id=-1 - int(player.index),
|
|
115
|
+
dt=float(ctx.dt),
|
|
116
|
+
players=ctx.players,
|
|
117
|
+
rand=rand,
|
|
118
|
+
)
|
|
119
|
+
if killed and death_start_needed:
|
|
120
|
+
ctx.deaths.append(
|
|
121
|
+
ctx.creatures.handle_death(
|
|
122
|
+
int(creature_idx),
|
|
123
|
+
state=ctx.state,
|
|
124
|
+
players=ctx.players,
|
|
125
|
+
rand=rand,
|
|
126
|
+
detail_preset=int(ctx.detail_preset),
|
|
127
|
+
world_width=float(ctx.world_size),
|
|
128
|
+
world_height=float(ctx.world_size),
|
|
129
|
+
fx_queue=ctx.fx_queue,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
ctx.state.bonus_spawn_guard = prev_guard
|
|
133
|
+
ctx.state.sfx_queue.append("sfx_explosion_large")
|
|
134
|
+
ctx.state.sfx_queue.append("sfx_shockwave")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_PLAYER_DEATH_HOOKS: tuple[_PlayerDeathHook, ...] = (_player_death_final_revenge,)
|
|
138
|
+
|
|
139
|
+
|
|
38
140
|
@dataclass(slots=True)
|
|
39
141
|
class WorldState:
|
|
40
142
|
spawn_env: SpawnEnv
|
|
@@ -84,9 +186,10 @@ class WorldState:
|
|
|
84
186
|
game_mode: int,
|
|
85
187
|
perk_progression_enabled: bool,
|
|
86
188
|
) -> WorldEvents:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
189
|
+
dt_ctx = _WorldDtCtx(dt=float(dt), players=self.players)
|
|
190
|
+
for step in _WORLD_DT_STEPS:
|
|
191
|
+
step(dt_ctx)
|
|
192
|
+
dt = float(dt_ctx.dt)
|
|
90
193
|
|
|
91
194
|
if inputs is None:
|
|
92
195
|
inputs = [PlayerInput() for _ in self.players]
|
|
@@ -194,66 +297,20 @@ class WorldState:
|
|
|
194
297
|
continue
|
|
195
298
|
if float(player.health) >= 0.0:
|
|
196
299
|
continue
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
pos_y=py,
|
|
206
|
-
scale=1.8,
|
|
207
|
-
rand=rand,
|
|
300
|
+
death_ctx = _PlayerDeathCtx(
|
|
301
|
+
state=self.state,
|
|
302
|
+
creatures=self.creatures,
|
|
303
|
+
players=self.players,
|
|
304
|
+
player_index=int(idx),
|
|
305
|
+
player=player,
|
|
306
|
+
dt=float(dt),
|
|
307
|
+
world_size=float(world_size),
|
|
208
308
|
detail_preset=int(detail_preset),
|
|
309
|
+
fx_queue=fx_queue,
|
|
310
|
+
deaths=deaths,
|
|
209
311
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
self.state.bonus_spawn_guard = True
|
|
213
|
-
for creature_idx, creature in enumerate(self.creatures.entries):
|
|
214
|
-
if not creature.active:
|
|
215
|
-
continue
|
|
216
|
-
if float(creature.hp) <= 0.0:
|
|
217
|
-
continue
|
|
218
|
-
|
|
219
|
-
dx = float(creature.x) - px
|
|
220
|
-
dy = float(creature.y) - py
|
|
221
|
-
if abs(dx) > 512.0 or abs(dy) > 512.0:
|
|
222
|
-
continue
|
|
223
|
-
|
|
224
|
-
remaining = 512.0 - math.hypot(dx, dy)
|
|
225
|
-
if remaining <= 0.0:
|
|
226
|
-
continue
|
|
227
|
-
|
|
228
|
-
damage = remaining * 5.0
|
|
229
|
-
death_start_needed = float(creature.hp) > 0.0 and float(creature.hitbox_size) == CREATURE_HITBOX_ALIVE
|
|
230
|
-
killed = creature_apply_damage(
|
|
231
|
-
creature,
|
|
232
|
-
damage_amount=damage,
|
|
233
|
-
damage_type=3,
|
|
234
|
-
impulse_x=0.0,
|
|
235
|
-
impulse_y=0.0,
|
|
236
|
-
owner_id=-1 - int(player.index),
|
|
237
|
-
dt=float(dt),
|
|
238
|
-
players=self.players,
|
|
239
|
-
rand=rand,
|
|
240
|
-
)
|
|
241
|
-
if killed and death_start_needed:
|
|
242
|
-
deaths.append(
|
|
243
|
-
self.creatures.handle_death(
|
|
244
|
-
int(creature_idx),
|
|
245
|
-
state=self.state,
|
|
246
|
-
players=self.players,
|
|
247
|
-
rand=rand,
|
|
248
|
-
detail_preset=int(detail_preset),
|
|
249
|
-
world_width=float(world_size),
|
|
250
|
-
world_height=float(world_size),
|
|
251
|
-
fx_queue=fx_queue,
|
|
252
|
-
)
|
|
253
|
-
)
|
|
254
|
-
self.state.bonus_spawn_guard = prev_guard
|
|
255
|
-
self.state.sfx_queue.append("sfx_explosion_large")
|
|
256
|
-
self.state.sfx_queue.append("sfx_shockwave")
|
|
312
|
+
for hook in _PLAYER_DEATH_HOOKS:
|
|
313
|
+
hook(death_ctx)
|
|
257
314
|
|
|
258
315
|
def _kill_creature_no_corpse(creature_index: int, owner_id: int) -> None:
|
|
259
316
|
idx = int(creature_index)
|
crimson/typo/spawns.py
CHANGED
|
@@ -3,16 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
import math
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
10
|
-
if value < lo:
|
|
11
|
-
return lo
|
|
12
|
-
if value > hi:
|
|
13
|
-
return hi
|
|
14
|
-
return value
|
|
6
|
+
from grim.math import clamp
|
|
15
7
|
|
|
8
|
+
from ..creatures.spawn import CreatureTypeId
|
|
16
9
|
|
|
17
10
|
@dataclass(frozen=True, slots=True)
|
|
18
11
|
class TypoSpawnCall:
|
|
@@ -47,9 +40,9 @@ def tick_typo_spawns(
|
|
|
47
40
|
y = math.cos(t) * 256.0 + float(world_height) * 0.5
|
|
48
41
|
|
|
49
42
|
tint_t = float(elapsed_ms + 1)
|
|
50
|
-
tint_r =
|
|
51
|
-
tint_g =
|
|
52
|
-
tint_b =
|
|
43
|
+
tint_r = clamp(tint_t * 0.0000083333334 + 0.3, 0.0, 1.0)
|
|
44
|
+
tint_g = clamp(tint_t * 10000.0 + 0.3, 0.0, 1.0)
|
|
45
|
+
tint_b = clamp(math.sin(tint_t * 0.0001) + 0.3, 0.0, 1.0)
|
|
53
46
|
tint = (tint_r, tint_g, tint_b, 1.0)
|
|
54
47
|
|
|
55
48
|
spawns.append(
|
crimson/ui/demo_trial_overlay.py
CHANGED
|
@@ -6,6 +6,7 @@ import pyray as rl
|
|
|
6
6
|
|
|
7
7
|
from grim.assets import PaqTextureCache
|
|
8
8
|
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
|
|
9
|
+
from grim.math import clamp
|
|
9
10
|
|
|
10
11
|
from ..demo_trial import DemoTrialOverlayInfo
|
|
11
12
|
from .perk_menu import (
|
|
@@ -20,15 +21,6 @@ from .perk_menu import (
|
|
|
20
21
|
|
|
21
22
|
DEMO_PURCHASE_URL = "http://buy.crimsonland.com"
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
25
|
-
if value < lo:
|
|
26
|
-
return lo
|
|
27
|
-
if value > hi:
|
|
28
|
-
return hi
|
|
29
|
-
return value
|
|
30
|
-
|
|
31
|
-
|
|
32
24
|
class DemoTrialOverlayUi:
|
|
33
25
|
def __init__(self, assets_root: Path) -> None:
|
|
34
26
|
self._assets_root = assets_root
|
|
@@ -94,8 +86,8 @@ class DemoTrialOverlayUi:
|
|
|
94
86
|
mouse = rl.get_mouse_position()
|
|
95
87
|
screen_w = float(rl.get_screen_width())
|
|
96
88
|
screen_h = float(rl.get_screen_height())
|
|
97
|
-
mouse.x =
|
|
98
|
-
mouse.y =
|
|
89
|
+
mouse.x = clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
|
|
90
|
+
mouse.y = clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
|
|
99
91
|
|
|
100
92
|
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
101
93
|
panel_x, panel_y = self._panel_xy(screen_w=screen_w, screen_h=screen_h)
|
crimson/ui/formatting.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_ordinal(value_1_based: int) -> str:
|
|
5
|
+
value = int(value_1_based)
|
|
6
|
+
if value % 100 in (11, 12, 13):
|
|
7
|
+
suffix = "th"
|
|
8
|
+
elif value % 10 == 1:
|
|
9
|
+
suffix = "st"
|
|
10
|
+
elif value % 10 == 2:
|
|
11
|
+
suffix = "nd"
|
|
12
|
+
elif value % 10 == 3:
|
|
13
|
+
suffix = "rd"
|
|
14
|
+
else:
|
|
15
|
+
suffix = "th"
|
|
16
|
+
return f"{value}{suffix}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_time_mm_ss(ms: int) -> str:
|
|
20
|
+
total_s = max(0, int(ms)) // 1000
|
|
21
|
+
minutes = total_s // 60
|
|
22
|
+
seconds = total_s % 60
|
|
23
|
+
return f"{minutes}:{seconds:02d}"
|
|
24
|
+
|
crimson/ui/game_over.py
CHANGED
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import pyray as rl
|
|
9
9
|
|
|
10
10
|
from grim.assets import TextureLoader
|
|
11
|
+
from grim.config import CrimsonConfig
|
|
11
12
|
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
|
|
12
13
|
|
|
13
14
|
from ..persistence.highscores import (
|
|
@@ -20,7 +21,9 @@ from ..persistence.highscores import (
|
|
|
20
21
|
upsert_highscore_record,
|
|
21
22
|
)
|
|
22
23
|
from ..weapons import WEAPON_BY_ID
|
|
24
|
+
from .formatting import format_ordinal, format_time_mm_ss
|
|
23
25
|
from .hud import HudAssets
|
|
26
|
+
from .layout import ui_origin, ui_scale
|
|
24
27
|
from .perk_menu import (
|
|
25
28
|
PerkMenuAssets,
|
|
26
29
|
UiButtonState,
|
|
@@ -32,20 +35,7 @@ from .perk_menu import (
|
|
|
32
35
|
load_perk_menu_assets,
|
|
33
36
|
)
|
|
34
37
|
from .shadow import UI_SHADOW_OFFSET, draw_ui_quad_shadow
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
UI_BASE_WIDTH = 640.0
|
|
38
|
-
UI_BASE_HEIGHT = 480.0
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def ui_scale(screen_w: float, screen_h: float) -> float:
|
|
42
|
-
# Matches the classic UI-space helpers we use elsewhere: render in 640x480 pixel space.
|
|
43
|
-
return 1.0
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
|
|
47
|
-
return 0.0, 0.0
|
|
48
|
-
|
|
38
|
+
from .text_input import poll_text_input
|
|
49
39
|
|
|
50
40
|
GAME_OVER_PANEL_X = -45.0
|
|
51
41
|
GAME_OVER_PANEL_Y = 210.0
|
|
@@ -69,28 +59,6 @@ COLOR_SCORE_LABEL = rl.Color(230, 230, 230, 255)
|
|
|
69
59
|
COLOR_SCORE_VALUE = rl.Color(230, 230, 255, 255)
|
|
70
60
|
|
|
71
61
|
|
|
72
|
-
def _format_ordinal(value_1_based: int) -> str:
|
|
73
|
-
value = int(value_1_based)
|
|
74
|
-
if value % 100 in (11, 12, 13):
|
|
75
|
-
suffix = "th"
|
|
76
|
-
elif value % 10 == 1:
|
|
77
|
-
suffix = "st"
|
|
78
|
-
elif value % 10 == 2:
|
|
79
|
-
suffix = "nd"
|
|
80
|
-
elif value % 10 == 3:
|
|
81
|
-
suffix = "rd"
|
|
82
|
-
else:
|
|
83
|
-
suffix = "th"
|
|
84
|
-
return f"{value}{suffix}"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _format_time_mm_ss(ms: int) -> str:
|
|
88
|
-
total_s = max(0, int(ms)) // 1000
|
|
89
|
-
minutes = total_s // 60
|
|
90
|
-
seconds = total_s % 60
|
|
91
|
-
return f"{minutes}:{seconds:02d}"
|
|
92
|
-
|
|
93
|
-
|
|
94
62
|
def _weapon_icon_src(texture: rl.Texture, weapon_id_native: int) -> rl.Rectangle | None:
|
|
95
63
|
weapon_id = int(weapon_id_native)
|
|
96
64
|
entry = WEAPON_BY_ID.get(int(weapon_id))
|
|
@@ -143,22 +111,6 @@ def _draw_texture_centered(tex: rl.Texture, x: float, y: float, w: float, h: flo
|
|
|
143
111
|
rl.draw_texture_pro(tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
|
|
144
112
|
|
|
145
113
|
|
|
146
|
-
def _poll_text_input(max_len: int, *, allow_space: bool = True) -> str:
|
|
147
|
-
out = ""
|
|
148
|
-
while True:
|
|
149
|
-
value = rl.get_char_pressed()
|
|
150
|
-
if value == 0:
|
|
151
|
-
break
|
|
152
|
-
if value < 0x20 or value > 0xFF:
|
|
153
|
-
continue
|
|
154
|
-
if not allow_space and value == 0x20:
|
|
155
|
-
continue
|
|
156
|
-
if len(out) >= max_len:
|
|
157
|
-
continue
|
|
158
|
-
out += chr(int(value))
|
|
159
|
-
return out
|
|
160
|
-
|
|
161
|
-
|
|
162
114
|
def _ease_out_cubic(t: float) -> float:
|
|
163
115
|
t = max(0.0, min(1.0, float(t)))
|
|
164
116
|
return 1.0 - (1.0 - t) ** 3
|
|
@@ -169,7 +121,7 @@ class GameOverUi:
|
|
|
169
121
|
assets_root: Path
|
|
170
122
|
base_dir: Path
|
|
171
123
|
|
|
172
|
-
config:
|
|
124
|
+
config: CrimsonConfig
|
|
173
125
|
|
|
174
126
|
assets: GameOverAssets | None = None
|
|
175
127
|
font: SmallFontData | None = None
|
|
@@ -309,7 +261,7 @@ class GameOverUi:
|
|
|
309
261
|
rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
|
|
310
262
|
if self.phase == -1:
|
|
311
263
|
# If in the top 100, prompt for a name. Otherwise show score-too-low message and buttons.
|
|
312
|
-
game_mode_id = int(
|
|
264
|
+
game_mode_id = int(self.config.data.get("game_mode", 1))
|
|
313
265
|
candidate = record.copy()
|
|
314
266
|
candidate.game_mode_id = game_mode_id
|
|
315
267
|
self._candidate_record = candidate
|
|
@@ -328,7 +280,7 @@ class GameOverUi:
|
|
|
328
280
|
# Basic text input behavior for the name-entry phase.
|
|
329
281
|
if self.phase == 0:
|
|
330
282
|
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
331
|
-
typed =
|
|
283
|
+
typed = poll_text_input(NAME_MAX_EDIT - len(self.input_text), allow_space=True)
|
|
332
284
|
if typed:
|
|
333
285
|
self.input_text = (self.input_text[: self.input_caret] + typed + self.input_text[self.input_caret :])[:NAME_MAX_EDIT]
|
|
334
286
|
self.input_caret = min(len(self.input_text), self.input_caret + len(typed))
|
|
@@ -369,6 +321,8 @@ class GameOverUi:
|
|
|
369
321
|
play_sfx("sfx_ui_typeenter")
|
|
370
322
|
candidate = (self._candidate_record or record).copy()
|
|
371
323
|
candidate.set_name(self.input_text)
|
|
324
|
+
self.config.set_player_name(self.input_text)
|
|
325
|
+
self.config.save()
|
|
372
326
|
path = scores_path_for_config(self.base_dir, self.config)
|
|
373
327
|
if not self._saved:
|
|
374
328
|
upsert_highscore_record(path, candidate)
|
|
@@ -449,7 +403,7 @@ class GameOverUi:
|
|
|
449
403
|
score_value_w = self._text_width(score_value, 1.0 * scale)
|
|
450
404
|
self._draw_small(score_value, base_x + 32.0 * scale - score_value_w * 0.5, base_y + 15.0 * scale, 1.0 * scale, value_color)
|
|
451
405
|
|
|
452
|
-
rank_value =
|
|
406
|
+
rank_value = format_ordinal(int(self.rank) + 1)
|
|
453
407
|
rank_text = f"Rank: {rank_value}"
|
|
454
408
|
rank_w = self._text_width(rank_text, 1.0 * scale)
|
|
455
409
|
self._draw_small(rank_text, base_x + 32.0 * scale - rank_w * 0.5, base_y + 30.0 * scale, 1.0 * scale, label_color)
|
|
@@ -487,7 +441,7 @@ class GameOverUi:
|
|
|
487
441
|
origin = rl.Vector2(16.0 * scale, 16.0 * scale)
|
|
488
442
|
rl.draw_texture_pro(hud_assets.clock_pointer, src, dst, origin, rotation, rl.Color(255, 255, 255, int(255 * alpha)))
|
|
489
443
|
|
|
490
|
-
time_text =
|
|
444
|
+
time_text = format_time_mm_ss(elapsed_ms)
|
|
491
445
|
self._draw_small(time_text, col2_x + 40.0 * scale, base_y + 19.0 * scale, 1.0 * scale, label_color)
|
|
492
446
|
|
|
493
447
|
# Second row: weapon icon + frags + hit ratio (suppressed while entering the name).
|
|
@@ -572,7 +526,7 @@ class GameOverUi:
|
|
|
572
526
|
panel_tex = self.assets.menu_panel
|
|
573
527
|
src = rl.Rectangle(0.0, 0.0, float(panel_tex.width), float(panel_tex.height))
|
|
574
528
|
dst = rl.Rectangle(panel.x, panel.y, panel.width, panel.height)
|
|
575
|
-
fx_detail = bool(int(
|
|
529
|
+
fx_detail = bool(int(self.config.data.get("fx_detail_0", 0) or 0))
|
|
576
530
|
if fx_detail:
|
|
577
531
|
draw_ui_quad_shadow(
|
|
578
532
|
texture=panel_tex,
|
crimson/ui/hud.py
CHANGED
|
@@ -56,8 +56,6 @@ HUD_BONUS_PANEL_OFFSET_Y = -11.0
|
|
|
56
56
|
HUD_XP_BAR_RGBA = (0.1, 0.3, 0.6, 1.0)
|
|
57
57
|
HUD_QUEST_LEFT_Y_SHIFT = 80.0
|
|
58
58
|
|
|
59
|
-
_SURVIVAL_XP_SMOOTHED = 0
|
|
60
|
-
|
|
61
59
|
|
|
62
60
|
@dataclass(slots=True)
|
|
63
61
|
class HudAssets:
|
|
@@ -85,6 +83,46 @@ class HudRenderFlags:
|
|
|
85
83
|
show_quest_hud: bool
|
|
86
84
|
|
|
87
85
|
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class HudState:
|
|
88
|
+
survival_xp_smoothed: int = 0
|
|
89
|
+
|
|
90
|
+
def smooth_xp(self, target: int, frame_dt_ms: float) -> int:
|
|
91
|
+
target = int(target)
|
|
92
|
+
if target <= 0:
|
|
93
|
+
self.survival_xp_smoothed = 0
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
smoothed = int(self.survival_xp_smoothed)
|
|
97
|
+
if smoothed == target:
|
|
98
|
+
return smoothed
|
|
99
|
+
|
|
100
|
+
step = max(1, int(frame_dt_ms) // 2)
|
|
101
|
+
diff = abs(smoothed - target)
|
|
102
|
+
if diff > 1000:
|
|
103
|
+
step *= diff // 100
|
|
104
|
+
|
|
105
|
+
if smoothed < target:
|
|
106
|
+
smoothed += step
|
|
107
|
+
if smoothed > target:
|
|
108
|
+
smoothed = target
|
|
109
|
+
else:
|
|
110
|
+
smoothed -= step
|
|
111
|
+
if smoothed < target:
|
|
112
|
+
smoothed = target
|
|
113
|
+
|
|
114
|
+
self.survival_xp_smoothed = smoothed
|
|
115
|
+
return smoothed
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class HudLayout:
|
|
120
|
+
scale: float
|
|
121
|
+
text_scale: float
|
|
122
|
+
line_h: float
|
|
123
|
+
hud_y_shift: float
|
|
124
|
+
|
|
125
|
+
|
|
88
126
|
def hud_flags_for_game_mode(game_mode_id: int) -> HudRenderFlags:
|
|
89
127
|
"""Match `hud_update_and_render` (0x0041ca90) flag mapping."""
|
|
90
128
|
|
|
@@ -139,6 +177,14 @@ def hud_ui_scale(screen_w: float, screen_h: float) -> float:
|
|
|
139
177
|
return float(scale)
|
|
140
178
|
|
|
141
179
|
|
|
180
|
+
def hud_layout(screen_w: float, screen_h: float, *, font: SmallFontData | None, show_quest_hud: bool) -> HudLayout:
|
|
181
|
+
scale = hud_ui_scale(float(screen_w), float(screen_h))
|
|
182
|
+
text_scale = 1.0 * scale
|
|
183
|
+
line_h = float(font.cell_size) * text_scale if font is not None else 18.0 * text_scale
|
|
184
|
+
hud_y_shift = HUD_QUEST_LEFT_Y_SHIFT if show_quest_hud else 0.0
|
|
185
|
+
return HudLayout(scale=scale, text_scale=text_scale, line_h=line_h, hud_y_shift=hud_y_shift)
|
|
186
|
+
|
|
187
|
+
|
|
142
188
|
def load_hud_assets(assets_root: Path) -> HudAssets:
|
|
143
189
|
loader = TextureLoader.from_assets_root(assets_root)
|
|
144
190
|
return HudAssets(
|
|
@@ -178,29 +224,20 @@ def _with_alpha(color: rl.Color, alpha: float) -> rl.Color:
|
|
|
178
224
|
return rl.Color(color.r, color.g, color.b, int(color.a * alpha))
|
|
179
225
|
|
|
180
226
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
smoothed += step
|
|
196
|
-
if smoothed > target:
|
|
197
|
-
smoothed = target
|
|
198
|
-
else:
|
|
199
|
-
smoothed -= step
|
|
200
|
-
if smoothed < target:
|
|
201
|
-
smoothed = target
|
|
202
|
-
_SURVIVAL_XP_SMOOTHED = smoothed
|
|
203
|
-
return smoothed
|
|
227
|
+
def _quest_panel_slide_x(time_ms: float) -> float:
|
|
228
|
+
time_ms = float(time_ms)
|
|
229
|
+
if time_ms < 1000.0:
|
|
230
|
+
return (1000.0 - time_ms) * -0.128
|
|
231
|
+
return 0.0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _survival_xp_progress_ratio(*, xp: int, level: int) -> float:
|
|
235
|
+
level = max(1, int(level))
|
|
236
|
+
prev_threshold = 0 if level <= 1 else survival_level_threshold(level - 1)
|
|
237
|
+
next_threshold = survival_level_threshold(level)
|
|
238
|
+
if next_threshold <= prev_threshold:
|
|
239
|
+
return 0.0
|
|
240
|
+
return (int(xp) - prev_threshold) / float(next_threshold - prev_threshold)
|
|
204
241
|
|
|
205
242
|
|
|
206
243
|
def _draw_progress_bar(x: float, y: float, width: float, ratio: float, rgba: tuple[float, float, float, float], scale: float) -> None:
|
|
@@ -275,6 +312,7 @@ def _bonus_icon_src(texture: rl.Texture, icon_id: int) -> rl.Rectangle:
|
|
|
275
312
|
def draw_hud_overlay(
|
|
276
313
|
assets: HudAssets,
|
|
277
314
|
*,
|
|
315
|
+
state: HudState,
|
|
278
316
|
player: PlayerState,
|
|
279
317
|
players: list[PlayerState] | None = None,
|
|
280
318
|
bonus_hud: BonusHudState | None = None,
|
|
@@ -293,6 +331,7 @@ def draw_hud_overlay(
|
|
|
293
331
|
) -> float:
|
|
294
332
|
if frame_dt_ms is None:
|
|
295
333
|
frame_dt_ms = max(0.0, float(rl.get_frame_time()) * 1000.0)
|
|
334
|
+
state = state or HudState()
|
|
296
335
|
hud_players = list(players) if players is not None else [player]
|
|
297
336
|
if not hud_players:
|
|
298
337
|
hud_players = [player]
|
|
@@ -300,9 +339,10 @@ def draw_hud_overlay(
|
|
|
300
339
|
|
|
301
340
|
screen_w = float(rl.get_screen_width())
|
|
302
341
|
screen_h = float(rl.get_screen_height())
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
342
|
+
layout = hud_layout(screen_w, screen_h, font=font, show_quest_hud=show_quest_hud)
|
|
343
|
+
scale = layout.scale
|
|
344
|
+
text_scale = layout.text_scale
|
|
345
|
+
line_h = layout.line_h
|
|
306
346
|
|
|
307
347
|
def sx(value: float) -> float:
|
|
308
348
|
return value * scale
|
|
@@ -314,7 +354,7 @@ def draw_hud_overlay(
|
|
|
314
354
|
alpha = max(0.0, min(1.0, float(alpha)))
|
|
315
355
|
text_color = _with_alpha(HUD_TEXT_COLOR, alpha)
|
|
316
356
|
panel_text_color = _with_alpha(HUD_TEXT_COLOR, alpha * HUD_PANEL_ALPHA)
|
|
317
|
-
hud_y_shift =
|
|
357
|
+
hud_y_shift = layout.hud_y_shift
|
|
318
358
|
|
|
319
359
|
# Top bar background.
|
|
320
360
|
if assets.game_top is not None:
|
|
@@ -502,9 +542,7 @@ def draw_hud_overlay(
|
|
|
502
542
|
# Quest HUD panels (mm:ss timer + progress).
|
|
503
543
|
if show_quest_hud:
|
|
504
544
|
time_ms = max(0.0, float(elapsed_ms))
|
|
505
|
-
slide_x =
|
|
506
|
-
if time_ms < 1000.0:
|
|
507
|
-
slide_x = (1000.0 - time_ms) * -0.128
|
|
545
|
+
slide_x = _quest_panel_slide_x(time_ms)
|
|
508
546
|
|
|
509
547
|
quest_panel_alpha = alpha * 0.7
|
|
510
548
|
quest_text_color = _with_alpha(HUD_TEXT_COLOR, quest_panel_alpha)
|
|
@@ -580,7 +618,7 @@ def draw_hud_overlay(
|
|
|
580
618
|
|
|
581
619
|
# Survival XP panel.
|
|
582
620
|
xp_target = int(player.experience if score is None else score)
|
|
583
|
-
xp_display =
|
|
621
|
+
xp_display = state.smooth_xp(xp_target, frame_dt_ms) if show_xp else xp_target
|
|
584
622
|
if show_xp and assets.ind_panel is not None:
|
|
585
623
|
panel_x, panel_y = HUD_SURV_PANEL_POS
|
|
586
624
|
panel_y += hud_y_shift
|
|
@@ -623,12 +661,7 @@ def draw_hud_overlay(
|
|
|
623
661
|
panel_text_color,
|
|
624
662
|
)
|
|
625
663
|
|
|
626
|
-
|
|
627
|
-
prev_threshold = 0 if level <= 1 else survival_level_threshold(level - 1)
|
|
628
|
-
next_threshold = survival_level_threshold(level)
|
|
629
|
-
progress_ratio = 0.0
|
|
630
|
-
if next_threshold > prev_threshold:
|
|
631
|
-
progress_ratio = (xp_target - prev_threshold) / float(next_threshold - prev_threshold)
|
|
664
|
+
progress_ratio = _survival_xp_progress_ratio(xp=xp_target, level=int(player.level))
|
|
632
665
|
bar_x, bar_y = HUD_SURV_PROGRESS_POS
|
|
633
666
|
bar_y += hud_y_shift
|
|
634
667
|
bar_w = HUD_SURV_PROGRESS_WIDTH
|
crimson/ui/layout.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
UI_BASE_WIDTH = 640.0
|
|
5
|
+
UI_BASE_HEIGHT = 480.0
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ui_scale(screen_w: float, screen_h: float) -> float:
|
|
9
|
+
# Classic UI-space: draw in backbuffer pixels.
|
|
10
|
+
return 1.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
|
|
14
|
+
return 0.0, 0.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def menu_widescreen_y_shift(layout_w: float) -> float:
|
|
18
|
+
# ui_menu_layout_init: pos_y += (screen_width / 640.0) * 150.0 - 150.0
|
|
19
|
+
return (layout_w / UI_BASE_WIDTH) * 150.0 - 150.0
|
|
20
|
+
|