Scenarios Cut 1: a stance survives docking; it goes only with an entity gone for good

Every entity's zone-removal subscription dropped its IFF override for whatever
left the zone, and TryDock removes the docking ship, so a scenario stance was
lost one way (the other entity forgot; the docked ship, deactivated, kept its
own) as soon as the player docked. The removal event already tells the two
apart: TryDock parents the ship to its dock before removing it, and every
leaving-for-good path (death, warp, unload) leaves with no parent. The override
is now dropped only for an entity that leaves unparented.

Tests: a hostile stance holds both ways, including the live hostility each side
tracks, across dock and undock; a destroyed entity takes the player's stance
toward it with it. UndockingAdmitsWithNoAgent commits Soul's probe: undock
admits unpiloted, active, with no agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 15:09:12 +02:00
commit ff75b8ea34
2 changed files with 53 additions and 1 deletions

View file

@ -204,7 +204,9 @@ public abstract class Entity
VisibleEntities.Remove(remove.Value);
VisibleEnemies.Remove(remove.Value);
VisibleFriendlies.Remove(remove.Value);
_iffOverrides.Remove(remove.Value);
// A stance lasts the sitting: it goes with an entity gone for good (destroyed, warped away, unloaded), not
// with a ship docking, which leaves the zone parented to its dock (TryDock) and rejoins it on undock.
if (remove.Value.Parent == null) _iffOverrides.Remove(remove.Value);
if (_grudgeSubscriptions.TryGetValue(remove.Value, out var grudgeSubs))
{
foreach (var s in grudgeSubs) s.Dispose();

View file

@ -243,6 +243,56 @@ public sealed class RunStartTests : IDisposable
Assert.False(neutral.EntityHostility[player]);
}
// A stance lasts the sitting: the player docking and undocking leaves it standing both ways. It goes with an entity
// gone for good: once an entity is destroyed, the player holds no stance toward it.
[Fact]
public void AStanceSurvivesDockingButNotDestruction()
{
var zone = Arena(null);
var longinus = Bare(Hull("LonginusX"));
var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0),
Placed(longinus, 300, 0, ScenarioStance.Hostile, piloted: false),
Placed(longinus, -300, 0, ScenarioStance.Hostile, piloted: false)));
var player = staged.Player;
var hostile = staged.Entities[0];
var doomed = staged.Entities[1];
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
Assert.NotNull(station.TryDock(player));
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
Assert.True(station.TryUndock(player));
Assert.True(player.IsHostileTo(hostile), "the player's stance was lost over docking");
Assert.True(hostile.IsHostileTo(player), "the entity's stance was lost over docking");
Assert.True(player.EntityHostility[hostile], "the player's live hostility was lost over docking");
Assert.True(hostile.EntityHostility[player], "the entity's live hostility was lost over docking");
// Presets carry no faction, so without its stance the derived rule is not hostile
Assert.True(player.Faction == null && doomed.Faction == null, "the fixture's ships are factionless");
Assert.True(player.IsHostileTo(doomed));
doomed.Hull.Durability = 0;
doomed.HullDamage.OnNext(1);
SafeAssert.NotIn(zone, doomed, "a destroyed entity leaves the zone");
Assert.False(player.IsHostileTo(doomed), "the stance toward a destroyed entity outlived it");
}
// Undocking admits the ship unpiloted: back in the zone, active (its live hostility is tracked), with no agent.
[Fact]
public void UndockingAdmitsWithNoAgent()
{
var zone = Arena(null);
var player = Stage(zone, null).Player;
var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0);
var agents = zone.Agents.Count;
Assert.NotNull(station.TryDock(player));
SafeAssert.NotIn(zone, player, "a docked ship is out of the zone");
Assert.True(station.TryUndock(player));
SafeAssert.In(zone, player, "an undocked ship is back in the zone");
Assert.Equal(agents, zone.Agents.Count);
Assert.Equal(0, AgentsOf(zone, player));
Assert.True(player.EntityHostility.ContainsKey(station), "an undocked ship is active");
}
// Ambient: false keeps the stations and drops every generated ship and turret; ambient, and a plain New Game, keep
// them. The arena is the galaxy's entrance pack. The entrance holds a story station, placed as the narrative would
// place one, so a story station's own turrets are dropped too.