crimsonland 0.1.0.dev1__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 +153 -0
- crimson/bonuses.py +167 -0
- crimson/camera.py +75 -0
- crimson/cli.py +377 -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 +663 -0
- crimson/gameplay.py +2450 -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 +1039 -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 +1338 -0
- crimson/sim/__init__.py +1 -0
- crimson/sim/world_defs.py +56 -0
- crimson/sim/world_state.py +421 -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 +414 -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 +433 -0
- crimson/views/player_sprite_debug.py +314 -0
- crimson/views/projectile_fx.py +608 -0
- crimson/views/projectile_render_debug.py +407 -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.dev1.dist-info/METADATA +9 -0
- crimsonland-0.1.0.dev1.dist-info/RECORD +138 -0
- crimsonland-0.1.0.dev1.dist-info/WHEEL +4 -0
- crimsonland-0.1.0.dev1.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
crimson/views/empty.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pyray as rl
|
|
4
|
+
|
|
5
|
+
from .registry import register_view
|
|
6
|
+
from grim.view import View
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EmptyView:
|
|
10
|
+
def update(self, dt: float) -> None:
|
|
11
|
+
del dt
|
|
12
|
+
|
|
13
|
+
def draw(self) -> None:
|
|
14
|
+
rl.clear_background(rl.BLACK)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register_view("empty", "Empty window")
|
|
18
|
+
def build_empty_view() -> View:
|
|
19
|
+
return EmptyView()
|
crimson/views/fonts.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pyray as rl
|
|
4
|
+
from .quest_title_overlay import (
|
|
5
|
+
draw_quest_title_overlay,
|
|
6
|
+
quest_title_base_scale,
|
|
7
|
+
)
|
|
8
|
+
from .registry import register_view
|
|
9
|
+
from grim.fonts.grim_mono import (
|
|
10
|
+
GrimMonoFont,
|
|
11
|
+
draw_grim_mono_text,
|
|
12
|
+
load_grim_mono_font,
|
|
13
|
+
measure_grim_mono_text_height,
|
|
14
|
+
)
|
|
15
|
+
from grim.fonts.small import (
|
|
16
|
+
SmallFontData,
|
|
17
|
+
draw_small_text,
|
|
18
|
+
load_small_font,
|
|
19
|
+
measure_small_text_height,
|
|
20
|
+
)
|
|
21
|
+
from grim.view import View, ViewContext
|
|
22
|
+
|
|
23
|
+
DEFAULT_SAMPLE = """CRIMSONLAND
|
|
24
|
+
The quick brown fox jumps over the lazy dog.
|
|
25
|
+
0123456789 !@#$%^&*()[]{}<>?/\\"""
|
|
26
|
+
|
|
27
|
+
SMALL_SAMPLE_SCALE = 1.0
|
|
28
|
+
UI_TEXT_SCALE = 1.0
|
|
29
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
30
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
31
|
+
|
|
32
|
+
GRIM_MONO_FILTER_NAME = "Bilinear"
|
|
33
|
+
GRIM_MONO_FILTER_VALUE = rl.TEXTURE_FILTER_BILINEAR
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FontView:
|
|
37
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
38
|
+
self._assets_root = ctx.assets_dir
|
|
39
|
+
self._missing_assets: list[str] = []
|
|
40
|
+
self._small: SmallFontData | None = None
|
|
41
|
+
self._grim_mono: GrimMonoFont | None = None
|
|
42
|
+
self._sample = DEFAULT_SAMPLE
|
|
43
|
+
|
|
44
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
45
|
+
if self._small is not None:
|
|
46
|
+
return int(self._small.cell_size * scale)
|
|
47
|
+
return int(20 * scale)
|
|
48
|
+
|
|
49
|
+
def _draw_ui_text(
|
|
50
|
+
self,
|
|
51
|
+
text: str,
|
|
52
|
+
x: float,
|
|
53
|
+
y: float,
|
|
54
|
+
color: rl.Color,
|
|
55
|
+
scale: float = UI_TEXT_SCALE,
|
|
56
|
+
) -> None:
|
|
57
|
+
if self._small is not None:
|
|
58
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
59
|
+
else:
|
|
60
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
61
|
+
|
|
62
|
+
def close(self) -> None:
|
|
63
|
+
if self._small is not None:
|
|
64
|
+
rl.unload_texture(self._small.texture)
|
|
65
|
+
if self._grim_mono is not None:
|
|
66
|
+
rl.unload_texture(self._grim_mono.texture)
|
|
67
|
+
|
|
68
|
+
def open(self) -> None:
|
|
69
|
+
self._missing_assets.clear()
|
|
70
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
71
|
+
self._grim_mono = load_grim_mono_font(self._assets_root, self._missing_assets)
|
|
72
|
+
self._apply_grim_filter()
|
|
73
|
+
|
|
74
|
+
def update(self, dt: float) -> None:
|
|
75
|
+
del dt
|
|
76
|
+
|
|
77
|
+
def _apply_grim_filter(self) -> None:
|
|
78
|
+
if self._grim_mono is None:
|
|
79
|
+
return
|
|
80
|
+
rl.set_texture_filter(self._grim_mono.texture, GRIM_MONO_FILTER_VALUE)
|
|
81
|
+
|
|
82
|
+
def draw(self) -> None:
|
|
83
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
84
|
+
if self._missing_assets:
|
|
85
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
86
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
87
|
+
return
|
|
88
|
+
y = 24
|
|
89
|
+
self._draw_ui_text("Small font", 24, y, UI_TEXT_COLOR)
|
|
90
|
+
y += self._ui_line_height() + 12
|
|
91
|
+
if self._small is not None:
|
|
92
|
+
draw_small_text(self._small, self._sample, 24, y, SMALL_SAMPLE_SCALE, rl.WHITE)
|
|
93
|
+
y += int(measure_small_text_height(self._small, self._sample, SMALL_SAMPLE_SCALE)) + 40
|
|
94
|
+
|
|
95
|
+
self._draw_ui_text("Grim2D mono font", 24, y, UI_TEXT_COLOR)
|
|
96
|
+
y += self._ui_line_height() + 12
|
|
97
|
+
if self._grim_mono is not None:
|
|
98
|
+
self._draw_ui_text(f"Filter: {GRIM_MONO_FILTER_NAME}", 24, y, UI_TEXT_COLOR)
|
|
99
|
+
y += self._ui_line_height(0.9) + 6
|
|
100
|
+
mono_scale = quest_title_base_scale(rl.get_screen_width())
|
|
101
|
+
draw_grim_mono_text(self._grim_mono, self._sample, 24, y, mono_scale, rl.WHITE)
|
|
102
|
+
y += int(measure_grim_mono_text_height(self._grim_mono, self._sample, mono_scale)) + 20
|
|
103
|
+
|
|
104
|
+
self._draw_quest_title_overlay()
|
|
105
|
+
|
|
106
|
+
def _draw_quest_title_overlay(self) -> None:
|
|
107
|
+
if self._grim_mono is None:
|
|
108
|
+
return
|
|
109
|
+
draw_quest_title_overlay(self._grim_mono, "Land Hostile", "1.10")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@register_view("fonts", "Font preview")
|
|
113
|
+
def build_font_view(ctx: ViewContext) -> View:
|
|
114
|
+
return FontView(ctx)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from grim.config import CrimsonConfig, default_crimson_cfg_data
|
|
8
|
+
from grim.view import ViewContext
|
|
9
|
+
|
|
10
|
+
from ..persistence.highscores import HighScoreRecord, scores_path_for_config, write_highscore_records
|
|
11
|
+
from ..ui.game_over import GameOverUi
|
|
12
|
+
from ..ui.hud import HudAssets, load_hud_assets
|
|
13
|
+
from .registry import register_view
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_BASE_DIR = Path("artifacts") / "game_over_debug"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _config_player_name_bytes(name: str) -> bytes:
|
|
20
|
+
raw = name.encode("latin-1", errors="ignore")[: 0x20 - 1]
|
|
21
|
+
return raw + b"\x00" * (0x20 - len(raw))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _seed_highscores(config: CrimsonConfig) -> None:
|
|
25
|
+
base_dir = _BASE_DIR
|
|
26
|
+
path = scores_path_for_config(base_dir, config)
|
|
27
|
+
records: list[HighScoreRecord] = []
|
|
28
|
+
for idx in range(100):
|
|
29
|
+
record = HighScoreRecord.blank()
|
|
30
|
+
record.game_mode_id = int(config.data.get("game_mode", 1))
|
|
31
|
+
record.set_name(f"bot{idx:03d}")
|
|
32
|
+
record.score_xp = 10_000 - idx
|
|
33
|
+
record.survival_elapsed_ms = (idx + 1) * 1000
|
|
34
|
+
record.creature_kill_count = 500 - idx
|
|
35
|
+
record.most_used_weapon_id = 1
|
|
36
|
+
record.shots_fired = 100
|
|
37
|
+
record.shots_hit = 42
|
|
38
|
+
records.append(record)
|
|
39
|
+
write_highscore_records(path, records)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GameOverDebugView:
|
|
43
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
44
|
+
self._assets_root = ctx.assets_dir
|
|
45
|
+
data = default_crimson_cfg_data()
|
|
46
|
+
data["game_mode"] = 1
|
|
47
|
+
data["player_name"] = _config_player_name_bytes("debugger")
|
|
48
|
+
self._config = CrimsonConfig(path=_BASE_DIR / "crimson.cfg", data=data)
|
|
49
|
+
self._hud: HudAssets | None = None
|
|
50
|
+
|
|
51
|
+
self._ui = GameOverUi(assets_root=self._assets_root, base_dir=_BASE_DIR, config=self._config)
|
|
52
|
+
self._record = HighScoreRecord.blank()
|
|
53
|
+
self._banner = "reaper"
|
|
54
|
+
self._qualifies = True
|
|
55
|
+
|
|
56
|
+
self.close_requested = False
|
|
57
|
+
|
|
58
|
+
def open(self) -> None:
|
|
59
|
+
self.close_requested = False
|
|
60
|
+
rl.hide_cursor()
|
|
61
|
+
self._hud = load_hud_assets(self._assets_root)
|
|
62
|
+
_seed_highscores(self._config)
|
|
63
|
+
self._reset_record()
|
|
64
|
+
self._ui.open()
|
|
65
|
+
|
|
66
|
+
def close(self) -> None:
|
|
67
|
+
rl.show_cursor()
|
|
68
|
+
self._ui.close()
|
|
69
|
+
if self._hud is not None:
|
|
70
|
+
self._hud = None
|
|
71
|
+
|
|
72
|
+
def _reset_record(self) -> None:
|
|
73
|
+
record = HighScoreRecord.blank()
|
|
74
|
+
record.game_mode_id = int(self._config.data.get("game_mode", 1))
|
|
75
|
+
record.score_xp = 20_000 if self._qualifies else 1
|
|
76
|
+
record.survival_elapsed_ms = 123_456
|
|
77
|
+
record.creature_kill_count = 123
|
|
78
|
+
record.most_used_weapon_id = 1
|
|
79
|
+
record.shots_fired = 120
|
|
80
|
+
record.shots_hit = 37
|
|
81
|
+
self._record = record
|
|
82
|
+
|
|
83
|
+
def update(self, dt: float) -> None:
|
|
84
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
85
|
+
self.close_requested = True
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
|
|
89
|
+
_seed_highscores(self._config)
|
|
90
|
+
self._reset_record()
|
|
91
|
+
self._ui.open()
|
|
92
|
+
|
|
93
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_F1):
|
|
94
|
+
self._qualifies = not self._qualifies
|
|
95
|
+
self._reset_record()
|
|
96
|
+
self._ui.open()
|
|
97
|
+
|
|
98
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_B):
|
|
99
|
+
self._banner = "well_done" if self._banner == "reaper" else "reaper"
|
|
100
|
+
|
|
101
|
+
action = self._ui.update(dt, record=self._record, player_name_default="debugger")
|
|
102
|
+
if action == "play_again":
|
|
103
|
+
self._reset_record()
|
|
104
|
+
self._ui.open()
|
|
105
|
+
return
|
|
106
|
+
if action in {"main_menu", "high_scores"}:
|
|
107
|
+
self.close_requested = True
|
|
108
|
+
|
|
109
|
+
def draw(self) -> None:
|
|
110
|
+
rl.clear_background(rl.Color(8, 8, 10, 255))
|
|
111
|
+
self._ui.draw(record=self._record, banner_kind=self._banner, hud_assets=self._hud)
|
|
112
|
+
rl.draw_text("F1 toggle qualify | B toggle banner | R reset | ESC close", 18, 18, 18, rl.Color(200, 200, 200, 255))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@register_view("game_over", "Game Over")
|
|
116
|
+
def _create_game_over_view(*, ctx: ViewContext) -> GameOverDebugView:
|
|
117
|
+
return GameOverDebugView(ctx)
|
crimson/views/ground.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pyray as rl
|
|
6
|
+
|
|
7
|
+
from crimson.creatures.anim import creature_corpse_frame_for_type
|
|
8
|
+
from crimson.effects import FxQueue, FxQueueRotated
|
|
9
|
+
from crimson.render.terrain_fx import FxQueueTextures, bake_fx_queues
|
|
10
|
+
from grim.assets import resolve_asset_path
|
|
11
|
+
from grim.config import ensure_crimson_cfg
|
|
12
|
+
from grim.terrain_render import GroundRenderer
|
|
13
|
+
from ..paths import default_runtime_dir
|
|
14
|
+
from ..quests import all_quests
|
|
15
|
+
from ..quests.types import QuestDefinition
|
|
16
|
+
from .quest_title_overlay import draw_quest_title_overlay
|
|
17
|
+
from .registry import register_view
|
|
18
|
+
from grim.fonts.grim_mono import GrimMonoFont, load_grim_mono_font
|
|
19
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
20
|
+
from grim.view import View, ViewContext
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
UI_TEXT_SCALE = 1.0
|
|
24
|
+
UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
|
|
25
|
+
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
|
|
26
|
+
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class GroundAssets:
|
|
31
|
+
textures: dict[int, rl.Texture]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
TERRAIN_TEXTURES: list[tuple[int, str]] = [
|
|
35
|
+
(0, "ter/ter_q1_base.png"),
|
|
36
|
+
(1, "ter/ter_q1_tex1.png"),
|
|
37
|
+
(2, "ter/ter_q2_base.png"),
|
|
38
|
+
(3, "ter/ter_q2_tex1.png"),
|
|
39
|
+
(4, "ter/ter_q3_base.png"),
|
|
40
|
+
(5, "ter/ter_q3_tex1.png"),
|
|
41
|
+
(6, "ter/ter_q4_base.png"),
|
|
42
|
+
(7, "ter/ter_q4_tex1.png"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GroundView:
|
|
47
|
+
def __init__(self, ctx: ViewContext) -> None:
|
|
48
|
+
self._assets_root = ctx.assets_dir
|
|
49
|
+
self._missing_assets: list[str] = []
|
|
50
|
+
self._small: SmallFontData | None = None
|
|
51
|
+
self._grim_mono: GrimMonoFont | None = None
|
|
52
|
+
self._assets: GroundAssets | None = None
|
|
53
|
+
self._renderer: GroundRenderer | None = None
|
|
54
|
+
self._camera_x = 0.0
|
|
55
|
+
self._camera_y = 0.0
|
|
56
|
+
self._quests: list[QuestDefinition] = []
|
|
57
|
+
self._quest_index = 0
|
|
58
|
+
self._terrain_seed: int | None = None
|
|
59
|
+
self._fx_queue = FxQueue()
|
|
60
|
+
self._fx_queue_rotated = FxQueueRotated()
|
|
61
|
+
self._fx_textures: FxQueueTextures | None = None
|
|
62
|
+
|
|
63
|
+
def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
|
|
64
|
+
if self._small is not None:
|
|
65
|
+
return int(self._small.cell_size * scale)
|
|
66
|
+
return int(20 * scale)
|
|
67
|
+
|
|
68
|
+
def _draw_ui_text(
|
|
69
|
+
self,
|
|
70
|
+
text: str,
|
|
71
|
+
x: float,
|
|
72
|
+
y: float,
|
|
73
|
+
color: rl.Color,
|
|
74
|
+
scale: float = UI_TEXT_SCALE,
|
|
75
|
+
) -> None:
|
|
76
|
+
if self._small is not None:
|
|
77
|
+
draw_small_text(self._small, text, x, y, scale, color)
|
|
78
|
+
else:
|
|
79
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
80
|
+
|
|
81
|
+
def open(self) -> None:
|
|
82
|
+
self._missing_assets.clear()
|
|
83
|
+
self._small = load_small_font(self._assets_root, self._missing_assets)
|
|
84
|
+
self._grim_mono = load_grim_mono_font(self._assets_root, self._missing_assets)
|
|
85
|
+
textures: dict[int, rl.Texture] = {}
|
|
86
|
+
for terrain_id, rel_path in TERRAIN_TEXTURES:
|
|
87
|
+
path = resolve_asset_path(self._assets_root, rel_path)
|
|
88
|
+
if path is None:
|
|
89
|
+
self._missing_assets.append(rel_path)
|
|
90
|
+
continue
|
|
91
|
+
textures[terrain_id] = rl.load_texture(str(path))
|
|
92
|
+
if self._missing_assets:
|
|
93
|
+
raise FileNotFoundError(f"Missing ground assets: {', '.join(self._missing_assets)}")
|
|
94
|
+
self._assets = GroundAssets(textures=textures)
|
|
95
|
+
self._quests = all_quests()
|
|
96
|
+
self._fx_queue.clear()
|
|
97
|
+
self._fx_queue_rotated.clear()
|
|
98
|
+
particles_path = resolve_asset_path(self._assets_root, "game/particles.png")
|
|
99
|
+
if particles_path is None:
|
|
100
|
+
self._missing_assets.append("game/particles.png")
|
|
101
|
+
bodyset_path = resolve_asset_path(self._assets_root, "game/bodyset.png")
|
|
102
|
+
if bodyset_path is None:
|
|
103
|
+
self._missing_assets.append("game/bodyset.png")
|
|
104
|
+
if self._missing_assets:
|
|
105
|
+
raise FileNotFoundError(f"Missing ground assets: {', '.join(self._missing_assets)}")
|
|
106
|
+
self._fx_textures = FxQueueTextures(
|
|
107
|
+
particles=rl.load_texture(str(particles_path)),
|
|
108
|
+
bodyset=rl.load_texture(str(bodyset_path)),
|
|
109
|
+
)
|
|
110
|
+
texture_scale, screen_w, screen_h = self._load_runtime_config()
|
|
111
|
+
if self._renderer is not None:
|
|
112
|
+
self._renderer.texture_scale = texture_scale
|
|
113
|
+
self._renderer.screen_width = screen_w
|
|
114
|
+
self._renderer.screen_height = screen_h
|
|
115
|
+
self._quest_index = 0
|
|
116
|
+
self._apply_quest()
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
if self._assets is not None:
|
|
120
|
+
for texture in self._assets.textures.values():
|
|
121
|
+
rl.unload_texture(texture)
|
|
122
|
+
self._assets = None
|
|
123
|
+
if self._renderer is not None and self._renderer.render_target is not None:
|
|
124
|
+
rl.unload_render_texture(self._renderer.render_target)
|
|
125
|
+
if self._small is not None:
|
|
126
|
+
rl.unload_texture(self._small.texture)
|
|
127
|
+
self._small = None
|
|
128
|
+
if self._grim_mono is not None:
|
|
129
|
+
rl.unload_texture(self._grim_mono.texture)
|
|
130
|
+
self._grim_mono = None
|
|
131
|
+
if self._fx_textures is not None:
|
|
132
|
+
rl.unload_texture(self._fx_textures.particles)
|
|
133
|
+
rl.unload_texture(self._fx_textures.bodyset)
|
|
134
|
+
self._fx_textures = None
|
|
135
|
+
self._fx_queue.clear()
|
|
136
|
+
self._fx_queue_rotated.clear()
|
|
137
|
+
|
|
138
|
+
def update(self, dt: float) -> None:
|
|
139
|
+
speed = 240.0
|
|
140
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_A):
|
|
141
|
+
self._camera_x += speed * dt
|
|
142
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_D):
|
|
143
|
+
self._camera_x -= speed * dt
|
|
144
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_W):
|
|
145
|
+
self._camera_y += speed * dt
|
|
146
|
+
if rl.is_key_down(rl.KeyboardKey.KEY_S):
|
|
147
|
+
self._camera_y -= speed * dt
|
|
148
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
149
|
+
self._quest_index = (self._quest_index - 1) % max(1, len(self._quests))
|
|
150
|
+
self._apply_quest()
|
|
151
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
152
|
+
self._quest_index = (self._quest_index + 1) % max(1, len(self._quests))
|
|
153
|
+
self._apply_quest()
|
|
154
|
+
if self._renderer is not None:
|
|
155
|
+
self._renderer.process_pending()
|
|
156
|
+
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
157
|
+
mouse = rl.get_mouse_position()
|
|
158
|
+
world_x = -self._camera_x + float(mouse.x)
|
|
159
|
+
world_y = -self._camera_y + float(mouse.y)
|
|
160
|
+
self._fx_queue.add(
|
|
161
|
+
effect_id=0x07, # blood
|
|
162
|
+
pos_x=world_x,
|
|
163
|
+
pos_y=world_y,
|
|
164
|
+
width=30.0,
|
|
165
|
+
height=30.0,
|
|
166
|
+
rotation=0.0,
|
|
167
|
+
rgba=(1.0, 1.0, 1.0, 1.0),
|
|
168
|
+
)
|
|
169
|
+
if self._fx_textures is not None and (self._fx_queue.count or self._fx_queue_rotated.count):
|
|
170
|
+
bake_fx_queues(
|
|
171
|
+
self._renderer,
|
|
172
|
+
fx_queue=self._fx_queue,
|
|
173
|
+
fx_queue_rotated=self._fx_queue_rotated,
|
|
174
|
+
textures=self._fx_textures,
|
|
175
|
+
corpse_frame_for_type=creature_corpse_frame_for_type,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def draw(self) -> None:
|
|
179
|
+
rl.clear_background(rl.Color(12, 12, 14, 255))
|
|
180
|
+
if self._missing_assets:
|
|
181
|
+
message = "Missing assets: " + ", ".join(self._missing_assets)
|
|
182
|
+
self._draw_ui_text(message, 24, 24, UI_ERROR_COLOR)
|
|
183
|
+
return
|
|
184
|
+
if self._renderer is None:
|
|
185
|
+
self._draw_ui_text("Ground renderer not initialized.", 24, 24, UI_ERROR_COLOR)
|
|
186
|
+
return
|
|
187
|
+
self._renderer.draw(self._camera_x, self._camera_y)
|
|
188
|
+
self._draw_quest_title_overlay()
|
|
189
|
+
|
|
190
|
+
def _load_runtime_config(self) -> tuple[float, float | None, float | None]:
|
|
191
|
+
runtime_dir = default_runtime_dir()
|
|
192
|
+
if runtime_dir.is_dir():
|
|
193
|
+
try:
|
|
194
|
+
cfg = ensure_crimson_cfg(runtime_dir)
|
|
195
|
+
return (
|
|
196
|
+
cfg.texture_scale,
|
|
197
|
+
float(cfg.screen_width),
|
|
198
|
+
float(cfg.screen_height),
|
|
199
|
+
)
|
|
200
|
+
except Exception:
|
|
201
|
+
return 1.0, None, None
|
|
202
|
+
return 1.0, None, None
|
|
203
|
+
|
|
204
|
+
def _apply_quest(self) -> None:
|
|
205
|
+
if not self._quests or self._assets is None:
|
|
206
|
+
return
|
|
207
|
+
quest = self._quests[self._quest_index]
|
|
208
|
+
base_id, overlay_id, detail_id = quest.terrain_ids or (0, 1, 0)
|
|
209
|
+
textures = self._assets.textures
|
|
210
|
+
base = textures.get(base_id)
|
|
211
|
+
if base is None:
|
|
212
|
+
return
|
|
213
|
+
overlay = textures.get(overlay_id)
|
|
214
|
+
detail = textures.get(detail_id)
|
|
215
|
+
if self._renderer is None:
|
|
216
|
+
texture_scale, screen_w, screen_h = self._load_runtime_config()
|
|
217
|
+
self._renderer = GroundRenderer(
|
|
218
|
+
texture=base,
|
|
219
|
+
overlay=overlay,
|
|
220
|
+
overlay_detail=detail,
|
|
221
|
+
width=1024,
|
|
222
|
+
height=1024,
|
|
223
|
+
texture_scale=texture_scale,
|
|
224
|
+
screen_width=screen_w,
|
|
225
|
+
screen_height=screen_h,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
self._renderer.texture = base
|
|
229
|
+
self._renderer.overlay = overlay
|
|
230
|
+
self._renderer.overlay_detail = detail
|
|
231
|
+
self._terrain_seed = self._quest_seed(quest.level)
|
|
232
|
+
self._regenerate_terrain(reset_camera=True)
|
|
233
|
+
|
|
234
|
+
def _regenerate_terrain(self, *, reset_camera: bool = False) -> None:
|
|
235
|
+
renderer = self._renderer
|
|
236
|
+
if renderer is None:
|
|
237
|
+
return
|
|
238
|
+
renderer.schedule_generate(seed=self._terrain_seed, layers=3)
|
|
239
|
+
if reset_camera:
|
|
240
|
+
self._camera_x = 0.0
|
|
241
|
+
self._camera_y = 0.0
|
|
242
|
+
|
|
243
|
+
def _quest_seed(self, level: str) -> int:
|
|
244
|
+
tier_text, quest_text = level.split(".", 1)
|
|
245
|
+
try:
|
|
246
|
+
return int(tier_text) * 100 + int(quest_text)
|
|
247
|
+
except ValueError:
|
|
248
|
+
return sum(ord(ch) for ch in level)
|
|
249
|
+
|
|
250
|
+
def _draw_quest_title_overlay(self) -> None:
|
|
251
|
+
if self._grim_mono is None or not self._quests:
|
|
252
|
+
return
|
|
253
|
+
quest = self._quests[self._quest_index]
|
|
254
|
+
draw_quest_title_overlay(self._grim_mono, quest.title, quest.level)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@register_view("ground", "Ground texture")
|
|
258
|
+
def build_ground_view(ctx: ViewContext) -> View:
|
|
259
|
+
return GroundView(ctx)
|