Files
blockout/BlockOutReplica/Assets/Scripts/Presentation/LevelViewFactory.cs
T
2026-10-20 13:21:04 +08:00

491 lines
27 KiB
C#

using System.Collections.Generic;
using BlockOutReplica.Domain;
using BlockOutReplica.Gameplay;
using UnityEngine;
namespace BlockOutReplica.Presentation
{
public sealed class LevelViewFactory
{
private static readonly Color Purple = new Color(0.225f, 0.155f, 0.57f);
private static readonly Color PurpleHighlight = new Color(0.285f, 0.215f, 0.67f);
private static readonly Color PurpleShadow = new Color(0.055f, 0.035f, 0.16f);
private static readonly Color DeepPurple = new Color(0.075f, 0.065f, 0.20f);
private static readonly Color CellPurple = new Color(0.105f, 0.09f, 0.27f);
private readonly Transform root;
private readonly Material baseMaterial;
private readonly List<GameObject> generated;
public LevelViewFactory(Transform root, Material material, List<GameObject> generated)
{ this.root = root; baseMaterial = material; this.generated = generated; }
public void BuildBoard(LevelDefinition level)
{
float width = level.MaxX - level.MinX + 1f;
float height = level.MaxY - level.MinY + 1f;
GameObject shadow = CreateCube("Board Drop Shadow", new Vector3(.12f, -0.38f, -.14f),
new Vector3(width + .34f, .20f, height + .34f), PurpleShadow);
Object.Destroy(shadow.GetComponent<Collider>());
GameObject back = CreateCube("Board Back", new Vector3(0f, -0.30f, 0f),
new Vector3(width + .08f, 0.30f, height + .08f), DeepPurple);
Object.Destroy(back.GetComponent<Collider>());
Sprite cellSprite = OriginalAssetCatalog.LoadBoardCell();
for (int i = 0; i < level.Cells.Count; i++)
{
GridPosition cell = level.Cells[i];
if (cellSprite != null)
{
Color tint = (cell.X + cell.Y) % 2 == 0 ? Color.white : new Color(.91f, .91f, .98f);
CreateFlatSprite($"Cell {cell.X},{cell.Y}", cellSprite,
level.GridToWorld(cell, -0.06f), new Vector2(.965f, .965f), tint, 2);
}
else
{
Color color = (cell.X + cell.Y) % 2 == 0 ? CellPurple : CellPurple * .91f;
GameObject cellObject = CreateCube($"Cell {cell.X},{cell.Y}",
level.GridToWorld(cell, -0.10f), new Vector3(.965f, 0.20f, .965f), color);
Object.Destroy(cellObject.GetComponent<Collider>());
}
}
if (BuildRecoveredWalls(level)) return;
Sprite wallSprite = OriginalAssetCatalog.LoadWallTile();
HashSet<GridPosition> wallSet = new HashSet<GridPosition>(level.Walls);
for (int i = 0; i < level.Walls.Count; i++)
{
GridPosition wall = level.Walls[i];
Vector3 wallPosition = level.GridToWorld(wall, 0.08f);
if (wallSprite == null)
{
GameObject fallback = CreateCube($"Wall {wall.X},{wall.Y}", wallPosition,
new Vector3(1.035f, 0.44f, 1.035f), Purple);
Object.Destroy(fallback.GetComponent<Collider>());
continue;
}
GameObject wallBody = CreateCube($"Wall Body {wall.X},{wall.Y}",
wallPosition + Vector3.down * .19f,
new Vector3(1.055f, .30f, 1.055f), Purple);
Object.Destroy(wallBody.GetComponent<Collider>());
CreateFlatSprite($"Wall Shadow {wall.X},{wall.Y}", wallSprite,
wallPosition + new Vector3(.075f, -.04f, -.075f), new Vector2(1.09f, 1.09f),
PurpleShadow, 8);
CreateFlatSprite($"Wall {wall.X},{wall.Y}", wallSprite, wallPosition,
new Vector2(1.04f, 1.04f), Purple, 10);
CreateFlatSprite($"Wall Highlight {wall.X},{wall.Y}", wallSprite,
wallPosition + new Vector3(-.018f, .025f, .018f), new Vector2(.94f, .94f),
PurpleHighlight, 11);
GridPosition right = new GridPosition(wall.X + 1, wall.Y);
if (wallSet.Contains(right))
CreateWallBridge(wallSprite, wallPosition + Vector3.right * .5f, true);
GridPosition up = new GridPosition(wall.X, wall.Y + 1);
if (wallSet.Contains(up))
CreateWallBridge(wallSprite, wallPosition + Vector3.forward * .5f, false);
}
}
// Also used by the isolated editor bounds validation, without creating
// floor primitives or invoking gameplay/controller code.
public bool BuildRecoveredWalls(LevelDefinition level)
{
Sprite top = Resources.Load<Sprite>("OriginalRecovered/LinearX-Up");
if (top == null) return false;
var corners = new Dictionary<string, Sprite>();
foreach (string key in new[] { "ConvexLeftTop", "ConvexRightTop", "ConvexLeftBottom", "ConvexRightBottom" })
{
Sprite art = Resources.Load<Sprite>("OriginalRecovered/" + key);
if (art == null) return false;
corners.Add(key, art);
}
var wallSet = new HashSet<GridPosition>(level.Walls);
var visited = new HashSet<GridPosition>();
var orderedWalls = new List<GridPosition>(level.Walls);
orderedWalls.Sort((a, b) => a.Y != b.Y ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X));
foreach (GridPosition cell in orderedWalls)
{
if (visited.Contains(cell)) continue;
bool left = cell.X < level.MinX, right = cell.X > level.MaxX;
bool down = cell.Y < level.MinY, up = cell.Y > level.MaxY;
bool corner = (left || right) && (up || down);
string name = corner ? "Convex" + (left ? "Left" : "Right") + (up ? "Top" : "Bottom")
: up ? "LinearX-Up" : down ? "LinearX-Down_0" : "LinearX-Up";
Sprite sprite = corner ? corners[name] : Resources.Load<Sprite>("OriginalRecovered/" + name);
if (sprite == null) sprite = top;
string cornerName = BoardRimLayout.ProfileNameFor(right ? DoorSide.Right : down ? DoorSide.Bottom : DoorSide.Top);
Sprite profile = corner ? sprite : corners[cornerName];
// The original corner meshes are .66x.82 (top) and .66x.74
// (bottom). Preserve their cross-section instead of squeezing
// bottom corners to .68 and applying unrelated hand offsets.
Vector2 artSize = profile.rect.size / profile.pixelsPerUnit;
if (!corner)
{
// Continue the exact cross-section of a wall corner. BorderLeft
// belongs to another UI frame; it is not this board's purple wall.
bool horizontalStrip = up || down;
sprite = BoardRimLayout.CreateStraightSprite(profile, horizontalStrip);
}
int length = 1;
if (!corner)
{
bool horizontal = up || down;
GridPosition next = new GridPosition(cell.X + (horizontal ? 1 : 0), cell.Y + (horizontal ? 0 : 1));
while (wallSet.Contains(next) && !visited.Contains(next) &&
(horizontal ? next.X <= level.MaxX : next.Y <= level.MaxY))
{
visited.Add(next);
length++;
next = new GridPosition(next.X + (horizontal ? 1 : 0), next.Y + (horizontal ? 0 : 1));
}
}
visited.Add(cell);
Rect bounds = BoardRimLayout.WorldBounds(level, cell, length, artSize);
Vector3 position = new Vector3(bounds.center.x, BoardRimLayout.SurfaceHeight, bounds.center.y);
Vector2 size = bounds.size;
CreateCenteredWallSprite("Continuous Rim Shadow", sprite,
position + new Vector3(.02f, -.07f, -.14f), size,
new Color(.32f, .30f, .60f), 8);
CreateCenteredWallSprite($"Original Continuous Rim {name} [{cell.X},{cell.Y};{length}]", sprite, position,
size, Color.white, 10);
}
return true;
}
private void CreateCenteredWallSprite(string name, Sprite sprite, Vector3 center,
Vector2 size, Color color, int sortingOrder)
{
Vector3 origin = BoardRimLayout.CenteredSpriteOrigin(center, size,
sprite.bounds.size, sprite.bounds.center);
CreateFlatSprite(name, sprite, origin, size, color, sortingOrder);
}
private void CreateWallBridge(Sprite sprite, Vector3 position, bool horizontal)
{
Vector2 size = horizontal ? new Vector2(.88f, .99f) : new Vector2(.99f, .88f);
CreateFlatSprite("Wall Connection Shadow", sprite,
position + new Vector3(.075f, -.04f, -.075f), size * 1.05f, PurpleShadow, 8);
CreateFlatSprite("Wall Connection", sprite, position, size, Purple, 10);
CreateFlatSprite("Wall Connection Highlight", sprite,
position + new Vector3(-.018f, .025f, .018f), size * .91f, PurpleHighlight, 11);
}
public DoorView CreateDoor(LevelDefinition level, DoorDefinition definition, int index)
{
Vector3 position = level.AverageWorld(definition.Cells, 0.28f);
bool horizontal = definition.Side == DoorSide.Top || definition.Side == DoorSide.Bottom;
Vector3 inward = definition.Side == DoorSide.Top ? Vector3.back
: definition.Side == DoorSide.Bottom ? Vector3.forward
: definition.Side == DoorSide.Left ? Vector3.right : Vector3.left;
position += inward * (horizontal ? .08f : .16f);
Vector3 scale = horizontal ? new Vector3(definition.Cells.Count, 0.58f, 0.72f)
: new Vector3(0.72f, 0.58f, definition.Cells.Count);
GameObject door = new GameObject($"Door {index} Color {definition.ColorId}");
door.transform.SetParent(root);
door.transform.position = position;
generated.Add(door);
DoorView doorView = door.AddComponent<DoorView>();
doorView.ConfigureWallClosure(level, definition);
GameObject visualRootObject = new GameObject("Animated Door Root");
visualRootObject.transform.SetParent(door.transform, false);
Transform visualRoot = visualRootObject.transform;
string prefabName = $"DoorPiece{definition.Side}{Mathf.Clamp(definition.Cells.Count, 1, 3)}";
GameObject recoveredPrefab = Resources.Load<GameObject>("OriginalDoorFx/Prefabs/" + prefabName);
if (recoveredPrefab != null && definition.Cells.Count <= 3)
{
GameObject recovered = Object.Instantiate(recoveredPrefab, visualRoot);
recovered.name = "Recovered Original Door Visual";
recovered.transform.localPosition = Vector3.zero;
recovered.transform.localRotation = Quaternion.identity;
recovered.transform.localScale = Vector3.one;
// Source prefabs use XY. Center their native panel, then map XY to
// the replica's XZ board without rotating individual particles.
Transform front = recovered.transform.Find("DoorViewRoot/DoorRoot/DoorImage");
Vector3 sourceCenter = front != null ? front.position - recovered.transform.position : Vector3.zero;
recovered.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
recovered.transform.localPosition = -(recovered.transform.localRotation * sourceCenter);
var effect = recovered.AddComponent<OriginalDoorExitEffect>();
effect.Configure(definition.ColorId, definition.Side);
doorView.Configure(definition.ColorId, definition.Side, visualRoot);
doorView.ConfigureRecovered(effect);
return doorView;
}
Color doorColor = OriginalAssetCatalog.FallbackColor(definition.ColorId);
Sprite panelFill = OriginalAssetCatalog.LoadTile(definition.ColorId, 4);
if (panelFill != null)
{
Vector2 panel = horizontal ? new Vector2(definition.Cells.Count, .84f)
: new Vector2(.68f, definition.Cells.Count);
float bevel = .16f;
float[] sizesX = { bevel, panel.x - bevel * 2f, bevel };
float[] sizesZ = { bevel, panel.y - bevel * 2f, bevel };
float[] centersX = { -(panel.x - bevel) * .5f, 0f, (panel.x - bevel) * .5f };
float[] centersZ = { -(panel.y - bevel) * .5f, 0f, (panel.y - bevel) * .5f };
for (int row = 0; row < 3; row++)
for (int column = 0; column < 3; column++)
{
Sprite slice = OriginalAssetCatalog.LoadTile(definition.ColorId, (2 - row) * 3 + column);
CreatePieceSlice("Original Door Bevel", visualRoot, slice,
new Vector3(centersX[column], .15f, centersZ[row]),
new Vector2(sizesX[column] + .002f, sizesZ[row] + .002f), Color.white, 20);
}
Sprite arrow = OriginalAssetCatalog.LoadDoor(definition.ColorId, definition.Side);
if (arrow != null)
{
Vector2 arrowSize = arrow.bounds.size;
float arrowScale = .68f / Mathf.Max(arrowSize.x, arrowSize.y);
CreatePieceSlice("Original Door Arrow", visualRoot, arrow,
new Vector3(0f, .22f, 0f), arrowSize * arrowScale, Color.white, 22);
}
doorView.Configure(definition.ColorId, definition.Side, visualRoot);
return doorView;
}
Vector3 panelSize = horizontal
? new Vector3(Mathf.Max(.92f, definition.Cells.Count - .04f), .28f, .82f)
: new Vector3(.82f, .28f, Mathf.Max(.92f, definition.Cells.Count - .04f));
CreateDoorCube("Door Shadow", visualRoot,
new Vector3(.055f, -.10f, -.055f), panelSize + new Vector3(.08f, .02f, .08f),
Color.Lerp(doorColor, Color.black, .67f));
CreateDoorCube("Door Panel", visualRoot, new Vector3(0f, .04f, 0f),
panelSize, doorColor);
Vector3 shineSize = horizontal
? new Vector3(panelSize.x * .88f, .035f, .12f)
: new Vector3(.12f, .035f, panelSize.z * .88f);
Vector3 shinePosition = horizontal
? new Vector3(0f, .195f, .24f)
: new Vector3(-.24f, .195f, 0f);
CreateDoorCube("Door Highlight", visualRoot, shinePosition, shineSize,
Color.Lerp(doorColor, Color.white, .34f));
CreateDoorGlow("Door Inner Glow", visualRoot, horizontal, panelSize,
Color.Lerp(doorColor, Color.white, .24f), .12f);
CreateDoorGlow("Door Outer Glow", visualRoot, horizontal, panelSize,
doorColor, .05f);
Sprite sprite = OriginalAssetCatalog.LoadDoor(definition.ColorId, definition.Side);
if (sprite == null)
{
GameObject fallback = CreateCube("Fallback Door", position, scale,
doorColor);
fallback.transform.SetParent(visualRoot, true);
Object.Destroy(fallback.GetComponent<Collider>());
doorView.Configure(definition.ColorId, definition.Side, visualRoot);
return doorView;
}
GameObject visual = new GameObject("Original Door Sprite");
visual.transform.SetParent(visualRoot, false);
visual.transform.localPosition = new Vector3(0f, 0.25f, 0f);
visual.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
float targetWidth = horizontal ? Mathf.Min(1.15f, definition.Cells.Count * .58f) : .38f;
float targetHeight = horizontal ? .38f : Mathf.Min(1.15f, definition.Cells.Count * .58f);
visual.transform.localScale = new Vector3(
targetWidth / Mathf.Max(0.001f, sprite.bounds.size.x),
targetHeight / Mathf.Max(0.001f, sprite.bounds.size.y), 1f);
SpriteRenderer renderer = visual.AddComponent<SpriteRenderer>();
renderer.sprite = sprite; renderer.sortingOrder = 22;
doorView.Configure(definition.ColorId, definition.Side, visualRoot);
return doorView;
}
private void CreateDoorCube(string name, Transform parent, Vector3 localPosition,
Vector3 size, Color color)
{
GameObject item = GameObject.CreatePrimitive(PrimitiveType.Cube);
item.name = name;
item.transform.SetParent(parent, false);
item.transform.localPosition = localPosition;
item.transform.localScale = size;
Object.Destroy(item.GetComponent<Collider>());
ApplyColor(item.GetComponent<Renderer>(), color);
}
private void CreateDoorGlow(string name, Transform parent, bool horizontal,
Vector3 panelSize, Color color, float height)
{
GameObject glow = GameObject.CreatePrimitive(PrimitiveType.Cube);
glow.name = name;
glow.transform.SetParent(parent, false);
glow.transform.localPosition = new Vector3(0f, height, 0f);
glow.transform.localScale = horizontal
? new Vector3(panelSize.x * 1.12f, .018f, panelSize.z * 1.22f)
: new Vector3(panelSize.x * 1.22f, .018f, panelSize.z * 1.12f);
Object.Destroy(glow.GetComponent<Collider>());
color.a = 0f;
ApplyColor(glow.GetComponent<Renderer>(), color);
}
public BlockPieceView CreatePiece(LevelDefinition level, PieceDefinition definition, int index)
{
GetBounds(definition, out int minX, out int maxX, out int minY, out int maxY);
Vector2 size = new Vector2(maxX - minX + 1, maxY - minY + 1);
GameObject piece = new GameObject($"Piece {index} Color {definition.ColorId}");
piece.transform.SetParent(root);
Vector3 minWorld = level.GridToWorld(new GridPosition(minX, minY), 0.28f);
Vector3 maxWorld = level.GridToWorld(new GridPosition(maxX, maxY), 0.28f);
piece.transform.position = (minWorld + maxWorld) * 0.5f;
piece.AddComponent<BoxCollider>().size = new Vector3(size.x - 0.08f, 0.48f, size.y - 0.08f);
generated.Add(piece);
GameObject visualRootObject = new GameObject("Animated Visual Root");
visualRootObject.transform.SetParent(piece.transform, false);
Transform visualRoot = visualRootObject.transform;
BlockPieceView view = piece.AddComponent<BlockPieceView>();
if (!CreateOriginalVisual(visualRoot, piece.transform, level, definition))
{
GameObject fallback = CreateCube("Fallback Body", piece.transform.position,
new Vector3(size.x - 0.08f, 0.48f, size.y - 0.08f),
OriginalAssetCatalog.FallbackColor(definition.ColorId));
fallback.transform.SetParent(visualRoot, true);
Object.Destroy(fallback.GetComponent<Collider>());
}
view.Configure(definition, size, level, visualRoot);
return view;
}
private bool CreateOriginalVisual(
Transform visualRoot, Transform piece, LevelDefinition level, PieceDefinition definition)
{
HashSet<GridPosition> occupied = new HashSet<GridPosition>(definition.Cells);
for (int i = 0; i < definition.Cells.Count; i++)
{
GridPosition cell = definition.Cells[i];
Vector3 world = level.GridToWorld(cell, 0.55f);
Vector3 local = piece.InverseTransformPoint(world);
bool up = !occupied.Contains(new GridPosition(cell.X, cell.Y + 1));
bool down = !occupied.Contains(new GridPosition(cell.X, cell.Y - 1));
bool left = !occupied.Contains(new GridPosition(cell.X - 1, cell.Y));
bool right = !occupied.Contains(new GridPosition(cell.X + 1, cell.Y));
// The original unit has nine separate renderers at +/-0.32.
// Only exposed edges receive a bevel; adjacent cells share a flat fill.
float[] centers = { -.34f, 0f, .34f };
float[] widths = { .32f, .36f, .32f };
for (int row = 0; row < 3; row++)
for (int column = 0; column < 3; column++)
{
int sliceRow = row == 2 && up ? 0 : row == 0 && down ? 2 : 1;
int sliceColumn = column == 0 && left ? 0 : column == 2 && right ? 2 : 1;
int tileIndex = sliceRow * 3 + sliceColumn;
Sprite sprite = OriginalAssetCatalog.LoadTile(definition.ColorId, tileIndex);
if (sprite == null) return false;
Vector3 offset = local + new Vector3(centers[column], 0f, centers[row]);
Vector2 size = new Vector2(widths[column] + .002f, widths[row] + .002f);
CreatePieceSlice("Block Contact Shadow", visualRoot, sprite,
offset + new Vector3(.025f, -.1f, -.075f), size,
new Color(.08f, .06f, .14f, .65f), 27);
CreatePieceSlice($"Original Nine Slice {tileIndex}", visualRoot,
sprite, offset, size, Color.white, 30);
}
}
CreateStuds(visualRoot, piece, level, definition);
return true;
}
private static void CreatePieceSlice(string name, Transform root, Sprite sprite,
Vector3 position, Vector2 size, Color color, int sortOrder)
{
var slice = new GameObject(name);
slice.transform.SetParent(root, false);
slice.transform.localPosition = position;
slice.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
slice.transform.localScale = new Vector3(size.x / sprite.bounds.size.x,
size.y / sprite.bounds.size.y, 1f);
var renderer = slice.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.color = color;
renderer.sortingOrder = sortOrder;
}
private void CreateStuds(Transform visualRoot, Transform piece,
LevelDefinition level, PieceDefinition definition)
{
float[] offsets = { -.23f, .23f };
for (int i = 0; i < definition.Cells.Count; i++)
{
Vector3 cellWorld = level.GridToWorld(definition.Cells[i], .64f);
for (int x = 0; x < offsets.Length; x++)
for (int z = 0; z < offsets.Length; z++)
{
Vector3 local = piece.InverseTransformPoint(cellWorld)
+ new Vector3(offsets[x], 0f, offsets[z]);
Sprite studSprite = OriginalAssetCatalog.LoadStud(definition.ColorId);
if (studSprite == null) continue;
GameObject stud = new GameObject("Original Stud");
stud.name = "Color-Matched Stud";
stud.transform.SetParent(visualRoot, false);
stud.transform.localPosition = local + Vector3.up * .075f;
stud.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
float size = .52f / Mathf.Max(.001f, studSprite.bounds.size.x);
stud.transform.localScale = new Vector3(size, size, 1f);
SpriteRenderer studRenderer = stud.AddComponent<SpriteRenderer>();
studRenderer.sprite = studSprite;
studRenderer.color = Color.white;
studRenderer.sortingOrder = 34;
}
}
}
private void CreateFlatSprite(string name, Sprite sprite, Vector3 position,
Vector2 size, Color color, int sortingOrder, float rotation = 0f)
{
GameObject item = new GameObject(name);
item.transform.SetParent(root);
item.transform.position = position;
item.transform.rotation = Quaternion.Euler(90f, 0f, 0f) * Quaternion.Euler(0f, 0f, rotation);
item.transform.localScale = new Vector3(
size.x / Mathf.Max(.001f, sprite.bounds.size.x),
size.y / Mathf.Max(.001f, sprite.bounds.size.y), 1f);
SpriteRenderer renderer = item.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.color = color;
renderer.sortingOrder = sortingOrder;
generated.Add(item);
}
private static int ResolveTileIndex(GridPosition cell, HashSet<GridPosition> occupied)
{
bool up = occupied.Contains(new GridPosition(cell.X, cell.Y + 1));
bool down = occupied.Contains(new GridPosition(cell.X, cell.Y - 1));
bool left = occupied.Contains(new GridPosition(cell.X - 1, cell.Y));
bool right = occupied.Contains(new GridPosition(cell.X + 1, cell.Y));
if (!up && !left) return 0;
if (!up && !right) return 2;
if (!down && !left) return 6;
if (!down && !right) return 8;
if (!up) return 1;
if (!left) return 3;
if (!right) return 5;
if (!down) return 7;
return 4;
}
private static void GetBounds(PieceDefinition definition,
out int minX, out int maxX, out int minY, out int maxY)
{
minX = maxX = definition.Cells[0].X; minY = maxY = definition.Cells[0].Y;
for (int i = 1; i < definition.Cells.Count; i++)
{
minX = Mathf.Min(minX, definition.Cells[i].X); maxX = Mathf.Max(maxX, definition.Cells[i].X);
minY = Mathf.Min(minY, definition.Cells[i].Y); maxY = Mathf.Max(maxY, definition.Cells[i].Y);
}
}
private GameObject CreateCube(string name, Vector3 position, Vector3 scale, Color color)
{
GameObject item = GameObject.CreatePrimitive(PrimitiveType.Cube);
item.name = name; item.transform.SetParent(root); item.transform.position = position;
item.transform.localScale = scale; ApplyColor(item.GetComponent<Renderer>(), color);
generated.Add(item); return item;
}
private void ApplyColor(Renderer target, Color color)
{
Material material = baseMaterial != null ? new Material(baseMaterial)
: new Material(Shader.Find("Sprites/Default"));
material.color = color; target.material = material;
}
}
}