This commit is contained in:
2026-08-21 17:59:13 +03:00
parent b98901c366
commit f6a7547e84
951 changed files with 909007 additions and 70938 deletions
+22 -1
View File
@@ -510,7 +510,28 @@ MonoBehaviour:
- Assets/JMO Assets/WarFX/Demo/Assets/WFX_Demo_M4.fbx
- Assets/JMO Assets/WarFX/Mesh/CirclePlane12v.FBX
- Assets/JMO Assets/WarFX/Mesh/Plane1x1.FBX
uvOverlapAssetList: 00000000ffffffff010100000101000001010000000000000000000000000000000000000000000001010000000000000000000001010000000000000000000000000000000000000000000000000000010100000000000000000000000000000101000000000000010100000000000000000000000000000101000000000000010100000000000001010000000000000000000001010000010100000000000000000000010100000000000000000000010100000101000000000000000000000000000000000000000000000000000000000000000000000101000000000000010100000101000000000000000000000000000000000000000000000000000000000000
- Assets/UnityTechnologies/EffectExamples/Misc Effects/Animations/@EllenIdle.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/Railing.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/SideWall.fbx
- Assets/UnityTechnologies/EffectExamples/Misc Effects/Models/Ellen.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/Stairs.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/AreaLine.fbx
- Assets/UnityTechnologies/EffectExamples/Magic Effects/Models/IceLance.FBX
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/FloorLong.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/SmallLight.fbx
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/Step.fbx
- Assets/UnityTechnologies/EffectExamples/WaterEffects/Models/DropSplash.fbx
- Assets/UnityTechnologies/EffectExamples/Misc Effects/Models/Candle.fbx
- Assets/UnityTechnologies/EffectExamples/Magic Effects/Models/IceShard.FBX
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/EndWall.fbx
- Assets/UnityTechnologies/EffectExamples/Magic Effects/Models/RockDebris.FBX
- Assets/UnityTechnologies/EffectExamples/Magic Effects/Models/RockSpike.FBX
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/CornerWall.fbx
- Assets/UnityTechnologies/EffectExamples/Misc Effects/Models/Flakes.fbx
- Assets/UnityTechnologies/EffectExamples/Misc Effects/Models/FireFly.fbx
- Assets/UnityTechnologies/EffectExamples/Smoke & Steam Effects/Models/RockDebris.FBX
- Assets/UnityTechnologies/EffectExamples/Shared/Environment/Models/LongOverhangWall.fbx
uvOverlapAssetList: 00000000ffffffff010100000101000001010000000000000000000000000000000000000000000001010000000000000000000001010000000000000000000000000000000000000000000000000000010100000000000000000000000000000101000000000000010100000000000000000000000000000101000000000000010100000000000001010000000000000000000001010000010100000000000000000000010100000000000000000000010100000101000000000000000000000000000000000000000000000000000000000000000000000101000000000000010100000101000000000000000000000000000000000000000000000000000000000000000000000101000001010000000000000000000000000000010100000000000001010000000000000000000001010000010100000101000000000000000000000101000001010000010100000000000001010000
xatlasWarningShown: 0
foundCompatibleSetup: 1
gpuName_2: NVIDIA GeForce RTX 5080
@@ -0,0 +1,22 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1dfcdffb89f04cd468e4f8386b4f67e8, type: 3}
m_Name: MapEnemyBalance
m_EditorClassIdentifier:
targetsPerWave: 5
simulationStep: 0.1
minimumSpawnDistanceFromBase: 8.35
spawnPositionAttempts: 32
movementSpeedMultiplier: 0.5
attackDelayMultiplier: 1.5
healthMultiplier: 1
baseDamageMultiplier: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 662ffa97b173faf47a6eebcb4c6abf50
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -7,6 +7,8 @@ public static class MapRadarNetworkSetup
{
private const string LeverPrefabPath =
"Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab";
private const string EnemyBalancePath =
"Assets/CONTENT/FEATURES/MapRadar/Configs/MapEnemyBalance.asset";
[MenuItem("FORT-BO/Map Radar/Configure Network Prototype")]
public static void ConfigureNetworkPrototype()
@@ -31,6 +33,7 @@ public static class MapRadarNetworkSetup
GetOrAdd<NetworkObject>(mapController.gameObject);
NetworkBaseHealth baseHealth = GetOrAdd<NetworkBaseHealth>(mapController.gameObject);
MapControlBinder binder = GetOrAdd<MapControlBinder>(mapController.gameObject);
MapEnemyBalanceConfig enemyBalance = GetOrCreateEnemyBalance();
Transform controlsRoot = GetOrCreateChild(
mapController.transform.root,
@@ -62,6 +65,7 @@ public static class MapRadarNetworkSetup
SetObject(map, "roundGameLoop",
Object.FindFirstObjectByType<RoundGameLoopController>());
SetObject(map, "baseHealth", baseHealth);
SetObject(map, "enemyBalance", enemyBalance);
map.ApplyModifiedPropertiesWithoutUndo();
SerializedObject binding = new(binder);
@@ -82,10 +86,24 @@ public static class MapRadarNetworkSetup
Debug.Log(
"[Map] Network prototype configured: X/Y wheels, fire lever, " +
"scan button and base health are connected.",
"scan button, enemy balance and base health are connected.",
mapController);
}
private static MapEnemyBalanceConfig GetOrCreateEnemyBalance()
{
MapEnemyBalanceConfig config =
AssetDatabase.LoadAssetAtPath<MapEnemyBalanceConfig>(EnemyBalancePath);
if (config != null)
return config;
config = ScriptableObject.CreateInstance<MapEnemyBalanceConfig>();
AssetDatabase.CreateAsset(config, EnemyBalancePath);
EditorUtility.SetDirty(config);
AssetDatabase.SaveAssets();
return config;
}
private static NetworkPhysicalWheelInteractable CreateWheel(
Transform parent,
string name,
@@ -9,12 +9,35 @@ public sealed class MapControlBinder : MonoBehaviour
[SerializeField] private NetworkPhysicalLeverInteractable fireLever;
[SerializeField] private NetworkButtonInteractable scanButton;
private bool controlsBound;
private bool initialValuesApplied;
private void Reset() => mapController = GetComponent<MapController>();
private void OnEnable()
private void OnEnable() => TryBindControls();
private void OnDisable() => UnbindControls();
private void Update()
{
TryBindControls();
if (!controlsBound || initialValuesApplied ||
horizontalWheel == null || verticalWheel == null)
{
return;
}
initialValuesApplied = true;
HandleHorizontalChanged(horizontalWheel.NormalizedValue);
HandleVerticalChanged(verticalWheel.NormalizedValue);
}
private void TryBindControls()
{
if (controlsBound || !AreNetworkObjectsReady())
return;
if (horizontalWheel != null)
horizontalWheel.ValueChanged += HandleHorizontalChanged;
if (verticalWheel != null)
@@ -23,10 +46,15 @@ public sealed class MapControlBinder : MonoBehaviour
fireLever.StateChanged += HandleFireChanged;
if (scanButton != null)
scanButton.Pressed += HandleScanPressed;
controlsBound = true;
}
private void OnDisable()
private void UnbindControls()
{
if (!controlsBound)
return;
if (horizontalWheel != null)
horizontalWheel.ValueChanged -= HandleHorizontalChanged;
if (verticalWheel != null)
@@ -35,20 +63,18 @@ public sealed class MapControlBinder : MonoBehaviour
fireLever.StateChanged -= HandleFireChanged;
if (scanButton != null)
scanButton.Pressed -= HandleScanPressed;
controlsBound = false;
initialValuesApplied = false;
}
private void Update()
private bool AreNetworkObjectsReady()
{
if (initialValuesApplied || mapController == null || !mapController.IsSpawned ||
horizontalWheel == null || verticalWheel == null ||
!horizontalWheel.IsSpawned || !verticalWheel.IsSpawned)
{
return;
}
initialValuesApplied = true;
HandleHorizontalChanged(horizontalWheel.NormalizedValue);
HandleVerticalChanged(verticalWheel.NormalizedValue);
return mapController != null && mapController.IsSpawned &&
(horizontalWheel == null || horizontalWheel.IsSpawned) &&
(verticalWheel == null || verticalWheel.IsSpawned) &&
(fireLever == null || fireLever.IsSpawned) &&
(scanButton == null || scanButton.IsSpawned);
}
private void HandleHorizontalChanged(float value) =>
+125 -39
View File
@@ -14,11 +14,11 @@ public sealed class MapController : NetworkBehaviour
[SerializeField] private MapPlayer mapPlayer;
[SerializeField] private RoundGameLoopController roundGameLoop;
[SerializeField] private NetworkBaseHealth baseHealth;
[SerializeField] private MapEnemyBalanceConfig enemyBalance;
[Header("Target System")]
[SerializeField] private MapTargetController targetController;
[SerializeField] private Transform targetParent;
[SerializeField, Min(0.02f)] private float simulationStep = 0.1f;
[Header("Radar System")]
[SerializeField] private MapRadarController radarController;
@@ -30,11 +30,12 @@ public sealed class MapController : NetworkBehaviour
[SerializeField] private Transform relayY;
[SerializeField] private Transform crosshairVisual;
[Header("Pseudo Shot")]
[Header("Shot Rules")]
[SerializeField, Min(0.01f)] private float hitRadius = 0.65f;
[SerializeField, Min(1)] private int shotDamage = 1;
[SerializeField] private MissileType pseudoMissileType = MissileType.Standard;
[SerializeField] private bool ignoreMissileTypeForPrototype = true;
[FormerlySerializedAs("ignoreMissileTypeForPrototype")]
[SerializeField] private bool ignoreMissileTypeRequirements = true;
[Header("Network Events")]
[SerializeField] private UnityEvent<int> waveStarted;
@@ -42,6 +43,9 @@ public sealed class MapController : NetworkBehaviour
[SerializeField] private UnityEvent allTargetsResolved;
[SerializeField] private UnityEvent allTargetsDestroyed;
[Header("Shot Presentation")]
[SerializeField] private MapImpactMarkerView impactMarkerView;
private readonly NetworkVariable<Vector2> crosshairCoordinates = new(
new Vector2(5f, 5f), NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
@@ -77,6 +81,10 @@ public sealed class MapController : NetworkBehaviour
public int CurrentWave => currentWave.Value;
public int ActiveTargetCount => activeTargets.Count;
public bool WaveIsActive => waveIsActive;
public MapEnemyBalanceConfig EnemyBalance => enemyBalance;
public float HitRadius => hitRadius;
public int ShotDamage => shotDamage;
public bool IgnoreMissileTypeRequirements => ignoreMissileTypeRequirements;
public double RadarCooldownRemaining => IsSpawned && NetworkManager != null
? Math.Max(0d, radarReadyAt.Value - NetworkManager.ServerTime.Time)
: 0d;
@@ -90,7 +98,6 @@ public sealed class MapController : NetworkBehaviour
private void OnValidate()
{
simulationStep = Mathf.Max(0.02f, simulationStep);
hitRadius = Mathf.Max(0.01f, hitRadius);
shotDamage = Mathf.Max(1, shotDamage);
}
@@ -143,11 +150,12 @@ public sealed class MapController : NetworkBehaviour
if (!IsServer || !IsSpawned || !waveIsActive)
return;
float step = enemyBalance != null ? enemyBalance.SimulationStep : 0.1f;
simulationAccumulator += Time.deltaTime;
while (simulationAccumulator >= simulationStep)
while (simulationAccumulator >= step)
{
simulationAccumulator -= simulationStep;
SimulateTargets(simulationStep);
simulationAccumulator -= step;
SimulateTargets(step);
}
}
@@ -176,11 +184,20 @@ public sealed class MapController : NetworkBehaviour
if (!IsSpawned)
return;
if (IsServer)
ResolvePseudoShotOnServer();
ResolveShotOnServer(pseudoMissileType, ignoreMissileTypeRequirements);
else
RequestPseudoFireRpc();
}
public bool RequestShot(MissileType missileType)
{
if (!IsSpawned || !IsServer || missileType == MissileType.None)
return false;
ResolveShotOnServer(missileType, ignoreMissileTypeRequirements);
return true;
}
public void RequestRadarScan()
{
if (!IsSpawned)
@@ -224,7 +241,8 @@ public sealed class MapController : NetworkBehaviour
private void SetCrosshairCoordinatesRpc(Vector2 coordinates) =>
SetCrosshairCoordinatesOnServer(coordinates);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestPseudoFireRpc() => ResolvePseudoShotOnServer();
private void RequestPseudoFireRpc() =>
ResolveShotOnServer(pseudoMissileType, ignoreMissileTypeRequirements);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestRadarScanRpc() => ScanRadarOnServer();
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
@@ -253,10 +271,12 @@ public sealed class MapController : NetworkBehaviour
private bool StartNextWaveOnServer()
{
if (!IsServer || targetController == null ||
if (!IsServer || targetController == null || enemyBalance == null ||
!targetController.HasValidConfiguration())
{
Debug.LogWarning("[Map] Cannot start target wave: map or target configs are incomplete.", this);
Debug.LogWarning(
"[Map] Cannot start target wave: map, enemy balance or target configs are incomplete.",
this);
return false;
}
@@ -268,19 +288,18 @@ public sealed class MapController : NetworkBehaviour
waveIsActive = true;
simulationAccumulator = 0f;
for (int index = 0; index < targetController.TargetsPerWave; index++)
for (int index = 0; index < enemyBalance.TargetsPerWave; index++)
{
int configIndex = targetController.GetRandomValidConfigIndex();
TargetConfig config = targetController.GetConfig(configIndex);
if (config == null)
continue;
Vector2 coordinates = new(
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum),
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum));
Vector2 coordinates = CreateSpawnCoordinates();
activeTargets.Add(new MapTargetNetworkState(
nextTargetId++, configIndex, coordinates,
config.Health, config.TimeBeforeAttack));
enemyBalance.GetHealth(config),
enemyBalance.GetAttackDelay(config)));
initialTargetCount++;
}
@@ -303,11 +322,12 @@ public sealed class MapController : NetworkBehaviour
}
bool isAirTarget = config.TargetPrefab is AirTarget;
if (isAirTarget && mapPlayer != null && config.MovementSpeed > 0f)
float movementSpeed = enemyBalance.GetMovementSpeed(config);
if (isAirTarget && mapPlayer != null && movementSpeed > 0f)
{
Vector2 destination = mapPlayer.MapCoordinates;
state.Coordinates = Vector2.MoveTowards(
state.Coordinates, destination, config.MovementSpeed * deltaTime);
state.Coordinates, destination, movementSpeed * deltaTime);
if (Vector2.Distance(state.Coordinates, destination) <= config.StoppingDistance)
{
ResolveEscapedTarget(index, state, config);
@@ -333,7 +353,9 @@ public sealed class MapController : NetworkBehaviour
private void ResolveEscapedTarget(int index, MapTargetNetworkState target, TargetConfig config)
{
int damage = config != null ? config.BaseDamage : 1;
int damage = enemyBalance != null
? enemyBalance.GetBaseDamage(config)
: config != null ? config.BaseDamage : 1;
escapedTargetCount++;
activeTargets.RemoveAt(index);
if (baseHealth != null)
@@ -341,11 +363,15 @@ public sealed class MapController : NetworkBehaviour
Debug.Log($"[Map] Target {target.TargetId} reached the base. Base damage: {damage}.", this);
}
private void ResolvePseudoShotOnServer()
private void ResolveShotOnServer(MissileType missileType, bool ignoreMissileType)
{
if (!IsServer || !waveIsActive)
{
PublishShotResult(false, -1, -1f);
PublishShotResult(
false, -1, crosshairCoordinates.Value, -1f, missileType);
Debug.Log(
$"[Map] MISS with {missileType}: there is no active target wave.",
this);
return;
}
@@ -365,36 +391,67 @@ public sealed class MapController : NetworkBehaviour
{
MapTargetNetworkState target = activeTargets[nearestIndex];
TargetConfig config = targetController.GetConfig(target.ConfigIndex);
bool missileMatches = ignoreMissileTypeForPrototype || config == null ||
bool missileMatches = ignoreMissileType || config == null ||
config.RequiredMissileType == MissileType.None ||
config.RequiredMissileType == pseudoMissileType;
if (missileMatches)
config.RequiredMissileType == missileType;
if (!missileMatches)
{
target.Health -= shotDamage;
if (target.Health <= 0)
activeTargets.RemoveAt(nearestIndex);
else
activeTargets[nearestIndex] = target;
PublishShotResult(true, target.TargetId, nearestDistance);
Debug.Log($"[Map] HIT target {target.TargetId} at {nearestDistance:0.00} map units.", this);
TryCompleteWaveOnServer();
PublishShotResult(
false, target.TargetId, crosshairCoordinates.Value,
nearestDistance, missileType);
Debug.Log(
$"[Map] MISS target {target.TargetId}: {missileType} does not " +
$"match required {config.RequiredMissileType}. Distance " +
$"{nearestDistance:0.00} is inside hit radius {hitRadius:0.00}.",
this);
return;
}
target.Health -= shotDamage;
if (target.Health <= 0)
activeTargets.RemoveAt(nearestIndex);
else
activeTargets[nearestIndex] = target;
PublishShotResult(
true, target.TargetId, target.Coordinates,
nearestDistance, missileType);
Debug.Log(
$"[Map] HIT target {target.TargetId} with {missileType} " +
$"at {nearestDistance:0.00} map units. Damage: {shotDamage}.",
this);
TryCompleteWaveOnServer();
return;
}
int missedTargetId = nearestIndex >= 0 ? activeTargets[nearestIndex].TargetId : -1;
PublishShotResult(false, missedTargetId, nearestDistance);
float printedDistance = float.IsPositiveInfinity(nearestDistance) ? -1f : nearestDistance;
Debug.Log($"[Map] MISS at {crosshairCoordinates.Value}. Nearest distance: {printedDistance:0.00}.", this);
PublishShotResult(
false, missedTargetId, crosshairCoordinates.Value,
nearestDistance, missileType);
if (nearestIndex < 0)
{
Debug.Log($"[Map] MISS with {missileType}: no active targets.", this);
return;
}
Debug.Log(
$"[Map] MISS target {activeTargets[nearestIndex].TargetId}: distance " +
$"{nearestDistance:0.00} exceeds hit radius {hitRadius:0.00}.",
this);
}
private void PublishShotResult(bool hit, int targetId, float distance)
private void PublishShotResult(
bool hit,
int targetId,
Vector2 coordinates,
float distance,
MissileType missileType)
{
lastShotResult.Value = new MapShotResultState(
lastShotResult.Value.Sequence + 1, hit, targetId,
crosshairCoordinates.Value,
float.IsPositiveInfinity(distance) ? -1f : distance);
coordinates,
float.IsPositiveInfinity(distance) ? -1f : distance,
missileType);
}
private void ScanRadarOnServer()
@@ -428,11 +485,39 @@ public sealed class MapController : NetworkBehaviour
$"escaped: {escapedTargetCount}.", this);
}
private Vector2 CreateSpawnCoordinates()
{
Vector2 baseCoordinates = mapPlayer != null
? mapPlayer.MapCoordinates
: new Vector2(5f, 5f);
Vector2 farthestCandidate = Vector2.zero;
float farthestDistance = -1f;
for (int attempt = 0; attempt < enemyBalance.SpawnPositionAttempts; attempt++)
{
Vector2 candidate = new(
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum),
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum));
float distance = Vector2.Distance(candidate, baseCoordinates);
if (distance >= enemyBalance.MinimumSpawnDistanceFromBase)
return candidate;
if (distance > farthestDistance)
{
farthestDistance = distance;
farthestCandidate = candidate;
}
}
return farthestCandidate;
}
private void ConfigurePresentation()
{
mapPlayer?.Configure(coordinateSystem);
targetController?.Configure(coordinateSystem, targetParent, mapPlayer);
radarController?.Configure(coordinateSystem, radarMarkPrefab);
impactMarkerView?.Configure(coordinateSystem);
crosshair?.Configure(
coordinateSystem, relayX, relayY, crosshairVisual, targetController);
}
@@ -498,6 +583,7 @@ public sealed class MapController : NetworkBehaviour
return;
ShotResolved?.Invoke(current);
shotResolved?.Invoke(current.Hit);
impactMarkerView?.Show(current);
}
private void HandleResolvedSequenceChanged(uint previous, uint current)
@@ -0,0 +1,63 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "MapEnemyBalance",
menuName = "Map Radar/Enemy Balance Config")]
public sealed class MapEnemyBalanceConfig : ScriptableObject
{
[Header("Wave")]
[SerializeField, Min(0)] private int targetsPerWave = 5;
[SerializeField, Min(0.02f)] private float simulationStep = 0.1f;
[Header("Spawn")]
[SerializeField, Range(0f, MapCoordinateSystem.CoordinateMaximum)]
private float minimumSpawnDistanceFromBase = 5f;
[SerializeField, Range(1, 128)] private int spawnPositionAttempts = 32;
[Header("Global Balance Multipliers")]
[SerializeField, Min(0f)] private float movementSpeedMultiplier = 0.5f;
[SerializeField, Min(0f)] private float attackDelayMultiplier = 1.5f;
[SerializeField, Min(0.01f)] private float healthMultiplier = 1f;
[SerializeField, Min(0f)] private float baseDamageMultiplier = 1f;
public int TargetsPerWave => Mathf.Max(0, targetsPerWave);
public float SimulationStep => Mathf.Max(0.02f, simulationStep);
public float MinimumSpawnDistanceFromBase => Mathf.Clamp(
minimumSpawnDistanceFromBase,
0f,
MapCoordinateSystem.CoordinateMaximum);
public int SpawnPositionAttempts => Mathf.Clamp(spawnPositionAttempts, 1, 128);
public float MovementSpeedMultiplier => Mathf.Max(0f, movementSpeedMultiplier);
public float AttackDelayMultiplier => Mathf.Max(0f, attackDelayMultiplier);
public float HealthMultiplier => Mathf.Max(0.01f, healthMultiplier);
public float BaseDamageMultiplier => Mathf.Max(0f, baseDamageMultiplier);
public float GetMovementSpeed(TargetConfig target) =>
target != null ? target.MovementSpeed * MovementSpeedMultiplier : 0f;
public float GetAttackDelay(TargetConfig target) =>
target != null ? target.TimeBeforeAttack * AttackDelayMultiplier : 0f;
public int GetHealth(TargetConfig target) => target != null
? Mathf.Max(1, Mathf.RoundToInt(target.Health * HealthMultiplier))
: 1;
public int GetBaseDamage(TargetConfig target) => target != null
? Mathf.Max(0, Mathf.RoundToInt(target.BaseDamage * BaseDamageMultiplier))
: 1;
private void OnValidate()
{
targetsPerWave = Mathf.Max(0, targetsPerWave);
simulationStep = Mathf.Max(0.02f, simulationStep);
minimumSpawnDistanceFromBase = Mathf.Clamp(
minimumSpawnDistanceFromBase,
0f,
MapCoordinateSystem.CoordinateMaximum);
spawnPositionAttempts = Mathf.Clamp(spawnPositionAttempts, 1, 128);
movementSpeedMultiplier = Mathf.Max(0f, movementSpeedMultiplier);
attackDelayMultiplier = Mathf.Max(0f, attackDelayMultiplier);
healthMultiplier = Mathf.Max(0.01f, healthMultiplier);
baseDamageMultiplier = Mathf.Max(0f, baseDamageMultiplier);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1dfcdffb89f04cd468e4f8386b4f67e8
@@ -0,0 +1,76 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class MapImpactMarkerView : MonoBehaviour
{
[SerializeField] private MapCoordinateSystem coordinateSystem;
[SerializeField, Min(0.01f)] private float markerLength = 0.35f;
[SerializeField, Min(0.005f)] private float markerThickness = 0.035f;
[SerializeField, Min(0f)] private float surfaceOffset = 0.025f;
[SerializeField, Min(0.1f)] private float lifetime = 5f;
[SerializeField] private Color hitColor = new(1f, 0.1f, 0.05f, 1f);
[SerializeField] private Color missColor = new(1f, 0.75f, 0f, 1f);
[SerializeField] private bool showMisses = true;
[SerializeField] private UnityEvent hitMarked;
[SerializeField] private UnityEvent missMarked;
public void Configure(MapCoordinateSystem coordinates) => coordinateSystem = coordinates;
public void Show(MapShotResultState result)
{
if (coordinateSystem == null || (!result.Hit && !showMisses))
return;
CreateMarker(result.Coordinates, result.Hit ? hitColor : missColor);
if (result.Hit)
hitMarked?.Invoke();
else
missMarked?.Invoke();
}
private void CreateMarker(Vector2 coordinates, Color color)
{
Vector3 horizontal = coordinateSystem.BottomRight.position -
coordinateSystem.BottomLeft.position;
Vector3 vertical = coordinateSystem.TopLeft.position -
coordinateSystem.BottomLeft.position;
Vector3 normal = Vector3.Cross(horizontal, vertical).normalized;
if (normal.sqrMagnitude < 0.5f)
normal = transform.forward;
GameObject marker = new($"Impact {coordinates.x:0.0}, {coordinates.y:0.0}");
marker.transform.SetParent(transform, true);
marker.transform.SetPositionAndRotation(
coordinateSystem.CoordinatesToWorldPosition(coordinates) + normal * surfaceOffset,
Quaternion.LookRotation(normal, vertical.normalized));
CreateBar(marker.transform, 45f, color);
CreateBar(marker.transform, -45f, color);
StartCoroutine(DestroyAfterLifetime(marker));
}
private void CreateBar(Transform parent, float angle, Color color)
{
GameObject bar = GameObject.CreatePrimitive(PrimitiveType.Cube);
bar.name = "Marker Bar";
bar.transform.SetParent(parent, false);
bar.transform.localRotation = Quaternion.Euler(0f, 0f, angle);
bar.transform.localScale = new Vector3(
markerLength, markerThickness, markerThickness);
Collider collider = bar.GetComponent<Collider>();
if (collider != null)
Destroy(collider);
Renderer renderer = bar.GetComponent<Renderer>();
if (renderer != null)
renderer.material.color = color;
}
private IEnumerator DestroyAfterLifetime(GameObject marker)
{
yield return new WaitForSeconds(lifetime);
if (marker != null)
Destroy(marker);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0b3852b847aa89b48b9d38476e56250e
@@ -5,8 +5,7 @@ using UnityEngine;
[DisallowMultipleComponent]
public sealed class MapTargetController : MonoBehaviour
{
[Header("Wave Settings")]
[SerializeField, Min(0)] private int targetsPerWave = 5;
[Header("Target Types")]
[SerializeField] private TargetConfig[] randomTargetConfigs = Array.Empty<TargetConfig>();
[SerializeField] private bool showLiveTargetVisuals;
@@ -24,11 +23,9 @@ public sealed class MapTargetController : MonoBehaviour
public IReadOnlyList<MapTarget> Targets => currentWaveTargets;
public IReadOnlyList<TargetConfig> TargetConfigs => randomTargetConfigs;
public int CurrentWave => currentWave;
public int TargetsPerWave => targetsPerWave;
private void OnValidate()
{
targetsPerWave = Mathf.Max(0, targetsPerWave);
currentWave = Mathf.Max(0, currentWave);
}
@@ -51,19 +51,22 @@ public struct MapShotResultState : INetworkSerializable,
public int TargetId;
public Vector2 Coordinates;
public float Distance;
public MissileType MissileType;
public MapShotResultState(
uint sequence,
bool hit,
int targetId,
Vector2 coordinates,
float distance)
float distance,
MissileType missileType)
{
Sequence = sequence;
Hit = hit;
TargetId = targetId;
Coordinates = coordinates;
Distance = distance;
MissileType = missileType;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
@@ -74,10 +77,11 @@ public struct MapShotResultState : INetworkSerializable,
serializer.SerializeValue(ref TargetId);
serializer.SerializeValue(ref Coordinates);
serializer.SerializeValue(ref Distance);
serializer.SerializeValue(ref MissileType);
}
public bool Equals(MapShotResultState other) =>
Sequence == other.Sequence && Hit == other.Hit &&
TargetId == other.TargetId && Coordinates == other.Coordinates &&
Distance.Equals(other.Distance);
Distance.Equals(other.Distance) && MissileType == other.MissileType;
}
@@ -52,6 +52,8 @@ GameObject:
- component: {fileID: 6442729922619842682}
- component: {fileID: 5032171862004131710}
- component: {fileID: 5463195673691487104}
- component: {fileID: 5685992952666159115}
- component: {fileID: 5909870405075214492}
m_Layer: 0
m_Name: NetworkPlayer
m_TagString: Untagged
@@ -205,6 +207,7 @@ MonoBehaviour:
sprintSpeed: 7
jumpHeight: 1.4
gravity: 20
impulseDamping: 9
--- !u!114 &3719056139727702052
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -219,6 +222,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
verticalLookLimit: 89
remotePitchFollowSpeed: 20
--- !u!114 &2519868927162778852
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -302,8 +306,33 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5e63651da7abbf744bc45585a2133e6f, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
--- !u!114 &5685992952666159115
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2724913065371725993}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b12d3c124e93b5f4f9c06c7e870d4068, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &5909870405075214492
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2724913065371725993}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6a7ddedc58d3467449dfbe83af1c9c51, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
--- !u!1 &4726019310047661441
GameObject:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2b80031cb8a86414c9b054ed5a280465
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e069cd58c327cc94e9c2598c9e4c449f, type: 3}
m_Name: CannonConfig
m_EditorClassIdentifier:
minimumDelay: 0
maximumDelay: 30
defaultDelay: 5
adjustmentStep: 1
acceptDefectiveRockets: 1
acceptCriticallyDamagedRockets: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23aa810e05d4cbc4da4bb729f5e54688
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 96e16f6f88696ea4c8b884f3e7cef9f7, type: 3}
m_Name: CannonShockwave
m_EditorClassIdentifier:
zones:
- Radius: 8
PlayerImpulse: 12
RigidbodyImpulse: 18
UpwardBias: 0.35
CameraShakeAmplitude: 0.22
CameraShakeDuration: 0.9
CameraShakeFrequency: 28
- Radius: 18
PlayerImpulse: 5
RigidbodyImpulse: 8
UpwardBias: 0.25
CameraShakeAmplitude: 0.12
CameraShakeDuration: 0.75
CameraShakeFrequency: 24
- Radius: 35
PlayerImpulse: 0
RigidbodyImpulse: 1.5
UpwardBias: 0.1
CameraShakeAmplitude: 0.055
CameraShakeDuration: 0.55
CameraShakeFrequency: 20
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9155e8ad462c3484ba4275bcb64babe2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 96e16f6f88696ea4c8b884f3e7cef9f7, type: 3}
m_Name: RocketImpactShockwave
m_EditorClassIdentifier:
zones:
- Radius: 4
PlayerImpulse: 8
RigidbodyImpulse: 12
UpwardBias: 0.4
CameraShakeAmplitude: 0.18
CameraShakeDuration: 0.7
CameraShakeFrequency: 27
- Radius: 8
PlayerImpulse: 3
RigidbodyImpulse: 5
UpwardBias: 0.3
CameraShakeAmplitude: 0.09
CameraShakeDuration: 0.55
CameraShakeFrequency: 23
- Radius: 14
PlayerImpulse: 0
RigidbodyImpulse: 0.5
UpwardBias: 0.1
CameraShakeAmplitude: 0.04
CameraShakeDuration: 0.4
CameraShakeFrequency: 18
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5afb0716a3dcecf469e0fe2128d5b98b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -16,4 +16,5 @@ MonoBehaviour:
displayName: Full-Rocket-Standart
worldPrefab: {fileID: 7456648717704822871, guid: 746f04d9ba6c87d4bae8a09287c5d090, type: 3}
rocketTypeId: standard
missileType: 1
criticalImpactSpeed: 7
@@ -17,6 +17,8 @@ GameObject:
- component: {fileID: 4269639932524941515}
- component: {fileID: 6493104616022895181}
- component: {fileID: 3635736001397410476}
- component: {fileID: 633656625536386338}
- component: {fileID: 4161244424843525666}
m_Layer: 0
m_Name: Ready-Rocket-Standart
m_TagString: Untagged
@@ -52,7 +54,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier:
GlobalObjectIdHash: 3332465624
GlobalObjectIdHash: 246746440
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
@@ -261,6 +263,44 @@ MonoBehaviour:
onCriticalImpactSpeed:
m_PersistentCalls:
m_Calls: []
--- !u!114 &633656625536386338
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 43f56ec799f9aec4ca1bf372c88592cd, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
profile: {fileID: 11400000, guid: 5afb0716a3dcecf469e0fe2128d5b98b, type: 2}
origin: {fileID: 1632953841127884940}
physicsLayers:
serializedVersion: 2
m_Bits: 4294967295
shockwaveEmitted:
m_PersistentCalls:
m_Calls: []
--- !u!114 &4161244424843525666
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ac11f143457835f46a1a2defaa624354, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
rocket: {fileID: 6493104616022895181}
shockwave: {fileID: 633656625536386338}
despawnAfterExplosion: 1
despawnDelay: 0.15
--- !u!1001 &5717474834163769460
PrefabInstance:
m_ObjectHideFlags: 0
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2dab561baa00bfb46b838b978cb45129
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "CannonConfig",
menuName = "FORT-BO/Cannon/Cannon Config")]
public sealed class CannonConfig : ScriptableObject
{
[SerializeField, Min(0f)] private float minimumDelay = 0f;
[SerializeField, Min(0.1f)] private float maximumDelay = 60f;
[SerializeField, Min(0f)] private float defaultDelay = 5f;
[SerializeField, Min(0.1f)] private float adjustmentStep = 1f;
[SerializeField] private bool acceptDefectiveRockets = true;
[SerializeField] private bool acceptCriticallyDamagedRockets;
public float MinimumDelay => minimumDelay;
public float MaximumDelay => maximumDelay;
public float DefaultDelay => ClampDelay(defaultDelay);
public float AdjustmentStep => adjustmentStep;
public bool AcceptDefectiveRockets => acceptDefectiveRockets;
public bool AcceptCriticallyDamagedRockets => acceptCriticallyDamagedRockets;
public float ClampDelay(float value) =>
Mathf.Clamp(value, minimumDelay, Mathf.Max(minimumDelay, maximumDelay));
private void OnValidate()
{
minimumDelay = Mathf.Max(0f, minimumDelay);
maximumDelay = Mathf.Max(minimumDelay, maximumDelay);
defaultDelay = ClampDelay(defaultDelay);
adjustmentStep = Mathf.Max(0.1f, adjustmentStep);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e069cd58c327cc94e9c2598c9e4c449f
@@ -0,0 +1,51 @@
using UnityEngine;
[DisallowMultipleComponent]
public sealed class CannonControlBinder : MonoBehaviour
{
[SerializeField] private NetworkCannonController cannon;
[SerializeField] private NetworkPhysicalLeverInteractable fireLever;
[SerializeField] private NetworkButtonInteractable decreaseTimerButton;
[SerializeField] private NetworkButtonInteractable increaseTimerButton;
private bool bound;
private void Update()
{
if (!bound)
TryBind();
}
private void OnDisable() => Unbind();
private void TryBind()
{
if (cannon == null || !cannon.IsSpawned || !cannon.IsServer)
return;
if (fireLever != null)
fireLever.StateChanged += HandleFireLever;
if (decreaseTimerButton != null)
decreaseTimerButton.Pressed += HandleDecreaseTimer;
if (increaseTimerButton != null)
increaseTimerButton.Pressed += HandleIncreaseTimer;
bound = true;
}
private void Unbind()
{
if (!bound)
return;
if (fireLever != null)
fireLever.StateChanged -= HandleFireLever;
if (decreaseTimerButton != null)
decreaseTimerButton.Pressed -= HandleDecreaseTimer;
if (increaseTimerButton != null)
increaseTimerButton.Pressed -= HandleIncreaseTimer;
bound = false;
}
private void HandleFireLever(bool isActivated) => cannon.SetFireRequested(isActivated);
private void HandleDecreaseTimer() => cannon.RequestDecreaseShotDelay();
private void HandleIncreaseTimer() => cannon.RequestIncreaseShotDelay();
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 24a208cb322bf904bb12481f5fc58a85
@@ -0,0 +1,81 @@
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider))]
public sealed class CannonRocketSlot : MonoBehaviour, IInteractable
{
[SerializeField] private Transform snapPoint;
private NetworkCannonController cannon;
private Collider triggerCollider;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
public Vector3 InteractionPosition => SnapPoint.position;
public string InteractionPrompt => cannon != null
? cannon.LoadedRocket != null
? $"Take {cannon.LoadedRocket.RocketTypeId} rocket"
: "Rocket slot"
: "Rocket slot";
public bool IsInteractionAvailable => cannon != null && cannon.CanTakeRocket;
private void Reset()
{
snapPoint = transform;
ResolveCollider();
triggerCollider.isTrigger = true;
}
private void Awake() => ResolveCollider();
private void OnValidate()
{
ResolveCollider();
if (triggerCollider != null)
triggerCollider.isTrigger = true;
}
public void Configure(NetworkCannonController owner) => cannon = owner;
public void RequestInteraction()
{
if (IsInteractionAvailable)
cannon.RequestTakeRocket();
}
private void OnTriggerEnter(Collider other) => TryInsert(other);
private void OnTriggerStay(Collider other) => TryInsert(other);
private void TryInsert(Collider other)
{
if (cannon == null || !cannon.IsServer || !cannon.IsSpawned)
return;
NetworkRocketItem rocket = other.GetComponentInParent<NetworkRocketItem>();
if (rocket != null)
cannon.TryInsertRocket(rocket);
}
public bool OverlapsAny(Collider[] itemColliders, float clearance)
{
ResolveCollider();
if (triggerCollider == null || itemColliders == null)
return false;
Bounds bounds = triggerCollider.bounds;
bounds.Expand(Mathf.Max(0f, clearance) * 2f);
foreach (Collider itemCollider in itemColliders)
{
if (itemCollider != null && itemCollider.enabled &&
bounds.Intersects(itemCollider.bounds))
return true;
}
return false;
}
private void ResolveCollider()
{
if (triggerCollider == null)
triggerCollider = GetComponent<Collider>();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ad0f04c740f9b343b2ed4f083c407a6
@@ -0,0 +1,46 @@
using TMPro;
using UnityEngine;
[DisallowMultipleComponent]
public sealed class CannonTimerView : MonoBehaviour
{
[SerializeField] private NetworkCannonController controller;
[SerializeField] private TMP_Text timerText;
[SerializeField] private string emptyText = "INSERT ROCKET";
[SerializeField] private string loadedFormat = "DELAY {0:0}s";
[SerializeField] private string countdownFormat = "FIRE IN {0:0.0}s";
private void OnEnable()
{
if (controller != null)
controller.Changed += Refresh;
Refresh();
}
private void OnDisable()
{
if (controller != null)
controller.Changed -= Refresh;
}
private void Update()
{
if (controller != null && controller.State == CannonState.Countdown)
Refresh();
}
private void Refresh()
{
if (timerText == null || controller == null)
return;
timerText.text = controller.State switch
{
CannonState.Empty => emptyText,
CannonState.Loaded => string.Format(loadedFormat, controller.SelectedDelay),
CannonState.Countdown => string.Format(
countdownFormat, controller.RemainingSeconds),
_ => emptyText
};
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 451ab5135cfc56f489740516a23e7834
@@ -0,0 +1,90 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class CannonView : MonoBehaviour
{
[SerializeField] private NetworkCannonController controller;
[SerializeField] private Transform recoilVisual;
[SerializeField] private Vector3 localRecoilOffset = new(0f, 0f, -0.75f);
[SerializeField, Min(0.01f)] private float recoilOutDuration = 0.08f;
[SerializeField, Min(0.01f)] private float recoilReturnDuration = 0.45f;
[SerializeField] private UnityEvent shotFired;
[SerializeField] private UnityEvent<MissileType> missileFired;
private Vector3 baseLocalPosition;
private Coroutine recoilRoutine;
private void Awake()
{
if (recoilVisual != null)
baseLocalPosition = recoilVisual.localPosition;
}
private void OnEnable()
{
if (controller != null)
controller.ShotFired += HandleShotFired;
}
private void OnDisable()
{
if (controller != null)
controller.ShotFired -= HandleShotFired;
if (recoilRoutine != null)
StopCoroutine(recoilRoutine);
if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition;
}
private void HandleShotFired(CannonShotState shot)
{
if (recoilRoutine != null)
StopCoroutine(recoilRoutine);
recoilRoutine = StartCoroutine(PlayRecoil());
shotFired?.Invoke();
missileFired?.Invoke(shot.MissileType);
}
private IEnumerator PlayRecoil()
{
if (recoilVisual == null)
yield break;
Vector3 recoilPosition = baseLocalPosition + localRecoilOffset;
yield return Move(recoilVisual.localPosition, recoilPosition, recoilOutDuration);
yield return Move(recoilVisual.localPosition, baseLocalPosition, recoilReturnDuration);
recoilRoutine = null;
}
private IEnumerator Move(Vector3 from, Vector3 to, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
recoilVisual.localPosition = Vector3.LerpUnclamped(
from, to, 1f - Mathf.Pow(1f - t, 3f));
yield return null;
}
recoilVisual.localPosition = to;
}
[ContextMenu("Preview/Recoil")]
private void PreviewRecoil()
{
if (Application.isPlaying)
HandleShotFired(new CannonShotState(0, MissileType.Standard));
else if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition + localRecoilOffset;
}
[ContextMenu("Preview/Reset")]
private void ResetPreview()
{
if (recoilVisual != null)
recoilVisual.localPosition = baseLocalPosition;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f174c161f1871f44bba1b7e09ce03e61
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4c3f0760c12468543bab2cf3f99294f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,458 @@
using TMPro;
using Unity.Netcode;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public static class CannonPrototypeSetup
{
private const string DataFolder = "Assets/CONTENT/Game/Data/Cannon";
private const string CannonConfigPath = DataFolder + "/CannonConfig.asset";
private const string CannonShockwavePath = DataFolder + "/CannonShockwave.asset";
private const string RocketShockwavePath = DataFolder + "/RocketImpactShockwave.asset";
private const string PlayerPrefabPath =
"Assets/CONTENT/Friendslop/Prefabs/NetworkPlayer.prefab";
private const string RocketPrefabPath =
"Assets/CONTENT/Game/Prefabs/Items/Ready-Rocket-Standart.prefab";
private const string StandardRocketDefinitionPath =
"Assets/CONTENT/Game/Data/Items/Rocket-Ready/Ready-Rocket-Standart.asset";
private const string LeverPrefabPath =
"Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab";
[MenuItem("FORT-BO/Cannon/Configure Network Prototype")]
public static void ConfigureNetworkPrototype()
{
if (Application.isPlaying)
{
Debug.LogWarning("Stop Play Mode before configuring the cannon prototype.");
return;
}
EnsureDataFolder();
CannonConfig config = GetOrCreateCannonConfig();
ShockwaveProfile cannonShockwave = GetOrCreateProfile(
CannonShockwavePath,
new ShockwaveZone
{
Radius = 8f,
PlayerImpulse = 12f,
RigidbodyImpulse = 18f,
UpwardBias = 0.35f,
CameraShakeAmplitude = 0.22f,
CameraShakeDuration = 0.9f,
CameraShakeFrequency = 28f
},
new ShockwaveZone
{
Radius = 18f,
PlayerImpulse = 5f,
RigidbodyImpulse = 8f,
UpwardBias = 0.25f,
CameraShakeAmplitude = 0.12f,
CameraShakeDuration = 0.75f,
CameraShakeFrequency = 24f
},
new ShockwaveZone
{
Radius = 35f,
PlayerImpulse = 0f,
RigidbodyImpulse = 1.5f,
UpwardBias = 0.1f,
CameraShakeAmplitude = 0.055f,
CameraShakeDuration = 0.55f,
CameraShakeFrequency = 20f
});
ShockwaveProfile rocketShockwave = GetOrCreateProfile(
RocketShockwavePath,
new ShockwaveZone
{
Radius = 4f,
PlayerImpulse = 8f,
RigidbodyImpulse = 12f,
UpwardBias = 0.4f,
CameraShakeAmplitude = 0.18f,
CameraShakeDuration = 0.7f,
CameraShakeFrequency = 27f
},
new ShockwaveZone
{
Radius = 8f,
PlayerImpulse = 3f,
RigidbodyImpulse = 5f,
UpwardBias = 0.3f,
CameraShakeAmplitude = 0.09f,
CameraShakeDuration = 0.55f,
CameraShakeFrequency = 23f
},
new ShockwaveZone
{
Radius = 14f,
PlayerImpulse = 0f,
RigidbodyImpulse = 0.5f,
UpwardBias = 0.1f,
CameraShakeAmplitude = 0.04f,
CameraShakeDuration = 0.4f,
CameraShakeFrequency = 18f
});
ConfigurePlayerPrefab();
ConfigureRocketPrefab(rocketShockwave);
ConfigureStandardRocketDefinition();
NetworkCannonController cannon = ConfigureCannonScene(config, cannonShockwave);
ConfigureMapMarker();
AssetDatabase.SaveAssets();
EditorSceneManager.MarkSceneDirty(cannon.gameObject.scene);
EditorSceneManager.SaveOpenScenes();
Selection.activeObject = cannon.gameObject;
Debug.Log(
"[Cannon] Network prototype configured. Insert a ready rocket, " +
"set delay with -/+ buttons and pull the fire lever.", cannon);
}
private static CannonConfig GetOrCreateCannonConfig()
{
CannonConfig config = AssetDatabase.LoadAssetAtPath<CannonConfig>(CannonConfigPath);
if (config == null)
{
config = ScriptableObject.CreateInstance<CannonConfig>();
AssetDatabase.CreateAsset(config, CannonConfigPath);
}
SerializedObject serialized = new(config);
SetFloat(serialized, "minimumDelay", 0f);
SetFloat(serialized, "maximumDelay", 30f);
SetFloat(serialized, "defaultDelay", 5f);
SetFloat(serialized, "adjustmentStep", 1f);
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(config);
return config;
}
private static ShockwaveProfile GetOrCreateProfile(
string path,
params ShockwaveZone[] zones)
{
ShockwaveProfile profile = AssetDatabase.LoadAssetAtPath<ShockwaveProfile>(path);
if (profile == null)
{
profile = ScriptableObject.CreateInstance<ShockwaveProfile>();
AssetDatabase.CreateAsset(profile, path);
}
SerializedObject serialized = new(profile);
SerializedProperty array = serialized.FindProperty("zones");
array.arraySize = zones.Length;
for (int index = 0; index < zones.Length; index++)
{
SerializedProperty element = array.GetArrayElementAtIndex(index);
ShockwaveZone zone = zones[index];
element.FindPropertyRelative("Radius").floatValue = zone.Radius;
element.FindPropertyRelative("PlayerImpulse").floatValue = zone.PlayerImpulse;
element.FindPropertyRelative("RigidbodyImpulse").floatValue = zone.RigidbodyImpulse;
element.FindPropertyRelative("UpwardBias").floatValue = zone.UpwardBias;
element.FindPropertyRelative("CameraShakeAmplitude").floatValue =
zone.CameraShakeAmplitude;
element.FindPropertyRelative("CameraShakeDuration").floatValue =
zone.CameraShakeDuration;
element.FindPropertyRelative("CameraShakeFrequency").floatValue =
zone.CameraShakeFrequency;
}
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(profile);
return profile;
}
private static void ConfigurePlayerPrefab()
{
GameObject root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath);
try
{
GetOrAdd<PlayerCameraShakeController>(root);
GetOrAdd<PlayerImpactController>(root);
PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void ConfigureRocketPrefab(ShockwaveProfile rocketShockwave)
{
GameObject root = PrefabUtility.LoadPrefabContents(RocketPrefabPath);
try
{
NetworkRocketItem rocket = root.GetComponent<NetworkRocketItem>();
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
RocketImpactExplosion explosion = GetOrAdd<RocketImpactExplosion>(root);
SerializedObject serializedEmitter = new(emitter);
SetObject(serializedEmitter, "profile", rocketShockwave);
SetObject(serializedEmitter, "origin", root.transform);
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedExplosion = new(explosion);
SetObject(serializedExplosion, "rocket", rocket);
SetObject(serializedExplosion, "shockwave", emitter);
serializedExplosion.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, RocketPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void ConfigureStandardRocketDefinition()
{
RocketItemDefinition definition =
AssetDatabase.LoadAssetAtPath<RocketItemDefinition>(StandardRocketDefinitionPath);
if (definition == null)
return;
SerializedObject serialized = new(definition);
serialized.FindProperty("missileType").enumValueIndex = (int)MissileType.Standard;
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(definition);
}
private static NetworkCannonController ConfigureCannonScene(
CannonConfig config,
ShockwaveProfile cannonShockwave)
{
GameObject root = GameObject.Find("Cannon Prototype");
bool created = root == null;
if (created)
{
root = new GameObject("Cannon Prototype");
root.transform.SetPositionAndRotation(
new Vector3(132f, 2.55f, 24.3f),
Quaternion.Euler(0f, 90f, 0f));
Undo.RegisterCreatedObjectUndo(root, "Create cannon prototype");
}
GetOrAdd<NetworkObject>(root);
NetworkCannonController controller = GetOrAdd<NetworkCannonController>(root);
NetworkShockwaveEmitter emitter = GetOrAdd<NetworkShockwaveEmitter>(root);
CannonControlBinder binder = GetOrAdd<CannonControlBinder>(root);
CannonView view = GetOrAdd<CannonView>(root);
Transform baseVisual = GetOrCreatePrimitive(
root.transform, "Cannon Base", PrimitiveType.Cube);
baseVisual.localPosition = new Vector3(0f, 0.75f, 0f);
baseVisual.localRotation = Quaternion.identity;
baseVisual.localScale = new Vector3(2.2f, 1.5f, 2.2f);
Transform recoil = GetOrCreateChild(root.transform, "Barrel Recoil");
recoil.localPosition = new Vector3(0f, 2.1f, 0f);
recoil.localRotation = Quaternion.identity;
recoil.localScale = Vector3.one;
Transform barrel = GetOrCreatePrimitive(recoil, "Barrel", PrimitiveType.Cube);
barrel.localPosition = new Vector3(0f, 0f, 2.1f);
barrel.localRotation = Quaternion.identity;
barrel.localScale = new Vector3(0.8f, 0.8f, 4.2f);
Transform muzzle = GetOrCreateChild(recoil, "Muzzle");
muzzle.localPosition = new Vector3(0f, 0f, 4.25f);
Transform slotObject = GetOrCreateChild(root.transform, "Rocket Slot");
slotObject.localPosition = new Vector3(-2.2f, 1.25f, 0f);
slotObject.localRotation = Quaternion.Euler(0f, 90f, 0f);
slotObject.localScale = Vector3.one;
BoxCollider slotCollider = GetOrAdd<BoxCollider>(slotObject.gameObject);
slotCollider.isTrigger = true;
slotCollider.size = new Vector3(1.4f, 1.4f, 3.2f);
CannonRocketSlot slot = GetOrAdd<CannonRocketSlot>(slotObject.gameObject);
Transform snapPoint = GetOrCreateChild(slotObject, "Rocket Snap Point");
snapPoint.localPosition = Vector3.zero;
snapPoint.localRotation = Quaternion.identity;
Transform controls = GetOrCreateChild(root.transform, "Controls");
controls.localPosition = Vector3.zero;
controls.localRotation = Quaternion.identity;
controls.localScale = Vector3.one;
NetworkPhysicalLeverInteractable lever = CreateLever(controls);
NetworkButtonInteractable minus = CreateButton(
controls, "Timer Minus", new Vector3(1.8f, 1.4f, -1.4f), "Timer -");
NetworkButtonInteractable plus = CreateButton(
controls, "Timer Plus", new Vector3(3.1f, 1.4f, -1.4f), "Timer +");
TMP_Text timerText = CreateTimerText(controls);
SerializedObject serializedEmitter = new(emitter);
SetObject(serializedEmitter, "profile", cannonShockwave);
SetObject(serializedEmitter, "origin", muzzle);
serializedEmitter.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedController = new(controller);
SetObject(serializedController, "config", config);
SetObject(serializedController, "rocketSlot", slot);
SetObject(serializedController, "rocketSnapPoint", snapPoint);
SetObject(serializedController, "mapController",
Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include));
SetObject(serializedController, "shotShockwave", emitter);
serializedController.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedSlot = new(slot);
SetObject(serializedSlot, "snapPoint", snapPoint);
serializedSlot.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedBinder = new(binder);
SetObject(serializedBinder, "cannon", controller);
SetObject(serializedBinder, "fireLever", lever);
SetObject(serializedBinder, "decreaseTimerButton", minus);
SetObject(serializedBinder, "increaseTimerButton", plus);
serializedBinder.ApplyModifiedPropertiesWithoutUndo();
SerializedObject serializedView = new(view);
SetObject(serializedView, "controller", controller);
SetObject(serializedView, "recoilVisual", recoil);
serializedView.ApplyModifiedPropertiesWithoutUndo();
CannonTimerView timerView = GetOrAdd<CannonTimerView>(timerText.gameObject);
SerializedObject serializedTimer = new(timerView);
SetObject(serializedTimer, "controller", controller);
SetObject(serializedTimer, "timerText", timerText);
serializedTimer.ApplyModifiedPropertiesWithoutUndo();
slot.Configure(controller);
EditorUtility.SetDirty(root);
return controller;
}
private static NetworkPhysicalLeverInteractable CreateLever(Transform parent)
{
Transform existing = parent.Find("Fire Lever");
GameObject root;
if (existing != null)
{
root = existing.gameObject;
}
else
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(LeverPrefabPath);
root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
root.name = "Fire Lever";
}
root.transform.localPosition = new Vector3(4.5f, 1.25f, -1.2f);
root.transform.localRotation = Quaternion.Euler(0f, -90f, 0f);
root.transform.localScale = Vector3.one * 0.65f;
NetworkPhysicalLeverInteractable lever =
root.GetComponent<NetworkPhysicalLeverInteractable>();
SerializedObject serialized = new(lever);
SetString(serialized, "interactionLabel", "Fire cannon");
SetBool(serialized, "initialPositionIsMaximum", false);
SetBool(serialized, "returnToInitialPositionAfterSwitch", true);
SetFloat(serialized, "automaticReturnDelay", 0.2f);
serialized.ApplyModifiedPropertiesWithoutUndo();
return lever;
}
private static NetworkButtonInteractable CreateButton(
Transform parent,
string name,
Vector3 localPosition,
string label)
{
Transform root = GetOrCreateChild(parent, name);
root.localPosition = localPosition;
root.localRotation = Quaternion.identity;
root.localScale = Vector3.one;
GetOrAdd<NetworkObject>(root.gameObject);
Transform visual = GetOrCreatePrimitive(root, "Button Visual", PrimitiveType.Cube);
visual.localPosition = Vector3.zero;
visual.localRotation = Quaternion.identity;
visual.localScale = new Vector3(0.8f, 0.8f, 0.3f);
NetworkButtonInteractable button = GetOrAdd<NetworkButtonInteractable>(root.gameObject);
SerializedObject serialized = new(button);
SetString(serialized, "interactionLabel", label);
SetFloat(serialized, "maximumInteractionDistance", 3.5f);
serialized.ApplyModifiedPropertiesWithoutUndo();
return button;
}
private static TMP_Text CreateTimerText(Transform parent)
{
Transform root = GetOrCreateChild(parent, "Timer Display");
root.localPosition = new Vector3(2.45f, 2.25f, -1.35f);
root.localRotation = Quaternion.Euler(0f, 180f, 0f);
root.localScale = Vector3.one * 0.12f;
TextMeshPro text = GetOrAdd<TextMeshPro>(root.gameObject);
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 4f;
text.text = "INSERT ROCKET";
text.rectTransform.sizeDelta = new Vector2(12f, 3f);
return text;
}
private static void ConfigureMapMarker()
{
MapController map = Object.FindFirstObjectByType<MapController>(FindObjectsInactive.Include);
if (map == null)
return;
Transform root = map.transform.Find("Impact Markers");
if (root == null)
{
root = new GameObject("Impact Markers").transform;
root.SetParent(map.transform, false);
}
MapImpactMarkerView view = GetOrAdd<MapImpactMarkerView>(root.gameObject);
view.Configure(map.CoordinateSystem);
SerializedObject serializedMap = new(map);
SetObject(serializedMap, "impactMarkerView", view);
serializedMap.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(map);
}
private static void EnsureDataFolder()
{
if (!AssetDatabase.IsValidFolder("Assets/CONTENT/Game/Data"))
AssetDatabase.CreateFolder("Assets/CONTENT/Game", "Data");
if (!AssetDatabase.IsValidFolder(DataFolder))
AssetDatabase.CreateFolder("Assets/CONTENT/Game/Data", "Cannon");
}
private static Transform GetOrCreateChild(Transform parent, string name)
{
Transform existing = parent.Find(name);
if (existing != null)
return existing;
GameObject child = new(name);
child.transform.SetParent(parent, false);
Undo.RegisterCreatedObjectUndo(child, $"Create {name}");
return child.transform;
}
private static Transform GetOrCreatePrimitive(
Transform parent,
string name,
PrimitiveType primitiveType)
{
Transform existing = parent.Find(name);
if (existing != null)
return existing;
GameObject primitive = GameObject.CreatePrimitive(primitiveType);
primitive.name = name;
primitive.transform.SetParent(parent, false);
Undo.RegisterCreatedObjectUndo(primitive, $"Create {name}");
return primitive.transform;
}
private static T GetOrAdd<T>(GameObject target) where T : Component
{
T component = target.GetComponent<T>();
return component != null ? component : Undo.AddComponent<T>(target);
}
private static void SetObject(SerializedObject target, string property, Object value) =>
target.FindProperty(property).objectReferenceValue = value;
private static void SetFloat(SerializedObject target, string property, float value) =>
target.FindProperty(property).floatValue = value;
private static void SetBool(SerializedObject target, string property, bool value) =>
target.FindProperty(property).boolValue = value;
private static void SetString(SerializedObject target, string property, string value) =>
target.FindProperty(property).stringValue = value;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe3002dfbec1fe1458dcd6058b1486c3
@@ -0,0 +1,388 @@
using System;
using Unity.Netcode;
using UnityEngine;
public enum CannonState : byte
{
Empty,
Loaded,
Countdown
}
public struct CannonShotState : INetworkSerializable, IEquatable<CannonShotState>
{
public uint Sequence;
public MissileType MissileType;
public CannonShotState(uint sequence, MissileType missileType)
{
Sequence = sequence;
MissileType = missileType;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
where T : IReaderWriter
{
serializer.SerializeValue(ref Sequence);
serializer.SerializeValue(ref MissileType);
}
public bool Equals(CannonShotState other) =>
Sequence == other.Sequence && MissileType == other.MissileType;
}
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkObject))]
public sealed class NetworkCannonController : NetworkBehaviour
{
private const ulong EmptyRocketId = ulong.MaxValue;
[Header("Configuration")]
[SerializeField] private CannonConfig config;
[Header("Rocket Slot")]
[SerializeField] private CannonRocketSlot rocketSlot;
[SerializeField] private Transform rocketSnapPoint;
[SerializeField, Min(0.5f)] private float maximumTakeDistance = 4f;
[SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f;
[Header("Output")]
[SerializeField] private MapController mapController;
[SerializeField] private NetworkShockwaveEmitter shotShockwave;
private readonly NetworkVariable<CannonState> state = new(
CannonState.Empty,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<ulong> loadedRocketId = new(
EmptyRocketId,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<float> selectedDelay = new(
5f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> fireAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<CannonShotState> lastShot = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private NetworkRocketItem loadedRocket;
private IWorkstationItemControl loadedRocketControl;
private ulong blockedRocketId = EmptyRocketId;
private Collider[] blockedRocketColliders;
private double unblockAfter;
public CannonState State => state.Value;
public CannonConfig Config => config;
public NetworkRocketItem LoadedRocket => loadedRocket != null
? loadedRocket
: ResolveLoadedRocket();
public float SelectedDelay => selectedDelay.Value;
public double FireAt => fireAt.Value;
public float RemainingSeconds => State == CannonState.Countdown && NetworkManager != null
? Mathf.Max(0f, (float)(fireAt.Value - NetworkManager.ServerTime.Time))
: 0f;
public bool CanTakeRocket => IsSpawned && State == CannonState.Loaded &&
loadedRocketId.Value != EmptyRocketId;
public CannonShotState LastShot => lastShot.Value;
public event Action Changed;
public event Action<CannonShotState> ShotFired;
private void Awake()
{
if (rocketSnapPoint == null && rocketSlot != null)
rocketSnapPoint = rocketSlot.SnapPoint;
rocketSlot?.Configure(this);
}
public override void OnNetworkSpawn()
{
state.OnValueChanged += HandleStateChanged;
loadedRocketId.OnValueChanged += HandleRocketIdChanged;
selectedDelay.OnValueChanged += HandleDelayChanged;
fireAt.OnValueChanged += HandleFireAtChanged;
lastShot.OnValueChanged += HandleShotChanged;
rocketSlot?.Configure(this);
if (IsServer)
{
state.Value = CannonState.Empty;
loadedRocketId.Value = EmptyRocketId;
selectedDelay.Value = config != null ? config.DefaultDelay : 5f;
fireAt.Value = 0d;
}
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
state.OnValueChanged -= HandleStateChanged;
loadedRocketId.OnValueChanged -= HandleRocketIdChanged;
selectedDelay.OnValueChanged -= HandleDelayChanged;
fireAt.OnValueChanged -= HandleFireAtChanged;
lastShot.OnValueChanged -= HandleShotChanged;
}
private void Update()
{
if (!IsServer || !IsSpawned)
return;
UpdateReinsertBlock();
if (State == CannonState.Countdown &&
NetworkManager != null && NetworkManager.ServerTime.Time >= fireAt.Value)
{
FireOnServer();
}
}
private void LateUpdate()
{
if (!IsServer || !IsSpawned || LoadedRocket == null ||
rocketSnapPoint == null)
return;
loadedRocket.transform.SetPositionAndRotation(
rocketSnapPoint.position,
rocketSnapPoint.rotation);
}
public bool TryInsertRocket(NetworkRocketItem rocket)
{
if (!IsServer || !IsSpawned || State != CannonState.Empty ||
rocket == null || !rocket.IsSpawned || rocket.Definition == null ||
rocket.NetworkObjectId == blockedRocketId ||
(config != null && !config.AcceptDefectiveRockets && rocket.IsDefective) ||
(config != null && !config.AcceptCriticallyDamagedRockets &&
rocket.IsCriticallyDamaged) ||
!WorkstationItemControlUtility.TryGetControl(
rocket.GetComponent<NetworkItem>(),
out IWorkstationItemControl control) ||
!control.TryAcquireExternalControl())
{
return false;
}
loadedRocket = rocket;
loadedRocketControl = control;
loadedRocketId.Value = rocket.NetworkObjectId;
state.Value = CannonState.Loaded;
SnapLoadedRocket();
return true;
}
public void RequestTakeRocket()
{
if (CanTakeRocket)
RequestTakeRocketRpc();
}
public void RequestSetShotDelay(float seconds)
{
if (!IsSpawned)
return;
if (IsServer)
SetShotDelayOnServer(seconds);
else
RequestSetShotDelayRpc(seconds);
}
public void RequestAdjustShotDelay(float delta)
{
RequestSetShotDelay(SelectedDelay + delta);
}
public void RequestIncreaseShotDelay()
{
RequestAdjustShotDelay(config != null ? config.AdjustmentStep : 1f);
}
public void RequestDecreaseShotDelay()
{
RequestAdjustShotDelay(-(config != null ? config.AdjustmentStep : 1f));
}
public void RequestStartCountdown()
{
if (!IsSpawned)
return;
if (IsServer)
TryStartCountdownOnServer();
else
RequestStartCountdownRpc();
}
public void SetFireRequested(bool isActivated)
{
if (isActivated && IsServer && IsSpawned)
TryStartCountdownOnServer();
}
[ContextMenu("Debug/Start Countdown (Server Play Mode)")]
private void StartCountdownFromContextMenu()
{
if (Application.isPlaying)
TryStartCountdownOnServer();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestSetShotDelayRpc(float seconds) => SetShotDelayOnServer(seconds);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStartCountdownRpc() => TryStartCountdownOnServer();
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestTakeRocketRpc(RpcParams rpcParams = default)
{
if (!CanTakeRocket || rocketSlot == null ||
!TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject player))
return;
float maxDistanceSquared = maximumTakeDistance * maximumTakeDistance;
if ((player.transform.position - rocketSlot.InteractionPosition).sqrMagnitude >
maxDistanceSquared)
return;
NetworkRocketItem rocket = LoadedRocket;
IWorkstationItemControl control = loadedRocketControl ?? ResolveLoadedControl();
if (rocket == null || control == null ||
!control.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId))
return;
blockedRocketId = rocket.NetworkObjectId;
blockedRocketColliders = rocket.GetComponentsInChildren<Collider>(true);
unblockAfter = GetNetworkTime() + minimumReinsertBlockDuration;
ClearRocketSlot();
}
private void SetShotDelayOnServer(float seconds)
{
if (!IsServer || State == CannonState.Countdown)
return;
selectedDelay.Value = config != null
? config.ClampDelay(seconds)
: Mathf.Clamp(seconds, 0f, 60f);
}
private bool TryStartCountdownOnServer()
{
if (!IsServer || !IsSpawned || State != CannonState.Loaded ||
LoadedRocket == null || NetworkManager == null)
return false;
fireAt.Value = NetworkManager.ServerTime.Time + selectedDelay.Value;
state.Value = CannonState.Countdown;
return true;
}
private void FireOnServer()
{
NetworkRocketItem rocket = LoadedRocket;
if (rocket == null || rocket.Definition == null)
{
ClearRocketSlot();
return;
}
MissileType missileType = rocket.Definition.MissileType;
mapController?.RequestShot(missileType);
shotShockwave?.EmitShockwave();
lastShot.Value = new CannonShotState(lastShot.Value.Sequence + 1, missileType);
NetworkObject rocketObject = rocket.NetworkObject;
loadedRocket = null;
loadedRocketControl = null;
loadedRocketId.Value = EmptyRocketId;
fireAt.Value = 0d;
state.Value = CannonState.Empty;
if (rocketObject != null && rocketObject.IsSpawned)
rocketObject.Despawn(true);
}
private void ClearRocketSlot()
{
loadedRocket = null;
loadedRocketControl = null;
loadedRocketId.Value = EmptyRocketId;
fireAt.Value = 0d;
state.Value = CannonState.Empty;
}
private void SnapLoadedRocket()
{
if (loadedRocket != null && rocketSnapPoint != null)
{
loadedRocket.transform.SetPositionAndRotation(
rocketSnapPoint.position,
rocketSnapPoint.rotation);
}
}
private NetworkRocketItem ResolveLoadedRocket()
{
if (loadedRocketId.Value == EmptyRocketId || NetworkManager == null ||
NetworkManager.SpawnManager == null ||
!NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(
loadedRocketId.Value, out NetworkObject networkObject))
return null;
loadedRocket = networkObject.GetComponent<NetworkRocketItem>();
return loadedRocket;
}
private IWorkstationItemControl ResolveLoadedControl()
{
NetworkRocketItem rocket = LoadedRocket;
WorkstationItemControlUtility.TryGetControl(
rocket != null ? rocket.GetComponent<NetworkItem>() : null,
out loadedRocketControl);
return loadedRocketControl;
}
private bool TryGetPlayerObject(ulong clientId, out NetworkObject player)
{
player = null;
return NetworkManager != null &&
NetworkManager.ConnectedClients.TryGetValue(clientId, out NetworkClient client) &&
(player = client.PlayerObject) != null;
}
private void UpdateReinsertBlock()
{
if (blockedRocketId == EmptyRocketId || rocketSlot == null ||
GetNetworkTime() < unblockAfter ||
rocketSlot.OverlapsAny(blockedRocketColliders, 0.05f))
return;
blockedRocketId = EmptyRocketId;
blockedRocketColliders = null;
}
private double GetNetworkTime() => NetworkManager != null
? NetworkManager.ServerTime.Time
: Time.timeAsDouble;
private void HandleStateChanged(CannonState _, CannonState __) => Changed?.Invoke();
private void HandleRocketIdChanged(ulong _, ulong __)
{
loadedRocket = null;
loadedRocketControl = null;
Changed?.Invoke();
}
private void HandleDelayChanged(float _, float __) => Changed?.Invoke();
private void HandleFireAtChanged(double _, double __) => Changed?.Invoke();
private void HandleShotChanged(CannonShotState previous, CannonShotState current)
{
if (current.Sequence == previous.Sequence)
return;
ShotFired?.Invoke(current);
Changed?.Invoke();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8382605c6a901154783b2faf23067629
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c09a47e212ae515468d691409560c5d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 03ef14f0e5db05941afa0b0132e3bfb3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,126 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class NetworkShockwaveEmitter : NetworkBehaviour
{
private const int MaximumOverlaps = 256;
[SerializeField] private ShockwaveProfile profile;
[SerializeField] private Transform origin;
[SerializeField] private LayerMask physicsLayers = ~0;
[SerializeField] private UnityEvent shockwaveEmitted;
private readonly NetworkVariable<uint> sequence = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly Collider[] overlapBuffer = new Collider[MaximumOverlaps];
private readonly HashSet<Rigidbody> affectedBodies = new();
public ShockwaveProfile Profile => profile;
public Vector3 Origin => origin != null ? origin.position : transform.position;
public uint Sequence => sequence.Value;
public event System.Action Emitted;
public override void OnNetworkSpawn()
{
sequence.OnValueChanged += HandleSequenceChanged;
}
public override void OnNetworkDespawn()
{
sequence.OnValueChanged -= HandleSequenceChanged;
}
public bool EmitShockwave() => EmitShockwave(Origin);
public bool EmitShockwave(Vector3 worldOrigin)
{
if (!IsServer || !IsSpawned || profile == null)
return false;
ApplyToPlayers(worldOrigin);
ApplyToPhysics(worldOrigin);
sequence.Value++;
return true;
}
[ContextMenu("Debug/Emit Shockwave (Server Play Mode)")]
private void EmitFromContextMenu()
{
if (Application.isPlaying)
EmitShockwave();
}
private void ApplyToPlayers(Vector3 worldOrigin)
{
foreach (PlayerImpactController player in
FindObjectsByType<PlayerImpactController>(FindObjectsSortMode.None))
{
if (!player.IsSpawned)
continue;
Vector3 offset = player.transform.position - worldOrigin;
float distance = offset.magnitude;
if (!profile.TryEvaluate(distance, out ShockwaveZone zone))
continue;
Vector3 direction = CreateDirection(offset, zone.UpwardBias);
player.ApplyImpactOnServer(direction * zone.PlayerImpulse, zone);
}
}
private void ApplyToPhysics(Vector3 worldOrigin)
{
int count = Physics.OverlapSphereNonAlloc(
worldOrigin,
profile.MaximumRadius,
overlapBuffer,
physicsLayers,
QueryTriggerInteraction.Ignore);
affectedBodies.Clear();
for (int index = 0; index < count; index++)
{
Collider hit = overlapBuffer[index];
Rigidbody body = hit != null ? hit.attachedRigidbody : null;
if (body == null || body.isKinematic || !affectedBodies.Add(body) ||
body.GetComponentInParent<PlayerImpactController>() != null)
{
continue;
}
Vector3 offset = body.worldCenterOfMass - worldOrigin;
if (!profile.TryEvaluate(offset.magnitude, out ShockwaveZone zone) ||
zone.RigidbodyImpulse <= 0f)
{
continue;
}
body.AddForce(
CreateDirection(offset, zone.UpwardBias) * zone.RigidbodyImpulse,
ForceMode.Impulse);
}
}
private static Vector3 CreateDirection(Vector3 offset, float upwardBias)
{
if (offset.sqrMagnitude < 0.0001f)
offset = Vector3.up;
offset.Normalize();
return (offset + Vector3.up * Mathf.Clamp01(upwardBias)).normalized;
}
private void HandleSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
Emitted?.Invoke();
shockwaveEmitted?.Invoke();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43f56ec799f9aec4ca1bf372c88592cd
@@ -0,0 +1,110 @@
using System;
using Unity.Netcode;
using UnityEngine;
public struct PlayerImpactState : INetworkSerializable, IEquatable<PlayerImpactState>
{
public uint Sequence;
public Vector3 Impulse;
public float ShakeAmplitude;
public float ShakeDuration;
public float ShakeFrequency;
public float ShakeSeed;
public PlayerImpactState(
uint sequence,
Vector3 impulse,
float shakeAmplitude,
float shakeDuration,
float shakeFrequency,
float shakeSeed)
{
Sequence = sequence;
Impulse = impulse;
ShakeAmplitude = shakeAmplitude;
ShakeDuration = shakeDuration;
ShakeFrequency = shakeFrequency;
ShakeSeed = shakeSeed;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
where T : IReaderWriter
{
serializer.SerializeValue(ref Sequence);
serializer.SerializeValue(ref Impulse);
serializer.SerializeValue(ref ShakeAmplitude);
serializer.SerializeValue(ref ShakeDuration);
serializer.SerializeValue(ref ShakeFrequency);
serializer.SerializeValue(ref ShakeSeed);
}
public bool Equals(PlayerImpactState other) =>
Sequence == other.Sequence && Impulse == other.Impulse &&
ShakeAmplitude.Equals(other.ShakeAmplitude) &&
ShakeDuration.Equals(other.ShakeDuration) &&
ShakeFrequency.Equals(other.ShakeFrequency) &&
ShakeSeed.Equals(other.ShakeSeed);
}
[DisallowMultipleComponent]
public sealed class PlayerImpactController : NetworkBehaviour, IPlayerSystem
{
private readonly NetworkVariable<PlayerImpactState> impactState = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private PlayerController movement;
private PlayerCameraShakeController cameraShake;
public uint LastImpactSequence => impactState.Value.Sequence;
public event Action<PlayerImpactState> ImpactApplied;
public void Initialize(PlayerFacade facade)
{
movement = GetComponent<PlayerController>();
cameraShake = GetComponent<PlayerCameraShakeController>();
}
public override void OnNetworkSpawn()
{
impactState.OnValueChanged += HandleImpactChanged;
}
public override void OnNetworkDespawn()
{
impactState.OnValueChanged -= HandleImpactChanged;
}
public bool ApplyImpactOnServer(Vector3 impulse, ShockwaveZone zone)
{
if (!IsServer || !IsSpawned)
return false;
impactState.Value = new PlayerImpactState(
impactState.Value.Sequence + 1,
impulse,
zone.CameraShakeAmplitude,
zone.CameraShakeDuration,
zone.CameraShakeFrequency,
UnityEngine.Random.Range(0f, 1000f));
return true;
}
private void HandleImpactChanged(PlayerImpactState previous, PlayerImpactState current)
{
if (current.Sequence == 0 || current.Sequence == previous.Sequence)
return;
ImpactApplied?.Invoke(current);
if (!IsOwner)
return;
movement?.AddImpulse(current.Impulse);
cameraShake?.PlayShake(
current.ShakeAmplitude,
current.ShakeDuration,
current.ShakeFrequency,
current.ShakeSeed);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a7ddedc58d3467449dfbe83af1c9c51
@@ -0,0 +1,57 @@
using System.Collections;
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkRocketItem), typeof(NetworkShockwaveEmitter))]
public sealed class RocketImpactExplosion : NetworkBehaviour
{
[SerializeField] private NetworkRocketItem rocket;
[SerializeField] private NetworkShockwaveEmitter shockwave;
[SerializeField] private bool despawnAfterExplosion = true;
[SerializeField, Min(0f)] private float despawnDelay = 0.15f;
private bool exploded;
private void Reset() => ResolveReferences();
private void Awake() => ResolveReferences();
public override void OnNetworkSpawn()
{
rocket.CriticalImpact += HandleCriticalImpact;
}
public override void OnNetworkDespawn()
{
if (rocket != null)
rocket.CriticalImpact -= HandleCriticalImpact;
}
private void HandleCriticalImpact()
{
if (!IsServer || exploded)
return;
exploded = true;
shockwave.EmitShockwave(rocket.LastCriticalImpactPoint);
if (despawnAfterExplosion)
StartCoroutine(DespawnAfterDelay());
}
private IEnumerator DespawnAfterDelay()
{
if (despawnDelay > 0f)
yield return new WaitForSeconds(despawnDelay);
if (IsServer && NetworkObject != null && NetworkObject.IsSpawned)
NetworkObject.Despawn(true);
}
private void ResolveReferences()
{
if (rocket == null)
rocket = GetComponent<NetworkRocketItem>();
if (shockwave == null)
shockwave = GetComponent<NetworkShockwaveEmitter>();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac11f143457835f46a1a2defaa624354
@@ -0,0 +1,68 @@
using System;
using UnityEngine;
[Serializable]
public struct ShockwaveZone
{
[Min(0.1f)] public float Radius;
[Min(0f)] public float PlayerImpulse;
[Min(0f)] public float RigidbodyImpulse;
[Range(0f, 1f)] public float UpwardBias;
[Min(0f)] public float CameraShakeAmplitude;
[Min(0f)] public float CameraShakeDuration;
[Min(0.1f)] public float CameraShakeFrequency;
}
[CreateAssetMenu(
fileName = "ShockwaveProfile",
menuName = "FORT-BO/Effects/Shockwave Profile")]
public sealed class ShockwaveProfile : ScriptableObject
{
[Tooltip("Zones are evaluated from the smallest radius to the largest.")]
[SerializeField] private ShockwaveZone[] zones = Array.Empty<ShockwaveZone>();
public float MaximumRadius
{
get
{
float maximum = 0f;
foreach (ShockwaveZone zone in zones)
maximum = Mathf.Max(maximum, zone.Radius);
return maximum;
}
}
public bool TryEvaluate(float distance, out ShockwaveZone result)
{
result = default;
float bestRadius = float.PositiveInfinity;
bool found = false;
foreach (ShockwaveZone zone in zones)
{
if (distance > zone.Radius || zone.Radius >= bestRadius)
continue;
result = zone;
bestRadius = zone.Radius;
found = true;
}
return found;
}
private void OnValidate()
{
for (int index = 0; index < zones.Length; index++)
{
ShockwaveZone zone = zones[index];
zone.Radius = Mathf.Max(0.1f, zone.Radius);
zone.PlayerImpulse = Mathf.Max(0f, zone.PlayerImpulse);
zone.RigidbodyImpulse = Mathf.Max(0f, zone.RigidbodyImpulse);
zone.CameraShakeAmplitude = Mathf.Max(0f, zone.CameraShakeAmplitude);
zone.CameraShakeDuration = Mathf.Max(0f, zone.CameraShakeDuration);
zone.CameraShakeFrequency = Mathf.Max(0.1f, zone.CameraShakeFrequency);
zones[index] = zone;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96e16f6f88696ea4c8b884f3e7cef9f7
@@ -5,14 +5,20 @@ using UnityEngine.Events;
public sealed class NetworkButtonInteractable : NetworkInteractable
{
[SerializeField] private UnityEvent pressed = new();
[SerializeField] private UnityEvent<bool> buttonStateChanged;
private readonly NetworkVariable<bool> isPressed = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<uint> pressSequence = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public bool IsPressed => isPressed.Value;
public uint PressSequence => pressSequence.Value;
public event Action Pressed;
public event Action<bool> StateChanged;
@@ -20,24 +26,35 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
public override void OnNetworkSpawn()
{
isPressed.OnValueChanged += HandleStateChanged;
pressSequence.OnValueChanged += HandlePressSequenceChanged;
ApplyState(isPressed.Value);
}
public override void OnNetworkDespawn()
{
isPressed.OnValueChanged -= HandleStateChanged;
pressSequence.OnValueChanged -= HandlePressSequenceChanged;
}
protected override bool CanInteractOnServer(ulong senderClientId) => true;
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
{
Pressed?.Invoke();
isPressed.Value = !isPressed.Value;
pressSequence.Value++;
}
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
private void HandlePressSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
Pressed?.Invoke();
pressed?.Invoke();
}
private void ApplyState(bool currentValue)
{
StateChanged?.Invoke(currentValue);
@@ -404,6 +404,12 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
{
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
if (IsExternallyControlled && !physicsBody.isKinematic)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
if (shouldBeKinematic)
{
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
@@ -417,11 +423,6 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
}
if (IsExternallyControlled)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
}
if (HasAnyDragger)
@@ -37,8 +37,7 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
if (!TryGetPlayerObject(senderClientId, out NetworkObject playerObject))
return;
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
if ((playerObject.transform.position - transform.position).sqrMagnitude > maximumDistanceSquared)
if (!IsWithinInteractionDistance(playerObject.transform.position))
return;
if (!CanInteractOnServer(senderClientId))
@@ -47,6 +46,28 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
InteractOnServer(senderClientId, playerObject);
}
private bool IsWithinInteractionDistance(Vector3 playerPosition)
{
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
if ((playerPosition - transform.position).sqrMagnitude <= maximumDistanceSquared)
return true;
foreach (Collider interactionCollider in GetComponentsInChildren<Collider>(true))
{
if (interactionCollider == null || !interactionCollider.enabled ||
!interactionCollider.gameObject.activeInHierarchy)
{
continue;
}
Vector3 closestPoint = interactionCollider.ClosestPoint(playerPosition);
if ((playerPosition - closestPoint).sqrMagnitude <= maximumDistanceSquared)
return true;
}
return false;
}
protected virtual bool CanInteractOnServer(ulong senderClientId) => false;
protected virtual void InteractOnServer(ulong senderClientId, NetworkObject playerObject) { }
@@ -58,4 +79,3 @@ public class NetworkInteractable : NetworkBehaviour, IInteractable
return playerObject != null;
}
}
@@ -7,11 +7,14 @@ public sealed class RocketItemDefinition : ItemDefinition
{
[Tooltip("Stable rocket family id used by launchers and orders.")]
[SerializeField] private string rocketTypeId;
[Tooltip("Combat type sent to the map when this rocket is fired.")]
[SerializeField] private MissileType missileType = MissileType.Standard;
[SerializeField, Min(0.1f)] private float criticalImpactSpeed = 7f;
public string RocketTypeId => string.IsNullOrWhiteSpace(rocketTypeId)
? ItemId
: rocketTypeId;
public MissileType MissileType => missileType;
public float CriticalImpactSpeed => criticalImpactSpeed;
protected override void OnValidate()
@@ -0,0 +1,75 @@
using UnityEngine;
[DisallowMultipleComponent]
public sealed class PlayerCameraShakeController : MonoBehaviour, IPlayerSystem
{
private Transform cameraTransform;
private Vector3 baseLocalPosition;
private float amplitude;
private float frequency;
private float seed;
private float endsAt;
private float startedAt;
private float duration;
public bool IsShaking => cameraTransform != null && Time.time < endsAt;
public void Initialize(PlayerFacade facade)
{
FirstPersonCameraController cameraController =
GetComponent<FirstPersonCameraController>();
cameraTransform = cameraController != null && cameraController.PlayerCamera != null
? cameraController.PlayerCamera.transform
: GetComponentInChildren<Camera>(true)?.transform;
if (cameraTransform != null)
baseLocalPosition = cameraTransform.localPosition;
}
public void PlayShake(
float newAmplitude,
float newDuration,
float newFrequency,
float newSeed = 0f)
{
if (cameraTransform == null || newAmplitude <= 0f || newDuration <= 0f)
return;
float remainingStrength = IsShaking
? amplitude * Mathf.Clamp01((endsAt - Time.time) / Mathf.Max(0.01f, duration))
: 0f;
amplitude = Mathf.Max(remainingStrength, newAmplitude);
duration = newDuration;
frequency = Mathf.Max(0.1f, newFrequency);
seed = newSeed;
startedAt = Time.time;
endsAt = Time.time + newDuration;
}
private void LateUpdate()
{
if (cameraTransform == null)
return;
if (!IsShaking)
{
cameraTransform.localPosition = baseLocalPosition;
return;
}
float normalizedTime = Mathf.Clamp01((Time.time - startedAt) / Mathf.Max(0.01f, duration));
float strength = amplitude * (1f - normalizedTime) * (1f - normalizedTime);
float sample = (Time.time - startedAt) * frequency;
Vector3 noise = new(
Mathf.PerlinNoise(seed, sample) * 2f - 1f,
Mathf.PerlinNoise(seed + 17.31f, sample) * 2f - 1f,
Mathf.PerlinNoise(seed + 39.77f, sample) * 2f - 1f);
cameraTransform.localPosition = baseLocalPosition + noise * strength;
}
private void OnDisable()
{
if (cameraTransform != null)
cameraTransform.localPosition = baseLocalPosition;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b12d3c124e93b5f4f9c06c7e870d4068
@@ -13,10 +13,16 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
[SerializeField, Min(0.1f)] private float jumpHeight = 1.4f;
[SerializeField, Min(0.1f)] private float gravity = 20f;
[Header("External impulses")]
[SerializeField, Min(0.1f)] private float impulseDamping = 9f;
private CharacterController characterController;
private Vector3 verticalVelocity;
private Vector3 externalImpulseVelocity;
private bool isInitialized;
public Vector3 ExternalImpulseVelocity => externalImpulseVelocity;
public void Initialize(PlayerFacade facade)
{
characterController = GetComponent<CharacterController>();
@@ -39,6 +45,14 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
transform.Rotate(0f, yawDelta, 0f, Space.Self);
}
public void AddImpulse(Vector3 velocityChange)
{
if (!isInitialized || !IsOwner)
return;
externalImpulseVelocity += velocityChange;
}
private void SimulateMovement(Vector2 moveInput, bool wantsToSprint, bool wantsToJump)
{
if (moveInput.sqrMagnitude > 1f)
@@ -54,6 +68,12 @@ public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
float speed = wantsToSprint ? sprintSpeed : walkSpeed;
Vector3 horizontalDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
characterController.Move((horizontalDirection * speed + verticalVelocity) * Time.deltaTime);
characterController.Move(
(horizontalDirection * speed + verticalVelocity + externalImpulseVelocity) *
Time.deltaTime);
externalImpulseVelocity = Vector3.MoveTowards(
externalImpulseVelocity,
Vector3.zero,
impulseDamping * Time.deltaTime);
}
}
@@ -80,6 +80,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
{
RaycastHit hit = interactionHits[hitIndex];
if (hit.collider != null && hit.collider.transform.IsChildOf(transform))
continue;
IInteractable interactable = FindInteractable(hit.collider);
if (interactable == null)
{
File diff suppressed because one or more lines are too long
@@ -1,8 +1,15 @@
fileFormatVersion: 2
guid: 6e59c59b81ab47f9b6ec5781fa725d2c
timeCreated: 1484171296
licenseType: Pro
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Fonts/LiberationSans - OFL.txt
uploadId: 691001
@@ -1,7 +1,7 @@
fileFormatVersion: 2
guid: e3265ab4bf004d28a9537516768c1c75
timeCreated: 1484171297
licenseType: Pro
licenseType: Store
TrueTypeFontImporter:
serializedVersion: 2
fontSize: 16
@@ -17,3 +17,10 @@ TrueTypeFontImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Fonts/LiberationSans.ttf
uploadId: 691001
@@ -2,7 +2,7 @@ fileFormatVersion: 2
guid: 731f1baa9d144a9897cb1d341c2092b8
folderAsset: yes
timeCreated: 1442040525
licenseType: Pro
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
@@ -1,8 +1,16 @@
fileFormatVersion: 2
guid: e73a58f6e2794ae7b1b7e50b7fb811b0
timeCreated: 1484172806
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Drop Shadow.mat
uploadId: 691001
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,16 @@
fileFormatVersion: 2
guid: 2e498d1c8094910479dc3e1b768306a4
timeCreated: 1484171803
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Fallback.asset
uploadId: 691001
@@ -1,8 +1,16 @@
fileFormatVersion: 2
guid: 79459efec17a4d00a321bdcc27bbc385
timeCreated: 1484172856
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Outline.mat
uploadId: 691001
@@ -1,8 +1,15 @@
fileFormatVersion: 2
guid: 8f586378b4e144a9851e7b34d9b748ee
timeCreated: 1484171803
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
uploadId: 691001
@@ -6,3 +6,10 @@ TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/LineBreaking Following Characters.txt
uploadId: 691001
@@ -6,3 +6,10 @@ TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/LineBreaking Leading Characters.txt
uploadId: 691001
@@ -2,7 +2,7 @@ fileFormatVersion: 2
guid: 512a49d95c0c4332bdd98131869c23c9
folderAsset: yes
timeCreated: 1441876896
licenseType: Pro
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
@@ -1,8 +1,15 @@
fileFormatVersion: 2
guid: c41005c129ba4d66911b75229fd70b45
timeCreated: 1480316912
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Sprite Assets/EmojiOne.asset
uploadId: 691001
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 8a731a09451cba54096a9002cd9b9be5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Sprite Assets/MixedIndexTest.asset
uploadId: 691001
@@ -2,7 +2,7 @@ fileFormatVersion: 2
guid: 4aecb92fff08436c8303b10eab8da368
folderAsset: yes
timeCreated: 1441876950
licenseType: Pro
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
@@ -6,3 +6,10 @@ NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/Style Sheets/Default Style Sheet.asset
uploadId: 691001
@@ -1,8 +1,15 @@
fileFormatVersion: 2
guid: 3f5b5dff67a942289a9defa416b206f3
timeCreated: 1436653997
licenseType: Pro
licenseType: Store
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Resources/TMP Settings.asset
uploadId: 691001
@@ -8,3 +8,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/SDFFunctions.hlsl
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_Bitmap-Custom-Atlas.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_Bitmap-Mobile.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_Bitmap.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF Overlay.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF SSD.shader
uploadId: 691001
@@ -8,3 +8,10 @@ ScriptedImporter:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP LIT.shadergraph
uploadId: 691001
@@ -8,3 +8,10 @@ ScriptedImporter:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-HDRP UNLIT.shadergraph
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Masking.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile Overlay.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile SSD.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile-2-Pass.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Mobile.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Surface-Mobile.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-Surface.shader
uploadId: 691001
@@ -8,3 +8,10 @@ ScriptedImporter:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-URP Lit.shadergraph
uploadId: 691001
@@ -8,3 +8,10 @@ ScriptedImporter:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF-URP Unlit.shadergraph
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_SDF.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMP_Sprite.shader
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData: Version 2.0
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMPro.cginc
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMPro_Mobile.cginc
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc
uploadId: 691001
@@ -7,3 +7,10 @@ ShaderImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Shaders/TMPro_Surface.cginc
uploadId: 691001
@@ -5,3 +5,10 @@ TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 127325
packageName: Particle Pack
packageVersion: 3.1
assetPath: Assets/TextMesh Pro/Sprites/EmojiOne Attribution.txt
uploadId: 691001

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