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.
Files changed (75) hide show
  1. crimson/cli.py +61 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/demo.py +38 -45
  6. crimson/effects.py +7 -13
  7. crimson/frontend/high_scores_layout.py +81 -0
  8. crimson/frontend/panels/base.py +4 -1
  9. crimson/frontend/panels/controls.py +0 -15
  10. crimson/frontend/panels/databases.py +291 -3
  11. crimson/frontend/panels/mods.py +0 -15
  12. crimson/frontend/panels/play_game.py +0 -16
  13. crimson/game.py +441 -1
  14. crimson/gameplay.py +905 -569
  15. crimson/modes/base_gameplay_mode.py +33 -12
  16. crimson/modes/components/__init__.py +2 -0
  17. crimson/modes/components/highscore_record_builder.py +58 -0
  18. crimson/modes/components/perk_menu_controller.py +325 -0
  19. crimson/modes/quest_mode.py +58 -273
  20. crimson/modes/rush_mode.py +12 -43
  21. crimson/modes/survival_mode.py +71 -328
  22. crimson/modes/tutorial_mode.py +46 -247
  23. crimson/modes/typo_mode.py +11 -38
  24. crimson/oracle.py +396 -0
  25. crimson/perks.py +5 -2
  26. crimson/player_damage.py +94 -37
  27. crimson/projectiles.py +539 -320
  28. crimson/render/projectile_draw_registry.py +637 -0
  29. crimson/render/projectile_render_registry.py +110 -0
  30. crimson/render/secondary_projectile_draw_registry.py +206 -0
  31. crimson/render/world_renderer.py +58 -707
  32. crimson/sim/world_state.py +118 -61
  33. crimson/typo/spawns.py +5 -12
  34. crimson/ui/demo_trial_overlay.py +3 -11
  35. crimson/ui/formatting.py +24 -0
  36. crimson/ui/game_over.py +12 -58
  37. crimson/ui/hud.py +72 -39
  38. crimson/ui/layout.py +20 -0
  39. crimson/ui/perk_menu.py +9 -34
  40. crimson/ui/quest_results.py +12 -64
  41. crimson/ui/text_input.py +20 -0
  42. crimson/views/_ui_helpers.py +27 -0
  43. crimson/views/aim_debug.py +15 -32
  44. crimson/views/animations.py +18 -28
  45. crimson/views/arsenal_debug.py +22 -32
  46. crimson/views/bonuses.py +23 -36
  47. crimson/views/camera_debug.py +16 -29
  48. crimson/views/camera_shake.py +9 -33
  49. crimson/views/corpse_stamp_debug.py +13 -21
  50. crimson/views/decals_debug.py +36 -23
  51. crimson/views/fonts.py +8 -25
  52. crimson/views/ground.py +4 -21
  53. crimson/views/lighting_debug.py +42 -45
  54. crimson/views/particles.py +33 -42
  55. crimson/views/perk_menu_debug.py +3 -10
  56. crimson/views/player.py +50 -44
  57. crimson/views/player_sprite_debug.py +24 -31
  58. crimson/views/projectile_fx.py +57 -52
  59. crimson/views/projectile_render_debug.py +24 -33
  60. crimson/views/projectiles.py +24 -37
  61. crimson/views/spawn_plan.py +13 -29
  62. crimson/views/sprites.py +14 -29
  63. crimson/views/terrain.py +6 -23
  64. crimson/views/ui.py +7 -24
  65. crimson/views/wicons.py +28 -33
  66. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  67. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
  68. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
  69. grim/config.py +29 -1
  70. grim/console.py +7 -10
  71. grim/math.py +12 -0
  72. crimson/.DS_Store +0 -0
  73. crimson/creatures/.DS_Store +0 -0
  74. grim/.DS_Store +0 -0
  75. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
