Major pipeline changes to map building, introducing 3player map, and adding Shared build mode to level authoring options

This commit is contained in:
Matt F 2026-08-04 22:50:33 -07:00
parent 749c9203de
commit 2b1aee3e03
37 changed files with 5418 additions and 30 deletions

View file

@ -802,17 +802,33 @@ namespace TD.Levels.Editor
}
// P5-8: zones with neither outgoing leak nor goal adjacency.
var zonesWithLeaks = new HashSet<PlayerSlot>();
foreach (var l in ctx.LeakExitVolumes) zonesWithLeaks.Add(l.sourceZone);
foreach (var owner in ctx.DeclaredOwners)
//
// OwnerOnly maps only. The rule asserts the lane-chain topology those maps are built
// around: each player's zone either hands off to the next or is the final defender.
//
// On a Shared map that assertion is wrong, not merely unnecessary. Zones there are
// typically directly adjacent with no gate between them, so most have no outgoing leak
// and are not goal-adjacent — and that is the intended shape, because the whole map is
// one contiguous maze. The property P5-8 actually protects ("enemies from this zone have
// somewhere to go") is enforced more strictly by P5-4 above, which runs a real BFS from
// every spawner to the exit set — and with no leak volumes present, that exit set is the
// goal tiles, which is exactly the right requirement. Zones with no spawners can't
// strand anything: WaveManager skips them.
if (ctx.Authoring.buildAccess == BuildAccess.OwnerOnly)
{
bool hasLeak = zonesWithLeaks.Contains(owner);
bool isGoalAdj = goalAdjacentZones.Contains(owner);
if (!hasLeak && !isGoalAdj)
var zonesWithLeaks = new HashSet<PlayerSlot>();
foreach (var l in ctx.LeakExitVolumes) zonesWithLeaks.Add(l.sourceZone);
foreach (var owner in ctx.DeclaredOwners)
{
report.Error("P5-8", $"Player zone {owner} has no outgoing leak exit AND is not goal-adjacent. " +
"Every zone must either lead somewhere or be a final defender.");
bool hasLeak = zonesWithLeaks.Contains(owner);
bool isGoalAdj = goalAdjacentZones.Contains(owner);
if (!hasLeak && !isGoalAdj)
{
report.Error("P5-8", $"Player zone {owner} has no outgoing leak exit AND is not goal-adjacent. " +
"Every zone must either lead somewhere or be a final defender. " +
"(This rule is skipped on Shared-build maps.)");
}
}
}
@ -919,6 +935,7 @@ namespace TD.Levels.Editor
data.PlayerCount = ctx.Authoring.playerCount;
data.MapDescription = ctx.Authoring.mapDescription;
data.Author = ctx.Authoring.author;
data.BuildAccess = ctx.Authoring.buildAccess;
data.GridOriginTile = ctx.GridOriginTile;
data.GridSize = ctx.GridSize;
@ -1152,7 +1169,10 @@ namespace TD.Levels.Editor
sb.Append("playerCount=").Append(ctx.Authoring.playerCount.ToString(inv)).Append('|');
sb.Append("expectedGoalCount=").Append(ctx.Authoring.expectedGoalCount.ToString(inv)).Append('|');
sb.Append("mapDescription=").Append(ctx.Authoring.mapDescription ?? "").Append('|');
sb.Append("author=").Append(ctx.Authoring.author ?? "").Append('\n');
sb.Append("author=").Append(ctx.Authoring.author ?? "").Append('|');
// By NAME, matching the per-volume enum convention below — a future reordering of the
// enum must not silently produce a matching hash for different rules.
sb.Append("buildAccess=").Append(ctx.Authoring.buildAccess.ToString()).Append('\n');
// Volume blocks, ordered by canonical path.
var rootTransform = ctx.Authoring.transform;
@ -1354,6 +1374,7 @@ namespace TD.Levels.Editor
target.PlayerCount = src.PlayerCount;
target.MapDescription = src.MapDescription;
target.Author = src.Author;
target.BuildAccess = src.BuildAccess;
target.ScenePath = src.ScenePath;
target.AuthoringHash = src.AuthoringHash;
target.LastBakeTimestamp = src.LastBakeTimestamp;