XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AshfallFrontier

【Unity】灰烬边境,一款生存策略游戏

公开
关注 0 Fork 0 Star 0
UTF-8
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();
        }
    }
}