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; }
}
}
}
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; }
}
}
}