starsec-client 0.910.0__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.
- starsec_client/__init__.py +16 -0
- starsec_client/_bootstrap.py +41 -0
- starsec_client/_vendor/starsec_core/__init__.py +28 -0
- starsec_client/_vendor/starsec_core/auth.py +130 -0
- starsec_client/_vendor/starsec_core/combat_hitbox.py +102 -0
- starsec_client/_vendor/starsec_core/config.py +777 -0
- starsec_client/_vendor/starsec_core/docker_transport.py +169 -0
- starsec_client/_vendor/starsec_core/editor/__init__.py +52 -0
- starsec_client/_vendor/starsec_core/editor/commands.py +725 -0
- starsec_client/_vendor/starsec_core/editor/document.py +205 -0
- starsec_client/_vendor/starsec_core/fighter_classes.py +67 -0
- starsec_client/_vendor/starsec_core/fighter_hud.py +103 -0
- starsec_client/_vendor/starsec_core/fighter_input.py +206 -0
- starsec_client/_vendor/starsec_core/gltf_meta.py +334 -0
- starsec_client/_vendor/starsec_core/home.py +128 -0
- starsec_client/_vendor/starsec_core/input.py +327 -0
- starsec_client/_vendor/starsec_core/installer/__init__.py +41 -0
- starsec_client/_vendor/starsec_core/installer/__main__.py +4 -0
- starsec_client/_vendor/starsec_core/installer/desktop.py +70 -0
- starsec_client/_vendor/starsec_core/installer/runner.py +47 -0
- starsec_client/_vendor/starsec_core/installer/state.py +113 -0
- starsec_client/_vendor/starsec_core/installer/wizard.py +281 -0
- starsec_client/_vendor/starsec_core/net/__init__.py +41 -0
- starsec_client/_vendor/starsec_core/net/bus.py +168 -0
- starsec_client/_vendor/starsec_core/net/codec.py +60 -0
- starsec_client/_vendor/starsec_core/net/messages.py +161 -0
- starsec_client/_vendor/starsec_core/net/stream/__init__.py +38 -0
- starsec_client/_vendor/starsec_core/net/stream/header.py +282 -0
- starsec_client/_vendor/starsec_core/net/stream/packet.py +72 -0
- starsec_client/_vendor/starsec_core/net/stream/reassemble.py +123 -0
- starsec_client/_vendor/starsec_core/net/stream/receiver.py +97 -0
- starsec_client/_vendor/starsec_core/net/stream/relay.py +284 -0
- starsec_client/_vendor/starsec_core/net/stream/sender.py +125 -0
- starsec_client/_vendor/starsec_core/net/terminal_protocol.py +74 -0
- starsec_client/_vendor/starsec_core/net/updates.py +149 -0
- starsec_client/_vendor/starsec_core/render/__init__.py +34 -0
- starsec_client/_vendor/starsec_core/render/assets.py +112 -0
- starsec_client/_vendor/starsec_core/render/audio.py +562 -0
- starsec_client/_vendor/starsec_core/render/avatars.py +74 -0
- starsec_client/_vendor/starsec_core/render/camera.py +75 -0
- starsec_client/_vendor/starsec_core/render/camera_modes.py +88 -0
- starsec_client/_vendor/starsec_core/render/color.py +11 -0
- starsec_client/_vendor/starsec_core/render/crt.py +148 -0
- starsec_client/_vendor/starsec_core/render/damage_fx.py +74 -0
- starsec_client/_vendor/starsec_core/render/datamosh.py +110 -0
- starsec_client/_vendor/starsec_core/render/explosions.py +146 -0
- starsec_client/_vendor/starsec_core/render/frustum.py +95 -0
- starsec_client/_vendor/starsec_core/render/lighting.py +333 -0
- starsec_client/_vendor/starsec_core/render/lod_director.py +74 -0
- starsec_client/_vendor/starsec_core/render/lod_policy.py +98 -0
- starsec_client/_vendor/starsec_core/render/markers.py +91 -0
- starsec_client/_vendor/starsec_core/render/material.py +154 -0
- starsec_client/_vendor/starsec_core/render/model_render.py +174 -0
- starsec_client/_vendor/starsec_core/render/model_shading.py +209 -0
- starsec_client/_vendor/starsec_core/render/model_view.py +542 -0
- starsec_client/_vendor/starsec_core/render/pick.py +144 -0
- starsec_client/_vendor/starsec_core/render/postfx.py +70 -0
- starsec_client/_vendor/starsec_core/render/scene.py +312 -0
- starsec_client/_vendor/starsec_core/render/shading.py +101 -0
- starsec_client/_vendor/starsec_core/render/shield_bubble.py +187 -0
- starsec_client/_vendor/starsec_core/render/ship_lod.py +161 -0
- starsec_client/_vendor/starsec_core/render/sky.py +209 -0
- starsec_client/_vendor/starsec_core/render/stats.py +52 -0
- starsec_client/_vendor/starsec_core/render/terminal_screen.py +196 -0
- starsec_client/_vendor/starsec_core/render/tri_pick.py +111 -0
- starsec_client/_vendor/starsec_core/render/vfx.py +717 -0
- starsec_client/_vendor/starsec_core/rig.py +225 -0
- starsec_client/_vendor/starsec_core/scene.py +266 -0
- starsec_client/_vendor/starsec_core/schema.py +209 -0
- starsec_client/_vendor/starsec_core/scoreboard.py +75 -0
- starsec_client/_vendor/starsec_core/spectator_camera.py +91 -0
- starsec_client/_vendor/starsec_core/terminal.py +905 -0
- starsec_client/_vendor/starsec_core/touch_input.py +184 -0
- starsec_client/_vendor/starsec_core/tracing.py +45 -0
- starsec_client/_vendor/starsec_core/ui/__init__.py +459 -0
- starsec_client/_vendor/starsec_core/ui/ascii_stars.py +85 -0
- starsec_client/_vendor/starsec_core/ui/audio.py +202 -0
- starsec_client/_vendor/starsec_core/ui/components.py +833 -0
- starsec_client/_vendor/starsec_core/ui/containers.py +343 -0
- starsec_client/_vendor/starsec_core/ui/core.py +250 -0
- starsec_client/_vendor/starsec_core/ui/feedback.py +213 -0
- starsec_client/_vendor/starsec_core/ui/font.py +110 -0
- starsec_client/_vendor/starsec_core/ui/layout.py +188 -0
- starsec_client/_vendor/starsec_core/ui/nav.py +232 -0
- starsec_client/_vendor/starsec_core/ui/theme.py +190 -0
- starsec_client/_vendor/starsec_core/ui/titlebar.py +239 -0
- starsec_client/_vendor/starsec_core/ui/tokens.py +77 -0
- starsec_client/_vendor/starsec_core/ui/widgets.py +654 -0
- starsec_client/_vendor/starsec_core/version.py +32 -0
- starsec_client/_vendor/starsec_core/world.py +813 -0
- starsec_client/_vendor/starsec_core/world_package.py +219 -0
- starsec_client/announce.py +87 -0
- starsec_client/bindings_ui.py +177 -0
- starsec_client/client.py +996 -0
- starsec_client/client_checks.py +89 -0
- starsec_client/installer.py +7 -0
- starsec_client/lobby.py +92 -0
- starsec_client/modes.py +17 -0
- starsec_client/net.py +229 -0
- starsec_client/pause_mode.py +172 -0
- starsec_client/pause_ui.py +165 -0
- starsec_client/play_view.py +249 -0
- starsec_client/settings_ui.py +248 -0
- starsec_client/spectate_mode.py +385 -0
- starsec_client/spectator.py +497 -0
- starsec_client/spectator_ui.py +334 -0
- starsec_client/terminal_remote.py +116 -0
- starsec_client/terminal_ws.py +154 -0
- starsec_client/transport.py +57 -0
- starsec_client-0.910.0.dist-info/METADATA +40 -0
- starsec_client-0.910.0.dist-info/RECORD +113 -0
- starsec_client-0.910.0.dist-info/WHEEL +4 -0
- starsec_client-0.910.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ruff: noqa: I001 (the vendored-core shim must import before starsec_core)
|
|
2
|
+
"""starsec-client — the standalone STARSEC player app.
|
|
3
|
+
|
|
4
|
+
Entrypoint is :func:`main` (console script ``starsec-client``). The player
|
|
5
|
+
app shows a main menu, joins a match lobby, and plays: FPS movement plus
|
|
6
|
+
in-world terminal focus. Level authoring lives in starsec-engine, not here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Must be first: registers the vendored starsec_core when it is not installed.
|
|
10
|
+
import starsec_client._bootstrap # noqa: F401
|
|
11
|
+
|
|
12
|
+
from starsec_client.client import Client, Mode, main
|
|
13
|
+
from starsec_core.version import CLIENT_VERSION
|
|
14
|
+
|
|
15
|
+
__version__ = CLIENT_VERSION
|
|
16
|
+
__all__ = ["Client", "Mode", "main"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Resolve ``starsec_core`` to the vendored copy when it is not installed.
|
|
2
|
+
|
|
3
|
+
The client ships a copy of the shared runtime (``_vendor/starsec_core``) so the
|
|
4
|
+
PyPI package never depends on the private core distribution. The copy is loaded
|
|
5
|
+
**under the name** ``starsec_core`` so every absolute import inside it (and in
|
|
6
|
+
the client) resolves unchanged.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def install() -> bool:
|
|
18
|
+
"""Make ``starsec_core`` importable. True when the vendored copy is used."""
|
|
19
|
+
if "starsec_core" in sys.modules:
|
|
20
|
+
return False
|
|
21
|
+
try:
|
|
22
|
+
importlib.import_module("starsec_core")
|
|
23
|
+
return False
|
|
24
|
+
except ImportError:
|
|
25
|
+
pass
|
|
26
|
+
package_dir = Path(__file__).resolve().parent / "_vendor" / "starsec_core"
|
|
27
|
+
init = package_dir / "__init__.py"
|
|
28
|
+
if not init.is_file():
|
|
29
|
+
raise ImportError("starsec_core is not installed and no vendored copy ships")
|
|
30
|
+
spec = importlib.util.spec_from_file_location(
|
|
31
|
+
"starsec_core", init, submodule_search_locations=[str(package_dir)]
|
|
32
|
+
)
|
|
33
|
+
if spec is None or spec.loader is None:
|
|
34
|
+
raise ImportError("vendored starsec_core is malformed")
|
|
35
|
+
module = importlib.util.module_from_spec(spec)
|
|
36
|
+
sys.modules["starsec_core"] = module
|
|
37
|
+
spec.loader.exec_module(module)
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
install()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""starsec-core — shared STARSEC engine runtime.
|
|
2
|
+
|
|
3
|
+
Public API re-exports:
|
|
4
|
+
- config, tracing
|
|
5
|
+
- world (World, Team, Platform, Terminal, Spawn, Camera, Model)
|
|
6
|
+
- terminal (Terminal, LocalPtyTransport, LocalDockerTransport)
|
|
7
|
+
- input (PlayerController)
|
|
8
|
+
- render (lighting, color, camera, terminal3d)
|
|
9
|
+
- net (codec, messages, bus)
|
|
10
|
+
- ui (Button, Panel, TextInput, Slider, Checkbox, Modal, modal_confirm)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from starsec_core.version import CORE_VERSION
|
|
14
|
+
|
|
15
|
+
from . import config, input, net, render, terminal, tracing, ui, world
|
|
16
|
+
|
|
17
|
+
__version__ = CORE_VERSION
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"config",
|
|
21
|
+
"input",
|
|
22
|
+
"net",
|
|
23
|
+
"render",
|
|
24
|
+
"terminal",
|
|
25
|
+
"tracing",
|
|
26
|
+
"ui",
|
|
27
|
+
"world",
|
|
28
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""D5i account auth — GitHub device flow + credentials store (Phase 11).
|
|
2
|
+
|
|
3
|
+
D5-Interactive accounts ride GitHub: client-side device-flow OAuth (stdlib
|
|
4
|
+
``urllib``; ``http`` injectable for tests), token + login stored in the
|
|
5
|
+
home's ``credentials.json`` (chmod 600; Windows: best-effort ACL later).
|
|
6
|
+
The updater (``net.updates``) reads the token for the private engine
|
|
7
|
+
channel; nothing else consumes credentials yet.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import stat
|
|
15
|
+
import time
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
DEVICE_URL = "https://github.com/login/device/code"
|
|
22
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
23
|
+
|
|
24
|
+
#: Set when the D5i OAuth app is registered; empty = the flow refuses so
|
|
25
|
+
#: callers fail loudly instead of hitting an unconfigured app entry.
|
|
26
|
+
CLIENT_ID = ""
|
|
27
|
+
|
|
28
|
+
CREDENTIALS_FALLBACK = "~/.starsec_credentials.json"
|
|
29
|
+
|
|
30
|
+
Http = Callable[[str, str, dict, dict], "tuple[int, bytes]"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _http_default(method: str, url: str, headers: dict, data: dict):
|
|
34
|
+
body = urllib.parse.urlencode(data).encode()
|
|
35
|
+
request = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
36
|
+
try:
|
|
37
|
+
with urllib.request.urlopen(request, timeout=30) as resp:
|
|
38
|
+
return resp.status, resp.read()
|
|
39
|
+
except urllib.error.HTTPError as exc: # GitHub answers errors in-band
|
|
40
|
+
return exc.code, exc.read()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def request_device_code(client_id: str, *, http: Http | None = None) -> dict:
|
|
44
|
+
"""Start the device flow; returns the device-code payload from GitHub."""
|
|
45
|
+
if not client_id:
|
|
46
|
+
raise ValueError("client_id not configured")
|
|
47
|
+
transport = http or _http_default
|
|
48
|
+
status, body = transport(
|
|
49
|
+
"POST", DEVICE_URL, {"Accept": "application/json"}, {"client_id": client_id}
|
|
50
|
+
)
|
|
51
|
+
if status != 200:
|
|
52
|
+
raise RuntimeError(f"device-code request failed: HTTP {status}")
|
|
53
|
+
return json.loads(body)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def poll_token(
|
|
57
|
+
device_code: dict,
|
|
58
|
+
client_id: str,
|
|
59
|
+
*,
|
|
60
|
+
http: Http | None = None,
|
|
61
|
+
tries: int = 8,
|
|
62
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Poll for the access token; ``None``-free contract: raise on timeout.
|
|
65
|
+
|
|
66
|
+
Bounded on purpose (Review Focus 3): ``tries`` rounds at the device
|
|
67
|
+
code's ``interval``; ``slow_down`` adds the 5-second penalty and keeps
|
|
68
|
+
polling. Exhaustion raises ``TimeoutError``.
|
|
69
|
+
"""
|
|
70
|
+
if not client_id:
|
|
71
|
+
raise ValueError("client_id not configured")
|
|
72
|
+
transport = http or _http_default
|
|
73
|
+
interval = float(device_code.get("interval", 5))
|
|
74
|
+
data = {
|
|
75
|
+
"client_id": client_id,
|
|
76
|
+
"device_code": device_code.get("device_code", ""),
|
|
77
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
78
|
+
}
|
|
79
|
+
for _attempt in range(tries):
|
|
80
|
+
status, body = transport("POST", TOKEN_URL, {"Accept": "application/json"}, dict(data))
|
|
81
|
+
payload = json.loads(body)
|
|
82
|
+
error = payload.get("error")
|
|
83
|
+
if status == 200 and payload.get("access_token"):
|
|
84
|
+
return payload
|
|
85
|
+
if error == "slow_down":
|
|
86
|
+
interval += 5.0
|
|
87
|
+
elif error not in (None, "authorization_pending"):
|
|
88
|
+
raise RuntimeError(f"device flow error: {error}")
|
|
89
|
+
sleep(interval)
|
|
90
|
+
raise TimeoutError(f"device flow timed out after {tries} rounds")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------- #
|
|
94
|
+
# Credentials store
|
|
95
|
+
# --------------------------------------------------------------------------- #
|
|
96
|
+
def credentials_path(env: dict[str, str] | None = None) -> Path:
|
|
97
|
+
"""``<home>/credentials.json`` when home is active, else the user file."""
|
|
98
|
+
from starsec_core.home import home_root
|
|
99
|
+
|
|
100
|
+
home = home_root(env=env)
|
|
101
|
+
if home is not None:
|
|
102
|
+
return home / "credentials.json"
|
|
103
|
+
return Path(CREDENTIALS_FALLBACK).expanduser()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def save_credentials(path: Path, creds: dict) -> None:
|
|
107
|
+
"""Write atomically-ish with owner-only permissions."""
|
|
108
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
path.write_text(json.dumps(creds, indent=2))
|
|
110
|
+
try:
|
|
111
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
112
|
+
except OSError: # Windows: no chmod; ACL pass later
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_credentials(path: Path) -> dict:
|
|
117
|
+
return json.loads(Path(path).read_text())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _self_check() -> None:
|
|
121
|
+
assert (
|
|
122
|
+
credentials_path(env={"STARSEC_HOME_MARKER": "/nonexistent"}).name
|
|
123
|
+
== ".starsec_credentials.json"
|
|
124
|
+
)
|
|
125
|
+
assert CLIENT_ID == "" # flow refuses until the OAuth app is registered
|
|
126
|
+
print("auth self-check ok")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
_self_check()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Rectangular OBB hitboxes (Phase 23C): same test for combat and editor.
|
|
2
|
+
|
|
3
|
+
Euler degrees use the same world convention as ``world.euler``: rotate around
|
|
4
|
+
X, then Y, then Z. Segment tests operate in box-local space via inverse
|
|
5
|
+
rotation, so a rotated fighter's hitbox matches its rendered box.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from starsec_core.world import Vec3, v3_add, v3_dot, v3_rotate_axis, v3_sub
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Hitbox:
|
|
18
|
+
half_extents: Vec3 = (1.0, 0.5, 1.0)
|
|
19
|
+
offset: Vec3 = (0.0, 0.0, 0.0)
|
|
20
|
+
rotation: Vec3 = (0.0, 0.0, 0.0)
|
|
21
|
+
|
|
22
|
+
def valid(self) -> bool:
|
|
23
|
+
return all(value > 0.0 for value in self.half_extents)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _to_local(point: Vec3, center: Vec3, rotation: Vec3) -> Vec3:
|
|
27
|
+
relative = v3_sub(point, center)
|
|
28
|
+
# inverse Euler order: undo Z, then Y, then X
|
|
29
|
+
out = v3_rotate_axis(relative, (0.0, 0.0, 1.0), -math.radians(rotation[2]))
|
|
30
|
+
out = v3_rotate_axis(out, (0.0, 1.0, 0.0), -math.radians(rotation[1]))
|
|
31
|
+
return v3_rotate_axis(out, (1.0, 0.0, 0.0), -math.radians(rotation[0]))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def hitbox_center(hitbox: Hitbox, position: Vec3, rotation: Vec3) -> Vec3:
|
|
35
|
+
# offset rotates with the fighter; reuse forward rotation via positive axes
|
|
36
|
+
return v3_add(position, _rotate_forward(hitbox.offset, rotation))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rotate_forward(vector: Vec3, rotation: Vec3) -> Vec3:
|
|
40
|
+
out = v3_rotate_axis(vector, (1.0, 0.0, 0.0), math.radians(rotation[0]))
|
|
41
|
+
out = v3_rotate_axis(out, (0.0, 1.0, 0.0), math.radians(rotation[1]))
|
|
42
|
+
return v3_rotate_axis(out, (0.0, 0.0, 1.0), math.radians(rotation[2]))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def point_in_hitbox(point: Vec3, hitbox: Hitbox, position: Vec3, rotation: Vec3) -> bool:
|
|
46
|
+
center = hitbox_center(hitbox, position, rotation)
|
|
47
|
+
local = _to_local(point, center, rotation)
|
|
48
|
+
return all(abs(local[i]) <= hitbox.half_extents[i] for i in range(3))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def segment_hitbox(
|
|
52
|
+
start: Vec3,
|
|
53
|
+
end: Vec3,
|
|
54
|
+
hitbox: Hitbox,
|
|
55
|
+
position: Vec3,
|
|
56
|
+
rotation: Vec3,
|
|
57
|
+
) -> float | None:
|
|
58
|
+
"""Ray/segment vs OBB; returns the entry fraction 0..1 or None."""
|
|
59
|
+
if not hitbox.valid():
|
|
60
|
+
return None
|
|
61
|
+
center = hitbox_center(hitbox, position, rotation)
|
|
62
|
+
local_start = _to_local(start, center, rotation)
|
|
63
|
+
local_end = _to_local(end, center, rotation)
|
|
64
|
+
direction = v3_sub(local_end, local_start)
|
|
65
|
+
t_min, t_max = 0.0, 1.0
|
|
66
|
+
for axis in range(3):
|
|
67
|
+
if abs(direction[axis]) < 1e-9:
|
|
68
|
+
if abs(local_start[axis]) > hitbox.half_extents[axis]:
|
|
69
|
+
return None
|
|
70
|
+
continue
|
|
71
|
+
first = (-hitbox.half_extents[axis] - local_start[axis]) / direction[axis]
|
|
72
|
+
second = (hitbox.half_extents[axis] - local_start[axis]) / direction[axis]
|
|
73
|
+
low, high = sorted((first, second))
|
|
74
|
+
t_min = max(t_min, low)
|
|
75
|
+
t_max = min(t_max, high)
|
|
76
|
+
if t_min > t_max:
|
|
77
|
+
return None
|
|
78
|
+
return t_min if t_max >= 0.0 else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sphere_hitbox(
|
|
82
|
+
center: Vec3, radius: float, hitbox: Hitbox, position: Vec3, rotation: Vec3
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""Sphere vs OBB via the closest local point."""
|
|
85
|
+
box_center = hitbox_center(hitbox, position, rotation)
|
|
86
|
+
local = _to_local(center, box_center, rotation)
|
|
87
|
+
closest = tuple(
|
|
88
|
+
max(-hitbox.half_extents[i], min(hitbox.half_extents[i], local[i])) for i in range(3)
|
|
89
|
+
)
|
|
90
|
+
delta = v3_sub(local, closest)
|
|
91
|
+
return v3_dot(delta, delta) <= radius * radius
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _self_check() -> None:
|
|
95
|
+
box = Hitbox((1.0, 1.0, 1.0))
|
|
96
|
+
assert point_in_hitbox((0.0, 0.0, 0.0), box, (0.0, 0.0, 0.0), (0, 0, 0))
|
|
97
|
+
assert segment_hitbox((-5, 0, 0), (5, 0, 0), box, (0, 0, 0), (0, 0, 0)) is not None
|
|
98
|
+
print("hitbox self-check ok")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
_self_check()
|