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
grim/terrain_render.py
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import math
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
from typing import Iterable, Sequence
|
|
8
|
+
|
|
9
|
+
import pyray as rl
|
|
10
|
+
|
|
11
|
+
from .rand import CrtRand
|
|
12
|
+
|
|
13
|
+
TERRAIN_TEXTURE_SIZE = 1024
|
|
14
|
+
TERRAIN_PATCH_SIZE = 128.0
|
|
15
|
+
TERRAIN_PATCH_OVERSCAN = 64.0
|
|
16
|
+
TERRAIN_CLEAR_COLOR = rl.Color(63, 56, 25, 255)
|
|
17
|
+
TERRAIN_BASE_TINT = rl.Color(178, 178, 178, 230)
|
|
18
|
+
TERRAIN_OVERLAY_TINT = rl.Color(178, 178, 178, 230)
|
|
19
|
+
TERRAIN_DETAIL_TINT = rl.Color(178, 178, 178, 153)
|
|
20
|
+
TERRAIN_DENSITY_BASE = 800
|
|
21
|
+
TERRAIN_DENSITY_OVERLAY = 0x23
|
|
22
|
+
TERRAIN_DENSITY_DETAIL = 0x0F
|
|
23
|
+
TERRAIN_DENSITY_SHIFT = 19
|
|
24
|
+
TERRAIN_ROTATION_MAX = 0x13A
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_ALPHA_TEST_REF_U8 = 4
|
|
28
|
+
_ALPHA_TEST_REF_F32 = float(_ALPHA_TEST_REF_U8) / 255.0
|
|
29
|
+
|
|
30
|
+
# Grim2D enables alpha test globally with:
|
|
31
|
+
# ALPHATESTENABLE=1, ALPHAFUNC=GREATER, ALPHAREF=4
|
|
32
|
+
# See: analysis/ghidra/raw/grim.dll_decompiled.c (FUN_10004520).
|
|
33
|
+
#
|
|
34
|
+
# raylib does not expose fixed-function alpha test, so we emulate it with a tiny
|
|
35
|
+
# discard shader for stamping into the terrain render target.
|
|
36
|
+
_ALPHA_TEST_SHADER: rl.Shader | None = None
|
|
37
|
+
_ALPHA_TEST_SHADER_TRIED = False
|
|
38
|
+
|
|
39
|
+
_ALPHA_TEST_VS_330 = r"""
|
|
40
|
+
#version 330
|
|
41
|
+
|
|
42
|
+
in vec3 vertexPosition;
|
|
43
|
+
in vec2 vertexTexCoord;
|
|
44
|
+
in vec4 vertexColor;
|
|
45
|
+
|
|
46
|
+
out vec2 fragTexCoord;
|
|
47
|
+
out vec4 fragColor;
|
|
48
|
+
|
|
49
|
+
uniform mat4 mvp;
|
|
50
|
+
|
|
51
|
+
void main() {
|
|
52
|
+
fragTexCoord = vertexTexCoord;
|
|
53
|
+
fragColor = vertexColor;
|
|
54
|
+
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_ALPHA_TEST_FS_330 = rf"""
|
|
59
|
+
#version 330
|
|
60
|
+
|
|
61
|
+
in vec2 fragTexCoord;
|
|
62
|
+
in vec4 fragColor;
|
|
63
|
+
|
|
64
|
+
uniform sampler2D texture0;
|
|
65
|
+
uniform vec4 colDiffuse;
|
|
66
|
+
|
|
67
|
+
out vec4 finalColor;
|
|
68
|
+
|
|
69
|
+
void main() {{
|
|
70
|
+
vec4 texel = texture(texture0, fragTexCoord) * fragColor * colDiffuse;
|
|
71
|
+
if (texel.a <= {_ALPHA_TEST_REF_F32:.10f}) discard;
|
|
72
|
+
finalColor = texel;
|
|
73
|
+
}}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_alpha_test_shader() -> rl.Shader | None:
|
|
78
|
+
global _ALPHA_TEST_SHADER, _ALPHA_TEST_SHADER_TRIED
|
|
79
|
+
if _ALPHA_TEST_SHADER_TRIED:
|
|
80
|
+
if _ALPHA_TEST_SHADER is not None and int(getattr(_ALPHA_TEST_SHADER, "id", 0)) > 0:
|
|
81
|
+
return _ALPHA_TEST_SHADER
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
_ALPHA_TEST_SHADER_TRIED = True
|
|
85
|
+
try:
|
|
86
|
+
shader = rl.load_shader_from_memory(_ALPHA_TEST_VS_330, _ALPHA_TEST_FS_330)
|
|
87
|
+
except Exception:
|
|
88
|
+
_ALPHA_TEST_SHADER = None
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
if int(getattr(shader, "id", 0)) <= 0:
|
|
92
|
+
_ALPHA_TEST_SHADER = None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
_ALPHA_TEST_SHADER = shader
|
|
96
|
+
return _ALPHA_TEST_SHADER
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def _blend_custom(src_factor: int, dst_factor: int, blend_equation: int) -> Iterator[None]:
|
|
101
|
+
# NOTE: raylib/rlgl tracks custom blend factors as state; some backends only
|
|
102
|
+
# apply them when switching the blend mode. Set factors both before and
|
|
103
|
+
# after BeginBlendMode() to ensure the current draw uses the intended values.
|
|
104
|
+
rl.rl_set_blend_factors(src_factor, dst_factor, blend_equation)
|
|
105
|
+
rl.begin_blend_mode(rl.BLEND_CUSTOM)
|
|
106
|
+
rl.rl_set_blend_factors(src_factor, dst_factor, blend_equation)
|
|
107
|
+
try:
|
|
108
|
+
yield
|
|
109
|
+
finally:
|
|
110
|
+
rl.end_blend_mode()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@contextmanager
|
|
114
|
+
def _blend_custom_separate(
|
|
115
|
+
src_rgb: int,
|
|
116
|
+
dst_rgb: int,
|
|
117
|
+
src_alpha: int,
|
|
118
|
+
dst_alpha: int,
|
|
119
|
+
eq_rgb: int,
|
|
120
|
+
eq_alpha: int,
|
|
121
|
+
) -> Iterator[None]:
|
|
122
|
+
# NOTE: raylib/rlgl tracks custom blend factors as state; some backends only
|
|
123
|
+
# apply them when switching the blend mode. Set factors both before and
|
|
124
|
+
# after BeginBlendMode() to ensure the current draw uses the intended values.
|
|
125
|
+
rl.rl_set_blend_factors_separate(src_rgb, dst_rgb, src_alpha, dst_alpha, eq_rgb, eq_alpha)
|
|
126
|
+
rl.begin_blend_mode(rl.BLEND_CUSTOM_SEPARATE)
|
|
127
|
+
rl.rl_set_blend_factors_separate(src_rgb, dst_rgb, src_alpha, dst_alpha, eq_rgb, eq_alpha)
|
|
128
|
+
try:
|
|
129
|
+
yield
|
|
130
|
+
finally:
|
|
131
|
+
rl.end_blend_mode()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@contextmanager
|
|
135
|
+
def _maybe_alpha_test(enabled: bool) -> Iterator[None]:
|
|
136
|
+
if not enabled:
|
|
137
|
+
yield
|
|
138
|
+
return
|
|
139
|
+
shader = _get_alpha_test_shader()
|
|
140
|
+
if shader is None:
|
|
141
|
+
yield
|
|
142
|
+
return
|
|
143
|
+
rl.begin_shader_mode(shader)
|
|
144
|
+
try:
|
|
145
|
+
yield
|
|
146
|
+
finally:
|
|
147
|
+
rl.end_shader_mode()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(slots=True)
|
|
151
|
+
class GroundDecal:
|
|
152
|
+
texture: rl.Texture
|
|
153
|
+
src: rl.Rectangle
|
|
154
|
+
x: float
|
|
155
|
+
y: float
|
|
156
|
+
width: float
|
|
157
|
+
height: float
|
|
158
|
+
rotation_rad: float = 0.0
|
|
159
|
+
tint: rl.Color = rl.WHITE
|
|
160
|
+
centered: bool = True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(slots=True)
|
|
164
|
+
class GroundCorpseDecal:
|
|
165
|
+
bodyset_frame: int
|
|
166
|
+
top_left_x: float
|
|
167
|
+
top_left_y: float
|
|
168
|
+
size: float
|
|
169
|
+
rotation_rad: float
|
|
170
|
+
tint: rl.Color = rl.WHITE
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(slots=True)
|
|
174
|
+
class GroundRenderer:
|
|
175
|
+
texture: rl.Texture
|
|
176
|
+
width: int = TERRAIN_TEXTURE_SIZE
|
|
177
|
+
height: int = TERRAIN_TEXTURE_SIZE
|
|
178
|
+
texture_scale: float = 1.0
|
|
179
|
+
alpha_test: bool = True
|
|
180
|
+
debug_log_stamps: bool = False
|
|
181
|
+
texture_failed: bool = False
|
|
182
|
+
screen_width: float | None = None
|
|
183
|
+
screen_height: float | None = None
|
|
184
|
+
overlay: rl.Texture | None = None
|
|
185
|
+
overlay_detail: rl.Texture | None = None
|
|
186
|
+
terrain_filter: float = 1.0
|
|
187
|
+
render_target: rl.RenderTexture | None = None
|
|
188
|
+
_debug_stamp_log: list[dict[str, object]] = field(default_factory=list, init=False, repr=False)
|
|
189
|
+
_render_target_ready: bool = field(default=False, init=False, repr=False)
|
|
190
|
+
_pending_generate: bool = field(default=False, init=False, repr=False)
|
|
191
|
+
_pending_generate_seed: int | None = field(default=None, init=False, repr=False)
|
|
192
|
+
_pending_generate_layers: int = field(default=3, init=False, repr=False)
|
|
193
|
+
_render_target_warmup_passes: int = field(default=0, init=False, repr=False)
|
|
194
|
+
_fallback_seed: int | None = field(default=None, init=False, repr=False)
|
|
195
|
+
_fallback_layers: int = field(default=0, init=False, repr=False)
|
|
196
|
+
_fallback_patches: list[GroundDecal] = field(default_factory=list, init=False, repr=False)
|
|
197
|
+
_fallback_decals: list[GroundDecal] = field(default_factory=list, init=False, repr=False)
|
|
198
|
+
_fallback_corpse_decals: list[GroundCorpseDecal] = field(default_factory=list, init=False, repr=False)
|
|
199
|
+
_fallback_bodyset_texture: rl.Texture | None = field(default=None, init=False, repr=False)
|
|
200
|
+
_fallback_corpse_shadow: bool = field(default=True, init=False, repr=False)
|
|
201
|
+
|
|
202
|
+
def debug_clear_stamp_log(self) -> None:
|
|
203
|
+
self._debug_stamp_log.clear()
|
|
204
|
+
|
|
205
|
+
def debug_stamp_log(self) -> tuple[dict[str, object], ...]:
|
|
206
|
+
return tuple(self._debug_stamp_log)
|
|
207
|
+
|
|
208
|
+
def _debug_stamp(self, kind: str, **payload: object) -> None:
|
|
209
|
+
if not self.debug_log_stamps:
|
|
210
|
+
return
|
|
211
|
+
self._debug_stamp_log.append({"kind": kind, **payload})
|
|
212
|
+
if len(self._debug_stamp_log) > 96:
|
|
213
|
+
del self._debug_stamp_log[:32]
|
|
214
|
+
|
|
215
|
+
def process_pending(self) -> None:
|
|
216
|
+
# Bound the amount of work per tick. Typical warmup sequence:
|
|
217
|
+
# 1) create RT
|
|
218
|
+
# 2) first fill (may be black/uninitialized on some platforms)
|
|
219
|
+
# 3) warmup retry fill
|
|
220
|
+
steps = 0
|
|
221
|
+
while self._pending_generate and steps < 4:
|
|
222
|
+
steps += 1
|
|
223
|
+
if self.render_target is None:
|
|
224
|
+
self.create_render_target()
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
seed = self._pending_generate_seed
|
|
228
|
+
layers = self._pending_generate_layers
|
|
229
|
+
self._pending_generate = False
|
|
230
|
+
self.generate_partial(seed=seed, layers=layers)
|
|
231
|
+
if self.render_target is None and not self.texture_failed:
|
|
232
|
+
self._pending_generate = True
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if self._render_target_warmup_passes > 0:
|
|
236
|
+
self._render_target_warmup_passes -= 1
|
|
237
|
+
# On some platforms/drivers the first draw into a new RT can come out as
|
|
238
|
+
# black/uninitialized (all-zero). Retry once before marking it ready.
|
|
239
|
+
self._render_target_ready = False
|
|
240
|
+
self._pending_generate = True
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
def create_render_target(self) -> None:
|
|
244
|
+
if self.texture_failed:
|
|
245
|
+
if self.render_target is not None:
|
|
246
|
+
rl.unload_render_texture(self.render_target)
|
|
247
|
+
self.render_target = None
|
|
248
|
+
self._render_target_ready = False
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
scale = self.texture_scale
|
|
252
|
+
if scale < 0.5:
|
|
253
|
+
scale = 0.5
|
|
254
|
+
elif scale > 4.0:
|
|
255
|
+
scale = 4.0
|
|
256
|
+
self.texture_scale = scale
|
|
257
|
+
|
|
258
|
+
render_w, render_h = self._render_target_size_for(scale)
|
|
259
|
+
if self._ensure_render_target(render_w, render_h):
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
old_scale = scale
|
|
263
|
+
self.texture_scale = scale + scale
|
|
264
|
+
render_w, render_h = self._render_target_size_for(self.texture_scale)
|
|
265
|
+
if self._ensure_render_target(render_w, render_h):
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
self.texture_failed = True
|
|
269
|
+
self.texture_scale = old_scale
|
|
270
|
+
if self.render_target is not None:
|
|
271
|
+
rl.unload_render_texture(self.render_target)
|
|
272
|
+
self.render_target = None
|
|
273
|
+
self._render_target_ready = False
|
|
274
|
+
|
|
275
|
+
def generate(self, seed: int | None = None) -> None:
|
|
276
|
+
self.generate_partial(seed=seed, layers=3)
|
|
277
|
+
|
|
278
|
+
def schedule_generate(self, seed: int | None = None, *, layers: int = 3) -> None:
|
|
279
|
+
self._pending_generate_seed = seed
|
|
280
|
+
self._pending_generate_layers = max(0, min(int(layers), 3))
|
|
281
|
+
self._pending_generate = True
|
|
282
|
+
|
|
283
|
+
def generate_partial(self, seed: int | None = None, *, layers: int) -> None:
|
|
284
|
+
layers = max(0, min(int(layers), 3))
|
|
285
|
+
# Always keep a deterministic fallback representation of the terrain.
|
|
286
|
+
# When the render target is unavailable (or not ready yet), we can render
|
|
287
|
+
# patches + baked decals directly to the screen, matching the exe's
|
|
288
|
+
# `terrain_texture_failed` path.
|
|
289
|
+
self._fallback_seed = seed
|
|
290
|
+
self._fallback_layers = layers
|
|
291
|
+
self._fallback_patches.clear()
|
|
292
|
+
self._fallback_decals.clear()
|
|
293
|
+
self._fallback_corpse_decals.clear()
|
|
294
|
+
self._fallback_bodyset_texture = None
|
|
295
|
+
self._fallback_corpse_shadow = True
|
|
296
|
+
|
|
297
|
+
rng_fallback = CrtRand(seed)
|
|
298
|
+
if layers >= 1:
|
|
299
|
+
self._scatter_texture_fallback(self.texture, TERRAIN_BASE_TINT, rng_fallback, TERRAIN_DENSITY_BASE)
|
|
300
|
+
if layers >= 2 and self.overlay is not None:
|
|
301
|
+
self._scatter_texture_fallback(self.overlay, TERRAIN_OVERLAY_TINT, rng_fallback, TERRAIN_DENSITY_OVERLAY)
|
|
302
|
+
if layers >= 3:
|
|
303
|
+
# Original uses base texture for detail pass, not overlay.
|
|
304
|
+
self._scatter_texture_fallback(self.texture, TERRAIN_DETAIL_TINT, rng_fallback, TERRAIN_DENSITY_DETAIL)
|
|
305
|
+
|
|
306
|
+
self.create_render_target()
|
|
307
|
+
if self.render_target is None:
|
|
308
|
+
return
|
|
309
|
+
rng = CrtRand(seed)
|
|
310
|
+
self._set_stamp_filters(point=True)
|
|
311
|
+
rl.begin_texture_mode(self.render_target)
|
|
312
|
+
rl.clear_background(TERRAIN_CLEAR_COLOR)
|
|
313
|
+
# Keep the ground RT alpha at 1.0 like the original exe (which typically uses
|
|
314
|
+
# an XRGB render target). We still alpha-blend RGB, but preserve destination A.
|
|
315
|
+
with _blend_custom_separate(
|
|
316
|
+
rl.RL_SRC_ALPHA,
|
|
317
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
318
|
+
rl.RL_ZERO,
|
|
319
|
+
rl.RL_ONE,
|
|
320
|
+
rl.RL_FUNC_ADD,
|
|
321
|
+
rl.RL_FUNC_ADD,
|
|
322
|
+
):
|
|
323
|
+
if layers >= 1:
|
|
324
|
+
self._scatter_texture(self.texture, TERRAIN_BASE_TINT, rng, TERRAIN_DENSITY_BASE)
|
|
325
|
+
if layers >= 2 and self.overlay is not None:
|
|
326
|
+
self._scatter_texture(self.overlay, TERRAIN_OVERLAY_TINT, rng, TERRAIN_DENSITY_OVERLAY)
|
|
327
|
+
if layers >= 3:
|
|
328
|
+
# Original uses base texture for detail pass, not overlay
|
|
329
|
+
self._scatter_texture(self.texture, TERRAIN_DETAIL_TINT, rng, TERRAIN_DENSITY_DETAIL)
|
|
330
|
+
rl.end_texture_mode()
|
|
331
|
+
self._set_stamp_filters(point=False)
|
|
332
|
+
self._render_target_ready = True
|
|
333
|
+
|
|
334
|
+
def bake_decals(self, decals: Sequence[GroundDecal]) -> bool:
|
|
335
|
+
if not decals:
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
self.create_render_target()
|
|
339
|
+
if self.render_target is None:
|
|
340
|
+
self._fallback_decals.extend(decals)
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
if self.debug_log_stamps:
|
|
344
|
+
head = decals[0]
|
|
345
|
+
self._debug_stamp(
|
|
346
|
+
"bake_decals",
|
|
347
|
+
count=len(decals),
|
|
348
|
+
pos0={"x": float(head.x), "y": float(head.y)},
|
|
349
|
+
rot0=float(head.rotation_rad),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
inv_scale = 1.0 / self._normalized_texture_scale()
|
|
353
|
+
textures = self._unique_textures([decal.texture for decal in decals])
|
|
354
|
+
self._set_texture_filters(textures, point=True)
|
|
355
|
+
|
|
356
|
+
rl.begin_texture_mode(self.render_target)
|
|
357
|
+
with _blend_custom_separate(
|
|
358
|
+
rl.RL_SRC_ALPHA,
|
|
359
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
360
|
+
rl.RL_ZERO,
|
|
361
|
+
rl.RL_ONE,
|
|
362
|
+
rl.RL_FUNC_ADD,
|
|
363
|
+
rl.RL_FUNC_ADD,
|
|
364
|
+
):
|
|
365
|
+
for decal in decals:
|
|
366
|
+
w = decal.width
|
|
367
|
+
h = decal.height
|
|
368
|
+
if decal.centered:
|
|
369
|
+
pivot_x = decal.x
|
|
370
|
+
pivot_y = decal.y
|
|
371
|
+
else:
|
|
372
|
+
pivot_x = decal.x + w * 0.5
|
|
373
|
+
pivot_y = decal.y + h * 0.5
|
|
374
|
+
pivot_x *= inv_scale
|
|
375
|
+
pivot_y *= inv_scale
|
|
376
|
+
w *= inv_scale
|
|
377
|
+
h *= inv_scale
|
|
378
|
+
dst = rl.Rectangle(pivot_x, pivot_y, w, h)
|
|
379
|
+
origin = rl.Vector2(w * 0.5, h * 0.5)
|
|
380
|
+
rl.draw_texture_pro(
|
|
381
|
+
decal.texture,
|
|
382
|
+
decal.src,
|
|
383
|
+
dst,
|
|
384
|
+
origin,
|
|
385
|
+
math.degrees(decal.rotation_rad),
|
|
386
|
+
decal.tint,
|
|
387
|
+
)
|
|
388
|
+
rl.end_texture_mode()
|
|
389
|
+
|
|
390
|
+
self._set_texture_filters(textures, point=False)
|
|
391
|
+
self._render_target_ready = True
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
def bake_corpse_decals(
|
|
395
|
+
self,
|
|
396
|
+
bodyset_texture: rl.Texture,
|
|
397
|
+
decals: Sequence[GroundCorpseDecal],
|
|
398
|
+
*,
|
|
399
|
+
shadow: bool = True,
|
|
400
|
+
) -> bool:
|
|
401
|
+
if not decals:
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
self.create_render_target()
|
|
405
|
+
if self.render_target is None:
|
|
406
|
+
self._fallback_bodyset_texture = bodyset_texture
|
|
407
|
+
self._fallback_corpse_shadow = bool(shadow)
|
|
408
|
+
self._fallback_corpse_decals.extend(decals)
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
if self.debug_log_stamps:
|
|
412
|
+
head = decals[0]
|
|
413
|
+
self._debug_stamp(
|
|
414
|
+
"bake_corpse_decals",
|
|
415
|
+
shadow=bool(shadow),
|
|
416
|
+
count=len(decals),
|
|
417
|
+
frame0=int(head.bodyset_frame),
|
|
418
|
+
top_left0={"x": float(head.top_left_x), "y": float(head.top_left_y)},
|
|
419
|
+
size0=float(head.size),
|
|
420
|
+
rot0=float(head.rotation_rad),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
scale = self._normalized_texture_scale()
|
|
424
|
+
inv_scale = 1.0 / scale
|
|
425
|
+
offset = 2.0 * scale / float(self.width)
|
|
426
|
+
self._set_texture_filters((bodyset_texture,), point=True)
|
|
427
|
+
|
|
428
|
+
rl.begin_texture_mode(self.render_target)
|
|
429
|
+
with _maybe_alpha_test(self.alpha_test):
|
|
430
|
+
if shadow:
|
|
431
|
+
if self.debug_log_stamps:
|
|
432
|
+
self._debug_stamp("corpse_shadow_pass", draws=len(decals))
|
|
433
|
+
self._draw_corpse_shadow_pass(bodyset_texture, decals, inv_scale, offset)
|
|
434
|
+
if self.debug_log_stamps:
|
|
435
|
+
self._debug_stamp("corpse_color_pass", draws=len(decals))
|
|
436
|
+
self._draw_corpse_color_pass(bodyset_texture, decals, inv_scale, offset)
|
|
437
|
+
rl.end_texture_mode()
|
|
438
|
+
|
|
439
|
+
self._set_texture_filters((bodyset_texture,), point=False)
|
|
440
|
+
self._render_target_ready = True
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
def _draw_fallback(
|
|
444
|
+
self,
|
|
445
|
+
camera_x: float,
|
|
446
|
+
camera_y: float,
|
|
447
|
+
*,
|
|
448
|
+
out_w: float,
|
|
449
|
+
out_h: float,
|
|
450
|
+
screen_w: float,
|
|
451
|
+
screen_h: float,
|
|
452
|
+
) -> None:
|
|
453
|
+
rl.draw_rectangle(0, 0, int(out_w + 0.5), int(out_h + 0.5), TERRAIN_CLEAR_COLOR)
|
|
454
|
+
if screen_w <= 0.0 or screen_h <= 0.0:
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
scale_x = out_w / screen_w
|
|
458
|
+
scale_y = out_h / screen_h
|
|
459
|
+
|
|
460
|
+
view_x0 = -camera_x
|
|
461
|
+
view_y0 = -camera_y
|
|
462
|
+
view_x1 = view_x0 + screen_w
|
|
463
|
+
view_y1 = view_y0 + screen_h
|
|
464
|
+
|
|
465
|
+
def draw_decal(decal: GroundDecal) -> None:
|
|
466
|
+
texture = decal.texture
|
|
467
|
+
if int(getattr(texture, "id", 0)) <= 0:
|
|
468
|
+
return
|
|
469
|
+
w = float(decal.width)
|
|
470
|
+
h = float(decal.height)
|
|
471
|
+
if w <= 0.0 or h <= 0.0:
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
pivot_x = float(decal.x)
|
|
475
|
+
pivot_y = float(decal.y)
|
|
476
|
+
if not decal.centered:
|
|
477
|
+
pivot_x += w * 0.5
|
|
478
|
+
pivot_y += h * 0.5
|
|
479
|
+
|
|
480
|
+
if pivot_x + w * 0.5 < view_x0 or pivot_x - w * 0.5 > view_x1:
|
|
481
|
+
return
|
|
482
|
+
if pivot_y + h * 0.5 < view_y0 or pivot_y - h * 0.5 > view_y1:
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
sx = (pivot_x + camera_x) * scale_x
|
|
486
|
+
sy = (pivot_y + camera_y) * scale_y
|
|
487
|
+
sw = w * scale_x
|
|
488
|
+
sh = h * scale_y
|
|
489
|
+
dst = rl.Rectangle(float(sx), float(sy), float(sw), float(sh))
|
|
490
|
+
origin = rl.Vector2(float(sw) * 0.5, float(sh) * 0.5)
|
|
491
|
+
rl.draw_texture_pro(
|
|
492
|
+
texture,
|
|
493
|
+
decal.src,
|
|
494
|
+
dst,
|
|
495
|
+
origin,
|
|
496
|
+
math.degrees(float(decal.rotation_rad)),
|
|
497
|
+
decal.tint,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
with _blend_custom_separate(
|
|
501
|
+
rl.RL_SRC_ALPHA,
|
|
502
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
503
|
+
rl.RL_ZERO,
|
|
504
|
+
rl.RL_ONE,
|
|
505
|
+
rl.RL_FUNC_ADD,
|
|
506
|
+
rl.RL_FUNC_ADD,
|
|
507
|
+
):
|
|
508
|
+
for patch in self._fallback_patches:
|
|
509
|
+
draw_decal(patch)
|
|
510
|
+
for decal in self._fallback_decals:
|
|
511
|
+
draw_decal(decal)
|
|
512
|
+
|
|
513
|
+
bodyset_texture = self._fallback_bodyset_texture
|
|
514
|
+
if bodyset_texture is None or not self._fallback_corpse_decals:
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
def draw_corpse(size: float, pivot_x: float, pivot_y: float, rotation_deg: float, tint: rl.Color, src: rl.Rectangle) -> None:
|
|
518
|
+
if pivot_x + size * 0.5 < view_x0 or pivot_x - size * 0.5 > view_x1:
|
|
519
|
+
return
|
|
520
|
+
if pivot_y + size * 0.5 < view_y0 or pivot_y - size * 0.5 > view_y1:
|
|
521
|
+
return
|
|
522
|
+
sx = (pivot_x + camera_x) * scale_x
|
|
523
|
+
sy = (pivot_y + camera_y) * scale_y
|
|
524
|
+
sw = size * scale_x
|
|
525
|
+
sh = size * scale_y
|
|
526
|
+
dst = rl.Rectangle(float(sx), float(sy), float(sw), float(sh))
|
|
527
|
+
origin = rl.Vector2(float(sw) * 0.5, float(sh) * 0.5)
|
|
528
|
+
rl.draw_texture_pro(bodyset_texture, src, dst, origin, rotation_deg, tint)
|
|
529
|
+
|
|
530
|
+
with _maybe_alpha_test(self.alpha_test):
|
|
531
|
+
if self._fallback_corpse_shadow:
|
|
532
|
+
with _blend_custom_separate(
|
|
533
|
+
rl.RL_ZERO,
|
|
534
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
535
|
+
rl.RL_ZERO,
|
|
536
|
+
rl.RL_ONE,
|
|
537
|
+
rl.RL_FUNC_ADD,
|
|
538
|
+
rl.RL_FUNC_ADD,
|
|
539
|
+
):
|
|
540
|
+
for decal in self._fallback_corpse_decals:
|
|
541
|
+
src = self._corpse_src(bodyset_texture, decal.bodyset_frame)
|
|
542
|
+
size = float(decal.size) * 1.064
|
|
543
|
+
pivot_x = float(decal.top_left_x - 0.5) + size * 0.5
|
|
544
|
+
pivot_y = float(decal.top_left_y - 0.5) + size * 0.5
|
|
545
|
+
tint = rl.Color(decal.tint.r, decal.tint.g, decal.tint.b, int(decal.tint.a * 0.5))
|
|
546
|
+
rotation_deg = math.degrees(float(decal.rotation_rad) - (math.pi * 0.5))
|
|
547
|
+
draw_corpse(size, pivot_x, pivot_y, rotation_deg, tint, src)
|
|
548
|
+
|
|
549
|
+
with _blend_custom_separate(
|
|
550
|
+
rl.RL_SRC_ALPHA,
|
|
551
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
552
|
+
rl.RL_ZERO,
|
|
553
|
+
rl.RL_ONE,
|
|
554
|
+
rl.RL_FUNC_ADD,
|
|
555
|
+
rl.RL_FUNC_ADD,
|
|
556
|
+
):
|
|
557
|
+
for decal in self._fallback_corpse_decals:
|
|
558
|
+
src = self._corpse_src(bodyset_texture, decal.bodyset_frame)
|
|
559
|
+
size = float(decal.size)
|
|
560
|
+
pivot_x = float(decal.top_left_x) + size * 0.5
|
|
561
|
+
pivot_y = float(decal.top_left_y) + size * 0.5
|
|
562
|
+
rotation_deg = math.degrees(float(decal.rotation_rad) - (math.pi * 0.5))
|
|
563
|
+
draw_corpse(size, pivot_x, pivot_y, rotation_deg, decal.tint, src)
|
|
564
|
+
|
|
565
|
+
def draw(
|
|
566
|
+
self,
|
|
567
|
+
camera_x: float,
|
|
568
|
+
camera_y: float,
|
|
569
|
+
*,
|
|
570
|
+
screen_w: float | None = None,
|
|
571
|
+
screen_h: float | None = None,
|
|
572
|
+
) -> None:
|
|
573
|
+
out_w = float(rl.get_screen_width())
|
|
574
|
+
out_h = float(rl.get_screen_height())
|
|
575
|
+
if screen_w is None:
|
|
576
|
+
screen_w = float(self.screen_width or out_w)
|
|
577
|
+
if screen_h is None:
|
|
578
|
+
screen_h = float(self.screen_height or out_h)
|
|
579
|
+
if screen_w <= 0.0:
|
|
580
|
+
screen_w = out_w
|
|
581
|
+
if screen_h <= 0.0:
|
|
582
|
+
screen_h = out_h
|
|
583
|
+
if screen_w > self.width:
|
|
584
|
+
screen_w = float(self.width)
|
|
585
|
+
if screen_h > self.height:
|
|
586
|
+
screen_h = float(self.height)
|
|
587
|
+
cam_x, cam_y = self._clamp_camera(camera_x, camera_y, screen_w, screen_h)
|
|
588
|
+
|
|
589
|
+
if self.render_target is None or not self._render_target_ready:
|
|
590
|
+
self._draw_fallback(cam_x, cam_y, out_w=out_w, out_h=out_h, screen_w=float(screen_w), screen_h=float(screen_h))
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
target = self.render_target
|
|
594
|
+
u0 = -cam_x / float(self.width)
|
|
595
|
+
v0 = -cam_y / float(self.height)
|
|
596
|
+
u1 = u0 + screen_w / float(self.width)
|
|
597
|
+
v1 = v0 + screen_h / float(self.height)
|
|
598
|
+
src_x = u0 * float(target.texture.width)
|
|
599
|
+
# Render textures are vertically flipped in raylib, so adjust the source
|
|
600
|
+
# rectangle to sample the correct world-space slice before flipping.
|
|
601
|
+
src_y = (1.0 - v1) * float(target.texture.height)
|
|
602
|
+
src_w = (u1 - u0) * float(target.texture.width)
|
|
603
|
+
src_h = (v1 - v0) * float(target.texture.height)
|
|
604
|
+
src = rl.Rectangle(src_x, src_y, src_w, -src_h)
|
|
605
|
+
dst = rl.Rectangle(0.0, 0.0, out_w, out_h)
|
|
606
|
+
if self.terrain_filter == 2.0:
|
|
607
|
+
rl.set_texture_filter(target.texture, rl.TEXTURE_FILTER_POINT)
|
|
608
|
+
# Disable alpha blending when drawing terrain to screen - the render target's
|
|
609
|
+
# alpha channel may be < 1.0 after stamp blending, but terrain should be opaque.
|
|
610
|
+
with _blend_custom(rl.RL_ONE, rl.RL_ZERO, rl.RL_FUNC_ADD):
|
|
611
|
+
rl.draw_texture_pro(target.texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
612
|
+
if self.terrain_filter == 2.0:
|
|
613
|
+
rl.set_texture_filter(target.texture, rl.TEXTURE_FILTER_BILINEAR)
|
|
614
|
+
|
|
615
|
+
def _scatter_texture(
|
|
616
|
+
self,
|
|
617
|
+
texture: rl.Texture,
|
|
618
|
+
tint: rl.Color,
|
|
619
|
+
rng: CrtRand,
|
|
620
|
+
density: int,
|
|
621
|
+
) -> None:
|
|
622
|
+
area = self.width * self.height
|
|
623
|
+
count = (area * density) >> TERRAIN_DENSITY_SHIFT
|
|
624
|
+
if count <= 0:
|
|
625
|
+
return
|
|
626
|
+
inv_scale = 1.0 / self._normalized_texture_scale()
|
|
627
|
+
size = TERRAIN_PATCH_SIZE * inv_scale
|
|
628
|
+
src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
|
|
629
|
+
origin = rl.Vector2(size * 0.5, size * 0.5)
|
|
630
|
+
span_w = self.width + int(TERRAIN_PATCH_OVERSCAN * 2)
|
|
631
|
+
# The original exe uses `terrain_texture_width` for both axes. Terrain is
|
|
632
|
+
# square (1024x1024) so this is equivalent, but keep it for parity.
|
|
633
|
+
span_h = span_w
|
|
634
|
+
for _ in range(count):
|
|
635
|
+
angle = ((rng.rand() % TERRAIN_ROTATION_MAX) * 0.01) % math.tau
|
|
636
|
+
# IMPORTANT: The exe consumes RNG as rotation, then Y, then X.
|
|
637
|
+
y = ((rng.rand() % span_h) - TERRAIN_PATCH_OVERSCAN) * inv_scale
|
|
638
|
+
x = ((rng.rand() % span_w) - TERRAIN_PATCH_OVERSCAN) * inv_scale
|
|
639
|
+
# raylib's DrawTexturePro positions the quad by the *origin point*,
|
|
640
|
+
# while the original engine uses x/y as the quad top-left.
|
|
641
|
+
dst = rl.Rectangle(float(x + size * 0.5), float(y + size * 0.5), size, size)
|
|
642
|
+
rl.draw_texture_pro(texture, src, dst, origin, math.degrees(angle), tint)
|
|
643
|
+
|
|
644
|
+
def _scatter_texture_fallback(
|
|
645
|
+
self,
|
|
646
|
+
texture: rl.Texture,
|
|
647
|
+
tint: rl.Color,
|
|
648
|
+
rng: CrtRand,
|
|
649
|
+
density: int,
|
|
650
|
+
) -> None:
|
|
651
|
+
"""Record terrain patch draws for the render-target fallback path."""
|
|
652
|
+
area = self.width * self.height
|
|
653
|
+
count = (area * density) >> TERRAIN_DENSITY_SHIFT
|
|
654
|
+
if count <= 0:
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
size = float(TERRAIN_PATCH_SIZE)
|
|
658
|
+
src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
|
|
659
|
+
span_w = self.width + int(TERRAIN_PATCH_OVERSCAN * 2)
|
|
660
|
+
# The original exe uses `terrain_texture_width` for both axes. Terrain is
|
|
661
|
+
# square (1024x1024) so this is equivalent, but keep it for parity.
|
|
662
|
+
span_h = span_w
|
|
663
|
+
|
|
664
|
+
for _ in range(count):
|
|
665
|
+
angle = ((rng.rand() % TERRAIN_ROTATION_MAX) * 0.01) % math.tau
|
|
666
|
+
# IMPORTANT: The exe consumes RNG as rotation, then Y, then X.
|
|
667
|
+
y = float((rng.rand() % span_h) - TERRAIN_PATCH_OVERSCAN)
|
|
668
|
+
x = float((rng.rand() % span_w) - TERRAIN_PATCH_OVERSCAN)
|
|
669
|
+
self._fallback_patches.append(
|
|
670
|
+
GroundDecal(
|
|
671
|
+
texture=texture,
|
|
672
|
+
src=src,
|
|
673
|
+
x=float(x + size * 0.5),
|
|
674
|
+
y=float(y + size * 0.5),
|
|
675
|
+
width=size,
|
|
676
|
+
height=size,
|
|
677
|
+
rotation_rad=float(angle),
|
|
678
|
+
tint=tint,
|
|
679
|
+
centered=True,
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def _clamp_camera(self, camera_x: float, camera_y: float, screen_w: float, screen_h: float) -> tuple[float, float]:
|
|
684
|
+
min_x = screen_w - float(self.width)
|
|
685
|
+
min_y = screen_h - float(self.height)
|
|
686
|
+
if camera_x > -1.0:
|
|
687
|
+
camera_x = -1.0
|
|
688
|
+
if camera_y > -1.0:
|
|
689
|
+
camera_y = -1.0
|
|
690
|
+
if camera_x < min_x:
|
|
691
|
+
camera_x = min_x
|
|
692
|
+
if camera_y < min_y:
|
|
693
|
+
camera_y = min_y
|
|
694
|
+
return camera_x, camera_y
|
|
695
|
+
|
|
696
|
+
def _ensure_render_target(self, render_w: int, render_h: int) -> bool:
|
|
697
|
+
if self.render_target is not None:
|
|
698
|
+
if self.render_target.texture.width == render_w and self.render_target.texture.height == render_h:
|
|
699
|
+
return True
|
|
700
|
+
rl.unload_render_texture(self.render_target)
|
|
701
|
+
self.render_target = None
|
|
702
|
+
self._render_target_ready = False
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
candidate = rl.load_render_texture(render_w, render_h)
|
|
706
|
+
except Exception:
|
|
707
|
+
return False
|
|
708
|
+
|
|
709
|
+
if not getattr(candidate, "id", 0) or not rl.is_render_texture_valid(candidate):
|
|
710
|
+
if getattr(candidate, "id", 0):
|
|
711
|
+
rl.unload_render_texture(candidate)
|
|
712
|
+
return False
|
|
713
|
+
if (
|
|
714
|
+
getattr(getattr(candidate, "texture", None), "width", 0) <= 0
|
|
715
|
+
or getattr(getattr(candidate, "texture", None), "height", 0) <= 0
|
|
716
|
+
):
|
|
717
|
+
rl.unload_render_texture(candidate)
|
|
718
|
+
return False
|
|
719
|
+
|
|
720
|
+
self.render_target = candidate
|
|
721
|
+
self._render_target_ready = False
|
|
722
|
+
self._render_target_warmup_passes = 1
|
|
723
|
+
rl.set_texture_filter(self.render_target.texture, rl.TEXTURE_FILTER_BILINEAR)
|
|
724
|
+
rl.set_texture_wrap(self.render_target.texture, rl.TEXTURE_WRAP_CLAMP)
|
|
725
|
+
return True
|
|
726
|
+
|
|
727
|
+
def _render_target_size_for(self, scale: float) -> tuple[int, int]:
|
|
728
|
+
render_w = max(1, int(self.width / scale))
|
|
729
|
+
render_h = max(1, int(self.height / scale))
|
|
730
|
+
return render_w, render_h
|
|
731
|
+
|
|
732
|
+
def _normalized_texture_scale(self) -> float:
|
|
733
|
+
scale = self.texture_scale
|
|
734
|
+
if scale < 0.5:
|
|
735
|
+
scale = 0.5
|
|
736
|
+
return scale
|
|
737
|
+
|
|
738
|
+
def _set_stamp_filters(self, *, point: bool) -> None:
|
|
739
|
+
self._set_texture_filters(
|
|
740
|
+
(self.texture, self.overlay, self.overlay_detail),
|
|
741
|
+
point=point,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
@staticmethod
|
|
745
|
+
def _unique_textures(textures: Iterable[rl.Texture]) -> list[rl.Texture]:
|
|
746
|
+
unique: list[rl.Texture] = []
|
|
747
|
+
seen: set[int] = set()
|
|
748
|
+
for texture in textures:
|
|
749
|
+
texture_id = int(getattr(texture, "id", 0))
|
|
750
|
+
if texture_id <= 0 or texture_id in seen:
|
|
751
|
+
continue
|
|
752
|
+
seen.add(texture_id)
|
|
753
|
+
unique.append(texture)
|
|
754
|
+
return unique
|
|
755
|
+
|
|
756
|
+
@staticmethod
|
|
757
|
+
def _set_texture_filters(textures: Iterable[rl.Texture | None], *, point: bool) -> None:
|
|
758
|
+
mode = rl.TEXTURE_FILTER_POINT if point else rl.TEXTURE_FILTER_BILINEAR
|
|
759
|
+
for texture in textures:
|
|
760
|
+
if texture is None:
|
|
761
|
+
continue
|
|
762
|
+
if int(getattr(texture, "id", 0)) <= 0:
|
|
763
|
+
continue
|
|
764
|
+
rl.set_texture_filter(texture, mode)
|
|
765
|
+
|
|
766
|
+
def _corpse_src(self, bodyset_texture: rl.Texture, frame: int) -> rl.Rectangle:
|
|
767
|
+
frame = int(frame) & 0xF
|
|
768
|
+
cell_w = float(bodyset_texture.width) * 0.25
|
|
769
|
+
cell_h = float(bodyset_texture.height) * 0.25
|
|
770
|
+
col = frame & 3
|
|
771
|
+
row = frame >> 2
|
|
772
|
+
return rl.Rectangle(cell_w * float(col), cell_h * float(row), cell_w, cell_h)
|
|
773
|
+
|
|
774
|
+
def _draw_corpse_shadow_pass(
|
|
775
|
+
self,
|
|
776
|
+
bodyset_texture: rl.Texture,
|
|
777
|
+
decals: Sequence[GroundCorpseDecal],
|
|
778
|
+
inv_scale: float,
|
|
779
|
+
offset: float,
|
|
780
|
+
) -> None:
|
|
781
|
+
with _blend_custom_separate(
|
|
782
|
+
rl.RL_ZERO,
|
|
783
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
784
|
+
rl.RL_ZERO,
|
|
785
|
+
rl.RL_ONE,
|
|
786
|
+
rl.RL_FUNC_ADD,
|
|
787
|
+
rl.RL_FUNC_ADD,
|
|
788
|
+
):
|
|
789
|
+
for decal in decals:
|
|
790
|
+
src = self._corpse_src(bodyset_texture, decal.bodyset_frame)
|
|
791
|
+
size = decal.size * inv_scale * 1.064
|
|
792
|
+
x = (decal.top_left_x - 0.5) * inv_scale - offset
|
|
793
|
+
y = (decal.top_left_y - 0.5) * inv_scale - offset
|
|
794
|
+
dst = rl.Rectangle(x + size * 0.5, y + size * 0.5, size, size)
|
|
795
|
+
origin = rl.Vector2(size * 0.5, size * 0.5)
|
|
796
|
+
tint = rl.Color(
|
|
797
|
+
decal.tint.r,
|
|
798
|
+
decal.tint.g,
|
|
799
|
+
decal.tint.b,
|
|
800
|
+
int(decal.tint.a * 0.5),
|
|
801
|
+
)
|
|
802
|
+
rl.draw_texture_pro(
|
|
803
|
+
bodyset_texture,
|
|
804
|
+
src,
|
|
805
|
+
dst,
|
|
806
|
+
origin,
|
|
807
|
+
math.degrees(decal.rotation_rad - (math.pi * 0.5)),
|
|
808
|
+
tint,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
def _draw_corpse_color_pass(
|
|
812
|
+
self,
|
|
813
|
+
bodyset_texture: rl.Texture,
|
|
814
|
+
decals: Sequence[GroundCorpseDecal],
|
|
815
|
+
inv_scale: float,
|
|
816
|
+
offset: float,
|
|
817
|
+
) -> None:
|
|
818
|
+
with _blend_custom_separate(
|
|
819
|
+
rl.RL_SRC_ALPHA,
|
|
820
|
+
rl.RL_ONE_MINUS_SRC_ALPHA,
|
|
821
|
+
rl.RL_ZERO,
|
|
822
|
+
rl.RL_ONE,
|
|
823
|
+
rl.RL_FUNC_ADD,
|
|
824
|
+
rl.RL_FUNC_ADD,
|
|
825
|
+
):
|
|
826
|
+
for decal in decals:
|
|
827
|
+
src = self._corpse_src(bodyset_texture, decal.bodyset_frame)
|
|
828
|
+
size = decal.size * inv_scale
|
|
829
|
+
x = decal.top_left_x * inv_scale - offset
|
|
830
|
+
y = decal.top_left_y * inv_scale - offset
|
|
831
|
+
dst = rl.Rectangle(x + size * 0.5, y + size * 0.5, size, size)
|
|
832
|
+
origin = rl.Vector2(size * 0.5, size * 0.5)
|
|
833
|
+
rl.draw_texture_pro(
|
|
834
|
+
bodyset_texture,
|
|
835
|
+
src,
|
|
836
|
+
dst,
|
|
837
|
+
origin,
|
|
838
|
+
math.degrees(decal.rotation_rad - (math.pi * 0.5)),
|
|
839
|
+
decal.tint,
|
|
840
|
+
)
|