Map lighting by the time of day, lit tiles and events; A2 autotiles

This commit is contained in:
cthangover committed 2026-10-24 12:56:08 +02:00
1 parent 0027c8ded0
commit 88ab26dcec
19 files changed
+573 -10

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
namespace Cthangover.Mods.Rpg.Tests
{
using Cthangover.Core.Mods.Testing;
using Godot;
/// <summary>Autotile cells are assembled from quarters of the A2 block picked by their neighbours.</summary>
public class RpgAutotilesTests
{
private const int Water = 40;
private static bool Is(Vector2I[] quarters, params int[] xy)
{
for (var i = 0; i < 4; i++)
{
if (quarters[i] != new Vector2I(xy[i * 2], xy[i * 2 + 1]))
return false;
}
return true;
}
[ModTest("autotiles/quarters_by_neighbours")]
static void QuartersByNeighbours(ModTestContext ctx)
{
ModAssert.True(Is(RpgAutotiles.Quarters(false, false, false, false, false, false, false, false), 0, 2, 3, 2, 0, 5, 3, 5),
"alone: the four outer corners");
ModAssert.True(Is(RpgAutotiles.Quarters(true, true, true, true, true, true, true, true), 2, 4, 1, 4, 2, 3, 1, 3),
"surrounded: the fill of the patch");
ModAssert.True(Is(RpgAutotiles.Quarters(true, true, true, true, false, false, false, false), 2, 0, 3, 0, 2, 1, 3, 1),
"sides alike, corners not: the inner corners");
ModAssert.True(Is(RpgAutotiles.Quarters(false, true, true, true, false, false, true, true), 2, 2, 1, 2, 2, 3, 1, 3),
"the top row of a pond: top edge above, fill below");
ModAssert.True(Is(RpgAutotiles.Quarters(true, true, false, true, false, true, false, true), 0, 4, 1, 4, 0, 3, 1, 3),
"the left column: left edge");
var corner = RpgAutotiles.Quarters(true, true, true, true, false, true, true, true);
ModAssert.True(corner[0] == new Vector2I(2, 0) && corner[3] == new Vector2I(1, 3), "only the missing corner turns inner");
}
[ModTest("autotiles/cells_read_their_layer_and_the_map_edge_counts_alike")]
static void QuartersAt(ModTestContext ctx)
{
var grid = new RpgMapGrid(3, 3);
grid.SetTile(RpgLayer.Back, 0, 0, Water);
grid.SetTile(RpgLayer.Back, 1, 0, Water);
grid.SetTile(RpgLayer.Main, 0, 1, Water);
var corner = RpgAutotiles.QuartersAt(grid, RpgLayer.Back, 0, 0, Water);
ModAssert.True(corner[0] == new Vector2I(2, 4), "off the map counts as water: the top-left quarter is fill");
ModAssert.True(corner[2] == new Vector2I(2, 5), "below lies grass on this layer: bottom edge (the main layer is not read)");
ModAssert.True(corner[1] == new Vector2I(1, 4), "to the right is water");
var end = RpgAutotiles.QuartersAt(grid, RpgLayer.Back, 1, 0, Water);
ModAssert.True(end[1] == new Vector2I(3, 4), "the right end: right edge");
ModAssert.True(end[3] == new Vector2I(3, 5), "and its outer bottom-right corner");
}
[ModTest("autotiles/source_and_preview_rects")]
static void Rects(ModTestContext ctx)
{
ModAssert.True(RpgAutotiles.SourceRect(new Vector2I(3, 5), 32) == new Rect2(48, 80, 16, 16), "quarters are half tiles");
ModAssert.True(RpgAutotiles.SourceRect(Vector2I.Zero, 48) == new Rect2(0, 0, 24, 24), "of any tile size");
ModAssert.True(RpgAutotiles.PreviewRect(32) == new Rect2(0, 0, 32, 32), "the preview is the top-left tile");
}
}
}
+23 -1
View File
@@ -7,7 +7,7 @@ namespace Cthangover.Mods.Rpg.Tests
using Cthangover.Core.Utils;
using Godot;
/// <summary>Event pages pick the last page whose condition holds; routes resolve their steps.</summary>
/// <summary>Event pages pick the last page whose condition holds; routes resolve their steps; a page light replaces the event's.</summary>
public class RpgEventPagesTests
{
private static RpgMapEvent Chest() => JsonSerializer.Deserialize<RpgMapEvent>(@"{
@@ -87,5 +87,27 @@ namespace Cthangover.Mods.Rpg.Tests
ModAssert.True(mapEvent.Route.Repeat, "repeats by default");
ModAssert.Null(mapEvent.Pages, "no pages");
}
[ModTest("lights/page_light_over_event_light_round_trip")]
static void PageLightOverEventLight(ModTestContext ctx)
{
var lamp = JsonSerializer.Deserialize<RpgMapEvent>(@"{
""Id"": ""lamp"", ""Tile"": 12, ""Light"": { ""Radius"": 2.5, ""Color"": ""#ff8040"" },
""Pages"": [ { ""Condition"": ""lamp_out == 1"", ""Tile"": 13, ""Light"": { ""Radius"": 1 } }, { ""Condition"": ""door == 1"" } ]
}", JsonOptionsProvider.ReadOptions);
ModAssert.CloseTo(2.5, lamp.Light.Radius, what: "the event's light");
ModAssert.Equal("#ff8040", lamp.Light.Color, "the event's colour");
var dim = RpgEventPages.Effective(lamp, 1);
ModAssert.CloseTo(1, dim.Light.Radius, what: "the page's light replaces it");
ModAssert.Equal("#ffd8a0", dim.Light.Color, "a page light without a colour is warm by default");
ModAssert.CloseTo(2.5, RpgEventPages.Effective(lamp, 2).Light.Radius, what: "a page without a light keeps the event's");
var again = JsonSerializer.Deserialize<RpgMapEvent>(JsonSerializer.Serialize(lamp), JsonOptionsProvider.ReadOptions);
ModAssert.CloseTo(2.5, again.Light.Radius, what: "radius survives the round trip");
ModAssert.Equal("#ff8040", again.Light.Color, "colour survives the round trip");
ModAssert.CloseTo(1, again.Pages[0].Light.Radius, what: "the page light survives the round trip");
ModAssert.Null(JsonSerializer.Deserialize<RpgMapEvent>(@"{ ""Id"": ""dark"" }", JsonOptionsProvider.ReadOptions).Light, "no light by default");
}
}
}
+59
View File
@@ -0,0 +1,59 @@
namespace Cthangover.Mods.Rpg.Tests
{
using System.Collections.Generic;
using System.Text.Json;
using Cthangover.Core.Mods.Testing;
using Cthangover.Core.Utils;
using Godot;
/// <summary>The map's light: ambient weights from the clock, lit tiles, the fixed ambient of a map.</summary>
public class RpgLightingTests
{
[ModTest("lights/weights_follow_clock_dark_mode_and_switch")]
static void WeightsFollowClock(ModTestContext ctx)
{
ModAssert.CloseTo(1, RpgLighting.Weights(13f, false, true).Y, what: "noon is day");
ModAssert.CloseTo(1, RpgLighting.Weights(0.5f, false, true).W, what: "midnight is night");
ModAssert.CloseTo(1, RpgLighting.Weights(13f, true, true).W, what: "dark mode is night at noon");
ModAssert.CloseTo(1, RpgLighting.Weights(0.5f, false, false).Y, what: "lighting switched off keeps day at midnight");
var dawn = RpgLighting.Weights(5f, false, true);
ModAssert.CloseTo(1, dawn.X + dawn.W, what: "dawn blends night into morning");
ModAssert.True(dawn.X > 0 && dawn.W > 0, "both phases at dawn");
}
[ModTest("lights/tile_lights_on_any_layer_once_per_cell")]
static void TileLights(ModTestContext ctx)
{
var tileset = new RpgTileset
{
ID = "lit",
Tiles = new List<RpgTileInfo>
{
new() { Index = 3, Light = new RpgLight { Radius = 4 } },
new() { Index = 7, Light = new RpgLight { Radius = 1.5f, Color = "#ffd27a" } },
new() { Index = 9, Solid = true },
},
};
var grid = new RpgMapGrid(4, 3);
grid.SetTile(RpgLayer.Front, 1, 0, 3);
grid.SetTile(RpgLayer.Main, 2, 2, 7);
grid.SetTile(RpgLayer.Back, 2, 2, 3);
grid.SetTile(RpgLayer.Main, 0, 1, 9);
var lights = RpgLighting.TileLights(grid, tileset);
ModAssert.Equal(2, lights.Count, "one light per lit cell, unlit tiles give none");
ModAssert.True(lights.Exists(l => l.Cell == new Vector2I(1, 0) && Mathf.IsEqualApprox(l.Light.Radius, 4f)), "a front-layer lamp");
ModAssert.True(lights.Exists(l => l.Cell == new Vector2I(2, 2)), "a cell lit on two layers counts once");
}
[ModTest("lights/map_ambient_json")]
static void MapAmbientJson(ModTestContext ctx)
{
var room = JsonSerializer.Deserialize<RpgMap>(@"{ ""ID"": ""room"", ""Ambient"": ""#e8dcc8"" }", JsonOptionsProvider.ReadOptions);
ModAssert.Equal("#e8dcc8", room.Ambient, "a lit room");
var again = JsonSerializer.Deserialize<RpgMap>(JsonSerializer.Serialize(room), JsonOptionsProvider.ReadOptions);
ModAssert.Equal("#e8dcc8", again.Ambient, "ambient survives the round trip");
ModAssert.Null(JsonSerializer.Deserialize<RpgMap>(@"{ ""ID"": ""field"" }", JsonOptionsProvider.ReadOptions).Ambient, "outdoors by default");
}
}
}
+20
View File
@@ -53,6 +53,26 @@ namespace Cthangover.Mods.Rpg.Tests
ModAssert.False(tileset.IsSolid(-1), "empty cell");
}
[ModTest("rules/tileset_light_and_autotile_lookup_round_trip")]
static void TilesetLightAndAutotileLookup(ModTestContext ctx)
{
var tileset = System.Text.Json.JsonSerializer.Deserialize<RpgTileset>(@"{
""Id"": ""lit"", ""Tiles"": [
{ ""Index"": 29, ""Tag"": ""lamp_top"", ""Light"": { ""Radius"": 4, ""Color"": ""#ffcf80"" } },
{ ""Index"": 40, ""Solid"": true, ""Tag"": ""water"", ""Autotile"": ""village/water_auto"" }
] }", Cthangover.Core.Utils.JsonOptionsProvider.ReadOptions);
tileset = System.Text.Json.JsonSerializer.Deserialize<RpgTileset>(System.Text.Json.JsonSerializer.Serialize(tileset), Cthangover.Core.Utils.JsonOptionsProvider.ReadOptions);
ModAssert.CloseTo(4, tileset.LightOf(29).Radius, what: "the lamp's light survives the round trip");
ModAssert.Equal("#ffcf80", tileset.LightOf(29).Color, "with its colour");
ModAssert.Null(tileset.LightOf(40), "the water gives no light");
ModAssert.Null(tileset.LightOf(-1), "an empty cell gives no light");
ModAssert.Equal("village/water_auto", tileset.AutotileOf(40)?.Autotile, "the autotile entry and its block");
ModAssert.Null(tileset.AutotileOf(29), "a plain tile is no autotile");
ModAssert.Null(tileset.AutotileOf(-1), "nor an empty cell");
ModAssert.True(tileset.IsSolid(40), "an autotile is solid like any tile");
}
[ModTest("rules/encounter_off_without_table")]
static void EncounterOffWithoutTable(ModTestContext ctx)
{