crimsonland 0.1.0.dev15__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 +61 -0
- crimson/creatures/damage.py +111 -36
- crimson/creatures/runtime.py +246 -156
- crimson/creatures/spawn.py +7 -3
- 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 +441 -1
- crimson/gameplay.py +905 -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 +58 -273
- crimson/modes/rush_mode.py +12 -43
- crimson/modes/survival_mode.py +71 -328
- 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 +94 -37
- 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 +12 -64
- 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.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
- {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
- grim/config.py +29 -1
- grim/console.py +7 -10
- grim/math.py +12 -0
- crimson/.DS_Store +0 -0
- crimson/creatures/.DS_Store +0 -0
- grim/.DS_Store +0 -0
- {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
|
@@ -11,6 +11,7 @@ from grim.audio import AudioState, update_audio
|
|
|
11
11
|
from grim.console import ConsoleState
|
|
12
12
|
from grim.config import CrimsonConfig
|
|
13
13
|
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
|
|
14
|
+
from grim.math import clamp
|
|
14
15
|
from grim.view import ViewContext
|
|
15
16
|
|
|
16
17
|
from ..gameplay import _creature_find_in_radius, perk_count_get
|
|
@@ -18,20 +19,12 @@ from ..game_world import GameWorld
|
|
|
18
19
|
from ..persistence.highscores import HighScoreRecord
|
|
19
20
|
from ..perks import PerkId
|
|
20
21
|
from ..ui.game_over import GameOverUi
|
|
21
|
-
from ..ui.hud import HudAssets, draw_target_health_bar, load_hud_assets
|
|
22
|
+
from ..ui.hud import HudAssets, HudState, draw_target_health_bar, load_hud_assets
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
24
25
|
from ..persistence.save_status import GameStatus
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def _clamp(value: float, lo: float, hi: float) -> float:
|
|
28
|
-
if value < lo:
|
|
29
|
-
return lo
|
|
30
|
-
if value > hi:
|
|
31
|
-
return hi
|
|
32
|
-
return value
|
|
33
|
-
|
|
34
|
-
|
|
35
28
|
class _ScreenFade(Protocol):
|
|
36
29
|
screen_fade_alpha: float
|
|
37
30
|
|
|
@@ -57,6 +50,7 @@ class BaseGameplayMode:
|
|
|
57
50
|
self._hud_missing: list[str] = []
|
|
58
51
|
self._small: SmallFontData | None = None
|
|
59
52
|
self._hud_assets: HudAssets | None = None
|
|
53
|
+
self._hud_state = HudState()
|
|
60
54
|
|
|
61
55
|
self._default_game_mode_id = int(default_game_mode_id)
|
|
62
56
|
self._config = config
|
|
@@ -141,7 +135,7 @@ class BaseGameplayMode:
|
|
|
141
135
|
return
|
|
142
136
|
hp = float(getattr(creature, "hp", 0.0))
|
|
143
137
|
max_hp = float(getattr(creature, "max_hp", 0.0))
|
|
144
|
-
if
|
|
138
|
+
if max_hp <= 0.0:
|
|
145
139
|
return
|
|
146
140
|
|
|
147
141
|
ratio = hp / max_hp
|
|
@@ -208,8 +202,8 @@ class BaseGameplayMode:
|
|
|
208
202
|
mouse = rl.get_mouse_position()
|
|
209
203
|
screen_w = float(rl.get_screen_width())
|
|
210
204
|
screen_h = float(rl.get_screen_height())
|
|
211
|
-
self._ui_mouse_x =
|
|
212
|
-
self._ui_mouse_y =
|
|
205
|
+
self._ui_mouse_x = clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
|
|
206
|
+
self._ui_mouse_y = clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
|
|
213
207
|
|
|
214
208
|
def _tick_frame(self, dt: float, *, clamp_cursor_pulse: bool = False) -> tuple[float, float]:
|
|
215
209
|
dt_frame = float(dt)
|
|
@@ -248,6 +242,7 @@ class BaseGameplayMode:
|
|
|
248
242
|
self._hud_assets = load_hud_assets(self._assets_root)
|
|
249
243
|
if self._hud_assets.missing:
|
|
250
244
|
self._hud_missing = list(self._hud_assets.missing)
|
|
245
|
+
self._hud_state = HudState()
|
|
251
246
|
|
|
252
247
|
self._game_over_active = False
|
|
253
248
|
self._game_over_record = None
|
|
@@ -283,6 +278,32 @@ class BaseGameplayMode:
|
|
|
283
278
|
self._action = None
|
|
284
279
|
return action
|
|
285
280
|
|
|
281
|
+
def _update_game_over_ui(self, dt: float) -> None:
|
|
282
|
+
record = self._game_over_record
|
|
283
|
+
if record is None:
|
|
284
|
+
self._enter_game_over()
|
|
285
|
+
record = self._game_over_record
|
|
286
|
+
if record is None:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
action = self._game_over_ui.update(
|
|
290
|
+
dt,
|
|
291
|
+
record=record,
|
|
292
|
+
player_name_default=self._player_name_default(),
|
|
293
|
+
play_sfx=self._world.audio_router.play_sfx,
|
|
294
|
+
rand=self._state.rng.rand,
|
|
295
|
+
mouse=self._ui_mouse_pos(),
|
|
296
|
+
)
|
|
297
|
+
if action == "play_again":
|
|
298
|
+
self.open()
|
|
299
|
+
return
|
|
300
|
+
if action == "high_scores":
|
|
301
|
+
self._action = "open_high_scores"
|
|
302
|
+
return
|
|
303
|
+
if action == "main_menu":
|
|
304
|
+
self._action = "back_to_menu"
|
|
305
|
+
self.close_requested = True
|
|
306
|
+
|
|
286
307
|
def draw_pause_background(self) -> None:
|
|
287
308
|
self._world.draw(draw_aim_indicators=False)
|
|
288
309
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...gameplay import GameplayState, PlayerState, most_used_weapon_id_for_player
|
|
4
|
+
from ...persistence.highscores import HighScoreRecord
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def clamp_shots(fired: int, hit: int) -> tuple[int, int]:
|
|
8
|
+
fired = max(0, int(fired))
|
|
9
|
+
hit = max(0, min(int(hit), fired))
|
|
10
|
+
return fired, hit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def shots_from_state(state: GameplayState, *, player_index: int) -> tuple[int, int]:
|
|
14
|
+
fired = 0
|
|
15
|
+
hit = 0
|
|
16
|
+
try:
|
|
17
|
+
fired = int(state.shots_fired[int(player_index)])
|
|
18
|
+
hit = int(state.shots_hit[int(player_index)])
|
|
19
|
+
except Exception:
|
|
20
|
+
fired = 0
|
|
21
|
+
hit = 0
|
|
22
|
+
return clamp_shots(fired, hit)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_highscore_record_for_game_over(
|
|
26
|
+
*,
|
|
27
|
+
state: GameplayState,
|
|
28
|
+
player: PlayerState,
|
|
29
|
+
survival_elapsed_ms: int,
|
|
30
|
+
creature_kill_count: int,
|
|
31
|
+
game_mode_id: int,
|
|
32
|
+
shots_fired: int | None = None,
|
|
33
|
+
shots_hit: int | None = None,
|
|
34
|
+
clamp_shots_hit: bool = True,
|
|
35
|
+
) -> HighScoreRecord:
|
|
36
|
+
record = HighScoreRecord.blank()
|
|
37
|
+
record.score_xp = int(player.experience)
|
|
38
|
+
record.survival_elapsed_ms = int(survival_elapsed_ms)
|
|
39
|
+
record.creature_kill_count = int(creature_kill_count)
|
|
40
|
+
|
|
41
|
+
weapon_id = most_used_weapon_id_for_player(
|
|
42
|
+
state, player_index=int(player.index), fallback_weapon_id=int(player.weapon_id)
|
|
43
|
+
)
|
|
44
|
+
record.most_used_weapon_id = int(weapon_id)
|
|
45
|
+
|
|
46
|
+
if shots_fired is None or shots_hit is None:
|
|
47
|
+
fired, hit = shots_from_state(state, player_index=int(player.index))
|
|
48
|
+
else:
|
|
49
|
+
fired = int(shots_fired)
|
|
50
|
+
hit = int(shots_hit)
|
|
51
|
+
if clamp_shots_hit:
|
|
52
|
+
fired, hit = clamp_shots(fired, hit)
|
|
53
|
+
|
|
54
|
+
record.shots_fired = fired
|
|
55
|
+
record.shots_hit = hit
|
|
56
|
+
record.game_mode_id = int(game_mode_id)
|
|
57
|
+
return record
|
|
58
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, Sequence
|
|
5
|
+
|
|
6
|
+
import pyray as rl
|
|
7
|
+
|
|
8
|
+
from grim.fonts.small import SmallFontData
|
|
9
|
+
from grim.math import clamp
|
|
10
|
+
|
|
11
|
+
from ...gameplay import GameplayState, PerkSelectionState, PlayerState, perk_selection_current_choices, perk_selection_pick
|
|
12
|
+
from ...perks import PerkId, perk_display_description, perk_display_name
|
|
13
|
+
from ...ui.menu_panel import draw_classic_menu_panel
|
|
14
|
+
from ...ui.perk_menu import (
|
|
15
|
+
PERK_MENU_TRANSITION_MS,
|
|
16
|
+
PerkMenuAssets,
|
|
17
|
+
PerkMenuLayout,
|
|
18
|
+
UiButtonState,
|
|
19
|
+
button_draw,
|
|
20
|
+
button_update,
|
|
21
|
+
button_width,
|
|
22
|
+
draw_menu_item,
|
|
23
|
+
draw_ui_text,
|
|
24
|
+
menu_item_hit_rect,
|
|
25
|
+
perk_menu_compute_layout,
|
|
26
|
+
perk_menu_panel_slide_x,
|
|
27
|
+
ui_origin,
|
|
28
|
+
ui_scale,
|
|
29
|
+
wrap_ui_text,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
PlaySfxFn = Callable[[str], None]
|
|
33
|
+
OnCloseFn = Callable[[], None]
|
|
34
|
+
|
|
35
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
36
|
+
UI_SPONSOR_COLOR = rl.Color(255, 255, 255, int(255 * 0.5))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class PerkMenuContext:
|
|
41
|
+
state: GameplayState
|
|
42
|
+
perk_state: PerkSelectionState
|
|
43
|
+
players: Sequence[PlayerState]
|
|
44
|
+
creatures: Sequence[object]
|
|
45
|
+
player: PlayerState
|
|
46
|
+
game_mode: int
|
|
47
|
+
player_count: int
|
|
48
|
+
fx_toggle: int
|
|
49
|
+
|
|
50
|
+
font: SmallFontData | None
|
|
51
|
+
assets: PerkMenuAssets | None
|
|
52
|
+
mouse: rl.Vector2
|
|
53
|
+
fx_detail: bool = False
|
|
54
|
+
play_sfx: PlaySfxFn | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PerkMenuController:
|
|
58
|
+
def __init__(self, *, cancel_label: str = "Cancel", on_close: OnCloseFn | None = None) -> None:
|
|
59
|
+
self._cancel_label = cancel_label
|
|
60
|
+
self._on_close = on_close
|
|
61
|
+
self.reset()
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def open(self) -> bool:
|
|
65
|
+
return bool(self._open)
|
|
66
|
+
|
|
67
|
+
@open.setter
|
|
68
|
+
def open(self, value: bool) -> None:
|
|
69
|
+
if not value and self._open:
|
|
70
|
+
self.close()
|
|
71
|
+
else:
|
|
72
|
+
self._open = bool(value)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def selected_index(self) -> int:
|
|
76
|
+
return int(self._selected_index)
|
|
77
|
+
|
|
78
|
+
@selected_index.setter
|
|
79
|
+
def selected_index(self, value: int) -> None:
|
|
80
|
+
self._selected_index = int(value)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def timeline_ms(self) -> float:
|
|
84
|
+
return float(self._timeline_ms)
|
|
85
|
+
|
|
86
|
+
@timeline_ms.setter
|
|
87
|
+
def timeline_ms(self, value: float) -> None:
|
|
88
|
+
self._timeline_ms = float(value)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def active(self) -> bool:
|
|
92
|
+
return bool(self._open) or self._timeline_ms > 1e-3
|
|
93
|
+
|
|
94
|
+
def reset(self) -> None:
|
|
95
|
+
self._layout = PerkMenuLayout()
|
|
96
|
+
self._cancel_button = UiButtonState(self._cancel_label)
|
|
97
|
+
self._open = False
|
|
98
|
+
self._selected_index = 0
|
|
99
|
+
self._timeline_ms = 0.0
|
|
100
|
+
|
|
101
|
+
def close(self) -> None:
|
|
102
|
+
if not self._open:
|
|
103
|
+
return
|
|
104
|
+
self._open = False
|
|
105
|
+
if self._on_close is not None:
|
|
106
|
+
self._on_close()
|
|
107
|
+
|
|
108
|
+
def open_if_available(self, ctx: PerkMenuContext) -> bool:
|
|
109
|
+
if self._open:
|
|
110
|
+
return True
|
|
111
|
+
if ctx.assets is None:
|
|
112
|
+
return False
|
|
113
|
+
choices = perk_selection_current_choices(
|
|
114
|
+
ctx.state,
|
|
115
|
+
ctx.players,
|
|
116
|
+
ctx.perk_state,
|
|
117
|
+
game_mode=int(ctx.game_mode),
|
|
118
|
+
player_count=int(ctx.player_count),
|
|
119
|
+
)
|
|
120
|
+
if not choices:
|
|
121
|
+
self._open = False
|
|
122
|
+
return False
|
|
123
|
+
if ctx.play_sfx is not None:
|
|
124
|
+
ctx.play_sfx("sfx_ui_panelclick")
|
|
125
|
+
self._open = True
|
|
126
|
+
self._selected_index = 0
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
def tick_timeline(self, dt_ui_ms: float) -> None:
|
|
130
|
+
if self._open:
|
|
131
|
+
self._timeline_ms = clamp(self._timeline_ms + float(dt_ui_ms), 0.0, PERK_MENU_TRANSITION_MS)
|
|
132
|
+
else:
|
|
133
|
+
self._timeline_ms = clamp(self._timeline_ms - float(dt_ui_ms), 0.0, PERK_MENU_TRANSITION_MS)
|
|
134
|
+
|
|
135
|
+
def handle_input(self, ctx: PerkMenuContext, *, dt_frame: float, dt_ui_ms: float) -> None:
|
|
136
|
+
if ctx.assets is None:
|
|
137
|
+
self.close()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
choices = perk_selection_current_choices(
|
|
141
|
+
ctx.state,
|
|
142
|
+
ctx.players,
|
|
143
|
+
ctx.perk_state,
|
|
144
|
+
game_mode=int(ctx.game_mode),
|
|
145
|
+
player_count=int(ctx.player_count),
|
|
146
|
+
)
|
|
147
|
+
if not choices:
|
|
148
|
+
self.close()
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if self._selected_index >= len(choices):
|
|
152
|
+
self._selected_index = 0
|
|
153
|
+
|
|
154
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
|
|
155
|
+
self._selected_index = (self._selected_index + 1) % len(choices)
|
|
156
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
|
|
157
|
+
self._selected_index = (self._selected_index - 1) % len(choices)
|
|
158
|
+
|
|
159
|
+
screen_w = float(rl.get_screen_width())
|
|
160
|
+
screen_h = float(rl.get_screen_height())
|
|
161
|
+
scale = ui_scale(screen_w, screen_h)
|
|
162
|
+
origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
|
|
163
|
+
slide_x = perk_menu_panel_slide_x(self._timeline_ms, width=self._layout.panel_w)
|
|
164
|
+
|
|
165
|
+
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
166
|
+
|
|
167
|
+
master_owned = int(ctx.player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
|
|
168
|
+
expert_owned = int(ctx.player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
|
|
169
|
+
computed = perk_menu_compute_layout(
|
|
170
|
+
self._layout,
|
|
171
|
+
screen_w=screen_w,
|
|
172
|
+
origin_x=origin_x,
|
|
173
|
+
origin_y=origin_y,
|
|
174
|
+
scale=scale,
|
|
175
|
+
choice_count=len(choices),
|
|
176
|
+
expert_owned=expert_owned,
|
|
177
|
+
master_owned=master_owned,
|
|
178
|
+
panel_slide_x=slide_x,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
for idx, perk_id in enumerate(choices):
|
|
182
|
+
label = perk_display_name(int(perk_id), fx_toggle=int(ctx.fx_toggle))
|
|
183
|
+
item_x = computed.list_x
|
|
184
|
+
item_y = computed.list_y + float(idx) * computed.list_step_y
|
|
185
|
+
rect = menu_item_hit_rect(ctx.font, label, x=item_x, y=item_y, scale=scale)
|
|
186
|
+
if rl.check_collision_point_rec(ctx.mouse, rect):
|
|
187
|
+
self._selected_index = idx
|
|
188
|
+
if click:
|
|
189
|
+
if ctx.play_sfx is not None:
|
|
190
|
+
ctx.play_sfx("sfx_ui_buttonclick")
|
|
191
|
+
picked = perk_selection_pick(
|
|
192
|
+
ctx.state,
|
|
193
|
+
ctx.players,
|
|
194
|
+
ctx.perk_state,
|
|
195
|
+
idx,
|
|
196
|
+
game_mode=int(ctx.game_mode),
|
|
197
|
+
player_count=int(ctx.player_count),
|
|
198
|
+
dt=float(dt_frame),
|
|
199
|
+
creatures=ctx.creatures,
|
|
200
|
+
)
|
|
201
|
+
if picked is not None and ctx.play_sfx is not None:
|
|
202
|
+
ctx.play_sfx("sfx_ui_bonus")
|
|
203
|
+
self.close()
|
|
204
|
+
return
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
cancel_w = button_width(
|
|
208
|
+
ctx.font,
|
|
209
|
+
self._cancel_button.label,
|
|
210
|
+
scale=scale,
|
|
211
|
+
force_wide=self._cancel_button.force_wide,
|
|
212
|
+
)
|
|
213
|
+
cancel_x = computed.cancel_x
|
|
214
|
+
cancel_y = computed.cancel_y
|
|
215
|
+
if button_update(
|
|
216
|
+
self._cancel_button,
|
|
217
|
+
x=cancel_x,
|
|
218
|
+
y=cancel_y,
|
|
219
|
+
width=cancel_w,
|
|
220
|
+
dt_ms=float(dt_ui_ms),
|
|
221
|
+
mouse=ctx.mouse,
|
|
222
|
+
click=click,
|
|
223
|
+
):
|
|
224
|
+
if ctx.play_sfx is not None:
|
|
225
|
+
ctx.play_sfx("sfx_ui_buttonclick")
|
|
226
|
+
self.close()
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
|
|
230
|
+
if ctx.play_sfx is not None:
|
|
231
|
+
ctx.play_sfx("sfx_ui_buttonclick")
|
|
232
|
+
picked = perk_selection_pick(
|
|
233
|
+
ctx.state,
|
|
234
|
+
ctx.players,
|
|
235
|
+
ctx.perk_state,
|
|
236
|
+
self._selected_index,
|
|
237
|
+
game_mode=int(ctx.game_mode),
|
|
238
|
+
player_count=int(ctx.player_count),
|
|
239
|
+
dt=float(dt_frame),
|
|
240
|
+
creatures=ctx.creatures,
|
|
241
|
+
)
|
|
242
|
+
if picked is not None and ctx.play_sfx is not None:
|
|
243
|
+
ctx.play_sfx("sfx_ui_bonus")
|
|
244
|
+
self.close()
|
|
245
|
+
|
|
246
|
+
def draw(self, ctx: PerkMenuContext) -> None:
|
|
247
|
+
menu_t = clamp(self._timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
|
|
248
|
+
if menu_t <= 1e-3:
|
|
249
|
+
return
|
|
250
|
+
if ctx.assets is None:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
choices = perk_selection_current_choices(
|
|
254
|
+
ctx.state,
|
|
255
|
+
ctx.players,
|
|
256
|
+
ctx.perk_state,
|
|
257
|
+
game_mode=int(ctx.game_mode),
|
|
258
|
+
player_count=int(ctx.player_count),
|
|
259
|
+
)
|
|
260
|
+
if not choices:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
screen_w = float(rl.get_screen_width())
|
|
264
|
+
screen_h = float(rl.get_screen_height())
|
|
265
|
+
scale = ui_scale(screen_w, screen_h)
|
|
266
|
+
origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
|
|
267
|
+
slide_x = perk_menu_panel_slide_x(self._timeline_ms, width=self._layout.panel_w)
|
|
268
|
+
|
|
269
|
+
master_owned = int(ctx.player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
|
|
270
|
+
expert_owned = int(ctx.player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
|
|
271
|
+
computed = perk_menu_compute_layout(
|
|
272
|
+
self._layout,
|
|
273
|
+
screen_w=screen_w,
|
|
274
|
+
origin_x=origin_x,
|
|
275
|
+
origin_y=origin_y,
|
|
276
|
+
scale=scale,
|
|
277
|
+
choice_count=len(choices),
|
|
278
|
+
expert_owned=expert_owned,
|
|
279
|
+
master_owned=master_owned,
|
|
280
|
+
panel_slide_x=slide_x,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
panel_tex = ctx.assets.menu_panel
|
|
284
|
+
if panel_tex is not None:
|
|
285
|
+
draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=bool(ctx.fx_detail))
|
|
286
|
+
|
|
287
|
+
title_tex = ctx.assets.title_pick_perk
|
|
288
|
+
if title_tex is not None:
|
|
289
|
+
src = rl.Rectangle(0.0, 0.0, float(title_tex.width), float(title_tex.height))
|
|
290
|
+
rl.draw_texture_pro(title_tex, src, computed.title, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
291
|
+
|
|
292
|
+
sponsor = None
|
|
293
|
+
if master_owned:
|
|
294
|
+
sponsor = "extra perks sponsored by the Perk Master"
|
|
295
|
+
elif expert_owned:
|
|
296
|
+
sponsor = "extra perk sponsored by the Perk Expert"
|
|
297
|
+
if sponsor:
|
|
298
|
+
draw_ui_text(ctx.font, sponsor, computed.sponsor_x, computed.sponsor_y, scale=scale, color=UI_SPONSOR_COLOR)
|
|
299
|
+
|
|
300
|
+
for idx, perk_id in enumerate(choices):
|
|
301
|
+
label = perk_display_name(int(perk_id), fx_toggle=int(ctx.fx_toggle))
|
|
302
|
+
item_x = computed.list_x
|
|
303
|
+
item_y = computed.list_y + float(idx) * computed.list_step_y
|
|
304
|
+
rect = menu_item_hit_rect(ctx.font, label, x=item_x, y=item_y, scale=scale)
|
|
305
|
+
hovered = rl.check_collision_point_rec(ctx.mouse, rect) or (idx == self._selected_index)
|
|
306
|
+
draw_menu_item(ctx.font, label, x=item_x, y=item_y, scale=scale, hovered=hovered)
|
|
307
|
+
|
|
308
|
+
selected = choices[self._selected_index]
|
|
309
|
+
desc = perk_display_description(int(selected), fx_toggle=int(ctx.fx_toggle))
|
|
310
|
+
desc_x = float(computed.desc.x)
|
|
311
|
+
desc_y = float(computed.desc.y)
|
|
312
|
+
desc_w = float(computed.desc.width)
|
|
313
|
+
desc_h = float(computed.desc.height)
|
|
314
|
+
desc_scale = scale * 0.85
|
|
315
|
+
desc_lines = wrap_ui_text(ctx.font, desc, max_width=desc_w, scale=desc_scale)
|
|
316
|
+
line_h = float(ctx.font.cell_size * desc_scale) if ctx.font is not None else float(20 * desc_scale)
|
|
317
|
+
y = desc_y
|
|
318
|
+
for line in desc_lines:
|
|
319
|
+
if y + line_h > desc_y + desc_h:
|
|
320
|
+
break
|
|
321
|
+
draw_ui_text(ctx.font, line, desc_x, y, scale=desc_scale, color=UI_TEXT_COLOR)
|
|
322
|
+
y += line_h
|
|
323
|
+
|
|
324
|
+
cancel_w = button_width(ctx.font, self._cancel_button.label, scale=scale, force_wide=self._cancel_button.force_wide)
|
|
325
|
+
button_draw(ctx.assets, ctx.font, self._cancel_button, x=computed.cancel_x, y=computed.cancel_y, width=cancel_w, scale=scale)
|