feat: improve map lighting and expand the playable training range

Add real-time lighting and shadows, training courses and targets, and animated salvage crates. Include bilingual assets and curated verification results.
This commit is contained in:
2026-11-12 18:03:04 +08:00
parent 0857e291d5
commit eb819c6688
75 changed files with 16903 additions and 121 deletions
+4 -2
View File
@@ -4,6 +4,8 @@ namespace Ironspore
{
public sealed class EnemyUnit : MonoBehaviour
{
// DynamicProps keeps its original physical obstruction semantics after render-layer separation.
const int WorldObstructionMask = (1 << 0) | (1 << 10);
GameSession game;
float health, cooldown, age;
int type;
@@ -41,7 +43,7 @@ namespace Ironspore
if (type != 1 && distance > 10)
{
Vector3 step = flat.normalized * Time.deltaTime * (type == 3 ? 1.4f : type == 0 ? 2.5f : 1.2f);
if (!Physics.Raycast(transform.position + Vector3.up, step.normalized, 1.1f, 1, QueryTriggerInteraction.Ignore))
if (!Physics.Raycast(transform.position + Vector3.up, step.normalized, 1.1f, WorldObstructionMask, QueryTriggerInteraction.Ignore))
transform.position += step;
}
if (type == 0)
@@ -54,7 +56,7 @@ namespace Ironspore
if (cooldown <= 0 && distance < 42)
{
Vector3 origin = transform.position + Vector3.up * (type == 3 ? 3.5f : 2);
if (!Physics.Linecast(origin, target, 1, QueryTriggerInteraction.Ignore))
if (!Physics.Linecast(origin, target, WorldObstructionMask, QueryTriggerInteraction.Ignore))
{
var bolt = WorldFactory.Primitive(PrimitiveType.Sphere, "EnemyBolt", origin, Vector3.one * .3f, new Color(1, .35f, .08f), null, false, true);
bolt.AddComponent<EnemyBolt>().Initialize((target - origin).normalized, type == 3 ? 23 : 12);
+32 -6
View File
@@ -1,24 +1,27 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Ironspore
{
public static class WorldFactory
{
static readonly Dictionary<string, Material> materials = new Dictionary<string, Material>();
public static Material Material(Color color, bool glow = false)
public enum Surface { Painted, Concrete, BareMetal }
public static Material Material(Color color, bool glow = false, Surface surface = Surface.Painted)
{
string key = ColorUtility.ToHtmlStringRGB(color) + (glow ? "g" : "");
string key = ColorUtility.ToHtmlStringRGB(color) + (glow ? "g" : "") + (int)surface;
if (materials.TryGetValue(key, out var cached) && cached)
return cached;
var mat = new Material(Shader.Find("Standard"));
mat.color = color;
mat.SetFloat("_Metallic", .3f);
mat.SetFloat("_Glossiness", .35f);
mat.SetFloat("_Metallic", surface == Surface.BareMetal ? .78f : surface == Surface.Concrete ? 0 : .04f);
mat.SetFloat("_Glossiness", surface == Surface.Concrete ? .15f : surface == Surface.BareMetal ? .48f : .32f);
mat.enableInstancing = true;
if (glow)
{
mat.EnableKeyword("_EMISSION");
mat.SetColor("_EmissionColor", color * 1.8f);
mat.SetColor("_EmissionColor", color * 1.25f);
}
materials[key] = mat;
return mat;
@@ -38,7 +41,13 @@ namespace Ironspore
obj.transform.SetParent(parent);
obj.transform.position = position;
obj.transform.localScale = scale;
obj.GetComponent<Renderer>().sharedMaterial = Material(color, glow);
var renderer = obj.GetComponent<Renderer>();
var surface = name == "Ground" || name == "AccessRamp" ? Surface.Concrete : name == "ContainerRib" || name == "CanopyRail" ? Surface.BareMetal : Surface.Painted;
renderer.sharedMaterial = Material(color, glow, surface);
bool detail = name == "FloorSeam" || name == "AmberStripe" || name == "CrateLatch" || name == "RelayScreen" || name == "LandingLight" || name == "CanopyIndicator" || name.EndsWith("WorkFixture");
renderer.shadowCastingMode = detail || name == "EnemyBolt" ? ShadowCastingMode.Off : ShadowCastingMode.On;
renderer.receiveShadows = !glow;
if (name == "EnemyBolt") obj.layer = 10;
if (!collision)
Object.Destroy(obj.GetComponent<Collider>());
return obj;
@@ -59,6 +68,12 @@ namespace Ironspore
dashboard.transform.localPosition = new Vector3(0, -.42f, .75f);
foreach (var child in root.GetComponentsInChildren<Transform>())
child.gameObject.layer = 8;
foreach (var renderer in root.GetComponentsInChildren<Renderer>())
{
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.lightProbeUsage = LightProbeUsage.Off;
renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
}
root.SetActive(false);
return root;
}
@@ -67,11 +82,17 @@ namespace Ironspore
var obj = new GameObject("Sign");
obj.transform.SetParent(parent);
obj.transform.position = position;
obj.layer = 11;
var label = obj.AddComponent<TextMesh>();
label.fontSize = 40;
label.characterSize = .09f;
label.anchor = TextAnchor.MiddleCenter;
label.color = color;
var renderer = obj.GetComponent<Renderer>();
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.receiveShadows = false;
renderer.lightProbeUsage = LightProbeUsage.Off;
renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
obj.AddComponent<LocalizedWorldLabel>().Initialize(key);
obj.AddComponent<FaceCamera>();
}
@@ -79,6 +100,11 @@ namespace Ironspore
{
var obj = new GameObject("ShotTrace");
var line = obj.AddComponent<LineRenderer>();
obj.layer = 11;
line.shadowCastingMode = ShadowCastingMode.Off;
line.receiveShadows = false;
line.lightProbeUsage = LightProbeUsage.Off;
line.reflectionProbeUsage = ReflectionProbeUsage.Off;
line.positionCount = 2;
line.SetPosition(0, from);
line.SetPosition(1, to);
+406
View File
@@ -0,0 +1,406 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
namespace Ironspore
{
// Opt-in player QA. Normal launches never instantiate this component.
public sealed class LightingValidation : MonoBehaviour
{
const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic;
static readonly FieldInfo Yaw = typeof(MechPilot).GetField("yaw", Private);
static readonly FieldInfo Pitch = typeof(MechPilot).GetField("pitch", Private);
static readonly PropertyInfo View = typeof(MechPilot).GetProperty("FirstPerson");
static readonly Vector3[] Route = { new Vector3(3, 0, -18), new Vector3(26, 0, -18), new Vector3(26, 0, 38), new Vector3(3, 0, 38), new Vector3(3, 0, -18), new Vector3(-26, 0, -18), new Vector3(-26, 0, 38), new Vector3(3, 0, 38) };
readonly FrameTiming[] timing = new FrameTiming[1];
readonly List<Sample> samples = new List<Sample>(40000);
readonly Report report = new Report();
GameSession game;
Keyboard keyboard;
Mouse mouse;
Camera benchmarkCamera;
int renderedFrames;
string folder;
bool measuring;
double previous;
ulong lastTiming;
int waypoint;
int visitedWaypoints, segmentWaypoints;
float travelled, segmentTravelled;
bool touring;
Color32[] captured;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Launch()
{
if (!Debug.isDebugBuild || Array.IndexOf(Environment.GetCommandLineArgs(), "-lighting-qa") < 0)
return;
new GameObject("LightingValidation").AddComponent<LightingValidation>();
}
static string Arg(string name, string fallback)
{
var args = Environment.GetCommandLineArgs();
int index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length ? args[index + 1] : fallback;
}
IEnumerator Start()
{
folder = Path.GetFullPath(Arg("-lighting-output", Path.Combine(Application.persistentDataPath, "LightingQA")));
Directory.CreateDirectory(folder);
Directory.CreateDirectory(Path.Combine(folder, "Screenshots"));
if (!Application.productName.Contains("Lighting tree3"))
throw new InvalidOperationException("Lighting QA requires the isolated tree3 product namespace.");
Application.runInBackground = true;
Screen.SetResolution(1920, 1080, FullScreenMode.FullScreenWindow);
InputSystem.settings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus;
while (!GameSession.Active)
yield return null;
game = GameSession.Active;
Camera.onPostRender += CameraRendered;
yield return null;
string quality = Arg("-lighting-quality", "Medium");
int level = Array.IndexOf(QualitySettings.names, quality);
if (level < 0)
throw new ArgumentException("Unknown quality: " + quality);
QualitySettings.SetQualityLevel(level, true);
game.World.BuildHangar();
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = int.Parse(Arg("-lighting-cap", "-1"), CultureInfo.InvariantCulture);
report.tag = Arg("-lighting-tag", "optimized");
report.branch = int.Parse(Arg("-lighting-branch", "0"), CultureInfo.InvariantCulture);
report.routeVersion = "perimeter-and-center-v2";
report.startedUtc = DateTime.UtcNow.ToString("O");
report.gpu = SystemInfo.graphicsDeviceName;
report.api = SystemInfo.graphicsDeviceType.ToString();
report.cpu = SystemInfo.processorType;
report.gpuMemoryMiB = SystemInfo.graphicsMemorySize;
report.systemMemoryMiB = SystemInfo.systemMemorySize;
report.colorSpace = QualitySettings.activeColorSpace.ToString();
report.quality = QualitySettings.names[level];
report.shadowDistance = QualitySettings.shadowDistance;
report.cascades = QualitySettings.shadowCascades;
report.vSync = QualitySettings.vSyncCount;
report.frameCap = Application.targetFrameRate;
report.persistentDataPath = Application.persistentDataPath;
report.frameTimingEnabled = FrameTimingManager.IsFeatureEnabled();
keyboard = InputSystem.AddDevice<Keyboard>("LightingQaKeyboard");
mouse = InputSystem.AddDevice<Mouse>("LightingQaMouse");
game.Input.devices = new InputDevice[] { keyboard, mouse };
yield return new WaitForSecondsRealtime(2);
yield return CaptureScreen("hangar");
game.StartTrial();
yield return new WaitForSecondsRealtime(2);
report.dynamicResolution = game.Pilot.GetComponentInChildren<Camera>().allowDynamicResolution;
report.renderWidthScale = ScalableBufferManager.widthScaleFactor;
report.renderHeightScale = ScalableBufferManager.heightScaleFactor;
report.sunResolution = GameObject.Find("Sun").GetComponent<Light>().shadowCustomResolution;
yield return VisualChecks();
if (Array.IndexOf(Environment.GetCommandLineArgs(), "-lighting-visual-only") < 0)
{
// Use the actual combat world, including the final-stage enemy population.
game.EndSortie(true);
game.BeginSortie();
game.Store.Commit(save => { save.run.seed = 1739; save.run.stage = 4; save.run.branch = report.branch; save.run.armor = 100000; return true; });
game.Resume();
benchmarkCamera = game.Pilot.GetComponentInChildren<Camera>();
report.cameraWidth = benchmarkCamera.pixelWidth;
report.cameraHeight = benchmarkCamera.pixelHeight;
yield return new WaitForSecondsRealtime(2);
foreach (var enemy in FindObjectsByType<EnemyUnit>())
// Keep the same combatants alive throughout every performance sample.
typeof(EnemyUnit).GetField("health", Private).SetValue(enemy, 1000000f);
game.World.CallExtraction();
touring = true;
waypoint = 0;
double warmup = double.Parse(Arg("-lighting-warmup", "60"), CultureInfo.InvariantCulture);
double duration = double.Parse(Arg("-lighting-seconds", "120"), CultureInfo.InvariantCulture);
int repeats = int.Parse(Arg("-lighting-repeats", "3"), CultureInfo.InvariantCulture);
report.warmupSeconds = warmup;
report.sampleSeconds = duration;
Debug.Log("Lighting QA warmup: " + warmup + " seconds");
yield return Wait(warmup);
for (int repeat = 0; repeat < repeats; repeat++)
{
for (int view = 0; view < 2; view++)
{
View.SetValue(game.Pilot, view == 1);
yield return Wait(2);
samples.Clear();
renderedFrames = 0;
segmentWaypoints = visitedWaypoints;
segmentTravelled = travelled;
previous = Time.realtimeSinceStartupAsDouble;
measuring = true;
yield return Wait(duration);
measuring = false;
string name = (view == 0 ? "shoulder" : "cockpit") + "-" + (repeat + 1);
WriteSegment(name);
yield return CaptureScreen(name);
Debug.Log("Lighting QA completed " + name);
}
}
touring = false;
}
report.width = Screen.width;
report.height = Screen.height;
report.finishedUtc = DateTime.UtcNow.ToString("O");
report.completed = true;
report.routeCompleted = game.InField && game.Run.routeCompleted;
SaveReport();
if (game.InField) game.EndSortie(true);
Debug.Log("Lighting QA finished: " + Path.Combine(folder, "report.json"));
if (!Application.isEditor)
Application.Quit();
}
static IEnumerator Wait(double seconds)
{
double until = Time.realtimeSinceStartupAsDouble + seconds;
while (Time.realtimeSinceStartupAsDouble < until)
yield return null;
}
IEnumerator VisualChecks()
{
var pilot = game.Pilot;
var visual = pilot.transform.Find("MechVisual");
var renderers = visual.GetComponentsInChildren<Renderer>(true);
var sun = GameObject.Find("Sun").GetComponent<Light>();
Pitch.SetValue(pilot, 25f);
Yaw.SetValue(pilot, 0f);
View.SetValue(pilot, false);
yield return new WaitForSecondsRealtime(.25f);
yield return CaptureScreen("shoulder");
View.SetValue(pilot, true);
yield return new WaitForSecondsRealtime(.25f);
yield return CaptureScreen("cockpit");
int activeCasters = 0;
foreach (var renderer in renderers)
if (renderer.enabled && renderer.gameObject.activeInHierarchy && renderer.shadowCastingMode == ShadowCastingMode.ShadowsOnly)
activeCasters++;
report.firstPersonShadowOnlyRenderers = activeCasters;
// Hold the real pilot in first-person mode. The observer sees its shadow only.
pilot.enabled = false;
var cockpit = (GameObject)typeof(MechPilot).GetField("cockpit", Private).GetValue(pilot);
cockpit.SetActive(false);
var observerObject = new GameObject("LightingQaObserver");
var observer = observerObject.AddComponent<Camera>();
observer.enabled = false;
observer.cullingMask = ~(1 << 11);
observer.fieldOfView = 52;
observer.nearClipPlane = .1f;
observer.farClipPlane = 140;
observer.transform.position = new Vector3(0, 9, -23);
observer.transform.LookAt(new Vector3(0, 0, -15));
yield return null;
CaptureCamera(observer, "shadow-before-move");
var before = captured;
var controller = pilot.GetComponent<CharacterController>();
controller.enabled = false;
pilot.transform.position += Vector3.right * 2;
controller.enabled = true;
yield return null;
CaptureCamera(observer, "shadow-after-move");
report.movingShadowPixels = DifferentPixels(before, captured);
var oldShadows = sun.shadows;
sun.shadows = LightShadows.None;
yield return null;
CaptureCamera(observer, "sun-shadows-disabled");
var noShadows = captured;
sun.shadows = oldShadows;
yield return null;
CaptureCamera(observer, "sun-shadows-enabled");
report.sunShadowPixels = DifferentPixels(noShadows, captured);
var localObject = GameObject.Find("NavigationWorkLight");
if (localObject)
{
var local = localObject.GetComponent<Light>();
controller.enabled = false;
pilot.transform.position = new Vector3(0, .1f, 18);
controller.enabled = true;
observer.transform.position = new Vector3(7, 10, 13);
observer.transform.LookAt(new Vector3(0, .3f, 21));
yield return new WaitForSecondsRealtime(.25f);
local.enabled = false;
var manager = local.GetComponentInParent<MonoBehaviour>();
if (manager && manager.GetType().Name == "WorldLighting")
manager.enabled = false;
CaptureCamera(observer, "local-off");
var off = captured;
local.enabled = true;
CaptureCamera(observer, "local-on");
var on = captured;
var blocker = GameObject.CreatePrimitive(PrimitiveType.Cube);
blocker.name = "LightingQaTemporaryBlocker";
blocker.transform.position = local.transform.position + local.transform.forward * 1.1f;
blocker.transform.localScale = new Vector3(1.4f, .15f, 1.4f);
blocker.GetComponent<Collider>().enabled = false;
local.enabled = false;
yield return null;
CaptureCamera(observer, "local-blocked-off");
var blockedOff = captured;
local.enabled = true;
CaptureCamera(observer, "local-blocked-on");
int lit = 0, blocked = 0;
for (int i = 0; i < on.Length; i++)
{
int direct = Brightness(on[i]) - Brightness(off[i]);
int obstructed = Brightness(captured[i]) - Brightness(blockedOff[i]);
if (direct > 30) { lit++; if (obstructed < direct * .45f) blocked++; }
}
report.localLitPixels = lit;
report.localBlockedPixels = blocked;
report.localLightCastsShadows = local.shadows != LightShadows.None;
Destroy(blocker);
if (manager) manager.enabled = true;
}
Destroy(observerObject);
pilot.enabled = true;
report.visualPassed = activeCasters > 0 && report.movingShadowPixels > 50 && report.sunShadowPixels > 50 && report.localLightCastsShadows && report.localBlockedPixels > 50;
SaveReport();
}
static int Brightness(Color32 color) => color.r + color.g + color.b;
static int DifferentPixels(Color32[] first, Color32[] second)
{
int changed = 0;
for (int i = 0; i < first.Length; i++)
if (Math.Abs(first[i].r - second[i].r) + Math.Abs(first[i].g - second[i].g) + Math.Abs(first[i].b - second[i].b) > 30)
changed++;
return changed;
}
void CaptureCamera(Camera camera, string name)
{
var target = RenderTexture.GetTemporary(1920, 1080, 24, RenderTextureFormat.ARGB32);
var oldTarget = camera.targetTexture;
var oldActive = RenderTexture.active;
camera.targetTexture = target;
camera.Render();
RenderTexture.active = target;
var image = new Texture2D(1920, 1080, TextureFormat.RGB24, false);
image.ReadPixels(new Rect(0, 0, 1920, 1080), 0, 0);
image.Apply();
captured = image.GetPixels32();
File.WriteAllBytes(Path.Combine(folder, "Screenshots", name + ".png"), image.EncodeToPNG());
camera.targetTexture = oldTarget;
RenderTexture.active = oldActive;
RenderTexture.ReleaseTemporary(target);
Destroy(image);
}
IEnumerator CaptureScreen(string name)
{
yield return new WaitForEndOfFrame();
var image = ScreenCapture.CaptureScreenshotAsTexture();
File.WriteAllBytes(Path.Combine(folder, "Screenshots", name + ".png"), image.EncodeToPNG());
Destroy(image);
}
void Update()
{
if (!touring || !game || !game.InField)
return;
var pilot = game.Pilot;
var goal = Route[waypoint];
var offset = goal - pilot.transform.position;
offset.y = 0;
if (offset.magnitude < 1.2f) { visitedWaypoints++; waypoint = (waypoint + 1) % Route.Length; offset = Route[waypoint] - pilot.transform.position; offset.y = 0; }
Yaw.SetValue(pilot, Mathf.Atan2(offset.x, offset.z) * Mathf.Rad2Deg);
Pitch.SetValue(pilot, 12f);
var beforeMove = pilot.transform.position;
pilot.GetComponent<CharacterController>().Move(offset.normalized * (3.5f * Time.deltaTime));
travelled += Vector3.Distance(beforeMove, pilot.transform.position);
var keys = new KeyboardState();
if ((int)Time.realtimeSinceStartup % 9 == 0) keys.Press(Key.Space);
InputSystem.QueueStateEvent(keyboard, keys);
var buttons = new MouseState();
if ((int)Time.realtimeSinceStartup % 3 == 0) buttons = buttons.WithButton(MouseButton.Left);
InputSystem.QueueStateEvent(mouse, buttons);
}
void LateUpdate()
{
FrameTimingManager.CaptureFrameTimings();
if (!measuring)
return;
double now = Time.realtimeSinceStartupAsDouble;
var sample = new Sample { wallMs = (now - previous) * 1000 };
previous = now;
if (FrameTimingManager.GetLatestTimings(1, timing) > 0 && timing[0].frameStartTimestamp != lastTiming)
{
lastTiming = timing[0].frameStartTimestamp;
sample.cpuMs = timing[0].cpuFrameTime;
sample.gpuMs = timing[0].gpuFrameTime;
sample.mainMs = timing[0].cpuMainThreadFrameTime;
sample.renderMs = timing[0].cpuRenderThreadFrameTime;
sample.presentWaitMs = timing[0].cpuMainThreadPresentWaitTime;
}
samples.Add(sample);
}
void CameraRendered(Camera camera)
{
if (measuring && camera == benchmarkCamera) renderedFrames++;
}
void WriteSegment(string name)
{
var wall = new List<double>(samples.Count);
var gpu = new List<double>(samples.Count);
var cpu = new List<double>(samples.Count);
var csv = new StringBuilder("wallMs,cpuMs,gpuMs,mainMs,renderMs,presentWaitMs\n");
double total = 0;
foreach (var s in samples)
{
wall.Add(s.wallMs); total += s.wallMs;
if (s.gpuMs > 0) gpu.Add(s.gpuMs);
if (s.cpuMs > 0) cpu.Add(s.cpuMs);
csv.AppendFormat(CultureInfo.InvariantCulture, "{0:F5},{1:F5},{2:F5},{3:F5},{4:F5},{5:F5}\n", s.wallMs, s.cpuMs, s.gpuMs, s.mainMs, s.renderMs, s.presentWaitMs);
}
wall.Sort(); gpu.Sort(); cpu.Sort();
File.WriteAllText(Path.Combine(folder, name + ".csv"), csv.ToString());
report.segments.Add(new Segment { name = name, frames = samples.Count, cameraRenderFrames = renderedFrames, meanFps = samples.Count * 1000.0 / total, p50Ms = Percentile(wall, .5), p95Ms = Percentile(wall, .95), p99Ms = Percentile(wall, .99), gpuP95Ms = Percentile(gpu, .95), cpuP95Ms = Percentile(cpu, .95), gpuSamples = gpu.Count, cpuSamples = cpu.Count, allocatedMiB = Profiler.GetTotalAllocatedMemoryLong() / 1048576.0, enemyCount = FindObjectsByType<EnemyUnit>().Length, width = Screen.width, height = Screen.height, waypointsVisited = visitedWaypoints - segmentWaypoints, distanceTravelled = travelled - segmentTravelled, passed = Screen.width == 1920 && Screen.height == 1080 && Percentile(wall, .95) <= 16.7 && visitedWaypoints - segmentWaypoints >= 4 && renderedFrames >= samples.Count * .98 });
SaveReport();
}
static double Percentile(List<double> sorted, double fraction) => sorted.Count == 0 ? 0 : sorted[Math.Max(0, (int)Math.Ceiling(sorted.Count * fraction) - 1)];
void SaveReport() => File.WriteAllText(Path.Combine(folder, "report.json"), JsonUtility.ToJson(report, true));
void OnDestroy()
{
Camera.onPostRender -= CameraRendered;
if (keyboard != null && keyboard.added) InputSystem.RemoveDevice(keyboard);
if (mouse != null && mouse.added) InputSystem.RemoveDevice(mouse);
}
[Serializable] struct Sample { public double wallMs, cpuMs, gpuMs, mainMs, renderMs, presentWaitMs; }
[Serializable] sealed class Segment
{
public string name;
public int frames, width, height, gpuSamples, cpuSamples, enemyCount, waypointsVisited, cameraRenderFrames;
public double meanFps, p50Ms, p95Ms, p99Ms, gpuP95Ms, cpuP95Ms, allocatedMiB, distanceTravelled;
public bool passed;
}
[Serializable] sealed class Report
{
public string tag, startedUtc, finishedUtc, gpu, api, cpu, colorSpace, quality, persistentDataPath, routeVersion;
public int width, height, gpuMemoryMiB, systemMemoryMiB, cascades, vSync, frameCap, firstPersonShadowOnlyRenderers, movingShadowPixels, sunShadowPixels, localLitPixels, localBlockedPixels, sunResolution, cameraWidth, cameraHeight, branch;
public float shadowDistance, renderWidthScale, renderHeightScale;
public double warmupSeconds, sampleSeconds;
public bool completed, frameTimingEnabled, localLightCastsShadows, visualPassed, dynamicResolution, routeCompleted;
public List<Segment> segments = new List<Segment>();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 833538f031bef3b448f712042795cb10
+19 -2
View File
@@ -1,4 +1,5 @@
using UnityEngine;
using UnityEngine.Rendering;
namespace Ironspore
{
@@ -31,6 +32,9 @@ namespace Ironspore
Camera view;
Transform visual;
GameObject cockpit;
Renderer[] worldRenderers;
ShadowCastingMode[] worldShadowModes;
bool displayedFirstPerson;
float yaw, pitch, vertical, boostRemaining, boostCooldown, shotCooldown, sideCooldown;
Vector3 boostDirection;
bool freshTrigger = true;
@@ -53,7 +57,12 @@ namespace Ironspore
visual.localPosition = Vector3.zero;
foreach (var t in visual.GetComponentsInChildren<Transform>())
t.gameObject.layer = 8;
worldRenderers = visual.GetComponentsInChildren<Renderer>(true);
worldShadowModes = new ShadowCastingMode[worldRenderers.Length];
for (int i = 0; i < worldRenderers.Length; i++)
worldShadowModes[i] = worldRenderers[i].shadowCastingMode;
var cam = new GameObject("PilotCamera");
cam.tag = "MainCamera";
cam.transform.SetParent(transform);
view = cam.AddComponent<Camera>();
view.nearClipPlane = .08f;
@@ -165,8 +174,14 @@ namespace Ironspore
view.transform.SetPositionAndRotation(desired, rotation);
float fov = game.MenuOpen ? 70 : game.Action("Aim").IsPressed() ? 50 : FirstPerson ? 78 : 70;
view.fieldOfView = Mathf.Lerp(view.fieldOfView, fov, Time.unscaledDeltaTime * 12);
visual.gameObject.SetActive(!FirstPerson);
cockpit.SetActive(FirstPerson);
if (displayedFirstPerson != FirstPerson)
{
displayedFirstPerson = FirstPerson;
for (int i = 0; i < worldRenderers.Length; i++)
if (worldRenderers[i])
worldRenderers[i].shadowCastingMode = FirstPerson ? ShadowCastingMode.ShadowsOnly : worldShadowModes[i];
cockpit.SetActive(FirstPerson);
}
}
void Shoot(bool secondary)
{
@@ -201,6 +216,8 @@ namespace Ironspore
enemy.Damage(part.damage * (1 + game.Run.upgrades * .15f));
lastHitTime = Time.time;
}
else if (game.Trial && hit.collider.GetComponentInParent<RangeTarget>() is RangeTarget plate && plate.Hit())
lastHitTime = Time.time;
}
WorldFactory.Tracer(muzzle, target, secondary ? new Color(1, .65f, .2f) : new Color(.2f, 1, .85f));
GameAudio.Cue(secondary ? 140 : 240, .06f, .07f);
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f503296d8ae461f41a8229186bbad63e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,178 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Ironspore
{
public sealed class WorldLighting : MonoBehaviour
{
readonly List<Light> fixtures = new List<Light>(2);
WorldLightingProfile profile;
Material sky;
ReflectionProbe environment;
Camera observer;
bool ownsProfile;
int shadowBudget;
bool captureQueued, capturing;
float captureAfter;
public bool ReflectionReady { get; private set; }
public void Initialize(WorldLightingProfile settings, bool hangar)
{
profile = settings;
if (!profile)
{
profile = ScriptableObject.CreateInstance<WorldLightingProfile>();
ownsProfile = true;
}
int level = QualitySettings.GetQualityLevel();
shadowBudget = level <= 1 ? 0 : level >= 4 ? 2 : 1;
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = profile.ambientSky;
RenderSettings.ambientEquatorColor = profile.ambientEquator;
RenderSettings.ambientGroundColor = profile.ambientGround;
RenderSettings.fog = !hangar;
RenderSettings.fogMode = FogMode.Linear;
RenderSettings.fogColor = profile.fogColor;
RenderSettings.fogStartDistance = profile.fogStart;
RenderSettings.fogEndDistance = profile.fogEnd;
RenderSettings.reflectionIntensity = profile.reflectionIntensity;
var sourceSky = RenderSettings.skybox;
sky = sourceSky ? new Material(sourceSky) : new Material(Shader.Find("Skybox/Procedural"));
sky.name = "RuntimeDaySky";
if (sky.HasProperty("_Exposure")) sky.SetFloat("_Exposure", profile.skyExposure);
RenderSettings.skybox = sky;
var sunObject = new GameObject("Sun");
sunObject.transform.SetParent(transform, false);
sunObject.transform.rotation = Quaternion.Euler(profile.sunEuler);
var sun = sunObject.AddComponent<Light>();
sun.type = LightType.Directional;
sun.color = profile.sunColor;
sun.intensity = hangar ? profile.hangarSunIntensity : profile.sunIntensity;
sun.renderMode = LightRenderMode.ForcePixel;
sun.shadows = level <= 1 ? LightShadows.Hard : LightShadows.Soft;
sun.shadowStrength = 1;
sun.shadowBias = profile.bias;
sun.shadowNormalBias = profile.normalBias;
sun.shadowNearPlane = profile.shadowNearPlane;
sun.shadowCustomResolution = level <= 1 ? 1024 : level >= 4 ? 4096 : 2048;
RenderSettings.sun = sun;
}
public void CompleteEnvironment(Camera camera, bool hangar)
{
observer = camera;
if (hangar)
{
WorldFactory.Box("HangarWorkFixture", new Vector3(2, 8.2f, 1), new Vector3(2, .18f, .65f), new Color(.8f, .87f, 1), transform, false, true);
AddSpot("HangarWorkLight", new Vector3(2, 8, 1), new Vector3(2, 1, 2), new Color(.82f, .9f, 1), profile.hangarIntensity, 17, 84);
}
else
{
WorldFactory.Box("NavigationWorkFixture", new Vector3(0, 3.4f, 22.7f), new Vector3(1.2f, .16f, .4f), new Color(.55f, 1, .85f), transform, false, true);
AddSpot("NavigationWorkLight", new Vector3(0, 3.25f, 22.3f), new Vector3(0, 0, 20.6f), new Color(.65f, 1, .85f), profile.navigationIntensity, 11, 78);
WorldFactory.Box("ExtractionLampPost", new Vector3(-22.4f, 3.1f, -8), new Vector3(.16f, 6.2f, .16f), new Color(.2f, .25f, .28f), transform, false);
WorldFactory.Box("ExtractionLampArm", new Vector3(-20.7f, 6.1f, -8), new Vector3(3.5f, .15f, .18f), new Color(.2f, .25f, .28f), transform, false);
WorldFactory.Box("ExtractionWorkFixture", new Vector3(-19, 6, -8), new Vector3(1, .18f, .6f), new Color(1, .77f, .45f), transform, false, true);
AddSpot("ExtractionWorkLight", new Vector3(-19, 5.8f, -8), new Vector3(-19, 0, -8), new Color(1, .8f, .55f), profile.extractionIntensity, 13, 76);
}
var probeObject = new GameObject("EnvironmentReflection");
probeObject.transform.SetParent(transform, false);
probeObject.transform.position = hangar ? new Vector3(2, 3, 2) : new Vector3(0, 3, 8);
environment = probeObject.AddComponent<ReflectionProbe>();
environment.mode = ReflectionProbeMode.Realtime;
environment.refreshMode = ReflectionProbeRefreshMode.ViaScripting;
environment.timeSlicingMode = ReflectionProbeTimeSlicingMode.IndividualFaces;
environment.resolution = QualitySettings.GetQualityLevel() >= 4 ? 256 : 128;
environment.hdr = true;
environment.boxProjection = true;
environment.blendDistance = 5;
environment.size = hangar ? new Vector3(66, 18, 45) : new Vector3(74, 20, 104);
// The captured environment must not freeze actors, transient props or world labels.
environment.cullingMask = ~((1 << 8) | (1 << 9) | (1 << 10) | (1 << 11));
environment.nearClipPlane = .2f;
environment.farClipPlane = 150;
environment.intensity = profile.reflectionIntensity;
SelectLights();
StartCoroutine(CaptureEnvironment());
}
void AddSpot(string name, Vector3 position, Vector3 target, Color color, float intensity, float range, float angle)
{
var obj = new GameObject(name);
obj.transform.SetParent(transform, false);
obj.transform.position = position;
obj.transform.LookAt(target);
var light = obj.AddComponent<Light>();
light.type = LightType.Spot;
light.renderMode = LightRenderMode.ForcePixel;
light.color = color;
light.intensity = intensity;
light.range = range;
light.spotAngle = angle;
light.innerSpotAngle = angle * .65f;
light.shadows = LightShadows.Soft;
light.shadowCustomResolution = 1024;
light.shadowBias = profile.bias;
light.shadowNormalBias = profile.normalBias;
light.shadowNearPlane = profile.shadowNearPlane;
light.enabled = false;
fixtures.Add(light);
}
void LateUpdate()
{
SelectLights();
if (environment && captureQueued && !capturing && Time.unscaledTime >= captureAfter)
StartCoroutine(CaptureEnvironment());
}
void SelectLights()
{
if (!observer)
return;
Vector3 position = observer.transform.position;
float limit = profile.localLightCullDistance * profile.localLightCullDistance;
Light nearest = null;
float distance = limit;
foreach (var light in fixtures)
{
float candidate = (light.transform.position - position).sqrMagnitude;
if (candidate < distance) { distance = candidate; nearest = light; }
}
foreach (var light in fixtures)
{
bool active = shadowBudget > 0 && (light == nearest || shadowBudget > 1 && (light.transform.position - position).sqrMagnitude < limit);
if (light.enabled == active)
continue;
light.enabled = active;
captureQueued = true;
}
}
IEnumerator CaptureEnvironment()
{
capturing = true;
captureQueued = false;
yield return null;
DynamicGI.UpdateEnvironment();
int render = environment.RenderProbe();
while (environment && !environment.IsFinishedRendering(render))
yield return null;
ReflectionReady = environment != null;
capturing = false;
captureAfter = Time.unscaledTime + 8;
}
void OnDestroy()
{
if (sky)
{
if (RenderSettings.skybox == sky) RenderSettings.skybox = null;
Destroy(sky);
}
if (ownsProfile && profile) Destroy(profile);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f71c474a8bca074aa2274b7a4054680
@@ -0,0 +1,31 @@
using UnityEngine;
namespace Ironspore
{
[CreateAssetMenu(menuName = "Ironspore/World Lighting Profile")]
public sealed class WorldLightingProfile : ScriptableObject
{
[Header("Daylight")]
public Vector3 sunEuler = new Vector3(48, -35, 0);
public Color sunColor = new Color(1, .94f, .84f);
[Min(0)] public float sunIntensity = 1.2f;
[Min(0)] public float hangarSunIntensity = .8f;
public Color ambientSky = new Color(.24f, .30f, .38f);
public Color ambientEquator = new Color(.12f, .16f, .19f);
public Color ambientGround = new Color(.045f, .055f, .065f);
public Color fogColor = new Color(.15f, .21f, .26f);
public float fogStart = 38;
public float fogEnd = 145;
[Range(0, 2)] public float skyExposure = .85f;
[Range(0, 1)] public float reflectionIntensity = .7f;
[Header("Shadows")]
[Range(0, 2)] public float bias = .08f;
[Range(0, 2)] public float normalBias = .4f;
[Min(.01f)] public float shadowNearPlane = .2f;
[Header("Industrial fixtures")]
[Min(0)] public float navigationIntensity = 6;
[Min(0)] public float extractionIntensity = 6;
[Min(0)] public float hangarIntensity = 5;
[Min(1)] public float localLightCullDistance = 50;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 90562a900c2114d48bd80969332d5ec6
+2 -1
View File
@@ -19,6 +19,7 @@ namespace Ironspore
public UnityEngine.UIElements.StyleSheet uiStyle;
public UnityEngine.UIElements.PanelSettings uiPanel;
public LocalizationCatalog localization;
public WorldLightingProfile lightingProfile;
public bool startInRange;
public ProfileStore Store
{
@@ -75,7 +76,7 @@ namespace Ironspore
{
Active = this;
GameLocalization.Initialize(localization);
Application.targetFrameRate = 120;
Application.targetFrameRate = 60;
mainStore = new ProfileStore(Path.Combine(Application.persistentDataPath, "Ironspore", "profile.json"));
Store = mainStore;
Input = Instantiate(inputTemplate);
+215
View File
@@ -0,0 +1,215 @@
using UnityEngine;
namespace Ironspore
{
public sealed class TrainingCourse : MonoBehaviour
{
static readonly Color Steel = new Color(.19f, .28f, .32f);
static readonly Color Amber = new Color(.95f, .48f, .12f);
static readonly Color Green = new Color(.16f, .92f, .65f);
GameSession game;
Renderer firstLamp, upperLamp;
RangeTarget plate, movingPlate;
Vector3 previousPosition;
int gateCount, hits;
float startedAt, finishSeconds;
static readonly Vector3 StartGate = new Vector3(19, 0, -15);
static readonly Vector3 UpperGate = new Vector3(19, 4.3f, 4);
static readonly Vector3 Landing = new Vector3(19, 4.3f, 22);
public string Instruction
{
get
{
Vector3 position = game.World.Player.transform.position;
if (game.World.TransportReady) return GameLocalization.Text("range.board");
if (position.x < -16 && (position.z > 6 || Mathf.Abs(position.z + 8) < 4))
return GameLocalization.Text(game.Run.podUsed ? "range.recoverySent" : "range.recoveryGuide");
if (position.x < -7) return GameLocalization.Text("range.targetGuide", hits);
if (gateCount == 3) return GameLocalization.Text("range.finish", finishSeconds);
if (gateCount == 2) return GameLocalization.Text("range.brake");
if (gateCount == 1) return GameLocalization.Text("range.gate");
return GameLocalization.Text("range.choose");
}
}
public void Build(GameSession session)
{
game = session;
previousPosition = game.World.Player.transform.position;
firstLamp = Gate(StartGate);
upperLamp = Gate(UpperGate);
WorldFactory.Label("range.start", StartGate + Vector3.up * 5.95f, Green, transform);
WorldFactory.Label("range.upper", UpperGate + Vector3.up * 5.95f, Green, transform);
WorldFactory.Label("range.landing", Landing + Vector3.up * 1.7f, Green, transform);
for (int i = -1; i <= 1; i++)
WorldFactory.Box("LandingStripe", Landing + new Vector3(0,.02f,i * 1.35f), new Vector3(4.7f,.035f,.17f), Amber, transform, false);
plate = Target(new Vector3(-13, 0, -2), false);
movingPlate = Target(new Vector3(-25, 0, -2), true);
WorldFactory.Box("TraverseRail", new Vector3(-25,.12f,-2), new Vector3(9,.24f,.8f), Steel, transform);
foreach (int side in new[] { -1, 1 })
WorldFactory.Box("RailStop", new Vector3(-25 + side * 4.5f,.4f,-2), new Vector3(.35f,.8f,1.3f), Amber, transform);
WorldFactory.Box("FiringLine", new Vector3(-19,.025f,-15), new Vector3(12,.04f,.15f), Amber, transform, false);
WorldFactory.Label("range.gallery", new Vector3(-19,4.6f,-2), Amber, transform);
WorldFactory.Label("range.salvage", new Vector3(-24,4,14), Green, transform);
var desk = WorldFactory.Box("RangeResetDesk", new Vector3(-12, .85f, -15), new Vector3(1.4f, 1.7f, .85f), Steel, transform);
WorldFactory.Box("ResetSwitch", desk.transform.position + Vector3.up * .92f, new Vector3(.8f,.14f,.5f), Green, transform, false, true);
var reset = desk.AddComponent<RangeReset>();
reset.kind = 4;
reset.course = this;
WorldFactory.Label("range.reset", desk.transform.position + Vector3.up * 1.6f, Green, transform);
}
Renderer Gate(Vector3 at)
{
foreach (int side in new[] { -1, 1 })
{
Vector3 foot = at + Vector3.right * side * 3.1f;
WorldFactory.Box("GateFoot", foot + Vector3.up * .14f, new Vector3(.8f,.28f,1.2f), Steel, transform);
WorldFactory.Box("GatePost", foot + Vector3.up * 2.65f, new Vector3(.35f,5.3f,.5f), Steel, transform);
WorldFactory.Box("GateStripe", foot + Vector3.up * 2, new Vector3(.38f,.8f,.53f), Amber, transform, false);
}
return WorldFactory.Box("GateLamp", at + Vector3.up * 5.35f, new Vector3(6.6f,.26f,.45f), Amber, transform, false, true).GetComponent<Renderer>();
}
RangeTarget Target(Vector3 at, bool moves)
{
var carriage = new GameObject(moves ? "TraversingTarget" : "FixedTarget").transform;
carriage.SetParent(transform, false);
carriage.position = at;
var stand = WorldFactory.Box("TargetStand", at + Vector3.up * .55f, new Vector3(2.2f,1.1f,.85f), Steel, carriage);
if (moves) stand.layer = 10;
var pivot = new GameObject("RangeTarget").transform;
pivot.SetParent(carriage, false);
pivot.position = at + Vector3.up * 1.15f;
var panel = WorldFactory.Box("StrikePlate", pivot.position + Vector3.up * 1.15f, new Vector3(2.1f,2.3f,.22f), Amber, pivot);
var bull = WorldFactory.Primitive(PrimitiveType.Cylinder, "Bullseye", panel.transform.position + Vector3.back * .14f, new Vector3(1.25f,.025f,1.25f), Steel, pivot, false, false);
bull.transform.rotation = Quaternion.Euler(90,0,0);
var lamp = WorldFactory.Box("TargetLamp", bull.transform.position + Vector3.back * .04f, new Vector3(.52f,.52f,.05f), Amber, pivot, false, true);
foreach (Transform part in pivot.GetComponentsInChildren<Transform>()) part.gameObject.layer = 10;
var target = pivot.gameObject.AddComponent<RangeTarget>();
target.Initialize(this, panel.GetComponent<Collider>(), lamp.GetComponent<Renderer>(), moves);
return target;
}
void Update()
{
if (!game.Pilot || game.Paused) return;
Vector3 position = game.Pilot.transform.position;
if ((gateCount == 0 || gateCount == 3) && Crossed(previousPosition, position, StartGate))
{
gateCount = 1;
startedAt = Time.time;
SetLamp(firstLamp, true);
SetLamp(upperLamp, false);
game.Notify("range.gate");
}
else if (gateCount == 1 && Crossed(previousPosition, position, UpperGate))
{
gateCount = 2;
SetLamp(upperLamp, true);
game.Notify("range.brake");
}
else if (gateCount == 2 && Mathf.Abs(position.x - Landing.x) < 2.65f &&
Mathf.Abs(position.z - Landing.z) < 2.4f && Mathf.Abs(position.y - Landing.y) < .6f &&
game.Pilot.Grounded && game.Pilot.HorizontalSpeed < 1)
{
gateCount = 3;
finishSeconds = Time.time - startedAt;
game.Notify("range.finish", finishSeconds);
GameAudio.Cue(620,.2f);
}
previousPosition = position;
}
static bool Crossed(Vector3 from, Vector3 to, Vector3 gate)
{
if (from.z >= gate.z || to.z < gate.z) return false;
Vector3 crossing = Vector3.Lerp(from, to, (gate.z - from.z) / (to.z - from.z));
return Mathf.Abs(crossing.x - gate.x) < 2.9f && crossing.y >= gate.y - .5f && crossing.y < gate.y + 5;
}
internal static void SetLamp(Renderer renderer, bool on)
{
renderer.sharedMaterial = WorldFactory.Material(on ? Green : Amber, true);
}
internal void Hit()
{
game.Notify("range.hit", ++hits);
GameAudio.Cue(880, .12f);
}
public void ResetPractice()
{
hits = gateCount = 0;
previousPosition = game.Pilot.transform.position;
SetLamp(firstLamp, false);
SetLamp(upperLamp, false);
plate.ResetTarget();
movingPlate.ResetTarget();
game.Notify("range.ready");
}
}
public sealed class RangeTarget : MonoBehaviour
{
TrainingCourse course;
Collider strike;
Renderer lamp;
bool down, moving;
float travelTime, holdTime;
Vector3 origin;
internal void Initialize(TrainingCourse owner, Collider collider, Renderer indicator, bool moves)
{
course = owner;
strike = collider;
lamp = indicator;
moving = moves;
origin = transform.parent.localPosition;
}
public bool Hit()
{
if (down || holdTime > 0) return false;
down = !moving;
if (moving) holdTime = 1f;
else strike.enabled = false;
TrainingCourse.SetLamp(lamp, true);
course.Hit();
return true;
}
void Update()
{
if (down) transform.localRotation = Quaternion.RotateTowards(transform.localRotation, Quaternion.Euler(87,0,0), 240 * Time.deltaTime);
if (!moving) return;
if (holdTime > 0)
{
holdTime -= Time.deltaTime;
if (holdTime <= 0) TrainingCourse.SetLamp(lamp, false);
return;
}
travelTime += Time.deltaTime;
transform.parent.localPosition = origin + Vector3.right * (3.1f * Mathf.Sin(travelTime * .85f));
}
internal void ResetTarget()
{
down = false;
holdTime = travelTime = 0;
transform.parent.localPosition = origin;
strike.enabled = true;
transform.localRotation = Quaternion.identity;
TrainingCourse.SetLamp(lamp, false);
}
}
public sealed class RangeReset : WorldInteractable
{
public TrainingCourse course;
public override string Prompt => GameLocalization.Text("range.reset");
public override void Activate() => course.ResetPractice();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c0607d512ea805f459c7d25872379957
+53
View File
@@ -0,0 +1,53 @@
using System.Collections;
using UnityEngine;
namespace Ironspore
{
public sealed class TrainingCrate : WorldInteractable
{
Transform hinge;
Transform latch;
const float UnlatchSeconds = .16f;
const float LiftSeconds = .55f;
bool started;
public void Build(Transform world, Vector3 basePosition, bool part, bool showLabel)
{
Color accent = part ? new Color(.98f,.53f,.17f) : new Color(.16f,.85f,.67f);
hinge = new GameObject("CaseHinge").transform;
hinge.SetParent(world, false);
hinge.position = basePosition + new Vector3(0,1.36f,.71f);
var lid = WorldFactory.Box("CaseLid", hinge.position + Vector3.back * .72f, new Vector3(1.96f,.16f,1.49f), accent, hinge, false);
lid.layer = 10;
foreach (int side in new[] { -1, 1 })
WorldFactory.Box("CaseStrap", lid.transform.position + new Vector3(side * .66f,.1f,0), new Vector3(.14f,.07f,1.42f), Color.gray, hinge, false).layer = 10;
latch = new GameObject("CaseLatchPivot").transform;
latch.SetParent(hinge, false);
latch.localPosition = new Vector3(0, 0, -1.49f);
WorldFactory.Box("CaseLatch", latch.position + Vector3.down * .13f, new Vector3(.38f, .26f, .09f), Color.gray, latch, false).layer = 10;
WorldFactory.Box("CaseLiner", basePosition + Vector3.up * 1.30f, new Vector3(1.58f, .02f, 1.18f), new Color(.07f, .10f, .11f), world, false).layer = 10;
if (showLabel)
WorldFactory.Label(part ? "range.parts" : "range.material", basePosition + Vector3.up * 2.4f, accent, world);
}
public override void RefreshLabel()
{
if (collected && !started)
{
started = true;
StartCoroutine(OpenLid());
}
}
IEnumerator OpenLid()
{
float elapsed = 0;
while (elapsed < UnlatchSeconds + LiftSeconds)
{
yield return null;
elapsed += Time.deltaTime;
latch.localRotation = Quaternion.Euler(Mathf.SmoothStep(0, 78, elapsed / UnlatchSeconds), 0, 0);
hinge.localRotation = Quaternion.Euler(Mathf.SmoothStep(0, 92, (elapsed - UnlatchSeconds) / LiftSeconds), 0, 0);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 165d2c6b7407be447abcb69a251294c5
+29 -23
View File
@@ -23,10 +23,12 @@ namespace Ironspore
}
Transform root;
GameSession game;
WorldLighting lighting;
public TrainingCourse Course { get; private set; }
Vector3 extractionPosition = new Vector3(-19, 0, -8);
GameObject transport;
void ResetWorld()
void ResetWorld(bool hangar)
{
game = GetComponent<GameSession>();
if (root)
@@ -38,19 +40,10 @@ namespace Ironspore
ExtractionRemaining = -1;
TransportReady = false;
Focused = null;
Course = null;
Escort = null;
RenderSettings.ambientLight = new Color(.27f, .34f, .38f);
RenderSettings.fog = true;
RenderSettings.fogColor = new Color(.09f, .14f, .18f);
RenderSettings.fogDensity = .009f;
var sun = new GameObject("Sun");
sun.transform.SetParent(root);
sun.transform.rotation = Quaternion.Euler(48, -35, 0);
var light = sun.AddComponent<Light>();
light.type = LightType.Directional;
light.intensity = 1.7f;
light.color = new Color(1, .87f, .72f);
light.shadows = LightShadows.Soft;
lighting = root.gameObject.AddComponent<WorldLighting>();
lighting.Initialize(game.lightingProfile, hangar);
WorldFactory.Box("Ground", new Vector3(0, -.3f, 8), new Vector3(72, .6f, 100), new Color(.11f, .16f, .18f), root);
for (int x = -30; x <= 30; x += 5)
WorldFactory.Box("FloorSeam", new Vector3(x, .008f, 8), new Vector3(.035f, .015f, 96), new Color(.18f, .24f, .26f), root, false);
@@ -68,7 +61,7 @@ namespace Ironspore
}
public void BuildHangar()
{
ResetWorld();
ResetWorld(true);
for (int i = 0; i < 3; i++)
{
var model = Instantiate(game.mechModel, root);
@@ -79,16 +72,18 @@ namespace Ironspore
WorldFactory.Cylinder("DisplayPad", new Vector3(i * 4 - 2, .05f, 2), new Vector3(3, .1f, 3), game.chassis[i].accent, root, false);
}
var camera = new GameObject("HangarCamera");
camera.tag = "MainCamera";
camera.transform.SetParent(root);
camera.AddComponent<Camera>().fieldOfView = 47;
camera.transform.position = new Vector3(11, 4.3f, -11);
camera.transform.LookAt(new Vector3(2, 1.5f, 2));
camera.AddComponent<AudioListener>();
WorldFactory.Box("RoofBeam", new Vector3(0, 9, 4), new Vector3(62, 1.3f, 2), new Color(.16f, .22f, .24f), root);
lighting.CompleteEnvironment(camera.GetComponent<Camera>(), true);
}
public void BuildWarzone()
{
ResetWorld();
ResetWorld(false);
var run = game.Run;
var avatar = new GameObject("PlayerMech");
avatar.transform.SetParent(root);
@@ -111,13 +106,14 @@ namespace Ironspore
for (int i = 0; i < 6; i++)
{
Vector3 pos = new Vector3(i % 2 == 0 ? -8 : 9, 0, -6 + i * 6);
bool demo = game.Trial && (i == 1 || i == 2);
if (demo) pos = new Vector3(-24, 0, i == 1 ? 9 : 18);
string key = "zone" + run.stage + "_crate" + i;
if (run.searched.Contains(key))
continue;
string item = i == 0 ? "blueprint_boost_efficient" : i == 3 ? "blueprint_coil_capacitor" : i == 5 ? "blueprint_reactive_guard" : (i + run.biome) % 2 == 0 ? "alloy" : "cells";
var crate = WorldFactory.Box("SalvageCrate", pos + Vector3.up * .65f, new Vector3(1.8f, 1.3f, 1.4f), new Color(.24f, .42f, .39f), root);
WorldFactory.Box("CrateLatch", pos + new Vector3(0, 1.32f, 0), new Vector3(1.4f, .08f, .85f), new Color(.15f, .95f, .73f), root, false, true);
var interaction = crate.AddComponent<WorldInteractable>();
var interaction = crate.AddComponent<TrainingCrate>();
interaction.kind = 0;
interaction.key = key;
interaction.itemId = item;
@@ -125,6 +121,7 @@ namespace Ironspore
interaction.offersPart = i == 2 || i == 4;
interaction.partSlot = i == 2 ? 2 : 0;
interaction.partIndex = run.stage % 2 == 0 ? 1 : 2;
interaction.Build(root, pos, interaction.offersPart, demo);
interaction.RefreshLabel();
}
var objective = WorldFactory.Box("NavigationRelay", new Vector3(0, 1.5f, 23), new Vector3(2.2f, 3, 1.6f), new Color(.21f, .33f, .36f), root);
@@ -149,7 +146,7 @@ namespace Ironspore
WorldFactory.Label("world.relay", new Vector3(0, 5, 23), new Color(.25f, 1, .85f), root);
int count = game.Trial ? 2 : 3 + run.stage;
for (int i = 0; i < count; i++)
SpawnEnemy(new Vector3((i % 2 == 0 ? -1 : 1) * (4 + i % 3 * 3), .1f, 6 + i * 5), i % 3, i);
SpawnEnemy(new Vector3((i % 2 == 0 ? -1 : 1) * (4 + i % 3 * 3), .1f, (game.Trial ? 39 : 6) + i * 5), i % 3, i);
if (!game.Trial && run.stage == 4)
{
objective.SetActive(false);
@@ -160,8 +157,15 @@ namespace Ironspore
var cart = WorldFactory.Box("EscortEngineer", new Vector3(0, 1.3f, 0), new Vector3(3, 2.6f, 4), new Color(.2f, .55f, .5f), root);
Escort = cart.AddComponent<EscortUnit>();
Escort.Initialize(game);
cart.layer = 10;
}
}
if (game.Trial)
{
Course = root.gameObject.AddComponent<TrainingCourse>();
Course.Build(game);
}
lighting.CompleteEnvironment(Player.GetComponentInChildren<Camera>(), false);
}
void SpawnEnemy(Vector3 pos, int type, int number)
{
@@ -198,7 +202,8 @@ namespace Ironspore
{
TransportReady = true;
transport = WorldFactory.Box("RecoveryTransport", extractionPosition + new Vector3(0, 5.5f, 0), new Vector3(7, 1.8f, 10), new Color(.24f, .3f, .32f), root, false);
WorldFactory.Box("TransportWing", transport.transform.position, new Vector3(14, .5f, 3), new Color(.32f, .4f, .41f), root, false);
transport.layer = 10;
WorldFactory.Box("TransportWing", transport.transform.position, new Vector3(14, .5f, 3), new Color(.32f, .4f, .41f), root, false).layer = 10;
game.Notify("notice.transportReady");
GameAudio.Cue(950, .3f);
}
@@ -220,18 +225,19 @@ namespace Ironspore
public void LaunchPod(Vector3 position)
{
var pod = WorldFactory.Box("CargoPod", position + Vector3.up * 2, new Vector3(1.4f, 1, 1.4f), new Color(.1f, .8f, .6f), root, false, true);
pod.layer = 10;
pod.AddComponent<PodFlight>();
}
}
public sealed class WorldInteractable : MonoBehaviour
public class WorldInteractable : MonoBehaviour
{
public int kind;
public string key, itemId;
public int amount;
public bool collected, offersPart;
public int partSlot, partIndex;
public string Prompt
public virtual string Prompt
{
get
{
@@ -245,12 +251,12 @@ namespace Ironspore
return GameLocalization.Text(g.World.TransportReady ? "prompt.board" : g.World.ExtractionRemaining >= 0 ? "prompt.inbound" : "prompt.call");
}
}
public void RefreshLabel()
public virtual void RefreshLabel()
{
if (collected)
transform.localScale = Vector3.one * .5f;
}
public void Activate()
public virtual void Activate()
{
var g = GameSession.Active;
if (kind == 0 && !collected)
+2 -1
View File
@@ -385,7 +385,8 @@ namespace Ironspore
if (status != null)
status.text = L("ui.hud.status", game.Trial ? L("ui.home.range") : L("ui.hud.zone", run.stage + 1), TimeSpan.FromSeconds(run.elapsed).ToString(@"mm\:ss"));
if (mission != null)
mission.text = L(run.routeCompleted ? "ui.hud.complete" : run.stage == 4 && !game.Trial ? run.branch == 0 ? "ui.hud.boss" : "ui.hud.escort" : run.objectiveDone ? "ui.hud.routeOpen" : "ui.hud.objective");
mission.text = game.World.Course ? game.World.Course.Instruction :
L(run.routeCompleted ? "ui.hud.complete" : run.stage == 4 && !game.Trial ? run.branch == 0 ? "ui.hud.boss" : "ui.hud.escort" : run.objectiveDone ? "ui.hud.routeOpen" : "ui.hud.objective");
if (armor != null)
{
armor.value = p.Health / game.chassis[run.chassis].armor * 100;