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,472 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
import pyray as rl
|
|
8
|
+
|
|
9
|
+
from grim.assets import PaqTextureCache
|
|
10
|
+
from grim.audio import AudioState
|
|
11
|
+
from grim.config import CrimsonConfig
|
|
12
|
+
from grim.view import ViewContext
|
|
13
|
+
|
|
14
|
+
from ..creatures.spawn import CreatureFlags, CreatureInit, CreatureTypeId
|
|
15
|
+
from ..game_modes import GameMode
|
|
16
|
+
from ..gameplay import most_used_weapon_id_for_player
|
|
17
|
+
from ..typo.player import build_typo_player_input, enforce_typo_player_frame
|
|
18
|
+
from ..persistence.highscores import HighScoreRecord
|
|
19
|
+
from ..typo.names import CreatureNameTable, load_typo_dictionary
|
|
20
|
+
from ..typo.spawns import tick_typo_spawns
|
|
21
|
+
from ..typo.typing import TypingBuffer
|
|
22
|
+
from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
|
|
23
|
+
from ..ui.hud import draw_hud_overlay
|
|
24
|
+
from ..ui.perk_menu import load_perk_menu_assets
|
|
25
|
+
from .base_gameplay_mode import BaseGameplayMode
|
|
26
|
+
|
|
27
|
+
WORLD_SIZE = 1024.0
|
|
28
|
+
|
|
29
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
30
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
31
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
32
|
+
|
|
33
|
+
NAME_LABEL_SCALE = 1.0
|
|
34
|
+
NAME_LABEL_BG_ALPHA = 0.67
|
|
35
|
+
|
|
36
|
+
# Original typoshooter input box constants (from 0x004457C0)
|
|
37
|
+
TYPING_PANEL_WIDTH = 182.0
|
|
38
|
+
TYPING_PANEL_HEIGHT = 53.0
|
|
39
|
+
TYPING_PANEL_ALPHA = 0.7
|
|
40
|
+
TYPING_TEXT_X = 6.0
|
|
41
|
+
TYPING_PROMPT = ">"
|
|
42
|
+
TYPING_CURSOR = "_"
|
|
43
|
+
TYPING_CURSOR_X_OFFSET = 14.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class _TypoState:
|
|
48
|
+
elapsed_ms: int = 0
|
|
49
|
+
spawn_cooldown_ms: int = 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TypoShooterMode(BaseGameplayMode):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
ctx: ViewContext,
|
|
56
|
+
*,
|
|
57
|
+
texture_cache: PaqTextureCache | None = None,
|
|
58
|
+
config: CrimsonConfig | None = None,
|
|
59
|
+
audio: AudioState | None = None,
|
|
60
|
+
audio_rng: random.Random | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
ctx,
|
|
64
|
+
world_size=WORLD_SIZE,
|
|
65
|
+
default_game_mode_id=int(GameMode.TYPO),
|
|
66
|
+
demo_mode_active=False,
|
|
67
|
+
difficulty_level=0,
|
|
68
|
+
hardcore=False,
|
|
69
|
+
texture_cache=texture_cache,
|
|
70
|
+
config=config,
|
|
71
|
+
audio=audio,
|
|
72
|
+
audio_rng=audio_rng,
|
|
73
|
+
)
|
|
74
|
+
self._typo = _TypoState()
|
|
75
|
+
self._typing = TypingBuffer()
|
|
76
|
+
self._names = CreatureNameTable.sized(0)
|
|
77
|
+
self._aim_target_x = 0.0
|
|
78
|
+
self._aim_target_y = 0.0
|
|
79
|
+
self._unique_words: list[str] | None = None
|
|
80
|
+
|
|
81
|
+
self._ui_assets = None
|
|
82
|
+
|
|
83
|
+
def open(self) -> None:
|
|
84
|
+
super().open()
|
|
85
|
+
self._ui_assets = load_perk_menu_assets(self._assets_root)
|
|
86
|
+
if self._ui_assets.missing:
|
|
87
|
+
self._missing_assets.extend(self._ui_assets.missing)
|
|
88
|
+
self._typo = _TypoState()
|
|
89
|
+
self._typing = TypingBuffer()
|
|
90
|
+
self._names = CreatureNameTable.sized(len(self._creatures.entries))
|
|
91
|
+
self._unique_words = None
|
|
92
|
+
|
|
93
|
+
dictionary_path = self._base_dir / "typo_dictionary.txt"
|
|
94
|
+
if dictionary_path.is_file():
|
|
95
|
+
words = load_typo_dictionary(dictionary_path)
|
|
96
|
+
if words:
|
|
97
|
+
self._unique_words = words
|
|
98
|
+
|
|
99
|
+
self._aim_target_x = float(self._player.pos_x) + 128.0
|
|
100
|
+
self._aim_target_y = float(self._player.pos_y)
|
|
101
|
+
|
|
102
|
+
enforce_typo_player_frame(self._player)
|
|
103
|
+
|
|
104
|
+
def close(self) -> None:
|
|
105
|
+
if self._ui_assets is not None:
|
|
106
|
+
self._ui_assets = None
|
|
107
|
+
super().close()
|
|
108
|
+
|
|
109
|
+
def _handle_input(self) -> None:
|
|
110
|
+
if self._game_over_active:
|
|
111
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
112
|
+
self._action = "back_to_menu"
|
|
113
|
+
self.close_requested = True
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
|
|
117
|
+
self._paused = not self._paused
|
|
118
|
+
|
|
119
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
120
|
+
self.close_requested = True
|
|
121
|
+
|
|
122
|
+
def _active_mask(self) -> list[bool]:
|
|
123
|
+
return [bool(entry.active) for entry in self._creatures.entries]
|
|
124
|
+
|
|
125
|
+
def _handle_typing_input(self) -> tuple[bool, bool]:
|
|
126
|
+
fire_pressed = False
|
|
127
|
+
reload_pressed = False
|
|
128
|
+
|
|
129
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
130
|
+
self._typing.backspace()
|
|
131
|
+
if self._world.audio_router is not None:
|
|
132
|
+
key = "sfx_ui_typeclick_01" if (self._state.rng.rand() & 1) == 0 else "sfx_ui_typeclick_02"
|
|
133
|
+
self._world.audio_router.play_sfx(key)
|
|
134
|
+
|
|
135
|
+
codepoint = int(rl.get_char_pressed())
|
|
136
|
+
while codepoint > 0:
|
|
137
|
+
if codepoint not in (13, 8) and 0x20 <= codepoint <= 0xFF:
|
|
138
|
+
try:
|
|
139
|
+
ch = chr(codepoint)
|
|
140
|
+
except ValueError:
|
|
141
|
+
ch = ""
|
|
142
|
+
if ch:
|
|
143
|
+
self._typing.push_char(ch)
|
|
144
|
+
if self._world.audio_router is not None:
|
|
145
|
+
key = "sfx_ui_typeclick_01" if (self._state.rng.rand() & 1) == 0 else "sfx_ui_typeclick_02"
|
|
146
|
+
self._world.audio_router.play_sfx(key)
|
|
147
|
+
codepoint = int(rl.get_char_pressed())
|
|
148
|
+
|
|
149
|
+
enter_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)
|
|
150
|
+
if enter_pressed:
|
|
151
|
+
had_text = bool(self._typing.text)
|
|
152
|
+
active = self._active_mask()
|
|
153
|
+
|
|
154
|
+
def _find_target(name: str) -> int | None:
|
|
155
|
+
return self._names.find_by_name(name, active_mask=active)
|
|
156
|
+
|
|
157
|
+
result = self._typing.enter(find_target=_find_target)
|
|
158
|
+
if had_text and self._world.audio_router is not None:
|
|
159
|
+
self._world.audio_router.play_sfx("sfx_ui_typeenter")
|
|
160
|
+
if result.fire_requested and result.target_creature_idx is not None:
|
|
161
|
+
target_idx = int(result.target_creature_idx)
|
|
162
|
+
if 0 <= target_idx < len(self._creatures.entries):
|
|
163
|
+
creature = self._creatures.entries[target_idx]
|
|
164
|
+
if creature.active:
|
|
165
|
+
self._aim_target_x = float(creature.x)
|
|
166
|
+
self._aim_target_y = float(creature.y)
|
|
167
|
+
fire_pressed = True
|
|
168
|
+
if result.reload_requested:
|
|
169
|
+
reload_pressed = True
|
|
170
|
+
|
|
171
|
+
return fire_pressed, reload_pressed
|
|
172
|
+
|
|
173
|
+
def _spawn_tinted_creature(
|
|
174
|
+
self, *, type_id: CreatureTypeId, pos_x: float, pos_y: float, tint_rgba: tuple[float, float, float, float]
|
|
175
|
+
) -> int:
|
|
176
|
+
rand = self._state.rng.rand
|
|
177
|
+
heading = float(int(rand()) % 314) * 0.01
|
|
178
|
+
size = float(int(rand()) % 20 + 47)
|
|
179
|
+
|
|
180
|
+
flags = CreatureFlags(0)
|
|
181
|
+
move_speed = 1.7
|
|
182
|
+
if int(type_id) in (int(CreatureTypeId.SPIDER_SP1), int(CreatureTypeId.SPIDER_SP2)):
|
|
183
|
+
flags |= CreatureFlags.AI7_LINK_TIMER
|
|
184
|
+
move_speed *= 1.2
|
|
185
|
+
size *= 0.8
|
|
186
|
+
|
|
187
|
+
init = CreatureInit(
|
|
188
|
+
origin_template_id=0,
|
|
189
|
+
pos_x=float(pos_x),
|
|
190
|
+
pos_y=float(pos_y),
|
|
191
|
+
heading=float(heading),
|
|
192
|
+
phase_seed=0.0,
|
|
193
|
+
type_id=type_id,
|
|
194
|
+
flags=flags,
|
|
195
|
+
ai_mode=2,
|
|
196
|
+
health=1.0,
|
|
197
|
+
max_health=1.0,
|
|
198
|
+
move_speed=float(move_speed),
|
|
199
|
+
reward_value=1.0,
|
|
200
|
+
size=float(size),
|
|
201
|
+
contact_damage=100.0,
|
|
202
|
+
tint=tint_rgba,
|
|
203
|
+
)
|
|
204
|
+
return self._creatures.spawn_init(init, rand=rand)
|
|
205
|
+
|
|
206
|
+
def _enter_game_over(self) -> None:
|
|
207
|
+
if self._game_over_active:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
record = HighScoreRecord.blank()
|
|
211
|
+
record.score_xp = int(self._player.experience)
|
|
212
|
+
record.survival_elapsed_ms = int(self._typo.elapsed_ms)
|
|
213
|
+
record.creature_kill_count = int(self._creatures.kill_count)
|
|
214
|
+
weapon_id = most_used_weapon_id_for_player(
|
|
215
|
+
self._state, player_index=int(self._player.index), fallback_weapon_id=int(self._player.weapon_id)
|
|
216
|
+
)
|
|
217
|
+
record.most_used_weapon_id = int(weapon_id)
|
|
218
|
+
record.shots_fired = int(self._typing.shots_fired)
|
|
219
|
+
record.shots_hit = int(self._typing.shots_hit)
|
|
220
|
+
record.game_mode_id = int(GameMode.TYPO)
|
|
221
|
+
|
|
222
|
+
self._game_over_record = record
|
|
223
|
+
self._game_over_ui.open()
|
|
224
|
+
self._game_over_active = True
|
|
225
|
+
|
|
226
|
+
def _update_game_over_ui(self, dt: float) -> None:
|
|
227
|
+
record = self._game_over_record
|
|
228
|
+
if record is None:
|
|
229
|
+
self._enter_game_over()
|
|
230
|
+
record = self._game_over_record
|
|
231
|
+
if record is None:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
action = self._game_over_ui.update(
|
|
235
|
+
dt,
|
|
236
|
+
record=record,
|
|
237
|
+
player_name_default=self._player_name_default(),
|
|
238
|
+
play_sfx=self._world.audio_router.play_sfx,
|
|
239
|
+
rand=self._state.rng.rand,
|
|
240
|
+
mouse=self._ui_mouse_pos(),
|
|
241
|
+
)
|
|
242
|
+
if action == "play_again":
|
|
243
|
+
self.open()
|
|
244
|
+
return
|
|
245
|
+
if action == "high_scores":
|
|
246
|
+
self._action = "open_high_scores"
|
|
247
|
+
return
|
|
248
|
+
if action == "main_menu":
|
|
249
|
+
self._action = "back_to_menu"
|
|
250
|
+
self.close_requested = True
|
|
251
|
+
|
|
252
|
+
def update(self, dt: float) -> None:
|
|
253
|
+
self._update_audio(dt)
|
|
254
|
+
|
|
255
|
+
dt_frame = self._tick_frame(dt)[0]
|
|
256
|
+
self._handle_input()
|
|
257
|
+
|
|
258
|
+
if self._game_over_active:
|
|
259
|
+
self._update_game_over_ui(dt)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
dt_world = 0.0 if self._paused else dt_frame
|
|
263
|
+
|
|
264
|
+
# Native: delay game-over transition until the trooper death animation finishes
|
|
265
|
+
# (checks `death_timer < 0.0` in the main gameplay loop).
|
|
266
|
+
if self._player.health <= 0.0:
|
|
267
|
+
if dt_world > 0.0:
|
|
268
|
+
self._player.death_timer -= float(dt_world) * 20.0
|
|
269
|
+
if self._player.death_timer < 0.0:
|
|
270
|
+
self._enter_game_over()
|
|
271
|
+
self._update_game_over_ui(dt)
|
|
272
|
+
return
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
fire_pressed = False
|
|
276
|
+
reload_pressed = False
|
|
277
|
+
if dt_world > 0.0:
|
|
278
|
+
fire_pressed, reload_pressed = self._handle_typing_input()
|
|
279
|
+
|
|
280
|
+
if dt_world <= 0.0:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
enforce_typo_player_frame(self._player)
|
|
284
|
+
input_state = build_typo_player_input(
|
|
285
|
+
aim_x=float(self._aim_target_x),
|
|
286
|
+
aim_y=float(self._aim_target_y),
|
|
287
|
+
fire_requested=bool(fire_pressed),
|
|
288
|
+
reload_requested=bool(reload_pressed),
|
|
289
|
+
)
|
|
290
|
+
self._world.update(
|
|
291
|
+
dt_world,
|
|
292
|
+
inputs=[input_state],
|
|
293
|
+
auto_pick_perks=False,
|
|
294
|
+
game_mode=int(GameMode.TYPO),
|
|
295
|
+
perk_progression_enabled=False,
|
|
296
|
+
)
|
|
297
|
+
enforce_typo_player_frame(self._player)
|
|
298
|
+
|
|
299
|
+
self._state.bonuses.weapon_power_up = 0.0
|
|
300
|
+
self._state.bonuses.reflex_boost = 0.0
|
|
301
|
+
self._state.bonus_pool.reset()
|
|
302
|
+
|
|
303
|
+
cooldown, spawns = tick_typo_spawns(
|
|
304
|
+
elapsed_ms=int(self._typo.elapsed_ms),
|
|
305
|
+
spawn_cooldown_ms=int(self._typo.spawn_cooldown_ms),
|
|
306
|
+
frame_dt_ms=int(dt_world * 1000.0),
|
|
307
|
+
player_count=1,
|
|
308
|
+
world_width=float(self._world.world_size),
|
|
309
|
+
world_height=float(self._world.world_size),
|
|
310
|
+
)
|
|
311
|
+
self._typo.spawn_cooldown_ms = int(cooldown)
|
|
312
|
+
for call in spawns:
|
|
313
|
+
creature_idx = self._spawn_tinted_creature(
|
|
314
|
+
type_id=call.type_id,
|
|
315
|
+
pos_x=float(call.pos_x),
|
|
316
|
+
pos_y=float(call.pos_y),
|
|
317
|
+
tint_rgba=call.tint_rgba,
|
|
318
|
+
)
|
|
319
|
+
self._names.assign_random(
|
|
320
|
+
creature_idx,
|
|
321
|
+
self._state.rng,
|
|
322
|
+
score_xp=int(self._player.experience),
|
|
323
|
+
active_mask=self._active_mask(),
|
|
324
|
+
unique_words=self._unique_words,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
self._typo.elapsed_ms += int(dt_world * 1000.0)
|
|
328
|
+
# Death/game-over flow is handled at the start of the next frame so the
|
|
329
|
+
# trooper death animation can play before the UI slides in.
|
|
330
|
+
|
|
331
|
+
def _draw_game_cursor(self) -> None:
|
|
332
|
+
mouse_x = float(self._ui_mouse_x)
|
|
333
|
+
mouse_y = float(self._ui_mouse_y)
|
|
334
|
+
cursor_tex = self._ui_assets.cursor if self._ui_assets is not None else None
|
|
335
|
+
draw_menu_cursor(
|
|
336
|
+
self._world.particles_texture,
|
|
337
|
+
cursor_tex,
|
|
338
|
+
x=mouse_x,
|
|
339
|
+
y=mouse_y,
|
|
340
|
+
pulse_time=float(self._cursor_pulse_time),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _draw_aim_cursor(self) -> None:
|
|
344
|
+
mouse_x = float(self._ui_mouse_x)
|
|
345
|
+
mouse_y = float(self._ui_mouse_y)
|
|
346
|
+
aim_tex = self._ui_assets.aim if self._ui_assets is not None else None
|
|
347
|
+
draw_aim_cursor(self._world.particles_texture, aim_tex, x=mouse_x, y=mouse_y)
|
|
348
|
+
|
|
349
|
+
def _draw_name_labels(self) -> None:
|
|
350
|
+
names = self._names.names
|
|
351
|
+
if not names:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
for idx, creature in enumerate(self._creatures.entries):
|
|
355
|
+
if not creature.active:
|
|
356
|
+
continue
|
|
357
|
+
if not (0 <= idx < len(names)):
|
|
358
|
+
continue
|
|
359
|
+
text = names[idx]
|
|
360
|
+
if not text:
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
label_alpha = 1.0
|
|
364
|
+
hitbox = float(creature.hitbox_size)
|
|
365
|
+
if hitbox < 0.0:
|
|
366
|
+
label_alpha = max(0.0, min(1.0, (hitbox + 10.0) * 0.1))
|
|
367
|
+
if label_alpha <= 1e-3:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
sx, sy = self._world.world_to_screen(float(creature.x), float(creature.y))
|
|
371
|
+
y = float(sy) - 50.0
|
|
372
|
+
text_w = float(self._ui_text_width(text, scale=NAME_LABEL_SCALE))
|
|
373
|
+
text_h = 15.0
|
|
374
|
+
x = float(sx) - text_w * 0.5
|
|
375
|
+
|
|
376
|
+
bg_alpha = label_alpha * NAME_LABEL_BG_ALPHA
|
|
377
|
+
bg = rl.Color(0, 0, 0, int(255 * bg_alpha))
|
|
378
|
+
fg = rl.Color(255, 255, 255, int(255 * label_alpha))
|
|
379
|
+
rl.draw_rectangle_rec(rl.Rectangle(x - 4.0, y, text_w + 8.0, text_h), bg)
|
|
380
|
+
self._draw_ui_text(text, x, y, fg, scale=NAME_LABEL_SCALE)
|
|
381
|
+
|
|
382
|
+
def _draw_typing_box(self) -> None:
|
|
383
|
+
screen_h = float(rl.get_screen_height())
|
|
384
|
+
|
|
385
|
+
# Original positioning from 0x004457C0:
|
|
386
|
+
# v38 = screen_height - 128.0
|
|
387
|
+
# Panel Y = v38 - 16.0 = screen_height - 144.0
|
|
388
|
+
# Text Y = v38 + 1.0 = screen_height - 127.0
|
|
389
|
+
panel_x = -1.0
|
|
390
|
+
panel_y = screen_h - 144.0 # v38 - 16.0
|
|
391
|
+
text_y = screen_h - 127.0 # v38 + 1.0
|
|
392
|
+
|
|
393
|
+
# Draw panel backdrop using ind_panel texture (original: DAT_0048f7c4)
|
|
394
|
+
if self._hud_assets is not None and self._hud_assets.ind_panel is not None:
|
|
395
|
+
tex = self._hud_assets.ind_panel
|
|
396
|
+
src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
|
|
397
|
+
dst = rl.Rectangle(
|
|
398
|
+
panel_x,
|
|
399
|
+
panel_y,
|
|
400
|
+
TYPING_PANEL_WIDTH,
|
|
401
|
+
TYPING_PANEL_HEIGHT,
|
|
402
|
+
)
|
|
403
|
+
tint = rl.Color(255, 255, 255, int(255 * TYPING_PANEL_ALPHA))
|
|
404
|
+
rl.draw_texture_pro(tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
|
|
405
|
+
|
|
406
|
+
# Draw prompt + typing text
|
|
407
|
+
# Original draws with format string that includes prompt "> "
|
|
408
|
+
text = self._typing.text
|
|
409
|
+
full_text = TYPING_PROMPT + text
|
|
410
|
+
text_color = rl.Color(255, 255, 255, 255)
|
|
411
|
+
self._draw_ui_text(full_text, TYPING_TEXT_X, text_y, text_color, scale=1.0)
|
|
412
|
+
|
|
413
|
+
# Draw cursor (original: alpha = sin(game_time_s * 4.0) > 0.0 ? 0.4 : 1.0)
|
|
414
|
+
cursor_dim = math.sin(float(self._cursor_pulse_time) * 4.0) > 0.0
|
|
415
|
+
cursor_alpha = 0.4 if cursor_dim else 1.0
|
|
416
|
+
cursor_color = rl.Color(255, 255, 255, int(255 * cursor_alpha))
|
|
417
|
+
|
|
418
|
+
# Cursor position: text_width + 14.0 (original)
|
|
419
|
+
text_w = float(self._ui_text_width(text))
|
|
420
|
+
cursor_x = text_w + TYPING_CURSOR_X_OFFSET
|
|
421
|
+
cursor_y = text_y
|
|
422
|
+
|
|
423
|
+
# Draw cursor as "_" character (original: DAT_004712b8 = "_")
|
|
424
|
+
self._draw_ui_text(TYPING_CURSOR, cursor_x, cursor_y, cursor_color, scale=1.0)
|
|
425
|
+
|
|
426
|
+
def draw(self) -> None:
|
|
427
|
+
alive = self._player.health > 0.0
|
|
428
|
+
show_gameplay_ui = alive and (not self._game_over_active)
|
|
429
|
+
|
|
430
|
+
self._world.draw(draw_aim_indicators=show_gameplay_ui)
|
|
431
|
+
self._draw_screen_fade()
|
|
432
|
+
|
|
433
|
+
if show_gameplay_ui:
|
|
434
|
+
self._draw_name_labels()
|
|
435
|
+
|
|
436
|
+
if show_gameplay_ui and self._hud_assets is not None:
|
|
437
|
+
draw_hud_overlay(
|
|
438
|
+
self._hud_assets,
|
|
439
|
+
player=self._player,
|
|
440
|
+
players=self._world.players,
|
|
441
|
+
bonus_hud=self._state.bonus_hud,
|
|
442
|
+
elapsed_ms=float(self._typo.elapsed_ms),
|
|
443
|
+
font=self._small,
|
|
444
|
+
frame_dt_ms=self._last_dt_ms,
|
|
445
|
+
show_weapon=False,
|
|
446
|
+
show_xp=True,
|
|
447
|
+
show_time=True,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if show_gameplay_ui:
|
|
451
|
+
self._draw_typing_box()
|
|
452
|
+
|
|
453
|
+
warn_y = float(rl.get_screen_height()) - 28.0
|
|
454
|
+
if self._world.missing_assets:
|
|
455
|
+
warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
|
|
456
|
+
self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
|
|
457
|
+
warn_y -= float(self._ui_line_height(scale=0.8)) + 2.0
|
|
458
|
+
if self._hud_missing:
|
|
459
|
+
warn = "Missing HUD assets: " + ", ".join(self._hud_missing)
|
|
460
|
+
self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
|
|
461
|
+
|
|
462
|
+
if show_gameplay_ui:
|
|
463
|
+
self._draw_aim_cursor()
|
|
464
|
+
elif self._game_over_active:
|
|
465
|
+
self._draw_game_cursor()
|
|
466
|
+
if self._game_over_record is not None:
|
|
467
|
+
self._game_over_ui.draw(
|
|
468
|
+
record=self._game_over_record,
|
|
469
|
+
banner_kind=self._game_over_banner,
|
|
470
|
+
hud_assets=self._hud_assets,
|
|
471
|
+
mouse=self._ui_mouse_pos(),
|
|
472
|
+
)
|
crimson/paths.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from platformdirs import PlatformDirs
|
|
7
|
+
|
|
8
|
+
APP_NAME = "banteg/crimsonland"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def default_runtime_dir() -> Path:
|
|
12
|
+
"""Return the default per-user runtime directory.
|
|
13
|
+
|
|
14
|
+
This is intended for saves/config/logs (e.g. `game.cfg`, `crimson.cfg`, highscores).
|
|
15
|
+
Override with `CRIMSON_RUNTIME_DIR` (or legacy `CRIMSON_BASE_DIR`).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
override = os.environ.get("CRIMSON_RUNTIME_DIR") or os.environ.get("CRIMSON_BASE_DIR")
|
|
19
|
+
if override:
|
|
20
|
+
return Path(override).expanduser()
|
|
21
|
+
|
|
22
|
+
dirs = PlatformDirs(appname=APP_NAME, appauthor=False)
|
|
23
|
+
return Path(dirs.user_data_path)
|