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

AshfallFrontier

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.Linq;
using Ashfall.Core;
using UnityEngine;
using UnityEngine.InputSystem;

namespace Ashfall.Runtime
{
    public sealed class GameHud : MonoBehaviour
    {
        private GameSession session;
        private WorldView view;
        private Font font;
        private GUIStyle title, heading, label, small, button, field;
        private string playerName = "远征者", address = "127.0.0.1", facilityId = "", toast = "";
        private float toastUntil;
        private bool brood, pause, inventory, map;
        private Vector2 scroll;
        private int rotation, buildFloor;
        private PieceKind piece = PieceKind.Stairs;
        private Rect panel;
        private readonly Color ink = new Color(.78f, .84f, .83f), accent = new Color(.36f, .83f, .73f);
        public bool BuildMode { get; private set; }
        public bool BlocksMovement => pause || inventory || map || facilityId != "";
        public bool CapturesPointer
        {
            get
            {
                if (BlocksMovement || !session.Running) return true;
                if (Mouse.current == null) return false;
                Vector2 p = Mouse.current.position.ReadValue(); p.y = Screen.height - p.y;
                return BuildMode && panel.Contains(p);
            }
        }
        public void Initialize(GameSession value)
        {
            session = value; view = GetComponent<WorldView>();
            session.Message += (text, good) => { toast = text; toastUntil = Time.unscaledTime + 5; };
            font = Font.CreateDynamicFontFromOSFont(new[] { "Microsoft YaHei", "SimHei", "Arial" }, 18);
        }
        private void Styles()
        {
            if (title != null) return;
            title = Style(36, Color.white); title.fontStyle = FontStyle.Bold;
            heading = Style(20, accent); label = Style(16, ink); small = Style(13, ink);
            button = new GUIStyle(GUI.skin.button) { font = font, fontSize = 16, fixedHeight = 35, margin = new RectOffset(0, 0, 4, 4), padding = new RectOffset(12, 12, 5, 5) };
            field = new GUIStyle(GUI.skin.textField) { font = font, fontSize = 17, fixedHeight = 32 };
        }
        private GUIStyle Style(int size, Color color) => new GUIStyle(GUI.skin.label)
        { font = font, fontSize = size, wordWrap = true, normal = { textColor = color } };
        private void Update()
        {
            if (!session.Running) { pause = inventory = map = BuildMode = false; facilityId = ""; return; }
            var k = Keyboard.current; if (k == null || session.LocalPlayer == null) return;
            if (k.escapeKey.wasPressedThisFrame)
            {
                if (facilityId != "" || inventory || map || BuildMode) { facilityId = ""; inventory = map = BuildMode = false; }
                else pause = !pause;
            }
            if (k.tabKey.wasPressedThisFrame && !pause && facilityId == "") inventory = !inventory;
            if (k.mKey.wasPressedThisFrame && !pause && facilityId == "") map = !map;
            if (k.bKey.wasPressedThisFrame && !BlocksMovement)
            {
                if (session.LocalPlayer.location.spaceId != "base") session.Toast("进入前哨基地后按 B 建造", false);
                else { BuildMode = !BuildMode; buildFloor = session.LocalPlayer.location.floor; }
            }
            if (!BuildMode || BlocksMovement) return;
            if (k.rKey.wasPressedThisFrame) rotation = (rotation + 1) % 4;
            if (Mouse.current == null || CapturesPointer) return;
            if (Mouse.current.leftButton.wasPressedThisFrame)
                Send(CommandKind.Build, position: view.MouseGround, value: "", target: "", floor: buildFloor);
            if (Mouse.current.rightButton.wasPressedThisFrame)
            {
                var at = view.MouseGround;
                var nearest = session.World.pieces.Where(b => b.location.spaceId == "base" && b.location.floor == buildFloor)
                    .OrderBy(b => V2.Distance(b.location.position, at)).ThenBy(b => b.kind == PieceKind.Floor ? 1 : 0).FirstOrDefault();
                if (nearest != null && V2.Distance(nearest.location.position, at) < .8f) Send(CommandKind.Dismantle, nearest.id);
            }
        }
        public void Interact()
        {
            var p = session.LocalPlayer; if (p == null) return;
            if (p.stationId != "") { Send(CommandKind.LeaveStation); return; }
            var seat = session.World.vehicles.SelectMany(v => v.stations).Where(s => session.Simulation.Near(p, s.location))
                .OrderBy(s => V2.Distance(p.location.position, s.location.position)).FirstOrDefault();
            if (seat != null) { Send(CommandKind.Interact, seat.id); return; }
            var f = session.World.facilities.Where(x => session.Simulation.Near(p, x.location))
                .OrderBy(x => V2.Distance(p.location.position, x.location.position)).FirstOrDefault();
            if (f != null)
            {
                if (f.kind == FacilityKind.Generator) Send(CommandKind.Interact, f.id);
                else { facilityId = f.id; scroll = Vector2.zero; }
                return;
            }
            var r = session.World.resources.Where(x => session.Simulation.Near(p, x.location, 2)).OrderBy(x => V2.Distance(p.location.position, x.location.position)).FirstOrDefault();
            if (r != null) Send(CommandKind.Collect, r.id);
            else session.Toast("靠近设备按 E;沿标记步行进出舱门和楼梯", true);
        }
        private void Send(CommandKind kind, string target = "", string value = "", string slot = "", int amount = 1, V2 position = default, int floor = 0)
            => session.Command(new GameCommand { kind = kind, targetId = target, value = value, slot = slot, amount = amount,
                position = position, floor = floor, piece = piece, rotation = rotation });
        private void OnGUI()
        {
            if (session == null) return; Styles();
            if (!session.Running) { Menu(); return; }
            if (session.LocalPlayer == null) { GUI.Label(new Rect(40, 40, 600, 80), session.Status, heading); return; }
            Readouts();
            if (pause) PauseMenu();
            else if (inventory) InventoryPanel();
            else if (map) MapPanel();
            else if (facilityId != "") FacilityPanel();
            else if (BuildMode) BuildingPanel();
            if (Time.unscaledTime < toastUntil)
            {
                var r = new Rect(Screen.width * .5f - 300, Screen.height - 130, 600, 62); Back(r);
                GUI.Label(new Rect(r.x + 16, r.y + 12, r.width - 32, r.height - 16), toast, label);
            }
        }
        private void Back(Rect r)
        {
            Color old = GUI.color; GUI.color = new Color(.06f, .10f, .11f, .96f); GUI.DrawTexture(r, Texture2D.whiteTexture);
            GUI.color = accent; GUI.DrawTexture(new Rect(r.x, r.y, 3, r.height), Texture2D.whiteTexture); GUI.color = old;
        }
        private void BeginPanel(Rect r)
        { panel = r; Back(r); GUILayout.BeginArea(new Rect(r.x + 20, r.y + 15, r.width - 40, r.height - 30)); }
        private void EndPanel() => GUILayout.EndArea();
        private bool Button(string text) => GUILayout.Button(text, button);
        private void Text(string text) => GUILayout.Label(text, label);
        private Rect Center(float width, float height) => new Rect((Screen.width - width) / 2, Mathf.Max(12, (Screen.height - height) / 2), width, Mathf.Min(height, Screen.height - 24));
        private void Menu()
        {
            GUI.color = new Color(.035f, .055f, .06f); GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), Texture2D.whiteTexture); GUI.color = Color.white;
            BeginPanel(Center(630, 685));
            GUILayout.Label("ASHFALL FRONTIER", title);
            GUILayout.Label("陨烬边境  /  远征原型", heading);
            GUILayout.Space(10); Text("进入基地,上楼研究,步入运输舱,在行驶途中协作。虫族从蠕虫幼体开始,通过采集和蜕变成长。");
            GUILayout.Space(10); Text("角色名称"); playerName = GUILayout.TextField(playerName, 24, field);
            GUILayout.BeginHorizontal(); if (Button((!brood ? "● " : "○ ") + "人类 · 游骑兵")) brood = false;
            if (Button((brood ? "● " : "○ ") + "虫族 · 蠕虫幼体")) brood = true; GUILayout.EndHorizontal();
            Species species = brood ? Species.Brood : Species.Human;
            if (Button("继续单人远征")) session.StartOffline(species, playerName, true);
            if (Button("建立合作房间 · 继续世界")) session.StartHost(species, playerName, true);
            GUILayout.BeginHorizontal(); address = GUILayout.TextField(address, field);
            if (Button("通过 IP 加入")) session.StartClient(address, species, playerName); GUILayout.EndHorizontal();
            GUILayout.Space(10);
            if (Button("开始全新远征")) session.StartFresh(species, playerName);
            GUILayout.Label("新远征会保留旧世界的归档。合作通过局域网 IP 连接,房主保存世界。", small);
            GUILayout.Space(8); GUILayout.Label(session.Status, small);
            GUILayout.Space(12); GUILayout.Label("阶段 0 · 两层建筑 / 四层建造 / 运输舱 / 三层飞行舱\n正式内容、美术动画及互联网 Relay 尚在后续制作范围。", small);
            EndPanel();
        }
        private void Readouts()
        {
            var p = session.LocalPlayer; var v = view.ControlledVehicle;
            Back(new Rect(18, 18, 330, 106));
            GUI.Label(new Rect(34, 28, 305, 28), p.name + "  /  " + (p.IsWorm ? "蠕虫幼体" : p.species == Species.Human ? "游骑兵" : "掘袭猎手"), heading);
            GUI.Label(new Rect(34, 60, 305, 27), $"生命 {p.health:0}/{p.maxHealth:0}    能量 {p.energy:0}", label);
            GUI.Label(new Rect(34, 90, 305, 25), $"{session.World.Space(p.location.spaceId)?.name}  ·  {p.location.floor + 1}F", small);
            Back(new Rect(Screen.width - 330, 18, 312, 158));
            GUI.Label(new Rect(Screen.width - 312, 29, 282, 140),
                "远征路线\n西南:前哨基地 / 蜕变茧\n西北:研究站 / 恢复供能\n东方:双层运输载具 / 敌群\n北方:三层飞行试验舱\nM 地图  ·  滚轮缩放", small);
            string controls = v == null ? "WASD 移动  ·  鼠标攻击  ·  Shift 冲刺  ·  1 技能  ·  E 交互  ·  B 建造  ·  Tab 背包  ·  Esc 菜单"
                : $"{v.name}  |  {v.speed:0.0} m/s  |  装甲 {v.hull:0}/{v.maxHull:0}  |  WASD 驾驶  ·  左/右键火控  ·  Space 制动  ·  1 巡航  ·  2 高度  ·  E 返回舱内";
            Back(new Rect(18, Screen.height - 54, Screen.width - 36, 36));
            GUI.Label(new Rect(32, Screen.height - 48, Screen.width - 64, 30), controls, small);
            foreach (var mate in session.World.players.Where(x => x.connected && x.id != p.id))
            {
                int index = session.World.players.IndexOf(mate);
                GUI.Label(new Rect(30, 130 + index * 22, 310, 24), mate.name + " · " + (session.World.Space(mate.location.spaceId)?.name ?? "") + " " + (mate.location.floor + 1) + "F", small);
            }
            if (BlocksMovement || v != null) return;
            var near = session.World.facilities.Where(f => session.Simulation.Near(p, f.location, 4));
            foreach (var f in near) WorldLabel(f.location, f.name + (f.powered ? " · E" : " · 缺少供能"));
            foreach (var s in session.World.vehicles.SelectMany(x => x.stations).Where(s => session.Simulation.Near(p, s.location, 4))) WorldLabel(s.location, (s.role == StationRole.Driver ? "驾驶台" : "主炮台") + " · E");
            foreach (var r in session.World.resources.Where(r => session.Simulation.Near(p, r.location, 3))) WorldLabel(r.location, Item(r.itemId) + " ×" + r.amount + " · E");
            foreach (var portal in session.World.portals.Where(x => session.Simulation.Near(p, x.from, 5))) WorldLabel(portal.from, portal.stairs ? "步行上下楼" : "步行通过入口");
        }
        private void WorldLabel(Location l, string text)
        {
            var at = view.GuiPosition(l); GUI.Label(new Rect(at.x - 95, at.y - 45, 220, 48), text, small);
        }
        private void PauseMenu()
        {
            BeginPanel(Center(480, 350)); GUILayout.Label("远征菜单", heading);
            Text("合作世界继续运行,请在安全处停留。");
            if (Button("返回远征")) pause = false;
            if (Button(view.Night ? "切换白昼" : "切换夜间")) view.Night = !view.Night;
            if (session.IsAuthority && Button("立即保存")) { session.Save(); session.Toast("世界已保存", true); }
            if (Button("保存并返回主菜单")) session.Stop(); EndPanel();
        }
        private void InventoryPanel()
        {
            BeginPanel(Center(500, 550)); GUILayout.Label("随身物资", heading);
            scroll = GUILayout.BeginScrollView(scroll);
            foreach (var item in session.LocalPlayer.inventory.items) Text(Item(item.id) + "  × " + item.amount);
            GUILayout.EndScrollView(); Text("前往仓储设施存取。虫族首次蜕变需要随身携带生物质 ×20、样本 ×1。");
            if (Button("关闭 · Tab")) inventory = false; EndPanel();
        }
        private void FacilityPanel()
        {
            var f = session.World.facilities.Find(x => x.id == facilityId);
            if (f == null || !session.Simulation.Near(session.LocalPlayer, f.location)) { facilityId = ""; return; }
            BeginPanel(Center(650, 620)); GUILayout.Label(f.name, heading); Text(f.powered ? "供能正常" : "缺少供能,请先启动动力设备");
            scroll = GUILayout.BeginScrollView(scroll);
            var vehicle = session.World.VehicleForSpace(f.location.spaceId);
            var stock = vehicle?.cargo ?? session.World.warehouse;
            if (f.kind == FacilityKind.Storage)
            {
                Text("仓储 / 随身物资");
                foreach (string id in stock.items.Select(i => i.id).Concat(session.LocalPlayer.inventory.items.Select(i => i.id)).Distinct().ToArray())
                {
                    GUILayout.BeginHorizontal(); GUILayout.Label(Item(id) + " " + stock.Count(id) + " / " + session.LocalPlayer.inventory.Count(id), label, GUILayout.Width(290));
                    if (Button("取 10")) Send(CommandKind.Transfer, f.id, id, "withdraw", Math.Min(10, stock.Count(id)));
                    if (Button("存全部")) Send(CommandKind.Transfer, f.id, id, "deposit", session.LocalPlayer.inventory.Count(id));
                    GUILayout.EndHorizontal();
                }
            }
            if (f.kind == FacilityKind.Research)
            {
                Text("重型武器研究:设施仓储消耗样本 ×2、金属或生物质 ×20。成果供全队使用。");
                if (Button("进行研究")) Send(CommandKind.Research, f.id);
            }
            if (f.kind == FacilityKind.Medical)
            {
                if (Button("医疗恢复")) Send(CommandKind.Interact, f.id);
                if (Button("切换为人类")) Send(CommandKind.SwitchSpecies, value: "Human");
                if (Button("切换为虫族")) Send(CommandKind.SwitchSpecies, value: "Brood");
            }
            if ((f.kind == FacilityKind.Medical || f.kind == FacilityKind.Research) && f.family == Family.Biological && session.LocalPlayer.IsWorm)
            {
                Text("首次蜕变 · 蠕虫 → 掘袭猎手\n随身生物质 ×20、样本 ×1。解锁骨针攻击及更长钻地。");
                if (Button("开始蜕变")) Send(CommandKind.Evolve, f.id);
            }
            if (f.kind == FacilityKind.Workshop)
            {
                foreach (var recipe in session.Content.recipes.Where(r => r.family == f.family))
                    if (Button(recipe.name + "  ·  " + string.Join(" / ", recipe.inputs.Select(i => Item(i.id) + " " + i.amount)) + $"  ·  {recipe.seconds:0}s")) Send(CommandKind.Craft, f.id, recipe.id);
                foreach (var job in f.jobs) Text("生产:" + Item(job.outputId) + "  " + job.remaining.ToString("0.0") + "s");
                if (vehicle != null)
                {
                    GUILayout.Space(10); Text("载具改装 · 需要落地停驻");
                    foreach (var hp in vehicle.hardpoints)
                    {
                        Text((hp.group == 0 ? "主" : "副") + "武器组 · " + session.Content.Module(hp.moduleId)?.name + " · 挂点尺寸 " + hp.size);
                        foreach (var module in session.Content.modules.Where(m => m.family == vehicle.family))
                        {
                            var validation = session.Simulation.ValidateModule(vehicle, hp, module);
                            if (Button("安装 " + module.name + (validation.success ? "" : " · " + validation.message))) Send(CommandKind.InstallModule, vehicle.id, module.id, hp.id);
                        }
                    }
                    if (Button("维修 / 补给 · 货仓材料 ×10")) Send(CommandKind.Repair, vehicle.id);
                }
            }
            GUILayout.EndScrollView(); if (Button("离开设施 · Esc")) facilityId = ""; EndPanel();
        }
        private void BuildingPanel()
        {
            BeginPanel(new Rect(Screen.width - 316, 190, 298, Mathf.Min(480, Screen.height - 260)));
            GUILayout.Label("基地建造", heading);
            var kinds = new[] { PieceKind.Floor, PieceKind.Wall, PieceKind.Door, PieceKind.Stairs, PieceKind.Research, PieceKind.Generator, PieceKind.Storage, PieceKind.Turret };
            string[] names = { "楼板", "墙体", "门", "楼梯 + 上层平台", "研究设施", "供能设施", "仓储", "防御炮塔" };
            int selection = Array.IndexOf(kinds, piece);
            selection = GUILayout.SelectionGrid(selection, names, 2, button); piece = kinds[selection];
            buildFloor = Mathf.RoundToInt(GUILayout.HorizontalSlider(buildFloor, 0, Mathf.Min(3, session.LocalPlayer.location.floor + 1)));
            Text($"建造 {buildFloor + 1}F  ·  朝向 {rotation * 90}°");
            Text("左键放置 / 右键拆除 / R 转向\n楼梯向画面上方连接四米远的平台。上楼后铺楼板,再放研究设施。");
            if (Button("结束建造 · B")) BuildMode = false; EndPanel();
        }
        private void MapPanel()
        {
            Rect r = Center(760, 650); Back(r); GUI.Label(new Rect(r.x + 20, r.y + 15, 700, 35), "陨落平原 · 原型试验区", heading);
            Vector2 origin = new Vector2(r.center.x, r.center.y + 90); const float scale = 4;
            void Mark(V2 at, string name, Color color)
            { Vector2 p = origin + new Vector2(at.x, -at.y) * scale; Color old = GUI.color; GUI.color = color; GUI.DrawTexture(new Rect(p.x - 4, p.y - 4, 8, 8), Texture2D.whiteTexture); GUI.color = old; GUI.Label(new Rect(p.x + 9, p.y - 15, 160, 38), name, small); }
            Mark(new V2(-20, -15), "前哨基地", accent); Mark(new V2(-20, 16), "两层研究站", accent);
            Mark(new V2(57, -23), "危险区 / 首领", new Color(.9f, .4f, .2f)); Mark(new V2(0, -40), "资源带", Color.yellow);
            foreach (var v in session.World.vehicles) Mark(v.position, v.name, new Color(.6f, .75f, .9f));
            var player = session.LocalPlayer; if (player.location.spaceId == "world") Mark(player.location.position, "你", Color.white);
            GUI.Label(new Rect(r.x + 20, r.yMax - 55, r.width - 40, 42), "载具从后部入口步行进入,登上顶层后使用驾驶台。按 M / Esc 关闭。", label);
        }
        public static string Item(string id)
        {
            switch (id) { case "metal": return "金属"; case "ore": return "矿石"; case "biomass": return "生物质"; case "sample": return "样本";
                case "ammo": return "弹药"; case "shell": return "炮弹"; case "alloy": return "合金"; case "chitin": return "甲壳板"; default: return id; }
        }
    }
}