nodes/weapon.py¶
Part of Q1K3.
1"""Weapons for Q1K3 port: Shotgun, Nailgun, GrenadeLauncher.
2
3Mirrors upstream `weapons.js`. Each weapon spawns a projectile entity at the
4player's position + a small offset rotated by the player's yaw/pitch.
5"""
6
7from __future__ import annotations
8
9import random
10
11from simvx.core import Vec3
12
13from . import audio
14from .mathutil import rotate_yaw_pitch
15
16
17class Weapon:
18 """Base class: caller invokes ``shoot(player_pos, yaw, pitch)``.
19
20 Subclasses set:
21 - ``reload``: cooldown in seconds
22 - ``ammo``: starting ammo (or ``None`` for infinite)
23 - ``projectile_factory``: callable(world, pos, vel, yaw, pitch, group) -> Projectile
24 - ``projectile_speed``: units/s
25 - ``shoot_sfx``: callable returning the AudioClip to play
26 - ``projectile_offset``: Vec3 added to muzzle position pre-yaw/pitch rotation
27 """
28
29 name = "weapon"
30 reload = 0.5
31 ammo: int | None = None
32 projectile_speed = 1000.0
33 projectile_offset = Vec3(0, 0, 8)
34 shoot_sfx = staticmethod(audio.sfx_shotgun_shoot)
35
36 def __init__(self) -> None:
37 self._projectile_offset = Vec3(self.projectile_offset.x, self.projectile_offset.y, self.projectile_offset.z)
38 self._ammo = self.ammo # None for infinite
39
40 def needs_ammo(self) -> bool:
41 return self._ammo is not None
42
43 def has_ammo(self) -> bool:
44 return self._ammo is None or self._ammo > 0
45
46 def display_ammo(self) -> str:
47 return "INF" if self._ammo is None else str(self._ammo)
48
49 def shoot(self, game, pos: Vec3, yaw: float, pitch: float) -> None:
50 if self.needs_ammo():
51 self._ammo -= 1
52 # Play sound (player view: not spatial, non-attenuated)
53 game.play_sfx(self.shoot_sfx())
54 self._spawn_projectile(game, pos, yaw, pitch)
55
56 def _spawn_projectile(self, game, pos: Vec3, yaw: float, pitch: float) -> None:
57 from .projectile import spawn_projectile
58
59 muzzle = pos + Vec3(0, 12, 0) + rotate_yaw_pitch(self._projectile_offset, yaw, pitch)
60 vel = rotate_yaw_pitch(Vec3(0, 0, self.projectile_speed), yaw, pitch)
61 spawn_projectile(game, self.projectile_kind(), muzzle, vel, yaw, pitch, group="enemy")
62 # Alternate L/R fire (nailgun)
63 self._projectile_offset = Vec3(-self._projectile_offset.x, self._projectile_offset.y, self._projectile_offset.z)
64
65 def projectile_kind(self) -> str:
66 raise NotImplementedError
67
68
69class Shotgun(Weapon):
70 name = "shotgun"
71 reload = 0.9
72 ammo = None # infinite
73 projectile_speed = 10000.0
74 shoot_sfx = staticmethod(audio.sfx_shotgun_shoot)
75
76 def projectile_kind(self) -> str:
77 return "shell"
78
79 def _spawn_projectile(self, game, pos: Vec3, yaw: float, pitch: float) -> None:
80 from .projectile import spawn_projectile
81
82 # 8 pellets, ±0.04 spread
83 for _ in range(8):
84 y = yaw + random.random() * 0.08 - 0.04
85 p = pitch + random.random() * 0.08 - 0.04
86 muzzle = pos + Vec3(0, 12, 0) + rotate_yaw_pitch(self._projectile_offset, y, p)
87 vel = rotate_yaw_pitch(Vec3(0, 0, self.projectile_speed), y, p)
88 spawn_projectile(game, "shell", muzzle, vel, y, p, group="enemy")
89
90
91class Nailgun(Weapon):
92 name = "nailgun"
93 reload = 0.09
94 ammo = 100
95 projectile_speed = 1300.0
96 projectile_offset = Vec3(6, 0, 8)
97 shoot_sfx = staticmethod(audio.sfx_nailgun_shoot)
98
99 def projectile_kind(self) -> str:
100 return "nail"
101
102
103class GrenadeLauncher(Weapon):
104 name = "grenadelauncher"
105 reload = 0.65
106 ammo = 10
107 projectile_speed = 900.0
108 shoot_sfx = staticmethod(audio.sfx_grenade_shoot)
109
110 def projectile_kind(self) -> str:
111 return "grenade"