nodes/prop.pyΒΆ
Part of Q1K3.
1"""Static props: barrels (explode on damage), torches (flickering lights),
2level-trigger (advances to next map)."""
3
4from __future__ import annotations
5
6import math
7import random
8from typing import TYPE_CHECKING
9
10from simvx.core import Material, MeshInstance3D, Node3D, PointLight3D, Vec3
11
12from . import audio, meshes, textures
13
14if TYPE_CHECKING: # pragma: no cover
15 from .root import Q1K3Root
16
17
18class Barrel(Node3D):
19 body_scale = (16, 32, 16)
20
21 def __init__(self, game: Q1K3Root, pos: Vec3) -> None:
22 super().__init__()
23 self.game = game
24 self.position = pos
25 self.p = Vec3(pos.x, pos.y, pos.z)
26 self.s = Vec3(8, 32, 8)
27 self._health = 10
28 self._dead = False
29 self._inst = MeshInstance3D(
30 mesh=meshes.cube(),
31 material=Material(albedo_map=textures.get(textures.TEX_PLASMA), roughness=0.5, metallic=0.4),
32 scale=type(self).body_scale,
33 position=(0, type(self).body_scale[1] / 2, 0),
34 )
35 self.add_child(self._inst)
36
37 def receive_damage(self, source, amount: float) -> None:
38 if self._dead:
39 return
40 self._health -= int(amount)
41 if self._health <= 0:
42 self._kill()
43
44 def _kill(self) -> None:
45 if self._dead:
46 return
47 self._dead = True
48 # AoE damage
49 for entity in self.game.enemies_list() + self.game.friendlies_list():
50 if entity is self:
51 continue
52 d = (self.p - entity.p).length()
53 if d < 256 and hasattr(entity, "receive_damage"):
54 falloff = max(0.0, (256 - d) / 256)
55 entity.receive_damage(self, 60 * falloff)
56 self.game.play_sfx_at(audio.sfx_grenade_explode(), self.p)
57 self.game.spawn_particles(
58 self.p + Vec3(0, 16, 0), count=20, speed=400, lifetime=0.8, tex_id=textures.TEX_PARTICLE
59 )
60 self.game.spawn_temp_light(self.p + Vec3(0, 16, 0), intensity=8.0, colour=(1.0, 0.4, 0.05), duration=0.4)
61 self.game.queue_remove(self)
62
63
64class Torch(Node3D):
65 """Flickering point light + textured cube body."""
66
67 body_scale = (8, 24, 8)
68
69 def __init__(self, game: Q1K3Root, pos: Vec3) -> None:
70 super().__init__()
71 self.game = game
72 self.position = pos
73 self._inst = MeshInstance3D(
74 mesh=meshes.cube(),
75 material=Material(
76 albedo_map=textures.get(textures.TEX_TORCH),
77 emissive_colour=(1.0, 0.7, 0.3, 6.0),
78 roughness=0.6,
79 metallic=0.0,
80 ),
81 scale=type(self).body_scale,
82 )
83 self.add_child(self._inst)
84 self.light = PointLight3D(
85 position=(0, 32, 0),
86 colour=(1.0, 0.75, 0.25),
87 intensity=3.0,
88 range=400.0,
89 )
90 self.add_child(self.light)
91 self._flicker = 0.0
92 self._t = random.random()
93
94 def on_update(self, dt: float) -> None:
95 self._t += dt
96 # Random flicker every few frames
97 if random.random() > 0.8:
98 self._flicker = random.random()
99 self.light.intensity = 2.5 + math.sin(self._t * 4) * 0.4 + self._flicker * 0.5
100
101
102class LevelTrigger(Node3D):
103 """Invisible node that advances the current map when player enters."""
104
105 RADIUS = 96.0
106
107 def __init__(self, game: Q1K3Root, pos: Vec3) -> None:
108 super().__init__()
109 self.game = game
110 self.position = pos
111 self._dead = False
112
113 def on_update(self, dt: float) -> None:
114 player = self.game.player
115 if not player or self._dead:
116 return
117 if (self.position - player.p).length() < type(self).RADIUS:
118 self._dead = True
119 self.game.next_level()