transporters

This commit is contained in:
2026-07-08 10:06:25 -05:00
parent e43dae54a8
commit f407b5d9a6
14 changed files with 323 additions and 34 deletions
+1
View File
@@ -6,6 +6,7 @@ mod portal_placement;
mod pushers;
mod round_trip;
mod spinners;
mod transporters;
use crate::board::Board;
use crate::map_file::MapFile;
@@ -0,0 +1,115 @@
//! Transporters are scripted objects (the `transporter_*` archetypes expand into
//! objects carrying the embedded `transporter.rhai` plus a
//! `BUILTIN_transporter_<dir>` tag). Bumping one from its facing side teleports
//! the bumper past it, or out of a paired opposite-facing transporter.
use super::{layer, load_board, map};
use crate::game::GameState;
use crate::map_file::MapFile;
use crate::utils::Direction;
use std::time::Duration;
#[test]
fn transporter_loads_as_a_tagged_scripted_solid_object() {
let board = load_board(&map(
3,
1,
&[layer(
"@T ",
&[("@", "kind = \"player\""), ("T", "kind = \"transporter_east\"")],
)],
));
let (_, obj) = board
.objects
.iter()
.find(|(_, o)| o.tags.contains("BUILTIN_transporter_east"))
.expect("transporter object");
assert_eq!((obj.x, obj.y), (1, 0));
assert!(obj.builtin_script.is_some(), "carries the embedded script");
assert!(obj.solid, "transporter is solid");
assert!(!obj.opaque, "transporter is see-through");
assert!(!obj.pushable, "transporter cannot be pushed");
}
#[test]
fn bumping_a_transporter_drops_you_on_its_far_side() {
// Player at (0,0), an east transporter at (1,0), and a free cell at (2,0).
let board = load_board(&map(
3,
1,
&[layer(
"@T ",
&[("@", "kind = \"player\""), ("T", "kind = \"transporter_east\"")],
)],
));
let mut game = GameState::new(board);
game.run_init();
// Walk east into the transporter: it's solid so the player doesn't step onto
// it, but the bump queues a teleport that the next tick applies.
game.try_move(Direction::East);
game.tick(Duration::from_secs_f64(0.1));
let b = game.board();
assert_eq!((b.player.x, b.player.y), (2, 0), "transported past the transporter");
}
#[test]
fn a_blocked_far_side_transports_out_of_the_paired_transporter() {
// Player, east transporter, a wall blocking its far side, the free entrance
// cell, then the paired west transporter. The player should pop out at the
// west transporter's entrance (the cell just before it), across the wall.
let board = load_board(&map(
6,
1,
&[layer(
"@T# W ",
&[
("@", "kind = \"player\""),
("T", "kind = \"transporter_east\""),
("#", "kind = \"wall\", tile = 35, fg = \"#808080\", bg = \"#606060\""),
("W", "kind = \"transporter_west\""),
],
)],
));
let mut game = GameState::new(board);
game.run_init();
game.try_move(Direction::East);
game.tick(Duration::from_secs_f64(0.1));
let b = game.board();
assert_eq!(
(b.player.x, b.player.y),
(3, 0),
"transported to the paired transporter's entrance",
);
}
#[test]
fn transporter_round_trips_to_its_keyword() {
let board = load_board(&map(
3,
1,
&[layer(
"@T ",
&[("@", "kind = \"player\""), ("T", "kind = \"transporter_east\"")],
)],
));
// Save collapses the expanded object back into the `transporter_east` keyword.
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
assert!(
toml_out.contains("transporter_east"),
"transporter should save as its archetype keyword, got:\n{toml_out}"
);
// Reload: it is a working transporter object again.
let board2 = load_board(&toml_out);
assert!(
board2
.objects
.values()
.any(|o| o.tags.contains("BUILTIN_transporter_east") && o.builtin_script.is_some()),
"reloads as a tagged transporter object",
);
}