crimson/game.py CHANGED
@@ -57,12 +57,78 @@ from .frontend.high_scores_layout import (
57
57
  HS_BUTTON_STEP_Y,
58
58
  HS_BUTTON_X,
59
59
  HS_BUTTON_Y0,
60
+ HS_LOCAL_DATE_X,
61
+ HS_LOCAL_DATE_Y,
62
+ HS_LOCAL_FRAGS_X,
63
+ HS_LOCAL_FRAGS_Y,
64
+ HS_LOCAL_HIT_X,
65
+ HS_LOCAL_HIT_Y,
66
+ HS_LOCAL_LABEL_X,
67
+ HS_LOCAL_LABEL_Y,
68
+ HS_LOCAL_NAME_X,
69
+ HS_LOCAL_NAME_Y,
70
+ HS_LOCAL_RANK_X,
71
+ HS_LOCAL_RANK_Y,
72
+ HS_LOCAL_SCORE_LABEL_X,
73
+ HS_LOCAL_SCORE_LABEL_Y,
74
+ HS_LOCAL_SCORE_VALUE_X,
75
+ HS_LOCAL_SCORE_VALUE_Y,
76
+ HS_LOCAL_TIME_LABEL_X,
77
+ HS_LOCAL_TIME_LABEL_Y,
78
+ HS_LOCAL_TIME_VALUE_X,
79
+ HS_LOCAL_TIME_VALUE_Y,
80
+ HS_LOCAL_WEAPON_X,
81
+ HS_LOCAL_WEAPON_Y,
82
+ HS_LOCAL_WICON_X,
83
+ HS_LOCAL_WICON_Y,
60
84
  HS_LEFT_PANEL_HEIGHT,
61
85
  HS_LEFT_PANEL_POS_X,
62
86
  HS_LEFT_PANEL_POS_Y,
87
+ HS_QUEST_ARROW_X,
88
+ HS_QUEST_ARROW_Y,
89
+ HS_RIGHT_CHECK_X,
90
+ HS_RIGHT_CHECK_Y,
63
91
  HS_RIGHT_PANEL_HEIGHT,
64
92
  HS_RIGHT_PANEL_POS_X,
65
93
  HS_RIGHT_PANEL_POS_Y,
94
+ HS_RIGHT_GAME_MODE_DROP_X,
95
+ HS_RIGHT_GAME_MODE_DROP_Y,
96
+ HS_RIGHT_GAME_MODE_VALUE_X,
97
+ HS_RIGHT_GAME_MODE_VALUE_Y,
98
+ HS_RIGHT_GAME_MODE_WIDGET_W,
99
+ HS_RIGHT_GAME_MODE_WIDGET_X,
100
+ HS_RIGHT_GAME_MODE_WIDGET_Y,
101
+ HS_RIGHT_GAME_MODE_X,
102
+ HS_RIGHT_GAME_MODE_Y,
103
+ HS_RIGHT_NUMBER_PLAYERS_X,
104
+ HS_RIGHT_NUMBER_PLAYERS_Y,
105
+ HS_RIGHT_PLAYER_COUNT_DROP_X,
106
+ HS_RIGHT_PLAYER_COUNT_DROP_Y,
107
+ HS_RIGHT_PLAYER_COUNT_VALUE_X,
108
+ HS_RIGHT_PLAYER_COUNT_VALUE_Y,
109
+ HS_RIGHT_PLAYER_COUNT_WIDGET_W,
110
+ HS_RIGHT_PLAYER_COUNT_WIDGET_X,
111
+ HS_RIGHT_PLAYER_COUNT_WIDGET_Y,
112
+ HS_RIGHT_SCORE_LIST_DROP_X,
113
+ HS_RIGHT_SCORE_LIST_DROP_Y,
114
+ HS_RIGHT_SCORE_LIST_VALUE_X,
115
+ HS_RIGHT_SCORE_LIST_VALUE_Y,
116
+ HS_RIGHT_SCORE_LIST_WIDGET_W,
117
+ HS_RIGHT_SCORE_LIST_WIDGET_X,
118
+ HS_RIGHT_SCORE_LIST_WIDGET_Y,
119
+ HS_RIGHT_SCORE_LIST_X,
120
+ HS_RIGHT_SCORE_LIST_Y,
121
+ HS_RIGHT_SHOW_INTERNET_X,
122
+ HS_RIGHT_SHOW_INTERNET_Y,
123
+ HS_RIGHT_SHOW_SCORES_DROP_X,
124
+ HS_RIGHT_SHOW_SCORES_DROP_Y,
125
+ HS_RIGHT_SHOW_SCORES_VALUE_X,
126
+ HS_RIGHT_SHOW_SCORES_VALUE_Y,
127
+ HS_RIGHT_SHOW_SCORES_WIDGET_W,
128
+ HS_RIGHT_SHOW_SCORES_WIDGET_X,
129
+ HS_RIGHT_SHOW_SCORES_WIDGET_Y,
130
+ HS_RIGHT_SHOW_SCORES_X,
131
+ HS_RIGHT_SHOW_SCORES_Y,
66
132
  )
