Compare commits

..

1 Commits

Author SHA1 Message Date
846e74e03a ui a main menu a join a host lobby 2026-02-21 10:50:37 +01:00
687 changed files with 14091 additions and 224007 deletions

2
.gitignore vendored
View File

@@ -186,6 +186,7 @@ StyleCopReport.xml
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
@@ -304,6 +305,7 @@ PublishScripts/
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"visualstudiotoolsforunity.vstuc"
]
}

10
.vscode/launch.json vendored
View File

@@ -1,10 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Unity",
"type": "vstuc",
"request": "attach"
}
]
}

72
.vscode/settings.json vendored
View File

@@ -1,72 +0,0 @@
{
"files.exclude": {
"**/.DS_Store": true,
"**/.git": true,
"**/.vs": true,
"**/.gitmodules": true,
"**/.vsconfig": true,
"**/*.booproj": true,
"**/*.pidb": true,
"**/*.suo": true,
"**/*.user": true,
"**/*.userprefs": true,
"**/*.unityproj": true,
"**/*.dll": true,
"**/*.exe": true,
"**/*.pdf": true,
"**/*.mid": true,
"**/*.midi": true,
"**/*.wav": true,
"**/*.gif": true,
"**/*.ico": true,
"**/*.jpg": true,
"**/*.jpeg": true,
"**/*.png": true,
"**/*.psd": true,
"**/*.tga": true,
"**/*.tif": true,
"**/*.tiff": true,
"**/*.3ds": true,
"**/*.3DS": true,
"**/*.fbx": true,
"**/*.FBX": true,
"**/*.lxo": true,
"**/*.LXO": true,
"**/*.ma": true,
"**/*.MA": true,
"**/*.obj": true,
"**/*.OBJ": true,
"**/*.asset": true,
"**/*.cubemap": true,
"**/*.flare": true,
"**/*.mat": true,
"**/*.meta": true,
"**/*.prefab": true,
"**/*.unity": true,
"build/": true,
"Build/": true,
"Library/": true,
"library/": true,
"obj/": true,
"Obj/": true,
"Logs/": true,
"logs/": true,
"ProjectSettings/": true,
"UserSettings/": true,
"temp/": true,
"Temp/": true
},
"files.associations": {
"*.asset": "yaml",
"*.meta": "yaml",
"*.prefab": "yaml",
"*.unity": "yaml",
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.sln": "*.csproj",
"*.slnx": "*.csproj"
},
"dotnet.defaultSolution": "GeoSusGame.slnx",
"dotnet.enableWorkspaceBasedDevelopment": false
}

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 7eaf47040f6a6ba4bb9df4eab675de30
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: a369fde97a303eb4ebfe7de3af10fac4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,10 @@ Material:
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: textik
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_Name: Mask 3 Default UI
m_Shader: {fileID: 4800000, guid: 2fa3c67be732d4f38be87103967c4b10, type: 3}
m_ValidKeywords:
- UNITY_UI_ALPHACLIP
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
@@ -19,7 +18,6 @@ Material:
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@@ -62,11 +60,12 @@ Material:
m_Ints: []
m_Floats:
- _BumpScale: 1
- _ColorMask: 15
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.583
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
@@ -75,10 +74,17 @@ Material:
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _Stencil: 0
- _StencilComp: 8
- _StencilOp: 0
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _UVSec: 0
- _UseUIAlphaClip: 1
- _ZWrite: 1
m_Colors:
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
- _ClipRectRadii: {r: 50, g: 20, b: 20, a: 50}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Rounded Rect Mask 2D</title>
<link href="https://fonts.cdnfonts.com/css/inter" rel="stylesheet">
<style>
* {
/* outline: 1px solid red; */
box-sizing: border-box;
}
/* THESE ENSURE THE WEBVIEW WILL BE OF CORRECT HEIGHT
* =======================================================================
*/
html {
margin: 0px;
padding: 0px;
font-family: 'Inter';
background-color: #F7F5EB;
}
body {
margin: 0px;
padding: 0px;
}
body :first-child {
margin-top: 0px;
}
body :last-child {
margin-bottom: 0px;
}
/* FIXES SCROLLING
* =======================================================================
*/
html,
body {
overflow-x: hidden;
}
/* MAIN STYLING
* =======================================================================
*/
:root {
--default-text-color: #333333;
--muted-text-color: #666666;
--link-color: #f05675;
--muted-border-color: #dddddd;
--muted-background-color: #eeeeee;
--codeblock-background-color: #222222;
--codeblock-text-color: #ffffff;
}
body {
padding: 16px;
line-height: 1.5rem;
color: var(--default-text-color);
word-wrap: break-word;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 2rem;
margin-bottom: 1rem;
}
h1 {
font-size: 1.6rem;
}
h2 {
font-size: 1.45rem;
}
h3 {
font-size: 1.3rem;
}
h4 {
font-size: 1.2rem;
}
h5 {
font-size: 1.1rem;
}
h6 {
font-size: 1.05rem;
}
h1,
h2 {
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--muted-border-color);
}
ul,
ol {
padding-inline-start: 1.8rem;
}
a {
text-decoration-thickness: 1px;
text-underline-offset: 2px;
color: var(--link-color);
}
del {
color: var(--muted-text-color);
}
pre {
padding: 1rem;
border-radius: 4px;
color: var(--codeblock-text-color);
background-color: var(--codeblock-background-color);
overflow-x: auto;
}
:not(pre)>code {
background-color: var(--muted-background-color);
padding: 0.1rem 0.4rem;
border-radius: 3px;
}
blockquote {
margin-inline: 0px;
padding-block: 12px;
padding-left: 16px;
color: var(--muted-text-color);
border-left: 2px solid var(--muted-border-color);
font-style: italic;
}
blockquote :last-child {
margin-bottom: 0px;
}
table {
display: block;
overflow-x: auto;
border-collapse: collapse;
}
th,
td {
padding: 0.6rem 1rem;
}
th,
td {
border: 1px solid var(--muted-border-color)
}
th {
font-size: 1rem;
background-color: var(--muted-background-color);
border-bottom-width: 2px;
}
img {
max-width: 100%;
}
</style>
</head>
<body>
<h1 id="rounded-rect-mask-2d">Rounded Rect Mask 2D</h1>
<p><img src="./Resources/RRM2D_Banner.png" alt="screenshot" /><br />
available on the <a href="https://assetstore.unity.com/packages/slug/326028" rel="noopener noreferrer" target="_blank">Asset Store</a></p>
<h2 id="documentation">Documentation</h2>
<p><img src="./Resources/RRM2D_ScreenshotScene.png" alt="screenshot" /></p>
<h3 id="basics">Basics</h3>
<ul>
<li>All Graphics (<a href="https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/comp-UIVisual.html" rel="noopener noreferrer" target="_blank">Visuals</a> &amp; <a href="https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/comp-UIInteraction.html" rel="noopener noreferrer" target="_blank">Interactions</a>) components child of the Rounded Mask need the <em>Default UI (Rounded Mask)</em> material to work.</li>
<li>All TextMeshPro texts need the <em>TMP SDF-Mobile (Rounded Mask)</em> material inside the TMP_FontAsset that needs to work with the rounded corners. If your text is only clipped on the straight edges of the mask, they don't need to be changed.</li>
<li>You will need one <em>Default UI (Rounded Mask)</em> material per RoundedRectMask2D radius configuration. If you always use the same corners throughout your project, you can use the Material provided in the package. Same for the TMP_FontAssets.</li>
<li>If you have different radii in your project, either <strong>duplicate the materials</strong>, or tick the checkbox <strong>Clone maskable materials on Start</strong> to automatically create new instances of the materials (for both Default UI &amp; TMP_FontAsset's material).</li>
</ul>
<h3 id="limitations">Limitations</h3>
<ul>
<li>As Rounded RectMask2D uses a special UI shader to work, it can only be used with normal UI (ie: Images with no special material). If you have custom UI shaders for your UI you will need to tweak them to support the RoundedRectMask2D component (otherwise the corners will not be correctly clipped).</li>
<li>Moreover, you will need one Material per corners configuration. If you always use the same values, you can use one Material for all of your UI, but otherwise, RoundedRectMask2D can auto instantiate your materials at runtime, allowing you to have any mask values.</li>
<li>Softness doesn't work with RoundedRectMask2D. If you need softness in your mask, you will have to use both RoundedRectMask2D &amp; a regular RectMask2D nested into each other.</li>
</ul>
<h3 id="custom-shaders">Custom Shaders</h3>
<p>If you have custom UI shaders and would like them to work with the RoundedRectMask2D component, you will need to edit them, to support the feature. You can check how it's done in the custom Shaders (UI Default &amp; TMP_SDF Mobile). <br />
Here is what you need to do:</p>
<ol>
<li>Either move your shaders to the AntoineCherel/RoundedRectMask2D/Shaders folder, or copy &amp; paste the <em>RoundedMaskCommon.hlsl</em> library to your custom shader's folder</li>
<li>in the <em>Pass</em>, include the library <br />
<code>#include "RoundedMaskCommon.hlsl"</code></li>
<li>in the Vertex to Fragment or Vertext to Pixel struct, add <br />
<code>float3 posLocal : TEXCOORD8;</code></li>
<li>fill it in the V2F or V2P function <br />
<code>OUT.posLocal = v.vertex.xyz;</code></li>
<li>in the frag or pixel return function, replace the existing clipping method by<br />
<code>clip( RMUnityUIClipRect(IN.posLocal.xy, _ClipRect, _ClipRectRadii) - 0.5);</code><br />
all of these added lines should be encapsulated inside <code>#ifdef UNITY_UI_CLIP_RECT</code> statements.</li>
</ol>
<p>If something goes wrong double check with the implemented shaders inside the Shaders folder.<br />
You can also reach out to me contact (at) antoinecherel.dev</p>
<h2 id="credits">Credits</h2>
<p>Unity Plugin, developped by <a href="https://www.antoinecherel.dev/" target="_blank">Antoine Cherel</a></p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1,124 @@
using UnityEditor;
using UnityEditor.UI;
using UnityEngine;
namespace ACRoundedRectMask
{
/// <summary>
/// Custom editor for the RoundedRectMask2D component.
/// </summary>
[CustomEditor(typeof(RoundedRectMask2D), true)]
[CanEditMultipleObjects]
public class RoundedRectMask2DInspector : RectMask2DEditor
{
private static GUIContent radiusContent = new GUIContent("Corner Radius", "Determines the radius of the rect mask's corners.");
private static GUIContent radiiContent = new GUIContent("Corner Radii", "Determines the radius of each corner of the rect mask.");
private static GUIContent topLeftContent = new GUIContent("Top Left", "Local -X, +Y corner.");
private static GUIContent topRightContent = new GUIContent("Top Right", "Local +X, +Y corner.");
private static GUIContent bottomLeftContent = new GUIContent("Bottom Left", "Local -X, -Y corner.");
private static GUIContent bottomRightContent = new GUIContent("Bottom Right", "Local +X, -Y corner.");
private static bool showRadii = false;
private static GUIContent paddingContent = new GUIContent("Padding");
private static GUIContent leftContent = new GUIContent("Left");
private static GUIContent rightContent = new GUIContent("Right");
private static GUIContent topContent = new GUIContent("Top");
private static GUIContent bottomContent = new GUIContent("Bottom");
private static bool showOffsets = false;
private SerializedProperty independentRadii;
private SerializedProperty radii;
private SerializedProperty padding;
private SerializedProperty cloneMaterials;
/// <inheritdoc/>
protected override void OnEnable()
{
independentRadii = serializedObject.FindProperty("independantRadii");
radii = serializedObject.FindProperty("radii");
padding = serializedObject.FindProperty("m_Padding");
cloneMaterials = serializedObject.FindProperty("cloneMaskableMaterialsOnStart");
base.OnEnable();
}
/// <summary>
/// Renders a custom inspector GUI that displays radius options and hides softness properties.
/// </summary>
public override void OnInspectorGUI()
{
EditorGUILayout.PropertyField(independentRadii);
if (independentRadii.boolValue)
{
showRadii = EditorGUILayout.Foldout(showRadii, radiiContent, true);
if (showRadii)
{
using (var check = new EditorGUI.ChangeCheckScope())
{
EditorGUI.indentLevel++;
Vector4 newRadii = radii.vector4Value;
newRadii.x = Mathf.Max(0.0f, EditorGUILayout.FloatField(topLeftContent, newRadii.x));
newRadii.y = Mathf.Max(0.0f, EditorGUILayout.FloatField(topRightContent, newRadii.y));
newRadii.z = Mathf.Max(0.0f, EditorGUILayout.FloatField(bottomLeftContent, newRadii.z));
newRadii.w = Mathf.Max(0.0f, EditorGUILayout.FloatField(bottomRightContent, newRadii.w));
if (check.changed)
{
radii.vector4Value = newRadii;
}
EditorGUI.indentLevel--;
}
}
}
else
{
using (var check = new EditorGUI.ChangeCheckScope())
{
float newRadius = radii.vector4Value.x;
newRadius = Mathf.Max(0.0f, EditorGUILayout.FloatField(radiusContent, newRadius));
if (check.changed)
{
Vector4 newRadii = radii.vector4Value;
newRadii = Vector4.one * newRadius;
radii.vector4Value = newRadii;
}
}
}
// Rather than call base.OnInspectorGUI() manually showing padding so that softness can be hidden since it is not supported by rounded rect masks.
showOffsets = EditorGUILayout.Foldout(showOffsets, paddingContent, true);
if (showOffsets)
{
using (var check = new EditorGUI.ChangeCheckScope())
{
EditorGUI.indentLevel++;
Vector4 newPadding = padding.vector4Value;
newPadding.x = EditorGUILayout.FloatField(leftContent, newPadding.x);
newPadding.z = EditorGUILayout.FloatField(rightContent, newPadding.z);
newPadding.w = EditorGUILayout.FloatField(topContent, newPadding.w);
newPadding.y = EditorGUILayout.FloatField(bottomContent, newPadding.y);
if (check.changed)
{
padding.vector4Value = newPadding;
}
EditorGUI.indentLevel--;
}
}
EditorGUILayout.PropertyField(cloneMaterials);
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -7,11 +7,10 @@ Material:
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: New Material
m_Shader: {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_Name: Default UI (RoundedMask)
m_Shader: {fileID: 4800000, guid: 2fa3c67be732d4f38be87103967c4b10, type: 3}
m_ValidKeywords:
- UNITY_UI_ALPHACLIP
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
@@ -19,14 +18,9 @@ Material:
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlphaTex:
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}
@@ -65,14 +59,11 @@ Material:
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- PixelSnap: 0
- _BumpScale: 1
- _ColorMask: 15
- _CullMode: 0
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _EnableExternalAlpha: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
@@ -89,13 +80,11 @@ Material:
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _UVSec: 0
- _UseUIAlphaClip: 0
- _UseUIAlphaClip: 1
- _ZWrite: 1
m_Colors:
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
- _ClipRectRadii: {r: 20, g: 20, b: 20, a: 20}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _Flip: {r: 1, g: 1, b: 1, a: 1}
- _RendererColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,359 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Reflection;
using TMPro;
#if UNITY_2021_1_OR_NEWER
using UnityEngine.Pool;
#endif
namespace ACRoundedRectMask
{
/// <summary>
/// Overrides the RectMask2D.PerformClipping method to add extra checks before doing exhaustive culling on
/// each maskable target.
/// </summary>
public class RoundedRectMask2D : RectMask2D
{
public static readonly string RadiiPropertyName = "_ClipRectRadii";
[SerializeField]
private bool independantRadii;
[Tooltip("The four corner radii of the rounded rect. (x: top left, y: top right, z: bottom left, w: bottom right)")]
[SerializeField]
private Vector4 radii = Vector4.one * 10.0f;
public Vector4 Radii
{
get => radii;
set
{
radii = value;
MaskUtilities.Notify2DMaskStateChanged(this);
ForceClip = true;
}
}
[Tooltip("If not set to true, you will need to handle that all masked UI elements have their own material instances")]
[SerializeField]
private bool cloneMaskableMaterialsOnStart = true;
private static int clipRectRadiiID = 0;
private HashSet<IClippable> clipTargets = null;
private HashSet<MaskableGraphic> maskableTargets = null;
private int lastclipTargetsCount = 0;
private int lastmaskableTargetsCount = 0;
private bool shouldRecalculateClipRects = false;
private Canvas cachedCanvas = null;
private Vector3[] cachedCorners = new Vector3[4];
private Rect lastClipRectCanvasSpace = new Rect();
private Vector2Int lastSoftness = new Vector2Int();
private List<RectMask2D> clippers = new List<RectMask2D>();
#region MonoBehaviour Implementation
/// <inheritdoc />
protected override void OnEnable()
{
base.OnEnable();
shouldRecalculateClipRects = true;
ForceClip = true;
}
#if UNITY_EDITOR
/// <inheritdoc />
protected override void OnValidate()
{
base.OnValidate();
shouldRecalculateClipRects = true;
ForceClip = true;
}
#endif
/// <inheritdoc />
protected override void OnDidApplyAnimationProperties()
{
base.OnDidApplyAnimationProperties();
shouldRecalculateClipRects = true;
ForceClip = true;
}
#endregion MonoBehaviour Implementation
#region RectMask2D Implementation
/// <inheritdoc />
protected override void OnTransformParentChanged()
{
base.OnTransformParentChanged();
shouldRecalculateClipRects = true;
}
/// <inheritdoc />
protected override void OnCanvasHierarchyChanged()
{
cachedCanvas = null;
base.OnCanvasHierarchyChanged();
shouldRecalculateClipRects = true;
}
protected override void Start()
{
base.Start();
shouldRecalculateClipRects = true;
PerformClipping();
if (cloneMaskableMaterialsOnStart && maskableTargets != null)
{
foreach (MaskableGraphic mg in maskableTargets)
{
if (mg.materialForRendering.Equals(mg.material))
{
Material m = new Material(mg.material);
mg.material = m;
}
else if (mg is TMP_Text tmpText)
{
Material m = new Material(tmpText.fontMaterial);
tmpText.fontMaterial = m;
}
else
{
Debug.Log("[RoundedRectMask2d] Can't clone material for " + mg.name + ". This will result in same rounded corners for all assets sharing its materiel " + mg.materialForRendering);
continue;
}
OnSetClipRect(mg);
}
}
}
/// <summary>
/// Improves the base class method by:
/// - Checks if the canvas renderer has moved before exhaustive culling.
/// - Interleaves UpdateClipSoftness so objects are not iterated over twice.
/// - Adds a OnSetClipRect callback for derived classes to use.
/// </summary>
public override void PerformClipping()
{
// Not calling the base class method intentionally to provide a more optimal version.
//base.PerformClipping();
if (clipRectRadiiID == 0)
{
clipRectRadiiID = Shader.PropertyToID(RadiiPropertyName);
}
Initialize();
if (ReferenceEquals(Canvas, null))
{
return;
}
//TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)
// if the parents are changed
// or something similar we
// do a recalculate here
if (shouldRecalculateClipRects || ForceClip)
{
MaskUtilities.GetRectMasksForClip(this, clippers);
shouldRecalculateClipRects = false;
}
// get the compound rects from
// the clippers that are valid
bool validRect = true;
Rect clipRect = Clipping.FindCullAndClipWorldRect(clippers, out validRect);
// If the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect
// overlaps that of the root canvas.
RenderMode renderMode = Canvas.rootCanvas.renderMode;
bool maskIsCulled =
(renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
!clipRect.Overlaps(RootCanvasRect, true);
if (maskIsCulled)
{
// Children are only displayed when inside the mask. If the mask is culled, then the children
// inside the mask are also culled. In that situation, we pass an invalid rect to allow callees
// to avoid some processing.
clipRect = Rect.zero;
validRect = false;
}
if (clipRect != lastClipRectCanvasSpace || softness != lastSoftness)
{
foreach (IClippable clipTarget in clipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
clipTarget.SetClipSoftness(softness);
}
foreach (MaskableGraphic maskableTarget in maskableTargets)
{
maskableTarget.SetClipRect(clipRect, validRect);
maskableTarget.SetClipSoftness(softness);
OnSetClipRect(maskableTarget);
maskableTarget.Cull(clipRect, validRect);
}
}
else if (ForceClip)
{
foreach (IClippable clipTarget in clipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
clipTarget.SetClipSoftness(softness);
}
foreach (MaskableGraphic maskableTarget in maskableTargets)
{
maskableTarget.SetClipRect(clipRect, validRect);
maskableTarget.SetClipSoftness(softness);
OnSetClipRect(maskableTarget);
if (maskableTarget.canvasRenderer.hasMoved)
{
maskableTarget.Cull(clipRect, validRect);
}
}
}
else
{
foreach (MaskableGraphic maskableTarget in maskableTargets)
{
if (!maskableTarget.canvasRenderer.hasMoved)
{
continue;
}
maskableTarget.Cull(clipRect, validRect);
}
}
ForceClip = false;
lastClipRectCanvasSpace = clipRect;
lastSoftness = softness;
}
#endregion RectMask2D Implementation
public bool ForceClip
{
get
{
// This is an imprecise check if a clip or mask target gets added then removed on the same frame.
// But... the alternative is we reflect into m_ForceClip base member which would be a per frame allocation due to it being a value type.
// If this check is return false negatives in your scenario, then set ForceClip to true.
return clipTargets.Count != lastclipTargetsCount ||
maskableTargets.Count != lastmaskableTargetsCount;
}
set
{
if (value == true)
{
lastclipTargetsCount = 0;
lastmaskableTargetsCount = 0;
}
else
{
Initialize();
lastclipTargetsCount = clipTargets.Count;
lastmaskableTargetsCount = maskableTargets.Count;
}
}
}
/// <summary>
/// Callback whenever the clip rect is mutated.
/// </summary>
protected virtual void OnSetClipRect(IClippable clippable)
{
}
/// <summary>
/// Callback whenever the clip rect is mutated.
/// </summary>
protected virtual void OnSetClipRect(MaskableGraphic maskableTarget)
{
Material targetMaterial = maskableTarget.materialForRendering;
if (targetMaterial != null)
{
targetMaterial.SetVector(clipRectRadiiID, Radii);
}
Debug.Log("Setting clip rect for " + maskableTarget.name);
}
private void Initialize()
{
// Check if we have already initialized.
if (clipTargets != null)
{
return;
}
// Many of the properties we need access to for clipping are not exposed. So, we have to do reflection to get access to them.
BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
clipTargets = (HashSet<IClippable>)typeof(RectMask2D).GetField("m_ClipTargets", bindFlags).GetValue(this);
maskableTargets = (HashSet<MaskableGraphic>)typeof(RectMask2D).GetField("m_MaskableTargets", bindFlags).GetValue(this);
}
private Canvas Canvas
{
get
{
if (cachedCanvas == null)
{
#if UNITY_2021_1_OR_NEWER
var list = ListPool<Canvas>.Get();
gameObject.GetComponentsInParent(false, list);
if (list.Count > 0)
cachedCanvas = list[list.Count - 1];
else
cachedCanvas = null;
ListPool<Canvas>.Release(list);
#else
var list = gameObject.GetComponentsInParent<Canvas>(false);
if (list.Length > 0)
cachedCanvas = list[list.Length - 1];
else
cachedCanvas = null;
#endif
}
return cachedCanvas;
}
}
private Rect RootCanvasRect
{
get
{
rectTransform.GetWorldCorners(cachedCorners);
if (!ReferenceEquals(Canvas, null))
{
Canvas rootCanvas = Canvas.rootCanvas;
for (int i = 0; i < 4; ++i)
cachedCorners[i] = rootCanvas.transform.InverseTransformPoint(cachedCorners[i]);
}
return new Rect(cachedCorners[0].x, cachedCorners[0].y, cachedCorners[2].x - cachedCorners[0].x, cachedCorners[2].y - cachedCorners[0].y);
}
}
}
}

View File

@@ -0,0 +1,112 @@
#ifndef RM_COMMON
#define RM_COMMON
/// <summary>
/// Constants
/// </summary>
#define RM_PI 3.14159265359
#define RM_DEGREES_TO_RADIANS (RM_PI / 180.0)
#define RM_HALF_MIN 6.103515625e-5 // 2^-14, the same value for 10, 11 and 16-bit: https://www.khronos.org/opengl/wiki/Small_Float_Formats
#define RM_MIN_CORNER_VALUE 1e-3
#define RM_MIN_CORNER_VALUE_RECT 1e-3
/// <summary>
/// SDF methods.
/// </summary>
inline float RMPointVsRoundedBox(in float2 position, in float2 cornerCircleDistance, in float cornerCircleRadius)
{
return length(max(abs(position) - cornerCircleDistance, 0.0)) - cornerCircleRadius;
}
inline float FilterDistance(in float distance)
{
float pixelDistance = distance / fwidth(distance);
#if defined(_INDEPENDENT_CORNERS) || defined(_UI_CLIP_RECT_ROUNDED_INDEPENDENT)
// To avoid artifacts at discontinuities in the SDF distance increase the pixel width.
return saturate(1.0 - pixelDistance);
#else
return saturate(0.5 - pixelDistance);
#endif
}
inline float RMRoundCornersSmooth(in float2 position, in float2 cornerCircleDistance, in float cornerCircleRadius, in float smoothingValue)
{
float distance = RMPointVsRoundedBox(position, cornerCircleDistance, cornerCircleRadius);
#if defined(_EDGE_SMOOTHING_AUTOMATIC)
return FilterDistance(distance);
#else
return smoothstep(1.0, 0.0, distance / smoothingValue);
#endif
}
inline float RMRoundCorners(in float2 position, in float2 cornerCircleDistance, in float cornerCircleRadius, in float smoothingValue)
{
#if defined(_TRANSPARENT)
return RMRoundCornersSmooth(position, cornerCircleDistance, cornerCircleRadius, smoothingValue);
#else
return (RMPointVsRoundedBox(position, cornerCircleDistance, cornerCircleRadius) < 0.0);
#endif
}
inline float RMFindCornerRadius(in float2 uv, in float4 radii)
{
if (uv.x < 0.5)
{
if (uv.y > 0.5) { return radii.x; } // Top left.
else { return radii.z; } // Bottom left.
}
else
{
if (uv.y > 0.5) { return radii.y; } // Top right.
else { return radii.w; } // Bottom right.
}
}
/// <summary>
/// UnityUI methods.
/// </summary>
inline float RMGet2DClippingRounded(in float2 position, in float4 clipRect, in float radius)
{
float2 halfSize = (clipRect.zw - clipRect.xy) * 0.5;
float2 center = clipRect.xy + halfSize;
float2 offset = position - center;
return RMPointVsRoundedBox(offset, halfSize - radius, radius);
}
inline float RMGet2DClippingRoundedSoft(in float2 position, in float4 clipRect, in float radius)
{
return saturate(FilterDistance(RMGet2DClippingRounded(position, clipRect, radius)));
}
inline float RMGet2DClippingRoundedIndependent(in float2 position, in float4 clipRect, in float4 radii)
{
float2 halfSize = (clipRect.zw - clipRect.xy) * 0.5;
float2 center = clipRect.xy + halfSize;
float2 offset = position - center;
float radius = RMFindCornerRadius(offset, radii);
return RMPointVsRoundedBox(offset, halfSize - radius, radius);
}
inline float RMGet2DClippingRoundedIndependentSoft(in float2 position, in float4 clipRect, in float4 radii)
{
return saturate(FilterDistance(RMGet2DClippingRoundedIndependent(position, clipRect, radii)));
}
inline float RMUnityUIClipRect(in float2 position, in float4 clipRect, in float4 radii)
{
radii = max(radii, RM_MIN_CORNER_VALUE_RECT);
#if defined(UNITY_UI_ALPHACLIP)
return RMGet2DClippingRoundedIndependent(position, clipRect, radii) <= 0.0;
#else
return RMGet2DClippingRoundedIndependentSoft(position, clipRect, radii);
#endif
}
#endif // RM_COMMON

View File

@@ -0,0 +1,126 @@
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
Shader "RoundedMask/UI/Default (RoundedMask)"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
[HideInInspector] _ClipRect("Clip Rect", Vector) = (-32767.0, -32767.0, 32767.0, 32767.0)
[HideInInspector] _ClipRectRadii("Clip Rect Radii", Vector) = (10.0, 10.0, 10.0, 10.0)
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#include "RoundedMaskCommon.hlsl"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
#ifdef UNITY_UI_CLIP_RECT
float3 posLocal : TEXCOORD8;
#endif
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _ClipRectRadii;
float4 _MainTex_ST;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
#ifdef UNITY_UI_CLIP_RECT
OUT.posLocal = v.vertex.xyz;
#endif
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
#ifdef UNITY_UI_CLIP_RECT
clip( RMUnityUIClipRect(IN.posLocal.xy, _ClipRect, _ClipRectRadii) - 0.5);
#endif
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}

View File

@@ -0,0 +1,249 @@
// Simplified SDF shader:
// - No Shading Option (bevel / bump / env map)
// - No Glow Option
// - Softness is applied on both side of the outline
Shader "RoundedMask/TextMeshPro/Mobile/Distance Field (RoundedMask)" {
Properties {
[HDR]_FaceColor ("Face Color", Color) = (1,1,1,1)
_FaceDilate ("Face Dilate", Range(-1,1)) = 0
[HDR]_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_OutlineWidth ("Outline Thickness", Range(0,1)) = 0
_OutlineSoftness ("Outline Softness", Range(0,1)) = 0
[HDR]_UnderlayColor ("Border Color", Color) = (0,0,0,.5)
_UnderlayOffsetX ("Border OffsetX", Range(-1,1)) = 0
_UnderlayOffsetY ("Border OffsetY", Range(-1,1)) = 0
_UnderlayDilate ("Border Dilate", Range(-1,1)) = 0
_UnderlaySoftness ("Border Softness", Range(0,1)) = 0
_WeightNormal ("Weight Normal", float) = 0
_WeightBold ("Weight Bold", float) = .5
_ShaderFlags ("Flags", float) = 0
_ScaleRatioA ("Scale RatioA", float) = 1
_ScaleRatioB ("Scale RatioB", float) = 1
_ScaleRatioC ("Scale RatioC", float) = 1
_MainTex ("Font Atlas", 2D) = "white" {}
_TextureWidth ("Texture Width", float) = 512
_TextureHeight ("Texture Height", float) = 512
_GradientScale ("Gradient Scale", float) = 5
_ScaleX ("Scale X", float) = 1
_ScaleY ("Scale Y", float) = 1
_PerspectiveFilter ("Perspective Correction", Range(0, 1)) = 0.875
_Sharpness ("Sharpness", Range(-1,1)) = 0
_VertexOffsetX ("Vertex OffsetX", float) = 0
_VertexOffsetY ("Vertex OffsetY", float) = 0
_ClipRect ("Clip Rect", vector) = (-32767, -32767, 32767, 32767)
[HideInInspector]
_ClipRectRadii ("Clip Rect Radii", vector) = (10.0, 10.0, 10.0, 10.0)
_MaskSoftnessX ("Mask SoftnessX", float) = 0
_MaskSoftnessY ("Mask SoftnessY", float) = 0
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_CullMode ("Cull Mode", Float) = 0
_ColorMask ("Color Mask", Float) = 15
}
SubShader {
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull [_CullMode]
ZWrite Off
Lighting Off
Fog { Mode Off }
ZTest [unity_GUIZTestMode]
Blend One OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass {
CGPROGRAM
#pragma vertex VertShader
#pragma fragment PixShader
#pragma shader_feature __ OUTLINE_ON
#pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER
#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#include "RoundedMaskCommon.hlsl"
#include "Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc"
struct vertex_t {
UNITY_VERTEX_INPUT_INSTANCE_ID
float4 vertex : POSITION;
float3 normal : NORMAL;
fixed4 color : COLOR;
float2 texcoord0 : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
};
struct pixel_t {
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
float4 vertex : SV_POSITION;
fixed4 faceColor : COLOR;
fixed4 outlineColor : COLOR1;
float4 texcoord0 : TEXCOORD0; // Texture UV, Mask UV
half4 param : TEXCOORD1; // Scale(x), BiasIn(y), BiasOut(z), Bias(w)
half4 mask : TEXCOORD2; // Position in clip space(xy), Softness(zw)
#if (UNDERLAY_ON | UNDERLAY_INNER)
float4 texcoord1 : TEXCOORD3; // Texture UV, alpha, reserved
half2 underlayParam : TEXCOORD4; // Scale(x), Bias(y)
#endif
#ifdef UNITY_UI_CLIP_RECT
float3 posLocal : TEXCOORD8;
#endif
};
float4 _ClipRectRadii;
pixel_t VertShader(vertex_t input)
{
pixel_t output;
UNITY_INITIALIZE_OUTPUT(pixel_t, output);
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
float bold = step(input.texcoord1.y, 0);
float4 vert = input.vertex;
vert.x += _VertexOffsetX;
vert.y += _VertexOffsetY;
float4 vPosition = UnityObjectToClipPos(vert);
float2 pixelSize = vPosition.w;
pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
float scale = rsqrt(dot(pixelSize, pixelSize));
scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);
if(UNITY_MATRIX_P[3][3] == 0) scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));
float weight = lerp(_WeightNormal, _WeightBold, bold) / 4.0;
weight = (weight + _FaceDilate) * _ScaleRatioA * 0.5;
float layerScale = scale;
scale /= 1 + (_OutlineSoftness * _ScaleRatioA * scale);
float bias = (0.5 - weight) * scale - 0.5;
float outline = _OutlineWidth * _ScaleRatioA * 0.5 * scale;
float opacity = input.color.a;
#if (UNDERLAY_ON | UNDERLAY_INNER)
opacity = 1.0;
#endif
fixed4 faceColor = fixed4(input.color.rgb, opacity) * _FaceColor;
faceColor.rgb *= faceColor.a;
fixed4 outlineColor = _OutlineColor;
outlineColor.a *= opacity;
outlineColor.rgb *= outlineColor.a;
outlineColor = lerp(faceColor, outlineColor, sqrt(min(1.0, (outline * 2))));
#if (UNDERLAY_ON | UNDERLAY_INNER)
layerScale /= 1 + ((_UnderlaySoftness * _ScaleRatioC) * layerScale);
float layerBias = (.5 - weight) * layerScale - .5 - ((_UnderlayDilate * _ScaleRatioC) * .5 * layerScale);
float x = -(_UnderlayOffsetX * _ScaleRatioC) * _GradientScale / _TextureWidth;
float y = -(_UnderlayOffsetY * _ScaleRatioC) * _GradientScale / _TextureHeight;
float2 layerOffset = float2(x, y);
#endif
// Generate UV for the Masking Texture
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (vert.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
// Populate structure for pixel shader
output.vertex = vPosition;
output.faceColor = faceColor;
output.outlineColor = outlineColor;
output.texcoord0 = float4(input.texcoord0.x, input.texcoord0.y, maskUV.x, maskUV.y);
output.param = half4(scale, bias - outline, bias + outline, bias);
output.mask = half4(vert.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_MaskSoftnessX, _MaskSoftnessY) + pixelSize.xy));
#if (UNDERLAY_ON || UNDERLAY_INNER)
output.texcoord1 = float4(input.texcoord0 + layerOffset, input.color.a, 0);
output.underlayParam = half2(layerScale, layerBias);
#endif
#ifdef UNITY_UI_CLIP_RECT
output.posLocal = vert.xyz;
#endif
return output;
}
// PIXEL SHADER
fixed4 PixShader(pixel_t input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
half d = tex2D(_MainTex, input.texcoord0.xy).a * input.param.x;
half4 c = input.faceColor * saturate(d - input.param.w);
#ifdef OUTLINE_ON
c = lerp(input.outlineColor, input.faceColor, saturate(d - input.param.z));
c *= saturate(d - input.param.y);
#endif
#if UNDERLAY_ON
d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;
c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * saturate(d - input.underlayParam.y) * (1 - c.a);
#endif
#if UNDERLAY_INNER
half sd = saturate(d - input.param.z);
d = tex2D(_MainTex, input.texcoord1.xy).a * input.underlayParam.x;
c += float4(_UnderlayColor.rgb * _UnderlayColor.a, _UnderlayColor.a) * (1 - saturate(d - input.underlayParam.y)) * sd * (1 - c.a);
#endif
// Alternative implementation to UnityGet2DClipping with support for softness.
#if UNITY_UI_CLIP_RECT
clip( RMUnityUIClipRect(input.posLocal.xy, _ClipRect, _ClipRectRadii) - 0.5);
#endif
#if (UNDERLAY_ON | UNDERLAY_INNER)
c *= input.texcoord1.z;
#endif
#if UNITY_UI_ALPHACLIP
clip(c.a - 0.001);
#endif
return c;
}
ENDCG
}
}
CustomEditor "TMPro.EditorUtilities.TMP_SDFShaderGUI"
}

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 50a0b21c151e150428fd2803d6b95db0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: bc06bb57786c7e142b06ec231e5cf709

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 1d2251b279edb0147bd274a884ac878b

