This commit is contained in:
2026-07-11 00:14:41 +02:00
parent 87dd8e1190
commit c9e463bb9a
15 changed files with 183 additions and 51 deletions
+4 -6
View File
@@ -8,12 +8,10 @@ static BASE: OnceLock<PathBuf> = OnceLock::new();
fn base() -> &'static PathBuf {
BASE.get_or_init(|| {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
if dir.join("assets").exists() {
return dir.to_path_buf();
}
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
&& dir.join("assets").exists() {
return dir.to_path_buf();
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
})
+3 -4
View File
@@ -71,10 +71,9 @@ impl CollisionWorld {
pub fn trace(&self, start: [f32; 3], end: [f32; 3], half: [f32; 3]) -> Option<Hit> {
let mut nearest: Option<Hit> = None;
for b in &self.brushes {
if let Some(h) = trace_brush(b, start, end, half) {
if nearest.as_ref().map_or(true, |n| h.frac < n.frac) {
nearest = Some(h);
}
if let Some(h) = trace_brush(b, start, end, half)
&& nearest.as_ref().is_none_or(|n| h.frac < n.frac) {
nearest = Some(h);
}
}
nearest
+7 -4
View File
@@ -29,14 +29,17 @@ pub struct Story {
}
impl Story {
pub fn load(path: &str, kv: &Store) -> Self {
/// Lädt und parst ein Ink-Skript. Fehlende oder kaputte Datei liefert
/// `Err(msg)` statt zu panicken — ein Tippfehler in einem `start_ink`-Tag
/// darf das laufende Spiel nicht crashen; der Aufrufer meldet den Fehler.
pub fn load(path: &str, kv: &Store) -> Result<Self, String> {
let src = read_to_string(path)
.unwrap_or_else(|e| panic!("ink load {}: {}", path, e));
.map_err(|e| format!("ink load {path}: {e}"))?;
let inner = BladeStory::new(&src)
.unwrap_or_else(|e| panic!("ink parse {}: {:?}", path, e));
.map_err(|e| format!("ink parse {path}: {e:?}"))?;
let mut s = Self { inner, tags: Vec::new() };
s.sync_from_kv(kv);
s
Ok(s)
}
pub fn take_tags(&mut self) -> Vec<String> { std::mem::take(&mut self.tags) }
+4 -4
View File
@@ -102,10 +102,9 @@ pub fn parse(src: &str) -> Map {
if let Some(face) = parse_face(line) {
brush.faces.push(face);
}
} else if let Some(entity) = cur_entity.as_mut() {
if let Some((k, v)) = parse_property(line) {
entity.props.insert(k, v);
}
} else if let Some(entity) = cur_entity.as_mut()
&& let Some((k, v)) = parse_property(line) {
entity.props.insert(k, v);
}
}
}
@@ -124,6 +123,7 @@ fn parse_property(line: &str) -> Option<(String, String)> {
}
/// `( x y z ) ( x y z ) ( x y z ) TEX offX offY rot sx sy`
#[allow(clippy::needless_range_loop)] // Punkt-/Komponenten-Index steuert zugleich den Token-Cursor `i`
fn parse_face(line: &str) -> Option<Face> {
let t: Vec<&str> = line.split_whitespace().collect();
let mut i = 0;
+1
View File
@@ -161,6 +161,7 @@ impl Player {
/// Reine Slide-Bewegung des Box-Mittelpunkts entlang `vel·dt` (keine Stufen):
/// bis zu [`MAX_SLIDES`] Iterationen, an Treffern entlanggleitend. Gibt
/// Endpunkt, Restgeschwindigkeit und Bodenkontakt zurück.
#[allow(clippy::needless_range_loop)] // Komponenten-Index läuft parallel über center/disp bzw. vel/normal
fn slide(world: &CollisionWorld, mut center: [f32; 3], mut vel: [f32; 3], dt: f32) -> ([f32; 3], [f32; 3], bool) {
let half = HALF_EXTENTS;
let mut grounded = false;
+39 -7
View File
@@ -27,7 +27,12 @@
//! Dispatch-Reihenfolge: erst Signal-Lookup; steht der Name in der Table,
//! laufen alle gemappten Actions. Sonst wird der String direkt als
//! Builtin-Aufruf probiert — so wirken Ink-Tags wie `# set has_key true`
//! ohne Umweg über signals.toml. Unbekannte Verben werden still ignoriert.
//! ohne Umweg über signals.toml.
//!
//! Unbekannte Verben: aus einer signals.toml-*Action* sind sie immer ein
//! Tippfehler und werden nach stderr gemeldet. Im Ink-Tag-Fall-Through bleiben
//! sie hingegen still — Tags dürfen bewusst reine Metadaten sein, die (noch)
//! kein Verb konsumiert (z.B. ein späteres `# speaker …`).
use std::collections::HashMap;
use std::fs::read_to_string;
@@ -51,17 +56,27 @@ pub fn signal_key(name: &str) -> &str {
}
pub fn dispatch(signal: &str, ctx: &mut ActionCtx) {
// Signal-Name in der Table? Dann alle gemappten Actions ausführen.
// Signal-Name in der Table? Dann alle gemappten Actions ausführen. Ein
// unbekanntes Verb in einer Action ist hier immer ein Tippfehler → melden.
if let Some(actions) = ctx.signals.get(signal) {
let actions = actions.clone(); // entkoppelt vom ctx.signals-Borrow
for a in &actions { execute(a, ctx); }
for a in &actions {
if !execute(a, ctx) {
eprintln!("[signal] {signal:?}: unbekanntes Verb in Action {a:?} \
— Tippfehler in signals.toml?");
}
}
return;
}
// Sonst direkt als Builtin probieren (Ink-Tag-Pfad).
execute(signal, ctx);
// Sonst direkt als Builtin probieren (Ink-Tag-Pfad). Unbekanntes bleibt
// hier still (Tags dürfen bloße Metadaten sein) — Rückgabe verworfen.
let _ = execute(signal, ctx);
}
fn execute(cmd: &str, ctx: &mut ActionCtx) {
/// Führt einen einzelnen Befehl (`verb args`) aus. Rückgabe: `true`, wenn das
/// Verb bekannt war und lief; `false` bei unbekanntem Verb (der Aufrufer
/// entscheidet, ob das eine Meldung wert ist — siehe `dispatch`).
fn execute(cmd: &str, ctx: &mut ActionCtx) -> bool {
// $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten,
// damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne
// instance_name (z.B. `init`-Signal) bleibt `$self` stehen — die Action
@@ -87,8 +102,9 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) {
"mode" => if let Some(t) = ModeTarget::parse(args) {
ctx.actions.push(Action::SetMode(t));
},
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
_ => return false, // Unbekanntes Verb — der Aufrufer meldet ggf.
}
true
}
/// Parst das signals.toml-Subset: `[signal]`-Header gefolgt von einem
@@ -195,6 +211,22 @@ mod tests {
assert!(game.actions.is_empty());
}
#[test]
fn typo_verb_in_table_is_tolerated_and_reported() {
// Ein Tippfehler-Verb in einer Table-Action wird gemeldet (stderr) und
// bleibt ohne Wirkung, während die gültige Nachbar-Action weiter läuft.
let mut signals = Signals::new();
signals.insert("thing".into(), vec![
"st has_key true".into(), // Tippfehler für `set` → No-Op + Meldung
"set ok true".into(), // gültig → wirkt
]);
let mut game = Game::new(signals);
let mut ctx = game.action_ctx(None);
dispatch("thing", &mut ctx);
assert!(!game.kv.contains_key("has_key"), "Tippfehler darf nichts setzen");
assert!(game.kv["ok"].coerce_to_bool().unwrap(), "gültige Action wirkt weiter");
}
#[test]
fn unknown_signal_falls_through_to_builtin() {
let mut game = Game::new(Signals::new());
+6 -1
View File
@@ -16,7 +16,12 @@ use crate::engine::ink::{Story, StoryState};
/// dafür `advance(None, …)`.
pub fn start(script: &str, ctx: &mut ActionCtx) {
let path = assets::path(&format!("assets/interactions/{script}"));
*ctx.story = Some(Story::load(&path, ctx.kv));
match Story::load(&path, ctx.kv) {
Ok(story) => *ctx.story = Some(story),
// Kaputtes/fehlendes Skript darf das Spiel nicht crashen: den Slot
// leer lassen (kein Dialog öffnet) und den Fehler nach stderr melden.
Err(msg) => eprintln!("[start_ink] {msg}"),
}
}
/// Treibt die laufende Story einen Schritt weiter. `sel` ist die