Commit b18116ac authored by Mahmoud Aglan's avatar Mahmoud Aglan

Pre-reset backup: all uncommitted IStalk Unity work + .claude config

parent d61ad480
...@@ -38,12 +38,12 @@ RenderSettings: ...@@ -38,12 +38,12 @@ RenderSettings:
m_ReflectionIntensity: 1 m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0} m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0} m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0.18028378, g: 0.22571412, b: 0.30692285, a: 1}
m_UseRadianceAmbientProbe: 0 m_UseRadianceAmbientProbe: 0
--- !u!157 &3 --- !u!157 &3
LightmapSettings: LightmapSettings:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
serializedVersion: 12 serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings: m_GISettings:
serializedVersion: 2 serializedVersion: 2
m_BounceScale: 1 m_BounceScale: 1
...@@ -206,7 +206,7 @@ Transform: ...@@ -206,7 +206,7 @@ Transform:
m_GameObject: {fileID: 330585543} m_GameObject: {fileID: 330585543}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalPosition: {x: 0, y: 1, z: -9.27}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
...@@ -248,14 +248,14 @@ MonoBehaviour: ...@@ -248,14 +248,14 @@ MonoBehaviour:
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0 m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0 m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings: m_TaaSettings:
quality: 3 m_Quality: 3
frameInfluence: 0.1 m_FrameInfluence: 0.1
jitterScale: 1 m_JitterScale: 1
mipBias: 0 m_MipBias: 0
varianceClampScale: 0.9 m_VarianceClampScale: 0.9
contrastAdaptiveSharpening: 0 m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!1 &410087039 --- !u!1 &410087039
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
...@@ -282,14 +282,14 @@ Light: ...@@ -282,14 +282,14 @@ Light:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 410087039} m_GameObject: {fileID: 410087039}
m_Enabled: 1 m_Enabled: 1
serializedVersion: 11 serializedVersion: 12
m_Type: 1 m_Type: 1
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Intensity: 2 m_Intensity: 2
m_Range: 10 m_Range: 10
m_SpotAngle: 30 m_SpotAngle: 30
m_InnerSpotAngle: 21.80208 m_InnerSpotAngle: 21.80208
m_CookieSize: 10 m_CookieSize2D: {x: 10, y: 10}
m_Shadows: m_Shadows:
m_Type: 2 m_Type: 2
m_Resolution: -1 m_Resolution: -1
...@@ -336,6 +336,9 @@ Light: ...@@ -336,6 +336,9 @@ Light:
m_ForceVisible: 0 m_ForceVisible: 0
m_ShadowRadius: 0 m_ShadowRadius: 0
m_ShadowAngle: 0 m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &410087041 --- !u!4 &410087041
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
...@@ -363,17 +366,23 @@ MonoBehaviour: ...@@ -363,17 +366,23 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Version: 3
m_UsePipelineSettings: 1 m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2 m_AdditionalLightsShadowResolutionTier: 2
m_LightLayerMask: 1
m_RenderingLayers: 1
m_CustomShadowLayers: 0 m_CustomShadowLayers: 0
m_ShadowLayerMask: 1
m_ShadowRenderingLayers: 1
m_LightCookieSize: {x: 1, y: 1} m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0} m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 1 m_SoftShadowQuality: 1
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &832575517 --- !u!1 &832575517
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
......
using System.Collections.Generic;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
using IStalk.Data;
using IStalk.Runner;
namespace IStalk.Core
{
public class AntiAfkRitual : NetworkBehaviour
{
private readonly SyncVar<float> _ritualTimer = new();
private readonly SyncVar<int> _targetObjectId = new();
private readonly SyncVar<int> _voterCount = new();
private readonly SyncVar<bool> _isActive = new();
private readonly HashSet<NetworkObject> _voters = new();
private NetworkObject _target;
private ExtractionManager _extractionManager;
private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public bool IsActive => _isActive.Value;
public float Timer => _ritualTimer.Value;
public int TargetId => _targetObjectId.Value;
public int VoterCount => _voterCount.Value;
public event System.Action<NetworkObject> OnRitualStarted;
public event System.Action OnRitualCancelled;
public event System.Action<NetworkObject> OnRitualComplete;
public override void OnStartServer()
{
base.OnStartServer();
_extractionManager = FindAnyObjectByType<ExtractionManager>();
}
[ServerRpc(RequireOwnership = false)]
public void ServerVoteKick(NetworkObject voter, NetworkObject target)
{
if (voter == null || target == null) return;
if (voter == target) return;
var voterHealth = voter.GetComponent<RunnerHealth>();
if (voterHealth != null && voterHealth.IsDown) return;
var targetHealth = target.GetComponent<RunnerHealth>();
if (targetHealth != null && targetHealth.IsDown) return;
if (_isActive.Value && _target != target)
return;
if (!_isActive.Value)
{
_target = target;
_targetObjectId.Value = target.ObjectId;
_ritualTimer.Value = Rules.antiAfkRitualDuration;
_isActive.Value = true;
_voters.Clear();
}
_voters.Add(voter);
_voterCount.Value = _voters.Count;
if (_voters.Count < Rules.antiAfkMinVoters)
{
_isActive.Value = false;
_voters.Clear();
_voterCount.Value = 0;
return;
}
RitualStartedObservers(target);
}
[ServerRpc(RequireOwnership = false)]
public void ServerCancelVote(NetworkObject voter)
{
if (!_isActive.Value) return;
_voters.Remove(voter);
_voterCount.Value = _voters.Count;
if (_voters.Count < Rules.antiAfkMinVoters)
{
_isActive.Value = false;
_target = null;
_targetObjectId.Value = 0;
_voters.Clear();
_voterCount.Value = 0;
RitualCancelledObservers();
}
}
private void Update()
{
if (!IsServerInitialized || !_isActive.Value) return;
if (_voters.Count < Rules.antiAfkMinVoters)
{
CancelRitual();
return;
}
_ritualTimer.Value -= Time.deltaTime;
if (_ritualTimer.Value <= 0)
CompleteRitual();
}
[Server]
private void CompleteRitual()
{
if (_target == null) return;
var health = _target.GetComponent<RunnerHealth>();
if (health != null)
health.TakeHit();
if (health != null && !health.IsDown)
{
health.TakeHit();
if (!health.IsDown) health.TakeHit();
}
if (_extractionManager != null)
_extractionManager.MarkRunnerDead(_target);
RitualCompleteObservers(_target);
ResetState();
}
[Server]
private void CancelRitual()
{
ResetState();
RitualCancelledObservers();
}
private void ResetState()
{
_isActive.Value = false;
_target = null;
_targetObjectId.Value = 0;
_voters.Clear();
_voterCount.Value = 0;
_ritualTimer.Value = 0;
}
[ObserversRpc]
private void RitualStartedObservers(NetworkObject target)
{
OnRitualStarted?.Invoke(target);
}
[ObserversRpc]
private void RitualCancelledObservers()
{
OnRitualCancelled?.Invoke();
}
[ObserversRpc]
private void RitualCompleteObservers(NetworkObject target)
{
OnRitualComplete?.Invoke(target);
}
}
}
fileFormatVersion: 2
guid: b7e2a3650cfba4653a85291a65dc4e2b
\ No newline at end of file
...@@ -11,24 +11,46 @@ namespace IStalk.Core ...@@ -11,24 +11,46 @@ namespace IStalk.Core
public class ExtractionManager : NetworkBehaviour public class ExtractionManager : NetworkBehaviour
{ {
private readonly SyncVar<int> _teamLootDeposited = new(); private readonly SyncVar<int> _teamLootDeposited = new();
private readonly SyncList<int> _extractedPlayerIds = new(); private readonly SyncVar<bool> _extractionRoomUnlocked = new();
private readonly SyncVar<bool> _allExtracted = new();
private GlobalRules_SO Rules => GlobalRules_SO.Instance; private GlobalRules_SO Rules => GlobalRules_SO.Instance;
private List<Vector3> _altarPositions = new(); private List<Vector3> _altarPositions = new();
private ExtractionRoom _extractionRoom;
private readonly HashSet<NetworkObject> _aliveRunners = new();
public int TeamLootDeposited => _teamLootDeposited.Value; public int TeamLootDeposited => _teamLootDeposited.Value;
public int ExtractionThreshold => Rules.extractionThreshold; public int ExtractionThreshold => Rules.extractionThreshold;
public bool IsExtractionMet => _teamLootDeposited.Value >= Rules.extractionThreshold; public bool IsExtractionMet => _teamLootDeposited.Value >= Rules.extractionThreshold;
public bool IsRoomUnlocked => _extractionRoomUnlocked.Value;
public int AliveRunnerCount => _aliveRunners.Count;
public event System.Action<int> OnLootDeposited; public event System.Action<int> OnLootDeposited;
public event System.Action<int> OnPlayerExtracted;
public event System.Action OnExtractionThresholdMet; public event System.Action OnExtractionThresholdMet;
public event System.Action OnAllExtracted;
public void SetAltarPositions(List<Vector3> positions) public void SetAltarPositions(List<Vector3> positions)
{ {
_altarPositions = positions; _altarPositions = positions;
} }
public void SetExtractionRoom(ExtractionRoom room)
{
_extractionRoom = room;
}
[Server]
public void RegisterRunner(NetworkObject runner)
{
_aliveRunners.Add(runner);
}
[Server]
public void MarkRunnerDead(NetworkObject runner)
{
_aliveRunners.Remove(runner);
}
[Server] [Server]
public void DepositLoot(PlayerInventory inventory, int amount) public void DepositLoot(PlayerInventory inventory, int amount)
{ {
...@@ -39,8 +61,13 @@ namespace IStalk.Core ...@@ -39,8 +61,13 @@ namespace IStalk.Core
_teamLootDeposited.Value += amount; _teamLootDeposited.Value += amount;
NotifyDepositObservers(_teamLootDeposited.Value); NotifyDepositObservers(_teamLootDeposited.Value);
if (IsExtractionMet) if (IsExtractionMet && !_extractionRoomUnlocked.Value)
{
_extractionRoomUnlocked.Value = true;
if (_extractionRoom != null)
_extractionRoom.Unlock();
OnExtractionThresholdMet?.Invoke(); OnExtractionThresholdMet?.Invoke();
}
} }
[ObserversRpc] [ObserversRpc]
...@@ -50,14 +77,10 @@ namespace IStalk.Core ...@@ -50,14 +77,10 @@ namespace IStalk.Core
} }
[Server] [Server]
public bool TryExtract(NetworkObject player) public void ExtractAll()
{ {
if (!IsExtractionMet) return false; _allExtracted.Value = true;
if (_extractedPlayerIds.Contains(player.ObjectId)) return false; OnAllExtracted?.Invoke();
_extractedPlayerIds.Add(player.ObjectId);
OnPlayerExtracted?.Invoke(player.ObjectId);
return true;
} }
public bool IsNearAltar(Vector3 position) public bool IsNearAltar(Vector3 position)
......
using System.Collections.Generic;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
using IStalk.Data;
namespace IStalk.Core
{
public class ExtractionRoom : NetworkBehaviour
{
[SerializeField] private Collider _roomTrigger;
private readonly SyncVar<bool> _isUnlocked = new();
private readonly SyncVar<float> _extractionTimer = new(new SyncTypeSettings(0.5f));
private readonly SyncVar<int> _runnersInsideCount = new();
private readonly HashSet<NetworkObject> _runnersInside = new();
private ExtractionManager _extractionManager;
public bool IsUnlocked => _isUnlocked.Value;
public float Timer => _extractionTimer.Value;
public int RunnersInside => _runnersInsideCount.Value;
public event System.Action OnExtractionStarted;
public event System.Action OnExtractionComplete;
public event System.Action OnExtractionInterrupted;
public override void OnStartServer()
{
base.OnStartServer();
_extractionManager = FindAnyObjectByType<ExtractionManager>();
_extractionTimer.Value = GlobalRules_SO.Instance.extractionChannelDuration;
}
public override void OnStartClient()
{
base.OnStartClient();
_extractionTimer.OnChange += OnTimerChanged;
}
[Server]
public void Unlock()
{
_isUnlocked.Value = true;
UnlockDoorObservers();
}
[ObserversRpc]
private void UnlockDoorObservers()
{
}
private void OnTriggerEnter(Collider other)
{
if (!IsServerInitialized || !_isUnlocked.Value) return;
var nob = other.GetComponentInParent<NetworkObject>();
if (nob == null) return;
var health = nob.GetComponent<Runner.RunnerHealth>();
if (health == null || health.IsDown) return;
_runnersInside.Add(nob);
_runnersInsideCount.Value = _runnersInside.Count;
}
private void OnTriggerExit(Collider other)
{
if (!IsServerInitialized) return;
var nob = other.GetComponentInParent<NetworkObject>();
if (nob == null) return;
if (_runnersInside.Remove(nob))
{
_runnersInsideCount.Value = _runnersInside.Count;
if (_extractionTimer.Value < GlobalRules_SO.Instance.extractionChannelDuration)
{
_extractionTimer.Value = GlobalRules_SO.Instance.extractionChannelDuration;
ExtractionInterruptedObservers();
}
}
}
private void Update()
{
if (!IsServerInitialized || !_isUnlocked.Value) return;
CleanupDownedRunners();
int aliveCount = _extractionManager != null ? _extractionManager.AliveRunnerCount : 0;
if (aliveCount <= 0) return;
if (_runnersInside.Count >= aliveCount && _runnersInside.Count > 0)
{
_extractionTimer.Value -= Time.deltaTime;
if (_extractionTimer.Value <= 0)
{
_extractionTimer.Value = 0;
CompleteExtraction();
}
}
else
{
if (_extractionTimer.Value < GlobalRules_SO.Instance.extractionChannelDuration)
{
_extractionTimer.Value = GlobalRules_SO.Instance.extractionChannelDuration;
ExtractionInterruptedObservers();
}
}
}
private void CleanupDownedRunners()
{
var toRemove = new List<NetworkObject>();
foreach (var nob in _runnersInside)
{
if (nob == null)
{
toRemove.Add(nob);
continue;
}
var health = nob.GetComponent<Runner.RunnerHealth>();
if (health != null && health.IsDown)
toRemove.Add(nob);
}
foreach (var nob in toRemove)
_runnersInside.Remove(nob);
if (toRemove.Count > 0)
_runnersInsideCount.Value = _runnersInside.Count;
}
[Server]
private void CompleteExtraction()
{
if (_extractionManager != null)
_extractionManager.ExtractAll();
ExtractionCompleteObservers();
}
[ObserversRpc]
private void ExtractionCompleteObservers()
{
OnExtractionComplete?.Invoke();
}
[ObserversRpc]
private void ExtractionInterruptedObservers()
{
OnExtractionInterrupted?.Invoke();
}
private void OnTimerChanged(float prev, float next, bool asServer)
{
if (!asServer && next < prev && prev >= GlobalRules_SO.Instance.extractionChannelDuration)
OnExtractionStarted?.Invoke();
}
}
}
fileFormatVersion: 2
guid: 3c7eb0798b2b847b2987bf36207239c3
\ No newline at end of file
...@@ -23,7 +23,6 @@ namespace IStalk.Core ...@@ -23,7 +23,6 @@ namespace IStalk.Core
private readonly SyncVar<int> _sealedRoomCount = new(); private readonly SyncVar<int> _sealedRoomCount = new();
private GlobalRules_SO Rules => GlobalRules_SO.Instance; private GlobalRules_SO Rules => GlobalRules_SO.Instance;
private int _extractedPlayers;
private int _totalRunners; private int _totalRunners;
private float _nextSealTime; private float _nextSealTime;
...@@ -35,8 +34,9 @@ namespace IStalk.Core ...@@ -35,8 +34,9 @@ namespace IStalk.Core
public event System.Action<MatchState> OnStateChanged; public event System.Action<MatchState> OnStateChanged;
public event System.Action<int> OnRoomSealed; public event System.Action<int> OnRoomSealed;
private void Awake() public override void OnStartNetwork()
{ {
base.OnStartNetwork();
Instance = this; Instance = this;
} }
...@@ -45,7 +45,7 @@ namespace IStalk.Core ...@@ -45,7 +45,7 @@ namespace IStalk.Core
base.OnStartServer(); base.OnStartServer();
_state.Value = MatchState.WaitingForPlayers; _state.Value = MatchState.WaitingForPlayers;
_state.OnChange += HandleStateChange; _state.OnChange += HandleStateChange;
_extractionManager.OnPlayerExtracted += HandlePlayerExtracted; _extractionManager.OnAllExtracted += HandleAllExtracted;
StartMatch(); StartMatch();
} }
...@@ -138,11 +138,9 @@ namespace IStalk.Core ...@@ -138,11 +138,9 @@ namespace IStalk.Core
OnRoomSealed?.Invoke(roomIndex); OnRoomSealed?.Invoke(roomIndex);
} }
private void HandlePlayerExtracted(int objectId) private void HandleAllExtracted()
{ {
_extractedPlayers++; _state.Value = MatchState.RunnersWin;
if (_extractedPlayers >= _totalRunners)
_state.Value = MatchState.RunnersWin;
} }
[Server] [Server]
......
using UnityEngine;
namespace IStalk.Data
{
public enum EyeAbility
{
Laser,
Scan,
Traps,
Incarnation
}
[CreateAssetMenu(fileName = "EyeType_", menuName = "IStalk/Eye Type")]
public class EyeType_SO : ScriptableObject
{
[Header("Identity")]
public string typeId;
public string displayName;
[TextArea(2, 4)]
public string description;
public Sprite icon;
[Header("Abilities")]
public EyeAbility[] enabledAbilities;
public bool hasEyelids;
[Header("Stat Overrides")]
[Tooltip("Multiplier for laser cooldown (lower = faster)")]
public float laserCooldownMultiplier = 1f;
[Tooltip("Multiplier for laser radius (higher = wider)")]
public float laserRadiusMultiplier = 1f;
[Tooltip("Multiplier for scan cooldown (lower = faster)")]
public float scanCooldownMultiplier = 1f;
[Tooltip("Multiplier for scan radius (higher = wider)")]
public float scanRadiusMultiplier = 1f;
[Tooltip("Multiplier for trap Dread cost (lower = cheaper)")]
public float trapCostMultiplier = 1f;
[Tooltip("Bonus to max active traps")]
public int trapMaxActiveBonus = 0;
[Header("Dread Overrides")]
[Tooltip("Override starting Dread (-1 = use default)")]
public float dreadStartOverride = -1f;
[Tooltip("Override Dread regen rate (-1 = use default)")]
public float dreadRegenOverride = -1f;
public bool HasAbility(EyeAbility ability)
{
if (enabledAbilities == null) return false;
for (int i = 0; i < enabledAbilities.Length; i++)
{
if (enabledAbilities[i] == ability) return true;
}
return false;
}
}
}
fileFormatVersion: 2
guid: 470b264832d4340b887b07cab10b6085
\ No newline at end of file
...@@ -93,6 +93,36 @@ namespace IStalk.Data ...@@ -93,6 +93,36 @@ namespace IStalk.Data
public float altarInteractRadius = 2.5f; public float altarInteractRadius = 2.5f;
public float playerSpawnTimeout = 10f; public float playerSpawnTimeout = 10f;
[Header("Eye Types")]
public float flashDuration = 5f;
public float flashTeamCooldown = 15f;
public float watcherScanCooldownMultiplier = 0.6f;
public float watcherScanRadiusMultiplier = 1.3f;
public float predatorLaserCooldownMultiplier = 0.5f;
public float predatorLaserRadiusMultiplier = 1.4f;
public float trapperDreadCostMultiplier = 0.6f;
public int trapperMaxActiveBonus = 3;
[Header("Wearables")]
public int maxWearableSlots = 2;
public float dashCooldown = 3f;
public float airDashCooldown = 5f;
public float grappleCooldown = 8f;
public float throwRangeBase = 8f;
public float throwRangeWeightScale = 0.5f;
[Header("Co-op")]
public float boostHeight = 2f;
public float boostRange = 1.5f;
public float linkedSprintBonus = 0.2f;
public float linkedSprintRadius = 5f;
public float linkedSprintSlowDuration = 1f;
[Header("Extraction Room")]
public float extractionChannelDuration = 30f;
public float antiAfkRitualDuration = 20f;
public int antiAfkMinVoters = 2;
public void Initialize() public void Initialize()
{ {
Instance = this; Instance = this;
......
...@@ -12,6 +12,8 @@ namespace IStalk.Data ...@@ -12,6 +12,8 @@ namespace IStalk.Data
private Dictionary<string, ShrineItemData_SO> _shrineItems = new(); private Dictionary<string, ShrineItemData_SO> _shrineItems = new();
private Dictionary<string, TrapData_SO> _traps = new(); private Dictionary<string, TrapData_SO> _traps = new();
private Dictionary<string, RoomNode_SO> _rooms = new(); private Dictionary<string, RoomNode_SO> _rooms = new();
private Dictionary<string, WearableData_SO> _wearables = new();
private Dictionary<string, EyeType_SO> _eyeTypes = new();
private void Awake() private void Awake()
{ {
...@@ -39,7 +41,13 @@ namespace IStalk.Data ...@@ -39,7 +41,13 @@ namespace IStalk.Data
foreach (var room in Resources.LoadAll<RoomNode_SO>("Items/Rooms")) foreach (var room in Resources.LoadAll<RoomNode_SO>("Items/Rooms"))
_rooms[room.roomId] = room; _rooms[room.roomId] = room;
Debug.Log($"[ItemDatabase] Loaded {_lootItems.Count} loot, {_shrineItems.Count} shrine, {_traps.Count} traps, {_rooms.Count} rooms"); foreach (var wearable in Resources.LoadAll<WearableData_SO>("Items/Wearables"))
_wearables[wearable.wearableId] = wearable;
foreach (var eyeType in Resources.LoadAll<EyeType_SO>("EyeTypes"))
_eyeTypes[eyeType.typeId] = eyeType;
Debug.Log($"[ItemDatabase] Loaded {_lootItems.Count} loot, {_shrineItems.Count} shrine, {_traps.Count} traps, {_rooms.Count} rooms, {_wearables.Count} wearables, {_eyeTypes.Count} eye types");
} }
public ItemData_SO GetLootItem(string id) => _lootItems.GetValueOrDefault(id); public ItemData_SO GetLootItem(string id) => _lootItems.GetValueOrDefault(id);
...@@ -49,6 +57,11 @@ namespace IStalk.Data ...@@ -49,6 +57,11 @@ namespace IStalk.Data
public IReadOnlyCollection<ItemData_SO> AllLootItems => _lootItems.Values; public IReadOnlyCollection<ItemData_SO> AllLootItems => _lootItems.Values;
public IReadOnlyCollection<RoomNode_SO> AllRooms => _rooms.Values; public IReadOnlyCollection<RoomNode_SO> AllRooms => _rooms.Values;
public IReadOnlyCollection<WearableData_SO> AllWearables => _wearables.Values;
public IReadOnlyCollection<EyeType_SO> AllEyeTypes => _eyeTypes.Values;
public WearableData_SO GetWearable(string id) => _wearables.GetValueOrDefault(id);
public EyeType_SO GetEyeType(string id) => _eyeTypes.GetValueOrDefault(id);
public List<RoomNode_SO> GetRoomsWithLootSlots() => public List<RoomNode_SO> GetRoomsWithLootSlots() =>
_rooms.Values.Where(r => r.lootSpawnSlots > 0).ToList(); _rooms.Values.Where(r => r.lootSpawnSlots > 0).ToList();
......
...@@ -9,7 +9,11 @@ namespace IStalk.Data ...@@ -9,7 +9,11 @@ namespace IStalk.Data
SmokeBomb, SmokeBomb,
Decoy, Decoy,
WeightReducer, WeightReducer,
GrapplingHook GrapplingHook,
FlashOrb,
LinkedSprintTotem,
SilenceShroud,
VitalityLink
} }
[CreateAssetMenu(fileName = "ShrineItem_", menuName = "IStalk/Item Database/Shrine Item")] [CreateAssetMenu(fileName = "ShrineItem_", menuName = "IStalk/Item Database/Shrine Item")]
......
using IStalk.Runner;
using UnityEngine;
namespace IStalk.Data
{
[CreateAssetMenu(fileName = "Wearable_", menuName = "IStalk/Item Database/Wearable")]
public class WearableData_SO : ScriptableObject
{
[Header("Identity")]
public string wearableId;
public string displayName;
[TextArea(2, 4)]
public string description;
public Sprite icon;
[Header("Ability")]
public WearableAbilityType abilityType;
public bool isPassive;
[Tooltip("Cooldown in seconds (0 for passive abilities)")]
public float cooldown;
[Header("World Pickup")]
public GameObject worldPrefab;
[Header("Shrine Purchase")]
[Tooltip("0 = not purchasable at shrine")]
public int lootCost;
[Header("Throw")]
[Tooltip("Max throw distance when passing to teammate")]
public float throwRange = 8f;
}
}
fileFormatVersion: 2
guid: f9a147f1024714a088cf7c466e30409e
\ No newline at end of file
...@@ -72,6 +72,54 @@ namespace IStalk.Editor ...@@ -72,6 +72,54 @@ namespace IStalk.Editor
ProjectWindowUtil.CreateAsset(item, "Assets/Resources/Items/Rooms/Room_New.asset"); ProjectWindowUtil.CreateAsset(item, "Assets/Resources/Items/Rooms/Room_New.asset");
} }
[MenuItem("IStalk/Create/Eye Type SO", false, 210)]
public static void CreateEyeType()
{
var item = ScriptableObject.CreateInstance<EyeType_SO>();
ProjectWindowUtil.CreateAsset(item, "Assets/Resources/EyeTypes/EyeType_New.asset");
}
[MenuItem("IStalk/Create/Wearable SO", false, 211)]
public static void CreateWearable()
{
var item = ScriptableObject.CreateInstance<WearableData_SO>();
ProjectWindowUtil.CreateAsset(item, "Assets/Resources/Items/Wearables/Wearable_New.asset");
}
[MenuItem("IStalk/Create/Extraction Room Prefab Setup", false, 110)]
public static void CreateExtractionRoomPrefab()
{
var root = new GameObject("ExtractionRoom");
var boxCol = root.AddComponent<BoxCollider>();
boxCol.isTrigger = true;
boxCol.size = new Vector3(15, 4, 15);
boxCol.center = new Vector3(0, 2, 0);
for (int i = 0; i < 6; i++)
{
var prop = new GameObject($"IncarnationProp_{i + 1}");
prop.transform.parent = root.transform;
float angle = i * 60f * Mathf.Deg2Rad;
prop.transform.localPosition = new Vector3(Mathf.Cos(angle) * 4f, 0.5f, Mathf.Sin(angle) * 4f);
var slot = prop.AddComponent<SpawnSlot>();
slot.slotType = SpawnSlotType.PropIncarnation;
}
Selection.activeGameObject = root;
Undo.RegisterCreatedObjectUndo(root, "Create Extraction Room");
EditorUtility.DisplayDialog("Extraction Room Created",
"Extraction Room prefab structure created.\n\n" +
"1. Add room geometry (floor, walls, ceiling)\n" +
"2. Replace SpawnSlot children with actual IncarnationProp prefabs\n" +
"3. Adjust BoxCollider to fit your room size\n" +
"4. Add NetworkObject + ExtractionRoom.cs + AntiAfkRitual.cs\n" +
"5. Save as prefab in Assets/Prefabs/Props/\n" +
"6. Register in FishNet DefaultPrefabObjects",
"OK");
}
[MenuItem("IStalk/Create/Global Rules SO", false, 300)] [MenuItem("IStalk/Create/Global Rules SO", false, 300)]
public static void CreateGlobalRules() public static void CreateGlobalRules()
{ {
...@@ -95,6 +143,8 @@ namespace IStalk.Editor ...@@ -95,6 +143,8 @@ namespace IStalk.Editor
"Assets/Resources/Items/Shrine", "Assets/Resources/Items/Shrine",
"Assets/Resources/Items/Traps", "Assets/Resources/Items/Traps",
"Assets/Resources/Items/Rooms", "Assets/Resources/Items/Rooms",
"Assets/Resources/Items/Wearables",
"Assets/Resources/EyeTypes",
"Assets/Prefabs/Player", "Assets/Prefabs/Player",
"Assets/Prefabs/Props", "Assets/Prefabs/Props",
"Assets/Prefabs/UI", "Assets/Prefabs/UI",
......
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using IStalk.Core;
using IStalk.Data;
namespace IStalk.Editor.Inspectors
{
[CustomEditor(typeof(ExtractionRoom))]
public class ExtractionRoomEditor : UnityEditor.Editor
{
private ExtractionRoom _room;
private void OnEnable()
{
_room = (ExtractionRoom)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Extraction Room Setup", EditorStyles.boldLabel);
var rules = FindRules();
if (rules != null)
{
EditorGUILayout.LabelField($"Channel Duration: {rules.extractionChannelDuration:F0}s");
EditorGUILayout.LabelField($"Anti-AFK Ritual: {rules.antiAfkRitualDuration:F0}s (min {rules.antiAfkMinVoters} voters)");
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Design Notes", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Extraction Room Rules:\n" +
"• All alive runners must be inside simultaneously\n" +
"• Timer pauses/resets if anyone leaves\n" +
"• Downed runners are ignored (not required)\n" +
"• Room should be dense with IncarnationProp objects\n" +
"• Place a Trigger collider covering the entire room area",
MessageType.Info);
EditorGUILayout.Space(5);
DrawSetupValidator();
if (Application.isPlaying && _room.IsSpawned)
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Runtime State", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Unlocked: {_room.IsUnlocked}");
EditorGUILayout.LabelField($" Timer: {_room.Timer:F1}s");
EditorGUILayout.LabelField($" Runners Inside: {_room.RunnersInside}");
Repaint();
}
}
private void DrawSetupValidator()
{
EditorGUILayout.LabelField("Setup Checklist", EditorStyles.boldLabel);
var trigger = _room.GetComponent<Collider>();
if (trigger == null)
{
trigger = _room.GetComponentInChildren<Collider>();
}
if (trigger == null)
EditorGUILayout.HelpBox("No Collider found! Add a trigger collider covering the room.", MessageType.Error);
else if (!trigger.isTrigger)
EditorGUILayout.HelpBox("Collider is not set as Trigger. Enable 'Is Trigger'.", MessageType.Error);
else
EditorGUILayout.LabelField(" ✓ Trigger collider present");
int incarnationCount = _room.GetComponentsInChildren<MonoBehaviour>().Length;
var incarnationProps = _room.transform.GetComponentsInChildren<Transform>();
int propCount = 0;
foreach (var t in incarnationProps)
{
if (t.GetComponent<FishNet.Object.NetworkObject>() != null && t != _room.transform)
propCount++;
}
if (propCount == 0)
EditorGUILayout.HelpBox("No IncarnationProp children found. Add props for the Eye to incarnate during extraction.", MessageType.Warning);
else
EditorGUILayout.LabelField($" ✓ {propCount} networked prop(s) in room");
}
private GlobalRules_SO FindRules()
{
if (GlobalRules_SO.Instance != null) return GlobalRules_SO.Instance;
string[] guids = AssetDatabase.FindAssets("t:GlobalRules_SO");
if (guids.Length == 0) return null;
return AssetDatabase.LoadAssetAtPath<GlobalRules_SO>(AssetDatabase.GUIDToAssetPath(guids[0]));
}
}
}
#endif
fileFormatVersion: 2
guid: 96e801393c5d6461bbd3a12f3cfcbebd
\ No newline at end of file
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using IStalk.Data;
namespace IStalk.Editor.Inspectors
{
[CustomEditor(typeof(EyeType_SO))]
public class EyeTypeEditor : UnityEditor.Editor
{
private EyeType_SO _eyeType;
private bool _showAbilityPreview = true;
private bool _showStatPreview = true;
private void OnEnable()
{
_eyeType = (EyeType_SO)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(15);
_showAbilityPreview = EditorGUILayout.Foldout(_showAbilityPreview, "Ability Summary", true);
if (_showAbilityPreview)
{
EditorGUI.indentLevel++;
DrawAbilityMatrix();
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(10);
_showStatPreview = EditorGUILayout.Foldout(_showStatPreview, "Effective Stats Preview", true);
if (_showStatPreview)
{
EditorGUI.indentLevel++;
DrawStatPreview();
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(10);
DrawQuickActions();
DrawValidation();
}
private void DrawAbilityMatrix()
{
EditorGUILayout.LabelField("Enabled Abilities:", EditorStyles.boldLabel);
bool hasLaser = _eyeType.HasAbility(EyeAbility.Laser);
bool hasScan = _eyeType.HasAbility(EyeAbility.Scan);
bool hasTraps = _eyeType.HasAbility(EyeAbility.Traps);
bool hasIncarnation = _eyeType.HasAbility(EyeAbility.Incarnation);
DrawAbilityRow("Laser", hasLaser, "Fire damaging beam at runners");
DrawAbilityRow("Scan", hasScan, "Reveal all runners briefly");
DrawAbilityRow("Traps", hasTraps, "Place traps on the map");
DrawAbilityRow("Incarnation", hasIncarnation, "Possess and move props");
DrawAbilityRow("Eyelids", _eyeType.hasEyelids, "Close eyes to dodge flash (skill timing)");
int abilityCount = (hasLaser ? 1 : 0) + (hasScan ? 1 : 0) + (hasTraps ? 1 : 0) + (hasIncarnation ? 1 : 0);
EditorGUILayout.Space(5);
EditorGUILayout.LabelField($"Total: {abilityCount}/4 abilities active");
}
private void DrawAbilityRow(string name, bool enabled, string tooltip)
{
string icon = enabled ? "✓" : "✗";
Color color = enabled ? new Color(0.2f, 0.8f, 0.2f) : new Color(0.5f, 0.5f, 0.5f);
var style = new GUIStyle(EditorStyles.label) { richText = true };
string colorHex = ColorUtility.ToHtmlStringRGB(color);
EditorGUILayout.LabelField(new GUIContent($" <color=#{colorHex}>{icon}</color> {name}", tooltip), style);
}
private void DrawStatPreview()
{
var rules = FindRules();
if (rules == null)
{
EditorGUILayout.HelpBox("GlobalRules_SO not found. Create one to see effective stats.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Laser", EditorStyles.boldLabel);
if (_eyeType.HasAbility(EyeAbility.Laser))
{
float cd = rules.laserCooldown * _eyeType.laserCooldownMultiplier;
float rad = rules.laserRadius * _eyeType.laserRadiusMultiplier;
EditorGUILayout.LabelField($" Cooldown: {cd:F1}s (base {rules.laserCooldown:F1} x {_eyeType.laserCooldownMultiplier:F2})");
EditorGUILayout.LabelField($" Radius: {rad:F2}m (base {rules.laserRadius:F2} x {_eyeType.laserRadiusMultiplier:F2})");
EditorGUILayout.LabelField($" DPS potential: {1f / cd:F2} shots/sec");
}
else
{
EditorGUILayout.LabelField(" (Disabled for this type)");
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Scan", EditorStyles.boldLabel);
if (_eyeType.HasAbility(EyeAbility.Scan))
{
float cd = (rules.laserCooldown * 3f) * _eyeType.scanCooldownMultiplier;
float rad = 30f * _eyeType.scanRadiusMultiplier;
EditorGUILayout.LabelField($" Cooldown: {cd:F1}s (x{_eyeType.scanCooldownMultiplier:F2})");
EditorGUILayout.LabelField($" Radius: {rad:F1}m (x{_eyeType.scanRadiusMultiplier:F2})");
}
else
{
EditorGUILayout.LabelField(" (Disabled for this type)");
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Traps", EditorStyles.boldLabel);
if (_eyeType.HasAbility(EyeAbility.Traps))
{
EditorGUILayout.LabelField($" Cost multiplier: {_eyeType.trapCostMultiplier:F2}x (lower = cheaper)");
EditorGUILayout.LabelField($" Max active bonus: +{_eyeType.trapMaxActiveBonus}");
}
else
{
EditorGUILayout.LabelField(" (Disabled for this type)");
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Dread", EditorStyles.boldLabel);
if (_eyeType.dreadStartOverride >= 0)
EditorGUILayout.LabelField($" Starting Dread: {_eyeType.dreadStartOverride:F0} (override)");
else
EditorGUILayout.LabelField($" Starting Dread: {rules.dreadStartingAmount:F0} (default)");
if (_eyeType.dreadRegenOverride >= 0)
EditorGUILayout.LabelField($" Regen rate: {_eyeType.dreadRegenOverride:F1}/s (override)");
else
EditorGUILayout.LabelField($" Regen rate: {rules.dreadBaseRegenRate:F1}/s (default)");
}
private void DrawQuickActions()
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Generate ID from Name"))
{
Undo.RecordObject(_eyeType, "Generate Eye Type ID");
_eyeType.typeId = _eyeType.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_eyeType);
}
if (GUILayout.Button("Preset: Watcher"))
{
ApplyPreset(new[] { EyeAbility.Scan, EyeAbility.Incarnation }, false,
1f, 1f, 0.6f, 1.3f, 1f, 0);
}
if (GUILayout.Button("Preset: Predator"))
{
ApplyPreset(new[] { EyeAbility.Laser, EyeAbility.Incarnation }, true,
0.5f, 1.4f, 1f, 1f, 1f, 0);
}
if (GUILayout.Button("Preset: Trapper"))
{
ApplyPreset(new[] { EyeAbility.Traps, EyeAbility.Scan, EyeAbility.Incarnation }, false,
1f, 1f, 1f, 1f, 0.6f, 3);
}
EditorGUILayout.EndHorizontal();
}
private void ApplyPreset(EyeAbility[] abilities, bool eyelids,
float laserCd, float laserRad, float scanCd, float scanRad, float trapCost, int trapBonus)
{
Undo.RecordObject(_eyeType, "Apply Eye Type Preset");
_eyeType.enabledAbilities = abilities;
_eyeType.hasEyelids = eyelids;
_eyeType.laserCooldownMultiplier = laserCd;
_eyeType.laserRadiusMultiplier = laserRad;
_eyeType.scanCooldownMultiplier = scanCd;
_eyeType.scanRadiusMultiplier = scanRad;
_eyeType.trapCostMultiplier = trapCost;
_eyeType.trapMaxActiveBonus = trapBonus;
EditorUtility.SetDirty(_eyeType);
}
private void DrawValidation()
{
EditorGUILayout.Space(10);
if (string.IsNullOrEmpty(_eyeType.typeId))
EditorGUILayout.HelpBox("Type ID is empty. Click 'Generate ID from Name'.", MessageType.Error);
if (string.IsNullOrEmpty(_eyeType.displayName))
EditorGUILayout.HelpBox("Display Name is empty.", MessageType.Warning);
if (_eyeType.enabledAbilities == null || _eyeType.enabledAbilities.Length == 0)
EditorGUILayout.HelpBox("No abilities enabled! This eye type will have no actions.", MessageType.Error);
if (_eyeType.icon == null)
EditorGUILayout.HelpBox("No icon assigned (optional for greybox).", MessageType.Info);
}
private GlobalRules_SO FindRules()
{
if (GlobalRules_SO.Instance != null) return GlobalRules_SO.Instance;
string[] guids = AssetDatabase.FindAssets("t:GlobalRules_SO");
if (guids.Length == 0) return null;
return AssetDatabase.LoadAssetAtPath<GlobalRules_SO>(AssetDatabase.GUIDToAssetPath(guids[0]));
}
}
}
#endif
fileFormatVersion: 2
guid: 90d3beee4dcfe4087b5974e307da06ac
\ No newline at end of file
...@@ -10,6 +10,10 @@ namespace IStalk.Editor.Inspectors ...@@ -10,6 +10,10 @@ namespace IStalk.Editor.Inspectors
{ {
private GlobalRules_SO _rules; private GlobalRules_SO _rules;
private bool _showBalancePreview = true; private bool _showBalancePreview = true;
private bool _showEyeTypes = true;
private bool _showWearables = true;
private bool _showCoop = true;
private bool _showExtraction = true;
private void OnEnable() private void OnEnable()
{ {
...@@ -23,41 +27,96 @@ namespace IStalk.Editor.Inspectors ...@@ -23,41 +27,96 @@ namespace IStalk.Editor.Inspectors
EditorGUILayout.Space(15); EditorGUILayout.Space(15);
_showBalancePreview = EditorGUILayout.Foldout(_showBalancePreview, "Balance Preview", true); _showBalancePreview = EditorGUILayout.Foldout(_showBalancePreview, "Balance Preview", true);
if (!_showBalancePreview) return; if (_showBalancePreview)
{
EditorGUILayout.LabelField("Weight Tiers", EditorStyles.boldLabel); EditorGUILayout.LabelField("Weight Tiers", EditorStyles.boldLabel);
DrawTierRow("Naked", 0, _rules.weightThresholdLight, _rules.speedMultNaked); DrawTierRow("Naked", 0, _rules.weightThresholdLight, _rules.speedMultNaked);
DrawTierRow("Light", _rules.weightThresholdLight, _rules.weightThresholdModerate, _rules.speedMultLight); DrawTierRow("Light", _rules.weightThresholdLight, _rules.weightThresholdModerate, _rules.speedMultLight);
DrawTierRow("Moderate", _rules.weightThresholdModerate, _rules.weightThresholdHeavy, _rules.speedMultModerate); DrawTierRow("Moderate", _rules.weightThresholdModerate, _rules.weightThresholdHeavy, _rules.speedMultModerate);
DrawTierRow("Heavy", _rules.weightThresholdHeavy, _rules.weightThresholdOverloaded, _rules.speedMultHeavy); DrawTierRow("Heavy", _rules.weightThresholdHeavy, _rules.weightThresholdOverloaded, _rules.speedMultHeavy);
DrawTierRow("Overloaded", _rules.weightThresholdOverloaded, float.MaxValue, _rules.speedMultOverloaded); DrawTierRow("Overloaded", _rules.weightThresholdOverloaded, float.MaxValue, _rules.speedMultOverloaded);
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Movement at Base Speed", EditorStyles.boldLabel); EditorGUILayout.LabelField("Movement at Base Speed", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Walk: {_rules.baseRunSpeed:F1} m/s"); EditorGUILayout.LabelField($" Walk: {_rules.baseRunSpeed:F1} m/s");
EditorGUILayout.LabelField($" Sprint: {_rules.baseRunSpeed * _rules.sprintMultiplier:F1} m/s"); EditorGUILayout.LabelField($" Sprint: {_rules.baseRunSpeed * _rules.sprintMultiplier:F1} m/s");
EditorGUILayout.LabelField($" Slide: {_rules.slideSpeed:F1} m/s"); EditorGUILayout.LabelField($" Slide: {_rules.slideSpeed:F1} m/s");
EditorGUILayout.LabelField($" Overloaded Walk: {_rules.baseRunSpeed * _rules.speedMultOverloaded:F1} m/s"); EditorGUILayout.LabelField($" Overloaded Walk: {_rules.baseRunSpeed * _rules.speedMultOverloaded:F1} m/s");
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Extraction Economy", EditorStyles.boldLabel); EditorGUILayout.LabelField("Extraction Economy", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Threshold: {_rules.extractionThreshold} loot value"); EditorGUILayout.LabelField($" Threshold: {_rules.extractionThreshold} loot value");
EditorGUILayout.LabelField($" Min loot in map: ~{_rules.extractionThreshold * _rules.lootValueToThresholdRatio:F0} value"); EditorGUILayout.LabelField($" Min loot in map: ~{_rules.extractionThreshold * _rules.lootValueToThresholdRatio:F0} value");
EditorGUILayout.LabelField($" Channel time (empty): {_rules.altarChannelBaseTime:F1}s"); EditorGUILayout.LabelField($" Channel time (empty): {_rules.altarChannelBaseTime:F1}s");
EditorGUILayout.LabelField($" Channel time (20kg): {_rules.GetChannelTime(20):F1}s"); EditorGUILayout.LabelField($" Channel time (20kg): {_rules.GetChannelTime(20):F1}s");
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Map Generation", EditorStyles.boldLabel); EditorGUILayout.LabelField("Map Generation", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Rooms: {_rules.minRoomsPerMap}{_rules.maxRoomsPerMap}"); EditorGUILayout.LabelField($" Rooms: {_rules.minRoomsPerMap}{_rules.maxRoomsPerMap}");
EditorGUILayout.LabelField($" Floors: {_rules.minFloors}{_rules.maxFloors}"); EditorGUILayout.LabelField($" Floors: {_rules.minFloors}{_rules.maxFloors}");
EditorGUILayout.LabelField($" Altars: {_rules.minAltars}{_rules.maxAltars}"); EditorGUILayout.LabelField($" Altars: {_rules.minAltars}{_rules.maxAltars}");
EditorGUILayout.LabelField($" Map area (max): ~{_rules.maxRoomsPerMap * _rules.cellWidth * _rules.cellWidth:F0}m²"); EditorGUILayout.LabelField($" Map area (max): ~{_rules.maxRoomsPerMap * _rules.cellWidth * _rules.cellWidth:F0}m²");
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Match Timing", EditorStyles.boldLabel); EditorGUILayout.LabelField("Match Timing", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Grace: {_rules.gracePeriodDuration}s"); EditorGUILayout.LabelField($" Grace: {_rules.gracePeriodDuration}s");
EditorGUILayout.LabelField($" Hunt: {_rules.matchTimeLimit / 60f:F1} min"); EditorGUILayout.LabelField($" Hunt: {_rules.matchTimeLimit / 60f:F1} min");
EditorGUILayout.LabelField($" Total max: {(_rules.gracePeriodDuration + _rules.matchTimeLimit) / 60f:F1} min"); EditorGUILayout.LabelField($" Total max: {(_rules.gracePeriodDuration + _rules.matchTimeLimit) / 60f:F1} min");
}
EditorGUILayout.Space(10);
_showEyeTypes = EditorGUILayout.Foldout(_showEyeTypes, "Eye Type Balance", true);
if (_showEyeTypes)
{
EditorGUILayout.LabelField("Flash/Eyelid System", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Flash blind duration: {_rules.flashDuration:F1}s");
EditorGUILayout.LabelField($" Team flash cooldown: {_rules.flashTeamCooldown:F1}s");
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Watcher", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Scan cooldown: x{_rules.watcherScanCooldownMultiplier:F2} (faster)");
EditorGUILayout.LabelField($" Scan radius: x{_rules.watcherScanRadiusMultiplier:F2} (wider)");
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Predator", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Laser cooldown: x{_rules.predatorLaserCooldownMultiplier:F2} (faster)");
EditorGUILayout.LabelField($" Laser radius: x{_rules.predatorLaserRadiusMultiplier:F2} (wider)");
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Trapper", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Trap cost: x{_rules.trapperDreadCostMultiplier:F2} (cheaper)");
EditorGUILayout.LabelField($" Max active bonus: +{_rules.trapperMaxActiveBonus}");
}
EditorGUILayout.Space(10);
_showWearables = EditorGUILayout.Foldout(_showWearables, "Wearable Balance", true);
if (_showWearables)
{
EditorGUILayout.LabelField($" Max slots: {_rules.maxWearableSlots}");
EditorGUILayout.LabelField($" Dash cooldown: {_rules.dashCooldown:F1}s");
EditorGUILayout.LabelField($" Air Dash cooldown: {_rules.airDashCooldown:F1}s");
EditorGUILayout.LabelField($" Grapple cooldown: {_rules.grappleCooldown:F1}s");
EditorGUILayout.LabelField($" Throw range: {_rules.throwRangeBase:F1}m (weight scale: {_rules.throwRangeWeightScale:F2})");
}
EditorGUILayout.Space(10);
_showCoop = EditorGUILayout.Foldout(_showCoop, "Co-op Balance", true);
if (_showCoop)
{
EditorGUILayout.LabelField($" Boost height: {_rules.boostHeight:F1}m (range: {_rules.boostRange:F1}m)");
EditorGUILayout.LabelField($" Linked Sprint bonus: +{_rules.linkedSprintBonus:P0} speed");
EditorGUILayout.LabelField($" Linked Sprint radius: {_rules.linkedSprintRadius:F1}m");
EditorGUILayout.LabelField($" Linked Sprint slow on break: {_rules.linkedSprintSlowDuration:F1}s");
}
EditorGUILayout.Space(10);
_showExtraction = EditorGUILayout.Foldout(_showExtraction, "Extraction Room", true);
if (_showExtraction)
{
EditorGUILayout.LabelField($" Channel duration: {_rules.extractionChannelDuration:F0}s (all runners present)");
EditorGUILayout.LabelField($" Anti-AFK ritual: {_rules.antiAfkRitualDuration:F0}s");
EditorGUILayout.LabelField($" Min voters to start ritual: {_rules.antiAfkMinVoters}");
}
} }
private void DrawTierRow(string name, float min, float max, float speedMult) private void DrawTierRow(string name, float min, float max, float speedMult)
......
...@@ -9,6 +9,7 @@ namespace IStalk.Editor.Inspectors ...@@ -9,6 +9,7 @@ namespace IStalk.Editor.Inspectors
public class ItemDataEditor : UnityEditor.Editor public class ItemDataEditor : UnityEditor.Editor
{ {
private ItemData_SO _item; private ItemData_SO _item;
private bool _showDesignerPanel = true;
private void OnEnable() private void OnEnable()
{ {
...@@ -19,50 +20,139 @@ namespace IStalk.Editor.Inspectors ...@@ -19,50 +20,139 @@ namespace IStalk.Editor.Inspectors
{ {
DrawDefaultInspector(); DrawDefaultInspector();
EditorGUILayout.Space(10); EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); _showDesignerPanel = EditorGUILayout.Foldout(_showDesignerPanel, "Designer Panel", true);
if (!_showDesignerPanel) return;
DrawIconPreview();
DrawBalanceInfo();
DrawComparison();
DrawQuickActions();
DrawValidation();
}
private void DrawIconPreview()
{
if (_item.icon != null) if (_item.icon != null)
{ {
Rect rect = GUILayoutUtility.GetRect(64, 64); EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
Rect rect = GUILayoutUtility.GetRect(80, 80);
EditorGUI.DrawPreviewTexture(rect, _item.icon.texture); EditorGUI.DrawPreviewTexture(rect, _item.icon.texture);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
} }
}
EditorGUILayout.Space(); private void DrawBalanceInfo()
EditorGUILayout.LabelField("Computed Values", EditorStyles.boldLabel); {
var rules = FindRules(); var rules = FindRules();
if (rules != null) if (rules == null)
{
EditorGUILayout.HelpBox("Create a GlobalRules_SO to see balance info.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Impact Analysis", EditorStyles.boldLabel);
WeightTier tier = rules.GetTier(_item.weight);
float speedMult = rules.GetSpeedMultiplier(tier);
float ratio = _item.weight > 0 ? (float)_item.lootValue / _item.weight : 0;
EditorGUILayout.LabelField($" Weight Tier (alone): {tier}");
EditorGUILayout.LabelField($" Speed when carrying: {rules.baseRunSpeed * speedMult:F1} m/s ({speedMult:P0})");
EditorGUILayout.LabelField($" Value/Weight Ratio: {ratio:F2}");
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Economy Context", EditorStyles.boldLabel);
float pctOfThreshold = (float)_item.lootValue / rules.extractionThreshold * 100f;
EditorGUILayout.LabelField($" Extraction contribution: {pctOfThreshold:F0}% of threshold ({_item.lootValue}/{rules.extractionThreshold})");
float channelTime = rules.GetChannelTime(_item.weight);
EditorGUILayout.LabelField($" Channel time carrying this: {channelTime:F1}s");
Color barColor = ratio > 2f ? Color.green : ratio > 1f ? Color.yellow : Color.red;
string riskLabel = ratio > 2f ? "High reward" : ratio > 1f ? "Balanced" : "Risky (heavy for value)";
var style = new GUIStyle(EditorStyles.label) { richText = true };
string hex = ColorUtility.ToHtmlStringRGB(barColor);
EditorGUILayout.LabelField($" Risk/Reward: <color=#{hex}>{riskLabel}</color>", style);
}
private void DrawComparison()
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Loot Pool Context", EditorStyles.boldLabel);
var allItems = Resources.LoadAll<ItemData_SO>("Items/Loot");
if (allItems.Length <= 1)
{ {
WeightTier tier = rules.GetTier(_item.weight); EditorGUILayout.LabelField(" (Create more items to see comparisons)");
float speedMult = rules.GetSpeedMultiplier(tier); return;
EditorGUILayout.LabelField($"Weight Tier (alone): {tier}");
EditorGUILayout.LabelField($"Speed Multiplier (alone): {speedMult:P0}");
EditorGUILayout.LabelField($"Value/Weight Ratio: {(_item.weight > 0 ? _item.lootValue / _item.weight : 0):F2}");
} }
EditorGUILayout.Space(); float avgWeight = 0, avgValue = 0;
if (GUILayout.Button("Generate ID from Name")) foreach (var item in allItems) { avgWeight += item.weight; avgValue += item.lootValue; }
avgWeight /= allItems.Length;
avgValue /= allItems.Length;
EditorGUILayout.LabelField($" This item: {_item.weight:F1}kg / {_item.lootValue} value");
EditorGUILayout.LabelField($" Pool average: {avgWeight:F1}kg / {avgValue:F1} value ({allItems.Length} items total)");
if (_item.weight > avgWeight * 1.5f)
EditorGUILayout.LabelField(" → Heavier than average (consider higher value)");
if (_item.lootValue > avgValue * 2f)
EditorGUILayout.LabelField(" → Very high value (rare spawn weight recommended)");
}
private void DrawQuickActions()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Generate ID"))
{ {
Undo.RecordObject(_item, "Generate Item ID"); Undo.RecordObject(_item, "Generate Item ID");
_item.itemId = _item.name.Replace(" ", "_").ToLower(); _item.itemId = _item.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_item); EditorUtility.SetDirty(_item);
} }
if (GUILayout.Button("Preset: Common"))
DrawValidation(); {
Undo.RecordObject(_item, "Preset Common");
_item.weight = 1; _item.lootValue = 1; _item.rarity = Rarity.Common;
EditorUtility.SetDirty(_item);
}
if (GUILayout.Button("Preset: Rare"))
{
Undo.RecordObject(_item, "Preset Rare");
_item.weight = 4; _item.lootValue = 7; _item.rarity = Rarity.Rare;
EditorUtility.SetDirty(_item);
}
if (GUILayout.Button("Preset: Legendary"))
{
Undo.RecordObject(_item, "Preset Legendary");
_item.weight = 7; _item.lootValue = 12; _item.rarity = Rarity.Legendary;
EditorUtility.SetDirty(_item);
}
EditorGUILayout.EndHorizontal();
} }
private void DrawValidation() private void DrawValidation()
{ {
EditorGUILayout.Space(); EditorGUILayout.Space(10);
bool hasIssues = false;
if (string.IsNullOrEmpty(_item.itemId)) if (string.IsNullOrEmpty(_item.itemId))
EditorGUILayout.HelpBox("Item ID is empty.", MessageType.Error); { EditorGUILayout.HelpBox("Item ID is empty. Click 'Generate ID'.", MessageType.Error); hasIssues = true; }
if (_item.worldPrefab == null) if (_item.worldPrefab == null)
EditorGUILayout.HelpBox("No world prefab assigned.", MessageType.Warning); { EditorGUILayout.HelpBox("No world prefab assigned (needed for spawning in-world).", MessageType.Warning); hasIssues = true; }
if (_item.lootValue <= 0) if (_item.lootValue <= 0)
EditorGUILayout.HelpBox("Loot value is 0 or negative.", MessageType.Warning); { EditorGUILayout.HelpBox("Loot value is 0 — item won't contribute to extraction.", MessageType.Warning); hasIssues = true; }
if (_item.weight <= 0) if (_item.weight <= 0)
EditorGUILayout.HelpBox("Weight is 0 or negative.", MessageType.Warning); { EditorGUILayout.HelpBox("Weight is 0 — item won't slow runner at all.", MessageType.Warning); hasIssues = true; }
if (!hasIssues)
EditorGUILayout.HelpBox("All checks pass. Item is ready.", MessageType.Info);
} }
private GlobalRules_SO FindRules() private GlobalRules_SO FindRules()
......
...@@ -9,6 +9,7 @@ namespace IStalk.Editor.Inspectors ...@@ -9,6 +9,7 @@ namespace IStalk.Editor.Inspectors
public class TrapDataEditor : UnityEditor.Editor public class TrapDataEditor : UnityEditor.Editor
{ {
private TrapData_SO _trap; private TrapData_SO _trap;
private bool _showDesignerPanel = true;
private void OnEnable() private void OnEnable()
{ {
...@@ -19,33 +20,105 @@ namespace IStalk.Editor.Inspectors ...@@ -19,33 +20,105 @@ namespace IStalk.Editor.Inspectors
{ {
DrawDefaultInspector(); DrawDefaultInspector();
EditorGUILayout.Space(10); EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Balance Preview", EditorStyles.boldLabel); _showDesignerPanel = EditorGUILayout.Foldout(_showDesignerPanel, "Designer Panel", true);
if (!_showDesignerPanel) return;
DrawBalancePreview();
DrawEyeTypeContext();
DrawQuickActions();
DrawValidation();
}
private void DrawBalancePreview()
{
EditorGUILayout.LabelField("Balance Analysis", EditorStyles.boldLabel);
float costPerSec = _trap.effectDuration > 0 ? _trap.dreadCost / _trap.effectDuration : 0;
float area = Mathf.PI * _trap.effectRadius * _trap.effectRadius;
EditorGUILayout.LabelField($"Cost/Duration ratio: {(_trap.effectDuration > 0 ? _trap.dreadCost / _trap.effectDuration : 0):F1} dread/sec"); EditorGUILayout.LabelField($" Dread cost: {_trap.dreadCost}");
EditorGUILayout.LabelField($"Area coverage: {Mathf.PI * _trap.effectRadius * _trap.effectRadius:F1}m²"); EditorGUILayout.LabelField($" Effect duration: {_trap.effectDuration:F1}s");
EditorGUILayout.LabelField($"Trigger-to-arm delay: {_trap.armDelay:F1}s"); EditorGUILayout.LabelField($" Cost efficiency: {costPerSec:F1} dread/sec");
EditorGUILayout.LabelField($" Area coverage: {area:F1}m² (radius {_trap.effectRadius:F1}m)");
EditorGUILayout.LabelField($" Arm delay: {_trap.armDelay:F1}s (time before trap is active)");
EditorGUILayout.LabelField($" Max active per match: {_trap.maxActivePerMatch}");
EditorGUILayout.Space(); var rules = FindRules();
if (GUILayout.Button("Generate ID from Name")) if (rules != null)
{
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Economy Context", EditorStyles.boldLabel);
float regenTime = _trap.dreadCost / rules.dreadBaseRegenRate;
EditorGUILayout.LabelField($" Time to regen cost: {regenTime:F1}s (passive only)");
EditorGUILayout.LabelField($" % of full dread pool: ~{_trap.dreadCost}% (pool varies by type)");
}
}
private void DrawEyeTypeContext()
{
var rules = FindRules();
if (rules == null) return;
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Per Eye Type Cost", EditorStyles.boldLabel);
float trapperCost = Mathf.RoundToInt(_trap.dreadCost * rules.trapperDreadCostMultiplier);
EditorGUILayout.LabelField($" Default Eye: {_trap.dreadCost} dread");
EditorGUILayout.LabelField($" Trapper Eye: {trapperCost} dread (x{rules.trapperDreadCostMultiplier:F2})");
EditorGUILayout.LabelField($" Trapper max active bonus: +{rules.trapperMaxActiveBonus}");
}
private void DrawQuickActions()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Generate ID"))
{ {
Undo.RecordObject(_trap, "Generate Trap ID"); Undo.RecordObject(_trap, "Generate Trap ID");
_trap.trapId = _trap.name.Replace(" ", "_").ToLower(); _trap.trapId = _trap.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_trap); EditorUtility.SetDirty(_trap);
} }
if (GUILayout.Button("Preset: Cheap/Small"))
DrawValidation(); {
Undo.RecordObject(_trap, "Preset Cheap");
_trap.dreadCost = 10; _trap.effectRadius = 2f; _trap.effectDuration = 999f; _trap.armDelay = 0.5f;
EditorUtility.SetDirty(_trap);
}
if (GUILayout.Button("Preset: Expensive/Large"))
{
Undo.RecordObject(_trap, "Preset Expensive");
_trap.dreadCost = 30; _trap.effectRadius = 5f; _trap.effectDuration = 20f; _trap.armDelay = 1f;
EditorUtility.SetDirty(_trap);
}
EditorGUILayout.EndHorizontal();
} }
private void DrawValidation() private void DrawValidation()
{ {
EditorGUILayout.Space(); EditorGUILayout.Space(10);
bool hasIssues = false;
if (string.IsNullOrEmpty(_trap.trapId)) if (string.IsNullOrEmpty(_trap.trapId))
EditorGUILayout.HelpBox("Trap ID is empty.", MessageType.Error); { EditorGUILayout.HelpBox("Trap ID is empty. Click 'Generate ID'.", MessageType.Error); hasIssues = true; }
if (_trap.placedPrefab == null) if (_trap.placedPrefab == null)
EditorGUILayout.HelpBox("No placed prefab assigned.", MessageType.Warning); { EditorGUILayout.HelpBox("No placed prefab assigned (needed for spawning).", MessageType.Warning); hasIssues = true; }
if (_trap.dreadCost <= 0) if (_trap.dreadCost <= 0)
EditorGUILayout.HelpBox("Dread cost is 0.", MessageType.Warning); { EditorGUILayout.HelpBox("Dread cost is 0 — Eye can spam this for free!", MessageType.Error); hasIssues = true; }
if (_trap.effectRadius <= 0)
{ EditorGUILayout.HelpBox("Effect radius is 0 — trap has no area.", MessageType.Warning); hasIssues = true; }
if (!hasIssues)
EditorGUILayout.HelpBox("All checks pass. Trap is ready.", MessageType.Info);
}
private GlobalRules_SO FindRules()
{
if (GlobalRules_SO.Instance != null) return GlobalRules_SO.Instance;
string[] guids = AssetDatabase.FindAssets("t:GlobalRules_SO");
if (guids.Length == 0) return null;
return AssetDatabase.LoadAssetAtPath<GlobalRules_SO>(AssetDatabase.GUIDToAssetPath(guids[0]));
} }
} }
...@@ -53,6 +126,7 @@ namespace IStalk.Editor.Inspectors ...@@ -53,6 +126,7 @@ namespace IStalk.Editor.Inspectors
public class ShrineItemDataEditor : UnityEditor.Editor public class ShrineItemDataEditor : UnityEditor.Editor
{ {
private ShrineItemData_SO _item; private ShrineItemData_SO _item;
private bool _showDesignerPanel = true;
private void OnEnable() private void OnEnable()
{ {
...@@ -63,24 +137,126 @@ namespace IStalk.Editor.Inspectors ...@@ -63,24 +137,126 @@ namespace IStalk.Editor.Inspectors
{ {
DrawDefaultInspector(); DrawDefaultInspector();
EditorGUILayout.Space(10); EditorGUILayout.Space(15);
EditorGUILayout.LabelField("Balance Preview", EditorStyles.boldLabel); _showDesignerPanel = EditorGUILayout.Foldout(_showDesignerPanel, "Designer Panel", true);
if (!_showDesignerPanel) return;
DrawBalancePreview();
DrawUsagePattern();
DrawQuickActions();
DrawValidation();
}
private void DrawBalancePreview()
{
EditorGUILayout.LabelField("Balance Analysis", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Loot cost: {_item.lootCost}");
EditorGUILayout.LabelField($" Net weight change: {(_item.weightDelta >= 0 ? "+" : "")}{_item.weightDelta:F1}kg");
if (_item.effectDuration > 0)
{
float costPerSec = (float)_item.lootCost / _item.effectDuration;
EditorGUILayout.LabelField($" Cost per second: {costPerSec:F2} loot/sec");
EditorGUILayout.LabelField($" Effect duration: {_item.effectDuration:F1}s");
}
else
{
EditorGUILayout.LabelField($" Instant effect (no duration)");
}
EditorGUILayout.LabelField($"Net weight change: {(_item.weightDelta >= 0 ? "+" : "")}{_item.weightDelta:F1}kg");
EditorGUILayout.LabelField($"Cost/Effect ratio: {(_item.effectDuration > 0 ? _item.lootCost / _item.effectDuration : 0):F1}");
if (_item.cooldown > 0) if (_item.cooldown > 0)
EditorGUILayout.LabelField($"Uptime: {_item.effectDuration / (_item.effectDuration + _item.cooldown):P0}"); {
float uptime = _item.effectDuration / (_item.effectDuration + _item.cooldown);
EditorGUILayout.LabelField($" Cooldown: {_item.cooldown:F1}s");
EditorGUILayout.LabelField($" Uptime: {uptime:P0}");
}
EditorGUILayout.Space(); EditorGUILayout.LabelField($" Consumed on use: {(_item.isConsumedOnUse ? "YES (one-shot)" : "No (reusable)")}");
if (GUILayout.Button("Generate ID from Name")) }
private void DrawUsagePattern()
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Usage Pattern", EditorStyles.boldLabel);
string pattern = _item.itemType switch
{
ShrineItemType.SmokeBomb => "Area denial — blocks Eye LoS in zone",
ShrineItemType.Mirror => "Stealth — runner invisible to Eye",
ShrineItemType.Decoy => "Misdirection — fake blip on Eye's screen",
ShrineItemType.OcularPaint => "Aggression — blinds Eye in area",
ShrineItemType.WeightReducer => "Economy — shed weight instantly",
ShrineItemType.GrapplingHook => "Mobility — reach high/far places",
ShrineItemType.FlashOrb => "Aggression — blinds Eye (dodgeable with eyelids)",
ShrineItemType.LinkedSprintTotem => "Co-op — speed buff when runners are close",
ShrineItemType.SilenceShroud => "Stealth — suppresses noise for duration",
ShrineItemType.VitalityLink => "Co-op — shared health/buff aura",
_ => "Unknown effect"
};
EditorGUILayout.LabelField($" {pattern}");
var rules = FindRules();
if (rules != null && _item.lootCost > 0)
{
float pctOfThreshold = (float)_item.lootCost / rules.extractionThreshold * 100f;
EditorGUILayout.LabelField($" Cost as % of extraction threshold: {pctOfThreshold:F0}%");
}
}
private void DrawQuickActions()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Generate ID"))
{ {
Undo.RecordObject(_item, "Generate Shrine Item ID"); Undo.RecordObject(_item, "Generate Shrine Item ID");
_item.itemId = _item.name.Replace(" ", "_").ToLower(); _item.itemId = _item.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_item); EditorUtility.SetDirty(_item);
} }
if (GUILayout.Button("Make Consumable"))
{
Undo.RecordObject(_item, "Make Consumable");
_item.isConsumedOnUse = true;
_item.cooldown = 0;
EditorUtility.SetDirty(_item);
}
if (GUILayout.Button("Make Reusable"))
{
Undo.RecordObject(_item, "Make Reusable");
_item.isConsumedOnUse = false;
if (_item.cooldown <= 0) _item.cooldown = 15f;
EditorUtility.SetDirty(_item);
}
EditorGUILayout.EndHorizontal();
}
private void DrawValidation()
{
EditorGUILayout.Space(10);
bool hasIssues = false;
if (string.IsNullOrEmpty(_item.itemId)) if (string.IsNullOrEmpty(_item.itemId))
EditorGUILayout.HelpBox("Item ID is empty.", MessageType.Error); { EditorGUILayout.HelpBox("Item ID is empty. Click 'Generate ID'.", MessageType.Error); hasIssues = true; }
if (_item.lootCost <= 0)
{ EditorGUILayout.HelpBox("Loot cost is 0 — item is free at shrine!", MessageType.Warning); hasIssues = true; }
if (!_item.isConsumedOnUse && _item.cooldown <= 0)
{ EditorGUILayout.HelpBox("Reusable with no cooldown — runner can spam this!", MessageType.Error); hasIssues = true; }
if (_item.isConsumedOnUse && _item.cooldown > 0)
{ EditorGUILayout.HelpBox("Consumed AND has cooldown — cooldown is pointless since item is destroyed.", MessageType.Warning); hasIssues = true; }
if (!hasIssues)
EditorGUILayout.HelpBox("All checks pass. Shrine item is ready.", MessageType.Info);
}
private GlobalRules_SO FindRules()
{
if (GlobalRules_SO.Instance != null) return GlobalRules_SO.Instance;
string[] guids = AssetDatabase.FindAssets("t:GlobalRules_SO");
if (guids.Length == 0) return null;
return AssetDatabase.LoadAssetAtPath<GlobalRules_SO>(AssetDatabase.GUIDToAssetPath(guids[0]));
} }
} }
} }
......
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using IStalk.Data;
using IStalk.Runner;
namespace IStalk.Editor.Inspectors
{
[CustomEditor(typeof(WearableData_SO))]
public class WearableDataEditor : UnityEditor.Editor
{
private WearableData_SO _wearable;
private bool _showBalancePreview = true;
private void OnEnable()
{
_wearable = (WearableData_SO)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(15);
if (_wearable.icon != null)
{
EditorGUILayout.LabelField("Icon Preview", EditorStyles.boldLabel);
Rect rect = GUILayoutUtility.GetRect(64, 64);
EditorGUI.DrawPreviewTexture(rect, _wearable.icon.texture);
EditorGUILayout.Space(5);
}
_showBalancePreview = EditorGUILayout.Foldout(_showBalancePreview, "Balance Preview", true);
if (_showBalancePreview)
{
EditorGUI.indentLevel++;
DrawBalancePreview();
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(10);
DrawQuickActions();
DrawValidation();
}
private void DrawBalancePreview()
{
var rules = FindRules();
EditorGUILayout.LabelField("Ability Info", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Type: {_wearable.abilityType}");
EditorGUILayout.LabelField($" Mode: {(_wearable.isPassive ? "Passive (always active while worn)" : "Active (cooldown-based)")}");
if (!_wearable.isPassive)
{
EditorGUILayout.LabelField($" Cooldown: {_wearable.cooldown:F1}s");
if (_wearable.cooldown > 0)
{
float uptime = 1f / (1f + _wearable.cooldown);
EditorGUILayout.LabelField($" Max activation rate: {60f / _wearable.cooldown:F1} uses/min");
}
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Movement Granted", EditorStyles.boldLabel);
string movementDesc = _wearable.abilityType switch
{
WearableAbilityType.Dash => "Ground dash burst (directional)",
WearableAbilityType.WallRun => "Run along vertical walls while sprinting",
WearableAbilityType.AirDash => "Mid-air directional dash",
WearableAbilityType.Slide => "Sprint + Crouch to slide under gaps",
WearableAbilityType.Vault => "Auto-vault waist-height obstacles",
WearableAbilityType.Grapple => "Hook to GrappleAnchor points, swing",
_ => "Unknown"
};
EditorGUILayout.LabelField($" {movementDesc}");
if (rules != null)
{
float cd = _wearable.abilityType switch
{
WearableAbilityType.Dash => rules.dashCooldown,
WearableAbilityType.AirDash => rules.airDashCooldown,
WearableAbilityType.Grapple => rules.grappleCooldown,
_ => 0
};
if (cd > 0)
EditorGUILayout.LabelField($" GlobalRules cooldown: {cd:F1}s");
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Economy", EditorStyles.boldLabel);
if (_wearable.lootCost > 0)
EditorGUILayout.LabelField($" Shrine cost: {_wearable.lootCost} loot");
else
EditorGUILayout.LabelField($" Not purchasable (world drop only)");
EditorGUILayout.LabelField($" Throw range: {_wearable.throwRange:F1}m");
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Slot Impact", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Occupies 1 of {(rules != null ? rules.maxWearableSlots : 2)} wearable slots");
EditorGUILayout.LabelField($" Weight: 0 (wearables are weightless)");
}
private void DrawQuickActions()
{
EditorGUILayout.LabelField("Quick Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Generate ID from Name"))
{
Undo.RecordObject(_wearable, "Generate Wearable ID");
_wearable.wearableId = _wearable.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_wearable);
}
if (GUILayout.Button("Set as Active Ability"))
{
Undo.RecordObject(_wearable, "Set Active Ability");
_wearable.isPassive = false;
if (_wearable.cooldown <= 0) _wearable.cooldown = 3f;
EditorUtility.SetDirty(_wearable);
}
if (GUILayout.Button("Set as Passive Ability"))
{
Undo.RecordObject(_wearable, "Set Passive Ability");
_wearable.isPassive = true;
_wearable.cooldown = 0;
EditorUtility.SetDirty(_wearable);
}
EditorGUILayout.EndHorizontal();
}
private void DrawValidation()
{
EditorGUILayout.Space(10);
if (string.IsNullOrEmpty(_wearable.wearableId))
EditorGUILayout.HelpBox("Wearable ID is empty. Click 'Generate ID from Name'.", MessageType.Error);
if (string.IsNullOrEmpty(_wearable.displayName))
EditorGUILayout.HelpBox("Display Name is empty.", MessageType.Warning);
if (_wearable.worldPrefab == null)
EditorGUILayout.HelpBox("No world prefab assigned (needed for drops).", MessageType.Warning);
if (!_wearable.isPassive && _wearable.cooldown <= 0)
EditorGUILayout.HelpBox("Active ability with 0 cooldown — intended?", MessageType.Warning);
if (_wearable.lootCost <= 0 && _wearable.worldPrefab == null)
EditorGUILayout.HelpBox("No purchase cost AND no world prefab. How will players get this?", MessageType.Error);
}
private GlobalRules_SO FindRules()
{
if (GlobalRules_SO.Instance != null) return GlobalRules_SO.Instance;
string[] guids = AssetDatabase.FindAssets("t:GlobalRules_SO");
if (guids.Length == 0) return null;
return AssetDatabase.LoadAssetAtPath<GlobalRules_SO>(AssetDatabase.GUIDToAssetPath(guids[0]));
}
}
}
#endif
fileFormatVersion: 2
guid: 6cf33eacfa7ea4d3db82832cc7702a23
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using IStalk.Core; using IStalk.Core;
using IStalk.Data; using IStalk.Data;
using IStalk.Network; using IStalk.Network;
...@@ -14,43 +15,71 @@ namespace IStalk.Editor.MapGen ...@@ -14,43 +15,71 @@ namespace IStalk.Editor.MapGen
private int _testCount = 100; private int _testCount = 100;
private string _lastResult; private string _lastResult;
private Vector2 _scrollPos; private Vector2 _scrollPos;
private bool _lastTestPassed;
[MenuItem("IStalk/Map Generation Tester")] [MenuItem("IStalk/Map Generation Tester")]
public static void ShowWindow() public static void ShowWindow()
{ {
GetWindow<MapGenTestWindow>("Map Gen Tester"); var window = GetWindow<MapGenTestWindow>("Map Gen Tester");
window.minSize = new Vector2(380, 500);
} }
private void OnGUI() private void OnGUI()
{ {
EditorGUILayout.LabelField("Map Generation Tester", EditorStyles.boldLabel); EditorGUILayout.LabelField("Map Generation Tester", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("Test procedural map generation without entering Play mode. Use to validate room pool and GlobalRules settings.", MessageType.None);
EditorGUILayout.Space(); EditorGUILayout.Space();
DrawPreflightCheck();
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Single Generation", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_seed = EditorGUILayout.IntField("Seed", _seed); _seed = EditorGUILayout.IntField("Seed", _seed);
if (GUILayout.Button("Randomize Seed")) if (GUILayout.Button("Random", GUILayout.Width(60)))
_seed = Random.Range(int.MinValue, int.MaxValue); _seed = Random.Range(int.MinValue, int.MaxValue);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(); if (GUILayout.Button("Generate Single Map (Preview)", GUILayout.Height(28)))
if (GUILayout.Button("Generate Single Map (Preview)"))
GenerateSingle(); GenerateSingle();
EditorGUILayout.Space(); EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Stress Test", EditorStyles.boldLabel); EditorGUILayout.LabelField("Stress Test", EditorStyles.boldLabel);
_testCount = EditorGUILayout.IntSlider("Iterations", _testCount, 10, 1000); _testCount = EditorGUILayout.IntSlider("Iterations", _testCount, 10, 1000);
if (GUILayout.Button($"Run {_testCount} Generations")) if (GUILayout.Button($"Run {_testCount} Generations (target: 0 failures, <10ms avg)", GUILayout.Height(28)))
StressTest(); StressTest();
if (!string.IsNullOrEmpty(_lastResult)) if (!string.IsNullOrEmpty(_lastResult))
{ {
EditorGUILayout.Space(); EditorGUILayout.Space(10);
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.Height(200)); _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.MinHeight(180));
EditorGUILayout.TextArea(_lastResult, GUILayout.ExpandHeight(true)); EditorGUILayout.TextArea(_lastResult, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView(); EditorGUILayout.EndScrollView();
} }
} }
private void DrawPreflightCheck()
{
string[] rulesGuids = AssetDatabase.FindAssets("t:GlobalRules_SO");
var rooms = Resources.LoadAll<RoomNode_SO>("Items/Rooms");
if (rulesGuids.Length == 0)
{
EditorGUILayout.HelpBox("No GlobalRules_SO found! Create one first: IStalk > Create > Global Rules SO", MessageType.Error);
return;
}
if (rooms.Length == 0)
{
EditorGUILayout.HelpBox("No RoomNode_SO assets found in Resources/Items/Rooms/. Create rooms first.", MessageType.Error);
return;
}
int withPrefab = rooms.Count(r => r.roomPrefab != null);
EditorGUILayout.LabelField($" Room pool: {rooms.Length} rooms ({withPrefab} with prefabs assigned)");
}
private void GenerateSingle() private void GenerateSingle()
{ {
var rules = FindGlobalRules(); var rules = FindGlobalRules();
...@@ -66,12 +95,17 @@ namespace IStalk.Editor.MapGen ...@@ -66,12 +95,17 @@ namespace IStalk.Editor.MapGen
gen.Generate(); gen.Generate();
sw.Stop(); sw.Stop();
_lastResult = $"Seed: {_seed}\n" + bool roomCountOk = gen.PlacedRooms.Count >= rules.minRoomsPerMap && gen.PlacedRooms.Count <= rules.maxRoomsPerMap;
$"Rooms placed: {gen.PlacedRooms.Count}\n" + bool altarCountOk = gen.AltarPositions.Count >= rules.minAltars;
$"Altars: {gen.AltarPositions.Count}\n" + _lastTestPassed = roomCountOk && altarCountOk;
_lastResult = $"Single Map — Seed: {_seed}\n" +
$"─────────────────────────────\n" +
$"Rooms placed: {gen.PlacedRooms.Count} (target: {rules.minRoomsPerMap}{rules.maxRoomsPerMap}) {(roomCountOk ? "✓" : "✗")}\n" +
$"Altars: {gen.AltarPositions.Count} (target: {rules.minAltars}{rules.maxAltars}) {(altarCountOk ? "✓" : "✗")}\n" +
$"Loot slots: {gen.LootSlotPositions.Count}\n" + $"Loot slots: {gen.LootSlotPositions.Count}\n" +
$"Time: {sw.ElapsedMilliseconds}ms\n" + $"Generation time: {sw.ElapsedMilliseconds}ms\n" +
$"Bounds: {gen.GetMapBounds()}\n\n" + $"Map bounds: {gen.GetMapBounds()}\n\n" +
"Room breakdown:\n"; "Room breakdown:\n";
var categoryCounts = new System.Collections.Generic.Dictionary<RoomCategory, int>(); var categoryCounts = new System.Collections.Generic.Dictionary<RoomCategory, int>();
...@@ -84,6 +118,9 @@ namespace IStalk.Editor.MapGen ...@@ -84,6 +118,9 @@ namespace IStalk.Editor.MapGen
foreach (var kvp in categoryCounts) foreach (var kvp in categoryCounts)
_lastResult += $" {kvp.Key}: {kvp.Value}\n"; _lastResult += $" {kvp.Key}: {kvp.Value}\n";
_lastResult += $"\nVerdict: {(_lastTestPassed ? "PASS" : "ISSUES DETECTED — check room pool")}";
DestroyImmediate(go); DestroyImmediate(go);
Repaint(); Repaint();
} }
...@@ -127,13 +164,16 @@ namespace IStalk.Editor.MapGen ...@@ -127,13 +164,16 @@ namespace IStalk.Editor.MapGen
DestroyImmediate(go); DestroyImmediate(go);
_lastResult = $"Stress Test Results ({_testCount} iterations)\n" + _lastTestPassed = failures == 0;
string verdict = _lastTestPassed ? "PASS" : "FAIL";
_lastResult = $"Stress Test Results ({_testCount} iterations) — {verdict}\n" +
$"─────────────────────────────\n" + $"─────────────────────────────\n" +
$"Failures: {failures}/{_testCount}\n" + $"Failures: {failures}/{_testCount}\n" +
$"Avg time: {totalMs / _testCount}ms\n" + $"Avg time: {totalMs / _testCount}ms\n" +
$"Max time: {totalMs}ms total\n" + $"Total time: {totalMs}ms\n" +
$"Room range: {minRooms}{maxRooms}\n" + $"Room range: {minRooms}{maxRooms}\n" +
$"Success rate: {((_testCount - failures) * 100f / _testCount):F1}%"; $"Success rate: {((_testCount - failures) * 100f / _testCount):F1}%\n\n" +
(failures > 0 ? "Fix: Check room connector directions and GlobalRules min/max room counts." : "All maps generated successfully.");
Repaint(); Repaint();
} }
......
...@@ -12,6 +12,7 @@ namespace IStalk.Editor.MapGen ...@@ -12,6 +12,7 @@ namespace IStalk.Editor.MapGen
{ {
private RoomNode_SO _room; private RoomNode_SO _room;
private bool _showValidation = true; private bool _showValidation = true;
private bool _showDesignerPanel = true;
private void OnEnable() private void OnEnable()
{ {
...@@ -22,17 +23,83 @@ namespace IStalk.Editor.MapGen ...@@ -22,17 +23,83 @@ namespace IStalk.Editor.MapGen
{ {
DrawDefaultInspector(); DrawDefaultInspector();
EditorGUILayout.Space(15);
_showDesignerPanel = EditorGUILayout.Foldout(_showDesignerPanel, "Designer Panel", true);
if (!_showDesignerPanel) return;
DrawRoomSummary();
DrawMapGenContext();
DrawRoomTools();
DrawValidationResults();
}
private void DrawRoomSummary()
{
EditorGUILayout.LabelField("Room Summary", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Category: {_room.category}");
EditorGUILayout.LabelField($" Grid Size: {_room.gridSize} ({_room.gridSize.x * _room.gridSize.z} cells)");
EditorGUILayout.LabelField($" Connections: {_room.openConnections.Count} ({string.Join(", ", _room.openConnections)})");
EditorGUILayout.Space(3);
EditorGUILayout.LabelField("Content Slots", EditorStyles.boldLabel);
EditorGUILayout.LabelField($" Loot: {_room.lootSpawnSlots}");
EditorGUILayout.LabelField($" Incarnation Props: {_room.incarnationPropSlots}");
EditorGUILayout.LabelField($" Runner Spawn: {(_room.isValidRunnerSpawn ? "Yes" : "No")}");
EditorGUILayout.LabelField($" Altar Location: {(_room.isValidAltarLocation ? "Yes" : "No")}");
EditorGUILayout.LabelField($" Shrine: {(_room.canContainShrine ? "Yes" : "No")}");
}
private void DrawMapGenContext()
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Map Generation Context", EditorStyles.boldLabel);
var allRooms = Resources.LoadAll<RoomNode_SO>("Items/Rooms");
if (allRooms.Length == 0) return;
int sameCategory = allRooms.Count(r => r.category == _room.category);
float totalWeight = allRooms.Where(r => r.category == _room.category).Sum(r => r.spawnWeight);
float myChance = totalWeight > 0 ? (_room.spawnWeight / totalWeight) * 100f : 0;
EditorGUILayout.LabelField($" Rooms in '{_room.category}' pool: {sameCategory}");
EditorGUILayout.LabelField($" Spawn weight: {_room.spawnWeight} ({myChance:F1}% chance when picking from pool)");
EditorGUILayout.LabelField($" Max per map: {(_room.maxPerMap <= 0 ? "Unlimited" : _room.maxPerMap.ToString())}");
if (_room.openConnections.Count <= 1)
EditorGUILayout.HelpBox("Only 1 connection → dead-end room. Good for special rooms, limits map flow.", MessageType.Info);
else if (_room.openConnections.Count >= 4)
EditorGUILayout.HelpBox("4+ connections → hub room. Creates branching paths, great for large rooms.", MessageType.Info);
}
private void DrawRoomTools()
{
EditorGUILayout.Space(10); EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Room Tools", EditorStyles.boldLabel); EditorGUILayout.LabelField("Room Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Auto-Populate from Prefab")) if (GUILayout.Button("Auto-Populate from Prefab"))
AutoPopulate(); AutoPopulate();
if (GUILayout.Button("Generate ID"))
{
Undo.RecordObject(_room, "Generate Room ID");
_room.roomId = _room.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_room);
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Validate Room")) EditorGUILayout.BeginHorizontal();
ValidateRoom(); if (GUILayout.Button("Select Prefab"))
{
if (_showValidation) if (_room.roomPrefab != null)
DrawValidationResults(); Selection.activeObject = _room.roomPrefab;
}
if (GUILayout.Button("Open Prefab"))
{
if (_room.roomPrefab != null)
AssetDatabase.OpenAsset(_room.roomPrefab);
}
EditorGUILayout.EndHorizontal();
} }
private void AutoPopulate() private void AutoPopulate()
...@@ -66,38 +133,45 @@ namespace IStalk.Editor.MapGen ...@@ -66,38 +133,45 @@ namespace IStalk.Editor.MapGen
_room.isValidAltarLocation = slots.Any(s => s.slotType == SpawnSlotType.AltarLocation); _room.isValidAltarLocation = slots.Any(s => s.slotType == SpawnSlotType.AltarLocation);
_room.canContainShrine = slots.Any(s => s.slotType == SpawnSlotType.Shrine); _room.canContainShrine = slots.Any(s => s.slotType == SpawnSlotType.Shrine);
if (string.IsNullOrEmpty(_room.roomId))
_room.roomId = _room.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(_room); EditorUtility.SetDirty(_room);
Debug.Log($"[RoomNodeEditor] Auto-populated: {connectors.Length} connectors, {slots.Length} slots, grid {_room.gridSize}"); Debug.Log($"[RoomNodeEditor] Auto-populated: {connectors.Length} connectors, {slots.Length} slots, grid {_room.gridSize}");
} }
private void ValidateRoom()
{
_showValidation = true;
Repaint();
}
private void DrawValidationResults() private void DrawValidationResults()
{ {
EditorGUILayout.Space(5); EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Validation", EditorStyles.boldLabel); EditorGUILayout.LabelField("Validation", EditorStyles.boldLabel);
bool valid = true; bool valid = true;
if (string.IsNullOrEmpty(_room.roomId)) if (string.IsNullOrEmpty(_room.roomId))
{ {
EditorGUILayout.HelpBox("Room ID is empty.", MessageType.Error); EditorGUILayout.HelpBox("Room ID is empty. Click 'Generate ID'.", MessageType.Error);
valid = false; valid = false;
} }
if (_room.roomPrefab == null) if (_room.roomPrefab == null)
{ {
EditorGUILayout.HelpBox("No prefab assigned.", MessageType.Error); EditorGUILayout.HelpBox("No prefab assigned. Drag a room prefab here.", MessageType.Error);
valid = false; valid = false;
} }
else
{
var connectors = _room.roomPrefab.GetComponentsInChildren<ConnectorMarker>(true);
if (connectors.Length == 0)
EditorGUILayout.HelpBox("Prefab has no ConnectorMarker components — room can't connect to anything.", MessageType.Error);
var bounds = _room.roomPrefab.GetComponentInChildren<RoomBounds>(true);
if (bounds == null)
EditorGUILayout.HelpBox("Prefab has no RoomBounds — MapGenerator can't size it.", MessageType.Warning);
}
if (_room.openConnections.Count == 0) if (_room.openConnections.Count == 0)
{ {
EditorGUILayout.HelpBox("No connections defined — room will be isolated.", MessageType.Warning); EditorGUILayout.HelpBox("No connections defined — click 'Auto-Populate' to read from prefab.", MessageType.Warning);
} }
if (_room.gridSize.x <= 0 || _room.gridSize.y <= 0 || _room.gridSize.z <= 0) if (_room.gridSize.x <= 0 || _room.gridSize.y <= 0 || _room.gridSize.z <= 0)
...@@ -108,13 +182,16 @@ namespace IStalk.Editor.MapGen ...@@ -108,13 +182,16 @@ namespace IStalk.Editor.MapGen
if (_room.spawnWeight <= 0) if (_room.spawnWeight <= 0)
{ {
EditorGUILayout.HelpBox("Spawn weight is 0 — room will never appear.", MessageType.Warning); EditorGUILayout.HelpBox("Spawn weight is 0 — room will NEVER appear in generated maps.", MessageType.Warning);
} }
if (valid) if (_room.lootSpawnSlots == 0 && _room.category != RoomCategory.Corridor)
{ {
EditorGUILayout.HelpBox("Room passes all checks.", MessageType.Info); EditorGUILayout.HelpBox("No loot slots — runners can't find items here.", MessageType.Info);
} }
if (valid && _room.roomPrefab != null)
EditorGUILayout.HelpBox("Room passes all checks. Ready for map generation.", MessageType.Info);
} }
} }
} }
......
...@@ -12,29 +12,61 @@ namespace IStalk.Editor.MapGen ...@@ -12,29 +12,61 @@ namespace IStalk.Editor.MapGen
{ {
private Vector2 _scrollPos; private Vector2 _scrollPos;
private List<ValidationResult> _results = new(); private List<ValidationResult> _results = new();
private int _errorCount;
private int _warningCount;
private int _passCount;
[MenuItem("IStalk/Room Validator")] [MenuItem("IStalk/Room Validator")]
public static void ShowWindow() public static void ShowWindow()
{ {
GetWindow<RoomValidator>("Room Validator"); var window = GetWindow<RoomValidator>("Room Validator");
window.minSize = new Vector2(400, 350);
} }
private void OnGUI() private void OnGUI()
{ {
EditorGUILayout.LabelField("Room Validator", EditorStyles.boldLabel); EditorGUILayout.LabelField("Room Validator", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("Validates all RoomNode_SO assets against their assigned prefabs. Run after editing any room.", MessageType.None);
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Validate All Rooms")) if (GUILayout.Button("Validate All Rooms", GUILayout.Height(28)))
ValidateAll(); ValidateAll();
if (GUILayout.Button("Validate Selected Prefab")) if (GUILayout.Button("Validate Selected", GUILayout.Height(28)))
ValidateSelected(); ValidateSelected();
if (GUILayout.Button("Auto-Fix All IDs", GUILayout.Height(28)))
AutoFixIds();
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
if (_results.Count > 0)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField($"Results: {_errorCount} errors, {_warningCount} warnings, {_passCount} passed",
_errorCount > 0 ? EditorStyles.boldLabel : EditorStyles.label);
}
EditorGUILayout.Space(); EditorGUILayout.Space();
DrawResults(); DrawResults();
} }
private void AutoFixIds()
{
var rooms = Resources.LoadAll<RoomNode_SO>("Items/Rooms");
int fixed_ = 0;
foreach (var room in rooms)
{
if (string.IsNullOrEmpty(room.roomId))
{
Undo.RecordObject(room, "Auto-Fix Room ID");
room.roomId = room.name.Replace(" ", "_").ToLower();
EditorUtility.SetDirty(room);
fixed_++;
}
}
Debug.Log($"[RoomValidator] Auto-fixed {fixed_} room IDs.");
ValidateAll();
}
private void ValidateAll() private void ValidateAll()
{ {
_results.Clear(); _results.Clear();
...@@ -43,9 +75,10 @@ namespace IStalk.Editor.MapGen ...@@ -43,9 +75,10 @@ namespace IStalk.Editor.MapGen
foreach (var room in rooms) foreach (var room in rooms)
_results.AddRange(ValidateRoom(room)); _results.AddRange(ValidateRoom(room));
int errors = _results.Count(r => r.Severity == MessageType.Error); _errorCount = _results.Count(r => r.Severity == MessageType.Error);
int warnings = _results.Count(r => r.Severity == MessageType.Warning); _warningCount = _results.Count(r => r.Severity == MessageType.Warning);
Debug.Log($"[RoomValidator] Validated {rooms.Length} rooms: {errors} errors, {warnings} warnings"); _passCount = _results.Count(r => r.Severity == MessageType.Info);
Debug.Log($"[RoomValidator] Validated {rooms.Length} rooms: {_errorCount} errors, {_warningCount} warnings, {_passCount} passed");
} }
private void ValidateSelected() private void ValidateSelected()
...@@ -54,10 +87,13 @@ namespace IStalk.Editor.MapGen ...@@ -54,10 +87,13 @@ namespace IStalk.Editor.MapGen
var selected = Selection.activeObject as RoomNode_SO; var selected = Selection.activeObject as RoomNode_SO;
if (selected == null) if (selected == null)
{ {
EditorUtility.DisplayDialog("Room Validator", "Select a RoomNode_SO asset first.", "OK"); EditorUtility.DisplayDialog("Room Validator", "Select a RoomNode_SO asset in the Project window first.", "OK");
return; return;
} }
_results.AddRange(ValidateRoom(selected)); _results.AddRange(ValidateRoom(selected));
_errorCount = _results.Count(r => r.Severity == MessageType.Error);
_warningCount = _results.Count(r => r.Severity == MessageType.Warning);
_passCount = _results.Count(r => r.Severity == MessageType.Info);
} }
private List<ValidationResult> ValidateRoom(RoomNode_SO room) private List<ValidationResult> ValidateRoom(RoomNode_SO room)
......
...@@ -38,7 +38,8 @@ namespace IStalk.Eye ...@@ -38,7 +38,8 @@ namespace IStalk.Eye
{ {
if (!IsServerStarted) return; if (!IsServerStarted) return;
float regen = Rules.dreadBaseRegenRate + _noiseBonus; float baseRegen = _regenOverride >= 0 ? _regenOverride : Rules.dreadBaseRegenRate;
float regen = baseRegen + _noiseBonus;
_currentDread.Value = UnityEngine.Mathf.Min(_currentDread.Value + regen * UnityEngine.Time.deltaTime, _maxDread); _currentDread.Value = UnityEngine.Mathf.Min(_currentDread.Value + regen * UnityEngine.Time.deltaTime, _maxDread);
_noiseBonus *= 0.95f; _noiseBonus *= 0.95f;
} }
...@@ -56,5 +57,19 @@ namespace IStalk.Eye ...@@ -56,5 +57,19 @@ namespace IStalk.Eye
{ {
_noiseBonus += intensity * Rules.dreadNoiseBonusMultiplier; _noiseBonus += intensity * Rules.dreadNoiseBonusMultiplier;
} }
[Server]
public void ApplyStartOverride(float amount)
{
_currentDread.Value = amount;
}
[Server]
public void ApplyRegenOverride(float rate)
{
_regenOverride = rate;
}
private float _regenOverride = -1f;
} }
} }
using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
namespace IStalk.Eye
{
public class EyeEyelidController : NetworkBehaviour
{
private readonly SyncVar<bool> _eyelidsClosed = new();
private GameObject _blackOverlay;
public bool IsClosed => _eyelidsClosed.Value;
public override void OnStartClient()
{
base.OnStartClient();
_eyelidsClosed.OnChange += OnEyelidStateChanged;
if (!IsOwner)
{
enabled = false;
return;
}
CreateBlackOverlay();
}
private void Update()
{
if (!IsOwner) return;
if (Input.GetMouseButtonDown(1))
ServerSetEyelids(!_eyelidsClosed.Value);
}
[ServerRpc]
private void ServerSetEyelids(bool closed)
{
_eyelidsClosed.Value = closed;
}
private void OnEyelidStateChanged(bool prev, bool next, bool asServer)
{
if (IsOwner && _blackOverlay != null)
_blackOverlay.SetActive(next);
}
private void CreateBlackOverlay()
{
var canvas = new GameObject("EyelidOverlay");
var canvasComp = canvas.AddComponent<Canvas>();
canvasComp.renderMode = RenderMode.ScreenSpaceOverlay;
canvasComp.sortingOrder = 999;
var panel = new GameObject("BlackPanel");
panel.transform.SetParent(canvas.transform, false);
var img = panel.AddComponent<UnityEngine.UI.Image>();
img.color = Color.black;
var rect = panel.GetComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
_blackOverlay = canvas;
_blackOverlay.SetActive(false);
}
public override void OnStopClient()
{
base.OnStopClient();
if (_blackOverlay != null)
Destroy(_blackOverlay);
}
}
}
fileFormatVersion: 2
guid: 2078889c66eab4f6f9c789aadc13ea75
\ No newline at end of file
using System;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using IStalk.Data;
using UnityEngine;
namespace IStalk.Eye
{
public class EyeFlashReceiver : NetworkBehaviour
{
private readonly SyncVar<float> _blindTimer = new(new SyncTypeSettings(0.5f));
private readonly SyncVar<float> _teamFlashCooldownTimer = new(new SyncTypeSettings(1f));
private EyeEyelidController _eyelid;
private EyeLaserController _laser;
private EyeScanAbility _scan;
private EyeTrapPlacer _traps;
private EyeIncarnationController _incarnation;
private GameObject _flashOverlay;
public bool IsBlinded => _blindTimer.Value > 0f;
public float BlindTimeRemaining => _blindTimer.Value;
public bool IsFlashOnCooldown => _teamFlashCooldownTimer.Value > 0f;
public event Action OnFlashDodged;
public event Action<float> OnFlashHit;
public override void OnStartNetwork()
{
base.OnStartNetwork();
_eyelid = GetComponent<EyeEyelidController>();
_laser = GetComponent<EyeLaserController>();
_scan = GetComponent<EyeScanAbility>();
_traps = GetComponent<EyeTrapPlacer>();
_incarnation = GetComponent<EyeIncarnationController>();
}
public override void OnStartClient()
{
base.OnStartClient();
if (IsOwner)
CreateFlashOverlay();
}
private void Update()
{
if (IsServerInitialized)
{
if (_blindTimer.Value > 0f)
_blindTimer.Value -= Time.deltaTime;
if (_teamFlashCooldownTimer.Value > 0f)
_teamFlashCooldownTimer.Value -= Time.deltaTime;
}
if (IsOwner && _flashOverlay != null)
_flashOverlay.SetActive(_blindTimer.Value > 0f);
}
[Server]
public bool TryApplyFlash()
{
if (_teamFlashCooldownTimer.Value > 0f)
return false;
var rules = GlobalRules_SO.Instance;
_teamFlashCooldownTimer.Value = rules.flashTeamCooldown;
if (_eyelid != null && _eyelid.IsClosed)
{
FlashDodgedObservers();
return false;
}
_blindTimer.Value = rules.flashDuration;
FlashHitObservers(rules.flashDuration);
return true;
}
[ObserversRpc]
private void FlashDodgedObservers()
{
OnFlashDodged?.Invoke();
}
[ObserversRpc]
private void FlashHitObservers(float duration)
{
OnFlashHit?.Invoke(duration);
}
private void CreateFlashOverlay()
{
var canvas = new GameObject("FlashOverlay");
var canvasComp = canvas.AddComponent<Canvas>();
canvasComp.renderMode = RenderMode.ScreenSpaceOverlay;
canvasComp.sortingOrder = 998;
var panel = new GameObject("WhitePanel");
panel.transform.SetParent(canvas.transform, false);
var img = panel.AddComponent<UnityEngine.UI.Image>();
img.color = Color.white;
var rect = panel.GetComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
_flashOverlay = canvas;
_flashOverlay.SetActive(false);
}
public override void OnStopClient()
{
base.OnStopClient();
if (_flashOverlay != null)
Destroy(_flashOverlay);
}
}
}
fileFormatVersion: 2
guid: 91e73192cfc004ff08cc2e471dfe7891
\ No newline at end of file
...@@ -21,9 +21,17 @@ namespace IStalk.Eye ...@@ -21,9 +21,17 @@ namespace IStalk.Eye
private float _chargeTimer; private float _chargeTimer;
private float _cooldownTimer; private float _cooldownTimer;
private DreadEconomy _dread; private DreadEconomy _dread;
private float _cooldownMultiplier = 1f;
private float _radiusMultiplier = 1f;
private GlobalRules_SO Rules => GlobalRules_SO.Instance; private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public void ApplyTypeModifiers(float cooldownMult, float radiusMult)
{
_cooldownMultiplier = cooldownMult;
_radiusMultiplier = radiusMult;
}
private enum LaserState { Idle, Telegraphing, Charging, Firing, Cooldown } private enum LaserState { Idle, Telegraphing, Charging, Firing, Cooldown }
public override void OnStartClient() public override void OnStartClient()
...@@ -98,14 +106,15 @@ namespace IStalk.Eye ...@@ -98,14 +106,15 @@ namespace IStalk.Eye
Vector3 dir = (_aimEnd.Value - _aimStart.Value).normalized; Vector3 dir = (_aimEnd.Value - _aimStart.Value).normalized;
if (dir.sqrMagnitude < 0.01f) dir = Vector3.forward; if (dir.sqrMagnitude < 0.01f) dir = Vector3.forward;
if (Physics.SphereCast(_aimStart.Value, Rules.laserRadius, dir, out RaycastHit hit, Rules.laserRange, _hitLayers)) float radius = Rules.laserRadius * _radiusMultiplier;
if (Physics.SphereCast(_aimStart.Value, radius, dir, out RaycastHit hit, Rules.laserRange, _hitLayers))
{ {
var runner = hit.collider.GetComponentInParent<Runner.RunnerController>(); var runner = hit.collider.GetComponentInParent<Runner.RunnerController>();
if (runner != null) if (runner != null)
HandleRunnerHit(runner); HandleRunnerHit(runner);
} }
SetCooldownObservers(Rules.laserCooldown); SetCooldownObservers(Rules.laserCooldown * _cooldownMultiplier);
Invoke(nameof(ResetToIdle), 0.3f); Invoke(nameof(ResetToIdle), 0.3f);
} }
......
...@@ -17,10 +17,18 @@ namespace IStalk.Eye ...@@ -17,10 +17,18 @@ namespace IStalk.Eye
private DreadEconomy _dread; private DreadEconomy _dread;
private float _scanTimer; private float _scanTimer;
private float _cooldownMultiplier = 1f;
private float _radiusMultiplier = 1f;
public void ApplyTypeModifiers(float cooldownMult, float radiusMult)
{
_cooldownMultiplier = cooldownMult;
_radiusMultiplier = radiusMult;
}
public bool IsScanning => _isScanning.Value; public bool IsScanning => _isScanning.Value;
public float CooldownRemaining => _cooldownRemaining.Value; public float CooldownRemaining => _cooldownRemaining.Value;
public float ScanRadius => _scanRadius; public float ScanRadius => _scanRadius * _radiusMultiplier;
public event System.Action OnScanStarted; public event System.Action OnScanStarted;
public event System.Action OnScanEnded; public event System.Action OnScanEnded;
...@@ -55,7 +63,7 @@ namespace IStalk.Eye ...@@ -55,7 +63,7 @@ namespace IStalk.Eye
if (_scanTimer <= 0) if (_scanTimer <= 0)
{ {
_isScanning.Value = false; _isScanning.Value = false;
_cooldownRemaining.Value = _scanCooldown; _cooldownRemaining.Value = _scanCooldown * _cooldownMultiplier;
} }
} }
} }
......
...@@ -13,6 +13,14 @@ namespace IStalk.Eye ...@@ -13,6 +13,14 @@ namespace IStalk.Eye
private DreadEconomy _dread; private DreadEconomy _dread;
private TrapData_SO _selectedTrap; private TrapData_SO _selectedTrap;
private readonly SyncVar<int> _activeTrapCount = new(); private readonly SyncVar<int> _activeTrapCount = new();
private float _costMultiplier = 1f;
private int _maxActiveBonus = 0;
public void ApplyTypeModifiers(float costMult, int maxActiveBonus)
{
_costMultiplier = costMult;
_maxActiveBonus = maxActiveBonus;
}
public override void OnStartClient() public override void OnStartClient()
{ {
...@@ -54,10 +62,12 @@ namespace IStalk.Eye ...@@ -54,10 +62,12 @@ namespace IStalk.Eye
var trapData = ItemDatabase.Instance.GetTrap(trapId); var trapData = ItemDatabase.Instance.GetTrap(trapId);
if (trapData == null) return; if (trapData == null) return;
if (trapData.maxActivePerMatch > 0 && _activeTrapCount.Value >= trapData.maxActivePerMatch) int maxActive = trapData.maxActivePerMatch + _maxActiveBonus;
if (maxActive > 0 && _activeTrapCount.Value >= maxActive)
return; return;
if (!_dread.TrySpend(trapData.dreadCost)) int adjustedCost = Mathf.RoundToInt(trapData.dreadCost * _costMultiplier);
if (!_dread.TrySpend(adjustedCost))
return; return;
if (trapData.placedPrefab == null) return; if (trapData.placedPrefab == null) return;
......
using FishNet.Object;
using FishNet.Object.Synchronizing;
using IStalk.Data;
using UnityEngine;
namespace IStalk.Eye
{
public class EyeTypeController : NetworkBehaviour
{
private readonly SyncVar<string> _eyeTypeId = new();
private EyeType_SO _eyeType;
private EyeLaserController _laser;
private EyeScanAbility _scan;
private EyeTrapPlacer _traps;
private EyeIncarnationController _incarnation;
private EyeEyelidController _eyelid;
private DreadEconomy _dread;
public EyeType_SO CurrentType => _eyeType;
public override void OnStartNetwork()
{
base.OnStartNetwork();
_laser = GetComponent<EyeLaserController>();
_scan = GetComponent<EyeScanAbility>();
_traps = GetComponent<EyeTrapPlacer>();
_incarnation = GetComponent<EyeIncarnationController>();
_eyelid = GetComponent<EyeEyelidController>();
_dread = GetComponent<DreadEconomy>();
_eyeTypeId.OnChange += OnTypeIdChanged;
}
public override void OnStartServer()
{
base.OnStartServer();
if (!string.IsNullOrEmpty(_eyeTypeId.Value))
ApplyType();
}
[Server]
public void SetEyeType(string typeId)
{
_eyeTypeId.Value = typeId;
ApplyType();
}
private void OnTypeIdChanged(string prev, string next, bool asServer)
{
if (!asServer)
ApplyType();
}
private void ApplyType()
{
_eyeType = ItemDatabase.Instance.GetEyeType(_eyeTypeId.Value);
if (_eyeType == null)
{
Debug.LogError($"[EyeTypeController] Unknown eye type: {_eyeTypeId.Value}");
return;
}
if (_laser != null)
{
bool hasLaser = _eyeType.HasAbility(EyeAbility.Laser);
_laser.enabled = hasLaser;
if (hasLaser)
_laser.ApplyTypeModifiers(_eyeType.laserCooldownMultiplier, _eyeType.laserRadiusMultiplier);
}
if (_scan != null)
{
bool hasScan = _eyeType.HasAbility(EyeAbility.Scan);
_scan.enabled = hasScan;
if (hasScan)
_scan.ApplyTypeModifiers(_eyeType.scanCooldownMultiplier, _eyeType.scanRadiusMultiplier);
}
if (_traps != null)
{
bool hasTraps = _eyeType.HasAbility(EyeAbility.Traps);
_traps.enabled = hasTraps;
if (hasTraps)
_traps.ApplyTypeModifiers(_eyeType.trapCostMultiplier, _eyeType.trapMaxActiveBonus);
}
if (_incarnation != null)
_incarnation.enabled = _eyeType.HasAbility(EyeAbility.Incarnation);
if (_eyelid != null)
_eyelid.enabled = _eyeType.hasEyelids;
if (_dread != null && IsServerInitialized)
{
if (_eyeType.dreadStartOverride >= 0)
_dread.ApplyStartOverride(_eyeType.dreadStartOverride);
if (_eyeType.dreadRegenOverride >= 0)
_dread.ApplyRegenOverride(_eyeType.dreadRegenOverride);
}
}
}
}
fileFormatVersion: 2
guid: 869c5d559bae24bd9b7905e666c4e8ad
\ No newline at end of file
...@@ -30,8 +30,9 @@ namespace IStalk.Eye ...@@ -30,8 +30,9 @@ namespace IStalk.Eye
public IReadOnlyList<DecoyBlip> ActiveDecoys => _decoyBlips; public IReadOnlyList<DecoyBlip> ActiveDecoys => _decoyBlips;
public event System.Action<List<int>> OnVisibilityChanged; public event System.Action<List<int>> OnVisibilityChanged;
private void Awake() public override void OnStartNetwork()
{ {
base.OnStartNetwork();
Instance = this; Instance = this;
} }
......
using System.Collections.Generic;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using IStalk.Data;
using UnityEngine;
namespace IStalk.Runner
{
public struct ActiveBuff
{
public string BuffId;
public float RemainingTime;
}
public class RunnerBuffSystem : NetworkBehaviour
{
private readonly SyncList<string> _activeBuffIds = new();
private readonly List<float> _buffTimers = new();
private RunnerController _controller;
private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public event System.Action OnBuffsChanged;
public override void OnStartNetwork()
{
base.OnStartNetwork();
_controller = GetComponent<RunnerController>();
_activeBuffIds.OnChange += (op, idx, old, next, server) => OnBuffsChanged?.Invoke();
}
private void Update()
{
if (!IsServerInitialized) return;
for (int i = _buffTimers.Count - 1; i >= 0; i--)
{
_buffTimers[i] -= Time.deltaTime;
if (_buffTimers[i] <= 0)
{
_activeBuffIds.RemoveAt(i);
_buffTimers.RemoveAt(i);
}
}
ApplyProximityBuffs();
}
[Server]
public void AddBuff(string buffId, float duration)
{
_activeBuffIds.Add(buffId);
_buffTimers.Add(duration);
}
[Server]
public void RemoveBuff(string buffId)
{
int idx = _activeBuffIds.IndexOf(buffId);
if (idx >= 0)
{
_activeBuffIds.RemoveAt(idx);
_buffTimers.RemoveAt(idx);
}
}
private void ApplyProximityBuffs()
{
float linkedBonus = 0f;
var colliders = Physics.OverlapSphere(transform.position, Rules.linkedSprintRadius);
foreach (var col in colliders)
{
if (col.gameObject == gameObject) continue;
var otherBuffs = col.GetComponentInParent<RunnerBuffSystem>();
if (otherBuffs == null) continue;
if (otherBuffs.HasBuff("linked_sprint"))
linkedBonus = Mathf.Max(linkedBonus, Rules.linkedSprintBonus);
}
if (HasBuff("linked_sprint"))
{
bool anyTeammateNearby = false;
foreach (var col in colliders)
{
if (col.gameObject == gameObject) continue;
if (col.GetComponentInParent<RunnerController>() != null)
{
anyTeammateNearby = true;
break;
}
}
if (anyTeammateNearby)
linkedBonus = Mathf.Max(linkedBonus, Rules.linkedSprintBonus);
}
if (_controller != null)
_controller.SetSpeedBonus(linkedBonus);
}
public bool HasBuff(string buffId) => _activeBuffIds.Contains(buffId);
public int ActiveBuffCount => _activeBuffIds.Count;
public string GetBuffAt(int index)
{
if (index < 0 || index >= _activeBuffIds.Count) return null;
return _activeBuffIds[index];
}
}
}
fileFormatVersion: 2
guid: 7357a637acc77476aabef8a9525b1e40
\ No newline at end of file
...@@ -43,8 +43,42 @@ namespace IStalk.Runner ...@@ -43,8 +43,42 @@ namespace IStalk.Runner
private Vector3 _parkourStart; private Vector3 _parkourStart;
private Vector3 _parkourEnd; private Vector3 _parkourEnd;
private bool _dashEnabled;
private bool _wallRunEnabled;
private bool _airDashEnabled;
private bool _slideEnabled;
private bool _vaultEnabled;
private float _speedBonus;
private GlobalRules_SO Rules => GlobalRules_SO.Instance; private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public void EnableAbility(WearableAbilityType ability)
{
switch (ability)
{
case WearableAbilityType.Dash: _dashEnabled = true; break;
case WearableAbilityType.WallRun: _wallRunEnabled = true; break;
case WearableAbilityType.AirDash: _airDashEnabled = true; break;
case WearableAbilityType.Slide: _slideEnabled = true; break;
case WearableAbilityType.Vault: _vaultEnabled = true; break;
}
}
public void DisableAbility(WearableAbilityType ability)
{
switch (ability)
{
case WearableAbilityType.Dash: _dashEnabled = false; break;
case WearableAbilityType.WallRun: _wallRunEnabled = false; break;
case WearableAbilityType.AirDash: _airDashEnabled = false; break;
case WearableAbilityType.Slide: _slideEnabled = false; break;
case WearableAbilityType.Vault: _vaultEnabled = false; break;
}
}
public void SetSpeedBonus(float bonus) => _speedBonus = bonus;
public enum MoveState { Normal, WallRunning, Vaulting, Mantling, Sliding, Dashing } public enum MoveState { Normal, WallRunning, Vaulting, Mantling, Sliding, Dashing }
private struct MoveData : IReplicateData private struct MoveData : IReplicateData
...@@ -201,32 +235,32 @@ namespace IStalk.Runner ...@@ -201,32 +235,32 @@ namespace IStalk.Runner
_isSprinting = md.Sprint && _isGrounded && canSprint; _isSprinting = md.Sprint && _isGrounded && canSprint;
float speedMult = _weight != null ? Rules.GetSpeedMultiplier(_weight.CurrentTier) : 1f; float speedMult = _weight != null ? Rules.GetSpeedMultiplier(_weight.CurrentTier) : 1f;
float healthMult = _health != null ? _health.SlowMultiplier : 1f; float healthMult = _health != null ? _health.SlowMultiplier : 1f;
float speed = Rules.baseRunSpeed * speedMult * healthMult; float speed = Rules.baseRunSpeed * speedMult * healthMult * (1f + _speedBonus);
if (_isSprinting) if (_isSprinting)
{ {
speed *= Rules.sprintMultiplier; speed *= Rules.sprintMultiplier;
if (IsServerStarted && _stamina != null) _stamina.DrainSprint(dt); if (IsServerStarted && _stamina != null) _stamina.DrainSprint(dt);
} }
if (md.Slide && _isSprinting && _isGrounded) if (_slideEnabled && md.Slide && _isSprinting && _isGrounded)
{ {
EnterSlide(md.Yaw); EnterSlide(md.Yaw);
return; return;
} }
if (md.Dash && _dashCooldown <= 0 && !_isGrounded && (_stamina == null || _stamina.CanDash)) if ((_dashEnabled || _airDashEnabled) && md.Dash && _dashCooldown <= 0 && !_isGrounded && (_stamina == null || _stamina.CanDash))
{ {
if (IsServerStarted && _stamina != null && !_stamina.TryDash()) { } if (IsServerStarted && _stamina != null && !_stamina.TryDash()) { }
else { EnterDash(md); return; } else { EnterDash(md); return; }
} }
if (!_isGrounded && _canWallRun && md.Move.y > 0.1f) if (_wallRunEnabled && !_isGrounded && _canWallRun && md.Move.y > 0.1f)
{ {
if (TryEnterWallRun(md)) if (TryEnterWallRun(md))
return; return;
} }
if (!_isGrounded && md.Move.y > 0.1f) if (_vaultEnabled && !_isGrounded && md.Move.y > 0.1f)
{ {
if (TryVault(md.Yaw)) if (TryVault(md.Yaw))
return; return;
......
using FishNet.Object;
using UnityEngine;
using IStalk.Data;
namespace IStalk.Runner
{
public class RunnerCoopActions : NetworkBehaviour
{
private PlayerInventory _inventory;
private RunnerController _controller;
private RunnerHealth _health;
private float _boostCooldown;
private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public override void OnStartClient()
{
base.OnStartClient();
if (!IsOwner)
{
enabled = false;
return;
}
_inventory = GetComponent<PlayerInventory>();
_controller = GetComponent<RunnerController>();
_health = GetComponent<RunnerHealth>();
}
private void Update()
{
if (!IsOwner) return;
if (_health != null && _health.IsDown) return;
_boostCooldown -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.F))
TryBoost();
if (Input.GetKey(KeyCode.G))
TryThrowItem();
}
private void TryBoost()
{
if (_boostCooldown > 0) return;
var colliders = Physics.OverlapSphere(transform.position, Rules.boostRange);
foreach (var col in colliders)
{
if (col.gameObject == gameObject) continue;
var otherController = col.GetComponentInParent<RunnerController>();
if (otherController == null) continue;
var otherHealth = col.GetComponentInParent<RunnerHealth>();
if (otherHealth != null && otherHealth.IsDown) continue;
if (IsCrouchingNearWall(otherController))
{
ServerRequestBoost(otherController.NetworkObject);
_boostCooldown = 1f;
return;
}
}
}
private bool IsCrouchingNearWall(RunnerController other)
{
Vector3 pos = other.transform.position;
return Physics.Raycast(pos, other.transform.forward, 1.2f) ||
Physics.Raycast(pos, -other.transform.forward, 1.2f) ||
Physics.Raycast(pos, other.transform.right, 1.2f) ||
Physics.Raycast(pos, -other.transform.right, 1.2f);
}
[ServerRpc]
private void ServerRequestBoost(NetworkObject booster)
{
if (booster == null) return;
var boosterHealth = booster.GetComponent<RunnerHealth>();
if (boosterHealth != null && boosterHealth.IsDown) return;
float dist = Vector3.Distance(transform.position, booster.transform.position);
if (dist > Rules.boostRange * 1.5f) return;
var cc = GetComponent<CharacterController>();
if (cc != null)
cc.Move(Vector3.up * Rules.boostHeight);
BoostVfxObservers(transform.position);
}
[ObserversRpc]
private void BoostVfxObservers(Vector3 position)
{
// VFX/SFX hook for boost effect
}
private void TryThrowItem()
{
if (_inventory == null) return;
var items = _inventory.GetItems();
if (items.Count == 0) return;
var target = FindThrowTarget();
if (target == null) return;
string lastItem = items[items.Count - 1];
ServerThrowItem(lastItem, target.NetworkObject);
}
private RunnerController FindThrowTarget()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 30f))
{
var runner = hit.collider.GetComponentInParent<RunnerController>();
if (runner != null && runner.gameObject != gameObject)
{
float dist = Vector3.Distance(transform.position, runner.transform.position);
if (dist <= Rules.throwRangeBase)
return runner;
}
}
var colliders = Physics.OverlapSphere(transform.position, Rules.throwRangeBase);
RunnerController closest = null;
float closestDist = float.MaxValue;
foreach (var col in colliders)
{
if (col.gameObject == gameObject) continue;
var runner = col.GetComponentInParent<RunnerController>();
if (runner == null) continue;
var health = col.GetComponentInParent<RunnerHealth>();
if (health != null && health.IsDown) continue;
float d = Vector3.Distance(transform.position, runner.transform.position);
if (d < closestDist)
{
closestDist = d;
closest = runner;
}
}
return closest;
}
[ServerRpc]
private void ServerThrowItem(string itemId, NetworkObject target)
{
if (target == null) return;
if (!_inventory.Contains(itemId)) return;
float dist = Vector3.Distance(transform.position, target.transform.position);
var itemData = ItemDatabase.Instance.GetLootItem(itemId);
float maxRange = Rules.throwRangeBase;
if (itemData != null)
maxRange -= itemData.weight * Rules.throwRangeWeightScale;
if (dist > maxRange * 1.5f) return;
_inventory.TryRemoveItem(itemId);
var targetInventory = target.GetComponent<PlayerInventory>();
if (targetInventory != null && targetInventory.HasSpace)
{
targetInventory.TryAddItem(itemId);
}
else
{
WorldItem.SpawnWorldItem(itemId, target.transform.position + Vector3.up * 0.5f);
}
ThrowVfxObservers(transform.position, target.transform.position);
}
[ObserversRpc]
private void ThrowVfxObservers(Vector3 from, Vector3 to)
{
// VFX/SFX hook for throw arc
}
}
}
fileFormatVersion: 2
guid: 7c442aac166254768ae9229889609d3e
\ No newline at end of file
...@@ -53,6 +53,9 @@ namespace IStalk.Runner ...@@ -53,6 +53,9 @@ namespace IStalk.Runner
var inventory = GetComponent<PlayerInventory>(); var inventory = GetComponent<PlayerInventory>();
if (inventory != null) if (inventory != null)
inventory.DropAll(transform.position); inventory.DropAll(transform.position);
var wearables = GetComponent<RunnerWearableManager>();
if (wearables != null)
wearables.DropAllWearables(transform.position);
} }
HitFeedbackObservers(); HitFeedbackObservers();
......
...@@ -146,10 +146,7 @@ namespace IStalk.Runner ...@@ -146,10 +146,7 @@ namespace IStalk.Runner
if (_channelProgress >= 1f) if (_channelProgress >= 1f)
{ {
_channelProgress = 0; _channelProgress = 0;
if (_extraction.IsExtractionMet) ServerDeposit();
ServerExtract();
else
ServerDeposit();
} }
} }
else else
...@@ -180,12 +177,6 @@ namespace IStalk.Runner ...@@ -180,12 +177,6 @@ namespace IStalk.Runner
_extraction.DepositLoot(_inventory, value); _extraction.DepositLoot(_inventory, value);
} }
[ServerRpc]
private void ServerExtract()
{
_extraction.TryExtract(NetworkObject);
}
[ServerRpc] [ServerRpc]
private void ServerUseItem() private void ServerUseItem()
{ {
......
using System;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using IStalk.Data;
using UnityEngine;
namespace IStalk.Runner
{
public class RunnerWearableManager : NetworkBehaviour
{
private readonly SyncList<string> _equippedIds = new();
private RunnerController _controller;
private GlobalRules_SO Rules => GlobalRules_SO.Instance;
public event Action OnWearablesChanged;
public int SlotCount => Rules.maxWearableSlots;
public int UsedSlots => _equippedIds.Count;
public bool HasSpace => _equippedIds.Count < Rules.maxWearableSlots;
public override void OnStartNetwork()
{
base.OnStartNetwork();
_controller = GetComponent<RunnerController>();
_equippedIds.OnChange += HandleWearableListChanged;
}
public override void OnStartClient()
{
base.OnStartClient();
for (int i = 0; i < _equippedIds.Count; i++)
ApplyWearableLocally(_equippedIds[i], true);
}
private void HandleWearableListChanged(SyncListOperation op, int index, string oldItem, string newItem, bool asServer)
{
switch (op)
{
case SyncListOperation.Add:
ApplyWearableLocally(newItem, true);
break;
case SyncListOperation.RemoveAt:
ApplyWearableLocally(oldItem, false);
break;
case SyncListOperation.Clear:
break;
}
OnWearablesChanged?.Invoke();
}
[ServerRpc]
public void ServerEquipWearable(string wearableId)
{
if (_equippedIds.Count >= Rules.maxWearableSlots) return;
if (_equippedIds.Contains(wearableId)) return;
var data = ItemDatabase.Instance.GetWearable(wearableId);
if (data == null) return;
_equippedIds.Add(wearableId);
}
[ServerRpc]
public void ServerUnequipWearable(string wearableId)
{
if (!_equippedIds.Contains(wearableId)) return;
_equippedIds.Remove(wearableId);
}
[Server]
public void DropAllWearables(Vector3 position)
{
for (int i = _equippedIds.Count - 1; i >= 0; i--)
{
string id = _equippedIds[i];
_equippedIds.RemoveAt(i);
SpawnWearableWorldItem(id, position + UnityEngine.Random.insideUnitSphere * 1.5f);
}
}
[Server]
private void SpawnWearableWorldItem(string wearableId, Vector3 position)
{
var data = ItemDatabase.Instance.GetWearable(wearableId);
if (data == null || data.worldPrefab == null) return;
var go = Instantiate(data.worldPrefab, position, Quaternion.identity);
var nob = go.GetComponent<FishNet.Object.NetworkObject>();
if (nob != null)
ServerManager.Spawn(nob);
}
private void ApplyWearableLocally(string wearableId, bool equip)
{
var data = ItemDatabase.Instance.GetWearable(wearableId);
if (data == null || _controller == null) return;
if (equip)
_controller.EnableAbility(data.abilityType);
else
_controller.DisableAbility(data.abilityType);
}
public string GetEquippedAt(int slot)
{
if (slot < 0 || slot >= _equippedIds.Count) return null;
return _equippedIds[slot];
}
public bool HasWearable(string wearableId) => _equippedIds.Contains(wearableId);
}
}
fileFormatVersion: 2
guid: 81f0ef915bf774648969dd4a7b3c4461
\ No newline at end of file
...@@ -79,6 +79,18 @@ namespace IStalk.Runner ...@@ -79,6 +79,18 @@ namespace IStalk.Runner
case ShrineItemType.GrapplingHook: case ShrineItemType.GrapplingHook:
ActivateGrapplingHook(); ActivateGrapplingHook();
break; break;
case ShrineItemType.FlashOrb:
ActivateFlashOrb();
break;
case ShrineItemType.LinkedSprintTotem:
ActivateLinkedSprint();
break;
case ShrineItemType.SilenceShroud:
ActivateSilenceShroud();
break;
case ShrineItemType.VitalityLink:
ActivateVitalityLink();
break;
} }
SpawnVfxObservers(); SpawnVfxObservers();
...@@ -142,6 +154,38 @@ namespace IStalk.Runner ...@@ -142,6 +154,38 @@ namespace IStalk.Runner
controller.GrantExtraDash(); controller.GrantExtraDash();
} }
[Server]
private void ActivateFlashOrb()
{
var eyeFlash = FindAnyObjectByType<EyeFlashReceiver>();
if (eyeFlash != null)
eyeFlash.TryApplyFlash();
}
[Server]
private void ActivateLinkedSprint()
{
var buffs = GetComponent<RunnerBuffSystem>();
if (buffs != null)
buffs.AddBuff("linked_sprint", _activeData.effectDuration);
}
[Server]
private void ActivateSilenceShroud()
{
var noise = GetComponent<NoiseEmitter>();
if (noise != null)
noise.Suppress(_activeData.effectDuration);
}
[Server]
private void ActivateVitalityLink()
{
var buffs = GetComponent<RunnerBuffSystem>();
if (buffs != null)
buffs.AddBuff("vitality_link", _activeData.effectDuration);
}
[Server] [Server]
private void EndEffect() private void EndEffect()
{ {
......
namespace IStalk.Runner
{
public enum WearableAbilityType
{
Dash,
WallRun,
AirDash,
Slide,
Vault,
Grapple
}
}
fileFormatVersion: 2
guid: 2f85d6ac9d9e1424a8e3a82788ace1c0
\ No newline at end of file
...@@ -11,6 +11,8 @@ namespace IStalk.Shared ...@@ -11,6 +11,8 @@ namespace IStalk.Shared
[Server] [Server]
public void EmitNoise(Vector3 position, float radius, float intensity) public void EmitNoise(Vector3 position, float radius, float intensity)
{ {
if (_suppressTimer > 0) return;
if (_cachedDread == null) if (_cachedDread == null)
_cachedDread = FindAnyObjectByType<DreadEconomy>(); _cachedDread = FindAnyObjectByType<DreadEconomy>();
...@@ -31,6 +33,23 @@ namespace IStalk.Shared ...@@ -31,6 +33,23 @@ namespace IStalk.Shared
Destroy(pulse, 0.5f); Destroy(pulse, 0.5f);
} }
private float _suppressTimer;
[Server]
public void Suppress(float duration)
{
_suppressTimer = duration;
}
public bool IsSuppressed => _suppressTimer > 0f;
private void Update()
{
if (!IsServerInitialized) return;
if (_suppressTimer > 0)
_suppressTimer -= Time.deltaTime;
}
public static void ClearCache() public static void ClearCache()
{ {
_cachedDread = null; _cachedDread = null;
......
...@@ -12,8 +12,8 @@ MonoBehaviour: ...@@ -12,8 +12,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3} m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
m_Name: Mobile_RPAsset m_Name: Mobile_RPAsset
m_EditorClassIdentifier: m_EditorClassIdentifier:
k_AssetVersion: 12 k_AssetVersion: 13
k_AssetPreviousVersion: 12 k_AssetPreviousVersion: 13
m_RendererType: 1 m_RendererType: 1
m_RendererData: {fileID: 0} m_RendererData: {fileID: 0}
m_RendererDataList: m_RendererDataList:
...@@ -53,6 +53,7 @@ MonoBehaviour: ...@@ -53,6 +53,7 @@ MonoBehaviour:
m_AdditionalLightsShadowResolutionTierHigh: 1024 m_AdditionalLightsShadowResolutionTierHigh: 1024
m_ReflectionProbeBlending: 1 m_ReflectionProbeBlending: 1
m_ReflectionProbeBoxProjection: 1 m_ReflectionProbeBoxProjection: 1
m_ReflectionProbeAtlas: 1
m_ShadowDistance: 50 m_ShadowDistance: 50
m_ShadowCascadeCount: 1 m_ShadowCascadeCount: 1
m_Cascade2Split: 0.25 m_Cascade2Split: 0.25
...@@ -78,11 +79,11 @@ MonoBehaviour: ...@@ -78,11 +79,11 @@ MonoBehaviour:
m_UseAdaptivePerformance: 1 m_UseAdaptivePerformance: 1
m_ColorGradingMode: 0 m_ColorGradingMode: 0
m_ColorGradingLutSize: 32 m_ColorGradingLutSize: 32
m_AllowPostProcessAlphaOutput: 0
m_UseFastSRGBLinearConversion: 1 m_UseFastSRGBLinearConversion: 1
m_SupportDataDrivenLensFlare: 1 m_SupportDataDrivenLensFlare: 1
m_SupportScreenSpaceLensFlare: 1 m_SupportScreenSpaceLensFlare: 1
m_GPUResidentDrawerMode: 0 m_GPUResidentDrawerMode: 0
m_UseLegacyLightmaps: 0
m_SmallMeshScreenPercentage: 0 m_SmallMeshScreenPercentage: 0
m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0 m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0
m_ShadowType: 1 m_ShadowType: 1
...@@ -109,6 +110,7 @@ MonoBehaviour: ...@@ -109,6 +110,7 @@ MonoBehaviour:
m_PrefilterDebugKeywords: 1 m_PrefilterDebugKeywords: 1
m_PrefilterWriteRenderingLayers: 1 m_PrefilterWriteRenderingLayers: 1
m_PrefilterHDROutput: 1 m_PrefilterHDROutput: 1
m_PrefilterAlphaOutput: 0
m_PrefilterSSAODepthNormals: 1 m_PrefilterSSAODepthNormals: 1
m_PrefilterSSAOSourceDepthLow: 1 m_PrefilterSSAOSourceDepthLow: 1
m_PrefilterSSAOSourceDepthMedium: 0 m_PrefilterSSAOSourceDepthMedium: 0
...@@ -126,8 +128,14 @@ MonoBehaviour: ...@@ -126,8 +128,14 @@ MonoBehaviour:
m_PrefilterSoftShadowsQualityHigh: 1 m_PrefilterSoftShadowsQualityHigh: 1
m_PrefilterSoftShadows: 0 m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 1 m_PrefilterScreenCoord: 1
m_PrefilterScreenSpaceIrradiance: 0
m_PrefilterNativeRenderPass: 1 m_PrefilterNativeRenderPass: 1
m_PrefilterUseLegacyLightmaps: 0 m_PrefilterUseLegacyLightmaps: 0
m_PrefilterBicubicLightmapSampling: 0
m_PrefilterReflectionProbeRotation: 0
m_PrefilterReflectionProbeBlending: 0
m_PrefilterReflectionProbeBoxProjection: 0
m_PrefilterReflectionProbeAtlas: 0
m_ShaderVariantLogLevel: 0 m_ShaderVariantLogLevel: 0
m_ShadowCascades: 0 m_ShadowCascades: 0
m_Textures: m_Textures:
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
--- !u!55 &1 --- !u!55 &1
PhysicsManager: PhysicsManager:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
serializedVersion: 13 serializedVersion: 23
m_Gravity: {x: 0, y: -9.81, z: 0} m_Gravity: {x: 0, y: -9.81, z: 0}
m_DefaultMaterial: {fileID: 0} m_DefaultMaterial: {fileID: 0}
m_BounceThreshold: 2 m_BounceThreshold: 2
m_DefaultMaxDepenetrationVelocity: 10
m_SleepThreshold: 0.005 m_SleepThreshold: 0.005
m_DefaultContactOffset: 0.01 m_DefaultContactOffset: 0.01
m_DefaultSolverIterations: 6 m_DefaultSolverIterations: 6
...@@ -16,11 +17,11 @@ PhysicsManager: ...@@ -16,11 +17,11 @@ PhysicsManager:
m_EnableAdaptiveForce: 0 m_EnableAdaptiveForce: 0
m_ClothInterCollisionDistance: 0.1 m_ClothInterCollisionDistance: 0.1
m_ClothInterCollisionStiffness: 0.2 m_ClothInterCollisionStiffness: 0.2
m_ContactsGeneration: 1 m_LayerCollisionMatrix: fffdfffffffdfffffffdfffffffffffffffdfffffffdffffffedffffffedfffffff8ffff08fcfffffffeffffffffffff3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff m_SimulationMode: 0
m_AutoSimulation: 1
m_AutoSyncTransforms: 0 m_AutoSyncTransforms: 0
m_ReuseCollisionCallbacks: 1 m_ReuseCollisionCallbacks: 1
m_InvokeCollisionCallbacks: 1
m_ClothInterCollisionSettingsToggle: 0 m_ClothInterCollisionSettingsToggle: 0
m_ClothGravity: {x: 0, y: -9.81, z: 0} m_ClothGravity: {x: 0, y: -9.81, z: 0}
m_ContactPairsMode: 0 m_ContactPairsMode: 0
...@@ -31,6 +32,14 @@ PhysicsManager: ...@@ -31,6 +32,14 @@ PhysicsManager:
m_WorldSubdivisions: 8 m_WorldSubdivisions: 8
m_FrictionType: 0 m_FrictionType: 0
m_EnableEnhancedDeterminism: 0 m_EnableEnhancedDeterminism: 0
m_EnableUnifiedHeightmaps: 1 m_ImprovedPatchFriction: 0
m_GenerateOnTriggerStayEvents: 1
m_SolverType: 0 m_SolverType: 0
m_DefaultMaxAngularSpeed: 50 m_DefaultMaxAngularSpeed: 50
m_ScratchBufferChunkCount: 4
m_CurrentBackendId: 4072204805
m_FastMotionThreshold: 3.4028235e+38
m_SceneBuffersReleaseInterval: 0
m_ReleaseSceneBuffers: 0
m_LogVerbosity: 3
m_IncrementalStaticBroadphase: 1
...@@ -941,7 +941,7 @@ PlayerSettings: ...@@ -941,7 +941,7 @@ PlayerSettings:
qnxGraphicConfPath: qnxGraphicConfPath:
apiCompatibilityLevel: 6 apiCompatibilityLevel: 6
captureStartupLogs: {} captureStartupLogs: {}
activeInputHandler: 1 activeInputHandler: 2
windowsGamepadBackendHint: 0 windowsGamepadBackendHint: 0
cloudProjectId: cloudProjectId:
framebufferDepthMemorylessMode: 0 framebufferDepthMemorylessMode: 0
......
{
"templatePinStates": [],
"dependencyTypeInfos": [
{
"userAdded": false,
"type": "UnityEngine.AnimationClip",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.Animations.AnimatorController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.AnimatorOverrideController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.Audio.AudioMixerController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.ComputeShader",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Cubemap",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.GameObject",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.LightingDataAsset",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.LightingSettings",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Material",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.MonoScript",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial2D",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessResources",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.VolumeProfile",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.SceneAsset",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Shader",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.ShaderVariantCollection",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Texture",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Texture2D",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Timeline.TimelineAsset",
"defaultInstantiationMode": 0
}
],
"defaultDependencyTypeInfo": {
"userAdded": false,
"type": "<default_scene_template_dependencies>",
"defaultInstantiationMode": 1
},
"newSceneOverride": 0
}
\ No newline at end of file
...@@ -2,8 +2,15 @@ ...@@ -2,8 +2,15 @@
%TAG !u! tag:unity3d.com,2011: %TAG !u! tag:unity3d.com,2011:
--- !u!78 &1 --- !u!78 &1
TagManager: TagManager:
serializedVersion: 2 serializedVersion: 3
tags: [] tags:
- Runner
- Eye
- WorldItem
- Altar
- Shrine
- Connector
- SpawnSlot
layers: layers:
- Default - Default
- TransparentFX - TransparentFX
...@@ -11,13 +18,13 @@ TagManager: ...@@ -11,13 +18,13 @@ TagManager:
- -
- Water - Water
- UI - UI
- - Wall
- - Prop
- - Runner
- - Eye
- - SmokeVolume
- - Interactable
- - Trap
- -
- -
- -
...@@ -50,27 +57,3 @@ TagManager: ...@@ -50,27 +57,3 @@ TagManager:
- Light Layer 5 - Light Layer 5
- Light Layer 6 - Light Layer 6
- Light Layer 7 - Light Layer 7
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
# IStalk — Complete Unity Editor Setup Guide # IStalk — Complete Unity Editor Setup Guide
> **Current State:** 69 C# scripts written, FishNet v4 + Synapse installed, DOTween installed, URP configured. **Nothing exists in the Unity Editor yet** — no prefabs, no scenes, no ScriptableObject assets, no materials, no physics layers. This guide takes you from zero to playable greybox. > **Current State:** 80+ C# scripts written (including Eye Types, Wearables, Co-op, Extraction Room systems), FishNet v4 + Synapse installed, DOTween installed, URP configured. **Nothing exists in the Unity Editor yet** — no prefabs, no scenes, no ScriptableObject assets, no materials, no physics layers. This guide takes you from zero to playable greybox.
--- ---
...@@ -9,16 +9,17 @@ ...@@ -9,16 +9,17 @@
``` ```
HAVE (code-only) NEED (Unity Editor work) HAVE (code-only) NEED (Unity Editor work)
───────────────── ──────────────────────── ───────────────── ────────────────────────
69 .cs scripts 2 Scenes (Lobby + Game) 80+ .cs scripts 2 Scenes (Lobby + Game)
Assembly definitions Physics layers configured Assembly definitions Physics layers configured
Folder structure Prefabs (15+) Folder structure Prefabs (18+)
FishNet v4 + Synapse transport ScriptableObject assets (15+) FishNet v4 + Synapse transport ScriptableObject assets (30)
URP pipeline settings Materials (greybox) URP pipeline settings Materials (greybox)
DOTween NetworkManager setup DOTween NetworkManager setup
Canvas prefabs (3 HUDs) Custom editor inspectors Canvas prefabs (4 HUDs)
Input System bindings Eye Type system (3 types) Input System bindings
Room prefabs (5) Wearable system (6 abilities) Room prefabs (5)
Build Settings configured Co-op mechanics Extraction Room prefab
Extraction Room system Build Settings configured
``` ```
--- ---
...@@ -203,7 +204,53 @@ Create in `Assets/Resources/Items/Traps/`: ...@@ -203,7 +204,53 @@ Create in `Assets/Resources/Items/Traps/`:
| `Trap_NoiseTrip.asset` | noisetrip | NoiseTrip | 10 | 2 | 999 | | `Trap_NoiseTrip.asset` | noisetrip | NoiseTrip | 10 | 2 | 999 |
| `Trap_PathBlocker.asset` | pathblocker | PathBlocker | 30 | 3 | 20 | | `Trap_PathBlocker.asset` | pathblocker | PathBlocker | 30 | 3 | 20 |
### 3.5 Room Node SOs (5 rooms) ### 3.5 Eye Type SOs (3 types)
Create in `Assets/Resources/EyeTypes/` (use `IStalk > Create > Eye Type SO`):
| Asset Name | typeId | displayName | Abilities | hasEyelids |
|-----------|--------|-------------|-----------|------------|
| `EyeType_Watcher.asset` | watcher | Watcher | Scan, Incarnation | No |
| `EyeType_Predator.asset` | predator | Predator | Laser, Incarnation | Yes |
| `EyeType_Trapper.asset` | trapper | Trapper | Traps, Scan, Incarnation | No |
**Stat multipliers (fill in Inspector):**
| Type | laserCdMult | laserRadMult | scanCdMult | scanRadMult | trapCostMult | trapMaxBonus |
|------|------------|-------------|-----------|-----------|------------|------------|
| Watcher | 1.0 | 1.0 | 0.6 | 1.3 | 1.0 | 0 |
| Predator | 0.5 | 1.4 | 1.0 | 1.0 | 1.0 | 0 |
| Trapper | 1.0 | 1.0 | 1.0 | 1.0 | 0.6 | 3 |
> **Tip:** Select any EyeType_SO and use the "Preset: Watcher/Predator/Trapper" buttons in the inspector to auto-fill these values.
### 3.6 Wearable SOs (6 wearables)
Create in `Assets/Resources/Items/Wearables/` (use `IStalk > Create > Wearable SO`):
| Asset Name | wearableId | abilityType | isPassive | cooldown | lootCost |
|-----------|-----------|-------------|-----------|----------|----------|
| `Wearable_Dash.asset` | bracelet_dash | Dash | false | 3 | 4 |
| `Wearable_WallRun.asset` | boots_wallrun | WallRun | true | 0 | 5 |
| `Wearable_AirDash.asset` | wings_airdash | AirDash | false | 5 | 6 |
| `Wearable_Slide.asset` | kneepads_slide | Slide | true | 0 | 3 |
| `Wearable_Vault.asset` | gloves_vault | Vault | true | 0 | 3 |
| `Wearable_Grapple.asset` | hook_grapple | Grapple | false | 8 | 8 |
All wearables: `throwRange = 8`, `worldPrefab = null` (for greybox, assign later)
### 3.7 New Shrine Item SOs (4 co-op items)
Add to `Assets/Resources/Items/Shrine/`:
| Asset Name | itemId | itemType | lootCost | effectDuration | cooldown | isConsumedOnUse |
|-----------|--------|----------|----------|----------------|----------|-----------------|
| `Shrine_FlashOrb.asset` | flash_orb | FlashOrb | 6 | 0 | 0 | true |
| `Shrine_LinkedSprint.asset` | linked_sprint_totem | LinkedSprintTotem | 4 | 15 | 30 | false |
| `Shrine_SilenceShroud.asset` | silence_shroud | SilenceShroud | 5 | 20 | 25 | false |
| `Shrine_VitalityLink.asset` | vitality_link | VitalityLink | 7 | 12 | 35 | false |
### 3.8 Room Node SOs (5 rooms)
Create in `Assets/Resources/Items/Rooms/`: Create in `Assets/Resources/Items/Rooms/`:
...@@ -327,6 +374,9 @@ Runner (Empty) ...@@ -327,6 +374,9 @@ Runner (Empty)
│ ├── RunnerInteraction.cs │ ├── RunnerInteraction.cs
│ ├── NoiseEmitter.cs │ ├── NoiseEmitter.cs
│ ├── ShrineItemEffect.cs │ ├── ShrineItemEffect.cs
│ ├── RunnerWearableManager.cs ← NEW: manages 2 wearable slots
│ ├── RunnerCoopActions.cs ← NEW: ledge boost (F) + bag throw (G)
│ ├── RunnerBuffSystem.cs ← NEW: proximity buffs (linked sprint, etc.)
│ ├── RunnerAnimator.cs │ ├── RunnerAnimator.cs
│ ├── RunnerFootsteps.cs │ ├── RunnerFootsteps.cs
│ ├── RunnerVisibility.cs │ ├── RunnerVisibility.cs
...@@ -361,12 +411,15 @@ Runner (Empty) ...@@ -361,12 +411,15 @@ Runner (Empty)
Eye (Empty) Eye (Empty)
├── Components on Root: ├── Components on Root:
│ ├── NetworkObject │ ├── NetworkObject
│ ├── EyeTypeController.cs ← NEW: master controller, reads EyeType_SO
│ ├── EyeCameraController.cs │ ├── EyeCameraController.cs
│ ├── EyeLaserController.cs │ ├── EyeLaserController.cs
│ ├── EyeScanAbility.cs │ ├── EyeScanAbility.cs
│ ├── EyeTrapPlacer.cs │ ├── EyeTrapPlacer.cs
│ ├── DreadEconomy.cs │ ├── DreadEconomy.cs
│ ├── EyeIncarnationController.cs │ ├── EyeIncarnationController.cs
│ ├── EyeEyelidController.cs ← NEW: right-click toggle, fullscreen black overlay
│ ├── EyeFlashReceiver.cs ← NEW: receives flash, checks eyelid state
│ └── AudioSource │ └── AudioSource
├── EyeCamera (Camera component) ├── EyeCamera (Camera component)
│ ├── Projection: Orthographic │ ├── Projection: Orthographic
...@@ -381,10 +434,12 @@ Eye (Empty) ...@@ -381,10 +434,12 @@ Eye (Empty)
**Steps:** **Steps:**
1. Create Empty → name "Eye" 1. Create Empty → name "Eye"
2. Add `NetworkObject` 2. Add `NetworkObject`
3. Add Eye scripts listed above 3. Add Eye scripts listed above (including 3 new ones)
4. Create child Camera "EyeCamera" → Orthographic, Size 20, Rotation (90,0,0), Position (0,50,0) 4. Create child Camera "EyeCamera" → Orthographic, Size 20, Rotation (90,0,0), Position (0,50,0)
5. Drag into `Assets/Prefabs/Player/Eye.prefab` 5. Drag into `Assets/Prefabs/Player/Eye.prefab`
> **EyeTypeController** dynamically enables/disables Laser/Scan/Traps/Incarnation/Eyelid scripts at spawn based on which EyeType_SO asset is selected. The Eye player picks their type in lobby.
--- ---
## Phase 7: Prop & Item Prefabs (20 min) ## Phase 7: Prop & Item Prefabs (20 min)
...@@ -476,6 +531,45 @@ DynamicProp (Empty) ...@@ -476,6 +531,45 @@ DynamicProp (Empty)
Save as `Assets/Prefabs/Props/DynamicProp.prefab` Save as `Assets/Prefabs/Props/DynamicProp.prefab`
### 7.6 Extraction Room Prefab (NEW)
Use `IStalk > Create > Extraction Room Prefab Setup` or build manually:
```
ExtractionRoom (Empty)
├── NetworkObject
├── ExtractionRoom.cs
├── AntiAfkRitual.cs
├── BoxCollider (trigger, size 15x4x15, center 0,2,0)
├── Floor (Cube, 15x0.1x15, layer: Wall, Mat_Floor)
├── Walls (4x Cube walls with NO connector openings — 1 entrance only)
├── IncarnationProps (6-8 DynamicProp instances scattered inside)
│ ├── Prop_Crate_1
│ ├── Prop_Barrel_1
│ ├── Prop_Table_1
│ ├── Prop_Chair_1
│ ├── Prop_Crate_2
│ └── Prop_Barrel_2
└── Entrance (single gap in one wall, 3m wide)
```
Save as `Assets/Prefabs/Props/ExtractionRoom.prefab`
> **Important:** The room must be dense with incarnation props (Eye can possess them during the 30s extraction). Only ONE entrance so runners can be trapped.
### 7.7 Wearable World Pickup Prefab (NEW)
```
WearablePickup (Empty)
├── NetworkObject
├── WorldWearable.cs (or reuse WorldItem pattern)
├── SphereCollider (trigger, radius 0.5, layer: Interactable)
├── Visual (Cube mesh, scale 0.3x0.3x0.3, Mat_Shrine — cyan tint)
└── PointLight (range 1.5, intensity 0.4, cyan)
```
Save as `Assets/Prefabs/Props/WearablePickup.prefab`
--- ---
## Phase 8: UI Canvas Prefabs (25 min) ## Phase 8: UI Canvas Prefabs (25 min)
...@@ -489,16 +583,25 @@ RunnerHUD (Canvas, Screen Space - Overlay) ...@@ -489,16 +583,25 @@ RunnerHUD (Canvas, Screen Space - Overlay)
├── CanvasScaler (Scale With Screen Size, 1920x1080 reference) ├── CanvasScaler (Scale With Screen Size, 1920x1080 reference)
├── Top Panel ├── Top Panel
│ ├── TimerText (TMP, center-top, "05:00") │ ├── TimerText (TMP, center-top, "05:00")
│ └── ExtractionBar (Image fill, left-right, shows progress) │ ├── ExtractionBar (Image fill, left-right, shows progress)
│ └── ExtractionRoomTimer (TMP, "EXTRACTING: 25s", hidden until active)
├── Bottom-Left Panel ├── Bottom-Left Panel
│ ├── StaminaBar (Image fill, green-to-red gradient) │ ├── StaminaBar (Image fill, green-to-red gradient)
│ ├── HealthBar (Image fill, red) │ ├── HealthBar (Image fill, red)
│ └── WeightIndicator (TMP, "LIGHT / MEDIUM / HEAVY") │ ├── WeightIndicator (TMP, "LIGHT / MEDIUM / HEAVY")
│ └── BuffIndicators (Horizontal Layout, icons for active buffs)
├── Bottom-Right Panel ├── Bottom-Right Panel
│ ├── InventorySlots (6x Image placeholders, horizontal) │ ├── InventorySlots (6x Image placeholders, horizontal)
│ └── ShrineItemSlot (Image + cooldown ring overlay + "Q" text) │ └── ShrineItemSlot (Image + cooldown ring overlay + "Q" text)
├── Bottom-Center Panel (NEW — Wearable Slots)
│ ├── WearableSlot_1 (Image + border + ability icon + hotkey "1")
│ └── WearableSlot_2 (Image + border + ability icon + hotkey "2")
├── Center ├── Center
│ └── Crosshair (Image, small dot) │ └── Crosshair (Image, small dot)
├── Co-op Prompts (hidden by default)
│ ├── BoostPrompt (TMP, "Press F to boost teammate")
│ ├── ThrowPrompt (TMP, "Press G to throw item")
│ └── RitualPrompt (TMP, "Hold V to vote kick AFK player")
└── InteractionPrompt (TMP, "Press F to pick up", hidden by default) └── InteractionPrompt (TMP, "Press F to pick up", hidden by default)
``` ```
...@@ -514,6 +617,7 @@ EyeHUD (Canvas, Screen Space - Overlay) ...@@ -514,6 +617,7 @@ EyeHUD (Canvas, Screen Space - Overlay)
├── Top Panel ├── Top Panel
│ ├── TimerText (TMP, "05:00") │ ├── TimerText (TMP, "05:00")
│ ├── VisibilityCount (TMP, "Runners: 2/4 visible") │ ├── VisibilityCount (TMP, "Runners: 2/4 visible")
│ ├── EyeTypeName (TMP, "PREDATOR", styled per type)
│ └── SealedRoomCount (TMP, "Sealed: 0", hidden until overtime) │ └── SealedRoomCount (TMP, "Sealed: 0", hidden until overtime)
├── Left Panel ├── Left Panel
│ └── DreadBar (Image fill, vertical, purple gradient) │ └── DreadBar (Image fill, vertical, purple gradient)
...@@ -521,12 +625,18 @@ EyeHUD (Canvas, Screen Space - Overlay) ...@@ -521,12 +625,18 @@ EyeHUD (Canvas, Screen Space - Overlay)
│ ├── AbilitySlot_Laser (Image + cooldown ring + cost text + "LMB") │ ├── AbilitySlot_Laser (Image + cooldown ring + cost text + "LMB")
│ ├── AbilitySlot_Scan (Image + cooldown ring + cost text + "SPACE") │ ├── AbilitySlot_Scan (Image + cooldown ring + cost text + "SPACE")
│ └── TrapSelector (3 icons with cost + hotkeys "1" "2" "3") │ └── TrapSelector (3 icons with cost + hotkeys "1" "2" "3")
├── Right Panel (NEW — Eyelid Status)
│ ├── EyelidIndicator (Image, eye icon, toggles open/closed)
│ ├── EyelidHint (TMP, "RMB: Close Eyes", small)
│ └── FlashCooldown (TMP, "Flash CD: 12s", shows team cooldown)
├── Minimap Panel (RawImage, top-right corner, for RenderTexture) ├── Minimap Panel (RawImage, top-right corner, for RenderTexture)
└── GraceIndicator (TMP, "GRACE PERIOD - 0:30", yellow, hidden after grace) └── GraceIndicator (TMP, "GRACE PERIOD - 0:30", yellow, hidden after grace)
``` ```
Add `EyeHUD.cs` component. Wire references. Add `EyeHUD.cs` component. Wire references.
> **Note:** Ability slots should be shown/hidden based on EyeType. A Watcher won't see the Laser slot. A Trapper won't see the Laser slot either. EyelidIndicator only shows if `hasEyelids = true`.
### 8.3 Lobby UI ### 8.3 Lobby UI
Create: `Assets/Prefabs/UI/LobbyCanvas.prefab` Create: `Assets/Prefabs/UI/LobbyCanvas.prefab`
...@@ -604,12 +714,14 @@ GameBoot (Empty, DontDestroyOnLoad) ...@@ -604,12 +714,14 @@ GameBoot (Empty, DontDestroyOnLoad)
- Runner.prefab - Runner.prefab
- Eye.prefab - Eye.prefab
- WorldItem.prefab - WorldItem.prefab
- WearablePickup.prefab
- Altar.prefab - Altar.prefab
- Shrine.prefab - Shrine.prefab
- SlowField.prefab - SlowField.prefab
- NoiseTrip.prefab - NoiseTrip.prefab
- PathBlocker.prefab - PathBlocker.prefab
- DynamicProp.prefab - DynamicProp.prefab
- ExtractionRoom.prefab
> **If you skip this, FishNet cannot spawn any networked objects and you'll get errors.** > **If you skip this, FishNet cannot spawn any networked objects and you'll get errors.**
...@@ -852,19 +964,21 @@ Once you have a working greybox game: ...@@ -852,19 +964,21 @@ Once you have a working greybox game:
| 1 | Runner | Prefabs/Player/ | YES | | 1 | Runner | Prefabs/Player/ | YES |
| 2 | Eye | Prefabs/Player/ | YES | | 2 | Eye | Prefabs/Player/ | YES |
| 3 | WorldItem | Prefabs/Props/ | YES | | 3 | WorldItem | Prefabs/Props/ | YES |
| 4 | Altar | Prefabs/Props/ | YES | | 4 | WearablePickup | Prefabs/Props/ | YES |
| 5 | Shrine | Prefabs/Props/ | YES | | 5 | Altar | Prefabs/Props/ | YES |
| 6 | SlowField | Prefabs/Props/ | YES | | 6 | Shrine | Prefabs/Props/ | YES |
| 7 | NoiseTrip | Prefabs/Props/ | YES | | 7 | SlowField | Prefabs/Props/ | YES |
| 8 | PathBlocker | Prefabs/Props/ | YES | | 8 | NoiseTrip | Prefabs/Props/ | YES |
| 9 | DynamicProp | Prefabs/Props/ | YES | | 9 | PathBlocker | Prefabs/Props/ | YES |
| 10 | NetworkManager | Prefabs/ | NO (manages others) | | 10 | DynamicProp | Prefabs/Props/ | YES |
| 11 | GameBoot | Prefabs/ | NO (singleton) | | 11 | ExtractionRoom | Prefabs/Props/ | YES |
| 12 | RunnerHUD | Prefabs/UI/ | NO (local UI) | | 12 | NetworkManager | Prefabs/ | NO (manages others) |
| 13 | EyeHUD | Prefabs/UI/ | NO (local UI) | | 13 | GameBoot | Prefabs/ | NO (singleton) |
| 14 | LobbyCanvas | Prefabs/UI/ | NO (local UI) | | 14 | RunnerHUD | Prefabs/UI/ | NO (local UI) |
| 15 | ResultsScreen | Prefabs/UI/ | NO (local UI) | | 15 | EyeHUD | Prefabs/UI/ | NO (local UI) |
| 16-20 | Room_* (5) | Prefabs/Rooms/ | NO (spawned by MapGen as children) | | 16 | LobbyCanvas | Prefabs/UI/ | NO (local UI) |
| 17 | ResultsScreen | Prefabs/UI/ | NO (local UI) |
| 18-22 | Room_* (5) | Prefabs/Rooms/ | NO (spawned by MapGen as children) |
--- ---
...@@ -875,8 +989,11 @@ Once you have a working greybox game: ...@@ -875,8 +989,11 @@ Once you have a working greybox game:
| 1 | GlobalRules | Resources/ | GlobalRules_SO | | 1 | GlobalRules | Resources/ | GlobalRules_SO |
| 2-6 | Loot_Coin/Gem/Idol/Crown/Relic | Resources/Items/Loot/ | ItemData_SO | | 2-6 | Loot_Coin/Gem/Idol/Crown/Relic | Resources/Items/Loot/ | ItemData_SO |
| 7-9 | Shrine_Mirror/Decoy/GrapplingHook | Resources/Items/Shrine/ | ShrineItemData_SO | | 7-9 | Shrine_Mirror/Decoy/GrapplingHook | Resources/Items/Shrine/ | ShrineItemData_SO |
| 10-12 | Trap_SlowField/NoiseTrip/PathBlocker | Resources/Items/Traps/ | TrapData_SO | | 10-13 | Shrine_FlashOrb/LinkedSprint/SilenceShroud/VitalityLink | Resources/Items/Shrine/ | ShrineItemData_SO |
| 13-17 | Room_Small_A/B, Medium_A/B, Large_A | Resources/Items/Rooms/ | RoomNode_SO | | 14-16 | Trap_SlowField/NoiseTrip/PathBlocker | Resources/Items/Traps/ | TrapData_SO |
| 17-21 | Room_Small_A/B, Medium_A/B, Large_A | Resources/Items/Rooms/ | RoomNode_SO |
| 22-24 | EyeType_Watcher/Predator/Trapper | Resources/EyeTypes/ | EyeType_SO |
| 25-30 | Wearable_Dash/WallRun/AirDash/Slide/Vault/Grapple | Resources/Items/Wearables/ | WearableData_SO |
--- ---
...@@ -893,12 +1010,15 @@ These are all accessible from the top menu bar after scripts compile: ...@@ -893,12 +1010,15 @@ These are all accessible from the top menu bar after scripts compile:
| `IStalk > Map Generation Tester` | Opens MapGenTestWindow — test generation without play mode | | `IStalk > Map Generation Tester` | Opens MapGenTestWindow — test generation without play mode |
| `IStalk > Room Validator` | Opens RoomValidator — batch-validate all room prefabs vs their SOs | | `IStalk > Room Validator` | Opens RoomValidator — batch-validate all room prefabs vs their SOs |
| `IStalk > Create > Room Prefab Setup` | Scaffolds a full room hierarchy (RoomBounds + 2 connectors + 1 slot) in scene | | `IStalk > Create > Room Prefab Setup` | Scaffolds a full room hierarchy (RoomBounds + 2 connectors + 1 slot) in scene |
| `IStalk > Create > Extraction Room Prefab Setup` | Scaffolds extraction room (trigger + 6 IncarnationProp slots) |
| `IStalk > Create > Loot Item SO` | Creates a new ItemData_SO asset at the correct path | | `IStalk > Create > Loot Item SO` | Creates a new ItemData_SO asset at the correct path |
| `IStalk > Create > Shrine Item SO` | Creates a new ShrineItemData_SO asset | | `IStalk > Create > Shrine Item SO` | Creates a new ShrineItemData_SO asset |
| `IStalk > Create > Trap SO` | Creates a new TrapData_SO asset | | `IStalk > Create > Trap SO` | Creates a new TrapData_SO asset |
| `IStalk > Create > Room Node SO` | Creates a new RoomNode_SO asset | | `IStalk > Create > Room Node SO` | Creates a new RoomNode_SO asset |
| `IStalk > Create > Eye Type SO` | Creates a new EyeType_SO asset in Resources/EyeTypes/ |
| `IStalk > Create > Wearable SO` | Creates a new WearableData_SO asset in Resources/Items/Wearables/ |
| `IStalk > Create > Global Rules SO` | Creates GlobalRules_SO (warns if one already exists) | | `IStalk > Create > Global Rules SO` | Creates GlobalRules_SO (warns if one already exists) |
| `IStalk > Quick Setup > Create All Resource Folders` | One-click creates the entire folder hierarchy | | `IStalk > Quick Setup > Create All Resource Folders` | One-click creates the entire folder hierarchy (now includes Wearables + EyeTypes) |
| `IStalk > Quick Setup > Setup Physics Layers` | Shows dialog with required layer assignments | | `IStalk > Quick Setup > Setup Physics Layers` | Shows dialog with required layer assignments |
### Quickstart: Use `IStalk > Quick Setup > Create All Resource Folders` FIRST ### Quickstart: Use `IStalk > Quick Setup > Create All Resource Folders` FIRST
...@@ -909,6 +1029,8 @@ Assets/Resources/Items/Loot/ ...@@ -909,6 +1029,8 @@ Assets/Resources/Items/Loot/
Assets/Resources/Items/Shrine/ Assets/Resources/Items/Shrine/
Assets/Resources/Items/Traps/ Assets/Resources/Items/Traps/
Assets/Resources/Items/Rooms/ Assets/Resources/Items/Rooms/
Assets/Resources/Items/Wearables/
Assets/Resources/EyeTypes/
Assets/Prefabs/Player/ Assets/Prefabs/Player/
Assets/Prefabs/Props/ Assets/Prefabs/Props/
Assets/Prefabs/UI/ Assets/Prefabs/UI/
...@@ -1101,6 +1223,101 @@ Shows per-item computed values: ...@@ -1101,6 +1223,101 @@ Shows per-item computed values:
--- ---
### EyeType_SO Custom Inspector (NEW)
When you select any `EyeType_SO` asset, the inspector shows:
**Ability Summary** — visual matrix showing which of the 4 abilities are enabled (checkmark/cross for each: Laser, Scan, Traps, Incarnation, Eyelids)
**Effective Stats Preview** — reads GlobalRules_SO and shows computed values:
```
Laser:
Cooldown: 2.0s (base 4.0 x 0.50)
Radius: 0.49m (base 0.35 x 1.40)
DPS potential: 0.50 shots/sec
Scan:
(Disabled for this type)
Traps:
(Disabled for this type)
Dread:
Starting Dread: 0 (default)
Regen rate: 3.0/s (default)
```
**Quick Actions:**
- "Generate ID from Name" — auto-fills `typeId`
- "Preset: Watcher" — sets Scan+Incarnation, faster scan, wider radius
- "Preset: Predator" — sets Laser+Incarnation+Eyelids, faster laser, wider beam
- "Preset: Trapper" — sets Traps+Scan+Incarnation, cheaper traps, +3 max active
**Validation:** Warns on empty ID, no abilities enabled, missing display name
---
### WearableData_SO Custom Inspector (NEW)
When you select any `WearableData_SO` asset, the inspector shows:
**Icon Preview** — shows assigned sprite at 64x64
**Balance Preview:**
```
Ability Info:
Type: Dash
Mode: Active (cooldown-based)
Cooldown: 3.0s
Max activation rate: 20.0 uses/min
Movement Granted:
Ground dash burst (directional)
GlobalRules cooldown: 3.0s
Economy:
Shrine cost: 4 loot
Throw range: 8.0m
Slot Impact:
Occupies 1 of 2 wearable slots
Weight: 0 (wearables are weightless)
```
**Quick Actions:**
- "Generate ID from Name"
- "Set as Active Ability" — sets isPassive=false, adds default cooldown
- "Set as Passive Ability" — sets isPassive=true, cooldown=0
**Validation:** Warns on empty ID, missing display name, no world prefab, active with 0 cooldown
---
### ExtractionRoom Custom Inspector (NEW)
When you select a GameObject with `ExtractionRoom.cs`:
**Setup Checklist:**
- Validates trigger collider is present and set as trigger
- Counts networked children (IncarnationProps) in the room
- Shows design notes about extraction mechanics
**Runtime State** (play mode only):
- Shows unlocked/timer/runners-inside in real-time
---
### GlobalRules_SO Custom Inspector (UPDATED)
Now includes additional foldout sections:
- **Eye Type Balance** — flash duration, team cooldown, per-type multiplier preview
- **Wearable Balance** — slot count, all ability cooldowns, throw range
- **Co-op Balance** — boost height/range, linked sprint stats, slow duration
- **Extraction Room** — channel duration, anti-AFK ritual duration, min voters
---
### Room Prefab Helper Components (Scene Gizmos) ### Room Prefab Helper Components (Scene Gizmos)
These MonoBehaviours are placed on room prefab children and draw colored gizmos in the Scene view: These MonoBehaviours are placed on room prefab children and draw colored gizmos in the Scene view:
...@@ -1317,6 +1534,9 @@ Open `Assets/Prefabs/Player/Runner.prefab`: ...@@ -1317,6 +1534,9 @@ Open `Assets/Prefabs/Player/Runner.prefab`:
| RunnerInteraction | `_interactRange` | 2.5 | | RunnerInteraction | `_interactRange` | 2.5 |
| NoiseEmitter | — | (auto) | | NoiseEmitter | — | (auto) |
| ShrineItemEffect | — | (auto, reads ItemDatabase.Instance) | | ShrineItemEffect | — | (auto, reads ItemDatabase.Instance) |
| RunnerWearableManager | — | (SyncList, no inspector refs, max 2 slots from GlobalRules) |
| RunnerCoopActions | — | (owner-only, reads GlobalRules for ranges) |
| RunnerBuffSystem | — | (SyncList, proximity checks via GlobalRules) |
| RunnerAnimator | `_animator` | Body child's Animator (add Animator component to Body) | | RunnerAnimator | `_animator` | Body child's Animator (add Animator component to Body) |
| RunnerFootsteps | `_audioSource` | AudioSource on root | | RunnerFootsteps | `_audioSource` | AudioSource on root |
| RunnerVisibility | `_bodyRenderer` | Body child's MeshRenderer | | RunnerVisibility | `_bodyRenderer` | Body child's MeshRenderer |
...@@ -1334,6 +1554,7 @@ Open `Assets/Prefabs/Player/Eye.prefab`: ...@@ -1334,6 +1554,7 @@ Open `Assets/Prefabs/Player/Eye.prefab`:
| Component | Field | Drag This | | Component | Field | Drag This |
|-----------|-------|-----------| |-----------|-------|-----------|
| EyeTypeController | — | (reads EyeType_SO from ItemDatabase at spawn, enables/disables ability scripts) |
| EyeCameraController | `_camera` | EyeCamera (Camera component) | | EyeCameraController | `_camera` | EyeCamera (Camera component) |
| EyeCameraController | `_panSpeed` | 15 | | EyeCameraController | `_panSpeed` | 15 |
| EyeCameraController | `_zoomSpeed` | 5 | | EyeCameraController | `_zoomSpeed` | 5 |
...@@ -1352,6 +1573,8 @@ Open `Assets/Prefabs/Player/Eye.prefab`: ...@@ -1352,6 +1573,8 @@ Open `Assets/Prefabs/Player/Eye.prefab`:
| EyeScanAbility | `_scanCost` | 25 | | EyeScanAbility | `_scanCost` | 25 |
| EyeScanAbility | `_scanDuration` | 3 | | EyeScanAbility | `_scanDuration` | 3 |
| EyeScanAbility | `_scanCooldown` | 15 | | EyeScanAbility | `_scanCooldown` | 15 |
| EyeEyelidController | — | (owner-only, RMB toggle, creates UI overlay at runtime) |
| EyeFlashReceiver | — | (server-side, checks eyelid state before applying blind) |
### 13.3 WorldItem Prefab — Full Wiring ### 13.3 WorldItem Prefab — Full Wiring
...@@ -1986,21 +2209,58 @@ Run through this ENTIRE checklist with 2+ players before calling the demo "done" ...@@ -1986,21 +2209,58 @@ Run through this ENTIRE checklist with 2+ players before calling the demo "done"
| 43 | Q/Escape → release possession | ☐ | | 43 | Q/Escape → release possession | ☐ |
| 44 | Cannot use abilities during grace period | ☐ | | 44 | Cannot use abilities during grace period | ☐ |
### Eye Types (NEW)
| # | Test | Pass? |
|---|------|-------|
| 45 | Eye type selection in lobby works | ☐ |
| 46 | Watcher: only Scan + Incarnation abilities available | ☐ |
| 47 | Predator: only Laser + Incarnation available, has eyelids | ☐ |
| 48 | Trapper: Traps + Scan + Incarnation, cheaper traps | ☐ |
| 49 | Predator eyelids: RMB closes eyes (full blackscreen) | ☐ |
| 50 | Flash orb hits → eyelids closed = flash dodged | ☐ |
| 51 | Flash orb hits → eyelids open = full blind duration | ☐ |
### Wearables & Co-op (NEW)
| # | Test | Pass? |
|---|------|-------|
| 52 | Pick up wearable from ground → equips to slot | ☐ |
| 53 | Max 2 wearables, third pickup rejected | ☐ |
| 54 | Dash wearable: press ability key → ground dash | ☐ |
| 55 | WallRun wearable: sprint + jump near wall → runs on wall | ☐ |
| 56 | Wearable dropped on death, visible to all | ☐ |
| 57 | Co-op boost: crouch at wall + teammate presses F → boosted up | ☐ |
| 58 | Bag throw: aim at teammate + press G → item flies to them | ☐ |
| 59 | Linked Sprint totem: both runners nearby get speed buff | ☐ |
| 60 | Anti-AFK ritual: 2 voters + 20s timer → AFK runner killed | ☐ |
### Extraction Room (NEW)
| # | Test | Pass? |
|---|------|-------|
| 61 | Room door locked until extraction threshold met | ☐ |
| 62 | All alive runners inside → timer starts (30s) | ☐ |
| 63 | Runner leaves room → timer resets | ☐ |
| 64 | Downed runner not required for extraction | ☐ |
| 65 | Timer completes → all runners extracted → Runners Win | ☐ |
| 66 | Eye can incarnate props inside room during extraction | ☐ |
### Match Flow ### Match Flow
| # | Test | Pass? | | # | Test | Pass? |
|---|------|-------| |---|------|-------|
| 45 | Lobby: Create room → get code | ☐ | | 67 | Lobby: Create room → get code | ☐ |
| 46 | Lobby: Join with code → player count updates | ☐ | | 68 | Lobby: Join with code → player count updates | ☐ |
| 47 | Start Game → all load GameScene together | ☐ | | 69 | Start Game → all load GameScene together | ☐ |
| 48 | Grace period: timer shows, Eye abilities blocked | ☐ | | 70 | Grace period: timer shows, Eye abilities blocked | ☐ |
| 49 | Grace ends → normal play begins | ☐ | | 71 | Grace ends → normal play begins | ☐ |
| 50 | Timer reaches 0 → "OVERTIME" message | ☐ | | 72 | Timer reaches 0 → "OVERTIME" message | ☐ |
| 51 | Rooms seal every N seconds (orange barriers appear) | ☐ | | 73 | Rooms seal every N seconds (orange barriers appear) | ☐ |
| 52 | Overtime timer expires → Eye wins | ☐ | | 74 | Overtime timer expires → Eye wins | ☐ |
| 53 | Results screen shows stats, buttons work | ☐ | | 75 | Results screen shows stats, buttons work | ☐ |
| 54 | Return to Lobby → back in lobby, can play again | ☐ | | 76 | Return to Lobby → back in lobby, can play again | ☐ |
| 55 | Player disconnect → game handles gracefully (no crash) | ☐ | | 77 | Player disconnect → game handles gracefully (no crash) | ☐ |
--- ---
......
...@@ -15,16 +15,42 @@ MonoBehaviour: ...@@ -15,16 +15,42 @@ MonoBehaviour:
m_PixelRect: m_PixelRect:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 61 y: 0
width: 1512 width: 2560
height: 921 height: 1440
m_ShowMode: 4 m_ShowMode: 4
m_Title: Project m_Title: Project
m_RootView: {fileID: 6} m_RootView: {fileID: 7}
m_MinSize: {x: 875, y: 300} m_MinSize: {x: 875, y: 300}
m_MaxSize: {x: 10000, y: 10000} m_MaxSize: {x: 10000, y: 10000}
m_Maximized: 1 m_Maximized: 0
--- !u!114 &2 --- !u!114 &2
MonoBehaviour:
m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
m_Name: InspectorWindow
m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.DockArea
m_Children: []
m_Position:
serializedVersion: 2
x: 2033.5
y: 0
width: 526.5
height: 1384
m_MinSize: {x: 276, y: 76}
m_MaxSize: {x: 4001, y: 4026}
m_ActualView: {fileID: 13}
m_Panes:
- {fileID: 13}
m_Selected: 0
m_LastSelected: 0
--- !u!114 &3
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -37,20 +63,21 @@ MonoBehaviour: ...@@ -37,20 +63,21 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: m_Children:
- {fileID: 9} - {fileID: 10}
- {fileID: 3} - {fileID: 4}
- {fileID: 2}
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 36 y: 36
width: 1512 width: 2560
height: 865 height: 1384
m_MinSize: {x: 300, y: 112} m_MinSize: {x: 400, y: 112}
m_MaxSize: {x: 24288, y: 16192} m_MaxSize: {x: 32384, y: 16192}
vertical: 0 vertical: 0
controlID: 1795 controlID: 239
draggingID: 0 draggingID: 0
--- !u!114 &3 --- !u!114 &4
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -60,23 +87,23 @@ MonoBehaviour: ...@@ -60,23 +87,23 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 1 m_EditorHideFlags: 1
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
m_Name: m_Name: InspectorWindow
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: [] m_Children: []
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 1154.5 x: 1565.5
y: 0 y: 0
width: 357.5 width: 468
height: 865 height: 1384
m_MinSize: {x: 276, y: 76} m_MinSize: {x: 277, y: 76}
m_MaxSize: {x: 4001, y: 4026} m_MaxSize: {x: 4002, y: 4026}
m_ActualView: {fileID: 14} m_ActualView: {fileID: 16}
m_Panes: m_Panes:
- {fileID: 14} - {fileID: 16}
m_Selected: 0 m_Selected: 0
m_LastSelected: 0 m_LastSelected: 0
--- !u!114 &4 --- !u!114 &5
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -93,16 +120,16 @@ MonoBehaviour: ...@@ -93,16 +120,16 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 286 width: 327.5
height: 512.5 height: 938.5
m_MinSize: {x: 201, y: 226} m_MinSize: {x: 201, y: 226}
m_MaxSize: {x: 4001, y: 4026} m_MaxSize: {x: 4001, y: 4026}
m_ActualView: {fileID: 15} m_ActualView: {fileID: 17}
m_Panes: m_Panes:
- {fileID: 15} - {fileID: 17}
m_Selected: 0 m_Selected: 0
m_LastSelected: 0 m_LastSelected: 0
--- !u!114 &5 --- !u!114 &6
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -118,18 +145,18 @@ MonoBehaviour: ...@@ -118,18 +145,18 @@ MonoBehaviour:
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 512.5 y: 938.5
width: 1154.5 width: 1565.5
height: 352.5 height: 445.5
m_MinSize: {x: 231, y: 276} m_MinSize: {x: 231, y: 276}
m_MaxSize: {x: 10001, y: 10026} m_MaxSize: {x: 10001, y: 10026}
m_ActualView: {fileID: 13} m_ActualView: {fileID: 15}
m_Panes: m_Panes:
- {fileID: 13} - {fileID: 15}
- {fileID: 18} - {fileID: 20}
m_Selected: 0 m_Selected: 0
m_LastSelected: 1 m_LastSelected: 1
--- !u!114 &6 --- !u!114 &7
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -142,22 +169,22 @@ MonoBehaviour: ...@@ -142,22 +169,22 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: m_Children:
- {fileID: 7}
- {fileID: 2}
- {fileID: 8} - {fileID: 8}
- {fileID: 3}
- {fileID: 9}
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 1512 width: 2560
height: 921 height: 1440
m_MinSize: {x: 875, y: 300} m_MinSize: {x: 875, y: 300}
m_MaxSize: {x: 10000, y: 10000} m_MaxSize: {x: 10000, y: 10000}
m_UseTopView: 1 m_UseTopView: 1
m_TopViewHeight: 36 m_TopViewHeight: 36
m_UseBottomView: 1 m_UseBottomView: 1
m_BottomViewHeight: 20 m_BottomViewHeight: 20
--- !u!114 &7 --- !u!114 &8
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -174,12 +201,12 @@ MonoBehaviour: ...@@ -174,12 +201,12 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 1512 width: 2560
height: 36 height: 36
m_MinSize: {x: 50, y: 50} m_MinSize: {x: 50, y: 50}
m_MaxSize: {x: 4000, y: 4000} m_MaxSize: {x: 4000, y: 4000}
m_ActualView: {fileID: 12} m_ActualView: {fileID: 14}
--- !u!114 &8 --- !u!114 &9
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -195,12 +222,12 @@ MonoBehaviour: ...@@ -195,12 +222,12 @@ MonoBehaviour:
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 901 y: 1420
width: 1512 width: 2560
height: 20 height: 20
m_MinSize: {x: 0, y: 0} m_MinSize: {x: 0, y: 0}
m_MaxSize: {x: 0, y: 0} m_MaxSize: {x: 0, y: 0}
--- !u!114 &9 --- !u!114 &10
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -213,20 +240,20 @@ MonoBehaviour: ...@@ -213,20 +240,20 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: m_Children:
- {fileID: 10} - {fileID: 11}
- {fileID: 5} - {fileID: 6}
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 1154.5 width: 1565.5
height: 865 height: 1384
m_MinSize: {x: 200, y: 112} m_MinSize: {x: 200, y: 112}
m_MaxSize: {x: 16192, y: 16192} m_MaxSize: {x: 16192, y: 16192}
vertical: 1 vertical: 1
controlID: 1796 controlID: 240
draggingID: 0 draggingID: 0
--- !u!114 &10 --- !u!114 &11
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -239,20 +266,20 @@ MonoBehaviour: ...@@ -239,20 +266,20 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: m_Children:
- {fileID: 4} - {fileID: 5}
- {fileID: 11} - {fileID: 12}
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 1154.5 width: 1565.5
height: 512.5 height: 938.5
m_MinSize: {x: 200, y: 56} m_MinSize: {x: 200, y: 56}
m_MaxSize: {x: 16192, y: 8096} m_MaxSize: {x: 16192, y: 8096}
vertical: 0 vertical: 0
controlID: 1844 controlID: 241
draggingID: 0 draggingID: 0
--- !u!114 &11 --- !u!114 &12
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -262,24 +289,76 @@ MonoBehaviour: ...@@ -262,24 +289,76 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 1 m_EditorHideFlags: 1
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
m_Name: m_Name: SceneView
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Children: [] m_Children: []
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 286 x: 327.5
y: 0 y: 0
width: 868.5 width: 1238
height: 512.5 height: 938.5
m_MinSize: {x: 202, y: 226} m_MinSize: {x: 202, y: 226}
m_MaxSize: {x: 4002, y: 4026} m_MaxSize: {x: 4002, y: 4026}
m_ActualView: {fileID: 16} m_ActualView: {fileID: 18}
m_Panes: m_Panes:
- {fileID: 16} - {fileID: 18}
- {fileID: 17} - {fileID: 19}
m_Selected: 0 m_Selected: 0
m_LastSelected: 1 m_LastSelected: 1
--- !u!114 &12 --- !u!114 &13
MonoBehaviour:
m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.InspectorWindow
m_MinSize: {x: 275, y: 50}
m_MaxSize: {x: 4000, y: 4000}
m_TitleContent:
m_Text: Inspector
m_Image: {fileID: -440750813802333266, guid: 0000000000000000d000000000000000, type: 0}
m_Tooltip:
m_TextWithWhitespace: "Inspector\u200B"
m_Pos:
serializedVersion: 2
x: 2033.5
y: 36
width: 525.5
height: 1358
m_SerializedDataModeController:
m_DataMode: 0
m_PreferredDataMode: 0
m_SupportedDataModes:
isAutomatic: 1
m_ViewDataDictionary: {fileID: 0}
m_OverlayCanvas:
m_LastAppliedPresetName: Default
m_SaveData: []
m_ContainerData: []
m_DynamicPanelContainerData: []
m_OverlaysVisible: 1
m_DynamicPanelBehavior: 0
m_ObjectsLockedBeforeSerialization:
- {fileID: 102900000, guid: 00000000000000001000000000000000, type: 3}
m_InstanceIDsLockedBeforeSerialization:
m_PreviewResizer:
m_CachedPref: 176
m_ControlHash: 1412526313
m_PrefName: Preview_InspectorPreview
m_LastInspectedObjectInstanceID: -1
m_LastVerticalScrollValue: 0
m_GlobalObjectId:
m_InspectorMode: 0
m_LockTracker:
m_IsLocked: 1
m_PreviewWindow: {fileID: 0}
--- !u!114 &14
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -302,7 +381,7 @@ MonoBehaviour: ...@@ -302,7 +381,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 1512 width: 2560
height: 36 height: 36
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
...@@ -573,7 +652,7 @@ MonoBehaviour: ...@@ -573,7 +652,7 @@ MonoBehaviour:
m_DynamicPanelContainerData: [] m_DynamicPanelContainerData: []
m_OverlaysVisible: 1 m_OverlaysVisible: 1
m_DynamicPanelBehavior: 0 m_DynamicPanelBehavior: 0
--- !u!114 &13 --- !u!114 &15
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -595,9 +674,9 @@ MonoBehaviour: ...@@ -595,9 +674,9 @@ MonoBehaviour:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 536.5 y: 974.5
width: 1153.5 width: 1564.5
height: 326.5 height: 419.5
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
...@@ -622,7 +701,7 @@ MonoBehaviour: ...@@ -622,7 +701,7 @@ MonoBehaviour:
m_SkipHidden: 0 m_SkipHidden: 0
m_SearchArea: 1 m_SearchArea: 1
m_Folders: m_Folders:
- Assets/Scripts/Network - Assets
m_Globs: [] m_Globs: []
m_ProductIds: m_ProductIds:
m_AnyWithAssetOrigin: 0 m_AnyWithAssetOrigin: 0
...@@ -630,10 +709,10 @@ MonoBehaviour: ...@@ -630,10 +709,10 @@ MonoBehaviour:
m_ImportLogFlags: 0 m_ImportLogFlags: 0
m_FilterByTypeIntersection: 0 m_FilterByTypeIntersection: 0
m_ViewMode: 1 m_ViewMode: 1
m_StartGridSize: 64 m_StartGridSize: 16
m_LastFolders: m_LastFolders:
- Assets/Scripts/Network - Assets
m_LastFoldersGridSize: -1 m_LastFoldersGridSize: 16
m_LastProjectPath: /Users/mahmoudaglan/istalk/IStalk m_LastProjectPath: /Users/mahmoudaglan/istalk/IStalk
m_LockTracker: m_LockTracker:
m_IsLocked: 0 m_IsLocked: 0
...@@ -641,12 +720,13 @@ MonoBehaviour: ...@@ -641,12 +720,13 @@ MonoBehaviour:
m_FolderTreeState: m_FolderTreeState:
scrollPos: {x: 0, y: 0} scrollPos: {x: 0, y: 0}
m_SelectedIDs: m_SelectedIDs:
- m_Data: 67370 - m_Data: 51986
m_LastClickedID: m_LastClickedID:
m_Data: 67370 m_Data: 51986
m_ExpandedIDs: m_ExpandedIDs:
- m_Data: 0 - m_Data: 0
- m_Data: 51926 - m_Data: 51986
- m_Data: 1000000000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
...@@ -679,7 +759,8 @@ MonoBehaviour: ...@@ -679,7 +759,8 @@ MonoBehaviour:
m_Data: 0 m_Data: 0
m_ExpandedIDs: m_ExpandedIDs:
- m_Data: 0 - m_Data: 0
- m_Data: 51926 - m_Data: 51986
- m_Data: 1000000000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
...@@ -707,8 +788,8 @@ MonoBehaviour: ...@@ -707,8 +788,8 @@ MonoBehaviour:
m_ResourceFile: m_ResourceFile:
m_ListAreaState: m_ListAreaState:
m_SelectedInstanceIDs: m_SelectedInstanceIDs:
- m_Data: 67370 - m_Data: 51986
m_LastClickedInstanceID: 67370 m_LastClickedInstanceID: 51986
m_HadKeyboardFocusLastEvent: 1 m_HadKeyboardFocusLastEvent: 1
m_ExpandedInstanceIDs: [] m_ExpandedInstanceIDs: []
m_RenameOverlay: m_RenameOverlay:
...@@ -737,10 +818,10 @@ MonoBehaviour: ...@@ -737,10 +818,10 @@ MonoBehaviour:
m_ResourceFile: m_ResourceFile:
m_NewAssetIndexInList: -1 m_NewAssetIndexInList: -1
m_ScrollPosition: {x: 0, y: 0} m_ScrollPosition: {x: 0, y: 0}
m_GridSize: 64 m_GridSize: 16
m_SkipHiddenPackages: 0 m_SkipHiddenPackages: 0
m_DirectoriesAreaWidth: 207 m_DirectoriesAreaWidth: 207
--- !u!114 &14 --- !u!114 &16
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -761,10 +842,10 @@ MonoBehaviour: ...@@ -761,10 +842,10 @@ MonoBehaviour:
m_TextWithWhitespace: "Inspector\u200B" m_TextWithWhitespace: "Inspector\u200B"
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 1155.5 x: 1565.5
y: 24 y: 36
width: 356.5 width: 466
height: 839 height: 1358
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
...@@ -778,7 +859,8 @@ MonoBehaviour: ...@@ -778,7 +859,8 @@ MonoBehaviour:
m_DynamicPanelContainerData: [] m_DynamicPanelContainerData: []
m_OverlaysVisible: 1 m_OverlaysVisible: 1
m_DynamicPanelBehavior: 0 m_DynamicPanelBehavior: 0
m_ObjectsLockedBeforeSerialization: [] m_ObjectsLockedBeforeSerialization:
- {fileID: 102900000, guid: 00000000000000001000000000000000, type: 3}
m_InstanceIDsLockedBeforeSerialization: m_InstanceIDsLockedBeforeSerialization:
m_PreviewResizer: m_PreviewResizer:
m_CachedPref: 160 m_CachedPref: 160
...@@ -789,9 +871,9 @@ MonoBehaviour: ...@@ -789,9 +871,9 @@ MonoBehaviour:
m_GlobalObjectId: m_GlobalObjectId:
m_InspectorMode: 0 m_InspectorMode: 0
m_LockTracker: m_LockTracker:
m_IsLocked: 0 m_IsLocked: 1
m_PreviewWindow: {fileID: 0} m_PreviewWindow: {fileID: 0}
--- !u!114 &15 --- !u!114 &17
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -813,9 +895,9 @@ MonoBehaviour: ...@@ -813,9 +895,9 @@ MonoBehaviour:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 24 y: 36
width: 285 width: 326.5
height: 486.5 height: 912.5
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
...@@ -832,7 +914,8 @@ MonoBehaviour: ...@@ -832,7 +914,8 @@ MonoBehaviour:
m_SceneHierarchy: m_SceneHierarchy:
m_TreeViewState: m_TreeViewState:
scrollPos: {x: 0, y: 0} scrollPos: {x: 0, y: 0}
m_SelectedIDs: [] m_SelectedIDs:
- m_Data: 51986
m_LastClickedID: m_LastClickedID:
m_Data: 0 m_Data: 0
m_ExpandedIDs: [] m_ExpandedIDs: []
...@@ -861,7 +944,7 @@ MonoBehaviour: ...@@ -861,7 +944,7 @@ MonoBehaviour:
m_IsLocked: 0 m_IsLocked: 0
m_CurrentSortingName: TransformSorting m_CurrentSortingName: TransformSorting
m_WindowGUID: 4c969a2b90040154d917609493e03593 m_WindowGUID: 4c969a2b90040154d917609493e03593
--- !u!114 &16 --- !u!114 &18
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -882,10 +965,10 @@ MonoBehaviour: ...@@ -882,10 +965,10 @@ MonoBehaviour:
m_TextWithWhitespace: "Scene\u200B" m_TextWithWhitespace: "Scene\u200B"
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 287 x: 327.5
y: 24 y: 36
width: 866.5 width: 1236
height: 486.5 height: 912.5
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
...@@ -1531,7 +1614,7 @@ MonoBehaviour: ...@@ -1531,7 +1614,7 @@ MonoBehaviour:
m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0} m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0}
m_LastSceneViewOrtho: 0 m_LastSceneViewOrtho: 0
m_Viewpoint: m_Viewpoint:
m_SceneView: {fileID: 16} m_SceneView: {fileID: 18}
m_CameraOverscanSettings: m_CameraOverscanSettings:
m_Opacity: 50 m_Opacity: 50
m_Scale: 1 m_Scale: 1
...@@ -1544,7 +1627,7 @@ MonoBehaviour: ...@@ -1544,7 +1627,7 @@ MonoBehaviour:
name: Contributors / Receivers name: Contributors / Receivers
section: Lighting section: Lighting
m_ViewIsLockedToObject: 0 m_ViewIsLockedToObject: 0
--- !u!114 &17 --- !u!114 &19
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -1565,10 +1648,10 @@ MonoBehaviour: ...@@ -1565,10 +1648,10 @@ MonoBehaviour:
m_TextWithWhitespace: "Game\u200B" m_TextWithWhitespace: "Game\u200B"
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 507 x: 193.5
y: 94 y: 97
width: 1532 width: 729
height: 790 height: 560.5
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
...@@ -1588,10 +1671,10 @@ MonoBehaviour: ...@@ -1588,10 +1671,10 @@ MonoBehaviour:
m_ShowGizmos: 0 m_ShowGizmos: 0
m_TargetDisplay: 0 m_TargetDisplay: 0
m_ClearColor: {r: 0, g: 0, b: 0, a: 0} m_ClearColor: {r: 0, g: 0, b: 0, a: 0}
m_TargetSize: {x: 1532, y: 769} m_TargetSize: {x: 729, y: 539.5}
m_TextureFilterMode: 0 m_TextureFilterMode: 0
m_TextureHideFlags: 61 m_TextureHideFlags: 61
m_RenderIMGUI: 0 m_RenderIMGUI: 1
m_EnterPlayModeBehavior: 0 m_EnterPlayModeBehavior: 0
m_UseMipMap: 0 m_UseMipMap: 0
m_VSyncEnabled: 0 m_VSyncEnabled: 0
...@@ -1603,10 +1686,10 @@ MonoBehaviour: ...@@ -1603,10 +1686,10 @@ MonoBehaviour:
m_VRangeLocked: 0 m_VRangeLocked: 0
hZoomLockedByDefault: 0 hZoomLockedByDefault: 0
vZoomLockedByDefault: 0 vZoomLockedByDefault: 0
m_HBaseRangeMin: -383 m_HBaseRangeMin: -182.25
m_HBaseRangeMax: 383 m_HBaseRangeMax: 182.25
m_VBaseRangeMin: -192.25 m_VBaseRangeMin: -134.875
m_VBaseRangeMax: 192.25 m_VBaseRangeMax: 134.875
m_HAllowExceedBaseRangeMin: 1 m_HAllowExceedBaseRangeMin: 1
m_HAllowExceedBaseRangeMax: 1 m_HAllowExceedBaseRangeMax: 1
m_VAllowExceedBaseRangeMin: 1 m_VAllowExceedBaseRangeMin: 1
...@@ -1624,30 +1707,30 @@ MonoBehaviour: ...@@ -1624,30 +1707,30 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 21 y: 21
width: 1532 width: 729
height: 769 height: 539.5
m_Scale: {x: 2, y: 2} m_Scale: {x: 2, y: 2}
m_Translation: {x: 766, y: 384.5} m_Translation: {x: 364.5, y: 269.75}
m_MarginLeft: 0 m_MarginLeft: 0
m_MarginRight: 0 m_MarginRight: 0
m_MarginTop: 0 m_MarginTop: 0
m_MarginBottom: 0 m_MarginBottom: 0
m_LastShownAreaInsideMargins: m_LastShownAreaInsideMargins:
serializedVersion: 2 serializedVersion: 2
x: -383 x: -182.25
y: -192.25 y: -134.875
width: 766 width: 364.5
height: 384.5 height: 269.75
m_MinimalGUI: 1 m_MinimalGUI: 1
m_defaultScale: 2 m_defaultScale: 2
m_LastWindowPixelSize: {x: 3064, y: 1580} m_LastWindowPixelSize: {x: 1458, y: 1121}
m_ClearInEditMode: 1 m_ClearInEditMode: 1
m_NoCameraWarning: 1 m_NoCameraWarning: 1
m_LowResolutionForAspectRatios: 01000000000000000000 m_LowResolutionForAspectRatios: 01000000000000000000
m_XRRenderMode: 0 m_XRRenderMode: 0
m_RenderTexture: {fileID: 0} m_RenderTexture: {fileID: 0}
m_showToolbar: 1 m_showToolbar: 1
--- !u!114 &18 --- !u!114 &20
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 52 m_ObjectHideFlags: 52
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
...@@ -1668,10 +1751,10 @@ MonoBehaviour: ...@@ -1668,10 +1751,10 @@ MonoBehaviour:
m_TextWithWhitespace: "Console\u200B" m_TextWithWhitespace: "Console\u200B"
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 2249
y: 598 y: 726.5
width: 1153.5 width: 920
height: 338 height: 250
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
......
...@@ -11,6 +11,56 @@ showStatusBar = false ...@@ -11,6 +11,56 @@ showStatusBar = false
scopes = { scopes = {
} }
providers = { providers = {
asset = {
active = true
priority = 25
defaultAction = null
}
scene = {
active = true
priority = 50
defaultAction = null
}
adb = {
active = false
priority = 2500
defaultAction = null
}
presets_provider = {
active = false
priority = -10
defaultAction = null
}
find = {
active = true
priority = 25
defaultAction = null
}
packages = {
active = false
priority = 90
defaultAction = null
}
profilermarkers = {
active = false
priority = 100
defaultAction = null
}
performance = {
active = false
priority = 100
defaultAction = null
}
store = {
active = false
priority = 100
defaultAction = null
}
log = {
active = false
priority = 210
defaultAction = null
}
} }
objectSelectors = { objectSelectors = {
} }
......
...@@ -41,10 +41,10 @@ Every match tightens. Dead Runners become minions. The Eye grows stronger. Extra ...@@ -41,10 +41,10 @@ Every match tightens. Dead Runners become minions. The Eye grows stronger. Extra
**Design test:** "Can this mechanic create a stalemate?" → If yes, add a timer, escalation trigger, or pressure valve that breaks the deadlock. **Design test:** "Can this mechanic create a stalemate?" → If yes, add a timer, escalation trigger, or pressure valve that breaks the deadlock.
### Pillar 5: Paranoia is a Feature ### Pillar 5: Together or Dead
Other Runners are unreliable. Objects might be watching. Safe rooms might be trapped. Trust is a resource that depletes. The social and environmental design should make players second-guess everything. Runners who coordinate survive. The game rewards proximity, timing, and sacrifice. Solo play is viable but the optimal path always involves teammates. The Eye exploits separation — clustered runners are harder to isolate but easier to area-deny.
**Design test:** "Is this interaction too predictable?" → If a Runner can always trust X, add a reason they might not be able to. **Design test:** "Can a solo runner accomplish this equally well?" → If yes, add a co-op bonus that makes the team version faster/safer.
--- ---
...@@ -52,7 +52,7 @@ Other Runners are unreliable. Objects might be watching. Safe rooms might be tra ...@@ -52,7 +52,7 @@ Other Runners are unreliable. Objects might be watching. Safe rooms might be tra
1. **NOT a precision shooter** — The Eye's skill is strategic, not mechanical aim. 1. **NOT a precision shooter** — The Eye's skill is strategic, not mechanical aim.
2. **NOT a survival horror resource game** — No health packs, ammo, or crafting. Parkour flow stays clean. 2. **NOT a survival horror resource game** — No health packs, ammo, or crafting. Parkour flow stays clean.
3. **NOT a team game with roles** — No Tank/Healer/DPS archetypes. All Runners have the same toolkit. Trust is optional. 3. **NOT a class-based team game** — No fixed roles or forced compositions. Any runner can equip any wearable. Teamwork is rewarded, not required for basic play.
4. **NOT a battle royale with shrinking zones** — Escalation is organic (minions + economy), not artificial. 4. **NOT a battle royale with shrinking zones** — Escalation is organic (minions + economy), not artificial.
5. **NOT a content-heavy live-service grind** — Replayability from procedural maps and player dynamics, not seasonal content. 5. **NOT a content-heavy live-service grind** — Replayability from procedural maps and player dynamics, not seasonal content.
......
# Update 01: Code Architecture Plan
**All paths relative to:** `Assets/Scripts/`
---
## New Scripts to Create
### Eye System (6 new files)
| File | Class | Purpose |
|------|-------|---------|
| `Data/EyeType_SO.cs` | `EyeType_SO : ScriptableObject` | Defines eye type: name, icon, hasEyelids, ability flags, stat overrides |
| `Eye/EyeTypeController.cs` | `EyeTypeController : NetworkBehaviour` | Master controller — reads EyeType_SO at spawn, enables/disables ability scripts, applies stat overrides to DreadEconomy/Laser |
| `Eye/EyeEyelidController.cs` | `EyeEyelidController : NetworkBehaviour` | Eyelid input (right-click toggle), SyncVar<bool> closed state, black overlay rendering, flash dodge validation |
| `Eye/EyeFlashReceiver.cs` | `EyeFlashReceiver : NetworkBehaviour` | Receives flash events from server, checks eyelid state, applies/skips blind overlay + ability lockout |
| `Data/WearableData_SO.cs` | `WearableData_SO : ScriptableObject` | Defines wearable: abilityType enum, cooldown, icon, description, isPassive, throwRange |
| `Runner/WearableAbilityType.cs` | `enum WearableAbilityType` | Dash, WallRun, AirDash, Slide, Vault, Grapple |
### Runner Co-op System (4 new files)
| File | Class | Purpose |
|------|-------|---------|
| `Runner/RunnerWearableManager.cs` | `RunnerWearableManager : NetworkBehaviour` | Manages 2 wearable slots (SyncList), enables/disables abilities on equip/unequip, handles drops on down |
| `Runner/RunnerCoopActions.cs` | `RunnerCoopActions : NetworkBehaviour` | Ledge boost (ServerRpc), bag throw (ServerRpc), anti-AFK vote ritual |
| `Runner/RunnerBuffSystem.cs` | `RunnerBuffSystem : NetworkBehaviour` | Active buff tracking (SyncList), timed expiry, proximity checks for aura effects |
| `Runner/GrappleController.cs` | `GrappleController : NetworkBehaviour` | Grapple hook physics — aim, fire, swing, detach. Separate script due to complexity. |
### Extraction Rework (2 new files)
| File | Class | Purpose |
|------|-------|---------|
| `Core/ExtractionRoom.cs` | `ExtractionRoom : NetworkBehaviour` | Room trigger zone, tracks runners inside, manages 30s timer, incarnation prop density |
| `Core/AntiAfkRitual.cs` | `AntiAfkRitual : NetworkBehaviour` | Vote kick logic — tracks voters, timer, kills target, removes from alive count |
---
## Existing Scripts to Modify
### Heavy Modifications (significant logic changes)
| File | Changes |
|------|---------|
| `Runner/RunnerController.cs` | **Strip all advanced movement.** Remove: dash, wall-run, air-dash, slide, vault logic. Keep: run, sprint, jump, crouch, coyote time, jump buffer, gravity. Add: public methods `EnableAbility(WearableAbilityType)` / `DisableAbility(WearableAbilityType)` that re-enable the stripped code paths conditionally. Each movement ability becomes a guarded block: `if (_dashEnabled) { /* existing dash code */ }` |
| `Runner/PlayerInventory.cs` | **Add wearable slot system.** New `SyncList<string> _wearableIds` (max 2). New methods: `TryEquipWearable(string id)`, `TryUnequipWearable(string id)`, `DropAllWearables(Vector3 pos)`. Modify `DropAll()` to also call `DropAllWearables()`. Add `OnWearableChanged` event. |
| `Core/ExtractionManager.cs` | **Replace per-player extraction with room-based.** Remove `_extractedPlayerIds` SyncList and `TryExtract(NetworkObject)`. Add: `SyncVar<bool> _roomUnlocked`, `SyncVar<float> _extractionTimer`, method `UnlockRoom()` (called when threshold met), server Update that ticks timer when all alive in room. Fire `OnAllExtracted` event. |
| `Core/GameManager.cs` | **Update win condition.** Subscribe to `ExtractionManager.OnAllExtracted` instead of individual `OnPlayerExtracted`. Remove individual extraction counting. Add Eye type selection relay from lobby. Update GracePeriod: Eye places traps (unchanged). |
| `Runner/RunnerInteraction.cs` | **Add co-op interactions.** Add: boost detection (scan for crouching runners near walls), throw targeting (aim at teammate while holding G), extraction room entry logic. Remove: any betrayal-related checks if they exist. |
| `Core/Shrine.cs` | **Expand stock categories.** Add wearable items and co-op buffs to `PopulateStock()`. Split UI display into categories: Loot Items / Wearables / Co-op Buffs. Add purchase validation for wearable slot availability. |
| `Data/GlobalRules_SO.cs` | **Add all new tuning constants** from Part E of design doc (eye type multipliers, wearable cooldowns, co-op values, extraction room timers). |
| `Runner/ShrineItemEffect.cs` | **Add Flash Orb effect.** When activated, fires `[ServerRpc] ServerTriggerFlash()` which broadcasts to Eye's `EyeFlashReceiver`. Add team cooldown check. |
| `Eye/EyeLaserController.cs` | **Accept stat overrides.** Add `public void ApplyTypeModifiers(float cooldownMult, float radiusMult)` called by `EyeTypeController` at spawn. Use modified values instead of raw `GlobalRules_SO` values. |
| `Eye/EyeScanAbility.cs` | **Accept stat overrides + disable capability.** Add `public void ApplyTypeModifiers(float cooldownMult, float radiusMult)`. Add `enabled` toggle driven by `EyeTypeController`. |
| `Eye/EyeTrapPlacer.cs` | **Accept stat overrides.** Add `public void ApplyTypeModifiers(float costMult, int maxActiveBonus)`. |
### Light Modifications (small additions)
| File | Changes |
|------|---------|
| `Eye/DreadEconomy.cs` | Add `public void ApplyStartOverride(float amount)` and `public void ApplyRegenOverride(float rate)` for eye type stat customization. |
| `Eye/EyeIncarnationController.cs` | No changes needed — works as-is in extraction room (just needs dense props placed by MapGenerator). |
| `Runner/RunnerHealth.cs` | On `_isDown` change: call `RunnerWearableManager.DropAllWearables()`. Add reference to wearable manager. |
| `Core/MapGenerator.cs` | Add extraction room generation: a locked room with dense `IncarnationProp` spawns, unlocked via `ExtractionManager`. |
| `Core/LobbyManager.cs` | Add Eye type selection UI hook. Sync selected `EyeType_SO` ID to all clients before match start. Pass to `PlayerSpawner`. |
| `Core/PlayerSpawner.cs` | When spawning Eye: read selected type, pass to `EyeTypeController` initialization. |
| `Runner/RunnerAnimator.cs` | Add animation triggers for: boost (being boosted up), throw (lobbing item), receiving item. |
| `UI/RunnerHUD.cs` | Add: wearable slot display (2 slots with icons + cooldown overlays), active buff indicators, extraction room timer. |
| `UI/EyeHUD.cs` | Add: eyelid state indicator, flash incoming warning, eye type ability display. |
| `UI/ShrineUI.cs` | Add: tabbed categories (Loot / Wearables / Buffs), wearable slot preview. |
| `Data/ItemDatabase.cs` | Add: `GetWearable(string id)` method, `WearableData_SO[]` cache loaded from Resources. |
| `Shared/WeightComponent.cs` | No changes — wearables are weightless by design. |
| `Runner/RunnerVisibility.cs` | No changes needed. |
---
## Scripts to Remove / Deprecate
None. All existing code is preserved — advanced movement code in `RunnerController` is guarded behind flags, not deleted. This allows wearables to re-enable the exact same physics code.
---
## New Prefab Requirements
| Prefab | Path | Components |
|--------|------|------------|
| Eye_Watcher | `Assets/Resources/Prefabs/Eye_Watcher.prefab` | All existing Eye scripts + EyeTypeController + EyeEyelidController + EyeFlashReceiver. Scan enabled, Laser weak, Traps standard. |
| Eye_Predator | `Assets/Resources/Prefabs/Eye_Predator.prefab` | All Eye scripts + EyeTypeController + EyeFlashReceiver (NO EyeEyelidController). Laser strong, NO Scan. |
| Eye_Trapper | `Assets/Resources/Prefabs/Eye_Trapper.prefab` | All Eye scripts + EyeTypeController + EyeEyelidController + EyeFlashReceiver. Traps buffed, NO Laser. |
| WorldItem_Wearable | `Assets/Resources/Prefabs/WorldItem_Wearable.prefab` | WorldItem + mesh/icon display for wearable ground pickups |
| ExtractionRoom | `Assets/Resources/Prefabs/Rooms/ExtractionRoom.prefab` | Room with trigger zone, locked door, 8-12 IncarnationProp slots, visual "eye" theming |
**Alternative to 3 prefabs:** Single Eye prefab with `EyeTypeController` that enables/disables scripts at runtime based on the `EyeType_SO`. This is cleaner — one prefab, dynamic configuration.
**Recommended approach:** Single Eye prefab. `EyeTypeController.OnStartServer()` reads the assigned `EyeType_SO` and:
- Sets `EyeScanAbility.enabled = hasAbility("Scan")`
- Sets `EyeLaserController.enabled = hasAbility("Laser")`
- Sets `EyeEyelidController.enabled = hasEyelids`
- Calls `ApplyTypeModifiers()` on each enabled ability
---
## Implementation Order (Dependencies)
### Phase 1: Data Foundation (no gameplay changes yet)
1. `EyeType_SO.cs` — define the ScriptableObject
2. `WearableData_SO.cs` — define the ScriptableObject
3. `WearableAbilityType.cs` — enum
4. `GlobalRules_SO.cs` — add new fields
5. `ItemDatabase.cs` — add wearable lookup
6. Create .asset files for all 3 eye types and 6 wearables
### Phase 2: Runner Movement Refactor
7. `RunnerController.cs` — strip advanced movement behind flags
8. `RunnerWearableManager.cs` — create, manages equip/unequip
9. `PlayerInventory.cs` — add wearable SyncList
10. `RunnerHealth.cs` — drop wearables on down
11. Test: runner spawns with basic movement only, equipping a wearable enables dash
### Phase 3: Eye Types
12. `EyeTypeController.cs` — master controller
13. `EyeEyelidController.cs` — eyelid input + blackscreen
14. `EyeFlashReceiver.cs` — flash blind logic
15. `EyeLaserController.cs` — accept modifiers
16. `EyeScanAbility.cs` — accept modifiers + disable
17. `EyeTrapPlacer.cs` — accept modifiers
18. `DreadEconomy.cs` — accept overrides
19. `ShrineItemEffect.cs` — add Flash Orb
20. `LobbyManager.cs` — eye type selection
21. `PlayerSpawner.cs` — pass type to controller
22. Test: each eye type spawns with correct abilities, flash/eyelid works
### Phase 4: Co-op Mechanics
23. `RunnerCoopActions.cs` — boost + throw
24. `RunnerInteraction.cs` — integrate boost/throw detection
25. `RunnerBuffSystem.cs` — proximity buff auras
26. `GrappleController.cs` — grapple physics
27. `RunnerAnimator.cs` — new animation triggers
28. Test: two runners can boost, throw items, linked sprint works
### Phase 5: Extraction Rework
29. `ExtractionRoom.cs` — room logic + timer
30. `ExtractionManager.cs` — rewrite to room-based
31. `AntiAfkRitual.cs` — vote kick
32. `GameManager.cs` — new win condition
33. `MapGenerator.cs` — extraction room generation
34. Test: team deposits loot, room unlocks, all gather, 30s countdown, extract
### Phase 6: UI + Polish
35. `RunnerHUD.cs` — wearable slots + buff indicators
36. `EyeHUD.cs` — eyelid indicator + type display
37. `ShrineUI.cs` — categories
38. `UI/EyeTypeSelectionUI.cs` (new) — lobby eye type picker
---
## Network Considerations
| Concern | Solution |
|---------|----------|
| Eye type must be known before spawn | Sync eye type ID in lobby phase via `LobbyManager` relay message. `PlayerSpawner` reads it. |
| Wearable equip/unequip must be server-authoritative | All equip/unequip via `[ServerRpc]`. Client sends request, server validates slot availability. |
| Flash must be validated server-side | `ShrineItemEffect` fires `ServerRpc` → server checks team cooldown → server fires flash to Eye's `EyeFlashReceiver` |
| Eyelid state must be checked server-side | `EyeFlashReceiver` checks `EyeEyelidController._eyelidsClosed` SyncVar on server when flash arrives |
| Extraction room presence must be server-authoritative | `ExtractionRoom` uses server-side trigger colliders (`OnTriggerEnter/Exit` with `[Server]` guard) |
| Boost/throw must prevent exploits | Server validates distance, state (crouching/alive), and cooldowns before executing |
| Anti-AFK must prevent abuse | Server requires majority vote (2+ of remaining alive runners), 20s uninterruptible channel |
---
## Editor Tooling Additions
| Tool | Purpose |
|------|---------|
| `Editor/EyeTypeEditor.cs` | Custom inspector for `EyeType_SO` — preview which abilities are enabled, stat modifiers visualized |
| `Editor/WearableDataEditor.cs` | Custom inspector for `WearableData_SO` — shows ability type, cooldown, weight (always 0), placement preview |
| `Editor/ExtractionRoomValidator.cs` | Validates extraction room prefab has enough IncarnationProp slots, trigger zone, locked door reference |
| `Editor/AnimatorSetupWizard.cs` | Guides setup of Runner Animator Controller — adds required states, transitions, parameters for new animations (boost, throw, receive, grapple) |
---
## Migration Notes
- No data loss — existing loot items, trap data, shrine items all remain valid
- `RunnerController` keeps ALL movement code — just wrapped in ability flags
- Existing `ExtractionManager` altar deposit flow is preserved (deposit loot → meet threshold)
- The change is what happens AFTER threshold: room unlocks instead of individual extraction
- Shrines expand their stock (additive, not destructive)
- Eye prefab gains scripts (additive)
# Update 01: Eye Types + Co-op Rework
**Status:** Approved Design — Ready for Implementation
**Affects:** Eye system, Runner movement, inventory, extraction, shrines, GlobalRules
**Breaking changes:** Yes — removes betrayal, strips innate runner abilities, reworks extraction
---
## Part A: Eye Types System
### Overview
The Eye is no longer a single archetype. Three distinct Eye types offer different playstyles, each built from the existing ability pool (Laser, Scan, TrapPlacer, Incarnation). Eye types are defined by `EyeType_SO` ScriptableObjects and selected in the lobby before match start.
### The 3 Eye Types
| Type | Primary Ability | Has Eyelids | Unique Trait |
|------|----------------|-------------|--------------|
| **The Watcher** | Scan | YES | Faster scan cooldown, wider scan radius, weaker laser (longer cooldown, smaller radius) |
| **The Predator** | Laser | NO | Stronger laser (shorter charge, larger radius), no scan ability, standard traps |
| **The Trapper** | Traps | YES | More active trap slots, cheaper trap Dread cost, no laser ability |
All three Eye types retain: Camera control, Dread Economy, Incarnation.
### Eyelid Mechanic
**Input:** Right-click (toggle) or hold middle-mouse (hold-to-close)
**When closed:**
- Screen goes fully black (render a black overlay, NOT disable camera)
- ALL abilities are locked (cannot fire, scan, place, or incarnate)
- UI remains visible: Dread bar, minimap silhouettes, flash duration indicator
- A "flash dodge window" indicator pulses if flash is incoming (audio cue only, no visual cheat)
**Flash interaction:**
- If flash hits while eyelids are OPEN → full blind for `flashDuration` seconds (white overlay, lose all vision + abilities)
- If flash hits while eyelids are CLOSED → completely dodged (0 effect)
- The Eye must predict when a flash is coming and commit to blindness preemptively
**Network sync:**
- `SyncVar<bool> _eyelidsClosed` on `EyeEyelidController`
- Server validates: if `_eyelidsClosed == true` when flash RPC arrives, flash is negated
- Runners see a visual indicator on the Eye (pupil narrows/closes) so they know the state
### Flash System (New)
**Source:** Shrine consumable item ("Flash Orb")
- Purchased at shrines using loot (costs defined in `ShrineItemData_SO`)
- One-use: activates immediately on use (Q key through existing `ShrineItemEffect`)
- Affects the Eye globally (not proximity-based — it's a flash against a giant eye in the sky)
- Flash duration: 4-6 seconds (tunable in `GlobalRules_SO`)
- Cooldown per team: 15 seconds (prevents flash-spam)
---
## Part B: Wearable Skill System
### Overview
Runners no longer have innate advanced movement. All parkour abilities (dash, wall-run, air-dash, slide, vault, grapple) are **wearable items** found in the world or purchased at shrines. This transforms the core loop: start basic → acquire skills → build your loadout.
### Base Runner (No Wearables)
- Run (8 m/s)
- Sprint (8 × 1.4 = 11.2 m/s, costs stamina)
- Jump (single, with coyote time + jump buffer)
- Crouch
- That's it. No dash, no wall-run, no slide, no vault, no air-dash.
### Wearable Slots
- **2 dedicated wearable slots** (separate from loot inventory)
- Wearables are **weightless** (do not affect WeightComponent)
- Wearables persist until manually unequipped or player is downed
- On death/down: wearables DROP as world items (other runners can pick them up)
### Skill Wearables (Initial Set)
| Wearable | Ability | Cooldown | Found/Bought |
|----------|---------|----------|--------------|
| Bracelet of Dash | Ground dash (5m, 0.2s) | 3s | World loot |
| Wall-Run Boots | Wall-run (min speed 4 m/s) | Passive (no CD) | World loot |
| Air Dash Wings | Mid-air dash (4m) | 5s | Shrine purchase |
| Slide Pads | Crouch-while-sprinting slides | Passive | World loot |
| Vault Gloves | Auto-vault low obstacles | Passive | World loot |
| Grappling Hook | Aim + fire grapple to surfaces | 8s | Shrine purchase (expensive) |
### Co-op Buff Items (Shrine Purchases)
| Item | Effect | Duration | Radius |
|------|--------|----------|--------|
| Linked Sprint Totem | +20% speed for all runners within 5m of holder | 30s | 5m |
| Flash Orb | Blinds the Eye for 5s (dodgeable with eyelids) | Instant | Global |
| Silence Shroud | Suppresses all noise from holder + nearby runners | 20s | 4m |
| Vitality Link | Nearby downed runners auto-revive over 10s | 10s | 3m |
### Inventory Architecture
```
PlayerInventory (existing)
├── _lootSlots: SyncList<string> (unchanged — loot items for extraction)
├── _wearableSlots: SyncList<string> (NEW — max 2, skill items)
└── _activeBuffs: SyncList<string> (NEW — timed co-op buffs, auto-expire)
```
---
## Part C: Co-op Mechanics
### Betrayal Removed
The "Paranoia" pillar is REPLACED with a new co-op pillar. Runners cannot hurt each other. All systems that allowed betrayal are removed.
### New Pillar: "Together or Dead"
Runners who work together move faster, reach more places, and survive longer. Solo play is viable but suboptimal — the game rewards proximity and coordination without forcing it.
**Design test:** "Can a solo runner do this equally well?" → If yes, add a co-op bonus or make the solo path harder/slower.
### Mechanic 1: Ledge Boost
**Setup:** One runner crouches against a wall at a high ledge. Another runner approaches and presses F.
**Execution:**
1. Boosted runner launches upward (2× jump height)
2. Boosting runner stays in place (vulnerable, crouched)
3. Creates access to high routes, shortcuts, hidden loot rooms
**Network flow:**
- Client presses F near crouching teammate + wall → `[ServerRpc] ServerRequestBoost(NetworkObject booster)`
- Server validates: booster is crouching, near wall, both alive, within range
- Server triggers boost physics on the requesting runner
- `[ObserversRpc]` plays boost VFX/SFX for all clients
### Mechanic 2: Bag Throw
**Setup:** Runner holds G (instead of tap to drop) while looking at a teammate within range.
**Execution:**
1. Item arcs toward targeted teammate
2. Range scales inversely with item weight (light items throw far, heavy items barely lob)
3. If teammate has inventory space, auto-pickup. If not, drops at their feet.
**Network flow:**
- Client holds G + aims at teammate → `[ServerRpc] ServerThrowItem(string itemId, NetworkObject target)`
- Server validates: item exists in inventory, target is alive runner, within max throw range
- Server removes from sender inventory, spawns temporary projectile
- On arrival: `TryAddItem` on target. If fails, spawn `WorldItem` at target position.
### Mechanic 3: Linked Sprint (Buff Item)
Purchased at shrine. When activated, creates a 5m aura around the holder for 30 seconds:
- All runners in radius get +20% sprint speed
- If ANY runner in the link gets hit by the Eye, ALL linked runners get a 1-second slow
- Creates a risk/reward clustering dynamic
- Visible tether VFX between linked runners
---
## Part D: Extraction Rework
### Overview
Extraction no longer works per-player. ALL alive runners must gather in the **Extraction Room** and survive 30 seconds together to escape.
### Flow
1. Team deposits loot at altars throughout the match (unchanged)
2. When `_teamLootDeposited >= extractionThreshold`, the Extraction Room unlocks
3. ALL alive runners must enter the Extraction Room
4. A 30-second countdown begins when all alive runners are inside
5. If ANY alive runner leaves the room, countdown pauses/resets
6. After 30 seconds: all runners in the room extract. Downed runners left behind.
### The Extraction Room
- A special room generated by `MapGenerator` (always present, locked until threshold met)
- **Dense with incarnation props** (eye paintings, gargoyles, mirrors) — the Eye can incarnate freely
- Eye objects are NOT new mechanics — they're existing `IncarnationProp` components at high density
- The room is a pressure cooker: runners move freely but the Eye has many attack vectors
### Anti-AFK / Griefer System
**Problem:** One alive player refuses to enter the room, blocking extraction.
**Solution:** Kill Ritual
1. 2+ runners can initiate a "Vote Kick" ritual near an altar (hold R together for 20s)
2. Target runner gets a warning ("Your team is voting to sacrifice you — return to group!")
3. After 20 seconds: target is killed (set to downed state, drops loot)
4. Target no longer counts as "alive" for extraction purposes
5. Requires majority of OTHER alive runners to participate
### ExtractionManager Changes
```csharp
// OLD: per-player extraction
TryExtract(NetworkObject player)
// NEW: room-based all-at-once
SyncVar<bool> _extractionRoomUnlocked
SyncVar<float> _extractionTimer (30s countdown)
SyncVar<int> _runnersInRoom
// Server Update: if all alive runners in room && timer > 0, tick down
// When timer hits 0: extract all runners in room
```
---
## Part E: GlobalRules_SO Additions
```csharp
[Header("Eye Types")]
public float flashDuration = 5f;
public float flashTeamCooldown = 15f;
public float watcherScanCooldownMultiplier = 0.6f;
public float predatorLaserCooldownMultiplier = 0.5f;
public float trapperDreadCostMultiplier = 0.6f;
public int trapperMaxActiveBonus = 3;
[Header("Wearables")]
public int maxWearableSlots = 2;
public float dashCooldown = 3f;
public float airDashCooldown = 5f;
public float grappleCooldown = 8f;
public float throwRangeBase = 8f;
public float throwRangeWeightScale = 0.5f;
[Header("Co-op")]
public float boostHeight = 2f;
public float boostRange = 1.5f;
public float linkedSprintBonus = 0.2f;
public float linkedSprintRadius = 5f;
public float linkedSprintSlowDuration = 1f;
[Header("Extraction Room")]
public float extractionChannelDuration = 30f;
public float antiAfkRitualDuration = 20f;
public int antiAfkMinVoters = 2;
```
---
## Part F: Pillar Updates
### REMOVED: Pillar 5 — "Paranoia is a Feature"
### NEW: Pillar 5 — "Together or Dead"
Runners who coordinate survive. The game rewards proximity, timing, and sacrifice. Solo play is viable but the optimal path always involves teammates. The Eye exploits separation — clustered runners are harder to isolate but easier to area-deny.
**Design test:** "Can a solo runner accomplish this equally well?" → If yes, add a co-op bonus that makes the team version faster/safer.
### UPDATED: Anti-Pillar 3
**OLD:** "NOT a team game with roles — No Tank/Healer/DPS archetypes. All Runners have the same toolkit. Trust is optional."
**NEW:** "NOT a class-based team game — No fixed roles or forced compositions. Any runner can equip any wearable. Teamwork is rewarded, not required for basic play."
---
## Part G: New ScriptableObjects Required
| Asset | Path | Purpose |
|-------|------|---------|
| `EyeType_SO` | `Assets/Scripts/Data/EyeType_SO.cs` | Defines an Eye type (abilities, eyelid, stat overrides) |
| `WearableData_SO` | `Assets/Scripts/Data/WearableData_SO.cs` | Defines a wearable skill item (ability type, cooldown, weight, icon) |
| Watcher.asset | `Assets/Resources/EyeTypes/Watcher.asset` | The Watcher Eye type |
| Predator.asset | `Assets/Resources/EyeTypes/Predator.asset` | The Predator Eye type |
| Trapper.asset | `Assets/Resources/EyeTypes/Trapper.asset` | The Trapper Eye type |
| BraceletOfDash.asset | `Assets/Resources/Items/Wearables/BraceletOfDash.asset` | Dash wearable |
| WallRunBoots.asset | `Assets/Resources/Items/Wearables/WallRunBoots.asset` | Wall-run wearable |
| AirDashWings.asset | `Assets/Resources/Items/Wearables/AirDashWings.asset` | Air dash wearable |
| SlidePads.asset | `Assets/Resources/Items/Wearables/SlidePads.asset` | Slide wearable |
| VaultGloves.asset | `Assets/Resources/Items/Wearables/VaultGloves.asset` | Vault wearable |
| GrapplingHook.asset | `Assets/Resources/Items/Wearables/GrapplingHook.asset` | Grapple wearable |
| FlashOrb.asset | `Assets/Resources/Items/Shrine/FlashOrb.asset` | Flash shrine item |
| LinkedSprintTotem.asset | `Assets/Resources/Items/Shrine/LinkedSprintTotem.asset` | Co-op buff |
| SilenceShroud.asset | `Assets/Resources/Items/Shrine/SilenceShroud.asset` | Co-op buff |
| VitalityLink.asset | `Assets/Resources/Items/Shrine/VitalityLink.asset` | Co-op buff |
# Planka (iStalk Board) API Reference
## Instance Details
| Key | Value |
|-----|-------|
| **URL** | `https://istalkboard.caprover.al-arcade.com` |
| **API Base** | `https://istalkboard.caprover.al-arcade.com/api` |
| **Admin Email** | `info@al-arcade.com` |
| **Admin Password** | `Alarcade123#` |
| **Admin Username** | `admin` |
| **API Key** | `jNNm5GuS_16Js4Z4GMc7TbE4e4OdSOIJMPUu0nde0` |
| **Admin User ID** | `1806474275138307073` |
| **Database** | `postgresql://planka:32fc5bf266a45f0c@srv-captain--istalkboard-db/planka` |
| **Docker Service** | `srv-captain--istalkboard` |
| **Version** | Planka 2.x (latest) |
---
## Authentication
### Option 1: API Key (Recommended for automation)
```bash
curl -H "X-Api-Key: jNNm5GuS_16Js4Z4GMc7TbE4e4OdSOIJMPUu0nde0" \
https://istalkboard.caprover.al-arcade.com/api/users/me
```
### Option 2: Bearer Token (Session-based)
```bash
# Step 1: Get pending token
RESPONSE=$(curl -s https://istalkboard.caprover.al-arcade.com/api/access-tokens \
-X POST -H "Content-Type: application/json" \
-d '{"emailOrUsername":"info@al-arcade.com","password":"Alarcade123#"}')
# If terms already accepted, response has "item" (the token)
# If terms need acceptance, response has "pendingToken" + "step": "accept-terms"
TOKEN=$(echo $RESPONSE | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('item',''))")
# Step 2: Use token
curl -H "Authorization: Bearer $TOKEN" \
https://istalkboard.caprover.al-arcade.com/api/projects
```
### Logout
```bash
DELETE /api/access-tokens/me
Authorization: Bearer <token>
```
---
## Projects
### List All Projects
```bash
GET /api/projects
```
Response includes: `items[]` (projects), `included.boards[]`, `included.projectManagers[]`
### Create Project
```bash
POST /api/projects
Content-Type: application/json
{
"name": "My Game Project",
"description": "A cool multiplayer game"
}
```
### Get Project Details
```bash
GET /api/projects/:id
```
Returns full project with all boards, lists, labels, members.
### Update Project
```bash
PATCH /api/projects/:id
Content-Type: application/json
{
"name": "Updated Name",
"description": "Updated description"
}
```
### Delete Project
```bash
DELETE /api/projects/:id
```
---
## Project Managers
### Add Manager to Project
```bash
POST /api/projects/:projectId/project-managers
Content-Type: application/json
{
"userId": "<user-id>"
}
```
### Remove Manager
```bash
DELETE /api/project-managers/:id
```
---
## Boards
### Create Board
```bash
POST /api/projects/:projectId/boards
Content-Type: application/json
{
"name": "Sprint 1",
"position": 1
}
```
### Get Board (with all lists, cards, labels)
```bash
GET /api/boards/:id
```
Returns full board data including `included.lists[]`, `included.cards[]`, `included.labels[]`, `included.cardMemberships[]`, `included.cardLabels[]`, `included.tasks[]`, `included.taskLists[]`
### Update Board
```bash
PATCH /api/boards/:id
Content-Type: application/json
{
"name": "Sprint 2",
"position": 2
}
```
### Delete Board
```bash
DELETE /api/boards/:id
```
---
## Board Memberships
### Add User to Board
```bash
POST /api/boards/:boardId/board-memberships
Content-Type: application/json
{
"userId": "<user-id>",
"role": "editor"
}
```
Roles: `editor`, `viewer`
### Update Membership Role
```bash
PATCH /api/board-memberships/:id
Content-Type: application/json
{
"role": "viewer",
"canComment": true
}
```
### Remove User from Board
```bash
DELETE /api/board-memberships/:id
```
---
## Labels
### Create Label
```bash
POST /api/boards/:boardId/labels
Content-Type: application/json
{
"name": "Bug",
"color": "berry-red",
"position": 1
}
```
Colors: `berry-red`, `pumpkin-orange`, `lagoon-blue`, `pink-tulip`, `light-mud`, `orange-peel`, `bright-moss`, `antique-blue`, `dark-granite`, `lagoon-blue`, `sunny-grass`, `morning-sky`, `light-orange`, `midnight-blue`, `tank-green`, `gun-metal`, `wet-moss`, `indian-flower`, `swim-lane`, `faded-blue`, `lemon-grass`
### Update Label
```bash
PATCH /api/labels/:id
Content-Type: application/json
{
"name": "Critical Bug",
"color": "berry-red"
}
```
### Delete Label
```bash
DELETE /api/labels/:id
```
---
## Lists (Columns)
### Create List
```bash
POST /api/boards/:boardId/lists
Content-Type: application/json
{
"name": "To Do",
"position": 1
}
```
### Get List
```bash
GET /api/lists/:id
```
### Update List
```bash
PATCH /api/lists/:id
Content-Type: application/json
{
"name": "In Progress",
"position": 2
}
```
### Sort Cards in List
```bash
POST /api/lists/:id/sort
Content-Type: application/json
{
"type": "name_asc"
}
```
Sort types: `name_asc`, `name_desc`, `dueDate_asc`, `dueDate_desc`, `createdAt_asc`, `createdAt_desc`
### Move All Cards from One List to Another
```bash
POST /api/lists/:id/move-cards
Content-Type: application/json
{
"listId": "<target-list-id>"
}
```
### Clear List (Archive all cards)
```bash
POST /api/lists/:id/clear
```
### Delete List
```bash
DELETE /api/lists/:id
```
---
## Cards
### List Cards in a List
```bash
GET /api/lists/:listId/cards
```
### Create Card
```bash
POST /api/lists/:listId/cards
Content-Type: application/json
{
"name": "Implement multiplayer lobby",
"position": 1,
"description": "Create the lobby system with matchmaking",
"dueDate": "2026-07-15T00:00:00.000Z",
"isDueDateCompleted": false
}
```
### Get Card Details
```bash
GET /api/cards/:id
```
### Update Card
```bash
PATCH /api/cards/:id
Content-Type: application/json
{
"name": "Updated card name",
"description": "Updated description with **markdown**",
"listId": "<new-list-id>",
"position": 3,
"dueDate": "2026-08-01T00:00:00.000Z",
"isDueDateCompleted": true,
"boardId": "<board-id>",
"stopwatch": {"startedAt": null, "total": 3600}
}
```
Move a card to another list by setting `listId`. Move to another board by setting `boardId`.
### Duplicate Card
```bash
POST /api/cards/:id/duplicate
Content-Type: application/json
{
"position": 2
}
```
### Delete Card
```bash
DELETE /api/cards/:id
```
---
## Card Memberships (Assign users to cards)
### Assign User to Card
```bash
POST /api/cards/:cardId/card-memberships
Content-Type: application/json
{
"userId": "<user-id>"
}
```
### Remove User from Card
```bash
DELETE /api/cards/:cardId/card-memberships/userId:<userId>
```
---
## Card Labels
### Add Label to Card
```bash
POST /api/cards/:cardId/card-labels
Content-Type: application/json
{
"labelId": "<label-id>"
}
```
### Remove Label from Card
```bash
DELETE /api/cards/:cardId/card-labels/labelId:<labelId>
```
---
## Task Lists (Checklists)
### Create Task List on Card
```bash
POST /api/cards/:cardId/task-lists
Content-Type: application/json
{
"name": "Acceptance Criteria",
"position": 1
}
```
### Get Task List
```bash
GET /api/task-lists/:id
```
### Update Task List
```bash
PATCH /api/task-lists/:id
Content-Type: application/json
{
"name": "Updated checklist name",
"position": 2
}
```
### Delete Task List
```bash
DELETE /api/task-lists/:id
```
---
## Tasks (Checklist items)
### Create Task
```bash
POST /api/task-lists/:taskListId/tasks
Content-Type: application/json
{
"name": "Write unit tests",
"position": 1,
"isCompleted": false
}
```
### Update Task
```bash
PATCH /api/tasks/:id
Content-Type: application/json
{
"name": "Write unit tests for lobby",
"isCompleted": true,
"position": 2
}
```
### Delete Task
```bash
DELETE /api/tasks/:id
```
---
## Comments
### List Comments on Card
```bash
GET /api/cards/:cardId/comments
```
### Create Comment
```bash
POST /api/cards/:cardId/comments
Content-Type: application/json
{
"text": "This looks good! Ready for review."
}
```
### Update Comment
```bash
PATCH /api/comments/:id
Content-Type: application/json
{
"text": "Updated comment text"
}
```
### Delete Comment
```bash
DELETE /api/comments/:id
```
---
## Attachments
### Upload Attachment to Card
```bash
POST /api/cards/:cardId/attachments
Content-Type: multipart/form-data
# Form field: "file" (the file to upload)
curl -X POST https://istalkboard.caprover.al-arcade.com/api/cards/:cardId/attachments \
-H "X-Api-Key: jNNm5GuS_16Js4Z4GMc7TbE4e4OdSOIJMPUu0nde0" \
-F "file=@/path/to/screenshot.png"
```
### Update Attachment
```bash
PATCH /api/attachments/:id
Content-Type: application/json
{
"name": "new-filename.png"
}
```
### Delete Attachment
```bash
DELETE /api/attachments/:id
```
---
## Users
### List All Users
```bash
GET /api/users
```
### Create User
```bash
POST /api/users
Content-Type: application/json
{
"email": "dev@al-arcade.com",
"password": "SecurePass123!",
"name": "Developer Name",
"username": "devuser"
}
```
### Get User
```bash
GET /api/users/:id
```
### Update User
```bash
PATCH /api/users/:id
Content-Type: application/json
{
"name": "New Name",
"phone": "+1234567890",
"organization": "Al Arcade"
}
```
### Update User Email
```bash
PATCH /api/users/:id/email
Content-Type: application/json
{
"email": "newemail@example.com"
}
```
### Update User Password
```bash
PATCH /api/users/:id/password
Content-Type: application/json
{
"password": "NewPassword123!"
}
```
### Update Username
```bash
PATCH /api/users/:id/username
Content-Type: application/json
{
"username": "newusername"
}
```
### Generate API Key for User
```bash
POST /api/users/:id/api-key
```
Returns the full API key in `included.apiKey` — only shown once!
### Delete User
```bash
DELETE /api/users/:id
```
---
## Notifications
### List Notifications
```bash
GET /api/notifications
```
### Get Notification
```bash
GET /api/notifications/:id
```
### Mark Notification as Read
```bash
PATCH /api/notifications/:id
Content-Type: application/json
{
"isRead": true
}
```
### Mark All as Read
```bash
POST /api/notifications/read-all
```
---
## Notification Services (Webhooks for boards/users)
### Create for Board
```bash
POST /api/boards/:boardId/notification-services
Content-Type: application/json
{
"type": "slack",
"url": "https://hooks.slack.com/services/...",
"isActive": true
}
```
### Create for User
```bash
POST /api/users/:userId/notification-services
Content-Type: application/json
{
"type": "telegram-bot",
"url": "https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>",
"isActive": true
}
```
Types: `slack`, `telegram-bot`, `discord`, `ms-teams`, `generic`
### Test Notification Service
```bash
POST /api/notification-services/:id/test
```
### Delete
```bash
DELETE /api/notification-services/:id
```
---
## Webhooks (Global)
### List Webhooks
```bash
GET /api/webhooks
```
### Create Webhook
```bash
POST /api/webhooks
Content-Type: application/json
{
"url": "https://your-server.com/webhook",
"isActive": true
}
```
### Update Webhook
```bash
PATCH /api/webhooks/:id
Content-Type: application/json
{
"url": "https://new-url.com/webhook",
"isActive": false
}
```
### Delete Webhook
```bash
DELETE /api/webhooks/:id
```
---
## Actions (Activity Log)
### Get Board Activity
```bash
GET /api/boards/:boardId/actions
```
### Get Card Activity
```bash
GET /api/cards/:cardId/actions
```
---
## Config (Admin only)
### Get Instance Config
```bash
GET /api/config
```
### Update Config
```bash
PATCH /api/config
Content-Type: application/json
{
"registrationEnabled": false,
"localRegistrationEnabled": true
}
```
### Test SMTP
```bash
POST /api/config/test-smtp
Content-Type: application/json
{
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"smtpUser": "user@example.com",
"smtpPassword": "pass",
"smtpFrom": "planka@example.com",
"smtpEmail": "test@example.com"
}
```
---
## Bootstrap (Initial load data)
```bash
GET /api/bootstrap
```
Returns everything needed to render the app: current user, all projects, boards, notifications count, etc.
---
## Swagger/OpenAPI
```bash
GET /swagger.json
```
Full OpenAPI spec for the instance.
---
## Complete Example: Create a Game Dev Board
```bash
API_KEY="jNNm5GuS_16Js4Z4GMc7TbE4e4OdSOIJMPUu0nde0"
BASE="https://istalkboard.caprover.al-arcade.com/api"
AUTH="-H 'X-Api-Key: $API_KEY'"
# 1. Create project
PROJECT=$(curl -s $BASE/projects \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"iStalk Game","description":"Multiplayer stalking game development"}')
PROJECT_ID=$(echo $PROJECT | python3 -c "import sys,json; print(json.load(sys.stdin)['item']['id'])")
# 2. Create board
BOARD=$(curl -s $BASE/projects/$PROJECT_ID/boards \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Sprint 1","position":1}')
BOARD_ID=$(echo $BOARD | python3 -c "import sys,json; print(json.load(sys.stdin)['item']['id'])")
# 3. Create labels
curl -s $BASE/boards/$BOARD_ID/labels \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Bug","color":"berry-red","position":1}'
curl -s $BASE/boards/$BOARD_ID/labels \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Feature","color":"lagoon-blue","position":2}'
curl -s $BASE/boards/$BOARD_ID/labels \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Urgent","color":"pumpkin-orange","position":3}'
# 4. Create lists (columns)
TODO=$(curl -s $BASE/boards/$BOARD_ID/lists \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Backlog","position":1}')
TODO_ID=$(echo $TODO | python3 -c "import sys,json; print(json.load(sys.stdin)['item']['id'])")
curl -s $BASE/boards/$BOARD_ID/lists \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"In Progress","position":2}'
curl -s $BASE/boards/$BOARD_ID/lists \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Review","position":3}'
curl -s $BASE/boards/$BOARD_ID/lists \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Done","position":4}'
# 5. Create a card
CARD=$(curl -s $BASE/lists/$TODO_ID/cards \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Implement lobby system","position":1,"description":"Create multiplayer lobby with matchmaking"}')
CARD_ID=$(echo $CARD | python3 -c "import sys,json; print(json.load(sys.stdin)['item']['id'])")
# 6. Add a checklist to the card
TASKLIST=$(curl -s $BASE/cards/$CARD_ID/task-lists \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Requirements","position":1}')
TASKLIST_ID=$(echo $TASKLIST | python3 -c "import sys,json; print(json.load(sys.stdin)['item']['id'])")
# 7. Add tasks to checklist
curl -s $BASE/task-lists/$TASKLIST_ID/tasks \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Design lobby UI","position":1,"isCompleted":false}'
curl -s $BASE/task-lists/$TASKLIST_ID/tasks \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Implement matchmaking logic","position":2,"isCompleted":false}'
curl -s $BASE/task-lists/$TASKLIST_ID/tasks \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Add FishNet networking","position":3,"isCompleted":false}'
# 8. Add comment
curl -s $BASE/cards/$CARD_ID/comments \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Priority for next sprint. Need this before alpha release."}'
echo "Done! Project: $PROJECT_ID, Board: $BOARD_ID"
```
---
## Common Response Patterns
### Success (Single item)
```json
{
"item": { ... },
"included": { ... }
}
```
### Success (List)
```json
{
"items": [ ... ],
"included": { ... }
}
```
### Error
```json
{
"code": "E_NOT_FOUND",
"message": "Board not found"
}
```
Error codes: `E_UNAUTHORIZED`, `E_FORBIDDEN`, `E_NOT_FOUND`, `E_MISSING_OR_INVALID_PARAMS`, `E_CONFLICT`
---
## Notes
- All IDs are snowflake-style strings (e.g., `"1806474275138307073"`)
- Positions are floating point numbers — use integers like 1, 2, 3 for simplicity
- Dates are ISO 8601 format: `"2026-07-15T00:00:00.000Z"`
- Markdown is supported in card descriptions and comments
- The API key never expires — only invalidated when a new one is generated
- File uploads use `multipart/form-data`, everything else uses `application/json`
- WebSocket support exists for real-time updates (Socket.IO on the same URL)
- The `included` object in responses contains related entities to avoid N+1 queries
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment