Compare commits

..

122 Commits

Author SHA1 Message Date
13f62000b3 Some conflicts resolved 2026-05-27 21:32:22 +02:00
e96ce2de20 Merge pull request 'Fix player name UI and persist name between menu scenes' (#15) from eda-fix-errors into main
Reviewed-on: #15
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-24 11:41:13 +02:00
d0f89893a5 Resolved conflict 2026-05-24 11:40:14 +02:00
015c9b5caa Merge pull request 'HappyWheelMinigame' (#11) from HappyWheelMinigame into main
Reviewed-on: #11
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-24 11:29:36 +02:00
31522cda01 conflict resolved 2026-05-24 11:28:32 +02:00
gravitrax-bublina
213e894640 Remove unused placeholder model asset 2026-05-22 20:52:55 +02:00
dea21e2af5 Merge pull request 'find-the-sweet-spot' (#25) from find-the-sweet-spot into main
Reviewed-on: #25
2026-05-21 20:14:38 +02:00
4f9aa12cad Merge pull request 'Eda minigame but new branch' (#22) from EdaSweeSpotMinigame into main
Reviewed-on: #22
2026-05-21 20:00:31 +02:00
gravitrax-bublina
abc5801b18 Remove unused placeholder model assets 2026-05-21 19:57:45 +02:00
gravitrax-bublina
f427dbb6e7 Move minigame audio into Assets 2026-05-21 19:57:39 +02:00
gravitrax-bublina
2d5b6fae60 Remove unused placeholder model assets 2026-05-21 19:45:22 +02:00
gravitrax-bublina
3b2a5a0677 Remove unused placeholder model assets 2026-05-21 19:39:54 +02:00
e776b9476e Revert "merge conflict"
This reverts commit 37797d5f7d.
2026-05-18 20:57:27 +02:00
37797d5f7d merge conflict
psl co tam mám změnit?
2026-05-18 18:06:56 +02:00
fed08d08ad Merge pull request 'Package patch' (#23) from Pajka into main
Reviewed-on: #23
2026-05-17 22:23:23 +02:00
843c121666 Package patch 2026-05-17 22:21:36 +02:00
a15a9790b5 Eda minigame but new branch 2026-05-17 20:44:46 +02:00
dfec8df767 Revert "Merge pull request 'find-the-sweet-spot' (#13) from find-the-sweet-spot into main"
This reverts commit 5b8e4eaeac, reversing
changes made to 03c0b158a4.
2026-05-17 20:22:01 +02:00
5b8e4eaeac Merge pull request 'find-the-sweet-spot' (#13) from find-the-sweet-spot into main
Reviewed-on: #13
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-17 20:18:15 +02:00
9571be257c resolved conflict 2026-05-17 20:17:21 +02:00
03c0b158a4 Merge pull request 'KeyInsertMinigame' (#16) from KeyInsertMinigame into main
Reviewed-on: #16
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-17 20:16:23 +02:00
b0945c9bdb resolving conflict 2026-05-17 20:15:06 +02:00
28a81b6014 Merge pull request 'fixnuty design mapy, soubory maji dobre jmeno' (#19) from DesignMapy into main
Reviewed-on: #19
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-17 20:11:07 +02:00
ab6938e6cf Merge branch 'DesignMapy' of ssh://git.honzuvkod.dev:222/BlueCabinetGames/GeoSusGame into DesignMapy 2026-05-17 20:09:21 +02:00
c5703fc92a resolved conflicts 2026-05-17 20:09:05 +02:00
82c120f362 Merge pull request 'Assets CleanUp' (#20) from MainCleanUp into main
Reviewed-on: #20
2026-05-17 14:52:28 +02:00
099f3ec939 Assets CleanUp 2026-05-17 14:50:32 +02:00
e0a4cf31b2 prejmenovani souboru na mapu a design mapy fixnuto 2026-05-17 14:20:33 +02:00
1aa3d2a787 Merge pull request 'MainMenuUiBugFix' (#18) from MainMenuUiBugFix into main
Reviewed-on: #18
2026-05-17 13:43:04 +02:00
d7211e62de Merge pull request 'Implemented ITask' (#17) from keyinsertminigame into KeyInsertMinigame
Reviewed-on: #17
2026-05-17 12:54:50 +02:00
a2a55a7135 Try of anchoring, deleted old scripts, mergable 2026-05-17 12:49:24 +02:00
a5e657ef05 Merge pull request 'opravene kabely (je jen 1 scena)' (#8) from KabelyOprava into main
Reviewed-on: #8
Reviewed-by: Trubkokrtek <agentdominik707@gmail.com>
2026-05-17 12:48:26 +02:00
2040b59593 meta patch 2026-05-17 12:47:09 +02:00
gravitrax-bublina
54bfcf745b Fix player name UI and persist name between menu scenes 2026-05-17 12:34:22 +02:00
8bec2f0cf8 conflict resolve 2026-05-17 12:32:20 +02:00
gravitrax-bublina
9a99405f4b Ignore .utmp temp folder 2026-05-17 12:15:46 +02:00
gravitrax-bublina
b4b746de25 Remove .utmp temp files from PR 2026-05-17 12:14:53 +02:00
023bddc91b conflict partially resolved 2026-05-17 11:31:00 +02:00
c8d8b6b802 Conflict resolve 2026-05-17 11:25:21 +02:00
1082fc9ad0 plne funkcni data transfer 2026-05-17 10:50:17 +02:00
2ae5d28cc9 Implemented ITask 2026-05-17 10:06:36 +02:00
gravitrax-bublina
dd5aefcb49 Update Eda minigame scene and project settings 2026-05-17 10:03:13 +02:00
2d7b5481c0 Code repair 2026-05-17 09:56:04 +02:00
37c6d7a552 male zmeny 2026-05-11 21:31:33 +02:00
e55aa6b258 A completely working minigame Insert Keys, Added SFX + Light, Still not mergable - needs dev team check 2026-05-11 21:19:01 +02:00
d7838c0a04 fix mensich ui bugu na ipadech crate a join loading furt funguji divne 2026-05-10 17:49:03 +02:00
90fd3514fb fix main menu ui designu v join loading a create podle me nejak vadi playerlistu honzuv kod a z nejakyho duvodu meni barvu nejakych tlacitek coz idk jak fixnout je take potreba zaimplementovat UiManager do designu coz idk jak udelat honzo sry pokud jsem neco omilem rozbil ale o nicem nevim 2026-05-10 01:15:07 +02:00
9a736e1d53 fix rozbiteho ui, potrbuje implementovat UiManager 2026-05-10 00:45:45 +02:00
2f86bab336 Almost comletely working minigame, but still in progress, code almost done 2026-05-08 10:20:22 +02:00
8948cbdb14 Added right numbers to keyholes, project clean up, safety commit - to not lose progress 2026-05-07 17:30:10 +02:00
7e89f37d90 z 99% dodelana dira minigame mozna jeste pridam malickosti 2026-05-02 17:15:31 +02:00
Bandwidth
207f997254 Added better GPS handling, Improved test mode, Fixed naming of players not being broadcast until a new HelloClient, Fixed lobby restarts, Fixed task handling 2026-04-27 23:18:13 +02:00
f9ceea4992 Small asset clean-up 2026-04-26 21:57:42 +02:00
Bandwidth
d886f97e14 GeoSus 2026-04-26 20:49:32 +02:00
ad604daec7 optimalizace 2026-04-26 20:03:12 +02:00
e0b808faed Zabiju je 2. Epicky thriller od tvurce Zabiju je 2026-04-26 14:58:39 +02:00
666f731b6d Created base UI of the minigame, added keyslots 2026-04-26 14:51:45 +02:00
a9ea88c125 minigame hole dokončeno 2026-04-26 14:41:06 +02:00
700e6bfbfc Zabiju je 2026-04-26 13:30:33 +02:00
ab6263cb10 skybox,design domecku a cest 2026-04-26 13:17:30 +02:00
abbe4842fe Repaired hole minigame 2026-04-26 12:55:21 +02:00
f800e78f14 kontrola simulace na telefonu 2026-04-26 11:40:58 +02:00
e29581cc21 opravene kabely (je jen 1 scena) 2026-04-26 10:30:47 +02:00
74fa735322 dominiku pomoc 2026-04-26 10:04:37 +02:00
208696487e Mergnuta minihra 2026-04-26 09:11:01 +02:00
3879c0879d Added Textures and models of key&keyhole 2026-04-25 22:50:53 +02:00
0a163b2a1e nekolik neonovych veci a animace kamery svetla z nejakyho duvodu funguji divne 2026-04-25 21:32:15 +02:00
1de91b0d57 Hra kabely 2026-03-28 19:28:59 +01:00
b0e90221dc fixed merges 2026-03-28 11:19:27 +01:00
dcb1066d80 Fixed merge conflict 2026-03-28 11:18:01 +01:00
d80ac111c2 Removed meta files 2026-03-28 11:06:53 +01:00
b872b52632 Added gps and ITask update 2026-03-28 10:59:01 +01:00
4fdfdea5cf udpatnuty na vsechny cudliky 2026-03-28 10:45:51 +01:00
32c7589ab3 prepinani scen 2026-03-28 10:36:08 +01:00
gravitrax-bublina
c11ca05ea8 nejlepsi minihra, velmi dobre funguje, nepouzil jsem chatbota nai jednoud a vsechno jsem programoval pomoci libraries a moc me to bavilo, esing neni nejaky skvely ale funguje to na iphone, takze se to mozna rozbije pokud to zkusite runnout na androidu 2026-03-28 10:18:32 +01:00
72a75c121a itask 2026-03-28 10:03:46 +01:00
677c875c58 uprava druhu velocity 2026-03-28 09:47:21 +01:00
44155796d0 rename Misovych slozek, clean up 2026-03-28 09:43:53 +01:00
a0a675676d flappy bird 2026-03-28 09:31:02 +01:00
5b166244b2 added Packages 2026-03-28 09:24:20 +01:00
11575ef9b1 scripty ve slozkach 2026-03-28 09:12:04 +01:00
76a38d741c minihra_dira
spawnuje itemy a uprostred je dira. cilem je nahazet x veci do diry. desing a smysl motivu je na vas
2026-03-28 09:07:10 +01:00
114a0d3997 player list + animace pro něj 2026-03-26 18:33:43 +01:00
ce6e4450e6 nove screeny(myslim)zvuky pro tlacitka klikani tlacitek novy font pro texty a uprava hvězd 2026-03-22 21:31:20 +01:00
be595da357 Added test mode and manual positioning 2026-03-21 22:40:20 +01:00
95f2f63259 6 novych screenu posledni 3 asi celkem nic moc 2026-03-20 20:24:16 +01:00
dc5ed7d49f napis na zemeguli, jestli nekdo chce, tak doladte barvu, vic belejsi mi to udelat neslo(ve scene neni svetlo, tak jsem radsi nic nedelal) 2026-03-04 18:26:34 +01:00
Jan Racek
2fadf819cc Upraveno hlavni menu, pridana 3D zeme bez rendertexture, pridano space HDRI 2026-03-01 21:14:00 +01:00
a1465de9a0 Merge pull request 'Added map' (#3) from GameClient into main
Reviewed-on: #3

Líbí se mi to. Je to fajn.
2026-03-01 20:52:28 +01:00
a1b40ad102 Added map 2026-03-01 13:26:13 +01:00
ff9a2cebd3 main menu 2026-02-27 20:22:49 +01:00
f7926a218e MinHraKrystof 2026-02-22 20:59:10 +01:00
9defaa314a Added 2 minigames 2026-02-21 14:56:19 +01:00
e14a3ddf2b scaling a cerny pozadí 2026-02-21 14:47:54 +01:00
5bd6eabec6 Opraven TMP, pridan anchoring hlavniho menu 2026-02-21 14:17:12 +01:00
f2ebd125f3 zeme kutululu 2026-02-21 14:04:25 +01:00
2fdfabe2b8 main menu 2026-02-21 13:44:23 +01:00
4a84e729f3 Added packages 2026-02-21 13:18:55 +01:00
055e8aa426 Added Metas 2026-02-21 13:16:44 +01:00
1957d26b1f added cables 2026-02-21 13:07:16 +01:00
8f62dc8873 scena_minigame-kabely. ps pridat ui 2026-02-21 12:32:31 +01:00
4b8e4c69f5 Fixed meta files 2026-02-21 11:58:53 +01:00
e086bedb19 Opravena textura 2026-02-21 11:53:57 +01:00
13300e885b Opravena moje objektivni a naprosto nezpochybnitelna neschopnost, ktera predstavuje hrozbu pro celou planetu na budocích 40 let (vic se nedoziju zeru moc KFC) 2026-02-21 11:52:25 +01:00
a73f75ffa4 funguj 2026-02-21 11:06:54 +01:00
e9beb05083 Merge pull request 'UI update' (#2) from GameClient into main
Reviewed-on: #2
2026-02-21 11:06:13 +01:00
7294466604 UI patch 2026-02-21 11:02:26 +01:00
67d3ee76c1 UI update 2026-02-21 10:59:27 +01:00
gravitrax-bublina
9f71b6a84a Init 2026-02-21 10:23:17 +01:00
a04ce40779 Merge pull request 'GameClient' (#1) from GameClient into main
Reviewed-on: #1
2026-02-21 10:08:32 +01:00
94a40e3d14 Added Lobbies 2026-02-19 16:14:30 +01:00
eeaf092780 double patch 2026-02-01 20:10:52 +01:00
fc22d4f544 Added ITask and basic connection 2026-01-31 15:44:30 +01:00
3b36d53b39 Merge pull request 'Mapz' (#5) from Mapz into main
Reviewed-on: BlueCabinetGames/Game#5
Reviewed-by: jracek <racek.janek@gmail.com>
2025-11-30 09:54:51 +01:00
b1fc3ac24a Coroutines patch 2025-11-30 08:46:19 +01:00
655d378dbb Added GPS 2025-11-23 19:42:34 +01:00
39b42e1e90 Code cleanup 2025-11-15 20:05:19 +01:00
0cb7d4b64d Functioning renderer 2025-11-15 17:38:49 +01:00
1c103a1496 Map renderer 2025-11-15 14:58:40 +01:00
1618ecd432 Map branch 2025-11-15 09:40:28 +01:00
ed6347e6bc Přidán build profile pro Android 2025-10-18 17:54:56 +02:00
59b6708437 Uklizeny assety 2025-10-18 17:41:24 +02:00
1367 changed files with 386613 additions and 1031 deletions

4
.gitignore vendored
View File

@@ -186,7 +186,6 @@ StyleCopReport.xml
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
@@ -305,7 +304,6 @@ 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
@@ -501,3 +499,5 @@ FodyWeavers.xsd
# End of https://www.toptal.com/developers/gitignore/api/unity,visualstudiocode,visualstudio,vim
.utmp/

60
.vscode/settings.json vendored
View File

@@ -1,60 +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
},
"dotnet.defaultSolution": "NegrBagrJaromirJagr_git.sln"
}

6
.vsconfig Normal file
View File

@@ -0,0 +1,6 @@
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Workload.ManagedGame"
]
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
<<<<<<<< HEAD:Assets/Scenes/Main Screen Scenes/bubak.unity.meta
guid: 2ac55b32f60c0f44691e1588d19d610f
========
guid: d6a21cbd9c8f68f4fbe65763566ca7c9
>>>>>>>> origin/main:Assets/2026-04-26 13-58-02.mp3.mkv.meta
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 6ab90ac2243447c478fbb930f101d94a
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4c92126c07bfe7e4f8eea9bf78d8f29f
VideoClipImporter:
externalObjects: {}
serializedVersion: 3
frameRange: 0
startFrame: -1
endFrame: -1
colorSpace: 0
deinterlace: 0
encodeAlpha: 0
flipVertical: 0
flipHorizontal: 0
importAudio: 1
targetSettings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5091877278b8b6a47a9afa6f50d6b2a6
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

8
Assets/ClientSDK.meta Normal file
View File

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

View File

@@ -0,0 +1,285 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace GeoSus.Client
{
// Klientská strana šifrování - generuje session key, šifruje RSA, AES-CBC session
// Používá AES-CBC místo AES-GCM pro kompatibilitu s Unity
public class ClientEncryption : IDisposable
{
private byte[] _sessionKey;
private byte[] _sessionIv;
private long _nonceCounter;
private readonly object _lock = new object();
// Kontrola, zda je session key nastaven
public bool HasSessionKey => _sessionKey != null && _sessionIv != null;
// Generuje nový session key a IV
public void GenerateSessionKey()
{
_sessionKey = new byte[32]; // AES-256
_sessionIv = new byte[16]; // CBC IV (16 bytes)
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(_sessionKey);
rng.GetBytes(_sessionIv);
}
}
public byte[] SessionKey => _sessionKey ?? throw new InvalidOperationException("Session key not generated");
public byte[] SessionIV => _sessionIv ?? throw new InvalidOperationException("Session IV not generated");
// Zašifruje session key pomocí RSA public key serveru
public (string EncryptedKey, string EncryptedIV) EncryptSessionKeyForServer(string rsaPublicKeyPem)
{
if (_sessionKey == null || _sessionIv == null)
throw new InvalidOperationException("Session key not generated");
using (var rsa = RSA.Create())
{
// Parse PEM - extrahuj Base64 obsah
var pemLines = rsaPublicKeyPem.Split('\n');
var base64 = new StringBuilder();
foreach (var line in pemLines)
{
var trimmed = line.Trim();
if (!trimmed.StartsWith("-----") && !string.IsNullOrEmpty(trimmed))
{
base64.Append(trimmed);
}
}
var keyBytes = Convert.FromBase64String(base64.ToString());
// Unity kompatibilní import - parsujeme SubjectPublicKeyInfo ručně
ImportSubjectPublicKeyInfoManual(rsa, keyBytes);
// Používáme OaepSHA1 pro Unity kompatibilitu (OaepSHA256 není podporován)
var encryptedKey = rsa.Encrypt(_sessionKey, RSAEncryptionPadding.OaepSHA1);
var encryptedIv = rsa.Encrypt(_sessionIv, RSAEncryptionPadding.OaepSHA1);
return (Convert.ToBase64String(encryptedKey), Convert.ToBase64String(encryptedIv));
}
}
// Ručně parsuje SubjectPublicKeyInfo (DER) a importuje RSA klíč - Unity kompatibilní
private static void ImportSubjectPublicKeyInfoManual(RSA rsa, byte[] subjectPublicKeyInfo)
{
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier,
// subjectPublicKey BIT STRING }
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
int index = 0;
// Outer SEQUENCE
if (subjectPublicKeyInfo[index++] != 0x30)
throw new InvalidOperationException("Invalid SubjectPublicKeyInfo");
ReadLength(subjectPublicKeyInfo, ref index);
// AlgorithmIdentifier SEQUENCE - skip it
if (subjectPublicKeyInfo[index++] != 0x30)
throw new InvalidOperationException("Invalid AlgorithmIdentifier");
int algLen = ReadLength(subjectPublicKeyInfo, ref index);
index += algLen;
// BIT STRING containing RSAPublicKey
if (subjectPublicKeyInfo[index++] != 0x03)
throw new InvalidOperationException("Invalid BIT STRING");
ReadLength(subjectPublicKeyInfo, ref index);
index++; // Skip unused bits byte (should be 0)
// RSAPublicKey SEQUENCE
if (subjectPublicKeyInfo[index++] != 0x30)
throw new InvalidOperationException("Invalid RSAPublicKey");
ReadLength(subjectPublicKeyInfo, ref index);
// Modulus INTEGER
byte[] modulus = ReadInteger(subjectPublicKeyInfo, ref index);
// Exponent INTEGER
byte[] exponent = ReadInteger(subjectPublicKeyInfo, ref index);
var parameters = new RSAParameters
{
Modulus = modulus,
Exponent = exponent
};
rsa.ImportParameters(parameters);
}
private static int ReadLength(byte[] data, ref int index)
{
int length = data[index++];
if ((length & 0x80) != 0)
{
int numBytes = length & 0x7F;
length = 0;
for (int i = 0; i < numBytes; i++)
{
length = (length << 8) | data[index++];
}
}
return length;
}
private static byte[] ReadInteger(byte[] data, ref int index)
{
if (data[index++] != 0x02)
throw new InvalidOperationException("Expected INTEGER");
int length = ReadLength(data, ref index);
// Skip leading zero if present (used for positive sign in DER)
int originalLength = length;
int start = index;
if (length > 1 && data[start] == 0x00)
{
start++;
length--;
}
byte[] result = new byte[length];
Buffer.BlockCopy(data, start, result, 0, length);
index += originalLength;
return result;
}
// Šifruje zprávu pomocí AES-256-CBC s HMAC
public byte[] Encrypt(byte[] plaintext)
{
if (_sessionKey == null || _sessionIv == null)
throw new InvalidOperationException("Session key not set");
lock (_lock)
{
// Generuj unikátní IV pro tuto zprávu
var iv = GetNextIV();
using (var aes = Aes.Create())
{
aes.Key = _sessionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
byte[] ciphertext;
using (var encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(plaintext, 0, plaintext.Length);
}
ciphertext = ms.ToArray();
}
// Compute HMAC pro integritu
byte[] hmac;
using (var hmacSha = new HMACSHA256(_sessionKey))
{
var toSign = new byte[iv.Length + ciphertext.Length];
Buffer.BlockCopy(iv, 0, toSign, 0, iv.Length);
Buffer.BlockCopy(ciphertext, 0, toSign, iv.Length, ciphertext.Length);
hmac = hmacSha.ComputeHash(toSign);
}
// Výstup: [16 bytes IV][32 bytes HMAC][ciphertext]
var result = new byte[16 + 32 + ciphertext.Length];
Buffer.BlockCopy(iv, 0, result, 0, 16);
Buffer.BlockCopy(hmac, 0, result, 16, 32);
Buffer.BlockCopy(ciphertext, 0, result, 48, ciphertext.Length);
return result;
}
}
}
// Dešifruje zprávu pomocí AES-256-CBC s HMAC ověřením
public byte[] Decrypt(byte[] encrypted)
{
if (_sessionKey == null)
throw new InvalidOperationException("Session key not set");
if (encrypted.Length < 48) return null;
try
{
var iv = new byte[16];
var hmac = new byte[32];
var ciphertext = new byte[encrypted.Length - 48];
Buffer.BlockCopy(encrypted, 0, iv, 0, 16);
Buffer.BlockCopy(encrypted, 16, hmac, 0, 32);
Buffer.BlockCopy(encrypted, 48, ciphertext, 0, ciphertext.Length);
// Ověř HMAC
byte[] expectedHmac;
using (var hmacSha = new HMACSHA256(_sessionKey))
{
var toVerify = new byte[iv.Length + ciphertext.Length];
Buffer.BlockCopy(iv, 0, toVerify, 0, iv.Length);
Buffer.BlockCopy(ciphertext, 0, toVerify, iv.Length, ciphertext.Length);
expectedHmac = hmacSha.ComputeHash(toVerify);
}
// Constant-time compare
var diff = 0;
for (int i = 0; i < 32; i++)
{
diff |= hmac[i] ^ expectedHmac[i];
}
if (diff != 0) return null; // HMAC mismatch
using (var aes = Aes.Create())
{
aes.Key = _sessionKey;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor())
using (var ms = new MemoryStream(ciphertext))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var output = new MemoryStream())
{
cs.CopyTo(output);
return output.ToArray();
}
}
}
catch (CryptographicException)
{
return null;
}
}
private byte[] GetNextIV()
{
if (_sessionIv == null)
throw new InvalidOperationException("Session IV not set");
var iv = new byte[16];
Buffer.BlockCopy(_sessionIv, 0, iv, 0, 8);
var counter = System.Threading.Interlocked.Increment(ref _nonceCounter);
var counterBytes = BitConverter.GetBytes(counter);
Buffer.BlockCopy(counterBytes, 0, iv, 8, 8);
return iv;
}
public void Dispose()
{
if (_sessionKey != null)
{
Array.Clear(_sessionKey, 0, _sessionKey.Length);
_sessionKey = null;
}
}
}
}

View File

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

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace GeoSus.Client
{
// Event dispatcher pro Unity main thread
// Unity může přidat SynchronizationContext, nebo polling z Update()
public class EventDispatcher
{
private readonly Queue<Action> _pendingActions = new Queue<Action>();
private readonly object _lock = new object();
private SynchronizationContext? _syncContext;
public EventDispatcher()
{
// Pokusíme se zachytit aktuální synchronization context (Unity main thread)
_syncContext = SynchronizationContext.Current;
}
// Volat z networking vlákna - naplánuje callback na main thread
public void Post(Action action)
{
if (_syncContext != null)
{
_syncContext.Post(_ => action(), null);
}
else
{
// Fallback - přidáme do fronty pro polling
lock (_lock)
{
_pendingActions.Enqueue(action);
}
}
}
// Volat z Unity Update() pokud není SynchronizationContext
public void ProcessPendingActions()
{
Action[] actions;
lock (_lock)
{
if (_pendingActions.Count == 0) return;
actions = _pendingActions.ToArray();
_pendingActions.Clear();
}
foreach (var action in actions)
{
try
{
action();
}
catch (Exception ex)
{
Console.WriteLine($"EventDispatcher error: {ex}");
}
}
}
public int PendingCount
{
get
{
lock (_lock)
{
return _pendingActions.Count;
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,682 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace GeoSus.Client
{
// Hlavní klientská třída pro připojení k serveru
public class GameClient : IDisposable
{
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private ClientEncryption? _encryption;
private CancellationTokenSource? _cts;
private Task? _receiveTask;
private int _clientSeq;
private readonly object _sendLock = new object();
private bool _handshakeComplete;
public string ClientUuid { get; }
public string DisplayName { get; set; }
public bool IsConnected => _tcpClient?.Connected ?? false;
public bool IsReady => IsConnected && _handshakeComplete && (_encryption?.HasSessionKey ?? false);
public EventDispatcher Dispatcher { get; }
// Events - voláno na main thread přes dispatcher
public event Action? OnConnected;
public event Action<string>? OnDisconnected;
public event Action<string>? OnError;
public event Action<Message>? OnMessage;
public event Action<GameEvent>? OnGameEvent;
// Lobby state
public string? LobbyId { get; private set; }
public string? JoinCode { get; private set; }
public LobbyState? CurrentLobbyState { get; private set; }
public PlayerRole? MyRole { get; private set; }
public List<GameTask> MyTasks { get; } = new List<GameTask>();
public Position MyPosition { get; set; }
public Dictionary<string, PlayerPositionInfo> PlayerPositions { get; } = new Dictionary<string, PlayerPositionInfo>();
public List<Body> Bodies { get; } = new List<Body>();
public int Ping { get; private set; }
public long LastEventId { get; private set; }
/// <summary>Returns true if this client is the current lobby owner</summary>
public bool IsOwner => CurrentLobbyState?.OwnerId == ClientUuid;
public GameClient(string clientUuid, string displayName)
{
ClientUuid = clientUuid;
DisplayName = displayName;
Dispatcher = new EventDispatcher();
}
#region Connection
public async Task<bool> ConnectAsync(string host, int port)
{
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(host, port);
_stream = _tcpClient.GetStream();
_encryption = new ClientEncryption();
_cts = new CancellationTokenSource();
// Handshake
if (!await PerformHandshakeAsync())
{
Disconnect("Handshake failed");
return false;
}
// Spustíme příjem zpráv
_receiveTask = Task.Run(() => ReceiveLoopAsync(_cts.Token));
Dispatcher.Post(() => OnConnected?.Invoke());
return true;
}
catch (Exception ex)
{
Dispatcher.Post(() => OnError?.Invoke(ex.Message));
return false;
}
}
private async Task<bool> PerformHandshakeAsync()
{
if (_stream == null || _encryption == null) return false;
// 1. ClientHello
var hello = new ClientHello
{
ClientUuid = ClientUuid,
DisplayName = DisplayName
};
await SendPlainAsync(hello);
// 2. ServerHello
var serverHelloData = await ReadMessageAsync();
if (serverHelloData == null) return false;
var serverHello = MessageSerializer.Deserialize(serverHelloData) as ServerHello;
if (serverHello == null) return false;
// 3. Generujeme session key a šifrujeme RSA
_encryption.GenerateSessionKey();
var (encKey, encIv) = _encryption.EncryptSessionKeyForServer(serverHello.RsaPublicKeyPem);
var keyExchange = new KeyExchange
{
EncryptedSessionKey = encKey,
EncryptedIV = encIv
};
await SendPlainAsync(keyExchange);
// 4. KeyExchangeAck (šifrovaně)
var ackData = await ReadMessageAsync();
if (ackData == null) return false;
var decrypted = _encryption.Decrypt(ackData);
if (decrypted == null) return false;
var ack = MessageSerializer.Deserialize(decrypted) as KeyExchangeAck;
if (ack?.Status == "success")
{
_handshakeComplete = true;
return true;
}
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)
{
_cts?.Cancel();
_tcpClient?.Close();
_tcpClient = null;
_stream = null;
_encryption?.Dispose();
_encryption = null;
if (!transient)
{
LobbyId = null;
JoinCode = null;
CurrentLobbyState = null;
MyRole = null;
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));
}
#endregion
#region Sending
public void Send(Message message)
{
if (_stream == null || _encryption == null || !IsConnected) return;
message.ClientSeq = Interlocked.Increment(ref _clientSeq);
if (string.IsNullOrEmpty(message.ActionId))
{
message.ActionId = Guid.NewGuid().ToString("N").Substring(0, 8);
}
var plain = MessageSerializer.Serialize(message);
var encrypted = _encryption.Encrypt(plain);
lock (_sendLock)
{
try
{
SendData(encrypted);
}
catch (Exception ex)
{
Dispatcher.Post(() => OnError?.Invoke($"Send error: {ex.Message}"));
}
}
}
private async Task SendPlainAsync(Message message)
{
if (_stream == null) return;
var data = MessageSerializer.Serialize(message);
await SendDataAsync(data);
}
private void SendData(byte[] data)
{
if (_stream == null) return;
var lengthBuffer = BitConverter.GetBytes(data.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(lengthBuffer);
_stream.Write(lengthBuffer, 0, 4);
_stream.Write(data, 0, data.Length);
_stream.Flush();
}
private async Task SendDataAsync(byte[] data)
{
if (_stream == null) return;
var lengthBuffer = BitConverter.GetBytes(data.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(lengthBuffer);
await _stream.WriteAsync(lengthBuffer, 0, 4);
await _stream.WriteAsync(data, 0, data.Length);
await _stream.FlushAsync();
}
#endregion
#region Receiving
private async Task ReceiveLoopAsync(CancellationToken ct)
{
int decryptFailures = 0;
try
{
while (!ct.IsCancellationRequested && IsConnected)
{
var data = await ReadMessageAsync();
if (data == null) break;
var decrypted = _encryption?.Decrypt(data);
if (decrypted == null)
{
decryptFailures++;
if (decryptFailures >= 3)
{
// Transient: keep LobbyId for the reconnect coroutine.
Disconnect("Too many decryption failures", transient: true);
return;
}
continue;
}
decryptFailures = 0;
var message = MessageSerializer.Deserialize(decrypted);
if (message != null)
{
ProcessMessage(message);
}
}
}
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);
}
}
private async Task<byte[]?> ReadMessageAsync()
{
if (_stream == null) return null;
var lengthBuffer = new byte[4];
var read = await _stream.ReadAsync(lengthBuffer, 0, 4);
if (read < 4) return null;
if (BitConverter.IsLittleEndian)
Array.Reverse(lengthBuffer);
var length = BitConverter.ToInt32(lengthBuffer, 0);
if (length <= 0 || length > 1048576) return null;
var buffer = new byte[length];
var totalRead = 0;
while (totalRead < length)
{
read = await _stream.ReadAsync(buffer, totalRead, length - totalRead);
if (read == 0) return null;
totalRead += read;
}
return buffer;
}
private void ProcessMessage(Message message)
{
// Zpracujeme speciální typy
switch (message)
{
case CreateLobbyResponse r:
if (r.Success)
{
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;
case JoinLobbyResponse r:
if (r.Success)
{
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;
case PositionBroadcast b:
ProcessPositionBroadcast(b);
break;
case Pong p:
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Ping = (int)(now - p.ClientTime);
break;
case GameEvent evt:
ProcessGameEvent(evt);
Dispatcher.Post(() => OnGameEvent?.Invoke(evt));
break;
}
Dispatcher.Post(() => OnMessage?.Invoke(message));
}
private void ProcessPositionBroadcast(PositionBroadcast broadcast)
{
PlayerPositions.Clear();
foreach (var player in broadcast.Players)
{
PlayerPositions[player.ClientUuid] = player;
}
}
private void ProcessGameEvent(GameEvent evt)
{
LastEventId = evt.EventId;
switch (evt.EventType)
{
case "PlayerJoined":
// Add player to lobby state
var joinedPayload = evt.GetPayload<PlayerJoinedPayload>();
if (joinedPayload != null && CurrentLobbyState?.Players != null)
{
bool exists = CurrentLobbyState.Players.Any(p => p.ClientUuid == joinedPayload.ClientUuid);
if (!exists)
{
CurrentLobbyState.Players.Add(new PlayerInfo
{
ClientUuid = joinedPayload.ClientUuid,
DisplayName = joinedPayload.DisplayName,
IsOwner = joinedPayload.ClientUuid == CurrentLobbyState.OwnerId,
IsReady = false,
State = PlayerState.Alive
});
}
}
break;
case "PlayerLeft":
// Remove player from lobby state
var leftPayload = evt.GetPayload<PlayerLeftPayload>();
if (leftPayload != null && CurrentLobbyState?.Players != null)
{
CurrentLobbyState.Players.RemoveAll(p => p.ClientUuid == leftPayload.ClientUuid);
}
break;
case "HostChanged":
// Update lobby owner
var hostPayload = evt.GetPayload<HostChangedPayload>();
if (hostPayload != null && CurrentLobbyState != null)
{
CurrentLobbyState.OwnerId = hostPayload.NewHostId;
// Update IsOwner flag on all players
foreach (var player in CurrentLobbyState.Players)
{
player.IsOwner = player.ClientUuid == hostPayload.NewHostId;
}
}
break;
case "GameStarting":
// Game is entering loading phase - update lobby state if available
if (CurrentLobbyState != null)
{
CurrentLobbyState.Phase = GamePhase.Loading;
}
break;
case "MapDataReady":
// Map data received - store it and send confirmation
var mapDataPayload = evt.GetPayload<MapDataReadyPayload>();
if (mapDataPayload != null && CurrentLobbyState != null)
{
CurrentLobbyState.MapData = mapDataPayload.MapData;
CurrentLobbyState.MapDataReady = true;
}
// Send confirmation to server
Send(new MapDataReceived());
break;
case "GameStarted":
// Game officially started - update phase
if (CurrentLobbyState != null)
{
CurrentLobbyState.Phase = GamePhase.Playing;
}
break;
case "RoleAssigned":
var rolePayload = evt.GetPayload<RoleAssignedPayload>();
if (rolePayload != null && rolePayload.ClientUuid == ClientUuid)
{
MyRole = rolePayload.Role;
MyTasks.Clear();
if (rolePayload.Tasks != null)
{
MyTasks.AddRange(rolePayload.Tasks);
}
}
break;
case "PlayerKilled":
var killPayload = evt.GetPayload<PlayerKilledPayload>();
if (killPayload != null)
{
Bodies.Add(new Body
{
BodyId = killPayload.BodyId,
VictimId = killPayload.VictimId,
Location = killPayload.Location
});
}
break;
case "MeetingStarted":
if (CurrentLobbyState != null)
{
CurrentLobbyState.Phase = GamePhase.Meeting;
}
break;
case "VotingClosed":
Bodies.Clear(); // Bodies zmizí po meetingu
if (CurrentLobbyState != null)
{
CurrentLobbyState.Phase = GamePhase.Playing;
}
break;
case "GameEnded":
if (CurrentLobbyState != null)
{
CurrentLobbyState.Phase = GamePhase.Ended;
}
break;
}
}
#endregion
#region Game Actions
public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500, GameSettingsOverrides? settings = null)
{
// 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
});
}
public void JoinLobby(string joinCode, string? password = null)
{
Send(new JoinLobby
{
JoinCode = joinCode.ToUpperInvariant(),
Password = password,
DisplayName = DisplayName
});
}
public void LeaveLobby()
{
Send(new LeaveLobby());
LobbyId = null;
JoinCode = null;
CurrentLobbyState = null;
}
public void StartGame()
{
Send(new StartGame());
}
public void ReturnToLobby()
{
Send(new ReturnToLobby());
}
public void UpdatePosition(Position position)
{
MyPosition = position;
Send(new UpdatePosition { Position = position });
}
public void Kill(string targetUuid)
{
Send(new KillAttempt { TargetClientUuid = targetUuid });
}
public void ReportBody(string bodyId)
{
Send(new ReportBody { BodyId = bodyId });
}
public void CallEmergencyMeeting()
{
Send(new CallEmergencyMeeting());
}
public void Vote(string? targetUuid)
{
Send(new CastVote { TargetClientUuid = targetUuid });
}
/// <summary>
/// Pokus o dokončení tasku. Server ověří že hráč je na správné pozici.
/// </summary>
public void CompleteTask(string taskId)
{
Send(new TaskComplete { TaskId = taskId });
}
public void SendPing()
{
Send(new Ping { ClientTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
public void Reconnect(string lobbyId)
{
Send(new Reconnect { LobbyId = lobbyId, LastEventId = LastEventId });
}
#endregion
#region Helpers
public Body? FindNearbyBody(double maxDistance)
{
foreach (var body in Bodies)
{
if (MyPosition.DistanceTo(body.Location) <= maxDistance)
{
return body;
}
}
return null;
}
public string? FindNearbyPlayer(double maxDistance, bool aliveOnly = true)
{
foreach (var (uuid, info) in PlayerPositions)
{
if (uuid == ClientUuid) continue;
if (aliveOnly && info.State != PlayerState.Alive) continue;
if (MyPosition.DistanceTo(info.Position) <= maxDistance)
{
return uuid;
}
}
return null;
}
public GameTask? FindNearbyTask(double maxDistance)
{
foreach (var task in MyTasks)
{
if (MyPosition.DistanceTo(task.Location) <= maxDistance)
{
return task;
}
}
return null;
}
// Volat z Unity Update() pro zpracování callbacků
public void Update()
{
Dispatcher.ProcessPendingActions();
}
#endregion
public void Dispose()
{
Disconnect("Disposed");
_encryption?.Dispose();
}
}
}

View File

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

1250
Assets/ClientSDK/Protocol.cs Normal file

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 58ebc3ffa125a9949953f6704e0a8c39
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
%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

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

View File

@@ -0,0 +1,72 @@
%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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 435 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 371 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 340 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 308 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 276 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,117 @@
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.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,117 @@
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:

8
Assets/Editor.meta Normal file
View File

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

View File

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

View File

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

8
Assets/GameManager.meta Normal file
View File

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

View File

@@ -0,0 +1,92 @@
%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

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

View File

@@ -0,0 +1,755 @@
using UnityEngine;
using GeoSus.Client;
using Subsystems;
using System.Collections;
using System;
using TMPro;
using UnityEngine.SceneManagement;
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;
public 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"
// Add minigame scene name here
};
[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
}
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)
{
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);
}
}
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)
{
for (int i = 0; i < _testBots.Count; i++)
_testBots[i].Client?.Update();
HandleTestBotInput();
}
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();
}
protected string GenerateUsername()
{
return "Player" + UnityEngine.Random.Range(1000, 9999).ToString();
}
/// <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;
}
}
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)
{
CommitNicknameFromInput();
if (!string.IsNullOrEmpty(code))
networkSubsystem.JoinLobby(code);
else
Debug.LogWarning("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}.");
}
}

View File

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

View File

@@ -0,0 +1,926 @@
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

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

View File

@@ -0,0 +1,808 @@
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

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

View File

@@ -0,0 +1,593 @@
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
{
public class GameManager_Network
{
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)
{
_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);
await state;
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)
{
Debug.LogError("Failed to connect after 10 attempts. Giving up.");
break;
}
Debug.Log($"Failed to connect (attempt {retries}). Retrying in {delayMs / 1000}s...");
await Task.Delay(delayMs);
delayMs = Mathf.Min(delayMs * 2, 30000);
}
}
public void RegisterEventHandlers()
{
_gameClient.OnConnected += OnConnected;
_gameClient.OnDisconnected += OnDisconnected;
_gameClient.OnError += OnError;
_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));
}
private IEnumerator ReconnectAfterDelay(float seconds)
{
yield return new UnityEngine.WaitForSeconds(seconds);
Debug.Log("Attempting to reconnect...");
OpenConnection();
}
private void OnMessage(Message message)
{
switch (message.Type)
{
case "CreateLobbyResponse":
HandleCreateLobbyResponse(message as CreateLobbyResponse);
break;
case "JoinLobbyResponse":
HandleJoinLobbyResponse(message as JoinLobbyResponse);
break;
case "PositionBroadcast":
HandlePositionBroadcast(message as PositionBroadcast);
break;
case "Error":
HandleErrorMessage(message as ErrorMessage);
break;
case "Ack":
case "GameEvent":
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)
{
case "PlayerJoined":
case "PlayerLeft":
case "HostChanged":
_manager?.uiSubsystem?.NotifyLobbyChanged();
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);
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();
}
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();
}
else
{
Debug.LogError("Failed to join lobby: " + message.Error);
}
}
// ── Game flow ─────────────────────────────────────────────────────────
private void HandleGameStarting()
{
_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()
{
_pendingMapBuild = true;
TryBuildMapAndMarkers();
}
public void OnClientSceneReady()
{
TryBuildMapAndMarkers();
}
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); }
}
public void LeaveLobby()
{
_gameClient.LeaveLobby();
State.Phase = GamePhase.Lobby;
SceneManager.LoadScene(_manager?.firstMenuScene ?? "main menu asi idk lol", LoadSceneMode.Single);
}
public void StartGame()
{
_gameClient.StartGame();
}
}
}

View File

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

View File

@@ -0,0 +1,328 @@
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

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

View File

@@ -0,0 +1,934 @@
using UnityEngine;
using UnityEngine.UI;
using Subsystems;
using GeoSus.Client;
using System.Collections.Generic;
using System;
using System.Linq;
using TMPro;
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)
{
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>();
}
_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)
{
_lobbyDirty = false;
LobbyDisplayUI.RefreshAll(lobbyState);
}
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;
}
}
}

View File

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

View File

@@ -1,7 +1,8 @@
//using GeoSus.Client;
using GeoSus.Client;
using System;
using UnityEngine;
public enum TaskType
{
Task

View File

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

View File

@@ -0,0 +1,101 @@
%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: New Material
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
- _ColorMask: 15
- _CullMode: 0
- _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
- _Stencil: 0
- _StencilComp: 8
- _StencilOp: 0
- _StencilReadMask: 255
- _StencilWriteMask: 255
- _UVSec: 0
- _UseUIAlphaClip: 0
- _ZWrite: 1
m_Colors:
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
- _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

View File

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

View File

@@ -0,0 +1,92 @@
%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

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: b6397bdf2a1c0de459fbfb30ea5c831b
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Hra_Kabely.meta Normal file
View File

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

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