using System; using System.Collections.Generic; using System.Linq; namespace Ashfall.Core { /// Host-owned simulation. Rendering, transport and serialization are adapters. public sealed partial class GameSimulation { public WorldState World { get; private set; } public ContentCatalog Content { get; } public Action 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(); } } }