Adding 9 Player level
This commit is contained in:
parent
fdada6f132
commit
a7be12fa9b
30 changed files with 45984 additions and 300 deletions
|
|
@ -3,7 +3,9 @@
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(Get-ChildItem -Path \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\Scripts\" -Recurse -File -Filter \"*.cs\")",
|
"Bash(Get-ChildItem -Path \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\Scripts\" -Recurse -File -Filter \"*.cs\")",
|
||||||
"Bash(Select-Object -ExpandProperty FullName)",
|
"Bash(Select-Object -ExpandProperty FullName)",
|
||||||
"Bash(powershell -Command \"Get-ChildItem -Recurse -Path 'C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\Scripts' -Filter '*.cs' | Select-String -Pattern 'class LevelData' | Select-Object -First 5\")"
|
"Bash(powershell -Command \"Get-ChildItem -Recurse -Path 'C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\Scripts' -Filter '*.cs' | Select-String -Pattern 'class LevelData' | Select-Object -First 5\")",
|
||||||
|
"Bash(xargs grep -l \"namespace TD.Levels\")",
|
||||||
|
"WebFetch(domain:assetstore.unity.com)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
137
Assets/_Project/Art/Materials/M_Dirt.mat
Normal file
137
Assets/_Project/Art/Materials/M_Dirt.mat
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!21 &2100000
|
||||||
|
Material:
|
||||||
|
serializedVersion: 8
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: M_Dirt
|
||||||
|
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||||
|
m_Parent: {fileID: 0}
|
||||||
|
m_ModifiedSerializedProperties: 0
|
||||||
|
m_ValidKeywords: []
|
||||||
|
m_InvalidKeywords: []
|
||||||
|
m_LightmapFlags: 4
|
||||||
|
m_EnableInstancingVariants: 0
|
||||||
|
m_DoubleSidedGI: 0
|
||||||
|
m_CustomRenderQueue: -1
|
||||||
|
stringTagMap:
|
||||||
|
RenderType: Opaque
|
||||||
|
disabledShaderPasses:
|
||||||
|
- MOTIONVECTORS
|
||||||
|
m_LockedProperties:
|
||||||
|
m_SavedProperties:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TexEnvs:
|
||||||
|
- _BaseMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _BumpMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailAlbedoMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailMask:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailNormalMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _EmissionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MainTex:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MetallicGlossMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _OcclusionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _ParallaxMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _SpecGlossMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_Lightmaps:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_LightmapsInd:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_ShadowMasks:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
m_Ints: []
|
||||||
|
m_Floats:
|
||||||
|
- _AddPrecomputedVelocity: 0
|
||||||
|
- _AlphaClip: 0
|
||||||
|
- _AlphaToMask: 0
|
||||||
|
- _Blend: 0
|
||||||
|
- _BlendModePreserveSpecular: 1
|
||||||
|
- _BumpScale: 1
|
||||||
|
- _ClearCoatMask: 0
|
||||||
|
- _ClearCoatSmoothness: 0
|
||||||
|
- _Cull: 2
|
||||||
|
- _Cutoff: 0.5
|
||||||
|
- _DetailAlbedoMapScale: 1
|
||||||
|
- _DetailNormalMapScale: 1
|
||||||
|
- _DstBlend: 0
|
||||||
|
- _DstBlendAlpha: 0
|
||||||
|
- _EnvironmentReflections: 1
|
||||||
|
- _GlossMapScale: 0
|
||||||
|
- _Glossiness: 0
|
||||||
|
- _GlossyReflections: 0
|
||||||
|
- _Metallic: 0
|
||||||
|
- _OcclusionStrength: 1
|
||||||
|
- _Parallax: 0.005
|
||||||
|
- _QueueOffset: 0
|
||||||
|
- _ReceiveShadows: 1
|
||||||
|
- _Smoothness: 0.5
|
||||||
|
- _SmoothnessTextureChannel: 0
|
||||||
|
- _SpecularHighlights: 1
|
||||||
|
- _SrcBlend: 1
|
||||||
|
- _SrcBlendAlpha: 1
|
||||||
|
- _Surface: 0
|
||||||
|
- _WorkflowMode: 1
|
||||||
|
- _XRMotionVectorsPass: 1
|
||||||
|
- _ZWrite: 1
|
||||||
|
m_Colors:
|
||||||
|
- _BaseColor: {r: 0.5450981, g: 0.2705882, b: 0.07450981, a: 1}
|
||||||
|
- _Color: {r: 0.54509807, g: 0.27058816, b: 0.07450979, a: 1}
|
||||||
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||||
|
m_BuildTextureStacks: []
|
||||||
|
m_AllowLocking: 1
|
||||||
|
--- !u!114 &7846884589835172231
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 11
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||||
|
version: 10
|
||||||
8
Assets/_Project/Art/Materials/M_Dirt.mat.meta
Normal file
8
Assets/_Project/Art/Materials/M_Dirt.mat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 24952dbaf661e434480183e106050156
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -157,13 +157,13 @@ namespace TD.Levels
|
||||||
|
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
|
|
||||||
// Convert tile-corner indices to world-space corners. Tiles are center-based with
|
// Convert tile-corner indices to world-space corners. Tiles are edge-aligned with
|
||||||
// TILE_SIZE = 1, so a tile at (x,y) spans world XZ from (x-0.5, y-0.5) to (x+0.5, y+0.5).
|
// TILE_SIZE = 1, so a tile at (x,y) spans world XZ from (x, y) to (x+1, y+1).
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
float tileSize = GridCoordinates.TILE_SIZE;
|
||||||
Vector3 sw = new Vector3(minTile.x - halfTile, MapBoundsY, minTile.y - halfTile);
|
Vector3 sw = new Vector3(minTile.x * tileSize, MapBoundsY, minTile.y * tileSize);
|
||||||
Vector3 se = new Vector3(maxTile.x + halfTile, MapBoundsY, minTile.y - halfTile);
|
Vector3 se = new Vector3((maxTile.x + 1) * tileSize, MapBoundsY, minTile.y * tileSize);
|
||||||
Vector3 ne = new Vector3(maxTile.x + halfTile, MapBoundsY, maxTile.y + halfTile);
|
Vector3 ne = new Vector3((maxTile.x + 1) * tileSize, MapBoundsY, (maxTile.y + 1) * tileSize);
|
||||||
Vector3 nw = new Vector3(minTile.x - halfTile, MapBoundsY, maxTile.y + halfTile);
|
Vector3 nw = new Vector3(minTile.x * tileSize, MapBoundsY, (maxTile.y + 1) * tileSize);
|
||||||
|
|
||||||
Color prev = Gizmos.color;
|
Color prev = Gizmos.color;
|
||||||
Gizmos.color = new Color(1f, 1f, 1f, 0.6f); // muted white
|
Gizmos.color = new Color(1f, 1f, 1f, 0.6f); // muted white
|
||||||
|
|
@ -219,8 +219,9 @@ namespace TD.Levels
|
||||||
VolumeBase.RasterizeBoundsToTiles(col.bounds, t => set.Add(t));
|
VolumeBase.RasterizeBoundsToTiles(col.bounds, t => set.Add(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each owner, draw perimeter using the general algorithm.
|
// For each owner, draw perimeter using the general algorithm. Tile (x,y) spans
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
// world XZ from (x, y) to (x+1, y+1) (edge-aligned, TILE_SIZE = 1).
|
||||||
|
float tileSize = GridCoordinates.TILE_SIZE;
|
||||||
|
|
||||||
Color prev = Gizmos.color;
|
Color prev = Gizmos.color;
|
||||||
foreach (var kv in perOwner)
|
foreach (var kv in perOwner)
|
||||||
|
|
@ -231,10 +232,10 @@ namespace TD.Levels
|
||||||
foreach (var tile in kv.Value)
|
foreach (var tile in kv.Value)
|
||||||
{
|
{
|
||||||
// World-space corners of this tile.
|
// World-space corners of this tile.
|
||||||
Vector3 sw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y - halfTile);
|
Vector3 sw = new Vector3(tile.x * tileSize, CombinedZoneOutlineY, tile.y * tileSize);
|
||||||
Vector3 se = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y - halfTile);
|
Vector3 se = new Vector3((tile.x + 1) * tileSize, CombinedZoneOutlineY, tile.y * tileSize);
|
||||||
Vector3 ne = new Vector3(tile.x + halfTile, CombinedZoneOutlineY, tile.y + halfTile);
|
Vector3 ne = new Vector3((tile.x + 1) * tileSize, CombinedZoneOutlineY, (tile.y + 1) * tileSize);
|
||||||
Vector3 nw = new Vector3(tile.x - halfTile, CombinedZoneOutlineY, tile.y + halfTile);
|
Vector3 nw = new Vector3(tile.x * tileSize, CombinedZoneOutlineY, (tile.y + 1) * tileSize);
|
||||||
|
|
||||||
// For each of the four edges, draw it ONLY if the neighbor across that edge
|
// For each of the four edges, draw it ONLY if the neighbor across that edge
|
||||||
// is not in the covered set (i.e., the edge is on the perimeter).
|
// is not in the covered set (i.e., the edge is on the perimeter).
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ namespace TD.Levels
|
||||||
[Tooltip("Path to the scene this LevelData was baked from. Auto-populated by bake.")]
|
[Tooltip("Path to the scene this LevelData was baked from. Auto-populated by bake.")]
|
||||||
public string ScenePath;
|
public string ScenePath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Just the scene name (no path, no extension) — what NetworkManager.SceneManager.LoadScene
|
||||||
|
/// expects. Derived from <see cref="ScenePath"/>; returns empty string if ScenePath is unset.
|
||||||
|
/// </summary>
|
||||||
|
public string SceneName =>
|
||||||
|
string.IsNullOrEmpty(ScenePath)
|
||||||
|
? string.Empty
|
||||||
|
: System.IO.Path.GetFileNameWithoutExtension(ScenePath);
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Bake metadata (used for dirty-detection and diagnostic display).
|
// Bake metadata (used for dirty-detection and diagnostic display).
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -75,21 +75,28 @@ namespace TD.Levels
|
||||||
if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
|
if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
|
||||||
|
|
||||||
const float thickness = 0.04f;
|
const float thickness = 0.04f;
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
float tileSize = GridCoordinates.TILE_SIZE;
|
||||||
|
|
||||||
// Three concentric rectangles: the original edge, slightly inset, slightly outset.
|
// Three concentric rectangles: the original edge, slightly inset, slightly outset.
|
||||||
DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, 0f, outlineColor);
|
DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, 0f, outlineColor);
|
||||||
DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, +thickness, outlineColor);
|
DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, +thickness, outlineColor);
|
||||||
DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, -thickness, outlineColor);
|
DrawOutlineAtInset(minTile, maxTile, tileSize, yLevel, -thickness, outlineColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawOutlineAtInset(Vector2Int minTile, Vector2Int maxTile, float halfTile,
|
// Tile (x, y) spans world XZ [x, x+1] (edge-aligned). The outline corners are at the
|
||||||
|
// tile rect's outer edges; `inset` shifts them outward (positive) or inward (negative)
|
||||||
|
// to draw the concentric thickness rectangles.
|
||||||
|
private static void DrawOutlineAtInset(Vector2Int minTile, Vector2Int maxTile, float tileSize,
|
||||||
float yLevel, float inset, Color color)
|
float yLevel, float inset, Color color)
|
||||||
{
|
{
|
||||||
Vector3 sw = new Vector3(minTile.x - halfTile - inset, yLevel, minTile.y - halfTile - inset);
|
float minX = minTile.x * tileSize - inset;
|
||||||
Vector3 se = new Vector3(maxTile.x + halfTile + inset, yLevel, minTile.y - halfTile - inset);
|
float maxX = (maxTile.x + 1) * tileSize + inset;
|
||||||
Vector3 ne = new Vector3(maxTile.x + halfTile + inset, yLevel, maxTile.y + halfTile + inset);
|
float minZ = minTile.y * tileSize - inset;
|
||||||
Vector3 nw = new Vector3(minTile.x - halfTile - inset, yLevel, maxTile.y + halfTile + inset);
|
float maxZ = (maxTile.y + 1) * tileSize + inset;
|
||||||
|
Vector3 sw = new Vector3(minX, yLevel, minZ);
|
||||||
|
Vector3 se = new Vector3(maxX, yLevel, minZ);
|
||||||
|
Vector3 ne = new Vector3(maxX, yLevel, maxZ);
|
||||||
|
Vector3 nw = new Vector3(minX, yLevel, maxZ);
|
||||||
|
|
||||||
Color prev = Gizmos.color;
|
Color prev = Gizmos.color;
|
||||||
Gizmos.color = color;
|
Gizmos.color = color;
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,12 @@ namespace TD.Levels
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Static rasterization helper. Single source of truth for converting a Bounds to the
|
// Static rasterization helper. Single source of truth for converting a Bounds to the
|
||||||
// set of tiles its rasterization covers. The bake will use the same primitive (a tile
|
// set of tiles its rasterization covers. Uses a half-open interval (min inclusive,
|
||||||
// is "covered" iff bounds.Contains(tileCenter)) so gizmos and bake stay in lock-step.
|
// max exclusive) on XZ so that:
|
||||||
|
// - a volume sized to N whole tiles rasterizes to exactly N tiles (no off-by-one),
|
||||||
|
// - two volumes that share a boundary (e.g., one ending at X=20, next starting at
|
||||||
|
// X=20) do not double-claim the boundary tile.
|
||||||
|
// Gizmos and bake share this helper so they stay in lock-step.
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -100,8 +104,8 @@ namespace TD.Levels
|
||||||
/// <paramref name="onTile"/> for each one. The candidate range is computed via
|
/// <paramref name="onTile"/> for each one. The candidate range is computed via
|
||||||
/// <see cref="GridCoordinates.WorldToGrid"/> with a one-tile padding on each side to guard
|
/// <see cref="GridCoordinates.WorldToGrid"/> with a one-tile padding on each side to guard
|
||||||
/// against rounding surprises (WorldToGrid uses round-to-nearest, which can over- or
|
/// against rounding surprises (WorldToGrid uses round-to-nearest, which can over- or
|
||||||
/// under-shoot by one when bounds align with tile edges); the per-tile
|
/// under-shoot by one when bounds align with tile edges); the per-tile half-open
|
||||||
/// <c>bounds.Contains(tileCenter)</c> test inside the loop guarantees correctness.
|
/// interval test inside the loop guarantees correctness.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RasterizeBoundsToTiles(Bounds bounds, System.Action<Vector2Int> onTile)
|
public static void RasterizeBoundsToTiles(Bounds bounds, System.Action<Vector2Int> onTile)
|
||||||
{
|
{
|
||||||
|
|
@ -120,8 +124,11 @@ namespace TD.Levels
|
||||||
for (int y = yLo; y <= yHi; y++)
|
for (int y = yLo; y <= yHi; y++)
|
||||||
{
|
{
|
||||||
Vector3 tileCenter = GridCoordinates.GridToWorld(new Vector2Int(x, y));
|
Vector3 tileCenter = GridCoordinates.GridToWorld(new Vector2Int(x, y));
|
||||||
Vector3 testPoint = new Vector3(tileCenter.x, bounds.center.y, tileCenter.z);
|
// Half-open interval on XZ: min inclusive, max exclusive. Y axis is implicitly
|
||||||
if (bounds.Contains(testPoint))
|
// satisfied because we test at bounds.center.y, which is always within Y range.
|
||||||
|
bool xIn = tileCenter.x >= bounds.min.x && tileCenter.x < bounds.max.x;
|
||||||
|
bool zIn = tileCenter.z >= bounds.min.z && tileCenter.z < bounds.max.z;
|
||||||
|
if (xIn && zIn)
|
||||||
{
|
{
|
||||||
onTile(new Vector2Int(x, y));
|
onTile(new Vector2Int(x, y));
|
||||||
}
|
}
|
||||||
|
|
@ -226,12 +233,12 @@ namespace TD.Levels
|
||||||
if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
|
if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return;
|
||||||
|
|
||||||
// Convert tile-corner indices to world-space corners. A tile at (x, y) spans world XZ
|
// Convert tile-corner indices to world-space corners. A tile at (x, y) spans world XZ
|
||||||
// from (x - 0.5, y - 0.5) to (x + 0.5, y + 0.5) (TILE_SIZE = 1, center-based).
|
// from (x, y) to (x+1, y+1) (TILE_SIZE = 1, edge-aligned).
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
float tileSize = GridCoordinates.TILE_SIZE;
|
||||||
Vector3 swCorner = new Vector3(minTile.x - halfTile, yLevel, minTile.y - halfTile);
|
Vector3 swCorner = new Vector3(minTile.x * tileSize, yLevel, minTile.y * tileSize);
|
||||||
Vector3 seCorner = new Vector3(maxTile.x + halfTile, yLevel, minTile.y - halfTile);
|
Vector3 seCorner = new Vector3((maxTile.x + 1) * tileSize, yLevel, minTile.y * tileSize);
|
||||||
Vector3 neCorner = new Vector3(maxTile.x + halfTile, yLevel, maxTile.y + halfTile);
|
Vector3 neCorner = new Vector3((maxTile.x + 1) * tileSize, yLevel, (maxTile.y + 1) * tileSize);
|
||||||
Vector3 nwCorner = new Vector3(minTile.x - halfTile, yLevel, maxTile.y + halfTile);
|
Vector3 nwCorner = new Vector3(minTile.x * tileSize, yLevel, (maxTile.y + 1) * tileSize);
|
||||||
|
|
||||||
Color prev = Gizmos.color;
|
Color prev = Gizmos.color;
|
||||||
Gizmos.color = outlineColor;
|
Gizmos.color = outlineColor;
|
||||||
|
|
|
||||||
3571
Assets/_Project/Scenes/Levels/9Player.asset
Normal file
3571
Assets/_Project/Scenes/Levels/9Player.asset
Normal file
File diff suppressed because one or more lines are too long
8
Assets/_Project/Scenes/Levels/9Player.asset.meta
Normal file
8
Assets/_Project/Scenes/Levels/9Player.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e65cce766e92f604f85ab939cf374abd
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
41054
Assets/_Project/Scenes/Levels/9Player.unity
Normal file
41054
Assets/_Project/Scenes/Levels/9Player.unity
Normal file
File diff suppressed because it is too large
Load diff
7
Assets/_Project/Scenes/Levels/9Player.unity.meta
Normal file
7
Assets/_Project/Scenes/Levels/9Player.unity.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74fdeec477e257d4484b38108c283d84
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
3
Assets/_Project/Scenes/Levels/9Player_Thumbnail.png
Normal file
3
Assets/_Project/Scenes/Levels/9Player_Thumbnail.png
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0e47a49b9bdc21d0b40f3f39d9f16c74113d2d90de7ffc2ece86b2ea8b916d19
|
||||||
|
size 26196
|
||||||
117
Assets/_Project/Scenes/Levels/9Player_Thumbnail.png.meta
Normal file
117
Assets/_Project/Scenes/Levels/9Player_Thumbnail.png.meta
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: af7534c7dbffefc4c8794fab0a322480
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 1
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -146,7 +146,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 154690529}
|
m_GameObject: {fileID: 154690529}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 27, y: 0, z: 52.05}
|
m_LocalPosition: {x: 13, y: 0, z: 60}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -185,8 +185,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 29, y: 1, z: 34}
|
m_Size: {x: 30, y: 1, z: 34}
|
||||||
m_Center: {x: -14, y: 0, z: 8.5}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &167151707
|
--- !u!1 &167151707
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -264,7 +264,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 304575571}
|
m_GameObject: {fileID: 304575571}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 13, y: 0, z: 81}
|
m_LocalPosition: {x: 14, y: 0, z: 80}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -305,7 +305,7 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 7, y: 1, z: 7}
|
m_Size: {x: 7, y: 1, z: 6}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &330585543
|
--- !u!1 &330585543
|
||||||
GameObject:
|
GameObject:
|
||||||
|
|
@ -745,118 +745,6 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 1994440963}
|
m_Father: {fileID: 1994440963}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
|
||||||
--- !u!1 &643505902
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 643505906}
|
|
||||||
- component: {fileID: 643505905}
|
|
||||||
- component: {fileID: 643505904}
|
|
||||||
- component: {fileID: 643505903}
|
|
||||||
m_Layer: 7
|
|
||||||
m_Name: Cube (6)
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!65 &643505903
|
|
||||||
BoxCollider:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 643505902}
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_IncludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_ExcludeLayers:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 0
|
|
||||||
m_LayerOverridePriority: 0
|
|
||||||
m_IsTrigger: 0
|
|
||||||
m_ProvidesContacts: 0
|
|
||||||
m_Enabled: 1
|
|
||||||
serializedVersion: 3
|
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!23 &643505904
|
|
||||||
MeshRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 643505902}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_CastShadows: 1
|
|
||||||
m_ReceiveShadows: 1
|
|
||||||
m_DynamicOccludee: 1
|
|
||||||
m_StaticShadowCaster: 0
|
|
||||||
m_MotionVectors: 1
|
|
||||||
m_LightProbeUsage: 1
|
|
||||||
m_ReflectionProbeUsage: 1
|
|
||||||
m_RayTracingMode: 2
|
|
||||||
m_RayTraceProcedural: 0
|
|
||||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
|
||||||
m_RayTracingAccelStructBuildFlags: 1
|
|
||||||
m_SmallMeshCulling: 1
|
|
||||||
m_ForceMeshLod: -1
|
|
||||||
m_MeshLodSelectionBias: 0
|
|
||||||
m_RenderingLayerMask: 1
|
|
||||||
m_RendererPriority: 0
|
|
||||||
m_Materials:
|
|
||||||
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
|
||||||
m_StaticBatchInfo:
|
|
||||||
firstSubMesh: 0
|
|
||||||
subMeshCount: 0
|
|
||||||
m_StaticBatchRoot: {fileID: 0}
|
|
||||||
m_ProbeAnchor: {fileID: 0}
|
|
||||||
m_LightProbeVolumeOverride: {fileID: 0}
|
|
||||||
m_ScaleInLightmap: 1
|
|
||||||
m_ReceiveGI: 1
|
|
||||||
m_PreserveUVs: 0
|
|
||||||
m_IgnoreNormalsForChartDetection: 0
|
|
||||||
m_ImportantGI: 0
|
|
||||||
m_StitchLightmapSeams: 1
|
|
||||||
m_SelectedEditorRenderState: 3
|
|
||||||
m_MinimumChartSize: 4
|
|
||||||
m_AutoUVMaxDistance: 0.5
|
|
||||||
m_AutoUVMaxAngle: 89
|
|
||||||
m_LightmapParameters: {fileID: 0}
|
|
||||||
m_GlobalIlluminationMeshLod: 0
|
|
||||||
m_SortingLayerID: 0
|
|
||||||
m_SortingLayer: 0
|
|
||||||
m_SortingOrder: 0
|
|
||||||
m_MaskInteraction: 0
|
|
||||||
m_AdditionalVertexStreams: {fileID: 0}
|
|
||||||
--- !u!33 &643505905
|
|
||||||
MeshFilter:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 643505902}
|
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
--- !u!4 &643505906
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 643505902}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
|
||||||
m_LocalPosition: {x: -35, y: 0, z: 1}
|
|
||||||
m_LocalScale: {x: 8, y: 5, z: 12.6126}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 1994440963}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
|
|
||||||
--- !u!1 &832575517
|
--- !u!1 &832575517
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -906,6 +794,118 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &857729253
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 857729254}
|
||||||
|
- component: {fileID: 857729257}
|
||||||
|
- component: {fileID: 857729256}
|
||||||
|
- component: {fileID: 857729255}
|
||||||
|
m_Layer: 7
|
||||||
|
m_Name: Cube (8)
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &857729254
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 857729253}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
||||||
|
m_LocalPosition: {x: -35, y: 0, z: 1.5}
|
||||||
|
m_LocalScale: {x: 7, y: 5, z: 12}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1994440963}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
|
||||||
|
--- !u!65 &857729255
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 857729253}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 0
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!23 &857729256
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 857729253}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_CastShadows: 1
|
||||||
|
m_ReceiveShadows: 1
|
||||||
|
m_DynamicOccludee: 1
|
||||||
|
m_StaticShadowCaster: 0
|
||||||
|
m_MotionVectors: 1
|
||||||
|
m_LightProbeUsage: 1
|
||||||
|
m_ReflectionProbeUsage: 1
|
||||||
|
m_RayTracingMode: 2
|
||||||
|
m_RayTraceProcedural: 0
|
||||||
|
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||||
|
m_RayTracingAccelStructBuildFlags: 1
|
||||||
|
m_SmallMeshCulling: 1
|
||||||
|
m_ForceMeshLod: -1
|
||||||
|
m_MeshLodSelectionBias: 0
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_RendererPriority: 0
|
||||||
|
m_Materials:
|
||||||
|
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
||||||
|
m_StaticBatchInfo:
|
||||||
|
firstSubMesh: 0
|
||||||
|
subMeshCount: 0
|
||||||
|
m_StaticBatchRoot: {fileID: 0}
|
||||||
|
m_ProbeAnchor: {fileID: 0}
|
||||||
|
m_LightProbeVolumeOverride: {fileID: 0}
|
||||||
|
m_ScaleInLightmap: 1
|
||||||
|
m_ReceiveGI: 1
|
||||||
|
m_PreserveUVs: 0
|
||||||
|
m_IgnoreNormalsForChartDetection: 0
|
||||||
|
m_ImportantGI: 0
|
||||||
|
m_StitchLightmapSeams: 1
|
||||||
|
m_SelectedEditorRenderState: 3
|
||||||
|
m_MinimumChartSize: 4
|
||||||
|
m_AutoUVMaxDistance: 0.5
|
||||||
|
m_AutoUVMaxAngle: 89
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_GlobalIlluminationMeshLod: 0
|
||||||
|
m_SortingLayerID: 0
|
||||||
|
m_SortingLayer: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_MaskInteraction: 0
|
||||||
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!33 &857729257
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 857729253}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
--- !u!1 &902199259
|
--- !u!1 &902199259
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1146,7 +1146,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1064792475}
|
m_GameObject: {fileID: 1064792475}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 11, y: 0, z: 40.5}
|
m_LocalPosition: {x: 13, y: 0, z: 40}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -1187,8 +1187,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 6, y: 1, z: 8}
|
m_Size: {x: 8, y: 1, z: 7}
|
||||||
m_Center: {x: 2, y: 0, z: -1.5}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1078485323
|
--- !u!1 &1078485323
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1216,7 +1216,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1078485323}
|
m_GameObject: {fileID: 1078485323}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 13, y: 0, z: 38}
|
m_LocalPosition: {x: 13, y: 0, z: 39}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -1257,8 +1257,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 7, y: 1, z: 7}
|
m_Size: {x: 8, y: 1, z: 4}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: -1}
|
||||||
--- !u!1 &1119106649
|
--- !u!1 &1119106649
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -1552,7 +1552,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1360337262}
|
m_GameObject: {fileID: 1360337262}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 13, y: 0, z: -3}
|
m_LocalPosition: {x: 13, y: 0, z: -1}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -1590,8 +1590,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 7, y: 1, z: 4}
|
m_Size: {x: 8, y: 1, z: 6}
|
||||||
m_Center: {x: 0, y: 0, z: 1.5}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1380211460
|
--- !u!1 &1380211460
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -2184,7 +2184,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1975687919}
|
m_GameObject: {fileID: 1975687919}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 24, y: 0, z: 31.25}
|
m_LocalPosition: {x: 13, y: 0, z: 19}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
|
|
@ -2223,8 +2223,8 @@ BoxCollider:
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 29, y: 1, z: 34}
|
m_Size: {x: 30, y: 1, z: 34}
|
||||||
m_Center: {x: -10.5, y: 0, z: -13.5}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1994440962
|
--- !u!1 &1994440962
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -2256,8 +2256,8 @@ Transform:
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 2024858689}
|
- {fileID: 2024858689}
|
||||||
- {fileID: 1949204945}
|
- {fileID: 1949204945}
|
||||||
- {fileID: 643505906}
|
|
||||||
- {fileID: 2105067738}
|
- {fileID: 2105067738}
|
||||||
|
- {fileID: 857729254}
|
||||||
- {fileID: 611926976}
|
- {fileID: 611926976}
|
||||||
- {fileID: 1464027364}
|
- {fileID: 1464027364}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
|
|
@ -2480,8 +2480,8 @@ Transform:
|
||||||
m_GameObject: {fileID: 2105067734}
|
m_GameObject: {fileID: 2105067734}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784}
|
||||||
m_LocalPosition: {x: -15, y: 0, z: 1}
|
m_LocalPosition: {x: -15, y: 0, z: 1.5}
|
||||||
m_LocalScale: {x: 8, y: 5, z: 12.6126}
|
m_LocalScale: {x: 7, y: 5, z: 12}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 1994440963}
|
m_Father: {fileID: 1994440963}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -606,6 +606,53 @@ Transform:
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &976902009
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 976902011}
|
||||||
|
- component: {fileID: 976902010}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: MapRegistry
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &976902010
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 976902009}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: c4dafc49d16a2eb4c8db0b00440af991, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.MapRegistry
|
||||||
|
maps:
|
||||||
|
- {fileID: 11400000, guid: e65cce766e92f604f85ab939cf374abd, type: 2}
|
||||||
|
- {fileID: 11400000, guid: 9cc56fbc3ae460a4b862f8510fdf5f09, type: 2}
|
||||||
|
--- !u!4 &976902011
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 976902009}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1648104262
|
--- !u!1 &1648104262
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -660,3 +707,4 @@ SceneRoots:
|
||||||
- {fileID: 1648104264}
|
- {fileID: 1648104264}
|
||||||
- {fileID: 626141754}
|
- {fileID: 626141754}
|
||||||
- {fileID: 514623722}
|
- {fileID: 514623722}
|
||||||
|
- {fileID: 976902011}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ namespace TD.Core
|
||||||
///
|
///
|
||||||
/// Conventions:
|
/// Conventions:
|
||||||
/// - Tiles are 1.0 world unit on each side (TILE_SIZE).
|
/// - Tiles are 1.0 world unit on each side (TILE_SIZE).
|
||||||
/// - Tiles are CENTER-BASED: tile (0, 0) has its center at world (0, 0, 0)
|
/// - Tiles are EDGE-ALIGNED: tile (N, N) occupies world XZ from (N, N) to (N+1, N+1),
|
||||||
/// and occupies world XZ from (-0.5, -0.5) to (+0.5, +0.5).
|
/// with its center at (N+0.5, N+0.5). This makes integer-aligned BoxCollider bounds
|
||||||
|
/// align naturally with tile boundaries — a volume sized to N whole tiles at an
|
||||||
|
/// integer position covers exactly N tiles, with the rasterized tile geometry
|
||||||
|
/// matching the bounds rectangle exactly (no half-tile overhang).
|
||||||
/// - The grid lives on the XZ plane at Y = BUILDABLE_PLANE_Y. Grid-y maps to world-z.
|
/// - The grid lives on the XZ plane at Y = BUILDABLE_PLANE_Y. Grid-y maps to world-z.
|
||||||
/// - 4-connected (no diagonals).
|
/// - 4-connected (no diagonals).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -32,26 +35,27 @@ namespace TD.Core
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the world-space center of the given tile, on the buildable plane.
|
/// Returns the world-space center of the given tile, on the buildable plane.
|
||||||
/// Use this for placing towers, drawing ghost previews, and for A* path waypoints.
|
/// Use this for placing towers, drawing ghost previews, and for A* path waypoints.
|
||||||
|
/// Tile (N, N) is centered at world (N+0.5, N+0.5) since tiles occupy [N, N+1].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Vector3 GridToWorld(Vector2Int gridPos)
|
public static Vector3 GridToWorld(Vector2Int gridPos)
|
||||||
{
|
{
|
||||||
return new Vector3(
|
return new Vector3(
|
||||||
gridPos.x * TILE_SIZE,
|
(gridPos.x + 0.5f) * TILE_SIZE,
|
||||||
BUILDABLE_PLANE_Y,
|
BUILDABLE_PLANE_Y,
|
||||||
gridPos.y * TILE_SIZE);
|
(gridPos.y + 0.5f) * TILE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the tile that contains the given world position.
|
/// Returns the tile that contains the given world position.
|
||||||
/// The Y component of worldPos is ignored. Uses round-to-nearest because
|
/// The Y component of worldPos is ignored. Uses floor because tiles are
|
||||||
/// tiles are center-based — any world point within ±0.5 of a tile's center
|
/// edge-aligned — tile N occupies the half-open interval [N, N+1) on each axis,
|
||||||
/// belongs to that tile.
|
/// so any world point in that range floors to tile N.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Vector2Int WorldToGrid(Vector3 worldPos)
|
public static Vector2Int WorldToGrid(Vector3 worldPos)
|
||||||
{
|
{
|
||||||
return new Vector2Int(
|
return new Vector2Int(
|
||||||
Mathf.RoundToInt(worldPos.x / TILE_SIZE),
|
Mathf.FloorToInt(worldPos.x / TILE_SIZE),
|
||||||
Mathf.RoundToInt(worldPos.z / TILE_SIZE));
|
Mathf.FloorToInt(worldPos.z / TILE_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -61,8 +65,8 @@ namespace TD.Core
|
||||||
public static Vector2Int WorldToGrid(Vector2 worldPosXZ)
|
public static Vector2Int WorldToGrid(Vector2 worldPosXZ)
|
||||||
{
|
{
|
||||||
return new Vector2Int(
|
return new Vector2Int(
|
||||||
Mathf.RoundToInt(worldPosXZ.x / TILE_SIZE),
|
Mathf.FloorToInt(worldPosXZ.x / TILE_SIZE),
|
||||||
Mathf.RoundToInt(worldPosXZ.y / TILE_SIZE));
|
Mathf.FloorToInt(worldPosXZ.y / TILE_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Grid helpers --------------------------------------------------------
|
// ----- Grid helpers --------------------------------------------------------
|
||||||
|
|
@ -200,9 +204,10 @@ namespace TD.Core
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the world-space center of a footprint anchored at the given tile.
|
/// Returns the world-space center of a footprint anchored at the given tile.
|
||||||
/// For a 2x2 footprint at anchor (5, 7) with TILE_SIZE = 1.0, returns (5.5, 0, 7.5).
|
/// For a 2x2 footprint at anchor (5, 7) with TILE_SIZE = 1.0, returns (6, 0, 8) —
|
||||||
/// Use this to position the tower's visual GameObject so it sits centered on its
|
/// the geometric center of the four tiles (5,7),(6,7),(5,8),(6,8), which span
|
||||||
/// footprint rather than on the anchor tile's center.
|
/// world XZ [5, 7]. Use this to position the tower's visual GameObject so it sits
|
||||||
|
/// centered on its footprint rather than on the anchor tile's center.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Vector3 GetFootprintCenterWorld(Vector2Int anchor, Vector2Int footprintSize)
|
public static Vector3 GetFootprintCenterWorld(Vector2Int anchor, Vector2Int footprintSize)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1231,12 +1231,13 @@ namespace TD.Levels.Editor
|
||||||
|
|
||||||
private static bool RenderThumbnail(BakeContext ctx, string thumbnailAssetPath)
|
private static bool RenderThumbnail(BakeContext ctx, string thumbnailAssetPath)
|
||||||
{
|
{
|
||||||
// Compute world-space bounds of the map's tile region.
|
// Compute world-space bounds of the map's tile region. Tile N spans world [N, N+1]
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
// (edge-aligned), so the rect spans from MapMinTile to MapMaxTile + 1 on each axis.
|
||||||
float minX = ctx.MapMinTile.x - halfTile;
|
float tileSize = GridCoordinates.TILE_SIZE;
|
||||||
float maxX = ctx.MapMaxTile.x + halfTile;
|
float minX = ctx.MapMinTile.x * tileSize;
|
||||||
float minZ = ctx.MapMinTile.y - halfTile;
|
float maxX = (ctx.MapMaxTile.x + 1) * tileSize;
|
||||||
float maxZ = ctx.MapMaxTile.y + halfTile;
|
float minZ = ctx.MapMinTile.y * tileSize;
|
||||||
|
float maxZ = (ctx.MapMaxTile.y + 1) * tileSize;
|
||||||
float worldW = maxX - minX;
|
float worldW = maxX - minX;
|
||||||
float worldH = maxZ - minZ;
|
float worldH = maxZ - minZ;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,6 @@ namespace TD.Levels.Editor
|
||||||
float outwardDelta = edgeIsPositive ? worldDeltaSnapped : -worldDeltaSnapped;
|
float outwardDelta = edgeIsPositive ? worldDeltaSnapped : -worldDeltaSnapped;
|
||||||
|
|
||||||
Vector3 size = col.size;
|
Vector3 size = col.size;
|
||||||
Vector3 center = col.center;
|
|
||||||
|
|
||||||
float currentSize = axisIsX ? size.x : size.z;
|
float currentSize = axisIsX ? size.x : size.z;
|
||||||
float newSize = currentSize + outwardDelta;
|
float newSize = currentSize + outwardDelta;
|
||||||
|
|
@ -165,29 +164,44 @@ namespace TD.Levels.Editor
|
||||||
// (don't move the edge at all). This is more predictable than partially honoring it.
|
// (don't move the edge at all). This is more predictable than partially honoring it.
|
||||||
if (newSize < MinSize) return;
|
if (newSize < MinSize) return;
|
||||||
|
|
||||||
// Apply the change. The edge that's NOT being dragged should stay put. To keep the
|
// Apply the change. The edge that's NOT being dragged should stay put. We achieve this
|
||||||
// opposite edge fixed, the center must shift by half the size change in the direction
|
// by adjusting transform.position rather than BoxCollider.center, so the collider's
|
||||||
// of the edge being dragged.
|
// center stays locked at (0, 0, 0) — the "center" of the volume's local frame is always
|
||||||
|
// the geometric center of the box. The position shifts by half the size change in the
|
||||||
|
// direction of the edge being dragged.
|
||||||
//
|
//
|
||||||
// Example (east edge dragged outward by 2 tiles): size.x += 2; center.x += 1.
|
// Example (east edge dragged outward by 2 tiles): size.x += 2; transform.position.x += 1.
|
||||||
// Example (west edge dragged outward by 1 tile): size.x += 1; center.x -= 0.5.
|
// Example (west edge dragged outward by 1 tile): size.x += 1; transform.position.x -= 0.5.
|
||||||
float centerShift = (edgeIsPositive ? 1f : -1f) * (outwardDelta * 0.5f);
|
//
|
||||||
|
// Note: positions may land at half-integer values when the size is odd. That's correct
|
||||||
|
// under the edge-aligned tile convention — bounds align with tile edges when
|
||||||
|
// (position - size/2) and (position + size/2) are both integers,
|
||||||
|
// which requires position to have the same fractional part as size/2.
|
||||||
|
float positionShift = (edgeIsPositive ? 1f : -1f) * (outwardDelta * 0.5f);
|
||||||
|
|
||||||
if (axisIsX)
|
if (axisIsX)
|
||||||
{
|
{
|
||||||
size.x = newSize;
|
size.x = newSize;
|
||||||
center.x += centerShift;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
size.z = newSize;
|
size.z = newSize;
|
||||||
center.z += centerShift;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector3 newPosition = col.transform.position;
|
||||||
|
if (axisIsX) newPosition.x += positionShift;
|
||||||
|
else newPosition.z += positionShift;
|
||||||
|
|
||||||
|
Undo.RecordObject(col.transform, "Resize Volume Edge");
|
||||||
Undo.RecordObject(col, "Resize Volume Edge");
|
Undo.RecordObject(col, "Resize Volume Edge");
|
||||||
|
col.transform.position = newPosition;
|
||||||
col.size = size;
|
col.size = size;
|
||||||
col.center = center;
|
// Force-lock collider center to zero in case it had drifted from prior edits made
|
||||||
|
// before this behavior change. Safe to do unconditionally — by design, this tool now
|
||||||
|
// never wants a non-zero center.
|
||||||
|
col.center = Vector3.zero;
|
||||||
EditorUtility.SetDirty(col);
|
EditorUtility.SetDirty(col);
|
||||||
|
EditorUtility.SetDirty(col.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 WithY(Vector3 v, float y)
|
private static Vector3 WithY(Vector3 v, float y)
|
||||||
|
|
|
||||||
|
|
@ -470,13 +470,13 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
Vector3 builderXZ = new Vector3(transform.position.x, 0f, transform.position.z);
|
Vector3 builderXZ = new Vector3(transform.position.x, 0f, transform.position.z);
|
||||||
|
|
||||||
// Find the point on the footprint rectangle nearest to the builder.
|
// Find the point on the footprint rectangle nearest to the builder. Tile N spans
|
||||||
float minX = anchor.x * GridCoordinates.TILE_SIZE - GridCoordinates.TILE_SIZE * 0.5f;
|
// world [N, N+1] (edge-aligned), so a footprint at anchor (ax, ay) with size (sx, sy)
|
||||||
float maxX = (anchor.x + footprintSize.x - 1) * GridCoordinates.TILE_SIZE
|
// spans world [ax, ax+sx] × [ay, ay+sy].
|
||||||
+ GridCoordinates.TILE_SIZE * 0.5f;
|
float minX = anchor.x * GridCoordinates.TILE_SIZE;
|
||||||
float minZ = anchor.y * GridCoordinates.TILE_SIZE - GridCoordinates.TILE_SIZE * 0.5f;
|
float maxX = (anchor.x + footprintSize.x) * GridCoordinates.TILE_SIZE;
|
||||||
float maxZ = (anchor.y + footprintSize.y - 1) * GridCoordinates.TILE_SIZE
|
float minZ = anchor.y * GridCoordinates.TILE_SIZE;
|
||||||
+ GridCoordinates.TILE_SIZE * 0.5f;
|
float maxZ = (anchor.y + footprintSize.y) * GridCoordinates.TILE_SIZE;
|
||||||
|
|
||||||
float nearestX = Mathf.Clamp(builderXZ.x, minX, maxX);
|
float nearestX = Mathf.Clamp(builderXZ.x, minX, maxX);
|
||||||
float nearestZ = Mathf.Clamp(builderXZ.z, minZ, maxZ);
|
float nearestZ = Mathf.Clamp(builderXZ.z, minZ, maxZ);
|
||||||
|
|
|
||||||
|
|
@ -219,19 +219,19 @@ namespace TD.Gameplay
|
||||||
//
|
//
|
||||||
// The grid covers tiles from GridOriginTile (inclusive, SW corner)
|
// The grid covers tiles from GridOriginTile (inclusive, SW corner)
|
||||||
// to GridOriginTile + GridSize - (1,1) (inclusive, NE corner).
|
// to GridOriginTile + GridSize - (1,1) (inclusive, NE corner).
|
||||||
// Each tile is TILE_SIZE wide and centered on its integer coords.
|
// Each tile is TILE_SIZE wide and edge-aligned: tile N occupies world [N, N+1].
|
||||||
//
|
//
|
||||||
// World extent on X:
|
// World extent on X:
|
||||||
// left = (GridOriginTile.x - 0.5) * TILE_SIZE
|
// left = GridOriginTile.x * TILE_SIZE
|
||||||
// right = (GridOriginTile.x + GridSize.x - 0.5) * TILE_SIZE
|
// right = (GridOriginTile.x + GridSize.x) * TILE_SIZE
|
||||||
// width = GridSize.x * TILE_SIZE
|
// width = GridSize.x * TILE_SIZE
|
||||||
// centerX = (left + right) / 2 = (GridOriginTile.x + (GridSize.x - 1) / 2) * TILE_SIZE
|
// centerX = (left + right) / 2 = (GridOriginTile.x + GridSize.x / 2) * TILE_SIZE
|
||||||
//
|
//
|
||||||
// Same shape on Z (grid-y maps to world-z).
|
// Same shape on Z (grid-y maps to world-z).
|
||||||
float worldCenterX =
|
float worldCenterX =
|
||||||
(level.GridOriginTile.x + (level.GridSize.x - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
(level.GridOriginTile.x + level.GridSize.x * 0.5f) * GridCoordinates.TILE_SIZE;
|
||||||
float worldCenterZ =
|
float worldCenterZ =
|
||||||
(level.GridOriginTile.y + (level.GridSize.y - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
(level.GridOriginTile.y + level.GridSize.y * 0.5f) * GridCoordinates.TILE_SIZE;
|
||||||
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
||||||
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
||||||
|
|
||||||
|
|
@ -442,16 +442,17 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
private void DrawGridBoundsGizmo()
|
private void DrawGridBoundsGizmo()
|
||||||
{
|
{
|
||||||
// One outlined wire box covering the entire grid extent.
|
// One outlined wire box covering the entire grid extent. Tile N spans world
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
// [N, N+1], so the grid's SW corner is at GridOriginTile and its NE corner
|
||||||
|
// is at GridOriginTile + GridSize.
|
||||||
Vector3 sw = new Vector3(
|
Vector3 sw = new Vector3(
|
||||||
level.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile,
|
level.GridOriginTile.x * GridCoordinates.TILE_SIZE,
|
||||||
GridCoordinates.BUILDABLE_PLANE_Y,
|
GridCoordinates.BUILDABLE_PLANE_Y,
|
||||||
level.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile);
|
level.GridOriginTile.y * GridCoordinates.TILE_SIZE);
|
||||||
Vector3 ne = new Vector3(
|
Vector3 ne = new Vector3(
|
||||||
(level.GridOriginTile.x + level.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile,
|
(level.GridOriginTile.x + level.GridSize.x) * GridCoordinates.TILE_SIZE,
|
||||||
GridCoordinates.BUILDABLE_PLANE_Y,
|
GridCoordinates.BUILDABLE_PLANE_Y,
|
||||||
(level.GridOriginTile.y + level.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile);
|
(level.GridOriginTile.y + level.GridSize.y) * GridCoordinates.TILE_SIZE);
|
||||||
|
|
||||||
Gizmos.color = new Color(1f, 1f, 1f, 0.9f); // bright white outline
|
Gizmos.color = new Color(1f, 1f, 1f, 0.9f); // bright white outline
|
||||||
Vector3 nw = new Vector3(sw.x, sw.y, ne.z);
|
Vector3 nw = new Vector3(sw.x, sw.y, ne.z);
|
||||||
|
|
@ -466,11 +467,12 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
// In play mode the collider exists; draw it directly. In edit mode
|
// In play mode the collider exists; draw it directly. In edit mode
|
||||||
// we don't have a collider yet, but we can draw the rectangle that
|
// we don't have a collider yet, but we can draw the rectangle that
|
||||||
// the loader WOULD instantiate, so designers can preview it.
|
// the loader WOULD instantiate, so designers can preview it. Uses
|
||||||
|
// the same formula as SpawnBuildablePlane (tiles are edge-aligned).
|
||||||
float worldCenterX =
|
float worldCenterX =
|
||||||
(level.GridOriginTile.x + (level.GridSize.x - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
(level.GridOriginTile.x + level.GridSize.x * 0.5f) * GridCoordinates.TILE_SIZE;
|
||||||
float worldCenterZ =
|
float worldCenterZ =
|
||||||
(level.GridOriginTile.y + (level.GridSize.y - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
(level.GridOriginTile.y + level.GridSize.y * 0.5f) * GridCoordinates.TILE_SIZE;
|
||||||
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
||||||
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
||||||
|
|
||||||
|
|
@ -505,10 +507,11 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
int idx = y * level.GridSize.x + x;
|
int idx = y * level.GridSize.x + x;
|
||||||
Gizmos.color = walk[idx] ? walkable : blocked;
|
Gizmos.color = walk[idx] ? walkable : blocked;
|
||||||
Vector3 c = new Vector3(
|
// Use GridToWorld so tile centers stay consistent with the convention
|
||||||
(level.GridOriginTile.x + x) * tile,
|
// (tile (N, N) center at world (N+0.5, N+0.5)).
|
||||||
drawY,
|
Vector3 c = GridCoordinates.GridToWorld(
|
||||||
(level.GridOriginTile.y + y) * tile);
|
new Vector2Int(level.GridOriginTile.x + x, level.GridOriginTile.y + y));
|
||||||
|
c.y = drawY;
|
||||||
Gizmos.DrawCube(c, size);
|
Gizmos.DrawCube(c, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -523,7 +526,6 @@ namespace TD.Gameplay
|
||||||
level.OwnerGrid.Length != level.GridSize.x * level.GridSize.y) return;
|
level.OwnerGrid.Length != level.GridSize.x * level.GridSize.y) return;
|
||||||
|
|
||||||
float tile = GridCoordinates.TILE_SIZE;
|
float tile = GridCoordinates.TILE_SIZE;
|
||||||
float halfTile = tile * 0.5f;
|
|
||||||
float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.010f;
|
float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.010f;
|
||||||
|
|
||||||
for (int y = 0; y < level.GridSize.y; y++)
|
for (int y = 0; y < level.GridSize.y; y++)
|
||||||
|
|
@ -534,18 +536,21 @@ namespace TD.Gameplay
|
||||||
PlayerSlot owner = level.OwnerGrid[idx];
|
PlayerSlot owner = level.OwnerGrid[idx];
|
||||||
if (owner == PlayerSlot.None) continue;
|
if (owner == PlayerSlot.None) continue;
|
||||||
|
|
||||||
Vector3 c = new Vector3(
|
// Tile (gx, gy) spans world XZ from (gx, gy) to (gx+1, gy+1) (edge-aligned).
|
||||||
(level.GridOriginTile.x + x) * tile,
|
int gx = level.GridOriginTile.x + x;
|
||||||
drawY,
|
int gy = level.GridOriginTile.y + y;
|
||||||
(level.GridOriginTile.y + y) * tile);
|
float wMinX = gx * tile;
|
||||||
|
float wMaxX = (gx + 1) * tile;
|
||||||
|
float wMinZ = gy * tile;
|
||||||
|
float wMaxZ = (gy + 1) * tile;
|
||||||
|
|
||||||
Gizmos.color = PlayerColors.Get(owner);
|
Gizmos.color = PlayerColors.Get(owner);
|
||||||
// Draw four edges as a wire square. We could DrawWireCube
|
// Draw four edges as a wire square. We could DrawWireCube
|
||||||
// but it would also draw vertical edges we don't want.
|
// but it would also draw vertical edges we don't want.
|
||||||
Vector3 sw = new Vector3(c.x - halfTile, drawY, c.z - halfTile);
|
Vector3 sw = new Vector3(wMinX, drawY, wMinZ);
|
||||||
Vector3 nw = new Vector3(c.x - halfTile, drawY, c.z + halfTile);
|
Vector3 nw = new Vector3(wMinX, drawY, wMaxZ);
|
||||||
Vector3 ne = new Vector3(c.x + halfTile, drawY, c.z + halfTile);
|
Vector3 ne = new Vector3(wMaxX, drawY, wMaxZ);
|
||||||
Vector3 se = new Vector3(c.x + halfTile, drawY, c.z - halfTile);
|
Vector3 se = new Vector3(wMaxX, drawY, wMinZ);
|
||||||
Gizmos.DrawLine(sw, nw);
|
Gizmos.DrawLine(sw, nw);
|
||||||
Gizmos.DrawLine(nw, ne);
|
Gizmos.DrawLine(nw, ne);
|
||||||
Gizmos.DrawLine(ne, se);
|
Gizmos.DrawLine(ne, se);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using TD.Core;
|
using TD.Core;
|
||||||
|
using TD.Levels;
|
||||||
using TD.Net;
|
using TD.Net;
|
||||||
|
|
||||||
namespace TD.Gameplay
|
namespace TD.Gameplay
|
||||||
|
|
@ -51,6 +52,40 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
public static LobbyService Instance { get; private set; }
|
public static LobbyService Instance { get; private set; }
|
||||||
|
|
||||||
|
// ----- Networked lobby state --------------------------------------
|
||||||
|
|
||||||
|
// Index into MapRegistry.Maps of the currently selected map. Server-write,
|
||||||
|
// everyone-read. Default 0 (the first registered map = MapRegistry.Default).
|
||||||
|
// UI subscribes to OnValueChanged to refresh the map browser highlight.
|
||||||
|
// A stale index (map removed between sessions) is tolerated by MapRegistry.Get
|
||||||
|
// returning null; RequestStartMatchRpc validates before loading.
|
||||||
|
private readonly NetworkVariable<int> selectedMapIndex =
|
||||||
|
new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the currently selected map within <see cref="MapRegistry.Maps"/>.
|
||||||
|
/// All clients read the same value; only the host can change it via
|
||||||
|
/// <see cref="RequestSelectMapRpc"/>.
|
||||||
|
/// </summary>
|
||||||
|
public int SelectedMapIndex => selectedMapIndex.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposed for UI subscription to <c>OnValueChanged</c>. Treat as read-only —
|
||||||
|
/// mutations must go through the server via <see cref="RequestSelectMapRpc"/>
|
||||||
|
/// so the host-only gate is enforced.
|
||||||
|
/// </summary>
|
||||||
|
public NetworkVariable<int> SelectedMapIndexVar => selectedMapIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience: resolves the currently selected <see cref="LevelData"/> via
|
||||||
|
/// <see cref="MapRegistry"/>. Returns null if the registry is missing (e.g. the
|
||||||
|
/// editor was started directly in the Lobby scene) or the index is stale.
|
||||||
|
/// </summary>
|
||||||
|
public LevelData SelectedMap =>
|
||||||
|
MapRegistry.Instance != null
|
||||||
|
? MapRegistry.Instance.Get(selectedMapIndex.Value)
|
||||||
|
: null;
|
||||||
|
|
||||||
// ----- Lifecycle --------------------------------------------------
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
|
|
@ -67,7 +102,19 @@ namespace TD.Gameplay
|
||||||
// are preserved from the previous lobby visit, but ready state
|
// are preserved from the previous lobby visit, but ready state
|
||||||
// resets so each match requires explicit re-readying.
|
// resets so each match requires explicit re-readying.
|
||||||
if (IsServer)
|
if (IsServer)
|
||||||
|
{
|
||||||
ResetAllReady();
|
ResetAllReady();
|
||||||
|
|
||||||
|
// If the current selection is invalid for any reason (registry missing,
|
||||||
|
// index stale from a previous session), snap to the default. Index 0
|
||||||
|
// is already the default-default; this is a no-op except after the
|
||||||
|
// registry's contents change between sessions.
|
||||||
|
var registry = MapRegistry.Instance;
|
||||||
|
if (registry != null && registry.Get(selectedMapIndex.Value) == null && registry.Count > 0)
|
||||||
|
{
|
||||||
|
selectedMapIndex.Value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
|
|
@ -79,8 +126,9 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Host-only request to begin the match. Validates that every connected
|
/// Host-only request to begin the match. Validates that every connected
|
||||||
/// player has picked a race and is ready, then transitions every peer
|
/// player has picked a race and is ready AND that the selected map can
|
||||||
/// to the Match scene via NGO scene management.
|
/// accommodate the current lobby's player count, then transitions every
|
||||||
|
/// peer to the selected map's scene via NGO scene management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
public void RequestStartMatchRpc(RpcParams rpcParams = default)
|
public void RequestStartMatchRpc(RpcParams rpcParams = default)
|
||||||
|
|
@ -100,7 +148,81 @@ namespace TD.Gameplay
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkBootstrap.LoadSceneAsHost(SceneNames.Match);
|
// Re-validate the selected map server-side. The UI greys out unselectable
|
||||||
|
// maps based on lobby size, but a late join could push us above the map's
|
||||||
|
// PlayerCount between the click and the RPC arriving. Defensive check.
|
||||||
|
var registry = MapRegistry.Instance;
|
||||||
|
if (registry == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbyService] Cannot start match: MapRegistry.Instance is null. " +
|
||||||
|
"Make sure MainMenu was loaded before the lobby (the registry " +
|
||||||
|
"DontDestroyOnLoads from there).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = registry.Get(selectedMapIndex.Value);
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[LobbyService] Cannot start match: selected map index " +
|
||||||
|
$"{selectedMapIndex.Value} is not registered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int playerCount = CountConnectedPlayers();
|
||||||
|
if (!MapRegistry.IsSelectableFor(selected, playerCount))
|
||||||
|
{
|
||||||
|
Debug.Log($"[LobbyService] Cannot start match: map '{selected.MapName}' supports " +
|
||||||
|
$"up to {selected.PlayerCount} players but the lobby has {playerCount}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(selected.SceneName))
|
||||||
|
{
|
||||||
|
Debug.LogError($"[LobbyService] Cannot start match: '{selected.MapName}' has " +
|
||||||
|
$"empty SceneName (ScenePath='{selected.ScenePath}'). Re-bake the level.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[LobbyService] Starting match on '{selected.MapName}' " +
|
||||||
|
$"(index={selectedMapIndex.Value}, scene='{selected.SceneName}', " +
|
||||||
|
$"scenePath='{selected.ScenePath}', players={playerCount}).");
|
||||||
|
NetworkBootstrap.LoadSceneAsHost(selected.SceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host-only request to change the selected map. Validates that the requested index
|
||||||
|
/// resolves to a real map in <see cref="MapRegistry"/>; if so, writes
|
||||||
|
/// <see cref="SelectedMapIndex"/> which replicates to every client.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Selectability for the current lobby size is NOT enforced here — players are
|
||||||
|
/// allowed to highlight an oversized map (e.g. while waiting for more friends to
|
||||||
|
/// join); the actual Start Match call enforces the rule. This keeps the host's
|
||||||
|
/// intent visible to everyone without preventing them from "claiming" a future map.
|
||||||
|
/// </remarks>
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
|
public void RequestSelectMapRpc(int mapIndex, RpcParams rpcParams = default)
|
||||||
|
{
|
||||||
|
if (rpcParams.Receive.SenderClientId != NetworkManager.Singleton.LocalClientId)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[LobbyService] Non-host client attempted to change the map. Ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry = MapRegistry.Instance;
|
||||||
|
if (registry == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbyService] Cannot change map: MapRegistry.Instance is null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.Get(mapIndex) == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[LobbyService] Rejected map index {mapIndex} — not registered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMapIndex.Value = mapIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -154,6 +276,18 @@ namespace TD.Gameplay
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current number of connected players (every <see cref="PlayerMatchState"/> in the
|
||||||
|
/// static registry). Used by map selectability checks both in the UI and the server-side
|
||||||
|
/// Start Match validator.
|
||||||
|
/// </summary>
|
||||||
|
public static int CountConnectedPlayers()
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
foreach (var _ in PlayerMatchState.AllPlayers) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Server helpers --------------------------------------------
|
// ----- Server helpers --------------------------------------------
|
||||||
|
|
||||||
private static void ResetAllReady()
|
private static void ResetAllReady()
|
||||||
|
|
|
||||||
196
Assets/_Project/Scripts/Gameplay/MapRegistry.cs
Normal file
196
Assets/_Project/Scripts/Gameplay/MapRegistry.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/MapRegistry.cs
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Levels;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Persistent (DontDestroyOnLoad) singleton that holds every <see cref="LevelData"/> available
|
||||||
|
/// to the lobby's map browser. Mirrors <see cref="RaceRegistry"/>'s pattern.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Inspector setup.</b> Place ONE <c>MapRegistry</c> GameObject in the <b>MainMenu
|
||||||
|
/// scene</b> only. Drag every <see cref="LevelData"/> asset that should appear in the lobby's
|
||||||
|
/// map browser into the <c>Maps</c> array. The first non-null entry becomes the
|
||||||
|
/// <see cref="Default"/> map (auto-selected when a lobby first opens). Mark itself
|
||||||
|
/// <c>DontDestroyOnLoad</c> on Awake so it survives MainMenu → Lobby → Match transitions and
|
||||||
|
/// is reachable from any scene via <see cref="Instance"/>.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Why MainMenu-only?</b> Same reason as <see cref="RaceRegistry"/>: a single
|
||||||
|
/// authoritative array prevents the "I updated one and forgot the other" designer trap.
|
||||||
|
/// Duplicate instances dropped into Lobby or Match scenes self-destroy on Awake.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Editor-only standalone testing.</b> If you open the Lobby or Match scene directly
|
||||||
|
/// from the editor without going through MainMenu, no MapRegistry exists. <see cref="Instance"/>
|
||||||
|
/// is null; callers should handle that (the lobby UI shows an empty browser, Quick Start falls
|
||||||
|
/// back to a hardcoded scene, etc.). For standalone testing, temporarily add a registry to
|
||||||
|
/// whatever scene you're testing — but don't commit it.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Selectability.</b> A map is selectable in a lobby of N players iff
|
||||||
|
/// <c>N <= map.PlayerCount</c>. The lobby UI still shows maps it can't currently use, just
|
||||||
|
/// greyed out with an explanatory label — so players can see what other maps exist and how
|
||||||
|
/// many players they'd need to play them.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Plain MonoBehaviour.</b> Not a NetworkBehaviour — every peer has the same LevelData
|
||||||
|
/// assets in the build. Network state tracks only the selected map's index via
|
||||||
|
/// <c>LobbyService.SelectedMapIndex</c>.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class MapRegistry : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ----- Singleton -------------------------------------------------
|
||||||
|
|
||||||
|
public static MapRegistry Instance { get; private set; }
|
||||||
|
|
||||||
|
// ----- Inspector --------------------------------------------------
|
||||||
|
|
||||||
|
[Tooltip("All LevelData assets that should appear in the lobby's map browser. Drag each " +
|
||||||
|
"asset into the array. The FIRST non-null entry becomes the default selection " +
|
||||||
|
"when a lobby first opens; order subsequent entries however you want them sorted " +
|
||||||
|
"in the UI. Null entries and assets with empty MapName are skipped with a warning.")]
|
||||||
|
[SerializeField] private LevelData[] maps;
|
||||||
|
|
||||||
|
// ----- Public API -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All registered maps in inspector order. Never returns null entries; entries that failed
|
||||||
|
/// validation in Awake are filtered out. Safe to iterate any time after Awake.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LevelData> Maps => validatedMaps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of valid registered maps. Equivalent to <c>Maps.Count</c> but cheaper
|
||||||
|
/// since it doesn't allocate an enumerator.
|
||||||
|
/// </summary>
|
||||||
|
public int Count => validatedMaps.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The map auto-selected when a lobby first opens. Returns the first valid entry in the
|
||||||
|
/// inspector array, or null if the registry has no valid maps (which would be a setup
|
||||||
|
/// error — Quick Start and lobby will both log and degrade).
|
||||||
|
/// </summary>
|
||||||
|
public LevelData Default => validatedMaps.Count > 0 ? validatedMaps[0] : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the map at the given index, or null if the index is out of range. Tolerant of
|
||||||
|
/// stale indices that might survive a registry edit (e.g. a NetworkVariable holding an
|
||||||
|
/// index whose map was removed).
|
||||||
|
/// </summary>
|
||||||
|
public LevelData Get(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= validatedMaps.Count) return null;
|
||||||
|
return validatedMaps[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the index of the given map in <see cref="Maps"/>, or -1 if it isn't registered.
|
||||||
|
/// Use this when you have a LevelData reference and need to sync the selection over the
|
||||||
|
/// network as an integer.
|
||||||
|
/// </summary>
|
||||||
|
public int IndexOf(LevelData map)
|
||||||
|
{
|
||||||
|
if (map == null) return -1;
|
||||||
|
for (int i = 0; i < validatedMaps.Count; i++)
|
||||||
|
{
|
||||||
|
if (validatedMaps[i] == map) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if a lobby with <paramref name="playerCount"/> players is allowed to start a match
|
||||||
|
/// on <paramref name="map"/>. Currently the rule is just "lobby size must fit"; if minimum
|
||||||
|
/// player counts become a thing later, gate them here.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsSelectableFor(LevelData map, int playerCount)
|
||||||
|
{
|
||||||
|
if (map == null) return false;
|
||||||
|
if (playerCount < 1) return false;
|
||||||
|
return playerCount <= map.PlayerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
|
// Validated subset of the inspector array; built once in Awake and never mutated.
|
||||||
|
// Holding a separate list lets us silently filter out null/invalid entries without
|
||||||
|
// mutating the inspector array (which would surprise designers).
|
||||||
|
private readonly List<LevelData> validatedMaps = new List<LevelData>();
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
// Persistent-singleton pattern: first instance wins, duplicates self-destroy.
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Debug.LogWarning(
|
||||||
|
$"[MapRegistry] Persistent instance already exists. " +
|
||||||
|
$"Destroying duplicate in scene '{gameObject.scene.name}'. " +
|
||||||
|
$"Keep MapRegistry in the MainMenu scene only.");
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
BuildLookup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Private ----------------------------------------------------
|
||||||
|
|
||||||
|
private void BuildLookup()
|
||||||
|
{
|
||||||
|
validatedMaps.Clear();
|
||||||
|
|
||||||
|
if (maps == null || maps.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[MapRegistry] No LevelData assets assigned. Drag assets into " +
|
||||||
|
"the Maps array. Lobby map browser will be empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seen = new HashSet<LevelData>();
|
||||||
|
foreach (var map in maps)
|
||||||
|
{
|
||||||
|
if (map == null) continue;
|
||||||
|
|
||||||
|
// Duplicates are likely an authoring mistake (designer dragged the same asset
|
||||||
|
// twice); keep the first occurrence and warn.
|
||||||
|
if (!seen.Add(map))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[MapRegistry] Duplicate LevelData '{map.name}' in Maps " +
|
||||||
|
$"array. Keeping first occurrence only.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(map.MapName))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[MapRegistry] '{map.name}' has empty MapName — skipping. " +
|
||||||
|
"Set MapName on the LevelData asset.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(map.ScenePath))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[MapRegistry] '{map.name}' has empty ScenePath — skipping. " +
|
||||||
|
"Bake the level so ScenePath gets populated.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.PlayerCount < 1)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[MapRegistry] '{map.name}' has PlayerCount={map.PlayerCount} " +
|
||||||
|
"(must be >= 1) — skipping.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedMaps.Add(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[MapRegistry] Registered {validatedMaps.Count} map(s). " +
|
||||||
|
$"Default = '{(Default != null ? Default.MapName : "<none>")}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/MapRegistry.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/MapRegistry.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c4dafc49d16a2eb4c8db0b00440af991
|
||||||
|
|
@ -6,6 +6,7 @@ using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
using TD.Core;
|
using TD.Core;
|
||||||
using TD.Gameplay;
|
using TD.Gameplay;
|
||||||
|
using TD.Levels;
|
||||||
using TD.Net;
|
using TD.Net;
|
||||||
|
|
||||||
namespace TD.UI
|
namespace TD.UI
|
||||||
|
|
@ -41,11 +42,18 @@ namespace TD.UI
|
||||||
{
|
{
|
||||||
// ----- Cached UI elements -----------------------------------------
|
// ----- Cached UI elements -----------------------------------------
|
||||||
|
|
||||||
|
private VisualElement mapListContainer;
|
||||||
private VisualElement playerListContainer;
|
private VisualElement playerListContainer;
|
||||||
private Button startMatchButton;
|
private Button startMatchButton;
|
||||||
private Button leaveButton;
|
private Button leaveButton;
|
||||||
private Label statusLabel;
|
private Label statusLabel;
|
||||||
|
|
||||||
|
// Signature snapshot for the map list, mirroring the player-list pattern.
|
||||||
|
// Components: registry count, selected index, current player count, host flag.
|
||||||
|
// Without this, the cards rebuild every frame and the Clickable manipulator
|
||||||
|
// loses presses (same root cause as the player-list signature).
|
||||||
|
private string lastMapListSignature = string.Empty;
|
||||||
|
|
||||||
// ----- Race selection overlay ------------------------------------
|
// ----- Race selection overlay ------------------------------------
|
||||||
|
|
||||||
[Tooltip("Sibling RaceSelectionOverlay component that owns the race-pick UI. " +
|
[Tooltip("Sibling RaceSelectionOverlay component that owns the race-pick UI. " +
|
||||||
|
|
@ -88,6 +96,7 @@ namespace TD.UI
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (playerListContainer == null) return;
|
if (playerListContainer == null) return;
|
||||||
|
RefreshMapList();
|
||||||
RefreshPlayerList();
|
RefreshPlayerList();
|
||||||
RefreshStartButton();
|
RefreshStartButton();
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +121,28 @@ namespace TD.UI
|
||||||
title.style.marginBottom = 24;
|
title.style.marginBottom = 24;
|
||||||
root.Add(title);
|
root.Add(title);
|
||||||
|
|
||||||
|
// Map selection panel — host clicks to change, everyone sees the current pick.
|
||||||
|
// Horizontal row of cards rebuilt by RefreshMapList when its signature changes.
|
||||||
|
var mapPanelLabel = new Label("Map");
|
||||||
|
mapPanelLabel.style.fontSize = 18;
|
||||||
|
mapPanelLabel.style.color = new Color(0.85f, 0.85f, 0.85f);
|
||||||
|
mapPanelLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
mapPanelLabel.style.marginBottom = 6;
|
||||||
|
root.Add(mapPanelLabel);
|
||||||
|
|
||||||
|
mapListContainer = new VisualElement();
|
||||||
|
mapListContainer.style.flexDirection = FlexDirection.Row;
|
||||||
|
mapListContainer.style.justifyContent = Justify.Center;
|
||||||
|
mapListContainer.style.flexWrap = Wrap.Wrap;
|
||||||
|
mapListContainer.style.minWidth = 520;
|
||||||
|
mapListContainer.style.paddingTop = 8;
|
||||||
|
mapListContainer.style.paddingBottom = 8;
|
||||||
|
mapListContainer.style.paddingLeft = 8;
|
||||||
|
mapListContainer.style.paddingRight = 8;
|
||||||
|
mapListContainer.style.backgroundColor = new Color(0f, 0f, 0f, 0.4f);
|
||||||
|
mapListContainer.style.marginBottom = 24;
|
||||||
|
root.Add(mapListContainer);
|
||||||
|
|
||||||
// Player list container (rebuilt every Update from AllPlayers).
|
// Player list container (rebuilt every Update from AllPlayers).
|
||||||
playerListContainer = new VisualElement();
|
playerListContainer = new VisualElement();
|
||||||
playerListContainer.style.flexDirection = FlexDirection.Column;
|
playerListContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
|
@ -153,6 +184,157 @@ namespace TD.UI
|
||||||
|
|
||||||
// ----- Per-frame refresh ------------------------------------------
|
// ----- Per-frame refresh ------------------------------------------
|
||||||
|
|
||||||
|
private void RefreshMapList()
|
||||||
|
{
|
||||||
|
// Inputs that determine the rendered state of the map row. Anything not in this
|
||||||
|
// signature won't trigger a rebuild — match what BuildMapCard reads.
|
||||||
|
var registry = MapRegistry.Instance;
|
||||||
|
var svc = LobbyService.Instance;
|
||||||
|
int registryCount = registry != null ? registry.Count : 0;
|
||||||
|
int selectedIndex = svc != null ? svc.SelectedMapIndex : -1;
|
||||||
|
int playerCount = LobbyService.CountConnectedPlayers();
|
||||||
|
bool isHost = NetworkManager.Singleton != null && NetworkManager.Singleton.IsHost;
|
||||||
|
|
||||||
|
string signature = $"{registryCount}:{selectedIndex}:{playerCount}:{(isHost ? 'H' : 'C')}";
|
||||||
|
if (signature == lastMapListSignature) return;
|
||||||
|
lastMapListSignature = signature;
|
||||||
|
|
||||||
|
mapListContainer.Clear();
|
||||||
|
|
||||||
|
if (registry == null || registryCount == 0)
|
||||||
|
{
|
||||||
|
var emptyLabel = new Label("(no maps available — MapRegistry missing)");
|
||||||
|
emptyLabel.style.color = new Color(0.7f, 0.5f, 0.5f);
|
||||||
|
emptyLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
mapListContainer.Add(emptyLabel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < registryCount; i++)
|
||||||
|
{
|
||||||
|
var map = registry.Get(i);
|
||||||
|
if (map == null) continue;
|
||||||
|
|
||||||
|
bool isSelected = i == selectedIndex;
|
||||||
|
bool fitsLobby = MapRegistry.IsSelectableFor(map, playerCount);
|
||||||
|
bool clickable = isHost && fitsLobby;
|
||||||
|
|
||||||
|
mapListContainer.Add(BuildMapCard(map, i, isSelected, fitsLobby, clickable, playerCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildMapCard(LevelData map, int index, bool isSelected,
|
||||||
|
bool fitsLobby, bool clickable, int playerCount)
|
||||||
|
{
|
||||||
|
var card = new VisualElement();
|
||||||
|
card.style.width = 180;
|
||||||
|
card.style.height = 220;
|
||||||
|
card.style.marginRight = 8;
|
||||||
|
card.style.marginLeft = 8;
|
||||||
|
card.style.paddingTop = 8;
|
||||||
|
card.style.paddingBottom = 8;
|
||||||
|
card.style.paddingLeft = 8;
|
||||||
|
card.style.paddingRight = 8;
|
||||||
|
card.style.alignItems = Align.Center;
|
||||||
|
card.style.justifyContent = Justify.FlexStart;
|
||||||
|
|
||||||
|
// Background reflects selectability: highlighted when selected, dim when oversized.
|
||||||
|
Color background = isSelected
|
||||||
|
? new Color(0.20f, 0.32f, 0.55f)
|
||||||
|
: new Color(0.12f, 0.12f, 0.16f);
|
||||||
|
if (!fitsLobby) background.a *= 0.7f;
|
||||||
|
card.style.backgroundColor = background;
|
||||||
|
|
||||||
|
// Border to make the selection visually unambiguous.
|
||||||
|
float borderWidth = isSelected ? 3f : 1f;
|
||||||
|
Color borderColor = isSelected
|
||||||
|
? new Color(0.45f, 0.70f, 1f)
|
||||||
|
: new Color(0.25f, 0.25f, 0.30f);
|
||||||
|
card.style.borderTopWidth = borderWidth;
|
||||||
|
card.style.borderBottomWidth = borderWidth;
|
||||||
|
card.style.borderLeftWidth = borderWidth;
|
||||||
|
card.style.borderRightWidth = borderWidth;
|
||||||
|
card.style.borderTopColor = borderColor;
|
||||||
|
card.style.borderBottomColor = borderColor;
|
||||||
|
card.style.borderLeftColor = borderColor;
|
||||||
|
card.style.borderRightColor = borderColor;
|
||||||
|
|
||||||
|
// Thumbnail (top of card). Falls back to a neutral placeholder if MapThumbnail isn't set.
|
||||||
|
var thumb = new VisualElement();
|
||||||
|
thumb.style.width = 140;
|
||||||
|
thumb.style.height = 105;
|
||||||
|
thumb.style.marginBottom = 6;
|
||||||
|
thumb.style.backgroundColor = new Color(0.05f, 0.05f, 0.08f);
|
||||||
|
if (map.MapThumbnail != null)
|
||||||
|
{
|
||||||
|
thumb.style.backgroundImage = new StyleBackground(map.MapThumbnail);
|
||||||
|
}
|
||||||
|
// Dim the thumbnail when the map doesn't fit the lobby — reinforces the disabled look.
|
||||||
|
if (!fitsLobby)
|
||||||
|
{
|
||||||
|
thumb.style.opacity = 0.45f;
|
||||||
|
}
|
||||||
|
card.Add(thumb);
|
||||||
|
|
||||||
|
// Map name.
|
||||||
|
var nameLabel = new Label(map.MapName);
|
||||||
|
nameLabel.style.color = fitsLobby ? Color.white : new Color(0.7f, 0.6f, 0.6f);
|
||||||
|
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
nameLabel.style.fontSize = 14;
|
||||||
|
nameLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
card.Add(nameLabel);
|
||||||
|
|
||||||
|
// Player count label — tells the user the capacity, and why the card is greyed out
|
||||||
|
// when oversized. Live player count is shown alongside the cap so it's obvious why.
|
||||||
|
string countText = fitsLobby
|
||||||
|
? $"Up to {map.PlayerCount} players"
|
||||||
|
: $"Up to {map.PlayerCount} — needs ≤ {map.PlayerCount} (lobby has {playerCount})";
|
||||||
|
var countLabel = new Label(countText);
|
||||||
|
countLabel.style.color = fitsLobby
|
||||||
|
? new Color(0.75f, 0.75f, 0.75f)
|
||||||
|
: new Color(0.85f, 0.55f, 0.45f);
|
||||||
|
countLabel.style.fontSize = 11;
|
||||||
|
countLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
countLabel.style.marginTop = 2;
|
||||||
|
countLabel.style.whiteSpace = WhiteSpace.Normal;
|
||||||
|
card.Add(countLabel);
|
||||||
|
|
||||||
|
// Make the whole card clickable for host (when the map fits the lobby). Non-hosts
|
||||||
|
// see the cards but can't change selection. Capture `index` in a local so the
|
||||||
|
// closure doesn't bind to the loop variable.
|
||||||
|
if (clickable)
|
||||||
|
{
|
||||||
|
int capturedIndex = index;
|
||||||
|
card.RegisterCallback<ClickEvent>(_ => OnMapCardClicked(capturedIndex));
|
||||||
|
// Standard hover affordance to advertise interactivity.
|
||||||
|
card.RegisterCallback<MouseEnterEvent>(_ =>
|
||||||
|
{
|
||||||
|
if (!isSelected)
|
||||||
|
card.style.backgroundColor = new Color(0.17f, 0.20f, 0.28f);
|
||||||
|
});
|
||||||
|
card.RegisterCallback<MouseLeaveEvent>(_ =>
|
||||||
|
{
|
||||||
|
if (!isSelected)
|
||||||
|
card.style.backgroundColor = new Color(0.12f, 0.12f, 0.16f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapCardClicked(int index)
|
||||||
|
{
|
||||||
|
var svc = LobbyService.Instance;
|
||||||
|
if (svc == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[LobbyController] LobbyService.Instance is null — cannot change map.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't bother round-tripping if we're already on this map.
|
||||||
|
if (svc.SelectedMapIndex == index) return;
|
||||||
|
svc.RequestSelectMapRpc(index);
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshPlayerList()
|
private void RefreshPlayerList()
|
||||||
{
|
{
|
||||||
// Sort by slot for stable ordering. AllPlayers is keyed by clientId
|
// Sort by slot for stable ordering. AllPlayers is keyed by clientId
|
||||||
|
|
@ -285,7 +467,32 @@ namespace TD.UI
|
||||||
startMatchButton.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
|
startMatchButton.style.display = isHost ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
if (!isHost) return;
|
if (!isHost) return;
|
||||||
|
|
||||||
bool canStart = LobbyService.AreAllPlayersReady(out string reason);
|
// Two gates: readiness (existing) and map selectability (new). Show whichever
|
||||||
|
// failure is preventing the start so the host knows what to fix. Readiness
|
||||||
|
// is checked first because it's the more common blocker.
|
||||||
|
bool readyOk = LobbyService.AreAllPlayersReady(out string readyReason);
|
||||||
|
string reason = readyOk ? string.Empty : readyReason;
|
||||||
|
|
||||||
|
bool mapOk = true;
|
||||||
|
if (readyOk)
|
||||||
|
{
|
||||||
|
var svc = LobbyService.Instance;
|
||||||
|
var selected = svc != null ? svc.SelectedMap : null;
|
||||||
|
int playerCount = LobbyService.CountConnectedPlayers();
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
mapOk = false;
|
||||||
|
reason = "No map selected.";
|
||||||
|
}
|
||||||
|
else if (!MapRegistry.IsSelectableFor(selected, playerCount))
|
||||||
|
{
|
||||||
|
mapOk = false;
|
||||||
|
reason = $"'{selected.MapName}' supports up to {selected.PlayerCount} " +
|
||||||
|
$"players; lobby has {playerCount}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canStart = readyOk && mapOk;
|
||||||
startMatchButton.SetEnabled(canStart);
|
startMatchButton.SetEnabled(canStart);
|
||||||
statusLabel.text = canStart ? string.Empty : reason;
|
statusLabel.text = canStart ? string.Empty : reason;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,13 +191,13 @@ namespace TD.UI.Minimap
|
||||||
terrainLayer.style.backgroundImage =
|
terrainLayer.style.backgroundImage =
|
||||||
new StyleBackground(Background.FromTexture2D(bakedTerrain));
|
new StyleBackground(Background.FromTexture2D(bakedTerrain));
|
||||||
|
|
||||||
// World extents of the baked rectangle. Tile (n) covers world n - 0.5 to n + 0.5.
|
// World extents of the baked rectangle. Tile (n) spans world [n, n+1] (edge-aligned),
|
||||||
|
// so the rectangle covers [GridOriginTile, GridOriginTile + GridSize] on each axis.
|
||||||
var data = loader.LevelData;
|
var data = loader.LevelData;
|
||||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE;
|
||||||
float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile;
|
float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE;
|
||||||
float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile;
|
float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE;
|
||||||
float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile;
|
float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE;
|
||||||
float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile;
|
|
||||||
worldMin = new Vector2(minX, minZ);
|
worldMin = new Vector2(minX, minZ);
|
||||||
worldMax = new Vector2(maxX, maxZ);
|
worldMax = new Vector2(maxX, maxZ);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"com.unity.inputsystem": "1.19.0",
|
"com.unity.inputsystem": "1.19.0",
|
||||||
"com.unity.multiplayer.center": "1.0.1",
|
"com.unity.multiplayer.center": "1.0.1",
|
||||||
"com.unity.netcode.gameobjects": "2.11.0",
|
"com.unity.netcode.gameobjects": "2.11.0",
|
||||||
|
"com.unity.probuilder": "6.0.9",
|
||||||
"com.unity.render-pipelines.universal": "17.4.0",
|
"com.unity.render-pipelines.universal": "17.4.0",
|
||||||
"com.unity.sdk.linux-x86_64": "1.0.2",
|
"com.unity.sdk.linux-x86_64": "1.0.2",
|
||||||
"com.unity.services.multiplayer": "2.1.3",
|
"com.unity.services.multiplayer": "2.1.3",
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,18 @@
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
|
"com.unity.probuilder": {
|
||||||
|
"version": "6.0.9",
|
||||||
|
"depth": 0,
|
||||||
|
"source": "registry",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.shadergraph": "17.0.3",
|
||||||
|
"com.unity.modules.imgui": "1.0.0",
|
||||||
|
"com.unity.modules.physics": "1.0.0",
|
||||||
|
"com.unity.settings-manager": "1.0.3"
|
||||||
|
},
|
||||||
|
"url": "https://packages.unity.com"
|
||||||
|
},
|
||||||
"com.unity.profiling.core": {
|
"com.unity.profiling.core": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
|
@ -308,7 +320,7 @@
|
||||||
},
|
},
|
||||||
"com.unity.settings-manager": {
|
"com.unity.settings-manager": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"depth": 2,
|
"depth": 1,
|
||||||
"source": "registry",
|
"source": "registry",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,8 @@ EditorBuildSettings:
|
||||||
- enabled: 1
|
- enabled: 1
|
||||||
path: Assets/_Project/Scenes/Levels/Main.unity
|
path: Assets/_Project/Scenes/Levels/Main.unity
|
||||||
guid: 99c9720ab356a0642a771bea13969a05
|
guid: 99c9720ab356a0642a771bea13969a05
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/_Project/Scenes/Levels/9Player.unity
|
||||||
|
guid: 74fdeec477e257d4484b38108c283d84
|
||||||
m_configObjects:
|
m_configObjects:
|
||||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
||||||
|
|
|
||||||
136
ProjectSettings/Packages/com.unity.probuilder/Settings.json
Normal file
136
ProjectSettings/Packages/com.unity.probuilder/Settings.json
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
{
|
||||||
|
"m_Dictionary": {
|
||||||
|
"m_DictionaryValues": [
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.LogLevel, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "log.level",
|
||||||
|
"value": "{\"m_Value\":3}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.LogOutput, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "log.output",
|
||||||
|
"value": "{\"m_Value\":1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "log.path",
|
||||||
|
"value": "{\"m_Value\":\"ProBuilderLog.txt\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.SemVer, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "about.identifier",
|
||||||
|
"value": "{\"m_Value\":{\"m_Major\":6,\"m_Minor\":0,\"m_Patch\":9,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.SemVer, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "preferences.version",
|
||||||
|
"value": "{\"m_Value\":{\"m_Major\":6,\"m_Minor\":0,\"m_Patch\":9,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "ShapeComponent.ResetSettings",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "mesh.newShapesSnapToGrid",
|
||||||
|
"value": "{\"m_Value\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "mesh.meshColliderIsConvex",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "lightmapping.autoUnwrapLightmapUV",
|
||||||
|
"value": "{\"m_Value\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "editor.autoRecalculateCollisions",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "experimental.enabled",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "ShapeComponent.SettingsEnabled",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "editor.backFaceSelectEnabled",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "editor.showSceneInfo",
|
||||||
|
"value": "{\"m_Value\":false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.Material, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "mesh.userMaterial",
|
||||||
|
"value": "{\"m_Value\":{\"instanceID\":0}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.Rendering.ShadowCastingMode, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "mesh.shadowCastingMode",
|
||||||
|
"value": "{\"m_Value\":1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEditor.StaticEditorFlags, UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "mesh.defaultStaticEditorFlags",
|
||||||
|
"value": "{\"m_Value\":0}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.ColliderType, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "mesh.newShapeColliderType",
|
||||||
|
"value": "{\"m_Value\":2}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.UnwrapParameters, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "lightmapping.defaultLightmapUnwrapParameters",
|
||||||
|
"value": "{\"m_Value\":{\"m_HardAngle\":88.0,\"m_PackMargin\":20.0,\"m_AngleError\":8.0,\"m_AreaError\":15.0}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
|
||||||
|
"key": "ShapeBuilder.ActiveShapeIndex",
|
||||||
|
"value": "{\"m_Value\":6}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.Vector3, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "ShapeBuilder.LastSize.Cube",
|
||||||
|
"value": "{\"m_Value\":{\"x\":5.5,\"y\":3.0,\"z\":-2.5}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.Quaternion, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "ShapeBuilder.LastRotation.Cube",
|
||||||
|
"value": "{\"m_Value\":{\"x\":0.0,\"y\":0.0,\"z\":0.0,\"w\":1.0}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.PivotLocation, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "ShapeBuilder.PivotLocation.Cube",
|
||||||
|
"value": "{\"m_Value\":0}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.Shapes.Shape, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "ShapeBuilder.Cube",
|
||||||
|
"value": "{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.SelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "s_SelectMode",
|
||||||
|
"value": "{\"m_Value\":1}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "UnityEngine.ProBuilder.RectSelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||||
|
"key": "editor.dragSelectRectMode",
|
||||||
|
"value": "{\"m_Value\":0}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue