object tags

This commit is contained in:
2026-06-07 01:26:18 -05:00
parent 9c2212520c
commit a8d2cb8303
8 changed files with 240 additions and 6 deletions
@@ -51,6 +51,83 @@ bg = "#000000"
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
}
#[test]
fn object_tags_round_trip_through_toml() {
// Tags declared in the map file must survive a save→load cycle, sorted.
let toml = r##"
[map]
name = "Test"
width = 3
height = 3
player_start = [0, 0]
[palette]
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
[grid]
content = """
...
...
...
"""
[[objects]]
x = 1
y = 1
tile = 64
fg = "#ffffff"
bg = "#000000"
tags = ["enemy", "boss"]
"##;
let mf: MapFile = toml::from_str(toml).unwrap();
let board = Board::try_from(mf).unwrap();
let obj = &board.objects[&1];
assert!(obj.tags.contains("enemy"));
assert!(obj.tags.contains("boss"));
assert_eq!(obj.tags.len(), 2);
// Save and reload; tags must survive, and be sorted alphabetically.
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
// Both tags must appear; "boss" must come before "enemy" (sorted).
let boss_pos = toml_out.find("\"boss\"").expect("boss in TOML");
let enemy_pos = toml_out.find("\"enemy\"").expect("enemy in TOML");
assert!(boss_pos < enemy_pos, "tags must be sorted: boss before enemy");
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
let board2 = Board::try_from(mf2).unwrap();
let obj2 = &board2.objects[&1];
assert_eq!(obj2.tags, obj.tags);
}
#[test]
fn object_empty_tags_omitted_from_toml() {
// An object with no tags must not emit a `tags` key in TOML.
let toml = r##"
[map]
name = "Test"
width = 1
height = 1
player_start = [0, 0]
[palette]
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
[grid]
content = """
.
"""
[[objects]]
x = 0
y = 0
tile = 64
fg = "#ffffff"
bg = "#000000"
"##;
let board = load_board(toml);
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
assert!(!toml_out.contains("tags"), "empty tags must not appear in TOML output");
}
#[test]
fn floor_spec_round_trips_through_toml() {
// A 2×2 board with a grid floor (a generator and a fixed glyph) must survive
+1
View File
@@ -15,6 +15,7 @@ use crate::game::GameState;
/// script, plus the given `(name, source)` script table entries.
fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> Board {
let mut object = ObjectDef::new(0, 0);
object.id = 1;
object.script_name = object_script.map(str::to_string);
Board {
name: "test".into(),
+74
View File
@@ -117,6 +117,80 @@ fn start_map_greeter_runs_init() {
);
}
#[test]
fn set_tag_adds_and_removes_via_my_id() {
// A script calls set_tag(MY_ID, "active", true) in init; the tag must be
// present on the object afterward.
let board = board_with_object(
Some("t"),
&[("t", r#"fn init() { set_tag(MY_ID, "active", true); }"#)],
);
let mut game = GameState::new(board);
game.run_init();
assert!(game.board().objects[&1].tags.contains("active"));
// A script removes a pre-existing tag.
let board2 = board_with_object(
Some("t2"),
&[("t2", r#"fn init() { set_tag(MY_ID, "active", false); }"#)],
);
// Seed the tag before construction.
let mut game2 = GameState::new({
let mut b = board2; // need mut to insert tag
b.objects.get_mut(&1).unwrap().tags.insert("active".to_string());
b
});
game2.run_init();
assert!(!game2.board().objects[&1].tags.contains("active"));
}
#[test]
fn has_tag_reads_own_tags() {
// set_tag queues an action; has_tag reads the board state. So set_tag in
// init() takes effect after init returns; a subsequent tick sees it.
let board = board_with_object(
Some("t"),
&[(
"t",
r#"fn init() { set_tag(MY_ID, "active", true); }
fn tick(dt) { log(has_tag("active").to_string()); }"#,
)],
);
let mut game = GameState::new(board);
game.run_init();
game.tick(Duration::from_millis(16));
assert!(log_texts(&game).iter().any(|t| t == "true"));
}
#[test]
fn objects_with_tag_returns_matching_ids() {
// A 5×1 board with two objects; one has tag "enemy". The script on the first
// object queries objects_with_tag("enemy") and logs the ids it finds.
let obj1 = scripted_object(0, 0, "q");
let mut obj2 = scripted_object(1, 0, "none");
// obj2 (id=2) has the "enemy" tag; obj1 (id=1) does not.
obj2.tags.insert("enemy".to_string());
let board = open_board(
5,
1,
(4, 0),
vec![obj1, obj2],
&[
("q", r#"fn init() {
let ids = objects_with_tag("enemy");
log(ids.len().to_string());
log(ids[0].to_string());
}"#),
("none", ""),
],
);
let mut game = GameState::new(board);
game.run_init();
let texts = log_texts(&game);
assert_eq!(texts[0], "1"); // exactly one match
assert_eq!(texts[1], "2"); // obj2 is id 2
}
// Ensure try_move from the player side also triggers scripted bump
#[test]
fn player_bump_fires_with_negative_one() {