67
133
  from .frontend.menu import (
68
134
  MENU_PANEL_HEIGHT,
@@ -1801,6 +1867,10 @@ class HighScoresView:
1801
1867
  self._small_font: SmallFontData | None = None
1802
1868
  self._button_tex: rl.Texture2D | None = None
1803
1869
  self._button_textures: UiButtonTextureSet | None = None
1870
+ self._check_on: rl.Texture2D | None = None
1871
+ self._drop_off: rl.Texture2D | None = None
1872
+ self._arrow_tex: rl.Texture2D | None = None
1873
+ self._wicons_tex: rl.Texture2D | None = None
1804
1874
  self._update_button = UiButtonState("Update scores", force_wide=True)
1805
1875
  self._play_button = UiButtonState("Play a game", force_wide=True)
1806
1876
  self._back_button = UiButtonState("Back", force_wide=False)
@@ -1831,6 +1901,16 @@ class HighScoresView:
1831
1901
  self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
1832
1902
  button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
1833
1903
  self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
1904
+ self._check_on = cache.get_or_load("ui_checkOn", "ui/ui_checkOn.jaz").texture
1905
+ self._drop_off = cache.get_or_load("ui_dropOff", "ui/ui_dropDownOff.jaz").texture
1906
+ self._arrow_tex = cache.get_or_load("ui_arrow", "ui/ui_arrow.jaz").texture
1907
+
1908
+ if self._wicons_tex is not None:
1909
+ rl.unload_texture(self._wicons_tex)
1910
+ self._wicons_tex = None
1911
+ wicons_path = self._state.assets_dir / "crimson" / "ui" / "ui_wicons.png"
1912
+ if wicons_path.is_file():
1913
+ self._wicons_tex = rl.load_texture(str(wicons_path))
1834
1914
 
1835
1915
  request = self._state.pending_high_scores
1836
1916
  self._state.pending_high_scores = None
@@ -1866,9 +1946,15 @@ class HighScoresView:
1866
1946
  if self._small_font is not None:
1867
1947
  rl.unload_texture(self._small_font.texture)
1868
1948
  self._small_font = None
1949
+ if self._wicons_tex is not None:
1950
+ rl.unload_texture(self._wicons_tex)
1951
+ self._wicons_tex = None
1869
1952
  self._assets = None
1870
1953
  self._button_tex = None
1871
1954
  self._button_textures = None
1955
+ self._check_on = None
1956
+ self._drop_off = None
1957
+ self._arrow_tex = None
1872
1958
  self._request = None
1873
1959
  self._records = []
1874
1960
  self._scroll_index = 0
@@ -2022,10 +2108,22 @@ class HighScoresView:
2022
2108
  )
2023
2109
 
2024
2110
  title = "High scores - Quests" if int(mode_id) == 3 else f"High scores - {self._mode_label(mode_id, quest_major, quest_minor)}"
