Add practice range: shooting, AK47, intro cinematic, dressed warehouse scene

- PortalTestDestination practice range: Valorant-style hitscan rifle (recoil,
  spray/bloom, reload, controller support), debug HUD, practice-dummy targets
  with head/body hitboxes, hit/death/respawn animations, procedural SFX/FX
- AK47 model (Quaternius, CC0) held via Animator IK, with fire/reload/equip anims
- Intro cinematic (camera shots, title card, target beam-in, GO hand-off)
- Scene dressing editor pass: CC0 textures, props, warehouse roof, lighting and
  post-processing; custom stylized skybox shader
- CharacterMovement: camera recoil, shoulder offset, cinematic control lock
- Unity CLI pipeline package; plus in-progress chat/Rive/player changes

Asset credits: Assets/Models/Props/PROPS_CREDITS.txt (3 CC-BY 3.0 props),
Assets/Textures/Range/TEXTURES_LICENSE.txt, Assets/Resources/*/..._LICENSE.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pelpanagiotis
2026-09-25 08:56:35 +03:00
co-authored by Claude Opus 5.5
parent c674d76549
commit 5fc9ebf1e6
211 changed files with 34492 additions and 177 deletions
@@ -0,0 +1,224 @@
using Mirror;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
namespace ProceduralEditor
{
// Rebuilds PortalTestDestination.unity as a small greybox shooter-map prototype:
// a walled arena with a central street flanked by two rows of walkable buildings,
// in the style of a tactical-shooter blockout pass.
public static class SetupPortalTestDestinationScene
{
const string ScenePath = "Assets/Scenes/PortalTestDestination.unity";
const string MaterialsFolder = "Assets/Materials";
const string SkyMaterialPath = "Assets/Materials/RangeSky.mat";
static readonly Color FloorColor = new Color(0.55f, 0.55f, 0.55f);
static readonly Color WallColor = new Color(0.28f, 0.28f, 0.3f);
static readonly Color BoxColor = new Color(0.76f, 0.58f, 0.34f);
[MenuItem("Tools/Scene/Setup Portal Test Destination Shooter Map")]
public static void Setup()
{
if (EditorApplication.isPlaying)
{
Debug.LogError("Setup Portal Test Destination: exit Play Mode first — EditorSceneManager.OpenScene() can't run during Play Mode.");
return;
}
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
// Clear out the old studio/pedestal content (Geometry, Lighting rig, post-process
// volume, probe volume data, static lighting sky) but keep Main Camera and reconfigure it.
foreach (var existingRoot in scene.GetRootGameObjects())
{
if (existingRoot.name == "Main Camera") continue;
Object.DestroyImmediate(existingRoot);
}
var floorMat = GetOrCreateMaterial("ShooterProto_Floor", FloorColor);
var wallMat = GetOrCreateMaterial("ShooterProto_Wall", WallColor);
var boxMat = GetOrCreateMaterial("ShooterProto_Box", BoxColor);
SetupCamera();
SetupLighting();
var root = new GameObject("ShooterMapPrototype");
CreateBlock(root.transform, "Floor", new Vector3(0, -0.25f, 0), new Vector3(44, 0.5f, 34), floorMat);
// Perimeter walls (floor spans x:[-22,22], z:[-17,17])
CreateBlock(root.transform, "Wall_North", new Vector3(0, 2, 17.5f), new Vector3(46, 4, 1), wallMat);
CreateBlock(root.transform, "Wall_South", new Vector3(0, 2, -17.5f), new Vector3(46, 4, 1), wallMat);
CreateBlock(root.transform, "Wall_East", new Vector3(22.5f, 2, 0), new Vector3(1, 4, 34), wallMat);
CreateBlock(root.transform, "Wall_West", new Vector3(-22.5f, 2, 0), new Vector3(1, 4, 34), wallMat);
var buildingsRoot = new GameObject("Buildings").transform;
buildingsRoot.SetParent(root.transform);
// A central street (x: -4..4) runs the full length of the map between two spawns.
// Three buildings line each side of it, each walkable with a door facing the street
// and a door facing the back alley, so players can cut through or around them.
const float buildingWidth = 6f;
const float buildingDepth = 6f;
const float wallHeight = 3f;
const float wallThickness = 0.5f;
const float doorWidth = 2.2f;
float[] rowZ = { -9f, 0f, 9f };
for (int i = 0; i < rowZ.Length; i++)
{
// Left row: door faces east (street), back door faces west (outer alley)
CreateBuilding(buildingsRoot, $"Building_L{i + 1}", new Vector3(-13, 0, rowZ[i]),
buildingWidth, buildingDepth, wallHeight, wallThickness,
doorEast: true, doorWest: true, doorWidth, wallMat);
// Right row: mirrored
CreateBuilding(buildingsRoot, $"Building_R{i + 1}", new Vector3(13, 0, rowZ[i]),
buildingWidth, buildingDepth, wallHeight, wallThickness,
doorEast: true, doorWest: true, doorWidth, wallMat);
}
// A couple of symmetric crates in the open street for players crossing between
// the two building rows to duck behind.
var streetCoverRoot = new GameObject("StreetCover").transform;
streetCoverRoot.SetParent(root.transform);
CreateBlock(streetCoverRoot, "StreetCover_1", new Vector3(-2, 1, -4), new Vector3(2, 2, 2), boxMat);
CreateBlock(streetCoverRoot, "StreetCover_2", new Vector3(2, 1, 4), new Vector3(2, 2, 2), boxMat);
SetupSpawnPoint("SpawnPoint_A", new Vector3(0, 0, -14));
SetupSpawnPoint("SpawnPoint_B", new Vector3(0, 0, 14));
EditorSceneManager.MarkSceneDirty(scene);
Debug.Log("Portal Test Destination rebuilt as a shooter-map prototype. Review it in the Scene view, then save (Ctrl+S).");
}
static void SetupCamera()
{
var cam = Camera.main;
if (cam == null)
{
var go = new GameObject("Main Camera", typeof(Camera), typeof(AudioListener));
go.tag = "MainCamera";
cam = go.GetComponent<Camera>();
}
// Overview framing for the editor / before a local player's camera gets reparented
// onto them at runtime (see PlayerScript.OnStartLocalPlayer).
cam.transform.position = new Vector3(0, 14, -26);
cam.transform.rotation = Quaternion.Euler(24f, 0f, 0f);
cam.fieldOfView = 60f;
cam.clearFlags = CameraClearFlags.Skybox;
cam.nearClipPlane = 0.1f;
cam.farClipPlane = 200f;
if (cam.GetComponent<AudioListener>() == null)
cam.gameObject.AddComponent<AudioListener>();
}
static void SetupLighting()
{
// Low dusk sun in the north-west; the RangeSky skybox draws its disc from this light.
var go = new GameObject("Directional Light", typeof(Light));
var light = go.GetComponent<Light>();
light.type = LightType.Directional;
go.transform.rotation = Quaternion.Euler(20f, 120f, 0f);
light.color = new Color(1f, 0.82f, 0.62f);
light.intensity = 1.3f;
light.shadows = LightShadows.Soft;
RenderSettings.sun = light;
var sky = AssetDatabase.LoadAssetAtPath<Material>(SkyMaterialPath);
if (sky != null) RenderSettings.skybox = sky;
else Debug.LogWarning($"Setup Portal Test Destination: {SkyMaterialPath} not found; keeping the current skybox.");
// Ambient and fog pick up the sky's palette (indigo above, warm horizon).
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(0.42f, 0.40f, 0.62f);
RenderSettings.ambientEquatorColor = new Color(0.62f, 0.45f, 0.42f);
RenderSettings.ambientGroundColor = new Color(0.16f, 0.14f, 0.18f);
RenderSettings.fog = true;
RenderSettings.fogMode = FogMode.Linear;
RenderSettings.fogColor = new Color(0.55f, 0.42f, 0.48f);
RenderSettings.fogStartDistance = 30f;
RenderSettings.fogEndDistance = 110f;
DynamicGI.UpdateEnvironment();
}
static void SetupSpawnPoint(string name, Vector3 position)
{
var go = new GameObject(name, typeof(NetworkStartPosition));
go.transform.position = position;
}
// Builds a walkable, roofless room: solid North/South walls and East/West walls that
// can each optionally have a centered door gap.
static void CreateBuilding(Transform parent, string name, Vector3 center, float width, float depth,
float wallHeight, float wallThickness, bool doorEast, bool doorWest, float doorWidth, Material material)
{
var buildingRoot = new GameObject(name).transform;
buildingRoot.SetParent(parent, false);
buildingRoot.localPosition = center;
float halfWidth = width / 2f;
float halfDepth = depth / 2f;
float wallY = wallHeight / 2f;
// North/South walls run along X; extend by wallThickness so they close the corners
// against the East/West walls instead of leaving a gap.
CreateBlock(buildingRoot, "Wall_N", new Vector3(0, wallY, halfDepth), new Vector3(width + wallThickness, wallHeight, wallThickness), material);
CreateBlock(buildingRoot, "Wall_S", new Vector3(0, wallY, -halfDepth), new Vector3(width + wallThickness, wallHeight, wallThickness), material);
CreateSideWall(buildingRoot, "Wall_E", new Vector3(halfWidth, wallY, 0), depth, wallThickness, wallHeight, doorEast, doorWidth, material);
CreateSideWall(buildingRoot, "Wall_W", new Vector3(-halfWidth, wallY, 0), depth, wallThickness, wallHeight, doorWest, doorWidth, material);
}
// An East/West wall runs along Z; a door splits it into two segments with a gap at the center.
static void CreateSideWall(Transform parent, string name, Vector3 center, float depth, float thickness,
float height, bool hasDoor, float doorWidth, Material material)
{
if (!hasDoor)
{
CreateBlock(parent, name, center, new Vector3(thickness, height, depth), material);
return;
}
float segmentLength = (depth - doorWidth) / 2f;
float segmentOffset = doorWidth / 2f + segmentLength / 2f;
CreateBlock(parent, name + "_A", center + new Vector3(0, 0, -segmentOffset), new Vector3(thickness, height, segmentLength), material);
CreateBlock(parent, name + "_B", center + new Vector3(0, 0, segmentOffset), new Vector3(thickness, height, segmentLength), material);
}
static void CreateBlock(Transform parent, string name, Vector3 position, Vector3 size, Material material)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = name;
go.transform.SetParent(parent, false);
go.transform.localPosition = position;
go.transform.localScale = size;
go.GetComponent<MeshRenderer>().sharedMaterial = material;
}
static Material GetOrCreateMaterial(string name, Color color)
{
string path = $"{MaterialsFolder}/{name}.mat";
var existing = AssetDatabase.LoadAssetAtPath<Material>(path);
if (existing != null)
{
existing.color = color;
return existing;
}
if (!AssetDatabase.IsValidFolder(MaterialsFolder))
AssetDatabase.CreateFolder("Assets", "Materials");
var shader = Shader.Find("Universal Render Pipeline/Lit");
var mat = new Material(shader) { color = color };
AssetDatabase.CreateAsset(mat, path);
AssetDatabase.SaveAssets();
return mat;
}
}
}