View File

@@ -132,20 +132,7 @@ public class GameClient : IDisposable
return false;
}
/// <summary>
/// Tears down the socket and crypto session. When `transient` is true
/// (network drop, decrypt-failure cascade, anything we expect to retry),
/// the lobby/role/task/state caches are preserved so the post-reconnect
/// flow can re-associate via Reconnect(LobbyId). Default false matches
/// pre-P9 behavior (full state wipe) for explicit user disconnects.
///
/// Critical for the P9 reconnect bug: previously every Disconnect path
/// nuked LobbyId, so by the time GameManager_Network's reconnect coroutine
/// fired, the client had no idea which lobby it had been in - the
/// post-handshake Reconnect call had nothing to send and the server
/// answered the next vote/action with NOT_IN_LOBBY.
/// </summary>
public void Disconnect(string reason = "User disconnected", bool transient = false)
public void Disconnect(string reason = "User disconnected")
{
_cts?.Cancel();
_tcpClient?.Close();
@@ -154,8 +141,6 @@ public class GameClient : IDisposable
_encryption?.Dispose();
_encryption = null;
if (!transient)
{
LobbyId = null;
JoinCode = null;
CurrentLobbyState = null;
@@ -163,11 +148,6 @@ public class GameClient : IDisposable
MyTasks.Clear();
PlayerPositions.Clear();
Bodies.Clear();
}
// PlayerPositions are stale anyway after a drop, but we keep them so
// the UI doesn't blink avatars off-map mid-meeting; the next position
// broadcast overwrites them. LastEventId is intentionally preserved
// so the Reconnect message can ask the server for missed events.
Dispatcher.Post(() => OnDisconnected?.Invoke(reason));
}
@@ -256,8 +236,7 @@ public class GameClient : IDisposable
decryptFailures++;
if (decryptFailures >= 3)
{
// Transient: keep LobbyId for the reconnect coroutine.
Disconnect("Too many decryption failures", transient: true);
Disconnect("Too many decryption failures");
return;
}
continue;
@@ -274,9 +253,7 @@ public class GameClient : IDisposable
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
// Transient: TCP RST / read failure is exactly what reconnect was
// designed for. Keep LobbyId so post-reconnect flow can re-attach.
Disconnect($"Connection error: {ex.Message}", transient: true);
Disconnect($"Connection error: {ex.Message}");
}
}
@@ -316,35 +293,7 @@ public class GameClient : IDisposable
{
LobbyId = r.LobbyId;
JoinCode = r.JoinCode;
// Ensure we always have a valid lobby state with the creator as owner
if (r.LobbyState != null)
{
CurrentLobbyState = r.LobbyState;
if (string.IsNullOrEmpty(CurrentLobbyState.OwnerId))
CurrentLobbyState.OwnerId = ClientUuid;
}
else
{
CurrentLobbyState = new LobbyState
{
LobbyId = r.LobbyId ?? "",
JoinCode = r.JoinCode ?? "",
OwnerId = ClientUuid
};
}
// Make sure creator appears in the player list
if (CurrentLobbyState.Players == null)
CurrentLobbyState.Players = new System.Collections.Generic.List<PlayerInfo>();
if (!CurrentLobbyState.Players.Any(p => p.ClientUuid == ClientUuid))
{
CurrentLobbyState.Players.Insert(0, new PlayerInfo
{
ClientUuid = ClientUuid,
DisplayName = DisplayName,
IsOwner = true,
State = PlayerState.Alive
});
}
}
break;
@@ -354,22 +303,6 @@ public class GameClient : IDisposable
LobbyId = r.LobbyId;
CurrentLobbyState = r.LobbyState;
JoinCode = r.LobbyState?.JoinCode;
// Ensure self is in the player list
if (CurrentLobbyState != null)
{
if (CurrentLobbyState.Players == null)
CurrentLobbyState.Players = new System.Collections.Generic.List<PlayerInfo>();
if (!CurrentLobbyState.Players.Any(p => p.ClientUuid == ClientUuid))
{
CurrentLobbyState.Players.Add(new PlayerInfo
{
ClientUuid = ClientUuid,
DisplayName = DisplayName,
IsOwner = CurrentLobbyState.OwnerId == ClientUuid,
State = PlayerState.Alive
});
}
}
}
break;
@@ -411,6 +344,7 @@ public class GameClient : IDisposable
var joinedPayload = evt.GetPayload<PlayerJoinedPayload>();
if (joinedPayload != null && CurrentLobbyState?.Players != null)
{
// Check if player already exists
bool exists = CurrentLobbyState.Players.Any(p => p.ClientUuid == joinedPayload.ClientUuid);
if (!exists)
{
@@ -418,7 +352,7 @@ public class GameClient : IDisposable
{
ClientUuid = joinedPayload.ClientUuid,
DisplayName = joinedPayload.DisplayName,
IsOwner = joinedPayload.ClientUuid == CurrentLobbyState.OwnerId,
IsOwner = false,
IsReady = false,
State = PlayerState.Alive
});
@@ -531,22 +465,15 @@ public class GameClient : IDisposable
#region Game Actions
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500, GameSettingsOverrides? settings = null)
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500)
{
// DisplayName is sent on every CreateLobby/JoinLobby so the server
// picks up the live nickname (typed into the input field after the
// ClientHello handshake fired). Without this the server uses the
// ClientHello-time name, which is the GameManager prefab default
// for any user who immediately created/joined a lobby.
Send(new CreateLobby
{
PlayAreaCenter = center,
PlayAreaRadius = playAreaRadius,
ImpostorCount = impostorCount,
TaskCount = taskCount,
Password = password,
Settings = settings,
DisplayName = DisplayName
Password = password
});
}
@@ -555,8 +482,7 @@ public class GameClient : IDisposable
Send(new JoinLobby
{
JoinCode = joinCode.ToUpperInvariant(),
Password = password,
DisplayName = DisplayName
Password = password
});
}
@@ -565,7 +491,6 @@ public class GameClient : IDisposable
Send(new LeaveLobby());
LobbyId = null;
JoinCode = null;
CurrentLobbyState = null;
}
public void StartGame()

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 91e0f647c37b0b94b83f53bb854db28c

View File

