using System;
using System.Collections.Generic;
using System.Linq;
namespace Ashfall.Core
{
/// <summary>Host-owned simulation. Rendering, transport and serialization are adapters.</summary>
public sealed partial class GameSimulation
{
public WorldState World { get; private set; }
public ContentCatalog Content { get; }
public Action<string> Notice;
public Action CriticalChange;
public const float TickSeconds = 1f / 30;
public GameSimulation(WorldState world, ContentCatalog content)
{ World = world ?? throw new ArgumentNullException(nameof(world)); Content = content ?? throw new ArgumentNullException(nameof(content)); content.Validate(); }
public PlayerState Connect(string id, string name, Species species)
{
if (string.IsNullOrWhiteSpace(id) || id.Length > 64 || !Enum.IsDefined(typeof(Species), species)) return null;
var p = World.Player(id);
if (p != null && p.connected) return null;
if (World.players.Count(x => x.connected) >= 4) return null;
if (p == null)
{
p = new PlayerState { id = id, name = string.IsNullOrWhiteSpace(name) ? "幸存者" : name.Substring(0, Math.Min(name.Length, 24)),
species = species, location = WorldFactory.At("world", 0, new V2(-3 - World.players.Count() * 1.1f, -9)) };
p.inventory.Add("ammo", 240); p.inventory.Add("biomass", 12); p.inventory.Add("metal", 30);
World.players.Add(p); RefreshHealth(p, true);
}
p.connected = true; p.input = new PlayerInput(); p.acknowledgedInput = 0; p.lastInputAt = World.time;
p.stationId = ""; p.transitionId = "";
if (!CanStand(p.location, Radius(p))) p.location = SafeSpawn();
var desired = FindStation(p.preferredStationId);
if (desired != null && desired.playerId == "" && Near(p, desired.location, 2.6f))
{ desired.playerId = p.id; p.stationId = desired.id; }
p.portalReadyAt = World.time + 1;
return p;
}
public void Disconnect(string id)
{
var p = World.Player(id); if (p == null) return;
p.preferredStationId = p.stationId;
ReleaseStation(p, false); p.connected = false; p.input = new PlayerInput(); p.transitionId = "";
}
public bool SetInput(string id, PlayerInput input)
{
var p = World.Player(id);
if (p == null || !p.connected || input == null || input.sequence <= p.input.sequence
|| !input.move.Finite || !input.aim.Finite || input.aim.Length > 10000) return false;
input.move = input.move.Length > 1 ? input.move.Normalized : input.move;
p.input = input; p.lastInputAt = World.time; return true;
}
public void Tick(float dt)
{
if (!MathEx.Finite(dt) || dt <= 0 || dt > .1f) throw new ArgumentOutOfRangeException(nameof(dt));
if (!World.players.Any(p => p.connected)) return;
World.time += dt; World.tick++;
foreach (var p in World.players.Where(p => p.connected))
{
if (World.time - p.lastInputAt > .3) p.input = new PlayerInput { sequence = p.input.sequence, aim = p.input.aim };
if (p.health <= 0) Respawn(p);
p.energy = Math.Min(100, p.energy + dt * 5);
if (p.location.height > 0 && World.time >= p.wingUntil) p.location.height = 0;
if (!string.IsNullOrEmpty(p.transitionId)) TickTransition(p, dt);
else if (string.IsNullOrEmpty(p.stationId))
{
StepPlayer(World, p, p.input, dt);
if (World.time >= p.portalReadyAt) TryWalkThroughPortal(p);
}
p.acknowledgedInput = p.input.sequence;
}
foreach (var v in World.vehicles) TickVehicle(v, dt);
TickProduction(dt);
TickCombat(dt);
World.shots.RemoveAll(s => s.expiresAt <= World.time);
}
public static float Radius(PlayerState p) => p.IsWorm ? .28f : .38f;
public static void StepPlayer(WorldState world, PlayerState p, PlayerInput input, float dt)
{
V2 move = input.move;
if (move.Length > 1) move = move.Normalized;
float speed = p.IsWorm ? 4.4f : 5.6f;
if (input.sprint && p.energy > 0) { speed *= 1.5f; p.energy = Math.Max(0, p.energy - dt * 12); }
if (p.burrowUntil > world.time) speed *= 1.3f;
V2 delta = move * (speed * dt);
var candidate = p.location.Copy();
candidate.position.x += delta.x;
if (CanStand(world, candidate, Radius(p))) p.location.position.x = candidate.position.x;
candidate = p.location.Copy(); candidate.position.y += delta.y;
if (CanStand(world, candidate, Radius(p))) p.location.position.y = candidate.position.y;
V2 direction = input.aim - p.location.position;
if (p.IsWorm && move.Length > .05f) direction = move;
if (direction.Length > .05f) p.heading = MathEx.Angle(direction);
}
public bool CanStand(Location location, float radius = .38f) => CanStand(World, location, radius);
public static bool CanStand(WorldState world, Location l, float radius)
{
if (l == null || !l.position.Finite || !MathEx.Finite(l.height)) return false;
var floor = world.Floor(l);
if (floor == null || !floor.bounds.Contains(l.position, radius)) return false;
if (floor.walls.Any(w => !w.open && w.height > l.height && w.bounds.Contains(l.position, -radius))) return false;
if (floor.playerBuilt && !world.pieces.Any(b => b.kind == PieceKind.Floor && b.location.SameFloor(l)
&& new Box2(b.location.position, new V2(1, 1)).Contains(l.position))) return false;
foreach (var b in world.pieces)
if (b.location.SameFloor(l) && b.kind == PieceKind.Wall && new Box2(b.location.position,
b.rotation % 2 == 0 ? new V2(1, .18f) : new V2(.18f, 1)).Contains(l.position, -radius)) return false;
if (l.spaceId == "world" && l.height < 3)
{
foreach (var s in world.spaces.Where(s => s.kind == SpaceKind.Building))
{
V2 size = s.id == "base" ? new V2(18, 18) : new V2(16, 14);
if (new Box2(s.worldAnchor, size).Contains(l.position, -radius)) return false;
}
foreach (var v in world.vehicles.Where(v => v.flightMode == FlightMode.Ground && v.dockedTo == ""))
{
V2 local = Projection.WorldToInterior(l.position, v.position, v.heading);
if (new Box2(new V2(), new V2(v.width, v.length)).Contains(local, -radius)) return false;
}
}
return true;
}
public bool Near(PlayerState p, Location l, float distance = 2.2f) => p.location.SameFloor(l)
&& Math.Abs(p.location.height - l.height) < .6f && V2.Distance(p.location.position, l.position) <= distance;
public StationState FindStation(string id) => World.vehicles.SelectMany(v => v.stations).FirstOrDefault(s => s.id == id);
public VehicleState StationVehicle(string id) => World.vehicles.Find(v => v.stations.Any(s => s.id == id));
public Inventory Supply(PlayerState p) => World.VehicleForSpace(p.location.spaceId)?.cargo ?? World.warehouse;
public Location PortalFrom(PortalState portal)
{
var from = portal.from.Copy();
if (portal.ownerId != "" && from.spaceId == "world") from.position = World.Vehicle(portal.ownerId).DoorPosition;
return from;
}
public Location PortalDestination(PortalState portal)
{
var to = portal.to.Copy();
if (portal.ownerId != "" && to.spaceId == "world")
{
var v = World.Vehicle(portal.ownerId);
to.position = v.DoorPosition + new V2(0, -1.2f).Rotate(v.heading);
}
return to;
}
public ActionResult ValidatePortal(PlayerState p, PortalState portal)
{
if (portal == null || !Near(p, PortalFrom(portal), .9f)) return ActionResult.Fail("请走到入口");
if (p.location.height > .1f || p.burrowUntil > World.time) return ActionResult.Fail("请先回到地面");
if (portal.ownerId != "")
{
var v = World.Vehicle(portal.ownerId);
if (v == null || !v.doorOpen || Math.Abs(v.speed) > .5f || v.flightMode != FlightMode.Ground || v.dockedTo != "")
return ActionResult.Fail("载具需落地停稳后才能进出");
}
var target = PortalDestination(portal);
if (!CanStand(target, Radius(p))) return ActionResult.Fail("出口受阻");
if (World.players.Any(other => other.connected && other.id != p.id && other.location.SameFloor(target)
&& V2.Distance(other.location.position, target.position) < .55f)) return ActionResult.Fail("出口有人,请稍候");
return ActionResult.Ok();
}
private void TryWalkThroughPortal(PlayerState p)
{
foreach (var portal in World.portals)
{
if (!Near(p, PortalFrom(portal), .55f)) continue;
if (!ValidatePortal(p, portal).success) continue;
p.transitionId = portal.id; p.transitionProgress = 0; return;
}
}
private void TickTransition(PlayerState p, float dt)
{
var portal = World.portals.Find(x => x.id == p.transitionId);
if (portal == null) { p.transitionId = ""; return; }
p.transitionProgress += dt / Math.Max(.1f, portal.seconds);
if (p.transitionProgress < 1) return;
// Recheck at commit: the vehicle or another passenger may have moved meanwhile.
if (ValidatePortal(p, portal).success) p.location = PortalDestination(portal);
p.transitionId = ""; p.transitionProgress = 0; p.portalReadyAt = World.time + 1.3;
}
private void TickVehicle(VehicleState v, float dt)
{
var driverSeat = v.stations.Find(s => s.role == StationRole.Driver);
var driver = World.Player(driverSeat?.playerId);
var input = driver != null && driver.connected ? driver.input : null;
StepVehicle(v, input, dt);
World.Space(v.spaceId).worldAnchor = v.position;
}
public void StepVehicle(VehicleState v, PlayerInput input, float dt)
{
bool powered = v.energy > 0 && v.engine > 0 && v.hull > 0 && v.dockedTo == "";
float target = v.cruise ? v.maxSpeed * .55f : 0;
float heading = v.heading;
if (input != null)
{
if (input.brake) { target = 0; v.cruise = false; }
else if (Math.Abs(input.move.y) > .02f) target = input.move.y * v.maxSpeed * (input.sprint ? 1.25f : 1);
heading = MathEx.NormalizeAngle(v.heading - input.move.x * 65 * dt);
}
if (!powered) { target = 0; v.cruise = false; }
target *= MathEx.Clamp(v.engine / 100, .2f, 1);
v.speed = MathEx.Approach(v.speed, target, (input != null && input.brake ? 12 : 4) * dt);
V2 next = v.position + new V2(0, v.speed * dt).Rotate(heading);
if (CanVehicleOccupy(v, next, heading)) { v.position = next; v.heading = heading; }
else { v.speed = 0; v.cruise = false; }
if (Math.Abs(v.speed) > .1f) v.energy = Math.Max(0, v.energy - dt * .04f);
v.doorOpen = Math.Abs(v.speed) <= .5f && v.flightMode == FlightMode.Ground && v.dockedTo == "";
}
public bool CanVehicleOccupy(VehicleState v, V2 at, float heading)
{
float radius = Math.Max(v.width, v.length) * .5f;
if (!World.Space("world").floors[0].bounds.Contains(at, radius)) return false;
if (v.flightMode == FlightMode.Cruise) return true;
foreach (var wall in World.Space("world").floors[0].walls)
if (!wall.open && wall.height >= v.Altitude && wall.bounds.Contains(at, -v.width * .5f)) return false;
foreach (var s in World.spaces.Where(s => s.kind == SpaceKind.Building))
if (v.Altitude < 8 && new Box2(s.worldAnchor, s.id == "base" ? new V2(18, 18) : new V2(16, 14)).Contains(at, -radius)) return false;
foreach (var other in World.vehicles)
if (other.id != v.id && other.dockedTo == "" && other.flightMode == v.flightMode
&& V2.Distance(at, other.position) < (v.width + other.width) * .5f + .5f) return false;
if (v.flightMode == FlightMode.Ground)
foreach (var p in World.players.Where(p => p.connected && p.location.spaceId == "world"))
if (new Box2(new V2(), new V2(v.width, v.length)).Contains(
Projection.WorldToInterior(p.location.position, at, heading), -.4f)) return false;
return true;
}
private void ReleaseStation(PlayerState p, bool clearPreferred = true)
{
var seat = FindStation(p.stationId); var vehicle = StationVehicle(p.stationId);
if (seat != null && seat.playerId == p.id) seat.playerId = "";
if (vehicle != null && seat.role == StationRole.Driver && !clearPreferred) { vehicle.cruise = false; vehicle.speed = 0; }
p.stationId = ""; p.input = new PlayerInput { sequence = p.input.sequence, aim = p.input.aim };
if (clearPreferred) p.preferredStationId = "";
}
private Location SafeSpawn() => WorldFactory.At("world", 0, new V2(-5, -8));
private static void RefreshHealth(PlayerState p, bool full)
{
p.maxHealth = p.IsWorm ? 160 : 250 + (p.Level - 1) * 40;
p.health = full ? p.maxHealth : Math.Min(p.health, p.maxHealth);
}
private void Respawn(PlayerState p)
{
ReleaseStation(p); p.transitionId = ""; p.burrowUntil = 0; p.location.height = 0;
foreach (var item in p.inventory.items.ToArray())
{
if (item.id != "ore" && item.id != "biomass" && item.id != "metal" && item.id != "sample") continue;
int lost = item.amount / 5;
if (lost > 0) { p.inventory.Take(item.id, lost); World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = item.id, amount = lost, location = p.location.Copy() }); }
}
p.location = SafeSpawn(); RefreshHealth(p, true); p.portalReadyAt = World.time + 1;
}
public void PrepareRestoredWorld()
{
if (World.version != WorldState.CurrentVersion) throw new InvalidOperationException("Unsupported save version.");
foreach (var p in World.players)
{
p.connected = false; p.preferredStationId = p.stationId != "" ? p.stationId : p.preferredStationId;
p.stationId = ""; p.input = new PlayerInput(); p.acknowledgedInput = 0;
p.transitionId = ""; p.transitionProgress = 0;
}
foreach (var v in World.vehicles)
{ v.speed = 0; v.cruise = false; foreach (var seat in v.stations) seat.playerId = ""; }
World.shots.Clear();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ashfall.Core
{
/// <summary>Host-owned simulation. Rendering, transport and serialization are adapters.</summary>
public sealed partial class GameSimulation
{
public WorldState World { get; private set; }
public ContentCatalog Content { get; }
public Action<string> Notice;
public Action CriticalChange;
public const float TickSeconds = 1f / 30;
public GameSimulation(WorldState world, ContentCatalog content)
{ World = world ?? throw new ArgumentNullException(nameof(world)); Content = content ?? throw new ArgumentNullException(nameof(content)); content.Validate(); }
public PlayerState Connect(string id, string name, Species species)
{
if (string.IsNullOrWhiteSpace(id) || id.Length > 64 || !Enum.IsDefined(typeof(Species), species)) return null;
var p = World.Player(id);
if (p != null && p.connected) return null;
if (World.players.Count(x => x.connected) >= 4) return null;
if (p == null)
{
p = new PlayerState { id = id, name = string.IsNullOrWhiteSpace(name) ? "幸存者" : name.Substring(0, Math.Min(name.Length, 24)),
species = species, location = WorldFactory.At("world", 0, new V2(-3 - World.players.Count() * 1.1f, -9)) };
p.inventory.Add("ammo", 240); p.inventory.Add("biomass", 12); p.inventory.Add("metal", 30);
World.players.Add(p); RefreshHealth(p, true);
}
p.connected = true; p.input = new PlayerInput(); p.acknowledgedInput = 0; p.lastInputAt = World.time;
p.stationId = ""; p.transitionId = "";
if (!CanStand(p.location, Radius(p))) p.location = SafeSpawn();
var desired = FindStation(p.preferredStationId);
if (desired != null && desired.playerId == "" && Near(p, desired.location, 2.6f))
{ desired.playerId = p.id; p.stationId = desired.id; }
p.portalReadyAt = World.time + 1;
return p;
}
public void Disconnect(string id)
{
var p = World.Player(id); if (p == null) return;
p.preferredStationId = p.stationId;
ReleaseStation(p, false); p.connected = false; p.input = new PlayerInput(); p.transitionId = "";
}
public bool SetInput(string id, PlayerInput input)
{
var p = World.Player(id);
if (p == null || !p.connected || input == null || input.sequence <= p.input.sequence
|| !input.move.Finite || !input.aim.Finite || input.aim.Length > 10000) return false;
input.move = input.move.Length > 1 ? input.move.Normalized : input.move;
p.input = input; p.lastInputAt = World.time; return true;
}
public void Tick(float dt)
{
if (!MathEx.Finite(dt) || dt <= 0 || dt > .1f) throw new ArgumentOutOfRangeException(nameof(dt));
if (!World.players.Any(p => p.connected)) return;
World.time += dt; World.tick++;
foreach (var p in World.players.Where(p => p.connected))
{
if (World.time - p.lastInputAt > .3) p.input = new PlayerInput { sequence = p.input.sequence, aim = p.input.aim };
if (p.health <= 0) Respawn(p);
p.energy = Math.Min(100, p.energy + dt * 5);
if (p.location.height > 0 && World.time >= p.wingUntil) p.location.height = 0;
if (!string.IsNullOrEmpty(p.transitionId)) TickTransition(p, dt);
else if (string.IsNullOrEmpty(p.stationId))
{
StepPlayer(World, p, p.input, dt);
if (World.time >= p.portalReadyAt) TryWalkThroughPortal(p);
}
p.acknowledgedInput = p.input.sequence;
}
foreach (var v in World.vehicles) TickVehicle(v, dt);
TickProduction(dt);
TickCombat(dt);
World.shots.RemoveAll(s => s.expiresAt <= World.time);
}
public static float Radius(PlayerState p) => p.IsWorm ? .28f : .38f;
public static void StepPlayer(WorldState world, PlayerState p, PlayerInput input, float dt)
{
V2 move = input.move;
if (move.Length > 1) move = move.Normalized;
float speed = p.IsWorm ? 4.4f : 5.6f;
if (input.sprint && p.energy > 0) { speed *= 1.5f; p.energy = Math.Max(0, p.energy - dt * 12); }
if (p.burrowUntil > world.time) speed *= 1.3f;
V2 delta = move * (speed * dt);
var candidate = p.location.Copy();
candidate.position.x += delta.x;
if (CanStand(world, candidate, Radius(p))) p.location.position.x = candidate.position.x;
candidate = p.location.Copy(); candidate.position.y += delta.y;
if (CanStand(world, candidate, Radius(p))) p.location.position.y = candidate.position.y;
V2 direction = input.aim - p.location.position;
if (p.IsWorm && move.Length > .05f) direction = move;
if (direction.Length > .05f) p.heading = MathEx.Angle(direction);
}
public bool CanStand(Location location, float radius = .38f) => CanStand(World, location, radius);
public static bool CanStand(WorldState world, Location l, float radius)
{
if (l == null || !l.position.Finite || !MathEx.Finite(l.height)) return false;
var floor = world.Floor(l);
if (floor == null || !floor.bounds.Contains(l.position, radius)) return false;
if (floor.walls.Any(w => !w.open && w.height > l.height && w.bounds.Contains(l.position, -radius))) return false;
if (floor.playerBuilt && !world.pieces.Any(b => b.kind == PieceKind.Floor && b.location.SameFloor(l)
&& new Box2(b.location.position, new V2(1, 1)).Contains(l.position))) return false;
foreach (var b in world.pieces)
if (b.location.SameFloor(l) && b.kind == PieceKind.Wall && new Box2(b.location.position,
b.rotation % 2 == 0 ? new V2(1, .18f) : new V2(.18f, 1)).Contains(l.position, -radius)) return false;
if (l.spaceId == "world" && l.height < 3)
{
foreach (var s in world.spaces.Where(s => s.kind == SpaceKind.Building))
{
V2 size = s.id == "base" ? new V2(18, 18) : new V2(16, 14);
if (new Box2(s.worldAnchor, size).Contains(l.position, -radius)) return false;
}
foreach (var v in world.vehicles.Where(v => v.flightMode == FlightMode.Ground && v.dockedTo == ""))
{
V2 local = Projection.WorldToInterior(l.position, v.position, v.heading);
if (new Box2(new V2(), new V2(v.width, v.length)).Contains(local, -radius)) return false;
}
}
return true;
}
public bool Near(PlayerState p, Location l, float distance = 2.2f) => p.location.SameFloor(l)
&& Math.Abs(p.location.height - l.height) < .6f && V2.Distance(p.location.position, l.position) <= distance;
public StationState FindStation(string id) => World.vehicles.SelectMany(v => v.stations).FirstOrDefault(s => s.id == id);
public VehicleState StationVehicle(string id) => World.vehicles.Find(v => v.stations.Any(s => s.id == id));
public Inventory Supply(PlayerState p) => World.VehicleForSpace(p.location.spaceId)?.cargo ?? World.warehouse;
public Location PortalFrom(PortalState portal)
{
var from = portal.from.Copy();
if (portal.ownerId != "" && from.spaceId == "world") from.position = World.Vehicle(portal.ownerId).DoorPosition;
return from;
}
public Location PortalDestination(PortalState portal)
{
var to = portal.to.Copy();
if (portal.ownerId != "" && to.spaceId == "world")
{
var v = World.Vehicle(portal.ownerId);
to.position = v.DoorPosition + new V2(0, -1.2f).Rotate(v.heading);
}
return to;
}
public ActionResult ValidatePortal(PlayerState p, PortalState portal)
{
if (portal == null || !Near(p, PortalFrom(portal), .9f)) return ActionResult.Fail("请走到入口");
if (p.location.height > .1f || p.burrowUntil > World.time) return ActionResult.Fail("请先回到地面");
if (portal.ownerId != "")
{
var v = World.Vehicle(portal.ownerId);
if (v == null || !v.doorOpen || Math.Abs(v.speed) > .5f || v.flightMode != FlightMode.Ground || v.dockedTo != "")
return ActionResult.Fail("载具需落地停稳后才能进出");
}
var target = PortalDestination(portal);
if (!CanStand(target, Radius(p))) return ActionResult.Fail("出口受阻");
if (World.players.Any(other => other.connected && other.id != p.id && other.location.SameFloor(target)
&& V2.Distance(other.location.position, target.position) < .55f)) return ActionResult.Fail("出口有人,请稍候");
return ActionResult.Ok();
}
private void TryWalkThroughPortal(PlayerState p)
{
foreach (var portal in World.portals)
{
if (!Near(p, PortalFrom(portal), .55f)) continue;
if (!ValidatePortal(p, portal).success) continue;
p.transitionId = portal.id; p.transitionProgress = 0; return;
}
}
private void TickTransition(PlayerState p, float dt)
{
var portal = World.portals.Find(x => x.id == p.transitionId);
if (portal == null) { p.transitionId = ""; return; }
p.transitionProgress += dt / Math.Max(.1f, portal.seconds);
if (p.transitionProgress < 1) return;
// Recheck at commit: the vehicle or another passenger may have moved meanwhile.
if (ValidatePortal(p, portal).success) p.location = PortalDestination(portal);
p.transitionId = ""; p.transitionProgress = 0; p.portalReadyAt = World.time + 1.3;
}
private void TickVehicle(VehicleState v, float dt)
{
var driverSeat = v.stations.Find(s => s.role == StationRole.Driver);
var driver = World.Player(driverSeat?.playerId);
var input = driver != null && driver.connected ? driver.input : null;
StepVehicle(v, input, dt);
World.Space(v.spaceId).worldAnchor = v.position;
}
public void StepVehicle(VehicleState v, PlayerInput input, float dt)
{
bool powered = v.energy > 0 && v.engine > 0 && v.hull > 0 && v.dockedTo == "";
float target = v.cruise ? v.maxSpeed * .55f : 0;
float heading = v.heading;
if (input != null)
{
if (input.brake) { target = 0; v.cruise = false; }
else if (Math.Abs(input.move.y) > .02f) target = input.move.y * v.maxSpeed * (input.sprint ? 1.25f : 1);
heading = MathEx.NormalizeAngle(v.heading - input.move.x * 65 * dt);
}
if (!powered) { target = 0; v.cruise = false; }
target *= MathEx.Clamp(v.engine / 100, .2f, 1);
v.speed = MathEx.Approach(v.speed, target, (input != null && input.brake ? 12 : 4) * dt);
V2 next = v.position + new V2(0, v.speed * dt).Rotate(heading);
if (CanVehicleOccupy(v, next, heading)) { v.position = next; v.heading = heading; }
else { v.speed = 0; v.cruise = false; }
if (Math.Abs(v.speed) > .1f) v.energy = Math.Max(0, v.energy - dt * .04f);
v.doorOpen = Math.Abs(v.speed) <= .5f && v.flightMode == FlightMode.Ground && v.dockedTo == "";
}
public bool CanVehicleOccupy(VehicleState v, V2 at, float heading)
{
float radius = Math.Max(v.width, v.length) * .5f;
if (!World.Space("world").floors[0].bounds.Contains(at, radius)) return false;
if (v.flightMode == FlightMode.Cruise) return true;
foreach (var wall in World.Space("world").floors[0].walls)
if (!wall.open && wall.height >= v.Altitude && wall.bounds.Contains(at, -v.width * .5f)) return false;
foreach (var s in World.spaces.Where(s => s.kind == SpaceKind.Building))
if (v.Altitude < 8 && new Box2(s.worldAnchor, s.id == "base" ? new V2(18, 18) : new V2(16, 14)).Contains(at, -radius)) return false;
foreach (var other in World.vehicles)
if (other.id != v.id && other.dockedTo == "" && other.flightMode == v.flightMode
&& V2.Distance(at, other.position) < (v.width + other.width) * .5f + .5f) return false;
if (v.flightMode == FlightMode.Ground)
foreach (var p in World.players.Where(p => p.connected && p.location.spaceId == "world"))
if (new Box2(new V2(), new V2(v.width, v.length)).Contains(
Projection.WorldToInterior(p.location.position, at, heading), -.4f)) return false;
return true;
}
private void ReleaseStation(PlayerState p, bool clearPreferred = true)
{
var seat = FindStation(p.stationId); var vehicle = StationVehicle(p.stationId);
if (seat != null && seat.playerId == p.id) seat.playerId = "";
if (vehicle != null && seat.role == StationRole.Driver && !clearPreferred) { vehicle.cruise = false; vehicle.speed = 0; }
p.stationId = ""; p.input = new PlayerInput { sequence = p.input.sequence, aim = p.input.aim };
if (clearPreferred) p.preferredStationId = "";
}
private Location SafeSpawn() => WorldFactory.At("world", 0, new V2(-5, -8));
private static void RefreshHealth(PlayerState p, bool full)
{
p.maxHealth = p.IsWorm ? 160 : 250 + (p.Level - 1) * 40;
p.health = full ? p.maxHealth : Math.Min(p.health, p.maxHealth);
}
private void Respawn(PlayerState p)
{
ReleaseStation(p); p.transitionId = ""; p.burrowUntil = 0; p.location.height = 0;
foreach (var item in p.inventory.items.ToArray())
{
if (item.id != "ore" && item.id != "biomass" && item.id != "metal" && item.id != "sample") continue;
int lost = item.amount / 5;
if (lost > 0) { p.inventory.Take(item.id, lost); World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = item.id, amount = lost, location = p.location.Copy() }); }
}
p.location = SafeSpawn(); RefreshHealth(p, true); p.portalReadyAt = World.time + 1;
}
public void PrepareRestoredWorld()
{
if (World.version != WorldState.CurrentVersion) throw new InvalidOperationException("Unsupported save version.");
foreach (var p in World.players)
{
p.connected = false; p.preferredStationId = p.stationId != "" ? p.stationId : p.preferredStationId;
p.stationId = ""; p.input = new PlayerInput(); p.acknowledgedInput = 0;
p.transitionId = ""; p.transitionProgress = 0;
}
foreach (var v in World.vehicles)
{ v.speed = 0; v.cruise = false; foreach (var seat in v.stations) seat.playerId = ""; }
World.shots.Clear();
}
}
}