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.
Files changed (139) hide show
  1. crimson/__init__.py +24 -0
  2. crimson/assets_fetch.py +60 -0
  3. crimson/atlas.py +92 -0
  4. crimson/audio_router.py +155 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +380 -0
  8. crimson/creatures/__init__.py +8 -0
  9. crimson/creatures/ai.py +186 -0
  10. crimson/creatures/anim.py +173 -0
  11. crimson/creatures/damage.py +103 -0
  12. crimson/creatures/runtime.py +1019 -0
  13. crimson/creatures/spawn.py +2871 -0
  14. crimson/debug.py +7 -0
  15. crimson/demo.py +1360 -0
  16. crimson/demo_trial.py +140 -0
  17. crimson/effects.py +1086 -0
  18. crimson/effects_atlas.py +73 -0
  19. crimson/frontend/__init__.py +1 -0
  20. crimson/frontend/assets.py +43 -0
  21. crimson/frontend/boot.py +424 -0
  22. crimson/frontend/menu.py +700 -0
  23. crimson/frontend/panels/__init__.py +1 -0
  24. crimson/frontend/panels/base.py +410 -0
  25. crimson/frontend/panels/controls.py +132 -0
  26. crimson/frontend/panels/mods.py +128 -0
  27. crimson/frontend/panels/options.py +409 -0
  28. crimson/frontend/panels/play_game.py +627 -0
  29. crimson/frontend/panels/stats.py +351 -0
  30. crimson/frontend/transitions.py +31 -0
  31. crimson/game.py +2533 -0
  32. crimson/game_modes.py +15 -0
  33. crimson/game_world.py +652 -0
  34. crimson/gameplay.py +2467 -0
  35. crimson/input_codes.py +176 -0
  36. crimson/modes/__init__.py +1 -0
  37. crimson/modes/base_gameplay_mode.py +219 -0
  38. crimson/modes/quest_mode.py +502 -0
  39. crimson/modes/rush_mode.py +300 -0
  40. crimson/modes/survival_mode.py +792 -0
  41. crimson/modes/tutorial_mode.py +648 -0
  42. crimson/modes/typo_mode.py +472 -0
  43. crimson/paths.py +23 -0
  44. crimson/perks.py +828 -0
  45. crimson/persistence/__init__.py +1 -0
  46. crimson/persistence/highscores.py +385 -0
  47. crimson/persistence/save_status.py +245 -0
  48. crimson/player_damage.py +77 -0
  49. crimson/projectiles.py +1133 -0
  50. crimson/quests/__init__.py +18 -0
  51. crimson/quests/helpers.py +147 -0
  52. crimson/quests/registry.py +49 -0
  53. crimson/quests/results.py +164 -0
  54. crimson/quests/runtime.py +91 -0
  55. crimson/quests/tier1.py +620 -0
  56. crimson/quests/tier2.py +652 -0
  57. crimson/quests/tier3.py +579 -0
  58. crimson/quests/tier4.py +721 -0
  59. crimson/quests/tier5.py +886 -0
  60. crimson/quests/timeline.py +115 -0
  61. crimson/quests/types.py +70 -0
  62. crimson/render/__init__.py +1 -0
  63. crimson/render/terrain_fx.py +88 -0
  64. crimson/render/world_renderer.py +1941 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +67 -0
  67. crimson/sim/world_state.py +422 -0
  68. crimson/terrain_assets.py +19 -0
  69. crimson/tutorial/__init__.py +12 -0
  70. crimson/tutorial/timeline.py +291 -0
  71. crimson/typo/__init__.py +2 -0
  72. crimson/typo/names.py +233 -0
  73. crimson/typo/player.py +43 -0
  74. crimson/typo/spawns.py +73 -0
  75. crimson/typo/typing.py +52 -0
  76. crimson/ui/__init__.py +3 -0
  77. crimson/ui/cursor.py +95 -0
  78. crimson/ui/demo_trial_overlay.py +235 -0
  79. crimson/ui/game_over.py +660 -0
  80. crimson/ui/hud.py +601 -0
  81. crimson/ui/perk_menu.py +388 -0
  82. crimson/views/__init__.py +40 -0
  83. crimson/views/aim_debug.py +276 -0
  84. crimson/views/animations.py +274 -0
  85. crimson/views/arsenal_debug.py +404 -0
  86. crimson/views/audio_bootstrap.py +47 -0
  87. crimson/views/bonuses.py +201 -0
  88. crimson/views/camera_debug.py +359 -0
  89. crimson/views/camera_shake.py +229 -0
  90. crimson/views/corpse_stamp_debug.py +324 -0
  91. crimson/views/decals_debug.py +739 -0
  92. crimson/views/empty.py +19 -0
  93. crimson/views/fonts.py +114 -0
  94. crimson/views/game_over.py +117 -0
  95. crimson/views/ground.py +259 -0
  96. crimson/views/lighting_debug.py +1166 -0
  97. crimson/views/particles.py +293 -0
  98. crimson/views/perk_menu_debug.py +430 -0
  99. crimson/views/perks.py +398 -0
  100. crimson/views/player.py +434 -0
  101. crimson/views/player_sprite_debug.py +314 -0
  102. crimson/views/projectile_fx.py +609 -0
  103. crimson/views/projectile_render_debug.py +393 -0
  104. crimson/views/projectiles.py +221 -0
  105. crimson/views/quest_title_overlay.py +108 -0
  106. crimson/views/registry.py +34 -0
  107. crimson/views/rush.py +16 -0
  108. crimson/views/small_font_debug.py +204 -0
  109. crimson/views/spawn_plan.py +363 -0
  110. crimson/views/sprites.py +214 -0
  111. crimson/views/survival.py +15 -0
  112. crimson/views/terrain.py +132 -0
  113. crimson/views/ui.py +123 -0
  114. crimson/views/wicons.py +166 -0
  115. crimson/weapon_sfx.py +63 -0
  116. crimson/weapons.py +860 -0
  117. crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
  118. crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
  119. crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
  120. crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
  121. grim/__init__.py +20 -0
  122. grim/app.py +92 -0
  123. grim/assets.py +231 -0
  124. grim/audio.py +106 -0
  125. grim/config.py +294 -0
  126. grim/console.py +737 -0
  127. grim/fonts/__init__.py +7 -0
  128. grim/fonts/grim_mono.py +111 -0
  129. grim/fonts/small.py +120 -0
  130. grim/input.py +44 -0
  131. grim/jaz.py +103 -0
  132. grim/math.py +17 -0
  133. grim/music.py +403 -0
  134. grim/paq.py +76 -0
  135. grim/rand.py +37 -0
  136. grim/sfx.py +276 -0
  137. grim/sfx_map.py +103 -0
  138. grim/terrain_render.py +840 -0
  139. grim/view.py +16 -0