@@ -39,9 +39,8 @@ namespace GeoSus.Client
return R * c;
}
public static bool operator ==(Position left, Position right) { if (left.Lat == right.Lat && left.Lon == right.Lon) { return true; } else { return false; } }
public static bool operator !=(Position left, Position right) { return !(left == right); }
}
[JsonConverter(typeof(StringEnumConverter))]
public enum PlayerRole { Crew, Impostor }
@@ -49,11 +48,6 @@ public enum PlayerRole { Crew, Impostor }
public enum PlayerState { Alive, Dead }
[JsonConverter(typeof(StringEnumConverter))]
// NOTE: `Voting` is reserved-but-unused on the wire as of 2026. The server
// keeps the entire vote cycle inside `Meeting` and uses MeetingStartedPayload
// timestamps (DiscussionEndTime / VotingEndTime) to distinguish sub-phases.
// The enum value is preserved here for serialization compatibility with old
// saves; new code should not assign it.
public enum GamePhase { Lobby, Loading, Playing, Meeting, Voting, Ended }
[JsonConverter(typeof(StringEnumConverter))]
@@ -189,24 +183,6 @@ public class CreateLobby : Message
[JsonProperty("taskCount")]
public int TaskCount { get; set; } = 5;
/// <summary>
/// P13b: optional per-lobby settings overrides supplied by the host.
/// Any field left null falls through to the server's current default
/// (snapshotted at lobby creation, immutable thereafter for this lobby).
/// </summary>
[JsonProperty("settings")]
public GameSettingsOverrides? Settings { get; set; }
/// <summary>
/// Optional. Live host display name from the nickname input field at
/// the moment of CreateLobby. ClientHello-time name is stale because
/// the handshake fires before the user has typed anything; this lets
/// the server pick up the freshly-typed name without a separate
/// rename round-trip.
/// </summary>
[JsonProperty("displayName")]
public string? DisplayName { get; set; }
}
public class CreateLobbyResponse : Message
@@ -238,13 +214,6 @@ public class JoinLobby : Message
[JsonProperty("password")]
public string? Password { get; set; }
/// <summary>
/// Optional. Live joiner display name from the nickname input field
/// at the moment of Join. See CreateLobby.DisplayName for rationale.
/// </summary>
[JsonProperty("displayName")]
public string? DisplayName { get; set; }
}
public class JoinLobbyResponse : Message
@@ -653,15 +622,6 @@ public class PlayerEjectedPayload
public PlayerRole Role { get; set; }
}
public class TaskStartedPayload
{
[JsonProperty("clientUuid")]
public string ClientUuid { get; set; } = "";
[JsonProperty("taskId")]
public string TaskId { get; set; } = "";
}
public class TaskCompletedPayload
{
[JsonProperty("clientUuid")]
@@ -829,162 +789,6 @@ public class LobbyState
/// <summary>True if map data has been loaded (or Overpass is disabled)</summary>
[JsonProperty("mapDataReady")]
public bool MapDataReady { get; set; } = true;
/// <summary>
/// P13b: full per-lobby settings snapshot. Clients use this for HUD
/// (button visibility, countdown timings, etc.) instead of hardcoded
/// values. Always populated for new server builds; old client builds
/// can ignore the field.
/// </summary>
[JsonProperty("settings")]
public GameSettings? Settings { get; set; }
}
/// <summary>
/// P13b: per-lobby gameplay settings on the wire. Server populates this from
/// its per-lobby snapshot so clients can drive HUD logic from authoritative
/// values rather than hardcoded constants.
/// </summary>
public class GameSettings
{
// Round shape
[JsonProperty("maxPlayers")]
public int MaxPlayers { get; set; }
[JsonProperty("impostorCount")]
public int ImpostorCount { get; set; }
[JsonProperty("taskCount")]
public int TaskCount { get; set; }
[JsonProperty("tiePolicy")]
public string TiePolicy { get; set; } = "NoEject";
// Distances (m)
[JsonProperty("killDistanceM")]
public double KillDistanceM { get; set; }
[JsonProperty("reportDistanceM")]
public double ReportDistanceM { get; set; }
[JsonProperty("taskStartDistanceM")]
public double TaskStartDistanceM { get; set; }
[JsonProperty("meetingArrivalRadiusM")]
public double MeetingArrivalRadiusM { get; set; }
[JsonProperty("emergencyMeetingCallRadiusM")]
public double EmergencyMeetingCallRadiusM { get; set; }
[JsonProperty("repairStationDistanceM")]
public double RepairStationDistanceM { get; set; }
// Cooldowns / counts
[JsonProperty("killCooldownMs")]
public int KillCooldownMs { get; set; }
[JsonProperty("emergencyMeetingCooldownMs")]
public int EmergencyMeetingCooldownMs { get; set; }
[JsonProperty("maxEmergencyMeetingsPerPlayer")]
public int MaxEmergencyMeetingsPerPlayer { get; set; }
// Meeting phases (ms)
[JsonProperty("arrivalBaseMs")]
public int ArrivalBaseMs { get; set; }
[JsonProperty("allowedLateMs")]
public int AllowedLateMs { get; set; }
[JsonProperty("discussionPhaseMs")]
public int DiscussionPhaseMs { get; set; }
[JsonProperty("votingPhaseMs")]
public int VotingPhaseMs { get; set; }
// Sabotage
[JsonProperty("sabotageCooldownMs")]
public int SabotageCooldownMs { get; set; }
[JsonProperty("commsBlackoutDurationMs")]
public int CommsBlackoutDurationMs { get; set; }
[JsonProperty("criticalMeltdownDeadlineMs")]
public int CriticalMeltdownDeadlineMs { get; set; }
[JsonProperty("repairStationHoldMs")]
public int RepairStationHoldMs { get; set; }
}
/// <summary>
/// P13b: host-supplied overrides at CreateLobby. Every field is nullable so
/// the host can opt into changing only what they care about; null = use the
/// server's current default at the moment of lobby creation.
/// </summary>
public class GameSettingsOverrides
{
[JsonProperty("maxPlayers")]
public int? MaxPlayers { get; set; }
[JsonProperty("impostorCount")]
public int? ImpostorCount { get; set; }
[JsonProperty("taskCount")]
public int? TaskCount { get; set; }
[JsonProperty("tiePolicy")]
public string? TiePolicy { get; set; }
[JsonProperty("killDistanceM")]
public double? KillDistanceM { get; set; }
[JsonProperty("reportDistanceM")]
public double? ReportDistanceM { get; set; }
[JsonProperty("taskStartDistanceM")]
public double? TaskStartDistanceM { get; set; }
[JsonProperty("meetingArrivalRadiusM")]
public double? MeetingArrivalRadiusM { get; set; }
[JsonProperty("emergencyMeetingCallRadiusM")]
public double? EmergencyMeetingCallRadiusM { get; set; }
[JsonProperty("repairStationDistanceM")]
public double? RepairStationDistanceM { get; set; }
[JsonProperty("killCooldownMs")]
public int? KillCooldownMs { get; set; }
[JsonProperty("emergencyMeetingCooldownMs")]
public int? EmergencyMeetingCooldownMs { get; set; }
[JsonProperty("maxEmergencyMeetingsPerPlayer")]
public int? MaxEmergencyMeetingsPerPlayer { get; set; }
[JsonProperty("arrivalBaseMs")]
public int? ArrivalBaseMs { get; set; }
[JsonProperty("allowedLateMs")]
public int? AllowedLateMs { get; set; }
[JsonProperty("discussionPhaseMs")]
public int? DiscussionPhaseMs { get; set; }
[JsonProperty("votingPhaseMs")]
public int? VotingPhaseMs { get; set; }
[JsonProperty("sabotageCooldownMs")]
public int? SabotageCooldownMs { get; set; }
[JsonProperty("commsBlackoutDurationMs")]
public int? CommsBlackoutDurationMs { get; set; }
[JsonProperty("criticalMeltdownDeadlineMs")]
public int? CriticalMeltdownDeadlineMs { get; set; }
[JsonProperty("repairStationHoldMs")]
public int? RepairStationHoldMs { get; set; }
}
// Map data classes for rendering - compact format from server

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 14463228dfea2264ebfc36c3a7dc4b99

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 80ef0979df5d1fe489225f3e5edadc5c

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 3a4035bdb812fee4f96cb1aa1b24c999
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 131d9de257c8edc49991d792c6e702f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 16c3692935d75294f9404be0a4ba0039
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 1b8722ddfeb323a4da4a18797ed7df32
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 83edd2ecead106542bc862143208dd4c
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 5145a323a08373d4a9074774f7f3c501
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 3f5e4c6e6f8367342893fd7030d1b4cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8b02f5e5a2bd2df479219d58104b58e4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 9f4fa73205ab4db41871cc3e9260180f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5d7070679a02c0f478502a9eac088352
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: d08b4a9b983113c4a9c56b2738a85291
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,53 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: border anim
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings: []
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 1
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: f5b8b3d1765137a40a4094e14ea0b1c8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,72 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-7814012930283619509
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: New Animation
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: f5b8b3d1765137a40a4094e14ea0b1c8, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: border
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 2563971018880681404}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1107 &2563971018880681404
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: -7814012930283619509}
m_Position: {x: 270, y: 0, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: -7814012930283619509}

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: cecdd3ffd08949d49bfa9bad93dddd3b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 79c3437643e68be4e88c3bf039f0680d
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: d962c88742b40ec4594c568cba2848e4
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: d68a7660c51d4454f915a1c427cb01ce
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 805221047ed3e7c48a13ff21d97f6c66
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: b3f2382597d46c640ab466c1609bd193
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: c1f13902211756a4d9b7246f52ac5005
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 997acda7ef7df0e4eaeb2a8dff863abf
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 8292414d4bad4364f874555af2f7e712
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 3b63bdfb82042f94887a00a48094f69a
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: ae21ad83b0f7d5941822a82c37238864
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 7a6ffeb1058a6f8409a669fbd1d5c463
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 68ab2eea03d99d544b9c5c607019b2c0
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: c7a206d138cef964aa45af4cfa97fa9a
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: a04092104e630434a84804e17040195a
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: 0
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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 982be63b1292049488295e60ce74abe2
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: 0
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:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8fa0d9c695119af49bd1693054cf3174
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 70729d202603eef42955f52bd64f7c69
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: bbd26b895bc2b894b8989c08d9fd9197
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,92 +0,0 @@
%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: AreaMat
m_Shader: {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlphaTex:
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}
m_Ints: []
m_Floats:
- PixelSnap: 0
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _EnableExternalAlpha: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0.0813297, g: 1, b: 0, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _Flip: {r: 1, g: 1, b: 1, a: 1}
- _RendererColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 5a46533bdf4003449bc9146ccef44e27
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,754 +1,91 @@
using UnityEngine;
using GeoSus.Client;
using Subsystems;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine.SceneManagement;
/*
GameManager - hlavní tøida pro správu hry
GameManager_Network - subsystém pro správu komunikace se serverem
GameManager_Game - subsystém pro správu logiky hry (sabotáže, tasky, atd.)
GameManager_Map - subsystém pro správu mapy a prostøedí
GameManager_Input - subsystém pro správu vstupu od hráèe
GameManager_UI - subsystém pro správu uživatelského rozhraní
GamaManager_Stats - subsystém pro správu statistik pro server
*/
public class GameManager : MonoBehaviour
{
// Singleton
public static GameManager Instance { get; private set; }
[Header("Subsystems")]
public GameManager_Network networkSubsystem;
public GameManager_UI uiSubsystem;
public GameManager_Map mapSubsystem;
public GameManager_Input inputSubsystem;
public GameManager_Tasks taskSubsystem;
protected GameManager_Network networkSubsystem;
protected GameManager_UI uiSubsystem;
public GameClient gameClient;
protected GameClient gameClient;
[Header("Player Info")]
public string displayName;
[Header("Scene Management")]
[SerializeField] public string firstMenuScene = "main menu asi idk lol";
[Header("UI Elements (Client.unity)")]
// Canvas names in Client.unity — found at runtime in OnSceneLoaded
private const string CanvasNameJoinCreate = "LobbySelector";
private const string CanvasNameInLobby = "InLobby";
private const string CanvasNameLoading = "LoadingScreen";
private const string CanvasNameGame = "InGame";
[Header("Map")]
// MapCenterPoint and Player are in Client.unity — wired at runtime in OnSceneLoaded.
// buildingSettings/pathwaySettings/areaSettings must be assigned in SampleScene Inspector.
public BuildingSettings buildingSettings;
public PathwaySettings pathwaySettings;
public AreaSettings areaSettings;
[Header("Lobby Settings")]
public double pendingRadius = 500;
public int pendingImpostorCount = 1;
public int pendingTaskCount = 5;
/// <summary>
/// P13b/c: full settings overrides accumulated by HostLobbyUI before the
/// host taps "Create". Null = host didn't change anything beyond the three
/// flat fields above; server falls through to its current defaults for
/// every field. Each field is independently nullable so the host can
/// opt into changing only what they care about.
/// </summary>
public GameSettingsOverrides pendingSettings;
[Header("Task Minigames (round-robin)")]
// Names MUST match the scene file names in Assets/Scenes (case-sensitive)
// and each one MUST be enabled in EditorBuildSettings, or LoadSceneAsync
// will silently fail and the task button will appear dead.
[SerializeField] public string[] minigameScenes = {
"MiniGame-Kabely V10",
"MiniGame-insertkeys",
"MiniGame-FlappyBird",
"MiniGame-ThrowInHole",
"MiniGame-Satelit"
};
[Header("Debug")]
public bool testMode = false;
/// <summary>
/// When true, draw a small GPS status banner across the top of every
/// screen. Useful for diagnosing why CreateLobby is blocked or why a
/// joiner's position isn't updating - failures otherwise only show up
/// in logcat which most users can't reach. Toggle off for release.
/// </summary>
public bool showGPSDebugOverlay = true;
/// <summary>
/// Number of in-process test client bots to spawn alongside the host
/// when testMode is on. Each gets its own GameClient + Network and
/// joins the host's lobby automatically. Bots are switchable via
/// number keys 1..N (host = 0). Default 3 keeps memory reasonable;
/// bump for stress-testing voting / sabotage flows.
/// </summary>
public int testClientCount = 3;
/// <summary>
/// Per-bot network + display-name + sim-position state. The active slot
/// (host = 0, bots = 1..N) gets WASD on the next tick.
/// </summary>
private class TestBot
{
public GameClient Client;
public GameManager_Network Network;
public string DisplayName;
public GeoSus.Client.Position SimPosition;
public bool Joined;
public float LastSendTime;
}
private System.Collections.Generic.List<TestBot> _testBots = new System.Collections.Generic.List<TestBot>();
/// <summary>Slot 0 = host (real player), 1..N = test bot index.</summary>
private int _activeClientSlot = 0;
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// Keep the screen on while the player is in the app. A geographic
// social-deduction game asks the user to walk around for 5-15 minutes
// staring at the map; default Android sleep timeout (15-60s) blacks
// the screen out mid-round, drops GPS updates, and requires the
// player to re-unlock the phone. Two layers of belt-and-suspenders:
// (1) Unity's Screen.sleepTimeout, which works on most devices and
// is one line, but is overridden by some MIUI/EMUI ROMs.
// (2) Android FLAG_KEEP_SCREEN_ON on the activity window, harder for
// OEM ROMs to override and the standard pattern for navigation/maps
// apps. Wrapped in #if UNITY_ANDROID so editor/iOS skip it.
Screen.sleepTimeout = SleepTimeout.NeverSleep;
AcquireAndroidWakelock();
}
/// <summary>
/// Set FLAG_KEEP_SCREEN_ON on the Unity activity's window. This is the
/// standard navigation/maps-app pattern and survives ROM-level overrides
/// of Unity's Screen.sleepTimeout. No-op on non-Android platforms.
/// </summary>
private static void AcquireAndroidWakelock()
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
{
// addFlags must run on the UI thread. Capture activity into a
// local for the closure - AndroidJavaObject can be reused.
var act = activity;
act.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
try
{
using (var window = act.Call<AndroidJavaObject>("getWindow"))
{
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
const int FLAG_KEEP_SCREEN_ON = 0x00000080;
window.Call("addFlags", FLAG_KEEP_SCREEN_ON);
}
}
catch (System.Exception ex)
{
Debug.LogWarning("[Wakelock] addFlags failed: " + ex.Message);
}
}));
}
}
catch (System.Exception ex)
{
Debug.LogWarning("[Wakelock] Android JNI bridge failed: " + ex.Message);
}
#endif
}
[Header("UI Elements")]
public Canvas JoinCreateLobby;
public Canvas InLobby;
void Start()
{
// The prefab default in SampleScene.unity is "Hrac" (Czech for
// "player"). Treat it as equivalent to "no name set" so users who
// never customize their name don't all show up identically. This
// override only fires at startup; users who explicitly type "Hrac"
// into the nickname field will still send "Hrac" via the live
// DisplayName payload field.
if (string.IsNullOrEmpty(displayName) || displayName == "Hrac")
displayName = PlayerPrefs.GetString("PlayerName", GenerateUsername());
gameClient = new GameClient(GenerateUUID(), displayName);
networkSubsystem = new GameManager_Network(gameClient, this);
mapSubsystem = new GameManager_Map(gameClient, null, buildingSettings, pathwaySettings, areaSettings);
uiSubsystem = new GameManager_UI(gameClient);
inputSubsystem = new GameManager_Input(gameClient, null, testMode);
taskSubsystem = new GameManager_Tasks(gameClient, minigameScenes, this);
if (testMode)
DontDestroyOnLoad(this);
if (displayName == null || displayName == "")
{
int n = Mathf.Max(0, testClientCount);
for (int i = 0; i < n; i++)
{
var bot = new TestBot
{
DisplayName = "TestBot" + (i + 1),
};
bot.Client = new GameClient(GenerateUUID(), bot.DisplayName);
bot.Network = new GameManager_Network(bot.Client, null);
bot.Network.OpenConnection();
_testBots.Add(bot);
displayName = "Player_" + Random.Range(1000, 9999).ToString();
}
gameClient = new GameClient(GenerateUUID(), /*displayName*/ GenerateUsername());
uiSubsystem = new GameManager_UI(gameClient, JoinCreateLobby, InLobby);
networkSubsystem = new GameManager_Network(gameClient);
networkSubsystem.OpenConection();
}
networkSubsystem.OpenConnection();
// Start GPS immediately at app launch. Acquiring a fix on a cold
// device can take 5-30 seconds; if we wait until CreateLobby is
// pressed, the lobby will be seeded with bad coords. Starting here
// means the user's normal navigation through the menus gives the
// GPS subsystem time to settle.
inputSubsystem?.EnsureGPSStarted();
// Load main menu after GameManager is ready
if (!string.IsNullOrEmpty(firstMenuScene))
SceneManager.LoadScene(firstMenuScene, LoadSceneMode.Single);
}
/// <summary>
/// Draws a GPS status banner across the top of every screen. We use OnGUI
/// rather than a uGUI Canvas element because OnGUI works without any
/// scene wiring - we want this visible from the very first frame, on
/// every screen, even if the lobby canvas hasn't been bound yet. This is
/// a debug overlay; toggle showGPSDebugOverlay off for release builds.
/// </summary>
private void OnGUI()
{
if (!showGPSDebugOverlay) return;
if (inputSubsystem == null) return;
var diag = inputSubsystem.GpsDiagnostic;
var label = "GPS: " + diag;
// Scale font size to screen so it's legible on phones (HDPI) and
// editor (lower DPI) alike. Phones tend to have ~400dpi; the
// editor game view runs at ~100dpi.
int fontSize = Mathf.Max(14, Screen.width / 50);
var style = new GUIStyle(GUI.skin.label)
{
fontSize = fontSize,
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleLeft,
wordWrap = false,
normal = { textColor = Color.white }
};
// Width covers most of the screen so longer error strings don't get
// clipped. Height auto-fits the chosen font size.
float pad = fontSize * 0.5f;
float bannerH = fontSize * 2f;
var rect = new Rect(pad, pad, Screen.width - pad * 2, bannerH);
// Translucent black background for legibility against the map.
var prevColor = GUI.color;
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(rect, GUIContent.none);
GUI.color = prevColor;
// Indent the label inside the box.
var textRect = new Rect(rect.x + pad, rect.y, rect.width - pad * 2, rect.height);
GUI.Label(textRect, label, style);
// Second row: position-source picker (tap to cycle) + active client
// indicator (testMode only). Both are diagnostic; the source picker
// is the recovery path when one backend silently fails on a phone.
float row2Y = rect.y + bannerH + pad * 0.5f;
var btnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = Mathf.Max(12, fontSize - 2),
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleCenter,
};
// Source button: shows current source name + invites tap.
string sourceLabel = "Source: " + inputSubsystem.CurrentSourceName + " [tap to cycle]";
// Width sized to the text so the touch area matches the label.
Vector2 sourceSize = btnStyle.CalcSize(new GUIContent(sourceLabel));
float sourceW = Mathf.Min(Screen.width - pad * 2, sourceSize.x + pad * 2);
var sourceRect = new Rect(pad, row2Y, sourceW, bannerH);
if (GUI.Button(sourceRect, sourceLabel, btnStyle))
{
inputSubsystem.CycleNextPositionSource();
}
// Active-client indicator (only when we have test bots).
if (testMode && _testBots.Count > 0)
{
string slot = _activeClientSlot == 0 ? "Host" : ("Bot " + _activeClientSlot);
string indicator = $"WASD: {slot} (0..{_testBots.Count} to switch)";
var indStyle = new GUIStyle(GUI.skin.label)
{
fontSize = Mathf.Max(12, fontSize - 2),
fontStyle = FontStyle.Bold,
alignment = TextAnchor.MiddleLeft,
normal = { textColor = new Color(0.9f, 1f, 0.4f) },
};
Vector2 indSize = indStyle.CalcSize(new GUIContent(indicator));
var indRect = new Rect(sourceRect.xMax + pad, row2Y, indSize.x + pad * 2, bannerH);
GUI.color = new Color(0f, 0f, 0f, 0.65f);
GUI.Box(indRect, GUIContent.none);
GUI.color = prevColor;
GUI.Label(new Rect(indRect.x + pad, indRect.y, indRect.width, indRect.height), indicator, indStyle);
}
}
private void Update()
{
// Tick the SDK dispatcher so callbacks fire on main thread
gameClient?.Update();
if (testMode)
if (gameClient.CurrentLobbyState != null)
{
for (int i = 0; i < _testBots.Count; i++)
_testBots[i].Client?.Update();
HandleTestBotInput();
}
uiSubsystem.UpdateLobbyUI();
if (gameClient?.CurrentLobbyState != null)
{
uiSubsystem?.UpdateLobbyUI();
taskSubsystem?.UpdateProximity();
}
if (gameClient?.MyRole == PlayerRole.Impostor)
UpdateKillCooldown();
inputSubsystem?.positionCheck();
if (testMode) StepActiveTestBot();
}
/// <summary>
/// Number-key handling for slot switching. 0 = host, 1..N = test bot N.
/// Suppress host WASD when a non-host bot is active so the host capsule
/// doesn't drift while the user is moving a bot. Only fires when
/// testMode is on; release builds never see this path.
/// </summary>
private void HandleTestBotInput()
{
// 0 = host. 1..9 = bots (capped by Unity KeyCode.Alpha9).
if (Input.GetKeyDown(KeyCode.Alpha0)) _activeClientSlot = 0;
for (int i = 1; i <= 9 && i <= _testBots.Count; i++)
{
if (Input.GetKeyDown(KeyCode.Alpha0 + i)) _activeClientSlot = i;
}
// Tell the host's input subsystem to ignore WASD when a bot is active.
if (inputSubsystem != null)
inputSubsystem.SuppressWasd = (_activeClientSlot != 0);
}
/// <summary>
/// If the active slot is a bot, step its sim position from WASD axes
/// and send to the server. Idle bots get a periodic keep-alive so their
/// avatars don't time out.
/// </summary>
private void StepActiveTestBot()
{
if (_testBots.Count == 0) return;
var state = gameClient?.CurrentLobbyState;
if (state == null || state.MapData == null) return;
// Lazy-init each bot's sim position to the lobby's map center on
// first lobby state. Until the bot has joined a lobby it can't
// send position updates.
for (int i = 0; i < _testBots.Count; i++)
{
var bot = _testBots[i];
if (!bot.Joined) continue;
if (bot.SimPosition.Lat == 0 && bot.SimPosition.Lon == 0)
{
// Spawn each bot in a small ring around the map center so
// they don't all stack on top of each other on frame one.
double offsetLat = 0.00003 * Mathf.Cos(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
double offsetLon = 0.00003 * Mathf.Sin(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
bot.SimPosition = new GeoSus.Client.Position(
state.MapData.Center.Lat + offsetLat,
state.MapData.Center.Lon + offsetLon);
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
// WASD only drives the active bot.
if (_activeClientSlot >= 1 && _activeClientSlot <= _testBots.Count)
{
var bot = _testBots[_activeClientSlot - 1];
if (bot.Joined)
{
float dx = Input.GetAxis("Horizontal");
float dy = Input.GetAxis("Vertical");
const double speed = 0.00001;
bool moved = Mathf.Abs(dx) > 0.001f || Mathf.Abs(dy) > 0.001f;
if (moved)
{
bot.SimPosition = new GeoSus.Client.Position(
bot.SimPosition.Lat + dy * speed,
bot.SimPosition.Lon + dx * speed);
}
// Send on movement OR on keep-alive cadence so the server
// doesn't drop our presence.
bool dueKeepAlive = (Time.time - bot.LastSendTime) >= 1.0f;
if (moved || dueKeepAlive)
{
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
}
else
{
// No bot is active. All bots get keep-alive only.
for (int i = 0; i < _testBots.Count; i++)
{
var bot = _testBots[i];
if (!bot.Joined) continue;
if ((Time.time - bot.LastSendTime) >= 1.0f)
{
bot.Client.UpdatePosition(bot.SimPosition);
bot.LastSendTime = Time.time;
}
}
}
}
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
/// <summary>
/// After Client.unity loads, re-bind all canvas/HUD references because
/// those GameObjects don't exist in the Art menu scenes.
/// </summary>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.name == "Client")
{
var roots = scene.GetRootGameObjects();
// Find a root or deep GameObject by name in the loaded scene
GameObject FindGO(string n) {
foreach (var go in roots) {
if (go.name == n) return go;
var found = go.transform.Find(n);
if (found != null) return found.gameObject;
}
return null;
}
Canvas FindCanvas(string n) {
var go = FindGO(n);
return go != null ? go.GetComponent<Canvas>() : null;
}
// ── Build HUD BEFORE BindClientScene so FindTMP/Find can locate new elements ──
var inGameGO = FindGO("InGame");
if (inGameGO != null)
{
var builder = inGameGO.GetComponent<InGameHUDBuilder>()
?? inGameGO.AddComponent<InGameHUDBuilder>();
builder.BuildNow();
}
// ── Wire canvases (after HUD is built) ──
// Apply our standard CanvasScaler (1080x1920 reference, match=0.5)
// to every canvas in the scene before binding so layouts scale
// identically across phones and tablets without per-device tweaks.
var cJoin = FindCanvas(CanvasNameJoinCreate);
var cLobby = FindCanvas(CanvasNameInLobby);
var cLoad = FindCanvas(CanvasNameLoading);
var cGame = FindCanvas(CanvasNameGame);
InGameHUDBuilder.ConfigureCanvasScaler(cJoin);
InGameHUDBuilder.ConfigureCanvasScaler(cLobby);
InGameHUDBuilder.ConfigureCanvasScaler(cLoad);
InGameHUDBuilder.ConfigureCanvasScaler(cGame);
uiSubsystem?.BindClientScene(cJoin, cLobby, cLoad, cGame);
// ── Wire map center point and player capsule ──
var mapCenter = FindGO("MapCenterPoint");
var player = FindGO("Capsule");
mapSubsystem?.SetMapCenterPoint(mapCenter);
inputSubsystem?.SetPlayerObject(player);
// ── Attach camera controller to Main Camera ──
var mainCamGO = FindGO("Main Camera");
if (mainCamGO != null)
{
var camCtrl = mainCamGO.GetComponent<MapCameraController>()
?? mainCamGO.AddComponent<MapCameraController>();
camCtrl.SetTarget(player);
}
// If MapDataReady arrived before Client scene finished loading,
// this will build the map now that scene references are valid.
networkSubsystem?.OnClientSceneReady();
}
else if (scene.name == "create" || scene.name == "join loading")
{
// Lobby scene just loaded — ensure LobbyDisplayUI refreshes once
// its Start() has run and registered itself (happens before Update).
uiSubsystem?.NotifyLobbyChanged();
}
}
private float _killCooldownSeconds = 0f;
private const float KillCooldownDuration = 20f;
private void UpdateKillCooldown()
{
if (_killCooldownSeconds > 0)
{
_killCooldownSeconds -= Time.deltaTime;
// Mirror into GameState so UI reads from the single source of truth
if (networkSubsystem?.State != null)
networkSubsystem.State.KillCooldownRemaining = _killCooldownSeconds;
uiSubsystem?.SetKillCooldownText($"Kill: {Mathf.CeilToInt(_killCooldownSeconds)}s");
}
else
{
_killCooldownSeconds = 0f;
if (networkSubsystem?.State != null)
networkSubsystem.State.KillCooldownRemaining = 0;
uiSubsystem?.SetKillCooldownText("");
}
}
/// <summary>
/// Called by the ActionButton. Routes to kill / report / emergency / use-task
/// depending on current proximity state.
/// </summary>
public void PerformAction()
{
if (uiSubsystem == null || uiSubsystem.IsPlayerDead) return;
bool isImpostor = gameClient?.MyRole == PlayerRole.Impostor;
// P13b: pull per-lobby distances from the server-snapshotted settings
// instead of hardcoding 5m for every check. ?? fallback keeps the
// pre-P13b behavior on old server builds that don't ship settings.
var settings = networkSubsystem?.State?.Settings;
double reportDist = settings?.ReportDistanceM ?? 5.0;
double emergencyDist = settings?.EmergencyMeetingCallRadiusM ?? 5.0;
double killDist = settings?.KillDistanceM ?? 5.0;
// 1. Nearby task → USE
var nearbyTask = taskSubsystem?.NearbyTask;
if (nearbyTask != null && !isImpostor)
{
taskSubsystem.TriggerNearbyTask();
return;
}
// 2. Nearby body → REPORT
if (!uiSubsystem.IsCommsBlackout)
{
var nearbyBody = gameClient?.FindNearbyBody(reportDist);
if (nearbyBody != null)
{
gameClient.ReportBody(nearbyBody.BodyId);
return;
}
// 3. Near map centre → EMERGENCY
if (gameClient?.CurrentLobbyState?.MapData != null)
{
double distToCenter = gameClient.MyPosition.DistanceTo(gameClient.CurrentLobbyState.MapData.Center);
if (distToCenter <= emergencyDist)
{
gameClient.CallEmergencyMeeting();
return;
}
}
}
// 4. Impostor kill
if (isImpostor && _killCooldownSeconds <= 0)
{
var targetUuid = gameClient?.FindNearbyPlayer(killDist);
if (!string.IsNullOrEmpty(targetUuid))
{
gameClient.Kill(targetUuid);
_killCooldownSeconds = KillCooldownDuration;
}
}
}
/// <summary>Called by Impostor sabotage buttons.</summary>
public void StartSabotage(int typeIndex)
{
gameClient?.Send(new GeoSus.Client.StartSabotage { SabotageType = (SabotageType)typeIndex });
}
/// <summary>Called by the meeting vote buttons. Pass null to skip.</summary>
public void CastVote(string targetUuid)
{
gameClient?.Vote(targetUuid);
}
protected string GenerateUUID()
{
return System.Guid.NewGuid().ToString();
string UUID = System.Guid.NewGuid().ToString();
Debug.Log(UUID);
return UUID;
}
protected string GenerateUsername()
{
return "Player" + UnityEngine.Random.Range(1000, 9999).ToString();
string Username = Random.Range(0,10).ToString() + Random.Range(0, 10).ToString() + Random.Range(0, 10).ToString() + Random.Range(0, 10).ToString();
Debug.Log(Username);
return Username;
}
/// <summary>
/// Pull the nickname input field's current text into displayName +
/// gameClient.DisplayName + PlayerPrefs before sending a network
/// action. Defensive against any TMP_InputField / soft-keyboard race
/// where the user types and immediately taps a button: onValueChanged
/// normally fires before the click handler in the same frame, but
/// some Android keyboards batch text events oddly. Call this at the
/// top of any Create/Join/Rename flow. No-op if the input field
/// doesn't exist in the current scene.
/// </summary>
private void CommitNicknameFromInput()
{
var nameGO = GameObject.Find("name");
if (nameGO == null) return;
var field = nameGO.GetComponent<TMPro.TMP_InputField>();
if (field == null) return;
// Force the InputField to flush any pending soft-keyboard text.
// ForceLabelUpdate() is harmless if there's nothing pending.
field.ForceLabelUpdate();
string typed = (field.text ?? "").Trim();
if (string.IsNullOrEmpty(typed)) return;
if (typed == displayName) return; // already in sync, skip the writes
displayName = typed;
if (gameClient != null) gameClient.DisplayName = typed;
PlayerPrefs.SetString("PlayerName", typed);
PlayerPrefs.Save();
}
// Called by HostLobbyUI
public void CreateLobbyButton()
{
CommitNicknameFromInput();
// Refuse to create a lobby without a real GPS fix. The previous
// behavior of silently using a hardcoded Czechia fallback meant the
// game always started at the same place no matter where the host was,
// and the player capsule would spawn miles away in coordinate space
// because they're at their real GPS while the map was built around
// the fallback. Both bugs share this single gate.
if (inputSubsystem?.LastKnownPosition == null)
{
// testMode bypasses the GPS gate entirely so debug runs still work.
if (!testMode)
{
// Surface the actual GPS state in both logs and the toast
// instead of the generic "Waiting for GPS fix..." that hides
// permission/timeout/device-disabled distinctions.
string diag = inputSubsystem?.GpsDiagnostic ?? "no input subsystem";
Debug.LogWarning("[GameManager] CreateLobby blocked. " + diag);
uiSubsystem?.ShowToast("Cannot create lobby. " + diag);
inputSubsystem?.EnsureGPSStarted();
return;
networkSubsystem.CrateLobby(50.0755, 14.4378);
}
}
var pos = inputSubsystem?.LastKnownPosition;
double lat = pos?.Lat ?? 0;
double lon = pos?.Lon ?? 0;
networkSubsystem.CreateLobby(lat, lon, pendingRadius, pendingImpostorCount, pendingTaskCount, pendingSettings);
if (testMode) StartCoroutine(ConnectTestClients());
}
// Called by JoinLobbyUI with the code from the input field
public void JoinLobbyButton(string code)
public void JoinLobbyButton()
{
CommitNicknameFromInput();
if (!string.IsNullOrEmpty(code))
networkSubsystem.JoinLobby(code);
TMP_InputField joinCode = JoinCreateLobby.transform.Find("InputCode").GetComponent<TMP_InputField>();
if (joinCode.text != null && joinCode.text != "")
{
networkSubsystem.JoinLobby(joinCode.text);
}
else
Debug.LogWarning("Join code is empty!");
{
Debug.Log("Join code is empty!");
}
}
public void LeaveLobbyButton()
{
networkSubsystem.LeaveLobby();
}
public void StartGameButton()
{
networkSubsystem.StartGame();
}
void OnApplicationQuit()
{
gameClient?.Disconnect();
for (int i = 0; i < _testBots.Count; i++)
_testBots[i].Client?.Disconnect();
}
IEnumerator ConnectTestClients()
{
if (_testBots.Count == 0) yield break;
// Wait until host lobby code exists
float wait = 0f;
while ((gameClient?.CurrentLobbyState == null || string.IsNullOrEmpty(gameClient.CurrentLobbyState.JoinCode)) && wait < 20f)
{
wait += 0.25f;
yield return new WaitForSeconds(0.25f);
}
var joinCode = gameClient?.CurrentLobbyState?.JoinCode;
if (string.IsNullOrEmpty(joinCode))
{
Debug.LogWarning("[TestMode] Could not join test bots: join code not available.");
yield break;
}
// Wait until every bot's client has finished its TCP handshake.
// IsReady flips once ClientHello + ClientHelloAck round-trip.
wait = 0f;
bool allReady;
do
{
allReady = true;
for (int i = 0; i < _testBots.Count; i++)
{
if (_testBots[i].Client == null || !_testBots[i].Client.IsReady)
{
allReady = false;
break;
}
}
if (!allReady)
{
wait += 0.25f;
yield return new WaitForSeconds(0.25f);
}
} while (!allReady && wait < 20f);
if (!allReady)
{
Debug.LogWarning("[TestMode] Some test bots not ready, joining the ready ones only.");
}
for (int i = 0; i < _testBots.Count; i++)
{
var bot = _testBots[i];
if (bot.Client != null && bot.Client.IsReady)
{
bot.Network?.JoinLobby(joinCode);
bot.Joined = true;
}
}
Debug.Log($"[TestMode] {_testBots.Count} bot(s) joined lobby with code {joinCode}.");
gameClient.Disconnect();
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 22bf82e679cf6e1419440d236360ba3b

View File

@@ -1,926 +0,0 @@
using UnityEngine;
using GeoSus.Client;
using System;
using System.Collections;
namespace Subsystems
{
internal class CoroutineHost : MonoBehaviour
{
public CoroutineHost() { }
}
internal enum GPSState
{
Uninitialized,
Initializing,
Running,
Failed
}
/// <summary>
/// Position source backend. Selectable at runtime via the GPS overlay
/// "Source" button so the user can recover when one path misbehaves on
/// their phone:
/// Auto - JNI: subscribe to gps + network, pick most recent fix.
/// GpsOnly - JNI: subscribe to gps only (network's frequent indoor
/// fixes don't drown out the slower-but-precise gps fix).
/// NetworkOnly - JNI: subscribe to network only (cell tower / WiFi).
/// Useful indoors when no satellite lock is possible.
/// UnityInput - Unity's Input.location wrapper. Verified to hang on
/// Mi 9T / A20e (which is why JNI exists), but works on
/// newer Android where the JNI streaming-callbacks path
/// silently doesn't fire (MIUI/HyperOS battery saver,
/// approximate-vs-precise permission split, minDistance
/// gating on stationary phones).
/// EditorWasd - WASD-driven simulated position. Available regardless
/// of testMode flag so desktop builds and editor sessions
/// can navigate the map without real GPS.
/// </summary>
public enum PositionSource
{
Auto,
GpsOnly,
NetworkOnly,
UnityInput,
EditorWasd,
}
#if UNITY_ANDROID && !UNITY_EDITOR
/// <summary>
/// Bridges android.location.LocationListener to managed code. The method
/// names here must match Java's LocationListener interface exactly so
/// AndroidJavaProxy's reflection dispatcher can find them.
/// </summary>
internal class AndroidLocationProxy : AndroidJavaProxy
{
public AndroidLocationProvider Owner { get; set; }
public AndroidLocationProxy() : base("android.location.LocationListener") { }
// Called by Android each time a new fix arrives from the registered provider.
public void onLocationChanged(AndroidJavaObject location)
{
try
{
if (location == null) return;
double lat = location.Call<double>("getLatitude");
double lon = location.Call<double>("getLongitude");
long t = location.Call<long>("getTime");
string provider = "";
try { provider = location.Call<string>("getProvider"); } catch { }
// Streaming callbacks are LIVE (never cached). The cached path
// calls UpdateLocation directly with isCached=true.
Owner?.UpdateLocation(lat, lon, t, provider, isCached: false);
}
catch (Exception ex)
{
Debug.LogWarning("[GPS-JNI] onLocationChanged failed: " + ex.Message);
}
}
// Required by the LocationListener interface even if we don't use them.
// Missing methods cause java.lang.AbstractMethodError at runtime.
public void onStatusChanged(string provider, int status, AndroidJavaObject extras) { }
public void onProviderEnabled(string provider) { }
public void onProviderDisabled(string provider) { }
}
/// <summary>
/// Direct wrapper around android.location.LocationManager via JNI, used as
/// a replacement for Unity's Input.location on Android when the user picks
/// Auto/GpsOnly/NetworkOnly. Subscribed providers are configurable so the
/// position-source picker can rewire live without restart.
/// </summary>
internal class AndroidLocationProvider
{
private AndroidJavaObject _activity;
private AndroidJavaObject _locationManager;
private AndroidLocationProxy _gpsListener;
private AndroidLocationProxy _networkListener;
private double _lat, _lon;
private long _lastTimeMillis;
private long _lastLiveTimeMillis; // Time of most recent NON-cached fix.
private bool _hasFix;
private bool _hasLiveFix; // True once any streaming callback fired.
private string _activeProvider = "";
// Captured at Initialize() so the diagnostic can report
// "GPS provider DISABLED, only network enabled" etc.
private bool _gpsProviderEnabled;
private bool _networkProviderEnabled;
private bool _gpsLastKnownExists;
private bool _networkLastKnownExists;
private string _enabledProvidersList = "";
// Subscription scope - set in Initialize, used in Shutdown to know
// which listeners we registered.
private bool _subscribedGps;
private bool _subscribedNetwork;
public bool HasFix => _hasFix;
public bool HasLiveFix => _hasLiveFix;
public long LastLiveTimeMillis => _lastLiveTimeMillis;
public long LastTimeMillis => _lastTimeMillis;
public double Lat => _lat;
public double Lon => _lon;
public string ActiveProvider => _activeProvider;
public bool GpsProviderEnabled => _gpsProviderEnabled;
public bool NetworkProviderEnabled => _networkProviderEnabled;
public bool GpsLastKnownExists => _gpsLastKnownExists;
public bool NetworkLastKnownExists => _networkLastKnownExists;
public string EnabledProvidersList => _enabledProvidersList;
public bool SubscribedGps => _subscribedGps;
public bool SubscribedNetwork => _subscribedNetwork;
public bool Initialize(out string error, bool useGps, bool useNetwork)
{
error = "";
try
{
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
_activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
}
if (_activity == null) { error = "no current activity"; return false; }
_locationManager = _activity.Call<AndroidJavaObject>("getSystemService", "location");
if (_locationManager == null) { error = "getSystemService(\"location\") returned null"; return false; }
// Capture provider enable state up front so the diagnostic
// can distinguish "provider disabled at OS level" from
// "provider enabled but produced no fix yet".
_gpsProviderEnabled = SafeIsProviderEnabled("gps");
_networkProviderEnabled = SafeIsProviderEnabled("network");
_enabledProvidersList = SafeGetEnabledProviders();
Debug.Log($"[GPS-JNI] init useGps={useGps} useNetwork={useNetwork} gps enabled={_gpsProviderEnabled} network enabled={_networkProviderEnabled} all enabled=[{_enabledProvidersList}]");
// Try cached last-known fixes from the providers we're about
// to subscribe to. If the OS already knows where we are
// (e.g. from another app that recently used GPS), we get a
// fix at zero cost and zero wait time. Tagged isCached so
// the diagnostic can mark them and we know we still need
// to wait for a streaming callback.
if (useNetwork) TryLastKnown("network", out _networkLastKnownExists);
if (useGps) TryLastKnown("gps", out _gpsLastKnownExists);
_subscribedGps = useGps;
_subscribedNetwork = useNetwork;
if (useGps) _gpsListener = new AndroidLocationProxy { Owner = this };
if (useNetwork) _networkListener = new AndroidLocationProxy { Owner = this };
// requestLocationUpdates must be called on a thread with a
// Looper. Use the Activity's UI thread, which always has one.
// minTime=1000ms, minDistance=0f - we want updates on every
// fix the OS produces. Previously this was 1f which gated
// out updates from a stationary phone (MIUI/newer Android
// are stricter about this and that's the suspected cause of
// "via gps (cached)" sticking forever).
_activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
if (useGps)
{
try { _locationManager.Call("requestLocationUpdates", "gps", 1000L, 0f, _gpsListener); }
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] gps subscribe failed: " + ex.Message); }
}
if (useNetwork)
{
try { _locationManager.Call("requestLocationUpdates", "network", 1000L, 0f, _networkListener); }
catch (Exception ex) { Debug.LogWarning("[GPS-JNI] network subscribe failed: " + ex.Message); }
}
}));
return true;
}
catch (Exception ex)
{
error = "JNI init exception: " + ex.Message;
return false;
}
}
void TryLastKnown(string provider, out bool nonNullReturned)
{
nonNullReturned = false;
try
{
var loc = _locationManager.Call<AndroidJavaObject>("getLastKnownLocation", provider);
if (loc != null)
{
nonNullReturned = true;
double lat = loc.Call<double>("getLatitude");
double lon = loc.Call<double>("getLongitude");
long t = loc.Call<long>("getTime");
UpdateLocation(lat, lon, t, provider, isCached: true);
}
}
catch (Exception ex)
{
Debug.LogWarning($"[GPS-JNI] getLastKnownLocation({provider}) failed: " + ex.Message);
}
}
bool SafeIsProviderEnabled(string provider)
{
try
{
return _locationManager.Call<bool>("isProviderEnabled", provider);
}
catch (Exception ex)
{
Debug.LogWarning($"[GPS-JNI] isProviderEnabled({provider}) failed: " + ex.Message);
return false;
}
}
// Build a comma-separated list of currently-enabled providers via
// LocationManager.getProviders(true). We iterate the returned
// java.util.List by index because AndroidJavaObject does not
// implement IEnumerable.
string SafeGetEnabledProviders()
{
try
{
var list = _locationManager.Call<AndroidJavaObject>("getProviders", true);
if (list == null) return "";
int size = list.Call<int>("size");
var parts = new System.Text.StringBuilder();
for (int i = 0; i < size; i++)
{
var name = list.Call<string>("get", i);
if (i > 0) parts.Append(",");
parts.Append(name);
}
return parts.ToString();
}
catch (Exception ex)
{
Debug.LogWarning("[GPS-JNI] getProviders failed: " + ex.Message);
return "";
}
}
public void UpdateLocation(double lat, double lon, long timeMillis, string provider, bool isCached)
{
// Ignore older fixes if a newer one is already in hand. This lets
// both gps + network listeners feed us without ping-ponging
// between stale and fresh data.
if (timeMillis < _lastTimeMillis) return;
_lat = lat;
_lon = lon;
_lastTimeMillis = timeMillis;
// Active-provider name carries cached/live state in the diagnostic
// banner so the user can see at a glance whether streaming has
// kicked in or we're still on the initial cached snapshot.
_activeProvider = (provider ?? "") + (isCached ? " (cached)" : "");
_hasFix = true;
if (!isCached)
{
_hasLiveFix = true;
_lastLiveTimeMillis = timeMillis;
}
}
public void Shutdown()
{
try
{
if (_locationManager != null)
{
if (_gpsListener != null) _locationManager.Call("removeUpdates", _gpsListener);
if (_networkListener != null) _locationManager.Call("removeUpdates", _networkListener);
}
}
catch (Exception ex)
{
Debug.LogWarning("[GPS-JNI] Shutdown failed: " + ex.Message);
}
_gpsListener = null;
_networkListener = null;
_locationManager = null;
_activity = null;
}
}
#endif
public static class PositonExtensions
{
public static Position ToLocal(this Position position, Position center)
{
double latDiff = position.Lat - center.Lat;
double lonDiff = position.Lon - center.Lon;
double metersPerDegreeLat = 111320.0;
double metersPerDegreeLon = 111320.0 * Math.Cos(center.Lat * Math.PI / 180.0);
float x = (float)(lonDiff * metersPerDegreeLon);
float z = (float)(latDiff * metersPerDegreeLat);
return new Position(z, x);
}
public static Vector3 ToLocalVector3(this Position position, Position center)
{
return position.ToLocal(center).ToVector3(); //TODO: Implementace v subsystemech
}
public static Vector3 ToVector3(this Position position)
{
return new Vector3((float)position.Lon, 0, (float)position.Lat); //TODO: Implementace v subsystemech
}
public static double DistanceTo(this Vector3 pos, Vector3 other)
{
return Math.Sqrt((other.x - pos.x) * (other.x - pos.x) + (other.z - pos.z) * (other.z - pos.z));
}
}
public class GameManager_Input
{
private GameClient _gameClient;
private Position _currentPosition;
private Position _lastSentPosition;
private GameObject _player;
private bool _testMode;
// PlayerPrefs key for the user's chosen position source. Persists
// across app restarts so a user who flipped to UnityInput because
// their phone hated the JNI path doesn't have to flip again every
// launch.
private const string PrefsSourceKey = "PositionSource_v1";
private PositionSource _currentSource = PositionSource.Auto;
// When the multi-client editor test mode picks a non-host bot as
// active, we need the host's WASD path to NOT also move. Set true
// by GameManager when active slot != 0.
public bool SuppressWasd = false;
private GPSState _GPSState = GPSState.Uninitialized;
private float _speed = 0.00001f;
private Position _mapCenter;
private CoroutineHost _coroutineHost;
private int _gpsRetryCount = 0;
private const int _maxGpsRetries = 5;
private float _lastPositionSendTime;
private const float _positionKeepAliveSeconds = 1.0f;
// Diagnostic state. We capture *why* GPS init failed so the UI can
// surface it to the user without requiring logcat. Older Android
// phones (Mi 9T, A20e) hit silent failure modes that are impossible
// to distinguish from "still warming up" without this.
private string _lastGpsError = "";
private float _gpsInitStartTime = -1f;
// Bump from the original 20s. Cold-start GPS on older Android can
// easily exceed 20s indoors or under cloud cover - by the time the
// user notices nothing is happening, we've already given up.
private const int _gpsInitTimeoutSeconds = 60;
#if UNITY_ANDROID && !UNITY_EDITOR
// JNI-backed location provider, used for Auto/GpsOnly/NetworkOnly.
// UnityInput uses Input.location instead and leaves this null.
private AndroidLocationProvider _androidProvider;
#endif
/// <summary>Last known GPS position (for CreateLobby centre point)</summary>
public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null;
/// <summary>Current GPS state machine value (debug/diagnostic).</summary>
public string GpsStateName => _GPSState.ToString();
/// <summary>Last GPS error reason captured during init (empty if none).</summary>
public string LastGpsError => _lastGpsError ?? "";
/// <summary>Retry count out of max (debug/diagnostic).</summary>
public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}";
/// <summary>Currently selected position source (for UI cycle button).</summary>
public PositionSource CurrentSource => _currentSource;
/// <summary>Display name for the current source (for UI label).</summary>
public string CurrentSourceName
{
get
{
switch (_currentSource)
{
case PositionSource.Auto: return "Auto (GPS+Net)";
case PositionSource.GpsOnly: return "GPS only";
case PositionSource.NetworkOnly: return "Network only";
case PositionSource.UnityInput: return "Unity Input";
case PositionSource.EditorWasd: return "WASD";
default: return _currentSource.ToString();
}
}
}
/// <summary>
/// Human-readable one-line GPS status for on-screen overlay. Designed
/// to be visible without ADB so users can self-diagnose permission
/// vs. timeout vs. device-disabled vs. running-but-no-fix-yet.
/// </summary>
public string GpsDiagnostic
{
get
{
if (_currentSource == PositionSource.EditorWasd)
{
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
return "WASD: waiting for map center";
return $"WASD lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}";
}
switch (_GPSState)
{
case GPSState.Uninitialized:
return "Uninitialized (will start on first lobby action)";
case GPSState.Initializing:
{
float elapsed = _gpsInitStartTime >= 0 ? Time.time - _gpsInitStartTime : 0;
string providers = "";
#if UNITY_ANDROID && !UNITY_EDITOR
if (_androidProvider != null && !string.IsNullOrEmpty(_androidProvider.EnabledProvidersList))
providers = $" providers=[{_androidProvider.EnabledProvidersList}]";
#endif
return $"Initializing ({elapsed:F1}s / max {_gpsInitTimeoutSeconds}s){providers}";
}
case GPSState.Running:
{
string suffix = "";
#if UNITY_ANDROID && !UNITY_EDITOR
if (_androidProvider != null)
{
string p = _androidProvider.ActiveProvider;
if (!string.IsNullOrEmpty(p)) suffix = " via " + p;
// Show how stale the most recent fix is (ms-level
// resolution) so "stuck on cached" is obvious at
// a glance: "via gps (cached) [no live, 47s old]".
if (!_androidProvider.HasLiveFix)
{
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
long ageMs = now - _androidProvider.LastTimeMillis;
if (_androidProvider.LastTimeMillis > 0 && ageMs > 0)
suffix += $" [no live, {ageMs / 1000}s old]";
else
suffix += " [no live]";
}
else
{
long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
long ageMs = now - _androidProvider.LastLiveTimeMillis;
if (ageMs > 5000) suffix += $" [live {ageMs / 1000}s old]";
}
}
#endif
if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
return "Running but no fix yet (waiting for satellites)" + suffix;
return $"Running lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}" + suffix;
}
case GPSState.Failed:
return $"Failed: {(_lastGpsError ?? "unknown")} (retries {GpsRetryProgress})";
default:
return "?";
}
}
}
public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
{
_gameClient = gameClient;
_player = player;
_testMode = testMode;
// CoroutineHost needs a MonoBehaviour on a real GameObject
var hostGO = new UnityEngine.GameObject("_CoroutineHost");
UnityEngine.Object.DontDestroyOnLoad(hostGO);
_coroutineHost = hostGO.AddComponent<CoroutineHost>();
// Restore the user's last picked source. Default depends on
// platform: editor defaults to EditorWasd (no GPS hardware in
// editor anyway); device defaults to Auto.
string saved = PlayerPrefs.GetString(PrefsSourceKey, "");
if (!string.IsNullOrEmpty(saved) && Enum.TryParse(saved, out PositionSource parsed))
{
_currentSource = parsed;
}
else
{
#if UNITY_EDITOR
_currentSource = PositionSource.EditorWasd;
#else
_currentSource = PositionSource.Auto;
#endif
}
// Legacy testMode flag forces EditorWasd. New code paths should
// use SwitchPositionSource(EditorWasd) instead, but we keep the
// old behavior for backward compatibility with the inspector flag.
if (_testMode) _currentSource = PositionSource.EditorWasd;
}
/// <summary>Called from OnSceneLoaded when Client.unity loads so the
/// Player capsule (which lives in Client.unity) can be wired at runtime.</summary>
public void SetPlayerObject(GameObject player) { _player = player; }
/// <summary>
/// Switch the active position source backend live. Tears down the
/// current backend's listeners (JNI proxies, Input.location), resets
/// the state machine, and kicks off init for the new source. Persists
/// the choice to PlayerPrefs.
/// </summary>
public void SwitchPositionSource(PositionSource newSource)
{
if (_currentSource == newSource) return;
Debug.Log($"[GPS] SwitchPositionSource {_currentSource} -> {newSource}");
// Tear down whatever's running.
ShutdownCurrentBackend();
_currentSource = newSource;
PlayerPrefs.SetString(PrefsSourceKey, newSource.ToString());
PlayerPrefs.Save();
_GPSState = GPSState.Uninitialized;
_gpsRetryCount = 0;
_lastGpsError = "";
_gpsInitStartTime = -1f;
// Don't clear _currentPosition - the user has presumably been
// playing somewhere. Map markers/avatar position can stay until
// the next fix arrives from the new source.
EnsureGPSStarted();
}
/// <summary>Cycle through the available sources for tap-to-cycle UI.</summary>
public void CycleNextPositionSource()
{
var values = (PositionSource[])Enum.GetValues(typeof(PositionSource));
int idx = Array.IndexOf(values, _currentSource);
var next = values[(idx + 1) % values.Length];
SwitchPositionSource(next);
}
private void ShutdownCurrentBackend()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (_androidProvider != null)
{
_androidProvider.Shutdown();
_androidProvider = null;
}
#endif
// Stop Unity Input.location too, in case it was running.
try { Input.location.Stop(); } catch { }
}
/// <summary>
/// Kick off GPS initialization if it hasn't started yet. Safe to call
/// repeatedly. Hosts must call this from the lobby setup screen so
/// that by the time they click "Create Lobby" we have a real GPS
/// fix to use as the play-area center, instead of falling back to
/// the hardcoded coordinates.
/// </summary>
public void EnsureGPSStarted()
{
if (_currentSource == PositionSource.EditorWasd) return;
if (_coroutineHost == null) return;
// Allow tapping "Create Lobby" again (or any caller of this
// method) to retry from Failed up to _maxGpsRetries times.
if (_GPSState == GPSState.Uninitialized)
{
_coroutineHost.StartCoroutine(InitiallizeGPS());
}
else if (_GPSState == GPSState.Failed && _gpsRetryCount < _maxGpsRetries)
{
_gpsRetryCount++;
_coroutineHost.StartCoroutine(InitiallizeGPS());
}
}
public void positionCheck()
{
var state = _gameClient?.CurrentLobbyState;
if (state == null || state.Phase != GamePhase.Playing)
return;
try
{
if (_currentSource == PositionSource.EditorWasd)
{
if (_currentPosition == new Position(0, 0))
{
if (state.MapData == null)
return;
//Init blok
_currentPosition = state.MapData.Center;
_mapCenter = state.MapData.Center;
_lastSentPosition = _currentPosition;
}
if (!SuppressWasd)
TestPlayerPosition();
else
TrySendCurrentPosition(); // keep-alive only
}
else
{
if (_GPSState == GPSState.Uninitialized)
{
_coroutineHost.StartCoroutine(InitiallizeGPS());
return;
}
else if (_GPSState == GPSState.Initializing)
{
return;
}
else if (_GPSState == GPSState.Running)
{
EnsureMapCenter();
TrySendCurrentPosition();
}
else
{
Debug.Log("GPS failed, trying again...");
if (_gpsRetryCount < _maxGpsRetries)
{
_gpsRetryCount++;
_GPSState = GPSState.Uninitialized;
}
else
{
Debug.LogWarning("GPS unavailable after max retries. Using last known position.");
// Keep _GPSState = Failed so we stop retrying
}
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}");
}
}
private void EnsureMapCenter()
{
if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0)
return;
var md = _gameClient?.CurrentLobbyState?.MapData;
if (md != null)
_mapCenter = md.Center;
}
private void TrySendCurrentPosition()
{
bool moved = _currentPosition != _lastSentPosition;
bool keepAliveDue = (Time.time - _lastPositionSendTime) >= _positionKeepAliveSeconds;
if (!moved && !keepAliveDue)
return;
var previous = _lastSentPosition;
_gameClient.UpdatePosition(_currentPosition);
_lastSentPosition = _currentPosition;
_lastPositionSendTime = Time.time;
if (_player == null || (_mapCenter.Lat == 0 && _mapCenter.Lon == 0))
return;
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
_player.transform.position = localCurrent;
if (previous.Lat == 0 && previous.Lon == 0)
return;
var heading = CalculateHeading(previous.ToLocalVector3(_mapCenter), localCurrent);
if (heading.HasValue)
_player.transform.rotation = Quaternion.Euler(0, (float)heading.Value, 0);
}
private void TestPlayerPosition()
{
double x = Input.GetAxis("Horizontal");
double y = Input.GetAxis("Vertical");
_currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
if (heading != null)
{
if (_player != null)
_player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
}
if (_player != null)
_player.transform.position = localCurrent;
try
{
TrySendCurrentPosition();
}
catch
{
_gameClient.UpdatePosition(_currentPosition);
_lastSentPosition = _currentPosition;
}
}
private double? CalculateHeading(Vector3 first, Vector3 second)
{
if ((first - second).magnitude < 0.0001f) return null;
float dx = second.x - first.x;
float dz = second.z - first.z;
float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg;
if (heading < 0) heading += 360f;
return heading;
}
IEnumerator InitiallizeGPS()
{
_GPSState = GPSState.Initializing;
_gpsInitStartTime = Time.time;
_lastGpsError = "";
#if UNITY_ANDROID
// Request fine location permission if not already granted.
// On Android 12+ a "precise" toggle exists separately from coarse,
// but Unity's FineLocation request covers both for our purposes.
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
{
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.FineLocation);
// Wait up to 10 seconds for user to respond to the permission dialog
float waited = 0f;
while (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation) && waited < 10f)
{
yield return new WaitForSeconds(0.5f);
waited += 0.5f;
}
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
{
_lastGpsError = "Permission denied (fine location)";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
}
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
// Choose subscription scope based on selected source. UnityInput
// skips JNI entirely and falls through to the Input.location path
// below (the same path iOS / editor use).
if (_currentSource == PositionSource.Auto ||
_currentSource == PositionSource.GpsOnly ||
_currentSource == PositionSource.NetworkOnly)
{
bool useGps = (_currentSource != PositionSource.NetworkOnly);
bool useNetwork = (_currentSource != PositionSource.GpsOnly);
if (_androidProvider != null)
{
_androidProvider.Shutdown();
_androidProvider = null;
}
_androidProvider = new AndroidLocationProvider();
if (!_androidProvider.Initialize(out var initError, useGps, useNetwork))
{
_lastGpsError = "Native LocationManager failed: " + initError;
Debug.LogError("[GPS] " + _lastGpsError);
_androidProvider = null;
_GPSState = GPSState.Failed;
yield break;
}
// Fast-fail if neither subscribed provider is enabled at OS
// level. Waiting 60s for fixes from disabled providers is
// pointless - tell the user immediately what's wrong.
bool anyUsableEnabled =
(useGps && _androidProvider.GpsProviderEnabled) ||
(useNetwork && _androidProvider.NetworkProviderEnabled);
if (!anyUsableEnabled)
{
string which = useGps && useNetwork ? "gps + network"
: useGps ? "gps"
: "network";
_lastGpsError = $"{which} provider DISABLED at OS level. Open Settings > Location and switch it ON. Or tap [Source] to try a different backend.";
Debug.LogError("[GPS] " + _lastGpsError);
_androidProvider.Shutdown();
_androidProvider = null;
_GPSState = GPSState.Failed;
yield break;
}
// Wait for the first fix (cached or live).
int maxWaitJni = _gpsInitTimeoutSeconds;
while (!_androidProvider.HasFix && maxWaitJni > 0)
{
yield return new WaitForSeconds(1);
maxWaitJni--;
}
if (!_androidProvider.HasFix)
{
string enabled = _androidProvider.EnabledProvidersList ?? "";
string gpsState = _androidProvider.GpsProviderEnabled ? "ON" : "OFF";
string netState = _androidProvider.NetworkProviderEnabled ? "ON" : "OFF";
string lastKnown = $"lastKnown[gps={(_androidProvider.GpsLastKnownExists ? "yes" : "no")}, net={(_androidProvider.NetworkLastKnownExists ? "yes" : "no")}]";
_lastGpsError = $"Timeout {_gpsInitTimeoutSeconds}s on {_currentSource}. enabled=[{enabled}] gps={gpsState} net={netState} {lastKnown}. Try [Source] cycle to switch backends.";
Debug.LogError("[GPS] " + _lastGpsError);
_androidProvider.Shutdown();
_androidProvider = null;
_GPSState = GPSState.Failed;
yield break;
}
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
_GPSState = GPSState.Running;
_gpsRetryCount = 0;
_coroutineHost.StartCoroutine(AndroidGPSService());
yield break;
}
// _currentSource == UnityInput on Android: fall through to the
// Input.location path below. This is the recovery path for
// newer Android phones where JNI's streaming-callbacks don't
// fire (MIUI/HyperOS background restrictions, approximate-vs-
// precise permission, minDistance gating on stationary phones).
#endif
// iOS / editor / non-Android / Android-with-UnityInput-source:
// use Unity's Input.location.
if (!Input.location.isEnabledByUser)
{
_lastGpsError = "Location services not enabled by user";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
float desiredAccuracyInMeters = 5f;
float updateDistanceInMeters = 1f;
Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
int maxWait = _gpsInitTimeoutSeconds;
while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
{
yield return new WaitForSeconds(1);
maxWait--;
}
if (maxWait < 1)
{
_lastGpsError = $"Timed out after {_gpsInitTimeoutSeconds}s waiting for first fix (try moving outdoors, or tap [Source] to try a different backend)";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
if (Input.location.status == LocationServiceStatus.Failed)
{
_lastGpsError = "Unity Input.location reported Failed status";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
_GPSState = GPSState.Running;
_gpsRetryCount = 0;
_coroutineHost.StartCoroutine(GPSService());
}
#if UNITY_ANDROID && !UNITY_EDITOR
/// <summary>
/// Mirrors the JNI provider's most recent fix into _currentPosition
/// every 0.5s so the rest of the game (which polls _currentPosition
/// indirectly via LastKnownPosition / TrySendCurrentPosition) keeps
/// working unchanged. Replaces GPSService on Android.
/// </summary>
IEnumerator AndroidGPSService()
{
while (_GPSState == GPSState.Running && _androidProvider != null)
{
if (_androidProvider.HasFix)
{
_currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
}
yield return new WaitForSeconds(0.5f);
}
// Loop ended (state != Running or provider disposed). Clean up
// listeners so we don't leak across retries.
if (_androidProvider != null)
{
_androidProvider.Shutdown();
_androidProvider = null;
}
}
#endif
IEnumerator GPSService()
{
while (_GPSState == GPSState.Running)
{
if (Input.location.status == LocationServiceStatus.Failed)
{
_lastGpsError = "Location service died after init (provider stopped)";
Debug.LogError("[GPS] " + _lastGpsError);
_GPSState = GPSState.Failed;
yield break;
}
// Keep current GPS position fresh; sending is throttled in positionCheck().
var data = Input.location.lastData;
_currentPosition = new Position(data.latitude, data.longitude);
yield return new WaitForSeconds(0.5f);
}
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2ef1abfb1e85a7943925f9dc3cfea742

View File

@@ -1,808 +0,0 @@
using GeoSus.Client;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Subsystems{
[System.Serializable]
public class BuildingSettings
{
public Material ResidentialBuildingsMat;
public float ResidentialBuildingHeight;
public Material CommercialBuildingsMat;
public float CommercialBuildingHeight;
public Material IndustrialBuildingsMat;
public float IndustrialBuildingHeight;
public Material DefaultBuildingMat;
public float DefaultBuildingHeight;
}
[System.Serializable]
public class PathwaySettings
{
public Material FootwayMat;
public float FootwayWidth;
public Material PathMat;
public float PathWidth;
public Material StepsMat;
public float StepsWidth;
public Material CyclewayMat;
public float CyclewayWidth;
public Material PedestrianMat;
public float PedestrianWidth;
public Material RoadMat;
public float RoadWidth;
public Material ServiceMat;
public float ServiceWidth;
public Material ResidentialMat;
public float ResidentialWidth;
public Material TrackMat;
public float TrackWidth;
public Material DefaultMat;
public float DefaultWidth;
}
[System.Serializable]
public class AreaSettings
{
public Material ParkMat;
public Material GardenMat;
public Material PlaygroundMat;
public Material ForestMat;
public Material GrassMat;
public Material WaterMat;
public Material DefaultMat;
}
public class GameManager_Map
{
private GameClient _gameClient;
private GameObject _mapCenterPoint;
private Position _centerPosition;
private BuildingSettings _buildingSettings;
private PathwaySettings _pathwaySettings;
private AreaSettings _areaSettings;
private const float _metersPerUnit = 1f;
// ── Layer Y separation (single source of truth for vertical stacking) ───
// Areas at the bottom, paths above areas, buildings extruded upward from
// their own base, POIs floating well above everything else. Z-fighting
// happens when adjacent geometry shares a Y; these constants keep each
// logical layer at a distinct elevation.
private const float kAreaBaseY = 0.10f;
private const float kPathY = 0.30f;
private const float kBuildingBaseY = 0.50f;
private const float kPoiY = 2.00f;
// Render-queue forcing was tried in P3 to disambiguate same-Y geometry
// but turned out to be the cause of the "blank map in mobile game view,
// fine in scene view" regression: forcing transparent-class shaders
// (default queue 3000+) into the Geometry range (2000-2150) breaks
// their depth-write/blend assumptions on mobile shader paths. The
// editor's scene view masks it because it uses different render paths
// and post-process is off there. Queue forcing removed in P8;
// disambiguation is now via Y-layering + per-area Y-stagger alone,
// which the depth buffer resolves correctly even on weak mobile GPUs.
// ── Marker sizing (top-down camera, units = meters) ─────────────────
// The camera's orthographic size pushes "1 meter" to a small fraction
// of the screen. Markers need to be visibly larger than buildings'
// footprints for instant recognition.
private const float kMarkerHeight = 8f; // pillar height
private const float kMarkerRadius = 3f; // pillar radius (cylinder X/Z)
private const float kMarkerY = 4f; // base Y so pillar centers ~mid-height
private const float kLabelY = 9f; // text label sits above pillar top
private const float kLabelFontSize = 14f; // 3D text size in world units
// Runtime marker collections
private Dictionary<string, GameObject> _taskMarkers = new Dictionary<string, GameObject>();
private Dictionary<string, GameObject> _bodyMarkers = new Dictionary<string, GameObject>();
private Dictionary<string, GameObject> _playerAvatars = new Dictionary<string, GameObject>();
private List<GameObject> _sabotageMarkers = new List<GameObject>();
public GameManager_Map(GameClient gameClient, GameObject mapCenterPoint, BuildingSettings buildingSettings, PathwaySettings pathwaySettings, AreaSettings areaSettings)
{
_gameClient = gameClient;
_mapCenterPoint = mapCenterPoint;
_buildingSettings = buildingSettings;
_pathwaySettings = pathwaySettings;
_areaSettings = areaSettings;
}
public bool IsSceneReady => _mapCenterPoint != null;
/// <summary>Called from OnSceneLoaded when Client.unity is loaded so the
/// MapCenterPoint (which lives in Client.unity) can be wired at runtime.</summary>
public void SetMapCenterPoint(GameObject go) { _mapCenterPoint = go; }
public void BuildMap()
{
if (_mapCenterPoint == null)
{
Debug.LogWarning("[Map] BuildMap skipped: MapCenterPoint is not yet bound.");
return;
}
if (_gameClient?.CurrentLobbyState?.MapData == null)
{
Debug.LogWarning("[Map] BuildMap skipped: no MapData in CurrentLobbyState.");
return;
}
ClearChildren();
_centerPosition = _gameClient.CurrentLobbyState.MapData.Center;
GameObject buildingsRoot = new GameObject("Buildings");
buildingsRoot.transform.parent = _mapCenterPoint.transform;
GameObject pathRoot = new GameObject("Pathways");
pathRoot.transform.parent = _mapCenterPoint.transform;
GameObject areaRoot = new GameObject("Areas");
areaRoot.transform.parent = _mapCenterPoint.transform;
foreach (var building in _gameClient.CurrentLobbyState.MapData.GetBuildings())
{
string buildingType = "Unknown";
try
{
buildingType = _gameClient.CurrentLobbyState.MapData.BuildingTypes[_gameClient.CurrentLobbyState.MapData.GetBuildings().IndexOf(building)];
}
catch (Exception ex) { Debug.Log($"Error: {ex.Message}"); }
building.Name = buildingType;
GameObject b = BuildBuildingMesh(building);
b.transform.parent = buildingsRoot.transform;
}
foreach (var path in _gameClient.CurrentLobbyState.MapData.GetPathways())
{
GameObject p = BuildPathwayMesh(path);
p.transform.parent = pathRoot.transform;
}
foreach (var area in _gameClient.CurrentLobbyState.MapData.GetAreas())
{
GameObject a = BuildAreaMesh(area);
a.transform.parent = areaRoot.transform;
}
GameObject poiRoot = new GameObject("POIs");
poiRoot.transform.parent = _mapCenterPoint.transform;
int poiCount = 0;
foreach (var poi in _gameClient.CurrentLobbyState.MapData.GetPOIs())
{
GameObject p = BuildPOIMarker(poi);
if (p != null) { p.transform.parent = poiRoot.transform; poiCount++; }
}
// Diagnostic - if the user reports "map missing in game view" but
// the counts here are non-zero, the bug is camera/culling related,
// not a build issue.
int buildings = _gameClient.CurrentLobbyState.MapData.GetBuildings()?.Count ?? 0;
int paths = _gameClient.CurrentLobbyState.MapData.GetPathways()?.Count ?? 0;
int areas = _gameClient.CurrentLobbyState.MapData.GetAreas()?.Count ?? 0;
Debug.Log($"[Map] BuildMap done: {buildings} buildings, {paths} paths, " +
$"{areas} areas, {poiCount} POIs. MapCenterPoint={_mapCenterPoint.name} " +
$"layer={_mapCenterPoint.layer} pos={_mapCenterPoint.transform.position} " +
$"scale={_mapCenterPoint.transform.localScale}");
}
/// <summary>
/// Build a tall, brightly-colored pillar for a Point of Interest with
/// a 3D text label above it (e.g. "FOOD", "SHOP"). The label is laid
/// flat on the XZ plane facing UP so it reads correctly under the
/// orthogonal top-down camera.
/// </summary>
private GameObject BuildPOIMarker(MapPOI poi)
{
if (poi == null) return null;
var color = ColorForPOI(poi.POIType);
string label = LabelForPOI(poi.POIType);
var pos = poi.Location.ToLocalVector3(_centerPosition);
return CreateMarkerWithLabel($"POI_{poi.POIType}_{poi.Id}", pos, color, label);
}
/// <summary>
/// Shared marker builder: tall colored cylinder pillar + 3D text label
/// above it. Used by POIs, tasks, bodies, and sabotage stations so
/// they all share a visual language ("colored pillar with a name").
/// </summary>
private GameObject CreateMarkerWithLabel(string name, Vector3 worldPos, Color color, string label)
{
var go = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
go.name = name;
// Strip the auto-added collider - markers are visual only.
var col = go.GetComponent<Collider>();
if (col != null) UnityEngine.Object.Destroy(col);
go.transform.position = worldPos + Vector3.up * kMarkerY;
// Cylinder's default unit is 2 tall, 1 wide. Scale Y by half of
// kMarkerHeight (built-in is 2 units), X/Z by kMarkerRadius.
go.transform.localScale = new Vector3(kMarkerRadius, kMarkerHeight * 0.5f, kMarkerRadius);
var mr = go.GetComponent<MeshRenderer>();
if (mr != null)
{
// One .material access -> single clone of the primitive's
// default mat. Don't touch renderQueue (P3 regression cause).
var inst = mr.material;
if (inst != null) inst.color = color;
}
// 3D text label - lays flat on top of the pillar facing up.
// Parented to the marker so it follows position changes.
var labelGO = new GameObject("Label");
labelGO.transform.SetParent(go.transform, worldPositionStays: false);
// Local Y offset: pillar's local scale Y is kMarkerHeight/2, but
// the cylinder primitive is 2 units tall in local space, so its
// top is at local +1. Label sits a hair above that.
labelGO.transform.localPosition = new Vector3(0, 1.05f, 0);
// Rotate 90 around X so the text quad's normal points +Y (toward
// the top-down camera). The default TMP forward is +Z.
labelGO.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
// Compensate for the cylinder's non-uniform parent scale so the
// text size in world units matches kLabelFontSize regardless of
// how the pillar was scaled.
labelGO.transform.localScale = new Vector3(
1f / kMarkerRadius,
1f / (kMarkerHeight * 0.5f),
1f / kMarkerRadius);
var tmp = labelGO.AddComponent<TextMeshPro>();
tmp.text = label;
tmp.fontSize = kLabelFontSize;
tmp.color = Color.white;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center;
tmp.outlineColor = Color.black;
tmp.outlineWidth = 0.25f;
// Reasonable bounds so the text mesh isn't auto-clipped.
var rt = tmp.rectTransform;
rt.sizeDelta = new Vector2(20, 4);
return go;
}
private static Color ColorForPOI(MapPOIType type)
{
switch (type)
{
case MapPOIType.FoodDrink: return new Color(1.00f, 0.55f, 0.00f); // orange
case MapPOIType.Shop: return new Color(0.20f, 0.60f, 1.00f); // blue
case MapPOIType.Health: return new Color(0.96f, 0.27f, 0.27f); // red
case MapPOIType.Transport: return new Color(0.85f, 0.85f, 0.20f); // yellow
case MapPOIType.Culture: return new Color(0.65f, 0.30f, 0.95f); // purple
case MapPOIType.Landmark: return new Color(0.95f, 0.85f, 0.40f); // gold
case MapPOIType.Recreation: return new Color(0.30f, 0.85f, 0.30f); // green
default: return new Color(0.75f, 0.75f, 0.80f); // muted grey
}
}
private static string LabelForPOI(MapPOIType type)
{
switch (type)
{
case MapPOIType.FoodDrink: return "FOOD";
case MapPOIType.Shop: return "SHOP";
case MapPOIType.Health: return "HEALTH";
case MapPOIType.Transport: return "TRANSIT";
case MapPOIType.Culture: return "CULTURE";
case MapPOIType.Landmark: return "LANDMARK";
case MapPOIType.Recreation: return "PARK";
default: return "POI";
}
}
void ClearChildren()
{
List<GameObject> toDestroy = new List<GameObject>();
foreach (Transform t in _mapCenterPoint.transform)
toDestroy.Add(t.gameObject);
foreach (var g in toDestroy)
{
UnityEngine.Object.DestroyImmediate(g);
}
}
#region Mesh Building
GameObject BuildBuildingMesh(MapBuilding b)
{
var building = new GameObject($"Building_{b.Name ?? "Unknown"}");
// Výpočet středu budovy. Lift the base above kPathY so building
// walls visibly extrude *upward* from above the road/area layer
// instead of starting at ground (which made them clip into paved
// areas that share their footprint).
Vector3 center = CalculatePolygonCenter(b.Outline);
building.transform.position = center + Vector3.up * kBuildingBaseY;
// Vytvoření mesh pro budovu
MeshFilter meshFilter = building.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = building.AddComponent<MeshRenderer>();
float height;
Material mat;
switch (b.BuildingType.ToLower())
{
case "residential":
mat = _buildingSettings.ResidentialBuildingsMat;
height = _buildingSettings.ResidentialBuildingHeight;
break;
case "commercial":
mat = _buildingSettings.CommercialBuildingsMat;
height = _buildingSettings.CommercialBuildingHeight;
break;
case "industrial":
mat = _buildingSettings.IndustrialBuildingsMat;
height = _buildingSettings.IndustrialBuildingHeight;
break;
default:
mat = _buildingSettings.DefaultBuildingMat;
height = _buildingSettings.DefaultBuildingHeight;
break;
}
Mesh mesh = CreateExtrudedPolygonMesh(b.Outline, height);
meshFilter.mesh = mesh;
//TODO: material by type
// Použijeme barvu podle typu budovy. Use sharedMaterial to keep
// the project's Material asset reference - no clone, no leak.
// Y-position alone disambiguates building geometry from area/path
// layers; we don't need renderQueue overrides (which broke mobile
// rendering for transparent-class shaders in P3).
meshRenderer.sharedMaterial = mat;
// Přidání collideru pro interakci
building.AddComponent<MeshCollider>();
return building;
}
GameObject BuildPathwayMesh(MapPathway w)
{
var path = new GameObject($"Path_{w.Name ?? "Unknown"}");
// Použijeme LineRenderer pro jednoduchost
LineRenderer line = path.AddComponent<LineRenderer>();
float width;
Material mat;
switch (w.PathType)
{
case PathType.Footway:
mat = _pathwaySettings.FootwayMat;
width = _pathwaySettings.FootwayWidth;
break;
case PathType.Path:
mat = _pathwaySettings.PathMat;
width = _pathwaySettings.PathWidth;
break;
case PathType.Steps:
mat = _pathwaySettings.StepsMat;
width = _pathwaySettings.PathWidth;
break;
case PathType.Cycleway:
mat = _pathwaySettings.CyclewayMat;
width = _pathwaySettings.CyclewayWidth;
break;
case PathType.Pedestrian:
mat = _pathwaySettings.PedestrianMat;
width = _pathwaySettings.PedestrianWidth;
break;
case PathType.Road:
mat = _pathwaySettings.RoadMat;
width = _pathwaySettings.RoadWidth;
break;
case PathType.Service:
mat = _pathwaySettings.ServiceMat;
width = _pathwaySettings.ServiceWidth;
break;
case PathType.Residential:
mat = _pathwaySettings.ResidentialMat;
width = _pathwaySettings.ResidentialWidth;
break;
case PathType.Track:
mat = _pathwaySettings.TrackMat;
width = _pathwaySettings.TrackWidth;
break;
default:
mat = _pathwaySettings.DefaultMat;
width = _pathwaySettings.DefaultWidth;
break;
}
// sharedMaterial avoids the LineRenderer cloning the project's
// shared path Material on every BuildMap call. Queue overrides
// dropped (P3 mobile-render regression cause).
line.sharedMaterial = mat;
line.widthMultiplier = width;
// Nastavení bodů cesty - kPathY sits above all area polygons but
// below building bases, so paths visibly run on top of areas.
line.positionCount = w.Points.Count;
for (int i = 0; i < w.Points.Count; i++)
{
Vector3 pos = w.Points[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center);
pos.y = kPathY;
line.SetPosition(i, pos);
}
return path;
}
GameObject BuildAreaMesh(MapArea a)
{
var area = new GameObject($"Area_{a.Name ?? "Unknown"}");
MeshFilter meshFilter = area.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = area.AddComponent<MeshRenderer>();
// Vytvoření plochého mesh
Mesh mesh = CreateFlatPolygonMesh(a.Outline);
meshFilter.mesh = mesh;
Material mat;
switch (a.AreaType)
{
case MapAreaType.Park:
mat = _areaSettings.ParkMat;
break;
case MapAreaType.Garden:
mat = _areaSettings.GardenMat;
break;
case MapAreaType.Playground:
mat = _areaSettings.PlaygroundMat;
break;
case MapAreaType.Forest:
mat = _areaSettings.ForestMat;
break;
case MapAreaType.Grass:
mat = _areaSettings.GrassMat;
break;
case MapAreaType.Water:
mat = _areaSettings.WaterMat;
break;
default:
mat = _areaSettings.DefaultMat;
break;
}
// sharedMaterial: no per-area material clone. Render-queue forcing
// dropped in P8 (caused mobile-render regression). The Y-stagger
// below alone now drives "smaller polygon on top of larger one"
// depth ordering - which is what the depth buffer was always
// designed to do, and works on mobile GPUs with weak precision
// because the stagger spread (0.04 units) is well above any
// reasonable depth-buffer epsilon.
meshRenderer.sharedMaterial = mat;
// Y stagger: smaller polygons sit a hair higher than larger ones,
// so depth-test draws them on top of bigger area polygons they sit
// inside (e.g. a playground inside a park). Total spread is 0.04
// units - visually invisible but plenty for the depth buffer.
float yStagger = ComputeAreaYStagger(a.Outline);
area.transform.position = new Vector3(0, kAreaBaseY + yStagger, 0);
return area;
}
/// <summary>
/// Returns a non-negative size proxy used to bucket areas by footprint.
/// Larger polygons return higher numbers; used inversely for queue/Y.
/// </summary>
private float AreaSizeBucket(List<Position> outline)
{
if (outline == null || outline.Count < 3) return 1f;
// Cheap bbox area in lat-lon space scaled by 1e6 - we only need a
// monotonic ordering, not a real geographic area.
double minLat = outline[0].Lat, maxLat = outline[0].Lat;
double minLon = outline[0].Lon, maxLon = outline[0].Lon;
for (int i = 1; i < outline.Count; i++)
{
if (outline[i].Lat < minLat) minLat = outline[i].Lat;
if (outline[i].Lat > maxLat) maxLat = outline[i].Lat;
if (outline[i].Lon < minLon) minLon = outline[i].Lon;
if (outline[i].Lon > maxLon) maxLon = outline[i].Lon;
}
double bbox = (maxLat - minLat) * (maxLon - minLon) * 1e6;
return (float)System.Math.Max(0.001, bbox);
}
/// <summary>
/// Smaller areas get a higher Y so they render on top of any larger
/// area they overlap. Returns a value in [0, 0.04] units.
/// </summary>
private float ComputeAreaYStagger(List<Position> outline)
{
float bucket = AreaSizeBucket(outline);
// Inverse mapping: huge area -> 0, tiny area -> 0.04.
float t = Mathf.Clamp01(1f - bucket / (bucket + 50f));
return t * 0.04f;
}
#endregion
#region Polygon Utils
private Vector3 CalculatePolygonCenter(List<Position> points)
{
Vector3 center = Vector3.zero;
foreach (var point in points)
{
center += point.ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center);
}
return center / points.Count;
}
/// <summary>
/// Signed XZ shoelace area for a polygon expressed in local Vector3.
/// Positive = CCW (Unity left-handed Y-up: upward-facing normal),
/// negative = CW (downward-facing normal -> top face invisible from
/// above unless we reverse the winding before triangulating).
/// </summary>
private static float PolygonSignedAreaXZ(List<Vector3> verts)
{
float area = 0f;
int n = verts.Count;
for (int i = 0; i < n; i++)
{
var a = verts[i];
var b = verts[(i + 1) % n];
area += (b.x - a.x) * (a.z + b.z);
}
return area * 0.5f;
}
private Mesh CreateExtrudedPolygonMesh(List<Position> outline, float height)
{
Mesh mesh = new Mesh();
// Reject degenerates - Recast/Overpass can hand back 1-2 vertex
// outlines on broken ways. Empty mesh -> renderer draws nothing,
// safer than a malformed triangle list.
if (outline == null || outline.Count < 3) return mesh;
// Convert to local space first so we can run a winding check, then
// reverse if needed. Without this, CW outlines from Overpass yield
// downward-facing top normals and the building roof is invisible
// from the top-down map camera.
int vertexCount = outline.Count;
var localVerts = new List<Vector3>(vertexCount);
Vector3 center = CalculatePolygonCenter(outline);
for (int i = 0; i < vertexCount; i++)
localVerts.Add(outline[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center) - center);
if (PolygonSignedAreaXZ(localVerts) < 0f)
localVerts.Reverse();
// Vertices - spodní a horní podstava
Vector3[] vertices = new Vector3[vertexCount * 2];
for (int i = 0; i < vertexCount; i++)
{
Vector3 pos = localVerts[i];
vertices[i] = pos; // Spodní
vertices[i + vertexCount] = pos + Vector3.up * height; // Horní
}
// Triangles - jen boční stěny pro jednoduchost
List<int> triangles = new List<int>();
for (int i = 0; i < vertexCount; i++)
{
int next = (i + 1) % vertexCount;
// Boční stěna - dva trojúhelníky
triangles.Add(i);
triangles.Add(i + vertexCount);
triangles.Add(next);
triangles.Add(next);
triangles.Add(i + vertexCount);
triangles.Add(next + vertexCount);
}
// Horní podstava - zjednodušená triangulace (fan)
if (vertexCount >= 3)
{
for (int i = 1; i < vertexCount - 1; i++)
{
triangles.Add(vertexCount); // Střed (první bod horní)
triangles.Add(vertexCount + i);
triangles.Add(vertexCount + i + 1);
}
}
mesh.vertices = vertices;
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
private Mesh CreateFlatPolygonMesh(List<Position> outline)
{
Mesh mesh = new Mesh();
// Reject degenerates (matches CreateExtrudedPolygonMesh).
if (outline == null || outline.Count < 3) return mesh;
int vertexCount = outline.Count;
var localVerts = new List<Vector3>(vertexCount);
Vector3 center = CalculatePolygonCenter(outline);
for (int i = 0; i < vertexCount; i++)
localVerts.Add(outline[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center) - center);
// Force CCW so RecalculateNormals produces an upward-facing normal.
// CW polygons from Overpass would otherwise render as black voids
// when the top-down camera looks at their back face.
if (PolygonSignedAreaXZ(localVerts) < 0f)
localVerts.Reverse();
Vector3[] vertices = localVerts.ToArray();
// Triangulace - fan pattern
List<int> triangles = new List<int>();
for (int i = 1; i < vertexCount - 1; i++)
{
triangles.Add(0);
triangles.Add(i);
triangles.Add(i + 1);
}
mesh.vertices = vertices;
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();
return mesh;
}
#endregion
#region Markers
public void CreateTaskMarkers(List<GeoSus.Client.GameTask> tasks)
{
if (_mapCenterPoint == null) return;
if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0)
{
var md = _gameClient?.CurrentLobbyState?.MapData;
if (md != null) _centerPosition = md.Center;
}
if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0) return;
var taskColor = new Color(0.20f, 0.95f, 0.55f); // bright green - "GO HERE"
foreach (var task in tasks)
{
if (_taskMarkers.ContainsKey(task.TaskId)) continue;
var pos = task.Location.ToLocalVector3(_centerPosition);
var go = CreateMarkerWithLabel($"Task_{task.TaskId}", pos, taskColor, "TASK");
go.transform.parent = _mapCenterPoint.transform;
// Pulsing point light so the task literally glows on the map.
var light = go.AddComponent<Light>();
light.color = taskColor;
light.intensity = 3f;
light.range = 25f;
_taskMarkers[task.TaskId] = go;
}
}
public void RemoveTaskMarker(string taskId)
{
if (_taskMarkers.TryGetValue(taskId, out var go))
{
UnityEngine.Object.Destroy(go);
_taskMarkers.Remove(taskId);
}
}
public void CreateBodyMarker(string bodyId, Position location)
{
if (_mapCenterPoint == null) return;
if (_bodyMarkers.ContainsKey(bodyId)) return;
var pos = location.ToLocalVector3(_centerPosition);
// Bright red pillar with "BODY" label - players need to see this
// from across the map to call it in.
var go = CreateMarkerWithLabel($"Body_{bodyId}", pos,
new Color(0.96f, 0.18f, 0.18f), "BODY");
go.transform.parent = _mapCenterPoint?.transform;
_bodyMarkers[bodyId] = go;
}
public void ClearBodyMarkers()
{
foreach (var go in _bodyMarkers.Values)
if (go) UnityEngine.Object.Destroy(go);
_bodyMarkers.Clear();
}
// ── Player avatar sizing ────────────────────────────────────────────
// The default Unity capsule primitive is 2m tall in local space. The
// map camera defaults to 150m orthographic-ish height (see
// MapCameraController), so anything smaller than ~3m world-size is a
// pixel on screen. Original code used scale=0.4 (~0.8m capsule) which
// was invisible. Markers (POIs/tasks/bodies) are 8m pillars; players
// need to be visibly distinct from those AND from each other. The
// local player gets a halo light + larger scale so the user can find
// themselves on the map at a glance.
private const float kLocalPlayerScale = 4f; // ~8m capsule (matches marker height)
private const float kRemotePlayerScale = 2f; // ~4m capsule (smaller than markers)
private const float kLocalPlayerHaloRange = 18f;
private const float kLocalPlayerHaloIntensity = 2.5f;
public void UpdatePlayerAvatars(Dictionary<string, PlayerPositionInfo> positions, string myUuid)
{
if (_mapCenterPoint == null) return;
if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0)
{
var md = _gameClient?.CurrentLobbyState?.MapData;
if (md != null) _centerPosition = md.Center;
}
if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0) return;
foreach (var kvp in positions)
{
string uuid = kvp.Key;
var info = kvp.Value;
bool isLocal = uuid == myUuid;
if (!_playerAvatars.TryGetValue(uuid, out var go) || go == null)
{
go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
go.name = $"Player_{uuid.Substring(0, Mathf.Min(8, uuid.Length))}";
go.transform.parent = _mapCenterPoint?.transform;
// Strip the auto-collider - avatars are visual only and the
// collider would interact with the map's MeshColliders.
var col = go.GetComponent<Collider>();
if (col != null) UnityEngine.Object.Destroy(col);
float scale = isLocal ? kLocalPlayerScale : kRemotePlayerScale;
go.transform.localScale = Vector3.one * scale;
if (isLocal)
{
// Halo light around the local player so the user can
// find themselves at a glance even at the widest zoom.
// Range/intensity tuned so it reads as "this is me"
// without bleeding far enough to drown POI markers.
var halo = go.AddComponent<Light>();
halo.color = new Color(0.30f, 1.00f, 0.55f); // matches green capsule color
halo.intensity = kLocalPlayerHaloIntensity;
halo.range = kLocalPlayerHaloRange;
}
_playerAvatars[uuid] = go;
}
// Lift the avatar so the bottom of the capsule sits roughly at
// ground level despite the larger scale. Capsule's local pivot
// is at center, height = 2 * localScale.y world units, so we
// raise by half the local height.
float halfHeight = (isLocal ? kLocalPlayerScale : kRemotePlayerScale);
go.transform.position = info.Position.ToLocalVector3(_centerPosition)
+ Vector3.up * halfHeight;
var mr = go.GetComponent<MeshRenderer>();
if (mr)
{
if (isLocal) mr.material.color = new Color(0.30f, 1.00f, 0.55f);
else if (info.State == GeoSus.Client.PlayerState.Dead) mr.material.color = Color.grey;
else mr.material.color = Color.white;
}
}
}
public void CreateSabotageMarkers(List<RepairStationInfo> stations)
{
var color = new Color(1.0f, 0.55f, 0.0f); // strong orange = repair urgency
foreach (var station in stations)
{
var pos = station.Location.ToLocalVector3(_centerPosition);
var go = CreateMarkerWithLabel($"Sabotage_{station.StationId}", pos,
color, "REPAIR");
go.transform.parent = _mapCenterPoint?.transform;
// Repair stations also pulse light so impostors and crew see
// the urgency from across the map.
var light = go.AddComponent<Light>();
light.color = color;
light.intensity = 4f;
light.range = 30f;
_sabotageMarkers.Add(go);
}
}
public void ClearSabotageMarkers()
{
foreach (var go in _sabotageMarkers)
if (go) UnityEngine.Object.Destroy(go);
_sabotageMarkers.Clear();
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 71870ee18b89dd7438e5362ff9e02a3b

View File

@@ -2,10 +2,6 @@ using GeoSus.Client;
using System.Collections;
using System.Threading.Tasks;
using UnityEngine;
using System.Collections.Generic;
using Subsystems;
using System.Linq;
using UnityEngine.SceneManagement;
namespace Subsystems
{
@@ -14,34 +10,8 @@ namespace Subsystems
private const string _serverAddress = "geosus.honzuvkod.dev";
private const int _serverPort = 7777;
private GameClient _gameClient;
private GameManager _manager;
private bool _pendingMapBuild;
/// <summary>
/// Authoritative game state; written here, read by GameManager_UI.
/// </summary>
public GameState State { get; } = new GameState();
public GameManager_Network(GameClient gameClient, GameManager manager)
public async void OpenConection()
{
_gameClient = gameClient;
_manager = manager;
RegisterEventHandlers();
}
public async void OpenConnection()
{
// Snapshot the lobby we believed we were in BEFORE the new connect
// attempt. If the client SDK preserved it across a transient drop
// (P9 fix), this is non-null and we'll send a Reconnect message
// post-handshake to re-associate with the lobby on the server side.
// Without it, the next CastVote / TaskComplete / etc. would arrive
// on a fresh connection the server doesn't recognize and bounce
// with NOT_IN_LOBBY.
var rejoinLobbyId = _gameClient.LobbyId;
int retries = 0;
int delayMs = 5000;
while (true)
{
Task<bool> state = _gameClient.ConnectAsync(_serverAddress, _serverPort);
@@ -49,29 +19,20 @@ namespace Subsystems
if (state.Result)
{
Debug.Log("Connected to server.");
// Re-attach to the prior lobby if we had one. Server-side
// HandleReconnectAsync will replay missed events and ack
// with a ReconnectResponse carrying the snapshot.
if (!string.IsNullOrEmpty(rejoinLobbyId))
{
Debug.Log($"Re-associating with lobby {rejoinLobbyId} after reconnect.");
_gameClient.Reconnect(rejoinLobbyId);
}
break;
}
retries++;
if (retries >= 10)
else
{
Debug.LogError("Failed to connect after 10 attempts. Giving up.");
break;
Debug.Log("Failed to connect to server");
}
Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s...");
await Task.Delay(delayMs);
delayMs = Mathf.Min(delayMs * 2, 30000);
await Task.Delay(5000);
}
}
public GameManager_Network(GameClient gameClient)
{
_gameClient = gameClient;
RegisterEventHandlers();
}
public void RegisterEventHandlers()
{
_gameClient.OnConnected += OnConnected;
@@ -80,514 +41,112 @@ namespace Subsystems
_gameClient.OnMessage += OnMessage;
_gameClient.OnGameEvent += OnGameEvent;
}
private void OnConnected()
{
Debug.Log("Successfully connected to the server.");
// Tear the reconnect overlay down once the socket is healthy.
// No-op if it wasn't shown.
_manager?.uiSubsystem?.HideReconnecting();
}
private void OnError(string e) => Debug.LogError($"Network error: {e}");
private void OnDisconnected(string reason)
{
Debug.Log($"Disconnected: {reason}");
// Show the reconnect overlay only if the user is mid-game; we
// don't want it flashing during a clean shutdown ("Disposed") or
// before a real game has started.
if (reason != "Disposed" && State.Phase != GamePhase.Lobby)
_manager?.uiSubsystem?.ShowReconnecting();
if (reason != "Disposed" && _manager != null)
_manager.StartCoroutine(ReconnectAfterDelay(3f));
Debug.Log($"Host disconnected due to {reason}");
}
private IEnumerator ReconnectAfterDelay(float seconds)
private void OnError(string error)
{
yield return new UnityEngine.WaitForSeconds(seconds);
Debug.Log("Attempting to reconnect...");
OpenConnection();
Debug.LogError($"Network error: {error}");
}
private void OnMessage(Message message)
{
switch (message.Type)
{
case "GameEvent":
OnGameEvent(message as GameEvent);
break;
case "CreateLobbyResponse":
Debug.Log("Received CreateLobbyResponse message");
HandleCreateLobbyResponse(message as CreateLobbyResponse);
break;
case "JoinLobbyResponse":
Debug.Log("Received JoinLobbyResponse message");
HandleJoinLobbyResponse(message as JoinLobbyResponse);
break;
case "PositionBroadcast":
HandlePositionBroadcast(message as PositionBroadcast);
break;
case "Error":
HandleErrorMessage(message as ErrorMessage);
break;
case "Ack":
case "GameEvent":
Debug.Log("Received Ack message");
break;
default:
Debug.Log("Received message of type: " + message.Type);
break;
}
}
/// <summary>
/// P9 defensive path: if the server tells us NOT_IN_LOBBY but we still
/// believe we have a lobby (LobbyId preserved across the transient
/// disconnect), the lobby association on the server's side of the new
/// connection is missing - typically a race between OpenConnection's
/// Reconnect call and an in-flight action message that beat it. Retry
/// the Reconnect; if the second attempt also bounces, the lobby really
/// is gone and we'll surface the error to the user.
/// </summary>
private void HandleErrorMessage(ErrorMessage err)
{
if (err == null) return;
Debug.Log($"Server error: code={err.ErrorCode} text={err.ErrorText}");
if (err.ErrorCode == "NOT_IN_LOBBY" && !string.IsNullOrEmpty(_gameClient.LobbyId))
{
Debug.Log($"NOT_IN_LOBBY but we still have LobbyId={_gameClient.LobbyId}; resending Reconnect.");
_gameClient.Reconnect(_gameClient.LobbyId);
}
}
private void OnGameEvent(GameEvent gameEvent)
{
// Always sync player list from lobby state after any event
SyncPlayersFromLobby();
switch (gameEvent.EventType)
switch (gameEvent.Type)
{
case "PlayerJoined":
case "PlayerLeft":
case "HostChanged":
_manager?.uiSubsystem?.NotifyLobbyChanged();
Debug.Log($"Player {gameEvent.GetPayload<PlayerJoinedPayload>().DisplayName} joined");
HandlePlayerJoined(gameEvent);
break;
case "GameStarting":
State.Phase = GamePhase.Loading;
HandleGameStarting();
break;
case "MapDataReady":
HandleMapDataReady();
break;
case "GameStarted":
State.Phase = GamePhase.Playing;
break;
case "RoleAssigned":
HandleRoleAssigned(gameEvent);
break;
case "TaskCompleted":
HandleTaskCompleted(gameEvent);
break;
case "PlayerKilled":
HandlePlayerKilled(gameEvent);
break;
case "BodyReported":
case "EmergencyMeetingCalled":
Toast("Meeting called! Head to the meeting point.");
break;
case "MeetingStarted":
HandleMeetingStarted(gameEvent);
break;
case "PlayerArrivedAtMeeting":
HandlePlayerArrivedAtMeeting(gameEvent);
break;
case "PlayerVoted":
HandlePlayerVoted(gameEvent);
break;
case "VotingClosed":
HandleVotingClosed(gameEvent);
break;
case "GameEnded":
HandleGameEnded(gameEvent);
break;
case "ReturnedToLobby":
HandleReturnedToLobby();
break;
case "SabotageStarted":
HandleSabotageStarted(gameEvent);
break;
case "RepairStarted":
HandleRepairStarted(gameEvent);
break;
case "RepairStopped":
HandleRepairStopped(gameEvent);
break;
case "SabotageRepaired":
case "SabotageMeltdown":
case "SabotageExpired":
State.ActiveSabotage = null;
State.ActiveRepairs.Clear();
_manager?.uiSubsystem?.HideSabotageTimer();
_manager?.mapSubsystem?.ClearSabotageMarkers();
break;
case "TaskStarted":
// Server now broadcasts when a player begins a task. Phase 1
// only acks; Phase 2/3 will surface this to other players.
break;
case "MapDataError":
HandleMapDataError(gameEvent);
break;
default:
Debug.Log("GameEvent: " + gameEvent.EventType);
Debug.Log("Received GameEvent of type: " + gameEvent.Type);
break;
}
}
// ── Lobby responses ───────────────────────────────────────────────────
private void HandleCreateLobbyResponse(CreateLobbyResponse message)
{
if (message == null) return;
if (message.Success)
{
Debug.Log($"Lobby created. Code: {message.JoinCode}");
// P13b: snapshot the server's authoritative settings into
// GameState so HUD / proximity code can read distances and
// cooldowns from a single source of truth instead of hardcodes.
State.Settings = _gameClient.CurrentLobbyState?.Settings;
SceneManager.LoadScene("create", LoadSceneMode.Single);
_manager?.uiSubsystem?.NotifyLobbyChanged();
Debug.Log("Lobby created successfully. Join Code: " + message.JoinCode + ", Lobby ID: " + message.LobbyId);
}
else
{
Debug.LogError("Failed to create lobby: " + message.Error);
}
}
private void HandleJoinLobbyResponse(JoinLobbyResponse message)
{
if (message == null) return;
if (message.Success)
{
Debug.Log($"Joined lobby: {message.LobbyId}");
// P13b: same settings snapshot path as host. Joiners read the
// server's snapshot taken at lobby creation; they cannot edit.
State.Settings = _gameClient.CurrentLobbyState?.Settings;
// Unified lobby: both host and joiners land on create.unity.
// LobbyDisplayUI handles the role split internally (start
// button for host, waiting text for joiners).
SceneManager.LoadScene("create", LoadSceneMode.Single);
_manager?.uiSubsystem?.NotifyLobbyChanged();
Debug.Log("Lobby created successfully." + ", Lobby ID: " + message.LobbyId);
}
else
{
Debug.LogError("Failed to join lobby: " + message.Error);
Debug.LogError("Failed to create lobby: " + message.Error);
}
}
// ── Game flow ─────────────────────────────────────────────────────────
private void HandleGameStarting()
private void HandlePlayerJoined(GameEvent gameEvent)
{
_pendingMapBuild = false;
// Reset per-game state
State.MyRole = null;
State.IsDead = false;
State.MyTasks = new List<GameTask>();
State.MyCompletedTaskIds = new HashSet<string>();
State.TotalCompleted = 0;
State.TotalRequired = 0;
State.ActiveMeeting = null;
State.LastVoteResult = null;
State.VotedPlayerIds = new HashSet<string>();
State.ActiveSabotage = null;
State.GameEndData = null;
State.KillCooldownRemaining = 0;
SceneManager.LoadScene("Client", LoadSceneMode.Single);
}
private void HandleMapDataReady()
var payload = gameEvent.GetPayload<PlayerJoinedPayload>();
_gameClient.CurrentLobbyState.Players.Add(new PlayerInfo
{
_pendingMapBuild = true;
TryBuildMapAndMarkers();
ClientUuid = payload.ClientUuid,
DisplayName = payload.DisplayName,
IsOwner = false,
IsReady = false,
State = PlayerState.Alive
});
}
public void OnClientSceneReady()
public void CrateLobby(double lat, double lon)
{
TryBuildMapAndMarkers();
_gameClient.CreateLobby(new Position(lat, lon));
}
private void TryBuildMapAndMarkers()
{
if (!_pendingMapBuild) return;
if (_manager?.mapSubsystem == null || !_manager.mapSubsystem.IsSceneReady) return;
if (_gameClient?.CurrentLobbyState?.MapData == null) return;
_manager.mapSubsystem.BuildMap();
_manager.mapSubsystem.CreateTaskMarkers(_gameClient.MyTasks);
_pendingMapBuild = false;
Debug.Log("[Network] Map built.");
}
private void HandleRoleAssigned(GameEvent evt)
{
var payload = evt.GetPayload<RoleAssignedPayload>();
if (payload == null || payload.ClientUuid != _gameClient.ClientUuid) return;
State.MyRole = payload.Role;
State.MyTasks = payload.Tasks ?? new List<GameTask>();
State.MyCompletedTaskIds.Clear();
Debug.Log($"Role: {payload.Role}, Tasks: {State.MyTasks.Count}");
_manager?.taskSubsystem?.Initialize(State.MyTasks);
}
private void HandleTaskCompleted(GameEvent evt)
{
var payload = evt.GetPayload<TaskCompletedPayload>();
if (payload == null) return;
// Track if it's our task
if (payload.ClientUuid == _gameClient.ClientUuid)
State.MyCompletedTaskIds.Add(payload.TaskId);
State.TotalCompleted = payload.TotalCompleted;
State.TotalRequired = payload.TotalTasks;
_manager?.uiSubsystem?.UpdateTaskProgress(payload.TotalCompleted, payload.TotalTasks);
_manager?.mapSubsystem?.RemoveTaskMarker(payload.TaskId);
}
private void HandlePlayerKilled(GameEvent evt)
{
var payload = evt.GetPayload<PlayerKilledPayload>();
if (payload == null) return;
_manager?.mapSubsystem?.CreateBodyMarker(payload.BodyId, payload.Location);
if (payload.VictimId == _gameClient.ClientUuid)
{
State.IsDead = true;
_manager?.uiSubsystem?.OnLocalPlayerDied();
}
// Update player state in our list
var p = State.Players.Find(x => x.ClientUuid == payload.VictimId);
if (p != null) p.State = PlayerState.Dead;
}
private void HandleMeetingStarted(GameEvent evt)
{
var payload = evt.GetPayload<MeetingStartedPayload>();
if (payload == null) return;
State.Phase = GamePhase.Meeting;
State.ActiveMeeting = payload;
State.VotedPlayerIds = new HashSet<string>();
State.ArrivedPlayerIds = new HashSet<string>();
State.VoterTargets = new Dictionary<string, string>();
State.VoteTallies = new Dictionary<string, int>();
State.MyVoteTarget = null;
State.LastVoteResult = null;
SyncPlayersFromLobby();
_manager?.uiSubsystem?.ShowMeetingPanel(State.Players, payload);
}
private void HandlePlayerArrivedAtMeeting(GameEvent evt)
{
var payload = evt.GetPayload<PlayerArrivedAtMeetingPayload>();
if (payload == null) return;
State.ArrivedPlayerIds.Add(payload.ClientUuid);
}
private void HandlePlayerVoted(GameEvent evt)
{
var payload = evt.GetPayload<PlayerVotedPayload>();
if (payload == null) return;
// Server allows vote changes within a 2s rate limit, so we always
// overwrite the voter's previous target rather than appending.
string target = payload.TargetId ?? GameState.VoteSkip;
State.VotedPlayerIds.Add(payload.VoterId);
State.VoterTargets[payload.VoterId] = target;
RecomputeVoteTallies();
if (payload.VoterId == _gameClient.ClientUuid)
State.MyVoteTarget = target;
}
private void RecomputeVoteTallies()
{
State.VoteTallies.Clear();
foreach (var t in State.VoterTargets.Values)
{
if (string.IsNullOrEmpty(t)) continue;
State.VoteTallies.TryGetValue(t, out var count);
State.VoteTallies[t] = count + 1;
}
}
private void HandleVotingClosed(GameEvent evt)
{
var payload = evt.GetPayload<VotingClosedPayload>();
if (payload == null) return;
State.Phase = GamePhase.Playing;
State.ActiveMeeting = null;
State.LastVoteResult = payload;
// Mark ejected player dead in our list
if (!string.IsNullOrEmpty(payload.EjectedPlayerId))
{
var p = State.Players.Find(x => x.ClientUuid == payload.EjectedPlayerId);
if (p != null) p.State = PlayerState.Dead;
}
_manager?.uiSubsystem?.ShowVoteResult(payload, State.Players);
_manager?.mapSubsystem?.ClearBodyMarkers();
}
private void HandleGameEnded(GameEvent evt)
{
var payload = evt.GetPayload<GameEndedPayload>();
if (payload == null) return;
State.Phase = GamePhase.Ended;
State.GameEndData = payload;
// If the round ended while the meeting/vote-result overlay was
// still up (e.g. ejection won the game outright), the auto-close
// coroutine would otherwise fire 5s later and tear down the
// meeting panel while the GameEndPanel sits on top - leaving a
// glimpse of the dead overlay during the transition.
_manager?.uiSubsystem?.HideMeetingPanel();
_manager?.uiSubsystem?.ShowGameEndPanel(payload, _gameClient.ClientUuid);
}
private void HandleReturnedToLobby()
{
State.Phase = GamePhase.Lobby;
_manager?.uiSubsystem?.HideMeetingPanel();
// Bodies survive the scene reload because the marker GameObjects are
// parented under MapCenterPoint (which lives in the persistent
// Client.unity scene). Without this clear, returning to lobby and
// starting a new round leaves stale corpses on the map of the new
// round. Server already cleared its `_bodies` set in
// ProcessReturnToLobby; this is the client-side mirror that was
// missing in HandleVotingClosed's symmetry.
_manager?.mapSubsystem?.ClearBodyMarkers();
_manager?.mapSubsystem?.ClearSabotageMarkers();
// Unified lobby: regardless of role, return to create.unity.
SceneManager.LoadScene("create", LoadSceneMode.Single);
}
private void HandleSabotageStarted(GameEvent evt)
{
var payload = evt.GetPayload<SabotageStartedPayload>();
if (payload == null) return;
State.ActiveSabotage = payload;
State.ActiveRepairs.Clear();
_manager?.mapSubsystem?.CreateSabotageMarkers(payload.RepairStations);
if (payload.Type == SabotageType.CriticalMeltdown && payload.Deadline.HasValue)
_manager?.uiSubsystem?.ShowSabotageTimer(payload.Deadline.Value);
if (payload.Type == SabotageType.CommsBlackout)
_manager?.uiSubsystem?.SetCommsBlackout(true);
}
private void HandleRepairStarted(GameEvent evt)
{
var payload = evt.GetPayload<RepairStartedPayload>();
if (payload == null || string.IsNullOrEmpty(payload.StationId)) return;
State.ActiveRepairs.Add(payload.StationId);
}
private void HandleRepairStopped(GameEvent evt)
{
// A player abandoned a repair station mid-fix. The station is no
// longer counted as active for the simultaneous-repair coaching;
// the marker stays on the map until the sabotage resolves.
var payload = evt.GetPayload<RepairStoppedPayload>();
if (payload != null && !string.IsNullOrEmpty(payload.StationId))
State.ActiveRepairs.Remove(payload.StationId);
}
private void HandleMapDataError(GameEvent evt)
{
// Server failed to fetch Overpass data. Without this the loading
// screen would hang forever. Drop back to lobby and surface the
// failure so the player can re-host or try a different center.
Debug.LogError("[Network] Server could not generate map data.");
State.Phase = GamePhase.Lobby;
_manager?.uiSubsystem?.ShowToast("Map fetch failed. Returning to lobby.");
LeaveLobby();
}
private void HandlePositionBroadcast(PositionBroadcast broadcast)
{
if (broadcast == null) return;
_manager?.mapSubsystem?.UpdatePlayerAvatars(_gameClient.PlayerPositions, _gameClient.ClientUuid);
}
// ── Helpers ───────────────────────────────────────────────────────────
private void SyncPlayersFromLobby()
{
var lobby = _gameClient.CurrentLobbyState;
if (lobby?.Players != null)
State.Players = lobby.Players;
}
private void Toast(string message)
{
State.ToastMessage = message;
State.ToastExpiry = UnityEngine.Time.time + 4f;
}
// ── Send helpers ──────────────────────────────────────────────────────
public void CreateLobby(double lat, double lon, double radius = 500, int impostorCount = 1, int taskCount = 5, GameSettingsOverrides settings = null)
{
_gameClient.CreateLobby(new Position(lat, lon), impostorCount, taskCount, null, radius, settings);
}
public void JoinLobby(string joinCode)
{
try { _gameClient.JoinLobby(joinCode); }
catch (System.Exception ex) { Debug.LogError("JoinLobby error: " + ex.Message); }
try
{
_gameClient.JoinLobby(joinCode);
}
catch (System.Exception ex)
{
Debug.LogError("Error joining lobby: " + ex.Message);
}
}
public void LeaveLobby()
{
_gameClient.LeaveLobby();
State.Phase = GamePhase.Lobby;
SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single);
_gameClient.Disconnect();
Application.Quit();
}
public void StartGame()
{
_gameClient.StartGame();
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 989e9292fe24c2a4ba95ceae191dd330

View File

@@ -1,328 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using GeoSus.Client;
namespace Subsystems
{
/// <summary>
/// Round-robin task-to-minigame assignment, proximity detection, additive scene launch.
/// </summary>
public class GameManager_Tasks
{
private class TaskEntry
{
public GeoSus.Client.GameTask ServerTask;
public string MinigameScene;
public bool Completed;
}
private GameClient _gameClient;
private string[] _minigameScenes;
private MonoBehaviour _host; // GameManager MonoBehaviour for coroutines
private List<TaskEntry> _tasks = new List<TaskEntry>();
private bool _minigameOpen;
private string _loadedMinigameScene;
private Camera _hostCameraSuspended;
private GameObject _hostInGameHudHidden;
// Proximity state (checked every frame in UpdateProximity)
public GeoSus.Client.GameTask NearbyTask { get; private set; }
// P13b: per-check distances pulled from the server-snapshotted lobby
// settings (null-fallback to 5m matches the old hardcoded behavior).
// Different actions use different fields so a host can tune e.g. a
// long-range "spotter" task radius without also widening kill range.
private const float ProximityRadiusFallback = 5f;
public GameManager_Tasks(GameClient gameClient, string[] minigameScenes, MonoBehaviour host)
{
_gameClient = gameClient;
_minigameScenes = minigameScenes ?? new string[0];
_host = host;
}
/// <summary>Called by Network subsystem when RoleAssigned fires.</summary>
public void Initialize(List<GeoSus.Client.GameTask> serverTasks)
{
_tasks.Clear();
if (_minigameScenes.Length == 0) return;
for (int i = 0; i < serverTasks.Count; i++)
{
_tasks.Add(new TaskEntry
{
ServerTask = serverTasks[i],
MinigameScene = _minigameScenes[i % _minigameScenes.Length],
Completed = false
});
}
// Create map markers
GameManager.Instance?.mapSubsystem?.CreateTaskMarkers(serverTasks);
Debug.Log($"[Tasks] Initialized {_tasks.Count} tasks.");
}
/// <summary>Called every frame from GameManager.Update().</summary>
public void UpdateProximity()
{
if (_minigameOpen) return;
// P13b: distances now come from the per-lobby settings snapshot
// instead of one hardcoded 5m radius for everything. ?? fallback
// matches the old behavior when running against an old server.
var state = GameManager.Instance?.networkSubsystem?.State;
var settings = state?.Settings;
double taskDist = settings?.TaskStartDistanceM ?? ProximityRadiusFallback;
double reportDist = settings?.ReportDistanceM ?? ProximityRadiusFallback;
double emergencyDist = settings?.EmergencyMeetingCallRadiusM?? ProximityRadiusFallback;
double killDist = settings?.KillDistanceM ?? ProximityRadiusFallback;
NearbyTask = null;
var myPos = _gameClient.MyPosition;
if (myPos.Lat == 0 && myPos.Lon == 0) return;
foreach (var entry in _tasks)
{
if (entry.Completed) continue;
double dist = myPos.DistanceTo(entry.ServerTask.Location);
if (dist <= taskDist)
{
NearbyTask = entry.ServerTask;
break;
}
}
// Drive the action button in UI
var ui = GameManager.Instance?.uiSubsystem;
if (ui == null || ui.IsPlayerDead) return;
bool isImpostor = _gameClient.MyRole == GeoSus.Client.PlayerRole.Impostor;
if (!isImpostor && NearbyTask != null)
{
ui.SetActionButton("USE", true, () => GameManager.Instance?.PerformAction());
return;
}
// Check body proximity
if (!ui.IsCommsBlackout)
{
var body = _gameClient.FindNearbyBody(reportDist);
if (body != null)
{
ui.SetActionButton("REPORT", true, () => GameManager.Instance?.PerformAction());
return;
}
// Emergency meeting proximity
if (_gameClient.CurrentLobbyState?.MapData != null)
{
double dist = myPos.DistanceTo(_gameClient.CurrentLobbyState.MapData.Center);
if (dist <= emergencyDist)
{
ui.SetActionButton("EMERGENCY", true, () => GameManager.Instance?.PerformAction());
return;
}
}
}
// Impostor kill
if (isImpostor)
{
var target = _gameClient.FindNearbyPlayer(killDist);
if (!string.IsNullOrEmpty(target))
{
ui.SetActionButton("KILL", true, () => GameManager.Instance?.PerformAction());
// Hide sabotage menu while a kill is on offer (cleaner HUD).
ui.SetSabotageMenuVisible(false);
return;
}
}
// Nothing nearby
ui.SetActionButton("", false);
// P13g: persistent sabotage menu for impostors when no proximity
// action is on offer. Hidden when state isn't suitable - dead,
// not-impostor, in meeting, sabotage already active, or comms
// blackout (the impostor's own sabotage triggers a UI lock).
bool inPlayingPhase = state != null && state.Phase == GeoSus.Client.GamePhase.Playing;
bool sabotageActive = state?.ActiveSabotage != null;
bool showSabMenu = isImpostor && !ui.IsPlayerDead && inPlayingPhase &&
!sabotageActive && !ui.IsCommsBlackout;
ui.SetSabotageMenuVisible(showSabMenu);
}
/// <summary>Called externally (e.g., GameManager.PerformAction) to launch the nearby task.</summary>
public void TriggerNearbyTask()
{
OnUsePressed();
}
private void OnUsePressed()
{
if (NearbyTask == null || _minigameOpen) return;
var entry = _tasks.Find(t => t.ServerTask.TaskId == NearbyTask.TaskId);
if (entry != null) _host.StartCoroutine(LaunchMinigame(entry));
}
private IEnumerator LaunchMinigame(TaskEntry entry)
{
_minigameOpen = true;
Debug.Log($"[Tasks] Launching minigame '{entry.MinigameScene}' for task '{entry.ServerTask.Name}'");
// Validate that the scene name resolves to a build-included scene.
// LoadSceneAsync silently returns null when the scene name doesn't
// match (case-sensitive) or isn't in EditorBuildSettings, which
// leaves the action button looking dead from the player's POV.
if (string.IsNullOrEmpty(entry.MinigameScene) ||
!Application.CanStreamedLevelBeLoaded(entry.MinigameScene))
{
Debug.LogError($"[Tasks] Minigame scene '{entry.MinigameScene}' is not loadable. " +
$"Check the scene name (case-sensitive) and that it's enabled in Build Settings.");
GameManager.Instance?.uiSubsystem?.ShowToast(
$"Task scene missing: {entry.MinigameScene}");
_minigameOpen = false;
yield break;
}
// Inform server that task started
_gameClient.Send(new TaskStart { TaskId = entry.ServerTask.TaskId });
// Disable the host scene's main camera while the minigame is up.
// With both cameras enabled the minigame's UI/3D content would
// fight the host's map camera for screen space, and what gets
// drawn depends on Camera.depth which isn't guaranteed across
// scenes. Restored in FinishMinigame.
_hostCameraSuspended = Camera.main;
if (_hostCameraSuspended != null) _hostCameraSuspended.enabled = false;
// Hide the persistent InGame HUD canvas (if present). It lives
// in Client.unity and renders Screen Space - Overlay so it would
// otherwise stack on top of the minigame's UI regardless of
// which scene is active. SetActive(false) is reversible.
_hostInGameHudHidden = GameObject.Find("InGame");
if (_hostInGameHudHidden != null && _hostInGameHudHidden.activeSelf)
_hostInGameHudHidden.SetActive(false);
else
_hostInGameHudHidden = null; // nothing to restore
var op = SceneManager.LoadSceneAsync(entry.MinigameScene, LoadSceneMode.Additive);
if (op == null)
{
Debug.LogError($"[Tasks] LoadSceneAsync returned null for '{entry.MinigameScene}'.");
GameManager.Instance?.uiSubsystem?.ShowToast(
$"Task scene failed to load: {entry.MinigameScene}");
if (_hostCameraSuspended != null) { _hostCameraSuspended.enabled = true; _hostCameraSuspended = null; }
_minigameOpen = false;
yield break;
}
yield return op;
_loadedMinigameScene = entry.MinigameScene;
// CRITICAL: switch the active scene to the loaded minigame.
// LoadSceneMode.Additive stacks scenes without changing which one
// is "active" - and an inactive scene's RenderSettings, ambient
// light, and skybox don't drive rendering. The host (Client.unity)
// remains active and its lighting context still applies, which
// is the root cause of "task opens to white screen": the
// minigame's content loads but its visuals don't take over.
// Without SetActiveScene, even minigames that ARE wired up
// correctly render against the host's lighting and look broken.
Scene scene = SceneManager.GetSceneByName(entry.MinigameScene);
if (scene.IsValid()) SceneManager.SetActiveScene(scene);
// Diagnostic: count cameras / canvases / lights in the loaded
// scene. If the white screen persists after this fix, the
// numbers tell us whether the scene is missing rendering bits
// (camera=0, canvas=0) or if the issue is elsewhere.
int camCount = 0, canvasCount = 0, lightCount = 0;
foreach (var root in scene.GetRootGameObjects())
{
camCount += root.GetComponentsInChildren<Camera>(true).Length;
canvasCount += root.GetComponentsInChildren<Canvas>(true).Length;
lightCount += root.GetComponentsInChildren<Light>(true).Length;
}
Debug.Log($"[Tasks] Loaded '{entry.MinigameScene}': cameras={camCount}, " +
$"canvases={canvasCount}, lights={lightCount}, " +
$"activeScene={SceneManager.GetActiveScene().name}");
// Find the ITask component in the newly loaded scene
ITask taskComponent = null;
foreach (var root in scene.GetRootGameObjects())
{
taskComponent = root.GetComponentInChildren<ITask>();
if (taskComponent != null) break;
}
if (taskComponent == null)
{
Debug.LogWarning($"[Tasks] No ITask found in '{entry.MinigameScene}'. " +
$"Either the minigame's controller script isn't attached to a GameObject in the scene, " +
$"or the script doesn't implement ITask. Auto-completing.");
yield return FinishMinigame(entry, true);
yield break;
}
// Set task metadata
taskComponent.TaskID = entry.ServerTask.TaskId;
taskComponent.TaskName = entry.ServerTask.Name;
taskComponent.TaskLocation = (entry.ServerTask.Location.Lat, entry.ServerTask.Location.Lon);
bool done = false;
taskComponent.Initialize(t => { done = true; });
// Wait for completion or exit
yield return new WaitUntil(() => done);
yield return FinishMinigame(entry, done);
}
private IEnumerator FinishMinigame(TaskEntry entry, bool completed)
{
if (completed)
{
entry.Completed = true;
_gameClient.CompleteTask(entry.ServerTask.TaskId);
Debug.Log($"[Tasks] Task '{entry.ServerTask.Name}' completed.");
}
else
{
Debug.Log($"[Tasks] Task '{entry.ServerTask.Name}' exited without completion.");
}
// Unload minigame scene. Switch the active scene back to the
// host BEFORE the unload so we don't end up with no active
// scene mid-frame (Unity will complain and lighting flickers).
if (!string.IsNullOrEmpty(_loadedMinigameScene))
{
var hostScene = SceneManager.GetSceneByName("Client");
if (hostScene.IsValid()) SceneManager.SetActiveScene(hostScene);
var unload = SceneManager.UnloadSceneAsync(_loadedMinigameScene);
yield return unload;
_loadedMinigameScene = null;
}
// Re-enable the host camera that was suspended during the minigame.
if (_hostCameraSuspended != null)
{
_hostCameraSuspended.enabled = true;
_hostCameraSuspended = null;
}
// Re-show the InGame HUD canvas hidden at minigame entry.
if (_hostInGameHudHidden != null)
{
_hostInGameHudHidden.SetActive(true);
_hostInGameHudHidden = null;
}
_minigameOpen = false;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 27a123dbda9eef8ba4815c0c0d30b6fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,934 +1,35 @@
using UnityEngine;
using UnityEngine.UI;
using Subsystems;
using GeoSus.Client;
using System.Collections.Generic;
using System;
using System.Linq;
using TMPro;
using System.ComponentModel;
namespace Subsystems
{
/// <summary>
/// Reads from GameManager_Network.State (the authoritative GameState) and drives
/// all in-game canvas panels. No business logic lives here.
/// </summary>
public class GameManager_UI
{
private GameClient _gameClient;
private GameState _state => GameManager.Instance?.networkSubsystem?.State;
// ── Canvas refs (wired by BindClientScene from Client.unity) ──────────
public Canvas ClientCreateJoinLobby;
public Canvas ClientInLobby;
public Canvas ClientLoadingScreen;
public Canvas ClientGameScreen;
// ── HUD element refs (resolved once in BindClientScene) ───────────────
private TMP_Text _roleText;
private TMP_Text _taskListText;
private TMP_Text _taskProgressText;
private Button _actionButton;
private TMP_Text _actionButtonText;
private TMP_Text _killCooldownText;
private GameObject _sabotagePanel;
private TMP_Text _sabotageTimerText;
private GameObject _meetingPanel;
private TMP_Text _meetingHeader;
private TMP_Text _meetingPhaseLabel;
private TMP_Text _meetingPhaseCountdown;
private Image _meetingPhaseProgressBar;
private TMP_Text _myVoteIndicator;
private GameObject _meetingScrollGO;
private Transform _meetingScrollContent;
private TMP_Text _meetingFallbackText;
private GameObject _voteResultPanel;
private TMP_Text _voteResultText;
private Button _skipButton;
private GameObject _gameEndPanel;
private TMP_Text _gameEndText;
private RectTransform _returnToLobbyBtn;
private TMP_Text _toastText;
private GameObject _toastGO;
private GameObject _reconnectOverlay;
// ── Internal state ────────────────────────────────────────────────────
private bool _isDead;
private bool _commsBlackout;
private DateTime _sabotageMeltdownDeadline;
private bool _sabotageTimerActive;
private volatile bool _lobbyDirty;
// Meeting vote-row references rebuilt each meeting
private readonly List<GameObject> _voteRows = new List<GameObject>();
private string _pendingVoteResultDisplay; // shown after voting
private Coroutine _meetingCloseCoroutine; // tracked so phase changes can cancel it
public GameManager_UI(GameClient gameClient) { _gameClient = gameClient; }
public void NotifyLobbyChanged() => _lobbyDirty = true;
public bool IsCommsBlackout => _commsBlackout;
public bool IsPlayerDead => _isDead;
// ── Scene binding ─────────────────────────────────────────────────────
public void BindClientScene(Canvas createJoin, Canvas inLobby, Canvas loading, Canvas game)
private Canvas _CreateJoinLobby;
private Canvas _InLobby;
public GameManager_UI(GameClient gameClient, Canvas CreateJoinLobby, Canvas InLobby)
{
ClientCreateJoinLobby = createJoin;
ClientInLobby = inLobby;
ClientLoadingScreen = loading;
ClientGameScreen = game;
foreach (var c in new[] { createJoin, inLobby, loading, game })
EnsureCanvasReady(c);
if (createJoin) createJoin.gameObject.SetActive(false);
if (inLobby) inLobby.gameObject.SetActive(false);
if (loading) loading.gameObject.SetActive(false);
if (game) game.gameObject.SetActive(false);
if (game == null) return;
var t = game.transform;
_roleText = FindTMP(t, "Role");
_taskListText = FindTMP(t, "TaskList");
_taskProgressText = FindTMP(t, "TaskProgress");
_killCooldownText = FindTMP(t, "KillCooldown");
_sabotageTimerText = FindTMP(t, "SabotageTimer");
_gameEndText = FindTMP(t, "GameEndText");
_toastText = FindTMP(t, "Toast");
_meetingHeader = FindTMP(t, "MeetingHeader");
_meetingPhaseLabel = FindTMP(t, "MeetingPhaseLabel");
_meetingPhaseCountdown = FindTMP(t, "MeetingPhaseCountdown");
_myVoteIndicator = FindTMP(t, "MyVoteIndicator");
_meetingFallbackText = FindTMP(t, "MeetingPlayerList");
_voteResultText = FindTMP(t, "VoteResult");
_meetingScrollContent = FindTransform(t, "MeetingContent");
_meetingScrollGO = FindTransformGO(t, "_MeetingScroll");
var progressBarGO = FindTransformGO(t, "MeetingPhaseProgressBar");
if (progressBarGO != null) _meetingPhaseProgressBar = progressBarGO.GetComponent<Image>();
var skipGO = FindTransformGO(t, "SkipButton");
if (skipGO != null) _skipButton = skipGO.GetComponent<Button>();
var actionGO = t.Find("ActionButton");
if (actionGO != null)
{
_actionButton = actionGO.GetComponent<Button>();
_actionButtonText = actionGO.GetComponentInChildren<TMP_Text>();
_gameClient = gameClient;
_CreateJoinLobby = CreateJoinLobby;
_InLobby = InLobby;
_CreateJoinLobby.enabled = true;
_InLobby.enabled = false;
}
_sabotagePanel = t.Find("SabotagePanel")?.gameObject;
_meetingPanel = t.Find("MeetingPanel")?.gameObject;
_gameEndPanel = t.Find("GameEndPanel")?.gameObject;
_voteResultPanel = FindTransformGO(t, "VoteResultPanel");
_toastGO = FindTransformGO(t, "Toast");
_reconnectOverlay = FindTransformGO(t, "ReconnectOverlay");
var retBtn = FindTransform(t, "ReturnToLobbyButton");
if (retBtn != null) _returnToLobbyBtn = retBtn as RectTransform;
if (_meetingPanel) _meetingPanel.SetActive(false);
if (_gameEndPanel) _gameEndPanel.SetActive(false);
if (_voteResultPanel) _voteResultPanel.SetActive(false);
if (_toastGO) _toastGO.SetActive(false);
if (_reconnectOverlay) _reconnectOverlay.SetActive(false);
}
// ── Update (called every frame from GameManager.Update) ───────────────
public void UpdateLobbyUI()
{
var lobbyState = _gameClient.CurrentLobbyState;
if (lobbyState == null) return;
if (_lobbyDirty)
_InLobby.enabled = true;
_CreateJoinLobby.enabled = false;
var playerList = _InLobby.transform.Find("PlayerList").GetComponent<TMPro.TMP_Text>();
playerList.text = "";
foreach (var player in _gameClient.CurrentLobbyState.Players)
{
_lobbyDirty = false;
LobbyDisplayUI.RefreshAll(lobbyState);
playerList.text += player.DisplayName + "\n";
}
if (ClientGameScreen == null) return;
switch (lobbyState.Phase)
{
case GamePhase.Loading:
SetCanvases(false, false, true, false);
break;
case GamePhase.Lobby:
SetCanvases(false, true, false, false);
break;
case GamePhase.Playing:
case GamePhase.Meeting:
case GamePhase.Voting:
SetCanvases(false, false, false, true);
UpdateGameHUD();
break;
case GamePhase.Ended:
SetCanvases(false, false, false, true);
break;
}
TickToast();
}
// ── Game HUD tick ─────────────────────────────────────────────────────
private void UpdateGameHUD()
{
var s = _state;
if (s == null) return;
// Role
if (_roleText != null)
{
string ghostSuffix = s.IsDead ? " (GHOST)" : "";
_roleText.text = $"{s.MyRole?.ToString() ?? "?"}{ghostSuffix}";
_roleText.color = s.MyRole == PlayerRole.Impostor ? new Color(0.9f,0.2f,0.2f) : new Color(0.2f,0.8f,1f);
}
// Task list with checkmarks
if (_taskListText != null)
{
var sb = new System.Text.StringBuilder();
foreach (var task in s.MyTasks)
{
bool done = s.MyCompletedTaskIds.Contains(task.TaskId);
string mark = done ? "<color=#2DB84B>✓</color>" : "○";
sb.AppendLine($"{mark} {task.Name}");
}
_taskListText.text = sb.ToString();
}
// Global task progress
if (_taskProgressText != null && s.TotalRequired > 0)
_taskProgressText.text = $"Tasks: {s.TotalCompleted}/{s.TotalRequired}";
// Kill cooldown
if (_killCooldownText != null)
{
bool show = s.KillCooldownRemaining > 0;
_killCooldownText.gameObject.SetActive(show);
if (show) _killCooldownText.text = $"Kill: {Mathf.CeilToInt(s.KillCooldownRemaining)}s";
}
// Sabotage banner - meltdown countdown plus simultaneous-repair coaching
if (_sabotageTimerActive && _sabotageTimerText != null)
{
double remaining = (_sabotageMeltdownDeadline - DateTime.UtcNow).TotalSeconds;
string head = remaining > 0 ? $"⚠ MELTDOWN: {remaining:F0}s" : "⚠ MELTDOWN!";
// For multi-station sabotages, surface how many of the required
// simultaneous repair stations are currently active. This is
// what makes "you're alone, you need a partner" obvious.
int required = s.ActiveSabotage?.RequiredSimultaneousRepairs ?? 0;
if (required > 1)
{
int active = s.ActiveRepairs.Count;
head += $" <size=32>{active}/{required} stations active</size>";
}
_sabotageTimerText.text = head;
}
// Keep meeting sub-phase strip, countdown, vote gating, tallies and
// my-vote indicator fresh each frame.
UpdateMeetingPhaseStrip();
}
// ── Kill cooldown helper (called from GameManager) ────────────────────
// ── Reconnect overlay ─────────────────────────────────────────────────
/// <summary>
/// Show a full-screen "Reconnecting..." overlay. Call when the socket
/// drops mid-game; the server keeps the player slot for ~60s before
/// removing them so a brief disconnect is recoverable.
/// </summary>
public void ShowReconnecting()
{
if (_reconnectOverlay) _reconnectOverlay.SetActive(true);
}
/// <summary>
/// Hide the reconnect overlay. Call from OnConnected once the socket
/// is healthy again.
/// </summary>
public void HideReconnecting()
{
if (_reconnectOverlay) _reconnectOverlay.SetActive(false);
}
public void SetKillCooldownText(string text)
{
if (_killCooldownText == null) return;
bool show = !string.IsNullOrEmpty(text);
_killCooldownText.gameObject.SetActive(show);
if (show) _killCooldownText.text = text;
}
public void UpdateTaskProgress(int completed, int total)
{
if (_taskProgressText != null)
_taskProgressText.text = $"Tasks: {completed}/{total}";
}
// ── Action button ─────────────────────────────────────────────────────
public void SetActionButton(string label, bool visible, UnityEngine.Events.UnityAction onClick = null)
{
if (_actionButton == null) return;
_actionButton.gameObject.SetActive(visible);
if (_actionButtonText != null) _actionButtonText.text = label;
if (onClick != null)
{
_actionButton.onClick.RemoveAllListeners();
_actionButton.onClick.AddListener(onClick);
}
}
// ── P13g: Impostor sabotage menu ──────────────────────────────────────
// The audit found that the production HUD never had an impostor
// sabotage trigger - GameManager.StartSabotage exists, the wire path
// is intact (StartSabotage -> server -> SabotageStarted broadcast +
// station markers), but no UI ever called it. So sabotages literally
// never fired in production. This menu fixes that gap with a runtime-
// built two-button overlay (no scene file change, no prefab needed).
private GameObject _sabotageMenuRoot;
private Button _sabotageBlackoutBtn;
private Button _sabotageMeltdownBtn;
private void EnsureSabotageMenu()
{
if (_sabotageMenuRoot != null || ClientGameScreen == null) return;
var canvasRT = ClientGameScreen.transform as RectTransform;
if (canvasRT == null) return;
// Root container - top-right corner, vertical stack.
_sabotageMenuRoot = new GameObject("ImpostorSabotageMenu", typeof(RectTransform), typeof(CanvasRenderer));
var rootRT = _sabotageMenuRoot.GetComponent<RectTransform>();
rootRT.SetParent(canvasRT, worldPositionStays: false);
rootRT.anchorMin = new Vector2(1, 1);
rootRT.anchorMax = new Vector2(1, 1);
rootRT.pivot = new Vector2(1, 1);
rootRT.anchoredPosition = new Vector2(-24, -180); // below the top-right safe-area
rootRT.sizeDelta = new Vector2(360, 240);
_sabotageBlackoutBtn = BuildSabotageOption(rootRT, "📡 BLACKOUT",
new Color(0.20f, 0.55f, 1.0f), 0, () => GameManager.Instance?.StartSabotage(0));
_sabotageMeltdownBtn = BuildSabotageOption(rootRT, "☢️ MELTDOWN",
new Color(1.0f, 0.30f, 0.30f), 1, () => GameManager.Instance?.StartSabotage(1));
_sabotageMenuRoot.SetActive(false);
}
private static Button BuildSabotageOption(RectTransform parent, string label, Color tint, int slot, UnityEngine.Events.UnityAction onClick)
{
// Each button: 360w x 110h, stacked vertically with 10px gap.
var go = new GameObject($"SabBtn_{slot}", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, worldPositionStays: false);
rt.anchorMin = new Vector2(0, 1);
rt.anchorMax = new Vector2(1, 1);
rt.pivot = new Vector2(0.5f, 1);
rt.anchoredPosition = new Vector2(0, -slot * 120);
rt.sizeDelta = new Vector2(0, 110);
var img = go.GetComponent<Image>();
img.color = new Color(tint.r * 0.4f, tint.g * 0.4f, tint.b * 0.4f, 0.92f);
// Border via outline component
var outline = go.AddComponent<Outline>();
outline.effectColor = tint;
outline.effectDistance = new Vector2(2, -2);
// Text child
var txtGO = new GameObject("Label", typeof(RectTransform));
var txtRT = txtGO.GetComponent<RectTransform>();
txtRT.SetParent(rt, worldPositionStays: false);
txtRT.anchorMin = Vector2.zero;
txtRT.anchorMax = Vector2.one;
txtRT.offsetMin = Vector2.zero;
txtRT.offsetMax = Vector2.zero;
var tmp = txtGO.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.alignment = TextAlignmentOptions.Center;
tmp.fontSize = 36;
tmp.color = Color.white;
tmp.fontStyle = FontStyles.Bold;
var btn = go.GetComponent<Button>();
btn.onClick.AddListener(onClick);
return btn;
}
/// <summary>
/// P13g: show the impostor sabotage menu when the local player is
/// alive impostor in the Playing phase with no active sabotage and
/// not in a meeting. Driven from GameManager_Tasks.UpdateProximity.
/// </summary>
public void SetSabotageMenuVisible(bool visible)
{
if (visible) EnsureSabotageMenu();
if (_sabotageMenuRoot != null && _sabotageMenuRoot.activeSelf != visible)
_sabotageMenuRoot.SetActive(visible);
}
// ── Player state ──────────────────────────────────────────────────────
public void OnLocalPlayerDied()
{
_isDead = true;
if (_state != null) _state.IsDead = true;
}
// ── Meeting ───────────────────────────────────────────────────────────
public void ShowMeetingAlert()
{
ShowToast("⚠ Meeting called! Head to the meeting point.");
}
public void ShowMeetingPanel(List<PlayerInfo> players, MeetingStartedPayload payload)
{
if (_meetingPanel == null) return;
_meetingPanel.SetActive(true);
if (_meetingHeader != null)
_meetingHeader.text = payload.Type == MeetingType.BodyReport ? "BODY REPORTED!" : "EMERGENCY MEETING!";
// Make sure the result subpanel is hidden at start of a fresh meeting,
// and the scroll list is visible (results phase will swap them).
if (_voteResultPanel) _voteResultPanel.SetActive(false);
if (_meetingScrollGO) _meetingScrollGO.SetActive(true);
if (_myVoteIndicator) _myVoteIndicator.text = "";
BuildMeetingVoteRows(players);
UpdateMeetingPhaseStrip();
}
private void BuildMeetingVoteRows(List<PlayerInfo> players)
{
// Clear old rows
foreach (var r in _voteRows) if (r) UnityEngine.Object.Destroy(r);
_voteRows.Clear();
if (_meetingScrollContent == null || players == null)
{
// Fall back to text list
if (_meetingFallbackText != null)
{
_meetingFallbackText.gameObject.SetActive(true);
var sb = new System.Text.StringBuilder();
foreach (var p in players ?? new List<PlayerInfo>())
sb.AppendLine($"{p.DisplayName} [{p.State}]");
_meetingFallbackText.text = sb.ToString();
}
return;
}
string myId = _gameClient.ClientUuid;
bool canVote = !_isDead;
// Dynamic row height: spread the available scroll-area height
// across however many players we have. Clamps so rows never get
// tinier than legible (small phone, many players -> 80px) or
// ridiculously tall (tablet, two players -> 140px).
float rowH = ComputeVoteRowHeight(players.Count);
foreach (var player in players)
{
bool isMe = player.ClientUuid == myId;
bool isAlive = player.State == PlayerState.Alive;
var row = BuildVoteRow(player, isMe, isAlive, canVote && isAlive && !isMe, rowH);
row.transform.SetParent(_meetingScrollContent, false);
_voteRows.Add(row);
}
}
/// <summary>
/// Compute a per-row height that fills the scroll viewport when there
/// are few players, and shrinks (until scrolling kicks in) when there
/// are many. Inputs are CanvasScaler reference coordinates, so the
/// values are device-independent.
/// </summary>
private float ComputeVoteRowHeight(int playerCount)
{
if (playerCount <= 0) return 110f;
// The scroll area occupies y=0.18 to y=0.74 of the canvas (per
// InGameHUDBuilder.BuildMeetingPanel) and reference height is 1920.
const float referenceHeight = 1920f;
const float scrollFraction = 0.74f - 0.18f; // 0.56
float available = referenceHeight * scrollFraction;
float h = available / playerCount;
return Mathf.Clamp(h, 80f, 140f);
}
private GameObject BuildVoteRow(PlayerInfo player, bool isMe, bool isAlive, bool canVote, float rowH)
{
var go = new GameObject($"VoteRow_{player.ClientUuid}");
var rt = go.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(0, rowH);
var le = go.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = rowH;
var bg = go.AddComponent<Image>();
bg.color = isMe ? new Color(0.12f,0.18f,0.30f) : new Color(0.10f,0.12f,0.20f);
// Dead overlay
if (!isAlive)
{
bg.color = new Color(0.08f,0.08f,0.10f,0.7f);
}
// Name label - left 50% (was 65%, gave width back to tally + button)
var namRT = MakeChild("Name", rt);
namRT.anchorMin = new Vector2(0,0); namRT.anchorMax = new Vector2(0.50f,1);
namRT.offsetMin = new Vector2(16,6); namRT.offsetMax = new Vector2(0,-6);
var namTmp = namRT.gameObject.AddComponent<TextMeshProUGUI>();
namTmp.text = (player.IsOwner ? "👑 " : "") + (player.DisplayName ?? "???");
namTmp.fontSize = 36;
namTmp.color = !isAlive ? Color.gray : (isMe ? Color.white : new Color(0.73f,0.8f,0.88f));
namTmp.fontStyle = isMe ? FontStyles.Bold : FontStyles.Normal;
namTmp.alignment = TextAlignmentOptions.MidlineLeft;
// Tally column - middle 18%, shows live vote count for this player
var tallyRT = MakeChild("Tally", rt);
tallyRT.anchorMin = new Vector2(0.50f,0); tallyRT.anchorMax = new Vector2(0.66f,1);
tallyRT.offsetMin = Vector2.zero; tallyRT.offsetMax = Vector2.zero;
var tallyTmp = tallyRT.gameObject.AddComponent<TextMeshProUGUI>();
tallyTmp.text = "";
tallyTmp.fontSize = 30;
tallyTmp.fontStyle = FontStyles.Bold;
tallyTmp.color = new Color(1f,0.72f,0.10f); // C_YELLOW-ish
tallyTmp.alignment = TextAlignmentOptions.Center;
// Vote button - right 30% (interactability is updated each frame)
var voteBtnRT = MakeChild("VoteBtn", rt);
voteBtnRT.anchorMin = new Vector2(0.68f,0.10f); voteBtnRT.anchorMax = new Vector2(0.95f,0.90f);
var voteBg = voteBtnRT.gameObject.AddComponent<Image>();
voteBg.color = canVote ? new Color(0.2f,0.6f,1f) : new Color(0.2f,0.2f,0.2f,0.5f);
var voteBtn = voteBtnRT.gameObject.AddComponent<Button>();
voteBtn.targetGraphic = voteBg;
voteBtn.interactable = canVote;
string capturedId = player.ClientUuid;
voteBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(capturedId));
var voteTxtRT = MakeChild("Txt", voteBtnRT);
Stretch(voteTxtRT);
var voteTmp = voteTxtRT.gameObject.AddComponent<TextMeshProUGUI>();
voteTmp.text = isAlive ? "VOTE" : "DEAD";
voteTmp.fontSize = 28;
voteTmp.fontStyle = FontStyles.Bold;
voteTmp.color = Color.white;
voteTmp.alignment = TextAlignmentOptions.Center;
// Voted-by-this-player checkmark (shown when the row's player has cast a vote)
var votedRT = MakeChild("VotedTick", rt);
votedRT.anchorMin = new Vector2(0.95f,0.20f); votedRT.anchorMax = new Vector2(1f,0.80f);
var vtTmp = votedRT.gameObject.AddComponent<TextMeshProUGUI>();
vtTmp.text = "✓"; vtTmp.fontSize = 34;
vtTmp.color = new Color(0.18f,0.75f,0.30f); vtTmp.alignment = TextAlignmentOptions.Center;
votedRT.gameObject.SetActive(false);
return go;
}
/// <summary>
/// Per-frame meeting UI update. Computes the meeting sub-phase from the
/// timestamps in MeetingStartedPayload (server doesn't broadcast a
/// discrete discussion-end event) and uses it to drive the countdown
/// label, progress bar, vote-button interactivity, live tallies, and
/// "Your vote: X" indicator.
/// </summary>
private void UpdateMeetingPhaseStrip()
{
var s = _state;
if (s == null) return;
// Only run if we're actually in a meeting; phase Playing skips the work.
if (s.Phase != GamePhase.Meeting && s.LastVoteResult == null) return;
var sub = s.GetMeetingSubPhase();
// ── Sub-phase label + countdown text + progress bar ───────────────
string label;
switch (sub)
{
case MeetingSubPhase.Arrival: label = "ARRIVAL"; break;
case MeetingSubPhase.Discussion: label = "DISCUSSION"; break;
case MeetingSubPhase.Voting: label = "VOTING"; break;
case MeetingSubPhase.Resolved: label = "RESULTS"; break;
default: label = ""; break;
}
if (_meetingPhaseLabel != null) _meetingPhaseLabel.text = label;
if (s.ActiveMeeting != null && sub != MeetingSubPhase.Resolved)
{
var deadline = s.GetMeetingSubPhaseDeadline(sub);
var remaining = (deadline - DateTime.UtcNow).TotalSeconds;
if (remaining < 0) remaining = 0;
if (_meetingPhaseCountdown != null)
{
int mins = (int)(remaining / 60);
int secs = (int)(remaining % 60);
string verb = sub == MeetingSubPhase.Voting ? "Voting ends in"
: sub == MeetingSubPhase.Discussion ? "Voting begins in"
: "Arrival ends in";
_meetingPhaseCountdown.text = $"{verb} {mins}:{secs:D2}";
}
// Progress bar drains over the current sub-phase. The server
// doesn't tell us when the meeting started, so we can only
// compute a meaningful fill for Discussion (start = arrival
// deadline) and Voting (start = discussion end / arrival
// deadline). Arrival's start time is unknown here; show full.
if (_meetingPhaseProgressBar != null)
{
if (sub == MeetingSubPhase.Arrival)
{
_meetingPhaseProgressBar.fillAmount = 1f;
}
else
{
DateTime start = sub == MeetingSubPhase.Discussion
? s.ActiveMeeting.ArrivalDeadline
: (s.ActiveMeeting.DiscussionEndTime ?? s.ActiveMeeting.ArrivalDeadline);
var total = (deadline - start).TotalSeconds;
var elapsed = (DateTime.UtcNow - start).TotalSeconds;
float fill = total > 0.001
? Mathf.Clamp01(1f - (float)(elapsed / total))
: 0f;
_meetingPhaseProgressBar.fillAmount = fill;
}
}
}
else
{
if (_meetingPhaseCountdown != null) _meetingPhaseCountdown.text = "";
if (_meetingPhaseProgressBar != null) _meetingPhaseProgressBar.fillAmount = 0f;
}
// ── Vote button gating + per-row tally / voted-indicator ──────────
bool votingOpen = sub == MeetingSubPhase.Voting && !_isDead;
bool iAmArrived = s.ActiveMeeting == null
|| s.ArrivedPlayerIds.Contains(_gameClient.ClientUuid);
// Skip button mirrors the same gate
if (_skipButton != null) _skipButton.interactable = votingOpen && iAmArrived;
foreach (var row in _voteRows)
{
if (row == null) continue;
string rowUuid = row.name.Replace("VoteRow_", "");
// Voted-tick: this row's player has cast a vote
var tick = row.transform.Find("VotedTick")?.gameObject;
if (tick != null) tick.SetActive(s.VotedPlayerIds.Contains(rowUuid));
// Tally text: how many votes is this row's player receiving?
var tally = row.transform.Find("Tally")?.GetComponent<TMP_Text>();
if (tally != null)
{
s.VoteTallies.TryGetValue(rowUuid, out var count);
tally.text = count > 0 ? count.ToString() : "";
}
// Vote button: gate by sub-phase + arrival + alive + not-self
var btnGO = row.transform.Find("VoteBtn")?.gameObject;
if (btnGO != null)
{
var btn = btnGO.GetComponent<Button>();
var btnImg = btnGO.GetComponent<Image>();
var rowPlayer = s.Players?.FirstOrDefault(p => p.ClientUuid == rowUuid);
bool isMe = rowUuid == _gameClient.ClientUuid;
bool rowAlive = rowPlayer?.State == PlayerState.Alive;
bool canPress = votingOpen && iAmArrived && rowAlive && !isMe;
if (btn != null) btn.interactable = canPress;
if (btnImg != null)
btnImg.color = canPress ? new Color(0.2f,0.6f,1f)
: new Color(0.2f,0.2f,0.2f,0.5f);
// Mark the row's button if it's the local player's chosen vote
if (s.MyVoteTarget != null && s.MyVoteTarget == rowUuid && btnImg != null)
btnImg.color = new Color(0.2f,0.75f,0.30f); // green = your vote
}
}
// ── My vote indicator strip ───────────────────────────────────────
if (_myVoteIndicator != null)
{
if (s.LastVoteResult != null) _myVoteIndicator.text = "";
else if (!iAmArrived) _myVoteIndicator.text = "Travel to the meeting point to vote";
else if (sub == MeetingSubPhase.Discussion) _myVoteIndicator.text = "Discussion - voting opens shortly";
else if (sub == MeetingSubPhase.Arrival) _myVoteIndicator.text = "Waiting for players to arrive";
else if (s.MyVoteTarget == null) _myVoteIndicator.text = "Cast your vote";
else if (s.MyVoteTarget == GameState.VoteSkip) _myVoteIndicator.text = "You voted: SKIP";
else
{
var target = s.Players?.FirstOrDefault(p => p.ClientUuid == s.MyVoteTarget);
_myVoteIndicator.text = $"You voted for: {target?.DisplayName ?? s.MyVoteTarget}";
}
}
}
public void AppendVoteInstruction()
{
// no-op - vote instructions are embedded in the row buttons
}
public void ShowVoteResult(VotingClosedPayload payload, List<PlayerInfo> players)
{
// Swap scroll list out, result subpanel in. They occupy the same
// anchor region (0.18-0.74) so the result text replaces the vote
// rows rather than overlapping them.
if (_meetingScrollGO != null) _meetingScrollGO.SetActive(false);
if (_voteResultPanel != null) _voteResultPanel.SetActive(true);
// Skip + my-vote strips are no longer relevant once voting ended.
if (_skipButton != null) _skipButton.gameObject.SetActive(false);
if (_myVoteIndicator != null) _myVoteIndicator.text = "";
if (_voteResultText != null)
{
// Build a compact tally summary alongside the headline.
var sb = new System.Text.StringBuilder();
if (payload.WasTie)
sb.AppendLine("⚖ TIE — nobody ejected.");
else if (string.IsNullOrEmpty(payload.EjectedPlayerId))
sb.AppendLine("Nobody ejected (skip).");
else
{
var ej = players?.Find(p => p.ClientUuid == payload.EjectedPlayerId);
sb.AppendLine($"🚪 {ej?.DisplayName ?? payload.EjectedPlayerId} ejected!");
}
if (payload.VoteCounts != null && payload.VoteCounts.Count > 0)
{
sb.AppendLine();
foreach (var kv in payload.VoteCounts.OrderByDescending(p => p.Value))
{
if (kv.Value <= 0) continue;
string name = kv.Key == GameState.VoteSkip
? "(skip)"
: (players?.Find(p => p.ClientUuid == kv.Key)?.DisplayName ?? kv.Key);
sb.AppendLine($"<size=24>{name}: {kv.Value}</size>");
}
}
_voteResultText.text = sb.ToString();
}
// Auto-close meeting panel after 5 s. Track the handle so we can
// cancel it if the game ends or returns to lobby before it fires
// (otherwise the coroutine fires mid-GameEndPanel and hides nothing
// useful while the meeting overlay sits visibly stacked on top).
CancelMeetingAutoClose();
var gm = GameManager.Instance;
if (gm != null) _meetingCloseCoroutine = gm.StartCoroutine(CloseMeetingAfterDelay(5f));
}
/// <summary>
/// Hide the meeting/vote panels immediately and cancel any pending
/// auto-close coroutine. Resets internal toggles (skip/result/scroll
/// visibility) so the next meeting starts from a clean state. Safe to
/// call from any phase transition.
/// </summary>
public void HideMeetingPanel()
{
CancelMeetingAutoClose();
if (_meetingPanel) _meetingPanel.SetActive(false);
if (_voteResultPanel) _voteResultPanel.SetActive(false);
if (_meetingScrollGO) _meetingScrollGO.SetActive(true);
if (_skipButton) _skipButton.gameObject.SetActive(true);
if (_myVoteIndicator) _myVoteIndicator.text = "";
if (_meetingPhaseLabel) _meetingPhaseLabel.text = "";
if (_meetingPhaseCountdown) _meetingPhaseCountdown.text = "";
if (_meetingPhaseProgressBar) _meetingPhaseProgressBar.fillAmount = 0f;
}
private void CancelMeetingAutoClose()
{
if (_meetingCloseCoroutine != null)
{
var gm = GameManager.Instance;
if (gm != null) gm.StopCoroutine(_meetingCloseCoroutine);
_meetingCloseCoroutine = null;
}
}
private System.Collections.IEnumerator CloseMeetingAfterDelay(float delay)
{
yield return new UnityEngine.WaitForSeconds(delay);
// Use HideMeetingPanel so we restore the scroll/skip/indicator
// state for the next meeting, not just hide the root panel.
HideMeetingPanel();
_meetingCloseCoroutine = null;
}
// ── Sabotage ──────────────────────────────────────────────────────────
public void ShowSabotageTimer(DateTime deadline)
{
_sabotageMeltdownDeadline = deadline;
_sabotageTimerActive = true;
if (_sabotagePanel) _sabotagePanel.SetActive(true);
if (_sabotageTimerText) _sabotageTimerText.gameObject.SetActive(true);
}
public void HideSabotageTimer()
{
_sabotageTimerActive = false;
if (_sabotagePanel) _sabotagePanel.SetActive(false);
SetCommsBlackout(false);
}
/// <summary>
/// Set the comms-blackout flag and (when active) raise the sabotage
/// banner with a clear "comms down" message. The flag is read by
/// GameManager_Tasks.UpdateProximity to suppress the REPORT/EMERGENCY
/// action button while comms are jammed - this gives the player the
/// visible reason why those buttons disappeared.
/// </summary>
public void SetCommsBlackout(bool active)
{
_commsBlackout = active;
if (active)
{
if (_sabotagePanel) _sabotagePanel.SetActive(true);
if (_sabotageTimerText)
{
_sabotageTimerText.gameObject.SetActive(true);
_sabotageTimerText.text = "📡 COMMS DOWN — reports & meetings disabled";
}
}
else if (!_sabotageTimerActive)
{
// Only tear the banner down if no meltdown timer is using it.
if (_sabotagePanel) _sabotagePanel.SetActive(false);
}
}
// ── Game end ──────────────────────────────────────────────────────────
public void ShowGameEndPanel(GameEndedPayload payload, string myUuid)
{
if (_gameEndPanel) _gameEndPanel.SetActive(true);
if (_gameEndText != null)
{
bool won = payload.Winners?.Contains(myUuid) ?? false;
string title = won ? "<color=#FFB800>🏆 VICTORY</color>" : "<color=#C43232>💔 DEFEAT</color>";
string faction = payload.WinningFaction == "Impostor" ? "Impostors win!" : "Crew wins!";
// Non-owners can't actually return to lobby themselves; tell
// them who they're waiting on so the panel doesn't read as
// "tap leave or stare at the wall." If we can't find an
// owner record, fall back to a generic message.
string waitMessage = "";
if (!_gameClient.IsOwner)
{
var s = _state;
var host = s?.Players?.Find(p => p.IsOwner);
string hostName = host?.DisplayName ?? "the host";
waitMessage = $"\n\n<size=32>Waiting for {hostName} to return to lobby...</size>";
}
_gameEndText.text = $"{title}\n{faction}\n<size=38>{payload.Reason}</size>{waitMessage}";
}
// Show "Return to Lobby" only for the host
if (_returnToLobbyBtn != null)
_returnToLobbyBtn.gameObject.SetActive(_gameClient.IsOwner);
}
// ── Toast ─────────────────────────────────────────────────────────────
public void ShowToast(string message)
{
if (_state != null) { _state.ToastMessage = message; _state.ToastExpiry = UnityEngine.Time.time + 4f; }
if (_toastGO == null) return;
_toastGO.SetActive(true);
if (_toastText != null) _toastText.text = message;
}
private void TickToast()
{
var s = _state;
if (_toastGO == null) return;
if (s != null && !string.IsNullOrEmpty(s.ToastMessage) && UnityEngine.Time.time < s.ToastExpiry)
{
_toastGO.SetActive(true);
if (_toastText != null) _toastText.text = s.ToastMessage;
}
else
{
_toastGO.SetActive(false);
}
}
// ── Canvas switching ──────────────────────────────────────────────────
private void SetCanvases(bool createJoin, bool inLobby, bool loading, bool game)
{
EnsureCanvasReady(ClientCreateJoinLobby);
EnsureCanvasReady(ClientInLobby);
EnsureCanvasReady(ClientLoadingScreen);
EnsureCanvasReady(ClientGameScreen);
if (ClientCreateJoinLobby) ClientCreateJoinLobby.gameObject.SetActive(createJoin);
if (ClientInLobby) ClientInLobby.gameObject.SetActive(inLobby);
if (ClientLoadingScreen) ClientLoadingScreen.gameObject.SetActive(loading);
if (ClientGameScreen) ClientGameScreen.gameObject.SetActive(game);
}
// ── Utilities ─────────────────────────────────────────────────────────
private static void EnsureCanvasReady(Canvas canvas)
{
if (canvas == null) return;
if (!canvas.enabled) canvas.enabled = true;
var t = canvas.transform;
if (t != null)
{
var s = t.localScale;
if (Mathf.Abs(s.x) < 0.001f || Mathf.Abs(s.y) < 0.001f || Mathf.Abs(s.z) < 0.001f)
t.localScale = Vector3.one;
}
}
private static TMP_Text FindTMP(Transform root, string name)
{
if (root == null) return null;
foreach (var tmp in root.GetComponentsInChildren<TMP_Text>(true))
if (tmp != null && tmp.name == name) return tmp;
return null;
}
private static Transform FindTransform(Transform root, string name)
{
if (root == null) return null;
foreach (Transform t in root.GetComponentsInChildren<Transform>(true))
if (t.name == name) return t;
return null;
}
private static GameObject FindTransformGO(Transform root, string name)
=> FindTransform(root, name)?.gameObject;
private static RectTransform MakeChild(string name, RectTransform parent)
{
var go = new GameObject(name);
var rt = go.AddComponent<RectTransform>();
rt.SetParent(parent, false);
rt.localScale = Vector3.one;
return rt;
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
_InLobby.transform.Find("JoinCode").GetComponent<TMPro.TMP_Text>().text = _gameClient.CurrentLobbyState.JoinCode;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: cbe0afd6cfb57b44781533cfa4ce4196

View File

@@ -1,21 +1,53 @@
//using GeoSus.Client;
using GeoSus.Client;
using System;
using UnityEngine;
public enum TaskType
{
Task
Task //TODO: Typy úkolù
}
public interface ITask
{
public string TaskID { get; } // Unikátní ID úkolu pro server
public TaskType TaskType { get; } // Typ úkolu
public string TaskName { get; } // Viditelný název úkolu
public (double, double) TaskLocation { get; } // Polohy na mapì
public bool IsCompleted { get; } // Stav dokonèení úkolu
void Initialize(Action<ITask> onCompleted); // Vytvoøení tasku + naètení postupu
void ExitTask(Action<ITask> onExit); // Pøi opuštìní úkolu poslat hotovo / uložit postup / reset
void Complete(); // Oznaèit úkol jako dokonèený, poslat na server a zavøít
}
/* Ukázoková implementace ITask
public class Wires : ITask{
public string TaskID { get; set; } // Unikátní ID úkolu pro server
public TaskType TaskType { get; set; } // Typ úkolu
public string TaskName { get; set; } // Viditelný název úkolu
public (double, double) TaskLocation { get; set; } // Poloha na mapě
public bool IsCompleted { get; } // Stav dokončení úkolu
public (double, double) TaskLocation { get; set; } // Poloha na mapì
public bool IsCompleted { get; private set; } // Stav dokonèení úkolu
private Action<ITask> _onCompleted;
void Initialize(Action<ITask> onCompleted); // Vytvoření tasku
void ExitTask(Action<ITask> onExit); // Při opuštění úkolu
void Complete(); // Označit úkol jako dokončený
public void Initialize(Action<ITask> onCompleted) // Vytvoøení tasku
{
IsCompleted = false;
_onCompleted = onCompleted;
}
public void ExitTask(Action<ITask> onExit) //Zavøení tasku
{
onExit?.Invoke(this);
}
public void Complete() // Dokonèení tasku a zavøení
{
IsCompleted = true;
_onCompleted?.Invoke(this);
ExitTask(null);
}
}
*/

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 00f17be43b5049645915f193bf99516b

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: dac0a6a54861f2c438fc5fd58864473d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,92 +0,0 @@
%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: TestMaterial
m_Shader: {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlphaTex:
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}
m_Ints: []
m_Floats:
- PixelSnap: 0
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _EnableExternalAlpha: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0, g: 0, b: 0, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _Flip: {r: 1, g: 1, b: 1, a: 1}
- _RendererColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 6744524496c8e1549882277283c132cc
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: e8c70d2b2080681448d8f781f73c73a0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 52b482693f234054aa4d20f92fbef10d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,84 +0,0 @@
%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: BLUE
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _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}
m_Ints: []
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0, g: 0, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 09c36d1bce0ccb84183ec9ae484ad36f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More