2025
- draw_small_text(font, title, left_x0 + 269.0 * scale, left_y0 + 41.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
2111
+ title_x = 269.0
2112
+ if int(mode_id) == 1:
2113
+ # state_14:High scores - Survival title at x=168 (panel left_x0 is -98).
2114
+ title_x = 266.0
2115
+ draw_small_text(font, title, left_x0 + title_x * scale, left_y0 + 41.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
2026
2116
  if int(mode_id) == 3:
2027
2117
  quest_label = f"{int(quest_major)}.{int(quest_minor)}: {self._quest_title(quest_major, quest_minor)}"
2028
2118
  draw_small_text(font, quest_label, left_x0 + 236.0 * scale, left_y0 + 63.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
2119
+ arrow = self._arrow_tex
2120
+ if arrow is not None:
2121
+ dst_w = float(arrow.width) * scale
2122
+ dst_h = float(arrow.height) * scale
2123
+ # state_14 draws ui_arrow.jaz flipped (uv 1..0) to point left.
2124
+ src = rl.Rectangle(float(arrow.width), 0.0, -float(arrow.width), float(arrow.height))
2125
+ dst = rl.Rectangle(left_x0 + HS_QUEST_ARROW_X * scale, left_y0 + HS_QUEST_ARROW_Y * scale, dst_w, dst_h)
2126
+ rl.draw_texture_pro(arrow, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
2029
2127
 
2030
2128
  header_color = rl.Color(255, 255, 255, int(255 * 0.85))
2031
2129
  row_y0 = left_y0 + 84.0 * scale
@@ -2084,9 +2182,351 @@ class HighScoresView:
2084
2182
  scale=scale,
2085
2183
  )
2086
2184
 
2185
+ self._draw_right_panel(font=font, right_x0=right_x0, right_y0=right_y0, scale=scale, mode_id=mode_id, highlight_rank=highlight_rank)
2087
2186
  self._draw_sign(assets)
2088
2187
  _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
2089
2188
 
2189
+ def _draw_right_panel(
2190
+ self,
2191
+ *,
2192
+ font: SmallFontData,
2193
+ right_x0: float,
2194
+ right_y0: float,
2195
+ scale: float,
2196
+ mode_id: int,
2197
+ highlight_rank: int | None,
2198
+ ) -> None:
2199
+ if int(mode_id) == 3:
2200
+ self._draw_right_panel_quest_options(font=font, right_x0=right_x0, right_y0=right_y0, scale=scale)
2201
+ return
2202
+ self._draw_right_panel_local_score(
2203
+ font=font,
2204
+ right_x0=right_x0,
2205
+ right_y0=right_y0,
2206
+ scale=scale,
2207
+ highlight_rank=highlight_rank,
2208
+ )
2209
+
2210
+ def _draw_right_panel_quest_options(self, *, font: SmallFontData, right_x0: float, right_y0: float, scale: float) -> None:
2211
+ text_scale = 1.0 * scale
2212
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
2213
+
2214
+ check_on = self._check_on
2215
+ if check_on is not None:
2216
+ check_w = float(check_on.width) * scale
2217
+ check_h = float(check_on.height) * scale
2218
+ rl.draw_texture_pro(
2219
+ check_on,
2220
+ rl.Rectangle(0.0, 0.0, float(check_on.width), float(check_on.height)),
2221
+ rl.Rectangle(right_x0 + HS_RIGHT_CHECK_X * scale, right_y0 + HS_RIGHT_CHECK_Y * scale, check_w, check_h),
2222
+ rl.Vector2(0.0, 0.0),
2223
+ 0.0,
2224
+ rl.WHITE,
2225
+ )
2226
+ draw_small_text(
2227
+ font,
2228
+ "Show internet scores",
2229
+ right_x0 + HS_RIGHT_SHOW_INTERNET_X * scale,
2230
+ right_y0 + HS_RIGHT_SHOW_INTERNET_Y * scale,
2231
+ text_scale,
2232
+ text_color,
2233
+ )
2234
+ draw_small_text(
2235
+ font,
2236
+ "Number of players",
2237
+ right_x0 + HS_RIGHT_NUMBER_PLAYERS_X * scale,
2238
+ right_y0 + HS_RIGHT_NUMBER_PLAYERS_Y * scale,
2239
+ text_scale,
2240
+ text_color,
2241
+ )
2242
+ draw_small_text(
2243
+ font,
2244
+ "Game mode",
2245
+ right_x0 + HS_RIGHT_GAME_MODE_X * scale,
2246
+ right_y0 + HS_RIGHT_GAME_MODE_Y * scale,
2247
+ text_scale,
2248
+ text_color,
2249
+ )
2250
+ draw_small_text(
2251
+ font,
2252
+ "Show scores:",
2253
+ right_x0 + HS_RIGHT_SHOW_SCORES_X * scale,
2254
+ right_y0 + HS_RIGHT_SHOW_SCORES_Y * scale,
2255
+ text_scale,
2256
+ text_color,
2257
+ )
2258
+ draw_small_text(
2259
+ font,
2260
+ "Selected score list:",
2261
+ right_x0 + HS_RIGHT_SCORE_LIST_X * scale,
2262
+ right_y0 + HS_RIGHT_SCORE_LIST_Y * scale,
2263
+ text_scale,
2264
+ text_color,
2265
+ )
2266
+
2267
+ # Closed list widgets (state_14 quest variant): white border + black fill.
2268
+ widget_h = 16.0 * scale
2269
+ for wx, wy, ww in (
2270
+ (HS_RIGHT_PLAYER_COUNT_WIDGET_X, HS_RIGHT_PLAYER_COUNT_WIDGET_Y, HS_RIGHT_PLAYER_COUNT_WIDGET_W),
2271
+ (HS_RIGHT_GAME_MODE_WIDGET_X, HS_RIGHT_GAME_MODE_WIDGET_Y, HS_RIGHT_GAME_MODE_WIDGET_W),
2272
+ (HS_RIGHT_SHOW_SCORES_WIDGET_X, HS_RIGHT_SHOW_SCORES_WIDGET_Y, HS_RIGHT_SHOW_SCORES_WIDGET_W),
2273
+ (HS_RIGHT_SCORE_LIST_WIDGET_X, HS_RIGHT_SCORE_LIST_WIDGET_Y, HS_RIGHT_SCORE_LIST_WIDGET_W),
2274
+ ):
2275
+ x = right_x0 + float(wx) * scale
2276
+ y = right_y0 + float(wy) * scale
2277
+ w = float(ww) * scale
2278
+ rl.draw_rectangle(int(x), int(y), int(w), int(widget_h), rl.WHITE)
2279
+ rl.draw_rectangle(int(x) + 1, int(y) + 1, max(0, int(w) - 2), max(0, int(widget_h) - 2), rl.BLACK)
2280
+
2281
+ # Values (static in the oracle).
2282
+ draw_small_text(
2283
+ font,
2284
+ "1 player",
2285
+ right_x0 + HS_RIGHT_PLAYER_COUNT_VALUE_X * scale,
2286
+ right_y0 + HS_RIGHT_PLAYER_COUNT_VALUE_Y * scale,
2287
+ text_scale,
2288
+ text_color,
2289
+ )
2290
+ draw_small_text(
2291
+ font,
2292
+ "Quests",
2293
+ right_x0 + HS_RIGHT_GAME_MODE_VALUE_X * scale,
2294
+ right_y0 + HS_RIGHT_GAME_MODE_VALUE_Y * scale,
2295
+ text_scale,
2296
+ text_color,
2297
+ )
2298
+ draw_small_text(
2299
+ font,
2300
+ "Best of all time",
2301
+ right_x0 + HS_RIGHT_SHOW_SCORES_VALUE_X * scale,
2302
+ right_y0 + HS_RIGHT_SHOW_SCORES_VALUE_Y * scale,
2303
+ text_scale,
2304
+ text_color,
2305
+ )
2306
+ draw_small_text(
2307
+ font,
2308
+ "default",
2309
+ right_x0 + HS_RIGHT_SCORE_LIST_VALUE_X * scale,
2310
+ right_y0 + HS_RIGHT_SCORE_LIST_VALUE_Y * scale,
2311
+ text_scale,
2312
+ text_color,
2313
+ )
2314
+
2315
+ drop_off = self._drop_off
2316
+ if drop_off is None:
2317
+ return
2318
+ drop_w = float(drop_off.width) * scale
2319
+ drop_h = float(drop_off.height) * scale
2320
+ for dx, dy in (
2321
+ (HS_RIGHT_PLAYER_COUNT_DROP_X, HS_RIGHT_PLAYER_COUNT_DROP_Y),
2322
+ (HS_RIGHT_GAME_MODE_DROP_X, HS_RIGHT_GAME_MODE_DROP_Y),
2323
+ (HS_RIGHT_SHOW_SCORES_DROP_X, HS_RIGHT_SHOW_SCORES_DROP_Y),
2324
+ (HS_RIGHT_SCORE_LIST_DROP_X, HS_RIGHT_SCORE_LIST_DROP_Y),
2325
+ ):
2326
+ rl.draw_texture_pro(
2327
+ drop_off,
2328
+ rl.Rectangle(0.0, 0.0, float(drop_off.width), float(drop_off.height)),
2329
+ rl.Rectangle(right_x0 + float(dx) * scale, right_y0 + float(dy) * scale, drop_w, drop_h),
2330
+ rl.Vector2(0.0, 0.0),
2331
+ 0.0,
2332
+ rl.WHITE,
2333
+ )
2334
+
2335
+ def _draw_right_panel_local_score(
2336
+ self,
2337
+ *,
2338
+ font: SmallFontData,
2339
+ right_x0: float,
2340
+ right_y0: float,
2341
+ scale: float,
2342
+ highlight_rank: int | None,
2343
+ ) -> None:
2344
+ if not self._records:
2345
+ return
2346
+ idx = int(highlight_rank) if highlight_rank is not None else int(self._scroll_index)
2347
+ if idx < 0:
2348
+ idx = 0
2349
+ if idx >= len(self._records):
2350
+ idx = len(self._records) - 1
2351
+ entry = self._records[idx]
2352
+
2353
+ text_scale = 1.0 * scale
2354
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
2355
+
2356
+ name = ""
2357
+ try:
2358
+ name = str(entry.name())
2359
+ except Exception:
2360
+ name = ""
2361
+ if not name:
2362
+ name = "???"
2363
+ draw_small_text(font, name, right_x0 + HS_LOCAL_NAME_X * scale, right_y0 + HS_LOCAL_NAME_Y * scale, text_scale, text_color)
2364
+ draw_small_text(
2365
+ font,
2366
+ "Local score",
2367
+ right_x0 + HS_LOCAL_LABEL_X * scale,
2368
+ right_y0 + HS_LOCAL_LABEL_Y * scale,
2369
+ text_scale,
2370
+ text_color,
2371
+ )
2372
+
2373
+ date_text = self._format_score_date(entry)
2374
+ if date_text:
2375
+ draw_small_text(
2376
+ font,
2377
+ date_text,
2378
+ right_x0 + HS_LOCAL_DATE_X * scale,
2379
+ right_y0 + HS_LOCAL_DATE_Y * scale,
2380
+ text_scale,
2381
+ text_color,
2382
+ )
2383
+
2384
+ draw_small_text(
2385
+ font,
2386
+ "Score",
2387
+ right_x0 + HS_LOCAL_SCORE_LABEL_X * scale,
2388
+ right_y0 + HS_LOCAL_SCORE_LABEL_Y * scale,
2389
+ text_scale,
2390
+ text_color,
2391
+ )
2392
+ draw_small_text(
2393
+ font,
2394
+ "Game time",
2395
+ right_x0 + HS_LOCAL_TIME_LABEL_X * scale,
2396
+ right_y0 + HS_LOCAL_TIME_LABEL_Y * scale,
2397
+ text_scale,
2398
+ text_color,
2399
+ )
2400
+
2401
+ score_value = f"{int(getattr(entry, 'score_xp', 0))}"
2402
+ draw_small_text(
2403
+ font,
2404
+ score_value,
2405
+ right_x0 + HS_LOCAL_SCORE_VALUE_X * scale,
2406
+ right_y0 + HS_LOCAL_SCORE_VALUE_Y * scale,
2407
+ text_scale,
2408
+ text_color,
2409
+ )
2410
+
2411
+ elapsed_ms = int(getattr(entry, "survival_elapsed_ms", 0) or 0)
2412
+ draw_small_text(
2413
+ font,
2414
+ self._format_elapsed_mm_ss(elapsed_ms),
2415
+ right_x0 + HS_LOCAL_TIME_VALUE_X * scale,
2416
+ right_y0 + HS_LOCAL_TIME_VALUE_Y * scale,
2417
+ text_scale,
2418
+ text_color,
2419
+ )
2420
+
2421
+ draw_small_text(
2422
+ font,
2423
+ f"Rank: {self._ordinal(idx + 1)}",
2424
+ right_x0 + HS_LOCAL_RANK_X * scale,
2425
+ right_y0 + HS_LOCAL_RANK_Y * scale,
2426
+ text_scale,
2427
+ text_color,
2428
+ )
2429
+
2430
+ frags = int(getattr(entry, "creature_kill_count", 0) or 0)
2431
+ draw_small_text(
2432
+ font,
2433
+ f"Frags: {frags}",
2434
+ right_x0 + HS_LOCAL_FRAGS_X * scale,
2435
+ right_y0 + HS_LOCAL_FRAGS_Y * scale,
2436
+ text_scale,
2437
+ text_color,
2438
+ )
2439
+
2440
+ shots_fired = int(getattr(entry, "shots_fired", 0) or 0)
2441
+ shots_hit = int(getattr(entry, "shots_hit", 0) or 0)
2442
+ hit_pct = 0
2443
+ if shots_fired > 0:
2444
+ hit_pct = int((shots_hit * 100) // shots_fired)
2445
+ draw_small_text(
2446
+ font,
2447
+ f"Hit %: {hit_pct}%",
2448
+ right_x0 + HS_LOCAL_HIT_X * scale,
2449
+ right_y0 + HS_LOCAL_HIT_Y * scale,
2450
+ text_scale,
2451
+ text_color,
2452
+ )
2453
+
2454
+ weapon_id = int(getattr(entry, "most_used_weapon_id", 0) or 0)
2455
+ weapon_name, icon_index = self._weapon_label_and_icon(weapon_id)
2456
+ if icon_index is not None:
2457
+ self._draw_wicon(icon_index, x=right_x0 + HS_LOCAL_WICON_X * scale, y=right_y0 + HS_LOCAL_WICON_Y * scale, scale=scale)
2458
+ draw_small_text(
2459
+ font,
2460
+ weapon_name,
2461
+ right_x0 + HS_LOCAL_WEAPON_X * scale,
2462
+ right_y0 + HS_LOCAL_WEAPON_Y * scale,
2463
+ text_scale,
2464
+ text_color,
2465
+ )
2466
+
2467
+ def _draw_wicon(self, icon_index: int, *, x: float, y: float, scale: float) -> None:
2468
+ tex = self._wicons_tex
2469
+ if tex is None:
2470
+ return
2471
+ idx = int(icon_index)
2472
+ if idx < 0 or idx > 31:
2473
+ return
2474
+ cols = 4
2475
+ rows = 8
2476
+ icon_w = float(tex.width) / float(cols)
2477
+ icon_h = float(tex.height) / float(rows)
2478
+ src_x = float(idx % cols) * icon_w
2479
+ src_y = float(idx // cols) * icon_h
2480
+ rl.draw_texture_pro(
2481
+ tex,
2482
+ rl.Rectangle(src_x, src_y, icon_w, icon_h),
2483
+ rl.Rectangle(float(x), float(y), icon_w * scale, icon_h * scale),
2484
+ rl.Vector2(0.0, 0.0),
2485
+ 0.0,
2486
+ rl.WHITE,
2487
+ )
2488
+
2489
+ @staticmethod
2490
+ def _ordinal(value: int) -> str:
2491
+ n = int(value)
2492
+ if 10 <= (n % 100) <= 20:
2493
+ return f"{n}th"
2494
+ suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
2495
+ return f"{n}{suffix}"
2496
+
2497
+ @staticmethod
2498
+ def _format_elapsed_mm_ss(value_ms: int) -> str:
2499
+ total = max(0, int(value_ms)) // 1000
2500
+ minutes, seconds = divmod(total, 60)
2501
+ return f"{minutes}:{seconds:02d}"
2502
+
2503
+ @staticmethod
2504
+ def _format_score_date(entry: object) -> str:
2505
+ try:
2506
+ day = int(getattr(entry, "day", 0) or 0)
2507
+ month = int(getattr(entry, "month", 0) or 0)
2508
+ year_off = int(getattr(entry, "year_offset", 0) or 0)
2509
+ except Exception:
2510
+ return ""
2511
+ if day <= 0 or month <= 0:
2512
+ return ""
2513
+ months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
2514
+ month_name = months[month - 1] if 1 <= month <= 12 else f"{month}"
2515
+ year = 2000 + year_off if year_off >= 0 else 2000
2516
+ return f"{day}. {month_name} {year}"
2517
+
2518
+ @staticmethod
2519
+ def _weapon_label_and_icon(weapon_id: int) -> tuple[str, int | None]:
2520
+ try:
2521
+ from .weapons import WEAPON_BY_ID
2522
+ except Exception:
2523
+ WEAPON_BY_ID = {}
2524
+ weapon = WEAPON_BY_ID.get(int(weapon_id))
2525
+ if weapon is None:
2526
+ return f"Weapon {int(weapon_id)}", None
2527
+ name = weapon.name or f"weapon_{int(weapon.weapon_id)}"
2528
+ return name, weapon.icon_index
2529
+
2090
2530
  def _draw_sign(self, assets: MenuAssets) -> None:
2091
2531
  if assets.sign is None:
2092
2532
  return