crimson/views/perks.py ADDED
@@ -0,0 +1,398 @@
1
+ from __future__ import annotations
2
+
3
+ import pyray as rl
4
+
5
+ from grim.fonts.small import SmallFontData, load_small_font, measure_small_text_width
6
+ from grim.view import ViewContext
7
+
8
+ from ..game_modes import GameMode
9
+ from ..gameplay import GameplayState, PlayerState, perk_selection_current_choices, perk_selection_pick, survival_check_level_up
10
+ from ..perks import PERK_BY_ID, PerkId, perk_display_description, perk_display_name
11
+ from ..ui.perk_menu import (
12
+ PerkMenuLayout,
13
+ UiButtonState,
14
+ button_draw,
15
+ button_update,
16
+ button_width,
17
+ cursor_draw,
18
+ draw_menu_panel,
19
+ draw_menu_item,
20
+ draw_ui_text,
21
+ load_perk_menu_assets,
22
+ menu_item_hit_rect,
23
+ perk_menu_compute_layout,
24
+ ui_origin,
25
+ ui_scale,
26
+ wrap_ui_text,
27
+ )
28
+ from .registry import register_view
29
+
30
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
31
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
32
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
33
+
34
+ UI_SPONSOR_COLOR = rl.Color(255, 255, 255, int(255 * 0.5))
35
+
36
+
37
+ class PerkSelectionView:
38
+ def __init__(self, ctx: ViewContext) -> None:
39
+ self._assets_root = ctx.assets_dir
40
+ self._small: SmallFontData | None = None
41
+ self._missing_assets: list[str] = []
42
+ self._ui_assets = None
43
+ self._layout = PerkMenuLayout()
44
+
45
+ self.close_requested = False
46
+ self._debug_overlay = False
47
+
48
+ self._state = GameplayState()
49
+ self._player = PlayerState(index=0, pos_x=0.0, pos_y=0.0)
50
+ self._game_mode = GameMode.SURVIVAL
51
+ self._player_count = 1
52
+
53
+ self._perk_menu_open = False
54
+ self._perk_menu_selected = 0
55
+
56
+ self._cancel_button = UiButtonState("Cancel")
57
+
58
+ def _ui_text_width(self, text: str, scale: float) -> float:
59
+ if self._small is not None:
60
+ return float(measure_small_text_width(self._small, text, scale))
61
+ return float(rl.measure_text(text, int(20 * scale)))
62
+
63
+ def _reset(self) -> None:
64
+ self._state = GameplayState()
65
+ self._state.rng.srand(0xBEEF)
66
+ self._player = PlayerState(index=0, pos_x=0.0, pos_y=0.0)
67
+ self._game_mode = GameMode.SURVIVAL
68
+ self._player_count = 1
69
+
70
+ self._state.perk_selection.pending_count = 1
71
+ self._state.perk_selection.choices_dirty = True
72
+
73
+ self._perk_menu_open = True
74
+ self._perk_menu_selected = 0
75
+ self._cancel_button = UiButtonState("Cancel")
76
+
77
+ def open(self) -> None:
78
+ self.close_requested = False
79
+ self._missing_assets.clear()
80
+ try:
81
+ self._small = load_small_font(self._assets_root, self._missing_assets)
82
+ except Exception:
83
+ self._small = None
84
+ self._ui_assets = load_perk_menu_assets(self._assets_root)
85
+ if self._ui_assets.missing:
86
+ self._missing_assets.extend(self._ui_assets.missing)
87
+ rl.hide_cursor()
88
+ self._reset()
89
+
90
+ def close(self) -> None:
91
+ rl.show_cursor()
92
+ if self._ui_assets is not None:
93
+ self._ui_assets = None
94
+ if self._small is not None:
95
+ rl.unload_texture(self._small.texture)
96
+ self._small = None
97
+
98
+ def _toggle_perk(self, perk_id: PerkId) -> None:
99
+ idx = int(perk_id)
100
+ value = int(self._player.perk_counts[idx])
101
+ self._player.perk_counts[idx] = 0 if value > 0 else 1
102
+ self._state.perk_selection.choices_dirty = True
103
+
104
+ def update(self, dt: float) -> None:
105
+ dt_ms = float(min(dt, 0.1) * 1000.0)
106
+
107
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F1):
108
+ self._debug_overlay = not self._debug_overlay
109
+
110
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
111
+ if self._perk_menu_open:
112
+ self._perk_menu_open = False
113
+ else:
114
+ self.close_requested = True
115
+ return
116
+
117
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
118
+ self._reset()
119
+ return
120
+
121
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ONE):
122
+ self._player_count = 1
123
+ self._state.perk_selection.choices_dirty = True
124
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TWO):
125
+ self._player_count = 2
126
+ self._state.perk_selection.choices_dirty = True
127
+
128
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_G):
129
+ self._game_mode = GameMode.QUESTS if int(self._game_mode) == int(GameMode.SURVIVAL) else GameMode.SURVIVAL
130
+ self._state.perk_selection.choices_dirty = True
131
+
132
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
133
+ self._state.perk_selection.pending_count = max(0, int(self._state.perk_selection.pending_count) - 1)
134
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
135
+ self._state.perk_selection.pending_count += 1
136
+
137
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_C):
138
+ self._state.perk_selection.choices_dirty = True
139
+
140
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_E):
141
+ self._toggle_perk(PerkId.PERK_EXPERT)
142
+
143
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_M):
144
+ if self._player.perk_counts[int(PerkId.PERK_MASTER)] > 0:
145
+ self._player.perk_counts[int(PerkId.PERK_MASTER)] = 0
146
+ else:
147
+ self._player.perk_counts[int(PerkId.PERK_EXPERT)] = max(1, int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]))
148
+ self._player.perk_counts[int(PerkId.PERK_MASTER)] = 1
149
+ self._state.perk_selection.choices_dirty = True
150
+
151
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_X):
152
+ self._player.experience += 5000
153
+ survival_check_level_up(self._player, self._state.perk_selection)
154
+
155
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
156
+ self._player.health = 100.0
157
+
158
+ if not self._perk_menu_open:
159
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_P) and int(self._state.perk_selection.pending_count) > 0:
160
+ self._perk_menu_open = True
161
+ self._perk_menu_selected = 0
162
+ return
163
+
164
+ perk_state = self._state.perk_selection
165
+ choices = perk_selection_current_choices(
166
+ self._state,
167
+ [self._player],
168
+ perk_state,
169
+ game_mode=self._game_mode,
170
+ player_count=self._player_count,
171
+ )
172
+ if not choices:
173
+ self._perk_menu_open = False
174
+ return
175
+
176
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
177
+ self._perk_menu_selected = (self._perk_menu_selected + 1) % len(choices)
178
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
179
+ self._perk_menu_selected = (self._perk_menu_selected - 1) % len(choices)
180
+
181
+ screen_w = float(rl.get_screen_width())
182
+ screen_h = float(rl.get_screen_height())
183
+ scale = ui_scale(screen_w, screen_h)
184
+ origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
185
+
186
+ mouse = rl.get_mouse_position()
187
+ click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
188
+
189
+ master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
190
+ expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
191
+ computed = perk_menu_compute_layout(
192
+ self._layout,
193
+ screen_w=screen_w,
194
+ origin_x=origin_x,
195
+ origin_y=origin_y,
196
+ scale=scale,
197
+ choice_count=len(choices),
198
+ expert_owned=expert_owned,
199
+ master_owned=master_owned,
200
+ )
201
+
202
+ for idx, perk_id in enumerate(choices):
203
+ label = perk_display_name(int(perk_id))
204
+ item_x = computed.list_x
205
+ item_y = computed.list_y + float(idx) * computed.list_step_y
206
+ rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
207
+ if rl.check_collision_point_rec(mouse, rect):
208
+ self._perk_menu_selected = idx
209
+ if click:
210
+ perk_selection_pick(
211
+ self._state,
212
+ [self._player],
213
+ perk_state,
214
+ idx,
215
+ game_mode=self._game_mode,
216
+ player_count=self._player_count,
217
+ )
218
+ self._perk_menu_open = False
219
+ return
220
+ break
221
+
222
+ cancel_w = button_width(self._small, self._cancel_button.label, scale=scale, force_wide=self._cancel_button.force_wide)
223
+ cancel_x = computed.cancel_x
224
+ button_y = computed.cancel_y
225
+
226
+ cancel_clicked = button_update(
227
+ self._cancel_button,
228
+ x=cancel_x,
229
+ y=button_y,
230
+ width=cancel_w,
231
+ dt_ms=dt_ms,
232
+ mouse=mouse,
233
+ click=click,
234
+ )
235
+ if cancel_clicked:
236
+ self._perk_menu_open = False
237
+ return
238
+
239
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
240
+ perk_selection_pick(
241
+ self._state,
242
+ [self._player],
243
+ perk_state,
244
+ self._perk_menu_selected,
245
+ game_mode=self._game_mode,
246
+ player_count=self._player_count,
247
+ )
248
+ self._perk_menu_open = False
249
+
250
+ def draw(self) -> None:
251
+ rl.clear_background(rl.Color(0, 0, 0, 255))
252
+
253
+ if self._missing_assets and self._debug_overlay:
254
+ draw_ui_text(
255
+ self._small,
256
+ "Missing assets: " + ", ".join(self._missing_assets),
257
+ 24.0,
258
+ 24.0,
259
+ scale=1.0,
260
+ color=UI_ERROR_COLOR,
261
+ )
262
+
263
+ if self._perk_menu_open:
264
+ self._draw_perk_menu()
265
+ if self._debug_overlay:
266
+ self._draw_debug_overlay()
267
+
268
+ def _draw_perk_menu(self) -> None:
269
+ if self._ui_assets is None:
270
+ return
271
+
272
+ perk_state = self._state.perk_selection
273
+ choices = perk_selection_current_choices(
274
+ self._state,
275
+ [self._player],
276
+ perk_state,
277
+ game_mode=self._game_mode,
278
+ player_count=self._player_count,
279
+ )
280
+ if not choices:
281
+ return
282
+
283
+ screen_w = float(rl.get_screen_width())
284
+ screen_h = float(rl.get_screen_height())
285
+ scale = ui_scale(screen_w, screen_h)
286
+ origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
287
+
288
+ master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
289
+ expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
290
+ computed = perk_menu_compute_layout(
291
+ self._layout,
292
+ screen_w=screen_w,
293
+ origin_x=origin_x,
294
+ origin_y=origin_y,
295
+ scale=scale,
296
+ choice_count=len(choices),
297
+ expert_owned=expert_owned,
298
+ master_owned=master_owned,
299
+ )
300
+
301
+ panel_tex = self._ui_assets.menu_panel
302
+ if panel_tex is not None:
303
+ draw_menu_panel(panel_tex, dst=computed.panel)
304
+
305
+ title_tex = self._ui_assets.title_pick_perk
306
+ if title_tex is not None:
307
+ src = rl.Rectangle(0.0, 0.0, float(title_tex.width), float(title_tex.height))
308
+ rl.draw_texture_pro(title_tex, src, computed.title, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
309
+
310
+ sponsor = None
311
+ if master_owned:
312
+ sponsor = "extra perks sponsored by the Perk Master"
313
+ elif expert_owned:
314
+ sponsor = "extra perk sponsored by the Perk Expert"
315
+ if sponsor:
316
+ draw_ui_text(
317
+ self._small,
318
+ sponsor,
319
+ computed.sponsor_x,
320
+ computed.sponsor_y,
321
+ scale=scale,
322
+ color=UI_SPONSOR_COLOR,
323
+ )
324
+
325
+ mouse = rl.get_mouse_position()
326
+ for idx, perk_id in enumerate(choices):
327
+ label = perk_display_name(int(perk_id))
328
+ item_x = computed.list_x
329
+ item_y = computed.list_y + float(idx) * computed.list_step_y
330
+ rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
331
+ hovered = rl.check_collision_point_rec(mouse, rect) or (idx == self._perk_menu_selected)
332
+ draw_menu_item(self._small, label, x=item_x, y=item_y, scale=scale, hovered=hovered)
333
+
334
+ selected = choices[self._perk_menu_selected]
335
+ desc = perk_display_description(int(selected))
336
+ desc_x = float(computed.desc.x)
337
+ desc_y = float(computed.desc.y)
338
+ desc_w = float(computed.desc.width)
339
+ desc_h = float(computed.desc.height)
340
+ desc_scale = scale * 0.85
341
+ desc_lines = wrap_ui_text(self._small, desc, max_width=desc_w, scale=desc_scale)
342
+ line_h = float(self._small.cell_size * desc_scale) if self._small is not None else float(20 * desc_scale)
343
+ y = desc_y
344
+ for line in desc_lines:
345
+ if y + line_h > desc_y + desc_h:
346
+ break
347
+ draw_ui_text(self._small, line, desc_x, y, scale=desc_scale, color=UI_TEXT_COLOR)
348
+ y += line_h
349
+
350
+ cancel_w = button_width(self._small, self._cancel_button.label, scale=scale, force_wide=self._cancel_button.force_wide)
351
+ cancel_x = computed.cancel_x
352
+ button_y = computed.cancel_y
353
+ button_draw(self._ui_assets, self._small, self._cancel_button, x=cancel_x, y=button_y, width=cancel_w, scale=scale)
354
+
355
+ cursor_draw(self._ui_assets, mouse=mouse, scale=scale)
356
+
357
+ def _draw_debug_overlay(self) -> None:
358
+ x = 24.0
359
+ y = 24.0
360
+ scale = 0.9
361
+ line_h = float(self._small.cell_size * scale) if self._small is not None else float(20 * scale)
362
+ perk_state = self._state.perk_selection
363
+ draw_ui_text(self._small, "Perk selection (debug overlay, F1)", x, y, scale=scale, color=UI_TEXT_COLOR)
364
+ y += line_h
365
+ draw_ui_text(
366
+ self._small,
367
+ f"mode={self._game_mode} players={self._player_count} pending={int(perk_state.pending_count)} level={self._player.level} xp={self._player.experience}",
368
+ x,
369
+ y,
370
+ scale=scale,
371
+ color=UI_HINT_COLOR,
372
+ )
373
+ y += line_h
374
+ draw_ui_text(
375
+ self._small,
376
+ "Keys: C reroll X +5000xp E/M toggle Expert/Master 1/2 players G mode R reset P reopen Esc close",
377
+ x,
378
+ y,
379
+ scale=scale,
380
+ color=UI_HINT_COLOR,
381
+ )
382
+ y += line_h
383
+ owned = [
384
+ (perk_display_name(int(meta.perk_id)), int(self._player.perk_counts[int(meta.perk_id)]))
385
+ for meta in PERK_BY_ID.values()
386
+ if int(self._player.perk_counts[int(meta.perk_id)]) > 0 and meta.perk_id != PerkId.ANTIPERK
387
+ ]
388
+ if owned:
389
+ draw_ui_text(self._small, "Owned:", x, y, scale=scale, color=UI_HINT_COLOR)
390
+ y += line_h
391
+ for name, count in owned[:8]:
392
+ draw_ui_text(self._small, f"- {name} x{count}", x, y, scale=scale, color=UI_HINT_COLOR)
393
+ y += line_h
394
+
395
+
396
+ @register_view("perks", "Perk selection (debug)")
397
+ def _create_perk_selection_view(*, ctx: ViewContext) -> PerkSelectionView:
398
+ return PerkSelectionView(ctx)