Merge branch 'main' into dump-dump
This commit is contained in:
@@ -24,3 +24,25 @@ MonoBehaviour:
|
|||||||
m_PreInfinity: 0
|
m_PreInfinity: 0
|
||||||
m_PostInfinity: 0
|
m_PostInfinity: 0
|
||||||
m_RotationOrder: 0
|
m_RotationOrder: 0
|
||||||
|
- From: '**ANY CAMERA**'
|
||||||
|
To: TargetCamera
|
||||||
|
Blend:
|
||||||
|
Style: 6
|
||||||
|
Time: 3
|
||||||
|
CustomCurve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve: []
|
||||||
|
m_PreInfinity: 0
|
||||||
|
m_PostInfinity: 0
|
||||||
|
m_RotationOrder: 0
|
||||||
|
- From: TargetCamera
|
||||||
|
To: '**ANY CAMERA**'
|
||||||
|
Blend:
|
||||||
|
Style: 1
|
||||||
|
Time: 1
|
||||||
|
CustomCurve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve: []
|
||||||
|
m_PreInfinity: 0
|
||||||
|
m_PostInfinity: 0
|
||||||
|
m_RotationOrder: 0
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ GameObject:
|
|||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 128829408}
|
- component: {fileID: 128829408}
|
||||||
- component: {fileID: 128829407}
|
- component: {fileID: 128829407}
|
||||||
|
- component: {fileID: 128829409}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: MinigameManager
|
m_Name: MinigameManager
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -307,6 +308,7 @@ MonoBehaviour:
|
|||||||
player: {fileID: 941621859}
|
player: {fileID: 941621859}
|
||||||
obstacleSpawner: {fileID: 938885957}
|
obstacleSpawner: {fileID: 938885957}
|
||||||
targetSpawner: {fileID: 1838778561}
|
targetSpawner: {fileID: 1838778561}
|
||||||
|
tapToStartController: {fileID: 128829409}
|
||||||
gameOverScreen: {fileID: 81231374}
|
gameOverScreen: {fileID: 81231374}
|
||||||
poopPrefab: {fileID: 5552423787977869117, guid: 066f9990a9b1f5547b387633d5d204c0, type: 3}
|
poopPrefab: {fileID: 5552423787977869117, guid: 066f9990a9b1f5547b387633d5d204c0, type: 3}
|
||||||
poopCooldown: 0.5
|
poopCooldown: 0.5
|
||||||
@@ -325,6 +327,23 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &128829409
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 128829406}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 2a6ee5aca3ca423c82b57e16c0b2cca3, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.BirdPooper.TapToStartController
|
||||||
|
fingerContainer: {fileID: 2064126204}
|
||||||
|
fingerImage: {fileID: 1481863789}
|
||||||
|
blinkDuration: 1.5
|
||||||
|
minAlpha: 0.3
|
||||||
|
maxAlpha: 1
|
||||||
--- !u!1 &402349268
|
--- !u!1 &402349268
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -771,53 +790,6 @@ MonoBehaviour:
|
|||||||
despawnPoint: {fileID: 938473626}
|
despawnPoint: {fileID: 938473626}
|
||||||
referenceMarker: {fileID: 1143700529}
|
referenceMarker: {fileID: 1143700529}
|
||||||
cameraAdapter: {fileID: 2103114179}
|
cameraAdapter: {fileID: 2103114179}
|
||||||
obstaclePrefabs:
|
|
||||||
- {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3}
|
|
||||||
- {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3}
|
|
||||||
- {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3}
|
|
||||||
- {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3}
|
|
||||||
- {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3}
|
|
||||||
- {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3}
|
|
||||||
- {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3}
|
|
||||||
minSpawnInterval: 2
|
|
||||||
maxSpawnInterval: 8
|
|
||||||
difficultyRampDuration: 360
|
|
||||||
difficultyCurve:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Curve:
|
|
||||||
- serializedVersion: 3
|
|
||||||
time: 0
|
|
||||||
value: 0
|
|
||||||
inSlope: 0
|
|
||||||
outSlope: 0
|
|
||||||
tangentMode: 0
|
|
||||||
weightedMode: 0
|
|
||||||
inWeight: 0
|
|
||||||
outWeight: 0
|
|
||||||
- serializedVersion: 3
|
|
||||||
time: 1
|
|
||||||
value: 1
|
|
||||||
inSlope: 2
|
|
||||||
outSlope: 2
|
|
||||||
tangentMode: 0
|
|
||||||
weightedMode: 0
|
|
||||||
inWeight: 0
|
|
||||||
outWeight: 0
|
|
||||||
m_PreInfinity: 2
|
|
||||||
m_PostInfinity: 2
|
|
||||||
m_RotationOrder: 4
|
|
||||||
intervalJitter: 0.3
|
|
||||||
recentDecayDuration: 60
|
|
||||||
minRecentWeight: 0.05
|
|
||||||
--- !u!1 &941621855
|
--- !u!1 &941621855
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1484,6 +1456,81 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1481863787
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1481863788}
|
||||||
|
- component: {fileID: 1481863790}
|
||||||
|
- component: {fileID: 1481863789}
|
||||||
|
m_Layer: 5
|
||||||
|
m_Name: Image
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &1481863788
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1481863787}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1.18, y: 1.18, z: 1.18}
|
||||||
|
m_ConstrainProportionsScale: 1
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 2064126205}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
|
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: 316, y: 246}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
|
--- !u!114 &1481863789
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1481863787}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
m_RaycastTarget: 1
|
||||||
|
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_Maskable: 1
|
||||||
|
m_OnCullStateChanged:
|
||||||
|
m_PersistentCalls:
|
||||||
|
m_Calls: []
|
||||||
|
m_Sprite: {fileID: -7164639588303836088, guid: 6fee60c82a4f504419e535456268a19e, type: 3}
|
||||||
|
m_Type: 0
|
||||||
|
m_PreserveAspect: 0
|
||||||
|
m_FillCenter: 1
|
||||||
|
m_FillMethod: 4
|
||||||
|
m_FillAmount: 1
|
||||||
|
m_FillClockwise: 1
|
||||||
|
m_FillOrigin: 0
|
||||||
|
m_UseSpriteMesh: 0
|
||||||
|
m_PixelsPerUnitMultiplier: 1
|
||||||
|
--- !u!222 &1481863790
|
||||||
|
CanvasRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1481863787}
|
||||||
|
m_CullTransparentMesh: 1
|
||||||
--- !u!1 &1498486830
|
--- !u!1 &1498486830
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1615,6 +1662,7 @@ RectTransform:
|
|||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1088771378}
|
- {fileID: 1088771378}
|
||||||
- {fileID: 81231372}
|
- {fileID: 81231372}
|
||||||
|
- {fileID: 2064126205}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
@@ -1844,6 +1892,42 @@ MonoBehaviour:
|
|||||||
- {fileID: 8373178063207716143, guid: 020f7494c613b06479ccad2c4cedde0f, type: 3}
|
- {fileID: 8373178063207716143, guid: 020f7494c613b06479ccad2c4cedde0f, type: 3}
|
||||||
minTargetSpawnInterval: 9
|
minTargetSpawnInterval: 9
|
||||||
maxTargetSpawnInterval: 15
|
maxTargetSpawnInterval: 15
|
||||||
|
--- !u!1 &2064126204
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 2064126205}
|
||||||
|
m_Layer: 5
|
||||||
|
m_Name: StartGamePrompt
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &2064126205
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2064126204}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 1481863788}
|
||||||
|
m_Father: {fileID: 1536057440}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
|
m_Pivot: {x: 0.5, y: 0.5}
|
||||||
--- !u!1 &2103114174
|
--- !u!1 &2103114174
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -788,8 +788,11 @@ MonoBehaviour:
|
|||||||
camera: {fileID: 761345710}
|
camera: {fileID: 761345710}
|
||||||
- state: 3
|
- state: 3
|
||||||
camera: {fileID: 263322553}
|
camera: {fileID: 263322553}
|
||||||
|
- state: 4
|
||||||
|
camera: {fileID: 1842736650}
|
||||||
inactivePriority: 10
|
inactivePriority: 10
|
||||||
activePriority: 20
|
activePriority: 20
|
||||||
|
cinemachineBrain: {fileID: 1810521058}
|
||||||
showDebugLogs: 0
|
showDebugLogs: 0
|
||||||
--- !u!4 &597044887
|
--- !u!4 &597044887
|
||||||
Transform:
|
Transform:
|
||||||
@@ -3948,7 +3951,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1810521056}
|
m_GameObject: {fileID: 1810521056}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 12.800001, z: -10}
|
m_LocalPosition: {x: -18.9, y: 9.100001, z: -10}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -4136,6 +4139,101 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
m_SpriteSortPoint: 0
|
||||||
|
--- !u!1 &1842736649
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1842736651}
|
||||||
|
- component: {fileID: 1842736650}
|
||||||
|
- component: {fileID: 1842736652}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: TargetCamera
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &1842736650
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1842736649}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
Priority:
|
||||||
|
Enabled: 0
|
||||||
|
m_Value: 0
|
||||||
|
OutputChannel: 1
|
||||||
|
StandbyUpdate: 2
|
||||||
|
m_StreamingVersion: 20241001
|
||||||
|
m_LegacyPriority: 0
|
||||||
|
Target:
|
||||||
|
TrackingTarget: {fileID: 0}
|
||||||
|
LookAtTarget: {fileID: 0}
|
||||||
|
CustomLookAtTarget: 0
|
||||||
|
Lens:
|
||||||
|
FieldOfView: 60
|
||||||
|
OrthographicSize: 20
|
||||||
|
NearClipPlane: 0.3
|
||||||
|
FarClipPlane: 1000
|
||||||
|
Dutch: 0
|
||||||
|
ModeOverride: 0
|
||||||
|
PhysicalProperties:
|
||||||
|
GateFit: 2
|
||||||
|
SensorSize: {x: 21.946, y: 16.002}
|
||||||
|
LensShift: {x: 0, y: 0}
|
||||||
|
FocusDistance: 10
|
||||||
|
Iso: 200
|
||||||
|
ShutterSpeed: 0.005
|
||||||
|
Aperture: 16
|
||||||
|
BladeCount: 5
|
||||||
|
Curvature: {x: 2, y: 11}
|
||||||
|
BarrelClipping: 0.25
|
||||||
|
Anamorphism: 0
|
||||||
|
BlendHint: 0
|
||||||
|
--- !u!4 &1842736651
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1842736649}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: -9.6, y: 12.900002, z: -10}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &1842736652
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1842736649}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: b617507da6d07e749b7efdb34e1173e1, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineFollow
|
||||||
|
TrackerSettings:
|
||||||
|
BindingMode: 4
|
||||||
|
PositionDamping: {x: 1, y: 1, z: 1}
|
||||||
|
AngularDampingMode: 0
|
||||||
|
RotationDamping: {x: 1, y: 1, z: 1}
|
||||||
|
QuaternionDamping: 1
|
||||||
|
FollowOffset: {x: 0, y: 0, z: -10}
|
||||||
--- !u!1 &1897459173
|
--- !u!1 &1897459173
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -4895,6 +4993,37 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &2041920295
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 2041920296}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: GameObject
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &2041920296
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2041920295}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 16.8, y: 12.6, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &2049960991
|
--- !u!1 &2049960991
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -5024,81 +5153,6 @@ CanvasRenderer:
|
|||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 2090212067}
|
m_GameObject: {fileID: 2090212067}
|
||||||
m_CullTransparentMesh: 1
|
m_CullTransparentMesh: 1
|
||||||
--- !u!1 &2103114174
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 2103114178}
|
|
||||||
- component: {fileID: 2103114177}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: CinemachineCamera
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!114 &2103114177
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 2103114174}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
Priority:
|
|
||||||
Enabled: 0
|
|
||||||
m_Value: 0
|
|
||||||
OutputChannel: 1
|
|
||||||
StandbyUpdate: 2
|
|
||||||
m_StreamingVersion: 20241001
|
|
||||||
m_LegacyPriority: 0
|
|
||||||
Target:
|
|
||||||
TrackingTarget: {fileID: 0}
|
|
||||||
LookAtTarget: {fileID: 0}
|
|
||||||
CustomLookAtTarget: 0
|
|
||||||
Lens:
|
|
||||||
FieldOfView: 60
|
|
||||||
OrthographicSize: 15
|
|
||||||
NearClipPlane: 0.3
|
|
||||||
FarClipPlane: 1000
|
|
||||||
Dutch: 0
|
|
||||||
ModeOverride: 0
|
|
||||||
PhysicalProperties:
|
|
||||||
GateFit: 2
|
|
||||||
SensorSize: {x: 21.946, y: 16.002}
|
|
||||||
LensShift: {x: 0, y: 0}
|
|
||||||
FocusDistance: 10
|
|
||||||
Iso: 200
|
|
||||||
ShutterSpeed: 0.005
|
|
||||||
Aperture: 16
|
|
||||||
BladeCount: 5
|
|
||||||
Curvature: {x: 2, y: 11}
|
|
||||||
BarrelClipping: 0.25
|
|
||||||
Anamorphism: 0
|
|
||||||
BlendHint: 0
|
|
||||||
--- !u!4 &2103114178
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 2103114174}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 12.800001, z: -10}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1 &2111947703
|
--- !u!1 &2111947703
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -5578,7 +5632,6 @@ SceneRoots:
|
|||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_Roots:
|
m_Roots:
|
||||||
- {fileID: 1810521061}
|
- {fileID: 1810521061}
|
||||||
- {fileID: 2103114178}
|
|
||||||
- {fileID: 742382011}
|
- {fileID: 742382011}
|
||||||
- {fileID: 2125208580}
|
- {fileID: 2125208580}
|
||||||
- {fileID: 1239857187}
|
- {fileID: 1239857187}
|
||||||
@@ -5596,8 +5649,10 @@ SceneRoots:
|
|||||||
- {fileID: 595230075}
|
- {fileID: 595230075}
|
||||||
- {fileID: 2041347574}
|
- {fileID: 2041347574}
|
||||||
- {fileID: 761345711}
|
- {fileID: 761345711}
|
||||||
|
- {fileID: 1842736651}
|
||||||
- {fileID: 263322554}
|
- {fileID: 263322554}
|
||||||
- {fileID: 1017291799}
|
- {fileID: 1017291799}
|
||||||
- {fileID: 1064542409}
|
- {fileID: 1064542409}
|
||||||
- {fileID: 1701327424}
|
- {fileID: 1701327424}
|
||||||
|
- {fileID: 2041920296}
|
||||||
- {fileID: 1710395163}
|
- {fileID: 1710395163}
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
using UnityEngine;
|
|
||||||
using Core;
|
|
||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using UnityEngine.Playables;
|
|
||||||
using Input;
|
using Input;
|
||||||
using Unity.Cinemachine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Playables;
|
||||||
|
|
||||||
public class LevelIntroDirector : ManagedBehaviour
|
namespace Cinematics
|
||||||
{
|
{
|
||||||
public bool playOnSceneReady;
|
public class LevelIntroDirector : ManagedBehaviour
|
||||||
|
|
||||||
[HideInInspector]
|
|
||||||
public PlayableDirector introPlayableDirector;
|
|
||||||
|
|
||||||
internal override void OnSceneReady()
|
|
||||||
{
|
{
|
||||||
base.OnSceneReady();
|
public bool playOnSceneReady;
|
||||||
if (playOnSceneReady)
|
|
||||||
|
[HideInInspector]
|
||||||
|
public PlayableDirector introPlayableDirector;
|
||||||
|
|
||||||
|
internal override void OnSceneReady()
|
||||||
{
|
{
|
||||||
introPlayableDirector = GetComponent<PlayableDirector>();
|
base.OnSceneReady();
|
||||||
introPlayableDirector.stopped += IntroTimelineStopped;
|
if (playOnSceneReady)
|
||||||
PlayIntroTimeline();
|
{
|
||||||
|
introPlayableDirector = GetComponent<PlayableDirector>();
|
||||||
|
introPlayableDirector.stopped += IntroTimelineStopped;
|
||||||
|
PlayIntroTimeline();
|
||||||
|
}
|
||||||
|
else { gameObject.SetActive(false); }
|
||||||
}
|
}
|
||||||
else { gameObject.SetActive(false); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void IntroTimelineStopped(PlayableDirector director)
|
private void IntroTimelineStopped(PlayableDirector director)
|
||||||
{
|
{
|
||||||
InputManager.Instance.SetInputMode(InputMode.Game);
|
InputManager.Instance.SetInputMode(InputMode.Game);
|
||||||
introPlayableDirector.stopped -= IntroTimelineStopped;
|
introPlayableDirector.stopped -= IntroTimelineStopped;
|
||||||
gameObject.SetActive(false);
|
gameObject.SetActive(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlayIntroTimeline()
|
public void PlayIntroTimeline()
|
||||||
{
|
{
|
||||||
introPlayableDirector.Play();
|
introPlayableDirector.Play();
|
||||||
InputManager.Instance.SetInputMode(InputMode.InputDisabled);
|
InputManager.Instance.SetInputMode(InputMode.InputDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ namespace Common.Camera
|
|||||||
[Tooltip("Priority for the active camera")]
|
[Tooltip("Priority for the active camera")]
|
||||||
[SerializeField] protected int activePriority = 20;
|
[SerializeField] protected int activePriority = 20;
|
||||||
|
|
||||||
|
[Header("Cinemachine Brain")]
|
||||||
|
[Tooltip("CinemachineBrain for blend detection (auto-finds if null)")]
|
||||||
|
[SerializeField] protected CinemachineBrain cinemachineBrain;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[SerializeField] protected bool showDebugLogs = false;
|
[SerializeField] protected bool showDebugLogs = false;
|
||||||
|
|
||||||
@@ -60,6 +64,11 @@ namespace Common.Camera
|
|||||||
private TState _currentState;
|
private TState _currentState;
|
||||||
private bool _isInitialized = false;
|
private bool _isInitialized = false;
|
||||||
|
|
||||||
|
// Event-driven blend tracking
|
||||||
|
private CinemachineCamera _pendingBlendTarget;
|
||||||
|
private bool _isBlendComplete;
|
||||||
|
private Action _pendingBlendCallback;
|
||||||
|
|
||||||
public TState CurrentState => _currentState;
|
public TState CurrentState => _currentState;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -71,6 +80,11 @@ namespace Common.Camera
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<TState, TState> OnStateChanged;
|
public event Action<TState, TState> OnStateChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when camera blend completes after state switch
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnBlendComplete;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Lifecycle
|
#region Lifecycle
|
||||||
@@ -84,6 +98,17 @@ namespace Common.Camera
|
|||||||
{
|
{
|
||||||
base.OnManagedAwake();
|
base.OnManagedAwake();
|
||||||
|
|
||||||
|
// Auto-find CinemachineBrain if not assigned
|
||||||
|
if (cinemachineBrain == null)
|
||||||
|
{
|
||||||
|
cinemachineBrain = UnityEngine.Camera.main?.GetComponent<CinemachineBrain>();
|
||||||
|
|
||||||
|
if (cinemachineBrain == null && showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Warning($"[{GetType().Name}] CinemachineBrain not found. Blend tracking will be unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize cameras from Inspector mappings
|
// Initialize cameras from Inspector mappings
|
||||||
InitializeCameraMap();
|
InitializeCameraMap();
|
||||||
|
|
||||||
@@ -91,6 +116,35 @@ namespace Common.Camera
|
|||||||
ValidateCameras();
|
ValidateCameras();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to Cinemachine events on enable
|
||||||
|
/// </summary>
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
// Subscribe to Cinemachine global events
|
||||||
|
CinemachineCore.BlendFinishedEvent.AddListener(OnBlendFinished);
|
||||||
|
CinemachineCore.CameraActivatedEvent.AddListener(OnCameraActivated);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Debug($"[{GetType().Name}] Subscribed to Cinemachine events");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe from Cinemachine events on disable
|
||||||
|
/// </summary>
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
// Unsubscribe from Cinemachine events to prevent memory leaks
|
||||||
|
CinemachineCore.BlendFinishedEvent.RemoveListener(OnBlendFinished);
|
||||||
|
CinemachineCore.CameraActivatedEvent.RemoveListener(OnCameraActivated);
|
||||||
|
|
||||||
|
// Clear any pending callbacks
|
||||||
|
_pendingBlendCallback = null;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Debug($"[{GetType().Name}] Unsubscribed from Cinemachine events");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Initialization
|
#region Initialization
|
||||||
@@ -229,6 +283,115 @@ namespace Common.Camera
|
|||||||
return _cameraMap.ContainsKey(state);
|
return _cameraMap.ContainsKey(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blend to a state and wait asynchronously (coroutine).
|
||||||
|
/// Yields until Cinemachine blend event fires. Event-driven, no polling.
|
||||||
|
/// Use this when you need to wait for the blend to complete before continuing.
|
||||||
|
/// </summary>
|
||||||
|
public System.Collections.IEnumerator BlendToStateAsync(TState newState)
|
||||||
|
{
|
||||||
|
// Reset completion flag
|
||||||
|
_isBlendComplete = false;
|
||||||
|
|
||||||
|
// Set pending target camera
|
||||||
|
if (!_cameraMap.TryGetValue(newState, out _pendingBlendTarget))
|
||||||
|
{
|
||||||
|
Logging.Error($"[{GetType().Name}] No camera for state {newState}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch camera state (triggers blend)
|
||||||
|
SwitchToState(newState);
|
||||||
|
|
||||||
|
// Fallback: if no brain, complete immediately
|
||||||
|
if (cinemachineBrain == null)
|
||||||
|
{
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Warning($"[{GetType().Name}] No brain, completing blend immediately");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for event to fire (event handlers set _isBlendComplete = true)
|
||||||
|
yield return new WaitUntil(() => _isBlendComplete);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Debug($"[{GetType().Name}] Blend to {newState} completed (event-driven)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blend to a state with callback invoked on completion.
|
||||||
|
/// Callback fires when Cinemachine blend event fires. Event-driven, no polling.
|
||||||
|
/// Use this when you want to perform an action after the blend completes.
|
||||||
|
/// </summary>
|
||||||
|
public void BlendToState(TState newState, Action onComplete)
|
||||||
|
{
|
||||||
|
// Set pending target camera
|
||||||
|
if (!_cameraMap.TryGetValue(newState, out _pendingBlendTarget))
|
||||||
|
{
|
||||||
|
Logging.Error($"[{GetType().Name}] No camera for state {newState}");
|
||||||
|
onComplete?.Invoke(); // Still invoke to prevent hanging
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store callback
|
||||||
|
_pendingBlendCallback = onComplete;
|
||||||
|
|
||||||
|
// Switch camera state (triggers blend)
|
||||||
|
SwitchToState(newState);
|
||||||
|
|
||||||
|
// Fallback: if no brain, invoke callback immediately
|
||||||
|
if (cinemachineBrain == null)
|
||||||
|
{
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Warning($"[{GetType().Name}] No brain, invoking callback immediately");
|
||||||
|
CompleteBlend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers will invoke callback when blend finishes
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when Cinemachine finishes a blend (non-zero length blends)
|
||||||
|
/// </summary>
|
||||||
|
private void OnBlendFinished(ICinemachineMixer mixer, ICinemachineCamera cam)
|
||||||
|
{
|
||||||
|
// Filter: only respond to blends from our brain to our expected camera
|
||||||
|
if (mixer == cinemachineBrain && cam == _pendingBlendTarget)
|
||||||
|
{
|
||||||
|
CompleteBlend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when Cinemachine activates a camera (handles instant cuts)
|
||||||
|
/// </summary>
|
||||||
|
private void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt)
|
||||||
|
{
|
||||||
|
// Filter: only respond to cuts from our brain to our expected camera
|
||||||
|
if (evt.Origin == cinemachineBrain && evt.IncomingCamera == _pendingBlendTarget && evt.IsCut)
|
||||||
|
{
|
||||||
|
CompleteBlend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark blend as complete and fire callbacks
|
||||||
|
/// </summary>
|
||||||
|
private void CompleteBlend()
|
||||||
|
{
|
||||||
|
_isBlendComplete = true;
|
||||||
|
_pendingBlendCallback?.Invoke();
|
||||||
|
_pendingBlendCallback = null;
|
||||||
|
OnBlendComplete?.Invoke();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
Logging.Debug($"[{GetType().Name}] Blend completed, callbacks invoked");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Validation
|
#region Validation
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AppleHills.Core.Settings;
|
using AppleHills.Core.Settings;
|
||||||
|
using Minigames.BirdPooper;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Core.Settings
|
namespace Core.Settings
|
||||||
@@ -33,8 +34,8 @@ namespace Core.Settings
|
|||||||
[Tooltip("Obstacle scroll speed in units/s")]
|
[Tooltip("Obstacle scroll speed in units/s")]
|
||||||
[SerializeField] private float obstacleMoveSpeed = 5f;
|
[SerializeField] private float obstacleMoveSpeed = 5f;
|
||||||
|
|
||||||
[Tooltip("Time between obstacle spawns in seconds")]
|
[Tooltip("Obstacle spawning configuration (pools, timing, difficulty)")]
|
||||||
[SerializeField] private float obstacleSpawnInterval = 2f;
|
[SerializeField] private ObstacleSpawnConfig obstacleSpawnConfiguration;
|
||||||
|
|
||||||
[Tooltip("X position where obstacles spawn (off-screen right)")]
|
[Tooltip("X position where obstacles spawn (off-screen right)")]
|
||||||
[SerializeField] private float obstacleSpawnXPosition = 12f;
|
[SerializeField] private float obstacleSpawnXPosition = 12f;
|
||||||
@@ -71,7 +72,7 @@ namespace Core.Settings
|
|||||||
public float MaxRotationAngle => maxRotationAngle;
|
public float MaxRotationAngle => maxRotationAngle;
|
||||||
public float RotationSpeed => rotationSpeed;
|
public float RotationSpeed => rotationSpeed;
|
||||||
public float ObstacleMoveSpeed => obstacleMoveSpeed;
|
public float ObstacleMoveSpeed => obstacleMoveSpeed;
|
||||||
public float ObstacleSpawnInterval => obstacleSpawnInterval;
|
public ObstacleSpawnConfig ObstacleSpawnConfiguration => obstacleSpawnConfiguration;
|
||||||
public float ObstacleSpawnXPosition => obstacleSpawnXPosition;
|
public float ObstacleSpawnXPosition => obstacleSpawnXPosition;
|
||||||
public float ObstacleDestroyXPosition => obstacleDestroyXPosition;
|
public float ObstacleDestroyXPosition => obstacleDestroyXPosition;
|
||||||
public float ObstacleMinSpawnY => obstacleMinSpawnY;
|
public float ObstacleMinSpawnY => obstacleMinSpawnY;
|
||||||
@@ -91,9 +92,14 @@ namespace Core.Settings
|
|||||||
maxFallSpeed = Mathf.Max(0f, maxFallSpeed);
|
maxFallSpeed = Mathf.Max(0f, maxFallSpeed);
|
||||||
maxRotationAngle = Mathf.Clamp(maxRotationAngle, 0f, 90f);
|
maxRotationAngle = Mathf.Clamp(maxRotationAngle, 0f, 90f);
|
||||||
rotationSpeed = Mathf.Max(0.1f, rotationSpeed);
|
rotationSpeed = Mathf.Max(0.1f, rotationSpeed);
|
||||||
obstacleSpawnInterval = Mathf.Max(0.1f, obstacleSpawnInterval);
|
|
||||||
targetMoveSpeed = Mathf.Max(0.1f, targetMoveSpeed);
|
targetMoveSpeed = Mathf.Max(0.1f, targetMoveSpeed);
|
||||||
targetSpawnInterval = Mathf.Max(0.1f, targetSpawnInterval);
|
targetSpawnInterval = Mathf.Max(0.1f, targetSpawnInterval);
|
||||||
|
|
||||||
|
// Validate obstacle spawn configuration
|
||||||
|
if (obstacleSpawnConfiguration != null)
|
||||||
|
{
|
||||||
|
obstacleSpawnConfiguration.Validate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
namespace Core.Settings
|
using Minigames.BirdPooper;
|
||||||
|
|
||||||
|
namespace Core.Settings
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Settings interface for Bird Pooper minigame.
|
/// Settings interface for Bird Pooper minigame.
|
||||||
/// Accessed via GameManager.GetSettingsObject<IBirdPooperSettings>()
|
/// Accessed via GameManager.GetSettingsObject of IBirdPooperSettings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IBirdPooperSettings
|
public interface IBirdPooperSettings
|
||||||
{
|
{
|
||||||
@@ -19,12 +21,14 @@
|
|||||||
|
|
||||||
// Obstacles
|
// Obstacles
|
||||||
float ObstacleMoveSpeed { get; }
|
float ObstacleMoveSpeed { get; }
|
||||||
float ObstacleSpawnInterval { get; }
|
|
||||||
float ObstacleSpawnXPosition { get; }
|
float ObstacleSpawnXPosition { get; }
|
||||||
float ObstacleDestroyXPosition { get; }
|
float ObstacleDestroyXPosition { get; }
|
||||||
float ObstacleMinSpawnY { get; }
|
float ObstacleMinSpawnY { get; }
|
||||||
float ObstacleMaxSpawnY { get; }
|
float ObstacleMaxSpawnY { get; }
|
||||||
|
|
||||||
|
// Obstacle Spawning Configuration
|
||||||
|
ObstacleSpawnConfig ObstacleSpawnConfiguration { get; }
|
||||||
|
|
||||||
// Poop Projectile
|
// Poop Projectile
|
||||||
float PoopFallSpeed { get; }
|
float PoopFallSpeed { get; }
|
||||||
float PoopDestroyYPosition { get; }
|
float PoopDestroyYPosition { get; }
|
||||||
|
|||||||
@@ -332,8 +332,11 @@ namespace Core.Settings
|
|||||||
float IntroDuration { get; }
|
float IntroDuration { get; }
|
||||||
float PersonIntroDuration { get; }
|
float PersonIntroDuration { get; }
|
||||||
float EvaluationDuration { get; }
|
float EvaluationDuration { get; }
|
||||||
|
float TargetFlybyLingerDuration { get; }
|
||||||
|
float TargetFlybyCameraBlendTime { get; }
|
||||||
|
|
||||||
// Spawn System
|
// Spawn System
|
||||||
|
float PreSpawnBeyondTargetDistance { get; }
|
||||||
float TargetMinDistance { get; }
|
float TargetMinDistance { get; }
|
||||||
float TargetMaxDistance { get; }
|
float TargetMaxDistance { get; }
|
||||||
float ObjectSpawnMinDistance { get; } // Min distance between spawned objects
|
float ObjectSpawnMinDistance { get; } // Min distance between spawned objects
|
||||||
|
|||||||
@@ -114,6 +114,30 @@ namespace Minigames.Airplane.Core
|
|||||||
if (showDebugLogs) Logging.Debug("[AirplaneCameraManager] Stopped following airplane");
|
if (showDebugLogs) Logging.Debug("[AirplaneCameraManager] Stopped following airplane");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the target flyby camera to track the target's position
|
||||||
|
/// </summary>
|
||||||
|
public void SetTargetFlybyTracking(Transform targetTransform)
|
||||||
|
{
|
||||||
|
var flybyCamera = GetCamera(AirplaneCameraState.TargetFlyby);
|
||||||
|
if (flybyCamera == null)
|
||||||
|
{
|
||||||
|
Logging.Warning("[AirplaneCameraManager] Cannot set flyby tracking - TargetFlyby camera not assigned!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTransform == null)
|
||||||
|
{
|
||||||
|
Logging.Warning("[AirplaneCameraManager] Cannot set flyby tracking - target transform is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the tracking target on the flyby camera
|
||||||
|
flybyCamera.Target.TrackingTarget = targetTransform;
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug($"[AirplaneCameraManager] TargetFlyby camera now tracking: {targetTransform.gameObject.name}");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
#region State
|
#region State
|
||||||
|
|
||||||
private AirplaneGameState _currentState = AirplaneGameState.AirplaneSelection;
|
private AirplaneGameState _currentState = AirplaneGameState.Intro;
|
||||||
private Person _currentPerson;
|
private Person _currentPerson;
|
||||||
private Person _previousPerson;
|
private Person _previousPerson;
|
||||||
private AirplaneController _currentAirplane;
|
private AirplaneController _currentAirplane;
|
||||||
@@ -74,7 +74,7 @@ namespace Minigames.Airplane.Core
|
|||||||
private int _successCount;
|
private int _successCount;
|
||||||
private int _failCount;
|
private int _failCount;
|
||||||
private int _totalTurns;
|
private int _totalTurns;
|
||||||
private AirplaneAbilityType _selectedAirplaneType;
|
private IAirplaneSettings _cachedSettings;
|
||||||
|
|
||||||
public AirplaneGameState CurrentState => _currentState;
|
public AirplaneGameState CurrentState => _currentState;
|
||||||
public Person CurrentPerson => _currentPerson;
|
public Person CurrentPerson => _currentPerson;
|
||||||
@@ -98,6 +98,13 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
_instance = this;
|
_instance = this;
|
||||||
|
|
||||||
|
// Cache settings for performance
|
||||||
|
_cachedSettings = GameManager.GetSettingsObject<IAirplaneSettings>();
|
||||||
|
if (_cachedSettings == null)
|
||||||
|
{
|
||||||
|
Logging.Error("[AirplaneGameManager] Failed to load IAirplaneSettings!");
|
||||||
|
}
|
||||||
|
|
||||||
// Validate references
|
// Validate references
|
||||||
ValidateReferences();
|
ValidateReferences();
|
||||||
}
|
}
|
||||||
@@ -216,74 +223,43 @@ namespace Minigames.Airplane.Core
|
|||||||
{
|
{
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ===== GAME STARTING =====");
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] ===== GAME STARTING =====");
|
||||||
|
|
||||||
// Start with intro camera blend, THEN show selection UI
|
|
||||||
StartCoroutine(IntroSequence());
|
StartCoroutine(IntroSequence());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Turn Type Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Airplane selection sequence: show selection UI, wait for player choice
|
/// Check if this is the first turn of the game
|
||||||
/// Called AFTER intro camera blend
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator AirplaneSelectionSequence()
|
private bool IsFirstTurn()
|
||||||
{
|
{
|
||||||
ChangeState(AirplaneGameState.AirplaneSelection);
|
return _totalTurns == 0;
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === AIRPLANE SELECTION STARTING ===");
|
|
||||||
|
|
||||||
// Show selection UI
|
|
||||||
if (selectionUI != null)
|
|
||||||
{
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[AirplaneGameManager] SelectionUI found! GameObject: {selectionUI.gameObject.name}, Active: {selectionUI.gameObject.activeSelf}");
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionUI.Show();
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[AirplaneGameManager] Called selectionUI.Show(). GameObject now active: {selectionUI.gameObject.activeSelf}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for player to select and confirm
|
|
||||||
yield return new WaitUntil(() => selectionUI.HasSelectedType);
|
|
||||||
|
|
||||||
_selectedAirplaneType = selectionUI.GetSelectedType();
|
|
||||||
selectionUI.Hide();
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[AirplaneGameManager] Selected airplane: {_selectedAirplaneType}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logging.Warning("[AirplaneGameManager] ⚠️ selectionUI is NULL! Cannot show selection UI. Check Inspector.");
|
|
||||||
Logging.Warning("[AirplaneGameManager] Using default airplane type from settings as fallback.");
|
|
||||||
|
|
||||||
// Fallback: use default type from settings
|
|
||||||
var settings = GameManager.GetSettingsObject<IAirplaneSettings>();
|
|
||||||
if (settings != null)
|
|
||||||
{
|
|
||||||
_selectedAirplaneType = settings.DefaultAirplaneType;
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[AirplaneGameManager] No selection UI, using default: {_selectedAirplaneType}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_selectedAirplaneType = AirplaneAbilityType.Jet; // Ultimate fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with hellos after selection
|
|
||||||
yield return StartCoroutine(IntroHellosSequence());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Intro sequence: blend to intro camera, THEN show airplane selection
|
/// Check if this is a new person (different from previous)
|
||||||
|
/// </summary>
|
||||||
|
private bool IsNewPerson()
|
||||||
|
{
|
||||||
|
return _previousPerson != _currentPerson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this is a retry turn (same person as previous)
|
||||||
|
/// </summary>
|
||||||
|
private bool IsRetryTurn()
|
||||||
|
{
|
||||||
|
return !IsNewPerson() && _previousPerson != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flow: Intro Sequence
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initial intro sequence: blend to intro camera, introduce all people (stub), then start new person flow
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator IntroSequence()
|
private IEnumerator IntroSequence()
|
||||||
{
|
{
|
||||||
@@ -291,116 +267,50 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Playing intro sequence...");
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Playing intro sequence...");
|
||||||
|
|
||||||
// 1. Blend to intro camera
|
// 1. Blend to intro camera and wait for blend to complete
|
||||||
if (cameraManager != null)
|
if (cameraManager != null)
|
||||||
{
|
{
|
||||||
cameraManager.SwitchToState(AirplaneCameraState.Intro);
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Intro));
|
||||||
yield return new WaitForSeconds(0.5f); // Camera blend time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro camera ready. Now showing airplane selection...");
|
// 2. Introduce all people (stub for now)
|
||||||
|
yield return StartCoroutine(IntroduceAllPeople());
|
||||||
|
|
||||||
// 2. Show airplane selection UI and wait for player choice
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro complete, moving to first person...");
|
||||||
yield return StartCoroutine(AirplaneSelectionSequence());
|
|
||||||
|
// 3. Start first person flow
|
||||||
|
yield return StartCoroutine(ExecuteNewPersonFlow());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hellos sequence: all people greet, then blend to aiming camera
|
/// Stub: All people say hello animation. Empty for now.
|
||||||
/// Called AFTER airplane selection is complete
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator IntroHellosSequence()
|
private IEnumerator IntroduceAllPeople()
|
||||||
{
|
{
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Starting hellos sequence...");
|
// TODO: All people say hello animation
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] (Stub) All people introduction");
|
||||||
// 1. Iterate over each person and allow them to say their hellos
|
yield return null;
|
||||||
if (personQueue != null && personQueue.HasMorePeople())
|
|
||||||
{
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Introducing all people...");
|
|
||||||
|
|
||||||
// Get all people from queue without removing them
|
|
||||||
var allPeople = personQueue.GetAllPeople();
|
|
||||||
foreach (var person in allPeople)
|
|
||||||
{
|
|
||||||
if (person != null)
|
|
||||||
{
|
|
||||||
// Wait for each person's greeting to complete
|
|
||||||
yield return StartCoroutine(person.OnHello());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] All introductions complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Blend to aiming camera (first person's turn will start)
|
|
||||||
if (cameraManager != null)
|
|
||||||
{
|
|
||||||
cameraManager.SwitchToState(AirplaneCameraState.Aiming);
|
|
||||||
yield return new WaitForSeconds(0.5f); // Camera blend time
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Intro complete");
|
|
||||||
|
|
||||||
// Move to first person's turn
|
|
||||||
StartCoroutine(SetupNextPerson());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flow: New Person Flow
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setup the next person's turn
|
/// New Person Flow: Executed for first person or after successful hit.
|
||||||
|
/// Steps: Pop person → Introduce → Prepare level → Flyby → Select airplane → Aim → Shoot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator SetupNextPerson()
|
private IEnumerator ExecuteNewPersonFlow()
|
||||||
{
|
{
|
||||||
// Check if there are more people
|
// Check if there are more people
|
||||||
if (personQueue == null || !personQueue.HasMorePeople())
|
if (personQueue == null || !personQueue.HasMorePeople())
|
||||||
{
|
{
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] No more people, ending game");
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] No more people, ending game");
|
||||||
StartCoroutine(GameOver());
|
yield return StartCoroutine(GameOver());
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeState(AirplaneGameState.NextPerson);
|
// Pop next person from queue
|
||||||
|
|
||||||
// If this is NOT the first turn, handle post-shot reaction
|
|
||||||
if (_currentPerson != null)
|
|
||||||
{
|
|
||||||
// Switch to next person camera for reaction/transition
|
|
||||||
if (cameraManager != null)
|
|
||||||
{
|
|
||||||
cameraManager.SwitchToState(AirplaneCameraState.NextPerson);
|
|
||||||
|
|
||||||
// Wait for camera blend to complete before cleanup and reaction
|
|
||||||
yield return new WaitForSeconds(0.5f); // Camera blend time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOW cleanup spawned objects after camera has blended (camera shows scene before cleanup)
|
|
||||||
if (spawnManager != null)
|
|
||||||
{
|
|
||||||
if (_lastShotHit)
|
|
||||||
{
|
|
||||||
// Success: Full cleanup - destroy all spawned objects and target
|
|
||||||
spawnManager.CleanupSpawnedObjects();
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug("[AirplaneGameManager] Cleaned up spawned objects after successful shot");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Failure: Keep spawned objects for retry, just reset tracking state
|
|
||||||
spawnManager.ResetForRetry();
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug("[AirplaneGameManager] Kept spawned objects for retry after miss");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the previous person's reaction (celebrate/disappointment), removal (if hit), and shuffle
|
|
||||||
yield return StartCoroutine(personQueue.HandlePostShotReaction(_lastShotHit));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the next person (now at front of queue after potential removal)
|
|
||||||
_previousPerson = _currentPerson;
|
_previousPerson = _currentPerson;
|
||||||
_currentPerson = personQueue.PopNextPerson();
|
_currentPerson = personQueue.PopNextPerson();
|
||||||
_totalTurns++;
|
_totalTurns++;
|
||||||
@@ -411,76 +321,308 @@ namespace Minigames.Airplane.Core
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a NEW person (different from previous) or a retry (same person)
|
if (showDebugLogs)
|
||||||
bool isNewPerson = _previousPerson != _currentPerson;
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
{
|
||||||
string turnType = isNewPerson ? "NEW PERSON" : "RETRY";
|
Logging.Debug($"[AirplaneGameManager] === NEW PERSON FLOW: Turn {_totalTurns}: {_currentPerson.PersonName} ===" +
|
||||||
Logging.Debug($"[AirplaneGameManager] === Turn {_totalTurns}: {_currentPerson.PersonName} ({turnType}) ===" +
|
|
||||||
$"\n Target: {_currentPerson.TargetName}");
|
$"\n Target: {_currentPerson.TargetName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
OnPersonStartTurn?.Invoke(_currentPerson);
|
OnPersonStartTurn?.Invoke(_currentPerson);
|
||||||
|
|
||||||
// Only introduce if this is a NEW person
|
// 1. Introduce this person
|
||||||
if (isNewPerson && _previousPerson != null)
|
yield return StartCoroutine(IntroducePerson());
|
||||||
{
|
|
||||||
// Switching to a new person (after success) - they say hello
|
|
||||||
yield return StartCoroutine(personQueue.IntroduceNextPerson());
|
|
||||||
}
|
|
||||||
else if (_previousPerson == null)
|
|
||||||
{
|
|
||||||
// First turn - they already said hello during intro, just brief camera pause
|
|
||||||
if (cameraManager != null)
|
|
||||||
{
|
|
||||||
cameraManager.SwitchToState(AirplaneCameraState.NextPerson);
|
|
||||||
yield return new WaitForSeconds(0.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else: Same person retry (after failure) - skip introduction, go straight to aiming
|
|
||||||
|
|
||||||
// Initialize spawn manager for this person's target
|
// 2. Prepare level (spawn objects, target)
|
||||||
|
yield return StartCoroutine(PrepareLevel());
|
||||||
|
|
||||||
|
// 3. Execute target flyby
|
||||||
|
yield return StartCoroutine(ExecuteTargetFlyby());
|
||||||
|
|
||||||
|
// 4. Select airplane
|
||||||
|
AirplaneAbilityType selectedType = AirplaneAbilityType.None;
|
||||||
|
yield return StartCoroutine(SelectAirplane((type) => selectedType = type));
|
||||||
|
|
||||||
|
// 5. Enter aiming state with selected airplane
|
||||||
|
EnterAimingState(selectedType);
|
||||||
|
|
||||||
|
// Flow continues to shot → evaluation → routing
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flow: Repeat Shot Flow
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repeat Shot Flow: Executed when same person retries after missing.
|
||||||
|
/// Steps: Brief camera → React (disappointed) → Select airplane → Aim → Shoot
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator ExecuteRepeatShotFlow()
|
||||||
|
{
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[AirplaneGameManager] === REPEAT SHOT FLOW: Turn {_totalTurns}: {_currentPerson.PersonName} (RETRY) ===" +
|
||||||
|
$"\n Target: {_currentPerson.TargetName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_totalTurns++;
|
||||||
|
OnPersonStartTurn?.Invoke(_currentPerson);
|
||||||
|
|
||||||
|
// 1. Switch to next person camera briefly and wait for blend
|
||||||
|
if (cameraManager != null)
|
||||||
|
{
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Play person reaction (disappointment)
|
||||||
|
yield return StartCoroutine(PlayPersonReaction(false));
|
||||||
|
|
||||||
|
// 3. Reset spawn manager for retry (keeps spawned objects)
|
||||||
if (spawnManager != null)
|
if (spawnManager != null)
|
||||||
{
|
{
|
||||||
// Pass retry flag: true if same person, false if new person
|
spawnManager.ResetForRetry();
|
||||||
bool isRetry = !isNewPerson;
|
|
||||||
spawnManager.InitializeForGame(_currentPerson.TargetName, isRetry);
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneGameManager] Kept spawned objects for retry");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue done - continue game flow
|
// 4. Select airplane (new choice for this shot)
|
||||||
|
AirplaneAbilityType selectedType = AirplaneAbilityType.None;
|
||||||
|
yield return StartCoroutine(SelectAirplane((type) => selectedType = type));
|
||||||
|
|
||||||
|
// 5. Enter aiming state with selected airplane
|
||||||
|
EnterAimingState(selectedType);
|
||||||
|
|
||||||
|
// Flow continues to shot → evaluation → routing
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flow: Person Shuffle Flow
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Person Shuffle Flow: Executed after successful hit.
|
||||||
|
/// Steps: Camera → React (celebrate) → Advance queue → Cleanup → Next person or game over
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator ExecutePersonShuffleFlow()
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === PERSON SHUFFLE FLOW ===");
|
||||||
|
|
||||||
|
// 1. Switch to next person camera and wait for blend
|
||||||
|
if (cameraManager != null)
|
||||||
|
{
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Play person reaction (celebration)
|
||||||
|
yield return StartCoroutine(PlayPersonReaction(true));
|
||||||
|
|
||||||
|
// 3. Advance queue (remove person, shuffle)
|
||||||
|
if (personQueue != null)
|
||||||
|
{
|
||||||
|
yield return StartCoroutine(personQueue.AdvanceQueue(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cleanup level (destroy all spawned objects)
|
||||||
|
if (spawnManager != null)
|
||||||
|
{
|
||||||
|
spawnManager.CleanupLevel(true);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneGameManager] Cleaned up spawned objects after success");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check if more people, then continue to new person flow or game over
|
||||||
|
if (personQueue != null && personQueue.HasMorePeople())
|
||||||
|
{
|
||||||
|
yield return StartCoroutine(ExecuteNewPersonFlow());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return StartCoroutine(GameOver());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Phase Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase: Introduce current person
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator IntroducePerson()
|
||||||
|
{
|
||||||
|
ChangeState(AirplaneGameState.NextPerson);
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Introducing {_currentPerson.PersonName}...");
|
||||||
|
|
||||||
|
// Blend to next person camera if not already there
|
||||||
|
// (Person shuffle flow already blends to NextPerson, so might already be there)
|
||||||
|
if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.NextPerson)
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blending to NextPerson camera...");
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.NextPerson));
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blend to NextPerson complete");
|
||||||
|
}
|
||||||
|
else if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneGameManager] Already on NextPerson camera, skipping blend");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Person says hello
|
||||||
|
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] Calling OnHello for {_currentPerson.PersonName}...");
|
||||||
|
yield return StartCoroutine(_currentPerson.OnHello());
|
||||||
|
if (showDebugLogs) Logging.Debug($"[AirplaneGameManager] OnHello complete for {_currentPerson.PersonName}");
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Introduction complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase: Prepare level (spawn ground, objects, target)
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator PrepareLevel()
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Preparing level...");
|
||||||
|
|
||||||
|
if (spawnManager == null)
|
||||||
|
{
|
||||||
|
Logging.Error("[AirplaneGameManager] Cannot prepare level - spawn manager not assigned!");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize spawn manager with new target
|
||||||
|
spawnManager.InitializeForGame(_currentPerson.TargetName, isRetry: false);
|
||||||
|
|
||||||
|
// Pre-spawn entire level
|
||||||
|
spawnManager.PreSpawnLevelToTarget();
|
||||||
|
|
||||||
|
// Set flyby camera tracking on spawned target
|
||||||
|
if (cameraManager != null)
|
||||||
|
{
|
||||||
|
Transform targetTransform = spawnManager.GetSpawnedTargetTransform();
|
||||||
|
if (targetTransform != null)
|
||||||
|
{
|
||||||
|
cameraManager.SetTargetFlybyTracking(targetTransform);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneGameManager] Target camera now tracking spawned target");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set expected target for validator
|
||||||
if (targetValidator != null)
|
if (targetValidator != null)
|
||||||
{
|
{
|
||||||
targetValidator.SetExpectedTarget(_currentPerson.TargetName);
|
targetValidator.SetExpectedTarget(_currentPerson.TargetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter aiming state
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Level preparation complete");
|
||||||
EnterAimingState();
|
|
||||||
|
yield return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enter aiming state - player can aim and launch
|
/// Phase: Select airplane type (shows UI, waits for player choice)
|
||||||
|
/// Blends to Aiming camera if not already there (needed for repeat shot flow)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void EnterAimingState()
|
private IEnumerator SelectAirplane(Action<AirplaneAbilityType> onSelected)
|
||||||
|
{
|
||||||
|
ChangeState(AirplaneGameState.AirplaneSelection);
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] === AIRPLANE SELECTION ===");
|
||||||
|
|
||||||
|
// Blend to aiming camera if not already there
|
||||||
|
// (New person flow: already on Aiming from flyby; Repeat shot flow: on NextPerson, needs blend)
|
||||||
|
if (cameraManager != null && cameraManager.CurrentState != AirplaneCameraState.Aiming)
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blending to Aiming camera...");
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Aiming));
|
||||||
|
}
|
||||||
|
else if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneGameManager] Already on Aiming camera, skipping blend");
|
||||||
|
}
|
||||||
|
|
||||||
|
AirplaneAbilityType selectedType = AirplaneAbilityType.None;
|
||||||
|
|
||||||
|
// Show selection UI
|
||||||
|
if (selectionUI != null)
|
||||||
|
{
|
||||||
|
selectionUI.Show();
|
||||||
|
|
||||||
|
// Wait for player to select and confirm
|
||||||
|
yield return new WaitUntil(() => selectionUI.HasSelectedType);
|
||||||
|
|
||||||
|
selectedType = selectionUI.GetSelectedType();
|
||||||
|
selectionUI.Hide();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[AirplaneGameManager] Selected airplane: {selectedType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logging.Warning("[AirplaneGameManager] SelectionUI not assigned! Using default airplane type.");
|
||||||
|
|
||||||
|
// Fallback: use default type from settings
|
||||||
|
if (_cachedSettings != null)
|
||||||
|
{
|
||||||
|
selectedType = _cachedSettings.DefaultAirplaneType;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectedType = AirplaneAbilityType.Jet; // Ultimate fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[AirplaneGameManager] No selection UI, using default: {selectedType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelected?.Invoke(selectedType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase: Play person reaction animation
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator PlayPersonReaction(bool success)
|
||||||
|
{
|
||||||
|
if (personQueue == null || _currentPerson == null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[AirplaneGameManager] Playing {_currentPerson.PersonName} reaction (success={success})");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return StartCoroutine(personQueue.PlayPersonReaction(success));
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Enter aiming state - player can aim and launch with selected airplane type
|
||||||
|
/// Camera should already be on Aiming state from SelectAirplane phase
|
||||||
|
/// </summary>
|
||||||
|
private void EnterAimingState(AirplaneAbilityType selectedType)
|
||||||
{
|
{
|
||||||
ChangeState(AirplaneGameState.Aiming);
|
ChangeState(AirplaneGameState.Aiming);
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Ready to aim and launch!");
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Ready to aim and launch!");
|
||||||
|
|
||||||
// Switch to aiming camera
|
|
||||||
if (cameraManager != null)
|
|
||||||
{
|
|
||||||
cameraManager.SwitchToState(AirplaneCameraState.Aiming);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn airplane at slingshot with selected type
|
// Spawn airplane at slingshot with selected type
|
||||||
if (launchController != null && _selectedAirplaneType != AirplaneAbilityType.None)
|
if (launchController != null && selectedType != AirplaneAbilityType.None)
|
||||||
{
|
{
|
||||||
launchController.SetAirplaneType(_selectedAirplaneType);
|
launchController.SetAirplaneType(selectedType);
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (showDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[AirplaneGameManager] Spawned airplane at slingshot: {_selectedAirplaneType}");
|
Logging.Debug($"[AirplaneGameManager] Spawned airplane at slingshot: {selectedType}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +639,45 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flyby Sequence
|
||||||
|
/// <summary>
|
||||||
|
/// Execute cinematic flyby of the target location.
|
||||||
|
/// Switches to TargetFlyby camera, waits for blend, lingers, then returns to Aiming camera.
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerator ExecuteTargetFlyby()
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Starting target flyby sequence...");
|
||||||
|
|
||||||
|
if (cameraManager == null)
|
||||||
|
{
|
||||||
|
Logging.Warning("[AirplaneGameManager] Cannot execute flyby - camera manager not assigned!");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cachedSettings == null)
|
||||||
|
{
|
||||||
|
Logging.Warning("[AirplaneGameManager] Cannot execute flyby - settings not available!");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Blend to TargetFlyby camera and wait for blend to complete
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.TargetFlyby));
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Blend to target camera complete, lingering...");
|
||||||
|
|
||||||
|
// 2. Linger on target
|
||||||
|
yield return new WaitForSeconds(_cachedSettings.TargetFlybyLingerDuration);
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Linger complete, blending back to aiming...");
|
||||||
|
|
||||||
|
// 3. Blend back to Aiming camera and wait for blend to complete
|
||||||
|
yield return StartCoroutine(cameraManager.BlendToStateAsync(AirplaneCameraState.Aiming));
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[AirplaneGameManager] Target flyby sequence complete!");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Event Handlers
|
#region Event Handlers
|
||||||
@@ -700,11 +881,17 @@ namespace Minigames.Airplane.Core
|
|||||||
launchController.ClearActiveAirplane();
|
launchController.ClearActiveAirplane();
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Spawned objects cleanup moved to SetupNextPerson() to happen AFTER camera blend
|
// Route to appropriate flow based on result
|
||||||
// This ensures camera shows the scene before cleanup and person reaction
|
if (success)
|
||||||
|
{
|
||||||
// Move to next person
|
// Success: Go to person shuffle flow
|
||||||
StartCoroutine(SetupNextPerson());
|
StartCoroutine(ExecutePersonShuffleFlow());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Failure: Go to repeat shot flow
|
||||||
|
StartCoroutine(ExecuteRepeatShotFlow());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace Minigames.Airplane.Core
|
|||||||
[Header("Spawn Threshold")]
|
[Header("Spawn Threshold")]
|
||||||
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback from settings.")]
|
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback from settings.")]
|
||||||
[SerializeField] private Transform dynamicSpawnThresholdMarker;
|
[SerializeField] private Transform dynamicSpawnThresholdMarker;
|
||||||
|
|
||||||
[Header("Spawn Parents")]
|
[Header("Spawn Parents")]
|
||||||
[Tooltip("Parent transform for spawned objects (optional, for organization)")]
|
[Tooltip("Parent transform for spawned objects (optional, for organization)")]
|
||||||
[SerializeField] private Transform spawnedObjectsParent;
|
[SerializeField] private Transform spawnedObjectsParent;
|
||||||
@@ -101,9 +101,9 @@ namespace Minigames.Airplane.Core
|
|||||||
// Plane tracking
|
// Plane tracking
|
||||||
private Transform _planeTransform;
|
private Transform _planeTransform;
|
||||||
private bool _isSpawningActive;
|
private bool _isSpawningActive;
|
||||||
private bool _hasPassedThreshold;
|
|
||||||
|
|
||||||
// Spawning positions (distance-based)
|
// Spawning positions (distance-based)
|
||||||
|
private float _lastSpawnedX; // Tracks the furthest forward X position that has been pre-spawned
|
||||||
private float _nextObjectSpawnX;
|
private float _nextObjectSpawnX;
|
||||||
private float _nextGroundSpawnX;
|
private float _nextGroundSpawnX;
|
||||||
|
|
||||||
@@ -139,7 +139,6 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_isSpawningActive || _planeTransform == null) return;
|
if (!_isSpawningActive || _planeTransform == null) return;
|
||||||
|
|
||||||
float planeX = _planeTransform.position.x;
|
float planeX = _planeTransform.position.x;
|
||||||
@@ -150,58 +149,26 @@ namespace Minigames.Airplane.Core
|
|||||||
_furthestReachedX = planeX;
|
_furthestReachedX = planeX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if target should be spawned (when plane gets within spawn distance)
|
// Check if plane has reached the point where we need to continue spawning beyond pre-spawned content
|
||||||
if (!_hasSpawnedTarget && _targetPrefabToSpawn != null)
|
// Only spawn new content if plane is beyond previous furthest point (for retries)
|
||||||
|
bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead);
|
||||||
|
|
||||||
|
if (shouldSpawnNewContent)
|
||||||
{
|
{
|
||||||
float distanceToTarget = _targetSpawnPosition.x - planeX;
|
// Continue spawning objects when plane approaches the last spawned position
|
||||||
if (distanceToTarget <= _settings.SpawnDistanceAhead)
|
float spawnTriggerX = _lastSpawnedX + _settings.SpawnDistanceAhead;
|
||||||
|
if (planeX >= spawnTriggerX && planeX >= _nextObjectSpawnX)
|
||||||
{
|
{
|
||||||
SpawnTarget();
|
SpawnRandomObject();
|
||||||
_hasSpawnedTarget = true;
|
ScheduleNextObjectSpawn(planeX);
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[SpawnManager] Target spawned at distance {distanceToTarget:F2} from plane");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if plane has crossed threshold
|
|
||||||
float threshold = dynamicSpawnThresholdMarker.position.x;
|
|
||||||
|
|
||||||
if (!_hasPassedThreshold && planeX >= threshold)
|
|
||||||
{
|
|
||||||
_hasPassedThreshold = true;
|
|
||||||
InitializeDynamicSpawning();
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
// Continue spawning ground tiles ahead of plane
|
||||||
|
float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance();
|
||||||
|
while (_nextGroundSpawnX < groundSpawnTargetX)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[SpawnManager] Plane crossed threshold at X={planeX:F2}");
|
SpawnGroundTile();
|
||||||
}
|
_nextGroundSpawnX += _settings.GroundSpawnInterval;
|
||||||
}
|
|
||||||
|
|
||||||
// If past threshold, handle spawning (only if we're going further than before)
|
|
||||||
if (_hasPassedThreshold)
|
|
||||||
{
|
|
||||||
// Only spawn new content if plane is beyond previous furthest point (for retries)
|
|
||||||
bool shouldSpawnNewContent = !_isRetryAttempt || planeX > (_furthestReachedX - _settings.SpawnDistanceAhead);
|
|
||||||
|
|
||||||
if (shouldSpawnNewContent)
|
|
||||||
{
|
|
||||||
// Spawn objects when plane reaches spawn position
|
|
||||||
if (planeX >= _nextObjectSpawnX)
|
|
||||||
{
|
|
||||||
SpawnRandomObject();
|
|
||||||
ScheduleNextObjectSpawn(planeX);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn ground tiles ahead of plane
|
|
||||||
float groundSpawnTargetX = planeX + GetGroundSpawnAheadDistance();
|
|
||||||
while (_nextGroundSpawnX < groundSpawnTargetX)
|
|
||||||
{
|
|
||||||
SpawnGroundTile();
|
|
||||||
_nextGroundSpawnX += _settings.GroundSpawnInterval;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +188,6 @@ namespace Minigames.Airplane.Core
|
|||||||
{
|
{
|
||||||
_currentTargetKey = targetKey;
|
_currentTargetKey = targetKey;
|
||||||
_isSpawningActive = false;
|
_isSpawningActive = false;
|
||||||
_hasPassedThreshold = false;
|
|
||||||
_isRetryAttempt = isRetry;
|
_isRetryAttempt = isRetry;
|
||||||
|
|
||||||
// Only reset target and spawn state if NOT a retry
|
// Only reset target and spawn state if NOT a retry
|
||||||
@@ -266,6 +232,94 @@ namespace Minigames.Airplane.Core
|
|||||||
SetupTargetUI();
|
SetupTargetUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-spawn the entire level from threshold marker to target + buffer.
|
||||||
|
/// Spawns range: (threshold_x, target_x + preSpawnBeyondTargetDistance)
|
||||||
|
/// Call this after InitializeForGame() for new turns (not retries).
|
||||||
|
/// </summary>
|
||||||
|
public void PreSpawnLevelToTarget()
|
||||||
|
{
|
||||||
|
if (_targetPrefabToSpawn == null)
|
||||||
|
{
|
||||||
|
Logging.Error("[SpawnManager] Cannot pre-spawn - target prefab not initialized! Call InitializeForGame first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hasSpawnedTarget)
|
||||||
|
{
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[SpawnManager] Target already spawned, skipping pre-spawn (retry scenario)");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicSpawnThresholdMarker == null)
|
||||||
|
{
|
||||||
|
Logging.Error("[SpawnManager] Cannot pre-spawn - dynamicSpawnThresholdMarker not assigned!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get pre-spawn range: from threshold to target + buffer
|
||||||
|
float preSpawnStartX = dynamicSpawnThresholdMarker.position.x;
|
||||||
|
float preSpawnEndX = _targetDistance + _settings.PreSpawnBeyondTargetDistance;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Pre-spawning level from X={preSpawnStartX:F2} (threshold) to X={preSpawnEndX:F2} (target={_targetDistance:F2} + buffer={_settings.PreSpawnBeyondTargetDistance:F2})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Spawn ground tiles FIRST across entire range (so target can raycast)
|
||||||
|
float currentGroundX = preSpawnStartX;
|
||||||
|
while (currentGroundX <= preSpawnEndX)
|
||||||
|
{
|
||||||
|
SpawnGroundTileAt(currentGroundX);
|
||||||
|
currentGroundX += _settings.GroundSpawnInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set next ground spawn position beyond pre-spawn range
|
||||||
|
_nextGroundSpawnX = preSpawnEndX + _settings.GroundSpawnInterval;
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Ground tiles spawned, now spawning objects");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Spawn objects across entire range (skipping near target)
|
||||||
|
float currentObjectX = preSpawnStartX + Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance);
|
||||||
|
|
||||||
|
while (currentObjectX <= preSpawnEndX)
|
||||||
|
{
|
||||||
|
// Spawn object at this position
|
||||||
|
SpawnRandomObjectAt(currentObjectX);
|
||||||
|
|
||||||
|
// Move to next spawn position (forward)
|
||||||
|
float spawnDistance = Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance);
|
||||||
|
currentObjectX += spawnDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Objects spawned, now spawning target with raycast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Spawn the target LAST (after ground exists for proper raycasting)
|
||||||
|
SpawnTarget();
|
||||||
|
_hasSpawnedTarget = true;
|
||||||
|
|
||||||
|
// 4. Store the furthest forward pre-spawn position as _lastSpawnedX
|
||||||
|
_lastSpawnedX = preSpawnEndX;
|
||||||
|
|
||||||
|
// 5. Schedule next object spawn beyond the pre-spawn range
|
||||||
|
_nextObjectSpawnX = preSpawnEndX + Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance);
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Pre-spawn complete! Last spawned X={_lastSpawnedX:F2}, next object at X={_nextObjectSpawnX:F2}");
|
||||||
|
Logging.Debug($"[SpawnManager] Spawned {_positiveSpawnCount} positive and {_negativeSpawnCount} negative objects");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start tracking the airplane and enable spawning.
|
/// Start tracking the airplane and enable spawning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -380,7 +434,6 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
// Reset all spawn state
|
// Reset all spawn state
|
||||||
_hasSpawnedTarget = false;
|
_hasSpawnedTarget = false;
|
||||||
_hasPassedThreshold = false;
|
|
||||||
_furthestReachedX = 0f;
|
_furthestReachedX = 0f;
|
||||||
_positiveSpawnCount = 0;
|
_positiveSpawnCount = 0;
|
||||||
_negativeSpawnCount = 0;
|
_negativeSpawnCount = 0;
|
||||||
@@ -391,6 +444,33 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up level based on shot result.
|
||||||
|
/// Success: Full cleanup (destroys all spawned objects).
|
||||||
|
/// Failure: Reset for retry (keeps spawned objects, resets tracking).
|
||||||
|
/// </summary>
|
||||||
|
public void CleanupLevel(bool success)
|
||||||
|
{
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
CleanupSpawnedObjects();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[SpawnManager] Level cleanup: SUCCESS - destroyed all objects");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResetForRetry();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[SpawnManager] Level cleanup: FAILURE - kept objects for retry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset tracking state for retry attempt (keeps spawned objects).
|
/// Reset tracking state for retry attempt (keeps spawned objects).
|
||||||
/// Call this when player fails and will retry the same shot.
|
/// Call this when player fails and will retry the same shot.
|
||||||
@@ -414,6 +494,15 @@ namespace Minigames.Airplane.Core
|
|||||||
return (_targetSpawnPosition, _targetDistance, _targetIconSprite);
|
return (_targetSpawnPosition, _targetDistance, _targetIconSprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the spawned target's transform for camera tracking.
|
||||||
|
/// Returns null if target hasn't been spawned yet.
|
||||||
|
/// </summary>
|
||||||
|
public Transform GetSpawnedTargetTransform()
|
||||||
|
{
|
||||||
|
return _spawnedTarget != null ? _spawnedTarget.transform : null;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Initialization
|
#region Initialization
|
||||||
@@ -494,6 +583,11 @@ namespace Minigames.Airplane.Core
|
|||||||
{
|
{
|
||||||
Logging.Warning("[SpawnManager] Launch controller not assigned! Distance calculation will use world origin.");
|
Logging.Warning("[SpawnManager] Launch controller not assigned! Distance calculation will use world origin.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dynamicSpawnThresholdMarker == null)
|
||||||
|
{
|
||||||
|
Logging.Warning("[SpawnManager] Dynamic spawn threshold marker not assigned! Pre-spawn will fail.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -501,7 +595,8 @@ namespace Minigames.Airplane.Core
|
|||||||
#region Target Spawning
|
#region Target Spawning
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawn the target at the predetermined position.
|
/// Spawn the target at the predetermined X position.
|
||||||
|
/// Y coordinate is determined via raycast from high position to find ground.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SpawnTarget()
|
private void SpawnTarget()
|
||||||
{
|
{
|
||||||
@@ -511,7 +606,13 @@ namespace Minigames.Airplane.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn target at initial position
|
// Determine Y position via raycast from high position (Y=20)
|
||||||
|
float targetY = DetermineTargetYPosition(_targetDistance);
|
||||||
|
|
||||||
|
// Update target spawn position with correct Y
|
||||||
|
_targetSpawnPosition = new Vector3(_targetDistance, targetY, 0f);
|
||||||
|
|
||||||
|
// Spawn target at determined position
|
||||||
_spawnedTarget = Instantiate(_currentTargetEntry.prefab, _targetSpawnPosition, Quaternion.identity);
|
_spawnedTarget = Instantiate(_currentTargetEntry.prefab, _targetSpawnPosition, Quaternion.identity);
|
||||||
|
|
||||||
if (spawnedObjectsParent != null)
|
if (spawnedObjectsParent != null)
|
||||||
@@ -519,14 +620,14 @@ namespace Minigames.Airplane.Core
|
|||||||
_spawnedTarget.transform.SetParent(spawnedObjectsParent);
|
_spawnedTarget.transform.SetParent(spawnedObjectsParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position target using configured spawn mode
|
// Position target using configured spawn mode (will refine Y if needed)
|
||||||
PositionObject(_spawnedTarget, _targetSpawnPosition.x,
|
PositionObject(_spawnedTarget, _targetDistance,
|
||||||
_currentTargetEntry.spawnPositionMode,
|
_currentTargetEntry.spawnPositionMode,
|
||||||
_currentTargetEntry.specifiedY,
|
_currentTargetEntry.specifiedY,
|
||||||
_currentTargetEntry.randomYMin,
|
_currentTargetEntry.randomYMin,
|
||||||
_currentTargetEntry.randomYMax);
|
_currentTargetEntry.randomYMax);
|
||||||
|
|
||||||
// Update target spawn position to actual positioned location
|
// Update target spawn position to actual final positioned location
|
||||||
_targetSpawnPosition = _spawnedTarget.transform.position;
|
_targetSpawnPosition = _spawnedTarget.transform.position;
|
||||||
|
|
||||||
// Extract sprite for UI icon
|
// Extract sprite for UI icon
|
||||||
@@ -538,6 +639,41 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine target Y position by raycasting downward from high position (Y=20).
|
||||||
|
/// Ensures target spawns touching the ground surface.
|
||||||
|
/// </summary>
|
||||||
|
private float DetermineTargetYPosition(float xPosition)
|
||||||
|
{
|
||||||
|
// Start raycast from high Y position (20 units up)
|
||||||
|
Vector2 rayOrigin = new Vector2(xPosition, 20f);
|
||||||
|
|
||||||
|
// Raycast downward to find ground
|
||||||
|
int layerMask = 1 << _settings.GroundLayer;
|
||||||
|
RaycastHit2D hit = Physics2D.Raycast(
|
||||||
|
rayOrigin,
|
||||||
|
Vector2.down,
|
||||||
|
_settings.MaxGroundRaycastDistance,
|
||||||
|
layerMask
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hit.collider != null)
|
||||||
|
{
|
||||||
|
// Found ground - return ground Y position
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Target Y determined via raycast: ground at {hit.point.y:F2}");
|
||||||
|
}
|
||||||
|
return hit.point.y;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No ground found - use default
|
||||||
|
Logging.Warning($"[SpawnManager] No ground found for target at X={xPosition:F2} (raycast from Y=20 for {_settings.MaxGroundRaycastDistance} units), using default Y={_settings.DefaultObjectYOffset}");
|
||||||
|
return _settings.DefaultObjectYOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract sprite from target prefab for UI display (without instantiation).
|
/// Extract sprite from target prefab for UI display (without instantiation).
|
||||||
/// Finds first SpriteRenderer in prefab or children.
|
/// Finds first SpriteRenderer in prefab or children.
|
||||||
@@ -613,24 +749,6 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
#region Dynamic Spawning
|
#region Dynamic Spawning
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize dynamic spawning when threshold is crossed.
|
|
||||||
/// </summary>
|
|
||||||
private void InitializeDynamicSpawning()
|
|
||||||
{
|
|
||||||
// Schedule first spawn trigger from current plane position
|
|
||||||
// Actual spawning will happen at look-ahead distance
|
|
||||||
if (_planeTransform != null)
|
|
||||||
{
|
|
||||||
ScheduleNextObjectSpawn(_planeTransform.position.x);
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[SpawnManager] Dynamic spawning initialized, first spawn trigger at planeX={_nextObjectSpawnX:F2}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the distance ahead to spawn ground (2x object spawn distance).
|
/// Get the distance ahead to spawn ground (2x object spawn distance).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -800,6 +918,105 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawn a ground tile at a specific X position (used for pre-spawn).
|
||||||
|
/// </summary>
|
||||||
|
private void SpawnGroundTileAt(float xPosition)
|
||||||
|
{
|
||||||
|
if (groundTilePrefabs == null || groundTilePrefabs.Length == 0) return;
|
||||||
|
|
||||||
|
// Pick random ground tile
|
||||||
|
GameObject tilePrefab = groundTilePrefabs[Random.Range(0, groundTilePrefabs.Length)];
|
||||||
|
|
||||||
|
// Calculate spawn position using configured Y
|
||||||
|
Vector3 spawnPosition = new Vector3(xPosition, groundSpawnY, 0f);
|
||||||
|
|
||||||
|
// Spawn tile
|
||||||
|
GameObject spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity);
|
||||||
|
|
||||||
|
if (groundTilesParent != null)
|
||||||
|
{
|
||||||
|
spawnedTile.transform.SetParent(groundTilesParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Pre-spawned ground tile at ({xPosition:F2}, {groundSpawnY:F2})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawn a random positive or negative object at a specific X position (used for pre-spawn).
|
||||||
|
/// </summary>
|
||||||
|
private void SpawnRandomObjectAt(float xPosition)
|
||||||
|
{
|
||||||
|
// Check if spawn position is too close to target (avoid obscuring it)
|
||||||
|
float distanceToTarget = Mathf.Abs(xPosition - _targetSpawnPosition.x);
|
||||||
|
float targetClearanceZone = 10f; // Don't spawn within 10 units of target
|
||||||
|
|
||||||
|
if (distanceToTarget < targetClearanceZone)
|
||||||
|
{
|
||||||
|
// Too close to target, skip this spawn
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Skipped pre-spawn at X={xPosition:F2} (too close to target at X={_targetSpawnPosition.x:F2})");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if spawning positive or negative based on weighted ratio
|
||||||
|
bool spawnPositive = ShouldSpawnPositive();
|
||||||
|
|
||||||
|
PrefabSpawnEntry entryToSpawn = null;
|
||||||
|
|
||||||
|
if (spawnPositive)
|
||||||
|
{
|
||||||
|
if (positiveObjectPrefabs != null && positiveObjectPrefabs.Length > 0)
|
||||||
|
{
|
||||||
|
entryToSpawn = positiveObjectPrefabs[Random.Range(0, positiveObjectPrefabs.Length)];
|
||||||
|
_positiveSpawnCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (negativeObjectPrefabs != null && negativeObjectPrefabs.Length > 0)
|
||||||
|
{
|
||||||
|
entryToSpawn = negativeObjectPrefabs[Random.Range(0, negativeObjectPrefabs.Length)];
|
||||||
|
_negativeSpawnCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryToSpawn == null || entryToSpawn.prefab == null) return;
|
||||||
|
|
||||||
|
// Spawn object at temporary position
|
||||||
|
Vector3 tempPosition = new Vector3(xPosition, 0f, 0f);
|
||||||
|
GameObject spawnedObject = Instantiate(entryToSpawn.prefab, tempPosition, Quaternion.identity);
|
||||||
|
|
||||||
|
if (spawnedObjectsParent != null)
|
||||||
|
{
|
||||||
|
spawnedObject.transform.SetParent(spawnedObjectsParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position object using entry's spawn configuration
|
||||||
|
PositionObject(spawnedObject, xPosition,
|
||||||
|
entryToSpawn.spawnPositionMode,
|
||||||
|
entryToSpawn.specifiedY,
|
||||||
|
entryToSpawn.randomYMin,
|
||||||
|
entryToSpawn.randomYMax);
|
||||||
|
|
||||||
|
// Initialize components that need post-spawn setup
|
||||||
|
var initializable = spawnedObject.GetComponent<Interactive.ISpawnInitializable>();
|
||||||
|
if (initializable != null)
|
||||||
|
{
|
||||||
|
initializable.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[SpawnManager] Pre-spawned {(spawnPositive ? "positive" : "negative")} object at {spawnedObject.transform.position}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Object Positioning
|
#region Object Positioning
|
||||||
|
|||||||
@@ -212,16 +212,15 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle post-shot reaction for current person (who just shot).
|
/// Play person reaction animation only (celebration or disappointment).
|
||||||
/// If successful: celebrate, remove from queue, shuffle remaining people.
|
/// Does not modify queue - use AdvanceQueue() separately for queue management.
|
||||||
/// If failed: show disappointment, stay in queue.
|
/// Awaitable - game flow waits for animation to complete.
|
||||||
/// Awaitable - game flow waits for reactions and animations to complete.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerator HandlePostShotReaction(bool targetHit)
|
public IEnumerator PlayPersonReaction(bool success)
|
||||||
{
|
{
|
||||||
if (peopleInQueue.Count == 0)
|
if (peopleInQueue.Count == 0)
|
||||||
{
|
{
|
||||||
Logging.Warning("[PersonQueue] HandlePostShotReaction called but queue is empty!");
|
Logging.Warning("[PersonQueue] PlayPersonReaction called but queue is empty!");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,57 +228,59 @@ namespace Minigames.Airplane.Core
|
|||||||
Person currentPerson = peopleInQueue[0];
|
Person currentPerson = peopleInQueue[0];
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (showDebugLogs)
|
||||||
Logging.Debug($"[PersonQueue] Post-shot reaction for {currentPerson.PersonName} (Hit: {targetHit})");
|
Logging.Debug($"[PersonQueue] Playing reaction for {currentPerson.PersonName} (success={success})");
|
||||||
|
|
||||||
// Call person's reaction based on result
|
// Call person's reaction based on result
|
||||||
if (targetHit)
|
if (success)
|
||||||
{
|
{
|
||||||
// Success reaction
|
// Success reaction
|
||||||
yield return StartCoroutine(currentPerson.OnTargetHit());
|
yield return StartCoroutine(currentPerson.OnTargetHit());
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[PersonQueue] Success! Removing person and shuffling queue...");
|
|
||||||
|
|
||||||
// Remember the first person's position BEFORE removing them
|
|
||||||
Vector3 firstPersonPosition = currentPerson.PersonTransform.position;
|
|
||||||
|
|
||||||
// Remove successful person from queue (they're no longer in peopleInQueue)
|
|
||||||
RemoveCurrentPerson();
|
|
||||||
|
|
||||||
// Shuffle remaining people forward to fill the first person's spot
|
|
||||||
yield return StartCoroutine(ShuffleToPosition(firstPersonPosition));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Failure reaction
|
// Failure reaction
|
||||||
yield return StartCoroutine(currentPerson.OnTargetMissed());
|
yield return StartCoroutine(currentPerson.OnTargetMissed());
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[PersonQueue] Failed - person stays in queue");
|
|
||||||
// On failure, don't remove or shuffle, person gets another turn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug("[PersonQueue] Post-shot reaction complete");
|
if (showDebugLogs) Logging.Debug("[PersonQueue] Reaction complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Introduce the next person (at front of queue) for their turn.
|
/// Advance the queue after a shot result.
|
||||||
/// Awaitable - game flow waits for introduction to complete.
|
/// If success: Remove current person and shuffle remaining people forward.
|
||||||
|
/// If failure: Do nothing (person stays in queue for retry).
|
||||||
|
/// Awaitable - game flow waits for removal and shuffle animations to complete.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerator IntroduceNextPerson()
|
public IEnumerator AdvanceQueue(bool success)
|
||||||
{
|
{
|
||||||
if (peopleInQueue.Count == 0)
|
if (peopleInQueue.Count == 0)
|
||||||
{
|
{
|
||||||
Logging.Warning("[PersonQueue] IntroduceNextPerson called but queue is empty!");
|
Logging.Warning("[PersonQueue] AdvanceQueue called but queue is empty!");
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Person nextPerson = peopleInQueue[0];
|
Person currentPerson = peopleInQueue[0];
|
||||||
|
|
||||||
if (showDebugLogs) Logging.Debug($"[PersonQueue] Introducing next person: {nextPerson.PersonName}");
|
if (success)
|
||||||
|
{
|
||||||
// Call person's hello sequence
|
if (showDebugLogs) Logging.Debug($"[PersonQueue] Advancing queue - removing {currentPerson.PersonName} and shuffling");
|
||||||
yield return StartCoroutine(nextPerson.OnHello());
|
|
||||||
|
// Remember the first person's position BEFORE removing them
|
||||||
if (showDebugLogs) Logging.Debug("[PersonQueue] Introduction complete");
|
Vector3 firstPersonPosition = currentPerson.PersonTransform.position;
|
||||||
|
|
||||||
|
// Remove successful person from queue
|
||||||
|
RemoveCurrentPerson();
|
||||||
|
|
||||||
|
// Shuffle remaining people forward to fill the first person's spot
|
||||||
|
yield return StartCoroutine(ShuffleToPosition(firstPersonPosition));
|
||||||
|
|
||||||
|
if (showDebugLogs) Logging.Debug("[PersonQueue] Queue advance complete");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (showDebugLogs) Logging.Debug($"[PersonQueue] Failed - {currentPerson.PersonName} stays in queue for retry");
|
||||||
|
// On failure, don't remove or shuffle, person gets another turn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ namespace Minigames.Airplane.Data
|
|||||||
Intro, // Intro sequence camera
|
Intro, // Intro sequence camera
|
||||||
NextPerson, // Camera focusing on the next person
|
NextPerson, // Camera focusing on the next person
|
||||||
Aiming, // Camera for aiming the airplane
|
Aiming, // Camera for aiming the airplane
|
||||||
Flight // Camera following the airplane in flight
|
Flight, // Camera following the airplane in flight
|
||||||
|
TargetFlyby // Camera showing the target location (cinematic flyby)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,15 @@ namespace Minigames.Airplane.Settings
|
|||||||
[Tooltip("Duration of result evaluation (seconds)")]
|
[Tooltip("Duration of result evaluation (seconds)")]
|
||||||
[SerializeField] private float evaluationDuration = 1f;
|
[SerializeField] private float evaluationDuration = 1f;
|
||||||
|
|
||||||
|
[Tooltip("Duration to linger on target during flyby (seconds)")]
|
||||||
|
[SerializeField] private float targetFlybyLingerDuration = 1.5f;
|
||||||
|
|
||||||
|
[Tooltip("Fallback blend time if CinemachineBrain blend detection unavailable (seconds)")]
|
||||||
|
[SerializeField] private float targetFlybyCameraBlendTime = 1f;
|
||||||
|
|
||||||
[Header("Spawn System")]
|
[Header("Spawn System")]
|
||||||
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback distance.")]
|
[Tooltip("Distance beyond target to pre-spawn (extends the pre-spawn range)")]
|
||||||
[SerializeField] private Transform dynamicSpawnThresholdMarker;
|
[SerializeField] private float preSpawnBeyondTargetDistance = 30f;
|
||||||
|
|
||||||
[Tooltip("Minimum random distance for target spawn")]
|
[Tooltip("Minimum random distance for target spawn")]
|
||||||
[SerializeField] private float targetMinDistance = 30f;
|
[SerializeField] private float targetMinDistance = 30f;
|
||||||
@@ -139,6 +145,9 @@ namespace Minigames.Airplane.Settings
|
|||||||
public float IntroDuration => introDuration;
|
public float IntroDuration => introDuration;
|
||||||
public float PersonIntroDuration => personIntroDuration;
|
public float PersonIntroDuration => personIntroDuration;
|
||||||
public float EvaluationDuration => evaluationDuration;
|
public float EvaluationDuration => evaluationDuration;
|
||||||
|
public float TargetFlybyLingerDuration => targetFlybyLingerDuration;
|
||||||
|
public float TargetFlybyCameraBlendTime => targetFlybyCameraBlendTime;
|
||||||
|
public float PreSpawnBeyondTargetDistance => preSpawnBeyondTargetDistance;
|
||||||
public float TargetMinDistance => targetMinDistance;
|
public float TargetMinDistance => targetMinDistance;
|
||||||
public float TargetMaxDistance => targetMaxDistance;
|
public float TargetMaxDistance => targetMaxDistance;
|
||||||
public float ObjectSpawnMinDistance => objectSpawnMinDistance;
|
public float ObjectSpawnMinDistance => objectSpawnMinDistance;
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ namespace Minigames.BirdPooper
|
|||||||
public UnityEngine.Events.UnityEvent OnFlap;
|
public UnityEngine.Events.UnityEvent OnFlap;
|
||||||
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
|
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
|
||||||
|
|
||||||
private Rigidbody2D rb;
|
private Rigidbody2D _rb;
|
||||||
private IBirdPooperSettings settings;
|
private IBirdPooperSettings _settings;
|
||||||
private float verticalVelocity = 0f;
|
private float _verticalVelocity;
|
||||||
private bool isDead = false;
|
private bool _isDead;
|
||||||
private float fixedXPosition; // Store the initial X position from the scene
|
private float _fixedXPosition; // Store the initial X position from the scene
|
||||||
|
private bool _isInitialized; // Flag to control when physics/input are active
|
||||||
|
|
||||||
internal override void OnManagedAwake()
|
internal override void OnManagedAwake()
|
||||||
{
|
{
|
||||||
@@ -31,33 +32,49 @@ namespace Minigames.BirdPooper
|
|||||||
if (OnPlayerDamaged == null)
|
if (OnPlayerDamaged == null)
|
||||||
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
|
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
|
||||||
|
|
||||||
// Load settings
|
// Only cache component references - NO setup yet
|
||||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
_rb = GetComponent<Rigidbody2D>();
|
||||||
if (settings == null)
|
if (_rb == null)
|
||||||
{
|
|
||||||
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Rigidbody2D component (Dynamic with gravityScale = 0)
|
|
||||||
rb = GetComponent<Rigidbody2D>();
|
|
||||||
if (rb != null)
|
|
||||||
{
|
|
||||||
rb.gravityScale = 0f; // Disable Unity physics gravity
|
|
||||||
rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
|
|
||||||
|
|
||||||
// Store the initial X position from the scene
|
|
||||||
fixedXPosition = rb.position.x;
|
|
||||||
Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
|
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register as default consumer (gets input if nothing else consumes it)
|
// Load settings
|
||||||
// This allows UI buttons to work while still flapping when tapping empty space
|
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||||
|
if (_settings == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("[BirdPlayerController] References cached, waiting for initialization...");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the player controller - enables physics and input.
|
||||||
|
/// Should be called by BirdPooperGameManager when ready to start the game.
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
if (_isInitialized)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[BirdPlayerController] Already initialized!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rb == null || _settings == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPlayerController] Cannot initialize - missing references!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup physics
|
||||||
|
_rb.gravityScale = 0f; // Disable Unity physics gravity
|
||||||
|
_rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
|
||||||
|
|
||||||
|
// Store the initial X position from the scene
|
||||||
|
_fixedXPosition = _rb.position.x;
|
||||||
|
|
||||||
|
// Register as default input consumer
|
||||||
if (Input.InputManager.Instance != null)
|
if (Input.InputManager.Instance != null)
|
||||||
{
|
{
|
||||||
Input.InputManager.Instance.SetDefaultConsumer(this);
|
Input.InputManager.Instance.SetDefaultConsumer(this);
|
||||||
@@ -67,52 +84,74 @@ namespace Minigames.BirdPooper
|
|||||||
{
|
{
|
||||||
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
|
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
Debug.Log($"[BirdPlayerController] Initialized! Fixed X position: {_fixedXPosition}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!isDead && settings != null && rb != null)
|
// Only run physics/movement if initialized
|
||||||
{
|
if (!_isInitialized || _isDead || _settings == null || _rb == null)
|
||||||
// Apply manual gravity
|
return;
|
||||||
verticalVelocity -= settings.Gravity * Time.deltaTime;
|
|
||||||
|
// Apply manual gravity
|
||||||
// Cap fall speed (terminal velocity)
|
_verticalVelocity -= _settings.Gravity * Time.deltaTime;
|
||||||
if (verticalVelocity < -settings.MaxFallSpeed)
|
|
||||||
verticalVelocity = -settings.MaxFallSpeed;
|
// Cap fall speed (terminal velocity)
|
||||||
|
if (_verticalVelocity < -_settings.MaxFallSpeed)
|
||||||
// Update position manually
|
_verticalVelocity = -_settings.MaxFallSpeed;
|
||||||
Vector2 newPosition = rb.position;
|
|
||||||
newPosition.y += verticalVelocity * Time.deltaTime;
|
// Update position manually
|
||||||
newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position
|
Vector2 newPosition = _rb.position;
|
||||||
|
newPosition.y += _verticalVelocity * Time.deltaTime;
|
||||||
// Clamp Y position to bounds
|
newPosition.x = _fixedXPosition; // Keep X fixed at scene-configured position
|
||||||
newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY);
|
|
||||||
|
// Clamp Y position to bounds
|
||||||
rb.MovePosition(newPosition);
|
newPosition.y = Mathf.Clamp(newPosition.y, _settings.MinY, _settings.MaxY);
|
||||||
|
|
||||||
// Update rotation based on velocity
|
_rb.MovePosition(newPosition);
|
||||||
UpdateRotation();
|
|
||||||
}
|
// Update rotation based on velocity
|
||||||
|
UpdateRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ITouchInputConsumer Implementation
|
#region ITouchInputConsumer Implementation
|
||||||
|
|
||||||
public void OnTap(Vector2 tapPosition)
|
public void OnTap(Vector2 tapPosition)
|
||||||
{
|
{
|
||||||
if (!isDead && settings != null)
|
// Only respond to input if initialized and alive
|
||||||
{
|
if (!_isInitialized || _isDead || _settings == null)
|
||||||
verticalVelocity = settings.FlapForce;
|
return;
|
||||||
Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}");
|
|
||||||
|
Flap();
|
||||||
// Emit flap event
|
|
||||||
OnFlap?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnHoldStart(Vector2 position) { }
|
public void OnHoldStart(Vector2 position) { }
|
||||||
public void OnHoldMove(Vector2 position) { }
|
public void OnHoldMove(Vector2 position) { }
|
||||||
public void OnHoldEnd(Vector2 position) { }
|
public void OnHoldEnd(Vector2 position) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Player Actions
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes the bird flap, applying upward velocity.
|
||||||
|
/// Can be called by input system or externally (e.g., for first tap).
|
||||||
|
/// </summary>
|
||||||
|
public void Flap()
|
||||||
|
{
|
||||||
|
if (!_isInitialized || _isDead || _settings == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_verticalVelocity = _settings.FlapForce;
|
||||||
|
Debug.Log($"[BirdPlayerController] Flap! velocity = {_verticalVelocity}");
|
||||||
|
|
||||||
|
// Emit flap event
|
||||||
|
OnFlap?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Rotation
|
#region Rotation
|
||||||
@@ -123,19 +162,19 @@ namespace Minigames.BirdPooper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateRotation()
|
private void UpdateRotation()
|
||||||
{
|
{
|
||||||
if (settings == null) return;
|
if (_settings == null) return;
|
||||||
|
|
||||||
// Map velocity to rotation angle
|
// Map velocity to rotation angle
|
||||||
// When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down)
|
// When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down)
|
||||||
// When at flap velocity (+FlapForce): +MaxRotationAngle (up)
|
// When at flap velocity (+FlapForce): +MaxRotationAngle (up)
|
||||||
float velocityPercent = Mathf.InverseLerp(
|
float velocityPercent = Mathf.InverseLerp(
|
||||||
-settings.MaxFallSpeed,
|
-_settings.MaxFallSpeed,
|
||||||
settings.FlapForce,
|
_settings.FlapForce,
|
||||||
verticalVelocity
|
_verticalVelocity
|
||||||
);
|
);
|
||||||
float targetAngle = Mathf.Lerp(
|
float targetAngle = Mathf.Lerp(
|
||||||
-settings.MaxRotationAngle,
|
-_settings.MaxRotationAngle,
|
||||||
settings.MaxRotationAngle,
|
_settings.MaxRotationAngle,
|
||||||
velocityPercent
|
velocityPercent
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,7 +187,7 @@ namespace Minigames.BirdPooper
|
|||||||
float smoothedAngle = Mathf.Lerp(
|
float smoothedAngle = Mathf.Lerp(
|
||||||
currentAngle,
|
currentAngle,
|
||||||
targetAngle,
|
targetAngle,
|
||||||
settings.RotationSpeed * Time.deltaTime
|
_settings.RotationSpeed * Time.deltaTime
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply rotation to Z axis only (2D rotation)
|
// Apply rotation to Z axis only (2D rotation)
|
||||||
@@ -175,10 +214,10 @@ namespace Minigames.BirdPooper
|
|||||||
private void HandleDeath()
|
private void HandleDeath()
|
||||||
{
|
{
|
||||||
// Only process death once
|
// Only process death once
|
||||||
if (isDead) return;
|
if (_isDead) return;
|
||||||
|
|
||||||
isDead = true;
|
_isDead = true;
|
||||||
verticalVelocity = 0f;
|
_verticalVelocity = 0f;
|
||||||
Debug.Log("[BirdPlayerController] Bird died!");
|
Debug.Log("[BirdPlayerController] Bird died!");
|
||||||
|
|
||||||
// Emit damage event - let the game manager handle UI
|
// Emit damage event - let the game manager handle UI
|
||||||
@@ -187,9 +226,9 @@ namespace Minigames.BirdPooper
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Public Properties
|
#region Public Accessors
|
||||||
|
|
||||||
public bool IsDead => isDead;
|
public bool IsDead => _isDead;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ namespace Minigames.BirdPooper
|
|||||||
[SerializeField] private BirdPlayerController player;
|
[SerializeField] private BirdPlayerController player;
|
||||||
[SerializeField] private ObstacleSpawner obstacleSpawner;
|
[SerializeField] private ObstacleSpawner obstacleSpawner;
|
||||||
[SerializeField] private TargetSpawner targetSpawner;
|
[SerializeField] private TargetSpawner targetSpawner;
|
||||||
|
[SerializeField] private TapToStartController tapToStartController;
|
||||||
[SerializeField] private GameOverScreen gameOverScreen;
|
[SerializeField] private GameOverScreen gameOverScreen;
|
||||||
[SerializeField] private GameObject poopPrefab;
|
[SerializeField] private GameObject poopPrefab;
|
||||||
|
|
||||||
[Header("Game State")]
|
[Header("Game State")]
|
||||||
private int targetsHit;
|
private int _targetsHit;
|
||||||
private bool isGameOver;
|
private bool _isGameOver;
|
||||||
|
|
||||||
[Header("Input")]
|
[Header("Input")]
|
||||||
[Tooltip("Minimum seconds between consecutive poop spawns")]
|
[Tooltip("Minimum seconds between consecutive poop spawns")]
|
||||||
@@ -60,6 +61,11 @@ namespace Minigames.BirdPooper
|
|||||||
Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn.");
|
Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tapToStartController == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPooperGameManager] TapToStartController reference not assigned!");
|
||||||
|
}
|
||||||
|
|
||||||
if (gameOverScreen == null)
|
if (gameOverScreen == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
|
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
|
||||||
@@ -76,34 +82,76 @@ namespace Minigames.BirdPooper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void OnManagedStart()
|
/// <summary>
|
||||||
|
/// Called after scene is fully loaded and any save data is restored.
|
||||||
|
/// Activates tap-to-start UI instead of starting immediately.
|
||||||
|
/// </summary>
|
||||||
|
internal override void OnSceneRestoreCompleted()
|
||||||
{
|
{
|
||||||
base.OnManagedStart();
|
base.OnSceneRestoreCompleted();
|
||||||
|
|
||||||
|
Debug.Log("[BirdPooperGameManager] Scene fully loaded, activating tap-to-start...");
|
||||||
|
|
||||||
|
if (tapToStartController != null)
|
||||||
|
{
|
||||||
|
tapToStartController.Activate();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPooperGameManager] TapToStartController missing! Starting game immediately as fallback.");
|
||||||
|
BeginMinigame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central method to begin the minigame.
|
||||||
|
/// Initializes player, starts spawners, and sets up game state.
|
||||||
|
/// </summary>
|
||||||
|
public void BeginMinigame()
|
||||||
|
{
|
||||||
// Initialize game state
|
// Initialize game state
|
||||||
isGameOver = false;
|
_isGameOver = false;
|
||||||
targetsHit = 0;
|
_targetsHit = 0;
|
||||||
|
|
||||||
// Subscribe to player events
|
// Initialize and enable player
|
||||||
if (player != null)
|
if (player != null)
|
||||||
{
|
{
|
||||||
|
player.Initialize();
|
||||||
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
|
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
|
||||||
Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event");
|
|
||||||
|
// Make bird do initial flap so first tap feels responsive
|
||||||
|
player.Flap();
|
||||||
|
|
||||||
|
Debug.Log("[BirdPooperGameManager] Player initialized and event subscribed");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - player reference missing!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start obstacle spawning
|
// Start obstacle spawning
|
||||||
if (obstacleSpawner != null)
|
if (obstacleSpawner != null)
|
||||||
{
|
{
|
||||||
obstacleSpawner.StartSpawning();
|
obstacleSpawner.StartSpawning();
|
||||||
Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
|
Debug.Log("[BirdPooperGameManager] Obstacle spawner started");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - obstacle spawner reference missing!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start target spawning
|
// Start target spawning
|
||||||
if (targetSpawner != null)
|
if (targetSpawner != null)
|
||||||
{
|
{
|
||||||
targetSpawner.StartSpawning();
|
targetSpawner.StartSpawning();
|
||||||
Debug.Log("[BirdPooperGameManager] Started target spawning");
|
Debug.Log("[BirdPooperGameManager] Target spawner started");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[BirdPooperGameManager] Target spawner reference missing - targets will not spawn");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("[BirdPooperGameManager] ✅ Minigame started successfully!");
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void OnManagedDestroy()
|
internal override void OnManagedDestroy()
|
||||||
@@ -129,10 +177,10 @@ namespace Minigames.BirdPooper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void HandlePlayerDamaged()
|
private void HandlePlayerDamaged()
|
||||||
{
|
{
|
||||||
if (isGameOver) return;
|
if (_isGameOver) return;
|
||||||
|
|
||||||
isGameOver = true;
|
_isGameOver = true;
|
||||||
Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {targetsHit}");
|
Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {_targetsHit}");
|
||||||
|
|
||||||
// Stop spawning obstacles
|
// Stop spawning obstacles
|
||||||
if (obstacleSpawner != null)
|
if (obstacleSpawner != null)
|
||||||
@@ -167,7 +215,7 @@ namespace Minigames.BirdPooper
|
|||||||
if (Time.time < _lastPoopTime + poopCooldown)
|
if (Time.time < _lastPoopTime + poopCooldown)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (isGameOver || player == null || poopPrefab == null)
|
if (_isGameOver || player == null || poopPrefab == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Vector3 spawnPosition = player.transform.position;
|
Vector3 spawnPosition = player.transform.position;
|
||||||
@@ -183,16 +231,17 @@ namespace Minigames.BirdPooper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnTargetHit()
|
public void OnTargetHit()
|
||||||
{
|
{
|
||||||
if (isGameOver) return;
|
if (_isGameOver) return;
|
||||||
|
|
||||||
targetsHit++;
|
_targetsHit++;
|
||||||
Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {targetsHit}");
|
Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {_targetsHit}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Public Accessors
|
#region Public Accessors
|
||||||
|
|
||||||
public bool IsGameOver => isGameOver;
|
public bool IsGameOver => _isGameOver;
|
||||||
public int TargetsHit => targetsHit;
|
public int TargetsHit => _targetsHit;
|
||||||
|
public BirdPlayerController Player => player;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
111
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
Normal file
111
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.BirdPooper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Container for a pool of obstacle prefabs at a specific difficulty tier.
|
||||||
|
/// Pools are ordered by difficulty, with pool[0] being the easiest.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class ObstaclePool
|
||||||
|
{
|
||||||
|
[Tooltip("Obstacles in this difficulty tier")]
|
||||||
|
public GameObject[] obstacles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for obstacle spawning in Bird Pooper minigame.
|
||||||
|
/// Includes difficulty pools, spawn timing, and diversity settings.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class ObstacleSpawnConfig
|
||||||
|
{
|
||||||
|
[Header("Difficulty Pools")]
|
||||||
|
[Tooltip("Obstacle pools ordered by difficulty (pool[0] = easiest, always active)")]
|
||||||
|
public ObstaclePool[] obstaclePools;
|
||||||
|
|
||||||
|
[Tooltip("Times (in seconds) when each additional pool unlocks. Length should be obstaclePools.Length - 1. At poolUnlockTimes[i], pool[i+1] becomes available.")]
|
||||||
|
public float[] poolUnlockTimes;
|
||||||
|
|
||||||
|
[Header("Spawn Timing")]
|
||||||
|
[Tooltip("Minimum interval between spawns (seconds) - represents high difficulty")]
|
||||||
|
public float minSpawnInterval = 1f;
|
||||||
|
|
||||||
|
[Tooltip("Maximum interval between spawns (seconds) - represents low difficulty")]
|
||||||
|
public float maxSpawnInterval = 2f;
|
||||||
|
|
||||||
|
[Header("Difficulty Scaling")]
|
||||||
|
[Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
|
||||||
|
public float difficultyRampDuration = 60f;
|
||||||
|
|
||||||
|
[Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
|
||||||
|
public AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
||||||
|
|
||||||
|
[Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
|
||||||
|
public float intervalJitter = 0.05f;
|
||||||
|
|
||||||
|
[Header("Recency / Diversity")]
|
||||||
|
[Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
|
||||||
|
public float recentDecayDuration = 10f;
|
||||||
|
|
||||||
|
[Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
|
||||||
|
[Range(0f, 1f)]
|
||||||
|
public float minRecentWeight = 0.05f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the configuration and logs warnings for invalid settings.
|
||||||
|
/// </summary>
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
// Validate pools
|
||||||
|
if (obstaclePools == null || obstaclePools.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ObstacleSpawnConfig] No obstacle pools defined!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pool unlock times
|
||||||
|
int expectedUnlockTimes = obstaclePools.Length - 1;
|
||||||
|
if (poolUnlockTimes == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes is null. Expected {expectedUnlockTimes} entries. Only pool[0] will be available.");
|
||||||
|
}
|
||||||
|
else if (poolUnlockTimes.Length != expectedUnlockTimes)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes.Length ({poolUnlockTimes.Length}) does not match expected value ({expectedUnlockTimes}). Should be obstaclePools.Length - 1.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate spawn intervals
|
||||||
|
if (minSpawnInterval < 0f)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is negative. Clamping to 0.");
|
||||||
|
}
|
||||||
|
if (maxSpawnInterval < 0f)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] maxSpawnInterval is negative. Clamping to 0.");
|
||||||
|
}
|
||||||
|
if (minSpawnInterval > maxSpawnInterval)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is greater than maxSpawnInterval. Values should be swapped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate difficulty ramp
|
||||||
|
if (difficultyRampDuration < 0.01f)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] difficultyRampDuration is too small. Should be at least 0.01.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate recency settings
|
||||||
|
if (recentDecayDuration < 0.01f)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] recentDecayDuration is too small. Should be at least 0.01.");
|
||||||
|
}
|
||||||
|
if (minRecentWeight < 0f || minRecentWeight > 1f)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[ObstacleSpawnConfig] minRecentWeight should be between 0 and 1.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 74f4387c76774225afa2d02d590d5ad4
|
||||||
|
timeCreated: 1765918010
|
||||||
@@ -4,6 +4,7 @@ using Core.Settings;
|
|||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using AppleHillsCamera;
|
using AppleHillsCamera;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Minigames.BirdPooper
|
namespace Minigames.BirdPooper
|
||||||
{
|
{
|
||||||
@@ -11,6 +12,7 @@ namespace Minigames.BirdPooper
|
|||||||
/// Spawns obstacles at regular intervals for Bird Pooper minigame.
|
/// Spawns obstacles at regular intervals for Bird Pooper minigame.
|
||||||
/// Uses Transform references for spawn and despawn positions instead of hardcoded values.
|
/// Uses Transform references for spawn and despawn positions instead of hardcoded values.
|
||||||
/// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly).
|
/// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly).
|
||||||
|
/// Supports dynamic difficulty pools that unlock over time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ObstacleSpawner : ManagedBehaviour
|
public class ObstacleSpawner : ManagedBehaviour
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,9 @@ namespace Minigames.BirdPooper
|
|||||||
|
|
||||||
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
|
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
|
||||||
[SerializeField] private Transform despawnPoint;
|
[SerializeField] private Transform despawnPoint;
|
||||||
|
|
||||||
|
[Tooltip("Optional parent transform for spawned obstacles (for scene organization)")]
|
||||||
|
[SerializeField] private Transform obstacleContainer;
|
||||||
|
|
||||||
[Header("EdgeAnchor References")]
|
[Header("EdgeAnchor References")]
|
||||||
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
|
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
|
||||||
@@ -28,55 +33,45 @@ namespace Minigames.BirdPooper
|
|||||||
[Tooltip("CameraScreenAdapter to pass to spawned obstacles")]
|
[Tooltip("CameraScreenAdapter to pass to spawned obstacles")]
|
||||||
[SerializeField] private CameraScreenAdapter cameraAdapter;
|
[SerializeField] private CameraScreenAdapter cameraAdapter;
|
||||||
|
|
||||||
[Header("Obstacle Prefabs")]
|
private IBirdPooperSettings _settings;
|
||||||
[Tooltip("Array of obstacle prefabs to spawn randomly")]
|
private ObstacleSpawnConfig _spawnConfig;
|
||||||
[SerializeField] private GameObject[] obstaclePrefabs;
|
private float _spawnTimer;
|
||||||
|
private bool _isSpawning;
|
||||||
[Header("Spawn Timing")]
|
|
||||||
[Tooltip("Minimum interval between spawns (seconds)")]
|
|
||||||
[SerializeField] private float minSpawnInterval = 1f;
|
|
||||||
[Tooltip("Maximum interval between spawns (seconds)")]
|
|
||||||
[SerializeField] private float maxSpawnInterval = 2f;
|
|
||||||
|
|
||||||
[Header("Difficulty")]
|
|
||||||
[Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
|
|
||||||
[SerializeField] private float difficultyRampDuration = 60f;
|
|
||||||
[Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
|
|
||||||
[SerializeField] private AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
|
||||||
[Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
|
|
||||||
[SerializeField] private float intervalJitter = 0.05f;
|
|
||||||
|
|
||||||
[Header("Recency / Diversity")]
|
|
||||||
[Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
|
|
||||||
[SerializeField] private float recentDecayDuration = 10f;
|
|
||||||
[Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
|
|
||||||
[Range(0f, 1f)]
|
|
||||||
[SerializeField] private float minRecentWeight = 0.05f;
|
|
||||||
|
|
||||||
private IBirdPooperSettings settings;
|
|
||||||
private float spawnTimer;
|
|
||||||
private bool isSpawning;
|
|
||||||
private float _currentSpawnInterval = 1f;
|
private float _currentSpawnInterval = 1f;
|
||||||
|
|
||||||
// difficulty tracking
|
// Difficulty tracking
|
||||||
private float _elapsedTime = 0f;
|
private float _elapsedTime;
|
||||||
|
|
||||||
// recency tracking
|
// Master obstacle list for recency tracking
|
||||||
|
private List<GameObject> _allObstacles;
|
||||||
|
private Dictionary<GameObject, int> _obstacleToGlobalIndex;
|
||||||
private float[] _lastUsedTimes;
|
private float[] _lastUsedTimes;
|
||||||
|
|
||||||
internal override void OnManagedAwake()
|
/// <summary>
|
||||||
|
/// Initializes the obstacle spawner by loading settings, validating references, and building obstacle pools.
|
||||||
|
/// Should be called once before spawning begins.
|
||||||
|
/// </summary>
|
||||||
|
private void Initialize()
|
||||||
{
|
{
|
||||||
base.OnManagedAwake();
|
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||||
if (settings == null)
|
if (_settings == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!");
|
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found! Cannot initialize.");
|
||||||
// continue — we now use min/max interval fields instead of relying on settings
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate references
|
_spawnConfig = _settings.ObstacleSpawnConfiguration;
|
||||||
|
if (_spawnConfig == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ObstacleSpawner] ObstacleSpawnConfiguration not found in settings! Cannot initialize.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate spawn configuration
|
||||||
|
_spawnConfig.Validate();
|
||||||
|
|
||||||
|
// Validate scene references
|
||||||
if (spawnPoint == null)
|
if (spawnPoint == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
|
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
|
||||||
@@ -87,11 +82,6 @@ namespace Minigames.BirdPooper
|
|||||||
Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
|
Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
|
|
||||||
{
|
|
||||||
Debug.LogError("[ObstacleSpawner] No obstacle prefabs assigned! Please assign at least one prefab in the Inspector.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referenceMarker == null)
|
if (referenceMarker == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning.");
|
Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning.");
|
||||||
@@ -102,58 +92,100 @@ namespace Minigames.BirdPooper
|
|||||||
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
|
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate interval range
|
// Build master obstacle list from all pools
|
||||||
if (minSpawnInterval < 0f) minSpawnInterval = 0f;
|
BuildMasterObstacleList();
|
||||||
if (maxSpawnInterval < 0f) maxSpawnInterval = 0f;
|
|
||||||
if (minSpawnInterval > maxSpawnInterval)
|
Debug.Log("[ObstacleSpawner] Initialized successfully with pool-based difficulty scaling");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a master list of all obstacles across all pools and creates index mappings for recency tracking.
|
||||||
|
/// </summary>
|
||||||
|
private void BuildMasterObstacleList()
|
||||||
|
{
|
||||||
|
_allObstacles = new List<GameObject>();
|
||||||
|
_obstacleToGlobalIndex = new Dictionary<GameObject, int>();
|
||||||
|
|
||||||
|
if (_spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
|
||||||
{
|
{
|
||||||
float tmp = minSpawnInterval;
|
Debug.LogError("[ObstacleSpawner] No obstacle pools defined in configuration!");
|
||||||
minSpawnInterval = maxSpawnInterval;
|
return;
|
||||||
maxSpawnInterval = tmp;
|
|
||||||
Debug.LogWarning("[ObstacleSpawner] minSpawnInterval was greater than maxSpawnInterval. Values were swapped.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp ramp duration
|
int globalIndex = 0;
|
||||||
if (difficultyRampDuration < 0.01f) difficultyRampDuration = 0.01f;
|
for (int poolIdx = 0; poolIdx < _spawnConfig.obstaclePools.Length; poolIdx++)
|
||||||
|
{
|
||||||
|
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||||
|
if (pool == null || pool.obstacles == null || pool.obstacles.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawner] Pool[{poolIdx}] is empty or null!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp recency
|
foreach (GameObject prefab in pool.obstacles)
|
||||||
if (recentDecayDuration < 0.01f) recentDecayDuration = 0.01f;
|
{
|
||||||
if (minRecentWeight < 0f) minRecentWeight = 0f;
|
if (prefab == null)
|
||||||
if (minRecentWeight > 1f) minRecentWeight = 1f;
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawner] Null prefab found in pool[{poolIdx}]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize last-used timestamps so prefabs start available (set to sufficiently negative so they appear with full weight)
|
// Allow duplicates - same prefab can appear in multiple pools
|
||||||
int n = obstaclePrefabs != null ? obstaclePrefabs.Length : 0;
|
if (!_obstacleToGlobalIndex.ContainsKey(prefab))
|
||||||
_lastUsedTimes = new float[n];
|
{
|
||||||
float initTime = -recentDecayDuration - 1f;
|
_obstacleToGlobalIndex[prefab] = globalIndex;
|
||||||
for (int i = 0; i < n; i++) _lastUsedTimes[i] = initTime;
|
_allObstacles.Add(prefab);
|
||||||
|
globalIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Debug.Log("[ObstacleSpawner] Initialized successfully");
|
// Initialize recency tracking
|
||||||
|
int totalObstacles = _allObstacles.Count;
|
||||||
|
_lastUsedTimes = new float[totalObstacles];
|
||||||
|
float initTime = Time.time - _spawnConfig.recentDecayDuration - 1f;
|
||||||
|
for (int i = 0; i < totalObstacles; i++)
|
||||||
|
{
|
||||||
|
_lastUsedTimes[i] = initTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log pool configuration
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"[ObstacleSpawner] Loaded {_spawnConfig.obstaclePools.Length} obstacle pools with {totalObstacles} unique obstacles:");
|
||||||
|
for (int i = 0; i < _spawnConfig.obstaclePools.Length; i++)
|
||||||
|
{
|
||||||
|
ObstaclePool pool = _spawnConfig.obstaclePools[i];
|
||||||
|
int obstacleCount = pool != null && pool.obstacles != null ? pool.obstacles.Length : 0;
|
||||||
|
float unlockTime = (i == 0) ? 0f : (_spawnConfig.poolUnlockTimes != null && i - 1 < _spawnConfig.poolUnlockTimes.Length ? _spawnConfig.poolUnlockTimes[i - 1] : -1f);
|
||||||
|
sb.AppendLine($" Pool[{i}]: {obstacleCount} obstacles, unlocks at {unlockTime}s");
|
||||||
|
}
|
||||||
|
Debug.Log(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!isSpawning || spawnPoint == null) return;
|
if (!_isSpawning || spawnPoint == null || _spawnConfig == null) return;
|
||||||
|
|
||||||
spawnTimer += Time.deltaTime;
|
_spawnTimer += Time.deltaTime;
|
||||||
_elapsedTime += Time.deltaTime;
|
_elapsedTime += Time.deltaTime;
|
||||||
|
|
||||||
if (spawnTimer >= _currentSpawnInterval)
|
if (_spawnTimer >= _currentSpawnInterval)
|
||||||
{
|
{
|
||||||
SpawnObstacle();
|
SpawnObstacle();
|
||||||
spawnTimer = 0f;
|
_spawnTimer = 0f;
|
||||||
|
|
||||||
// pick next interval based on difficulty ramp
|
// Pick next interval based on difficulty ramp
|
||||||
float t = Mathf.Clamp01(_elapsedTime / difficultyRampDuration);
|
float t = Mathf.Clamp01(_elapsedTime / _spawnConfig.difficultyRampDuration);
|
||||||
float difficulty = difficultyCurve.Evaluate(t); // 0..1
|
float difficulty = _spawnConfig.difficultyCurve.Evaluate(t); // 0..1
|
||||||
|
|
||||||
// map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
|
// Map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
|
||||||
float baseInterval = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, difficulty);
|
float baseInterval = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, difficulty);
|
||||||
|
|
||||||
// apply small jitter
|
// Apply small jitter
|
||||||
if (intervalJitter > 0f)
|
if (_spawnConfig.intervalJitter > 0f)
|
||||||
{
|
{
|
||||||
float jitter = Random.Range(-intervalJitter, intervalJitter);
|
float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
|
||||||
_currentSpawnInterval = Mathf.Max(0f, baseInterval * (1f + jitter));
|
_currentSpawnInterval = Mathf.Max(0f, baseInterval * (1f + jitter));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -167,14 +199,14 @@ namespace Minigames.BirdPooper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawn a random obstacle at the spawn point position (Y = 0).
|
/// Spawn a random obstacle from currently unlocked pools at the spawn point position (Y = 0).
|
||||||
/// Uses timestamp/decay weighting so prefabs used recently are less likely.
|
/// Uses timestamp/decay weighting so prefabs used recently are less likely.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SpawnObstacle()
|
private void SpawnObstacle()
|
||||||
{
|
{
|
||||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
|
if (_spawnConfig == null || _spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!");
|
Debug.LogWarning("[ObstacleSpawner] No obstacle pools configured!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,53 +216,133 @@ namespace Minigames.BirdPooper
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = obstaclePrefabs.Length;
|
// Determine which pools are currently unlocked based on elapsed time
|
||||||
|
int unlockedPoolCount = 1; // Pool[0] is always unlocked
|
||||||
// Defensive: ensure _lastUsedTimes is initialized and matches prefab count
|
if (_spawnConfig.poolUnlockTimes != null)
|
||||||
if (_lastUsedTimes == null || _lastUsedTimes.Length != count)
|
|
||||||
{
|
{
|
||||||
_lastUsedTimes = new float[count];
|
for (int i = 0; i < _spawnConfig.poolUnlockTimes.Length; i++)
|
||||||
float initTime = Time.time - recentDecayDuration - 1f;
|
{
|
||||||
for (int i = 0; i < count; i++) _lastUsedTimes[i] = initTime;
|
if (_elapsedTime >= _spawnConfig.poolUnlockTimes[i])
|
||||||
|
{
|
||||||
|
unlockedPoolCount = i + 2; // +2 because we're unlocking pool[i+1]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break; // Times should be in order, so stop when we hit a future unlock
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute weights based on recency (newer = lower weight)
|
// Clamp to available pools
|
||||||
float[] weights = new float[count];
|
unlockedPoolCount = Mathf.Min(unlockedPoolCount, _spawnConfig.obstaclePools.Length);
|
||||||
|
|
||||||
|
// Build list of available obstacles from unlocked pools
|
||||||
|
List<GameObject> availableObstacles = new List<GameObject>();
|
||||||
|
List<int> availableGlobalIndices = new List<int>();
|
||||||
|
|
||||||
|
for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
|
||||||
|
{
|
||||||
|
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||||
|
if (pool == null || pool.obstacles == null) continue;
|
||||||
|
|
||||||
|
foreach (GameObject prefab in pool.obstacles)
|
||||||
|
{
|
||||||
|
if (prefab == null) continue;
|
||||||
|
|
||||||
|
// Add to available list (duplicates allowed if same prefab is in multiple pools)
|
||||||
|
availableObstacles.Add(prefab);
|
||||||
|
|
||||||
|
// Look up global index for recency tracking
|
||||||
|
if (_obstacleToGlobalIndex.TryGetValue(prefab, out int globalIdx))
|
||||||
|
{
|
||||||
|
availableGlobalIndices.Add(globalIdx);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawner] Prefab '{prefab.name}' not found in global index!");
|
||||||
|
availableGlobalIndices.Add(-1); // Invalid index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableObstacles.Count == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ObstacleSpawner] No obstacles available in unlocked pools (0..{unlockedPoolCount-1})");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute weights based on recency
|
||||||
|
float[] weights = new float[availableObstacles.Count];
|
||||||
float now = Time.time;
|
float now = Time.time;
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
|
for (int i = 0; i < availableObstacles.Count; i++)
|
||||||
{
|
{
|
||||||
float age = now - _lastUsedTimes[i];
|
int globalIdx = availableGlobalIndices[i];
|
||||||
float normalized = Mathf.Clamp01(age / recentDecayDuration); // 0 = just used, 1 = fully recovered
|
if (globalIdx < 0 || globalIdx >= _lastUsedTimes.Length)
|
||||||
float weight = Mathf.Max(minRecentWeight, normalized); // ensure minimum probability
|
{
|
||||||
weights[i] = weight; // base weight = 1.0, could be extended to per-prefab weights
|
weights[i] = 1f; // Default weight for invalid indices
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float age = now - _lastUsedTimes[globalIdx];
|
||||||
|
float normalized = Mathf.Clamp01(age / _spawnConfig.recentDecayDuration); // 0 = just used, 1 = fully recovered
|
||||||
|
float weight = Mathf.Max(_spawnConfig.minRecentWeight, normalized);
|
||||||
|
weights[i] = weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute probabilities for logging
|
// Compute and log probabilities for debugging
|
||||||
float totalW = 0f;
|
float totalW = 0f;
|
||||||
for (int i = 0; i < count; i++) totalW += Mathf.Max(0f, weights[i]);
|
for (int i = 0; i < availableObstacles.Count; i++)
|
||||||
|
{
|
||||||
|
totalW += Mathf.Max(0f, weights[i]);
|
||||||
|
}
|
||||||
|
|
||||||
if (totalW > 0f)
|
if (totalW > 0f)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.Append("[ObstacleSpawner] Prefab pick probabilities: ");
|
sb.Append($"[ObstacleSpawner] Spawning from pools 0-{unlockedPoolCount-1}. Probabilities: ");
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < availableObstacles.Count; i++)
|
||||||
{
|
{
|
||||||
float p = weights[i] / totalW;
|
float p = weights[i] / totalW;
|
||||||
string name = obstaclePrefabs[i] != null ? obstaclePrefabs[i].name : i.ToString();
|
string prefabName = availableObstacles[i] != null ? availableObstacles[i].name : i.ToString();
|
||||||
sb.AppendFormat("{0}:{1:P1}", name, p);
|
sb.AppendFormat("{0}:{1:P1}", prefabName, p);
|
||||||
if (i < count - 1) sb.Append(", ");
|
if (i < availableObstacles.Count - 1) sb.Append(", ");
|
||||||
}
|
}
|
||||||
Debug.Log(sb.ToString());
|
Debug.Log(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select obstacle using weighted random
|
||||||
int chosenIndex = WeightedPickIndex(weights);
|
int chosenIndex = WeightedPickIndex(weights);
|
||||||
GameObject selectedPrefab = obstaclePrefabs[chosenIndex];
|
GameObject selectedPrefab = availableObstacles[chosenIndex];
|
||||||
|
int selectedGlobalIndex = availableGlobalIndices[chosenIndex];
|
||||||
|
|
||||||
// record usage timestamp
|
// Record usage timestamp for recency tracking
|
||||||
_lastUsedTimes[chosenIndex] = Time.time;
|
if (selectedGlobalIndex >= 0 && selectedGlobalIndex < _lastUsedTimes.Length)
|
||||||
|
{
|
||||||
|
_lastUsedTimes[selectedGlobalIndex] = Time.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which pool this obstacle came from (for logging)
|
||||||
|
int sourcePool = -1;
|
||||||
|
for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
|
||||||
|
{
|
||||||
|
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||||
|
if (pool != null && pool.obstacles != null && System.Array.IndexOf(pool.obstacles, selectedPrefab) >= 0)
|
||||||
|
{
|
||||||
|
sourcePool = poolIdx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn at spawn point position with Y = 0
|
// Spawn at spawn point position with Y = 0
|
||||||
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
|
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
|
||||||
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
|
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
|
||||||
|
|
||||||
|
// Parent to container if provided
|
||||||
|
if (obstacleContainer != null)
|
||||||
|
{
|
||||||
|
obstacleObj.transform.SetParent(obstacleContainer, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize obstacle with despawn X position and EdgeAnchor references
|
// Initialize obstacle with despawn X position and EdgeAnchor references
|
||||||
Obstacle obstacle = obstacleObj.GetComponent<Obstacle>();
|
Obstacle obstacle = obstacleObj.GetComponent<Obstacle>();
|
||||||
@@ -244,7 +356,7 @@ namespace Minigames.BirdPooper
|
|||||||
Destroy(obstacleObj);
|
Destroy(obstacleObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}");
|
Debug.Log($"[ObstacleSpawner] Spawned '{selectedPrefab.name}' from pool[{sourcePool}] at {spawnPosition}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private int WeightedPickIndex(float[] weights)
|
private int WeightedPickIndex(float[] weights)
|
||||||
@@ -275,20 +387,45 @@ namespace Minigames.BirdPooper
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start spawning obstacles.
|
/// Start spawning obstacles.
|
||||||
|
/// Initializes the spawner if not already initialized, then begins spawning logic.
|
||||||
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
|
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void StartSpawning()
|
public void StartSpawning()
|
||||||
{
|
{
|
||||||
isSpawning = true;
|
// Initialize if not already done
|
||||||
spawnTimer = 0f;
|
if (_spawnConfig == null)
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure initialization was successful
|
||||||
|
if (_spawnConfig == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ObstacleSpawner] Cannot start spawning - initialization failed!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the spawning process
|
||||||
|
BeginSpawningObstacles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal method that handles the actual spawning startup logic.
|
||||||
|
/// Sets initial state, computes first interval, and spawns the first obstacle.
|
||||||
|
/// </summary>
|
||||||
|
private void BeginSpawningObstacles()
|
||||||
|
{
|
||||||
|
_isSpawning = true;
|
||||||
|
_spawnTimer = 0f;
|
||||||
_elapsedTime = 0f;
|
_elapsedTime = 0f;
|
||||||
|
|
||||||
// choose initial interval based on difficulty (at time 0)
|
// Choose initial interval based on difficulty (at time 0)
|
||||||
float initialDifficulty = difficultyCurve.Evaluate(0f);
|
float initialDifficulty = _spawnConfig.difficultyCurve.Evaluate(0f);
|
||||||
float initialBase = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, initialDifficulty);
|
float initialBase = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, initialDifficulty);
|
||||||
if (intervalJitter > 0f)
|
|
||||||
|
if (_spawnConfig.intervalJitter > 0f)
|
||||||
{
|
{
|
||||||
float jitter = Random.Range(-intervalJitter, intervalJitter);
|
float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
|
||||||
_currentSpawnInterval = Mathf.Max(0f, initialBase * (1f + jitter));
|
_currentSpawnInterval = Mathf.Max(0f, initialBase * (1f + jitter));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -310,14 +447,14 @@ namespace Minigames.BirdPooper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void StopSpawning()
|
public void StopSpawning()
|
||||||
{
|
{
|
||||||
isSpawning = false;
|
_isSpawning = false;
|
||||||
Debug.Log("[ObstacleSpawner] Stopped spawning");
|
Debug.Log("[ObstacleSpawner] Stopped spawning");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if spawner is currently active.
|
/// Check if spawner is currently active.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSpawning => isSpawning;
|
public bool IsSpawning => _isSpawning;
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
153
Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
Normal file
153
Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using Core.Lifecycle;
|
||||||
|
using Pixelplacement.TweenSystem;
|
||||||
|
using Utils;
|
||||||
|
|
||||||
|
namespace Minigames.BirdPooper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages "tap to start" flow for Bird Pooper minigame.
|
||||||
|
/// Shows blinking finger UI, waits for first tap, then starts the game.
|
||||||
|
/// </summary>
|
||||||
|
public class TapToStartController : ManagedBehaviour, ITouchInputConsumer
|
||||||
|
{
|
||||||
|
[Header("UI References")]
|
||||||
|
[SerializeField] private GameObject fingerContainer;
|
||||||
|
[SerializeField] private Image fingerImage;
|
||||||
|
|
||||||
|
[Header("Animation Settings")]
|
||||||
|
[Tooltip("Duration for one complete fade in/out cycle")]
|
||||||
|
[SerializeField] private float blinkDuration = 1.5f;
|
||||||
|
[Tooltip("Minimum alpha value during blink")]
|
||||||
|
[SerializeField] private float minAlpha = 0.3f;
|
||||||
|
[Tooltip("Maximum alpha value during blink")]
|
||||||
|
[SerializeField] private float maxAlpha = 1f;
|
||||||
|
|
||||||
|
private bool _isWaitingForTap;
|
||||||
|
private TweenBase _blinkTween;
|
||||||
|
|
||||||
|
internal override void OnManagedAwake()
|
||||||
|
{
|
||||||
|
base.OnManagedAwake();
|
||||||
|
|
||||||
|
// Validate references
|
||||||
|
if (fingerContainer == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[TapToStartController] Finger container not assigned!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fingerImage == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[TapToStartController] Finger image not assigned!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start hidden
|
||||||
|
if (fingerContainer != null)
|
||||||
|
{
|
||||||
|
fingerContainer.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activates the tap-to-start UI and begins waiting for player input.
|
||||||
|
/// Called by BirdPooperGameManager when scene is ready.
|
||||||
|
/// </summary>
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
if (_isWaitingForTap)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[TapToStartController] Already waiting for tap!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log("[TapToStartController] Activating tap-to-start...");
|
||||||
|
|
||||||
|
_isWaitingForTap = true;
|
||||||
|
|
||||||
|
// Show finger UI
|
||||||
|
if (fingerContainer != null)
|
||||||
|
{
|
||||||
|
fingerContainer.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start blinking animation using tween utility
|
||||||
|
if (fingerImage != null)
|
||||||
|
{
|
||||||
|
_blinkTween = TweenAnimationUtility.StartBlinkImage(fingerImage, minAlpha, maxAlpha, blinkDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register as high-priority input consumer to catch first tap
|
||||||
|
if (Input.InputManager.Instance != null)
|
||||||
|
{
|
||||||
|
Input.InputManager.Instance.SetDefaultConsumer(this);
|
||||||
|
Debug.Log("[TapToStartController] Registered as input consumer");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogError("[TapToStartController] InputManager instance not found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ITouchInputConsumer Implementation
|
||||||
|
|
||||||
|
public void OnTap(Vector2 tapPosition)
|
||||||
|
{
|
||||||
|
if (!_isWaitingForTap) return;
|
||||||
|
|
||||||
|
Debug.Log("[TapToStartController] First tap received! Starting game...");
|
||||||
|
|
||||||
|
// Stop waiting for tap
|
||||||
|
_isWaitingForTap = false;
|
||||||
|
|
||||||
|
// Stop blinking animation
|
||||||
|
if (_blinkTween != null)
|
||||||
|
{
|
||||||
|
_blinkTween.Stop();
|
||||||
|
_blinkTween = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide finger UI
|
||||||
|
if (fingerContainer != null)
|
||||||
|
{
|
||||||
|
fingerContainer.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister from input system
|
||||||
|
if (Input.InputManager.Instance != null)
|
||||||
|
{
|
||||||
|
Input.InputManager.Instance.SetDefaultConsumer(null);
|
||||||
|
Debug.Log("[TapToStartController] Unregistered from input");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell game manager to start the game (it will handle the initial flap)
|
||||||
|
BirdPooperGameManager.Instance.BeginMinigame();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnHoldStart(Vector2 position) { }
|
||||||
|
public void OnHoldMove(Vector2 position) { }
|
||||||
|
public void OnHoldEnd(Vector2 position) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
internal override void OnManagedDestroy()
|
||||||
|
{
|
||||||
|
// Stop blinking animation if active
|
||||||
|
if (_blinkTween != null)
|
||||||
|
{
|
||||||
|
_blinkTween.Stop();
|
||||||
|
_blinkTween = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister from input if still registered
|
||||||
|
if (_isWaitingForTap && Input.InputManager.Instance != null)
|
||||||
|
{
|
||||||
|
Input.InputManager.Instance.SetDefaultConsumer(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnManagedDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2a6ee5aca3ca423c82b57e16c0b2cca3
|
||||||
|
timeCreated: 1765922092
|
||||||
@@ -31,9 +31,9 @@ namespace Minigames.BirdPooper
|
|||||||
[Tooltip("Array of target prefabs to spawn randomly")]
|
[Tooltip("Array of target prefabs to spawn randomly")]
|
||||||
[SerializeField] private GameObject[] targetPrefabs;
|
[SerializeField] private GameObject[] targetPrefabs;
|
||||||
|
|
||||||
private IBirdPooperSettings settings;
|
private IBirdPooperSettings _settings;
|
||||||
private float spawnTimer;
|
private float _spawnTimer;
|
||||||
private bool isSpawning;
|
private bool _isSpawning;
|
||||||
private float _currentTargetInterval = 1f;
|
private float _currentTargetInterval = 1f;
|
||||||
|
|
||||||
[Header("Spawn Timing")]
|
[Header("Spawn Timing")]
|
||||||
@@ -42,16 +42,17 @@ namespace Minigames.BirdPooper
|
|||||||
[Tooltip("Maximum interval between target spawns (seconds)")]
|
[Tooltip("Maximum interval between target spawns (seconds)")]
|
||||||
[SerializeField] private float maxTargetSpawnInterval = 2f;
|
[SerializeField] private float maxTargetSpawnInterval = 2f;
|
||||||
|
|
||||||
internal override void OnManagedAwake()
|
/// <summary>
|
||||||
|
/// Initializes the target spawner by loading settings and validating references.
|
||||||
|
/// Should be called once before spawning begins.
|
||||||
|
/// </summary>
|
||||||
|
private void Initialize()
|
||||||
{
|
{
|
||||||
base.OnManagedAwake();
|
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||||
if (settings == null)
|
if (_settings == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("[TargetSpawner] BirdPooperSettings not found!");
|
Debug.LogWarning("[TargetSpawner] BirdPooperSettings not found! Using inspector intervals.");
|
||||||
// continue – we'll use inspector intervals
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate interval range
|
// Validate interval range
|
||||||
@@ -96,15 +97,15 @@ namespace Minigames.BirdPooper
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!isSpawning)
|
if (!_isSpawning)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
spawnTimer += Time.deltaTime;
|
_spawnTimer += Time.deltaTime;
|
||||||
|
|
||||||
if (spawnTimer >= _currentTargetInterval)
|
if (_spawnTimer >= _currentTargetInterval)
|
||||||
{
|
{
|
||||||
SpawnTarget();
|
SpawnTarget();
|
||||||
spawnTimer = 0f;
|
_spawnTimer = 0f;
|
||||||
// pick next random interval
|
// pick next random interval
|
||||||
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
||||||
}
|
}
|
||||||
@@ -167,15 +168,34 @@ namespace Minigames.BirdPooper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start spawning targets at regular intervals.
|
/// Start spawning targets.
|
||||||
|
/// Initializes the spawner if not already initialized, then begins spawning logic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void StartSpawning()
|
public void StartSpawning()
|
||||||
{
|
{
|
||||||
isSpawning = true;
|
// Initialize if not already done
|
||||||
spawnTimer = 0f;
|
if (_settings == null)
|
||||||
// choose initial interval
|
{
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the spawning process
|
||||||
|
BeginSpawningTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal method that handles the actual spawning startup logic.
|
||||||
|
/// Sets initial state and computes first interval.
|
||||||
|
/// </summary>
|
||||||
|
private void BeginSpawningTargets()
|
||||||
|
{
|
||||||
|
_isSpawning = true;
|
||||||
|
_spawnTimer = 0f;
|
||||||
|
|
||||||
|
// Choose initial interval
|
||||||
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
||||||
Debug.Log("[TargetSpawner] Started spawning targets");
|
|
||||||
|
Debug.Log($"[TargetSpawner] Started spawning targets with interval {_currentTargetInterval:F2}s");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -183,14 +203,14 @@ namespace Minigames.BirdPooper
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void StopSpawning()
|
public void StopSpawning()
|
||||||
{
|
{
|
||||||
isSpawning = false;
|
_isSpawning = false;
|
||||||
Debug.Log("[TargetSpawner] Stopped spawning targets");
|
Debug.Log("[TargetSpawner] Stopped spawning targets");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if spawner is currently spawning.
|
/// Check if spawner is currently spawning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsSpawning => isSpawning;
|
public bool IsSpawning => _isSpawning;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw gizmos to visualize spawn and despawn points in the editor.
|
/// Draw gizmos to visualize spawn and despawn points in the editor.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Pixelplacement;
|
using Pixelplacement;
|
||||||
using Pixelplacement.TweenSystem;
|
using Pixelplacement.TweenSystem;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -145,6 +145,52 @@ namespace Utils
|
|||||||
return Tween.CanvasGroupAlpha(canvasGroup, targetAlpha, duration, 0f, Tween.EaseInOut, completeCallback: onComplete);
|
return Tween.CanvasGroupAlpha(canvasGroup, targetAlpha, duration, 0f, Tween.EaseInOut, completeCallback: onComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fade Image alpha
|
||||||
|
/// </summary>
|
||||||
|
public static TweenBase FadeImageAlpha(UnityEngine.UI.Image image, float targetAlpha, float duration, Action onComplete = null)
|
||||||
|
{
|
||||||
|
return Tween.Value(image.color.a, targetAlpha, (alpha) =>
|
||||||
|
{
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
Color color = image.color;
|
||||||
|
color.a = alpha;
|
||||||
|
image.color = color;
|
||||||
|
}
|
||||||
|
}, duration, 0f, Tween.EaseInOut, completeCallback: onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start blinking animation on Image (ping-pong alpha fade)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">Image to blink</param>
|
||||||
|
/// <param name="minAlpha">Minimum alpha value</param>
|
||||||
|
/// <param name="maxAlpha">Maximum alpha value</param>
|
||||||
|
/// <param name="duration">Duration for one complete cycle (in and out)</param>
|
||||||
|
/// <returns>TweenBase that can be cancelled</returns>
|
||||||
|
public static TweenBase StartBlinkImage(UnityEngine.UI.Image image, float minAlpha = 0.3f, float maxAlpha = 1f, float duration = 1.5f)
|
||||||
|
{
|
||||||
|
// Set initial alpha to max
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
Color color = image.color;
|
||||||
|
color.a = maxAlpha;
|
||||||
|
image.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ping-pong tween (half duration for each direction)
|
||||||
|
return Tween.Value(maxAlpha, minAlpha, (alpha) =>
|
||||||
|
{
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
Color color = image.color;
|
||||||
|
color.a = alpha;
|
||||||
|
image.color = color;
|
||||||
|
}
|
||||||
|
}, duration / 2f, 0f, Tween.EaseInOut, Tween.LoopType.PingPong);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pop-out with fade - scale to 0 and fade out simultaneously
|
/// Pop-out with fade - scale to 0 and fade out simultaneously
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -20,7 +20,60 @@ MonoBehaviour:
|
|||||||
maxRotationAngle: 40
|
maxRotationAngle: 40
|
||||||
rotationSpeed: 18
|
rotationSpeed: 18
|
||||||
obstacleMoveSpeed: 7
|
obstacleMoveSpeed: 7
|
||||||
obstacleSpawnInterval: 0.1
|
obstacleSpawnConfiguration:
|
||||||
|
obstaclePools:
|
||||||
|
- obstacles:
|
||||||
|
- {fileID: 8855270423038321603, guid: 20ae02a8f50484045aaf3dcee33fb9a2, type: 3}
|
||||||
|
- {fileID: 2514399078413048981, guid: ee834e7efcf7d8749881f71f8b0da99c, type: 3}
|
||||||
|
- {fileID: 842802843766402460, guid: cdc806fd167bba3488797031a28657fa, type: 3}
|
||||||
|
- {fileID: 4239333156730914246, guid: 332d8cce2ed99054c83ecf84fbfa14c8, type: 3}
|
||||||
|
- {fileID: 6660502783540694524, guid: 5d42fc70e5838544ab654e30aa4b0c48, type: 3}
|
||||||
|
- {fileID: 2421410811796775077, guid: 371a09b68a5c0654bac9ba58ad3bcbe5, type: 3}
|
||||||
|
- obstacles:
|
||||||
|
- {fileID: 1408173265900928789, guid: 871373a85e5da0e4cafdf0e47496e105, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: d2998934362713545a040d7017a1bd36, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: 146d99398c0e7964dbed504e256adab7, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: dc8a19e9a4d30b44596237d915b3b73f, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: 471f367e14f9cfb4fb2c40d799d4c292, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: 5f1734c5705cdfd49ae3180d678d28b3, type: 3}
|
||||||
|
- obstacles:
|
||||||
|
- {fileID: 1408173265900928789, guid: 6bc84c3ea9854b54f85a8fb69c769790, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: 166c7e1bfcc4c854fab0af51cdfff746, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: 65810bfd58ebbaf4482527452258ae50, type: 3}
|
||||||
|
- {fileID: 1408173265900928789, guid: ae3986a7db087c845b618a9c897705ec, type: 3}
|
||||||
|
poolUnlockTimes:
|
||||||
|
- 20
|
||||||
|
- 40
|
||||||
|
minSpawnInterval: 1
|
||||||
|
maxSpawnInterval: 2
|
||||||
|
difficultyRampDuration: 60
|
||||||
|
difficultyCurve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve:
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0
|
||||||
|
value: 0
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 1
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 1
|
||||||
|
value: 1
|
||||||
|
inSlope: 1
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
m_PreInfinity: 2
|
||||||
|
m_PostInfinity: 2
|
||||||
|
m_RotationOrder: 4
|
||||||
|
intervalJitter: 0.05
|
||||||
|
recentDecayDuration: 10
|
||||||
|
minRecentWeight: 0.1
|
||||||
obstacleSpawnXPosition: 12
|
obstacleSpawnXPosition: 12
|
||||||
obstacleDestroyXPosition: -12
|
obstacleDestroyXPosition: -12
|
||||||
obstacleMinSpawnY: -3
|
obstacleMinSpawnY: -3
|
||||||
|
|||||||
@@ -28,18 +28,24 @@ MonoBehaviour:
|
|||||||
forceDeviation: 0.3
|
forceDeviation: 0.3
|
||||||
thinkTimeMin: 0.5
|
thinkTimeMin: 0.5
|
||||||
thinkTimeMax: 1
|
thinkTimeMax: 1
|
||||||
|
trashBagDetonationDistanceMin: 0.3
|
||||||
|
trashBagDetonationDistanceMax: 0.5
|
||||||
- difficulty: 1
|
- difficulty: 1
|
||||||
data:
|
data:
|
||||||
angleDeviation: 30
|
angleDeviation: 30
|
||||||
forceDeviation: 0.2
|
forceDeviation: 0.2
|
||||||
thinkTimeMin: 0.2
|
thinkTimeMin: 0.2
|
||||||
thinkTimeMax: 0.8
|
thinkTimeMax: 0.8
|
||||||
|
trashBagDetonationDistanceMin: 0.2
|
||||||
|
trashBagDetonationDistanceMax: 0.4
|
||||||
- difficulty: 2
|
- difficulty: 2
|
||||||
data:
|
data:
|
||||||
angleDeviation: 10
|
angleDeviation: 10
|
||||||
forceDeviation: 0.1
|
forceDeviation: 0.1
|
||||||
thinkTimeMin: 0.2
|
thinkTimeMin: 0.2
|
||||||
thinkTimeMax: 0.8
|
thinkTimeMax: 0.8
|
||||||
|
trashBagDetonationDistanceMin: 0.1
|
||||||
|
trashBagDetonationDistanceMax: 0.5
|
||||||
defaultAIDifficulty: 1
|
defaultAIDifficulty: 1
|
||||||
aiAllowedProjectiles: 000000000100000003000000
|
aiAllowedProjectiles: 000000000100000003000000
|
||||||
weakPointExplosionRadius: 6
|
weakPointExplosionRadius: 6
|
||||||
|
|||||||
Reference in New Issue
Block a user