convert game playing..
This commit is contained in:
@@ -45,7 +45,7 @@ TextureImporter:
|
|||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 1
|
spriteMode: 1
|
||||||
spriteExtrude: 1
|
spriteExtrude: 1
|
||||||
spriteMeshType: 1
|
spriteMeshType: 0
|
||||||
alignment: 0
|
alignment: 0
|
||||||
spritePivot: {x: 0.5, y: 0.5}
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
spritePixelsToUnits: 32
|
spritePixelsToUnits: 32
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: db153e77cef645c49a74705c9d542ea5, type: 3}
|
m_Script: {fileID: 11500000, guid: db153e77cef645c49a74705c9d542ea5, type: 3}
|
||||||
m_Name: CombatConstants
|
m_Name: CombatConstants
|
||||||
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.CombatConstants
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.CombatConstants
|
||||||
|
enableFrontalBumpSelfDamage: 0
|
||||||
FrontThreshold: 0.707
|
FrontThreshold: 0.707
|
||||||
BackThreshold: -0.5
|
BackThreshold: -0.5
|
||||||
PositionDamageMultiplier: 1.2
|
PositionDamageMultiplier: 1.2
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ MonoBehaviour:
|
|||||||
CameraShakeDuration: 0.12
|
CameraShakeDuration: 0.12
|
||||||
CameraShakePixels: 3
|
CameraShakePixels: 3
|
||||||
HeavyCameraShakePixels: 5
|
HeavyCameraShakePixels: 5
|
||||||
|
RearHitCameraPunchDuration: 0.07
|
||||||
|
RearHitCameraPunchPixels: 1
|
||||||
|
RearHitCameraPunchRearmDelay: 0.45
|
||||||
HeavyKnockbackThreshold: 1
|
HeavyKnockbackThreshold: 1
|
||||||
BlockedBumpHoldDuration: 0.08
|
BlockedBumpHoldDuration: 0.08
|
||||||
BlockedBumpFadeDuration: 0.04
|
BlockedBumpFadeDuration: 0.04
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c70ac61900f04f0fb18a608ef6f6c38c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 8a83cd68928e48f89b011bc92f0ca8e3, type: 3}
|
||||||
|
m_Name: DefenseConstants
|
||||||
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.DefenseConstants
|
||||||
|
InitialConstructionCurrency: 100
|
||||||
|
DirectTowerCost: 30
|
||||||
|
SummonTowerCost: 40
|
||||||
|
PortalRewardCurrency: 20
|
||||||
|
BaseMaximumHealth: 12
|
||||||
|
PortalEnemyCount: 8
|
||||||
|
ArrivalBaseDamage: 1
|
||||||
|
PortalSpawnInterval: 1.25
|
||||||
|
RouteSpeedMultiplier: 0.75
|
||||||
|
PostAttackProgressSeconds: 0.65
|
||||||
|
BuildInteractionRadius: 1.5
|
||||||
|
ArenaCenter: {x: 0, y: 0}
|
||||||
|
ArenaPlayableHalfExtents: {x: 11, y: 6}
|
||||||
|
PortalPositions:
|
||||||
|
- {x: -9, y: 3.25}
|
||||||
|
- {x: -9, y: -3.25}
|
||||||
|
BasePositions:
|
||||||
|
- {x: 9, y: 0}
|
||||||
|
- {x: 9, y: 0}
|
||||||
|
BuildPadPositions:
|
||||||
|
- {x: -3.25, y: 3.25}
|
||||||
|
- {x: 3.25, y: -3.25}
|
||||||
|
WaypointPositions:
|
||||||
|
- {x: -3, y: 3.25}
|
||||||
|
- {x: 3, y: 3.25}
|
||||||
|
- {x: -3, y: -3.25}
|
||||||
|
- {x: 3, y: -3.25}
|
||||||
|
PortalImmuneTowerKinds:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
PlayerSightRadius: 6
|
||||||
|
OutsideSightDamageEfficiency: 0.7
|
||||||
|
TowerRange: 5.5
|
||||||
|
DirectTowerDamage: 10
|
||||||
|
DirectTowerAttackInterval: 0.75
|
||||||
|
SummonDamage: 6
|
||||||
|
SummonAttackInterval: 0.9
|
||||||
|
SummonUnitCount: 2
|
||||||
|
SummonAreaRadius: 2.25
|
||||||
|
SummonUnitMoveSpeed: 2.5
|
||||||
|
SummonAttackRange: 1.2
|
||||||
|
PlayerReviveDelaySeconds: 5
|
||||||
|
PlayerReviveInvulnerabilitySeconds: 1
|
||||||
|
RewardPickupRadius: 1.35
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1949e4e0aabb41ff8e66ddd2ee493ba3
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 42
|
maxHealth: 42
|
||||||
moveSpeed: 1.1
|
moveSpeed: 1.375
|
||||||
attackDamage: 18
|
attackDamage: 18
|
||||||
warningDuration: 0.85
|
warningDuration: 0.85
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ MonoBehaviour:
|
|||||||
m_Name: Bat
|
m_Name: Bat
|
||||||
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Enemies.EnemyDefinition
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Enemies.EnemyDefinition
|
||||||
kind: 5
|
kind: 5
|
||||||
role: 1
|
role: 0
|
||||||
attackShape: 0
|
attackShape: 0
|
||||||
maxHealth: 8
|
maxHealth: 8
|
||||||
moveSpeed: 1.5
|
moveSpeed: 1.875
|
||||||
attackDamage: 8
|
attackDamage: 8
|
||||||
warningDuration: 0.35
|
warningDuration: 0.35
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
@@ -27,7 +27,7 @@ MonoBehaviour:
|
|||||||
attackWidth: 0
|
attackWidth: 0
|
||||||
attackRadius: 0.45
|
attackRadius: 0.45
|
||||||
attackAnimationLeadTime: 0
|
attackAnimationLeadTime: 0
|
||||||
experienceValue: 1
|
experienceValue: 2
|
||||||
attackPatterns:
|
attackPatterns:
|
||||||
- animationState: Attack
|
- animationState: Attack
|
||||||
attackShape: 0
|
attackShape: 0
|
||||||
@@ -42,3 +42,4 @@ MonoBehaviour:
|
|||||||
damageMultiplier: 1
|
damageMultiplier: 1
|
||||||
separationRadius: 0.55
|
separationRadius: 0.55
|
||||||
separationStrength: 0.6
|
separationStrength: 0.6
|
||||||
|
isFlying: 1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 2
|
attackShape: 2
|
||||||
maxHealth: 50
|
maxHealth: 50
|
||||||
moveSpeed: 0.9
|
moveSpeed: 1.125
|
||||||
attackDamage: 18
|
attackDamage: 18
|
||||||
warningDuration: 1.2
|
warningDuration: 1.2
|
||||||
activeDuration: 0.22
|
activeDuration: 0.22
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 40
|
maxHealth: 40
|
||||||
moveSpeed: 1.15
|
moveSpeed: 1.4375
|
||||||
attackDamage: 15
|
attackDamage: 15
|
||||||
warningDuration: 0.9
|
warningDuration: 0.9
|
||||||
activeDuration: 0.18
|
activeDuration: 0.18
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 60
|
maxHealth: 60
|
||||||
moveSpeed: 0.65
|
moveSpeed: 0.8125
|
||||||
attackDamage: 30
|
attackDamage: 30
|
||||||
warningDuration: 1.05
|
warningDuration: 1.05
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 12
|
maxHealth: 12
|
||||||
moveSpeed: 0.9
|
moveSpeed: 1.125
|
||||||
attackDamage: 10
|
attackDamage: 10
|
||||||
warningDuration: 1.5
|
warningDuration: 1.5
|
||||||
activeDuration: 0.375
|
activeDuration: 0.375
|
||||||
@@ -44,3 +44,4 @@ MonoBehaviour:
|
|||||||
damageMultiplier: 1
|
damageMultiplier: 1
|
||||||
separationRadius: 0.55
|
separationRadius: 0.55
|
||||||
separationStrength: 0.6
|
separationStrength: 0.6
|
||||||
|
isFlying: 1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 2
|
attackShape: 2
|
||||||
maxHealth: 60
|
maxHealth: 60
|
||||||
moveSpeed: 0.55
|
moveSpeed: 0.6875
|
||||||
attackDamage: 30
|
attackDamage: 30
|
||||||
warningDuration: 1.1
|
warningDuration: 1.1
|
||||||
activeDuration: 0.375
|
activeDuration: 0.375
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ MonoBehaviour:
|
|||||||
kind: 1
|
kind: 1
|
||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 10
|
maxHealth: 24
|
||||||
moveSpeed: 1.2
|
moveSpeed: 1.5
|
||||||
attackDamage: 12
|
attackDamage: 12
|
||||||
warningDuration: 0.7
|
warningDuration: 0.7
|
||||||
activeDuration: 0.15
|
activeDuration: 0.15
|
||||||
@@ -26,7 +26,7 @@ MonoBehaviour:
|
|||||||
attackLength: 0.8
|
attackLength: 0.8
|
||||||
attackWidth: 0.6
|
attackWidth: 0.6
|
||||||
attackRadius: 0
|
attackRadius: 0
|
||||||
experienceValue: 2
|
experienceValue: 3
|
||||||
attackPatterns:
|
attackPatterns:
|
||||||
- animationState: Attack
|
- animationState: Attack
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 1
|
attackShape: 1
|
||||||
maxHealth: 14
|
maxHealth: 14
|
||||||
moveSpeed: 0.9
|
moveSpeed: 1.125
|
||||||
attackDamage: 12
|
attackDamage: 12
|
||||||
warningDuration: 1
|
warningDuration: 1
|
||||||
activeDuration: 0.15
|
activeDuration: 0.15
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ MonoBehaviour:
|
|||||||
m_Name: Slime
|
m_Name: Slime
|
||||||
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Enemies.EnemyDefinition
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Enemies.EnemyDefinition
|
||||||
kind: 0
|
kind: 0
|
||||||
role: 1
|
role: 0
|
||||||
attackShape: 0
|
attackShape: 0
|
||||||
maxHealth: 10
|
maxHealth: 10
|
||||||
moveSpeed: 1.3
|
moveSpeed: 1.625
|
||||||
attackDamage: 10
|
attackDamage: 10
|
||||||
warningDuration: 0.35
|
warningDuration: 0.35
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
@@ -26,6 +26,6 @@ MonoBehaviour:
|
|||||||
attackLength: 0
|
attackLength: 0
|
||||||
attackWidth: 0
|
attackWidth: 0
|
||||||
attackRadius: 0.45
|
attackRadius: 0.45
|
||||||
experienceValue: 1
|
experienceValue: 2
|
||||||
separationRadius: 0.55
|
separationRadius: 0.55
|
||||||
separationStrength: 0.6
|
separationStrength: 0.6
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 3
|
attackShape: 3
|
||||||
maxHealth: 30
|
maxHealth: 30
|
||||||
moveSpeed: 1
|
moveSpeed: 1.25
|
||||||
attackDamage: 20
|
attackDamage: 20
|
||||||
warningDuration: 0.8
|
warningDuration: 0.8
|
||||||
activeDuration: 0.15
|
activeDuration: 0.15
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 2
|
attackShape: 2
|
||||||
maxHealth: 55
|
maxHealth: 55
|
||||||
moveSpeed: 0.8
|
moveSpeed: 1
|
||||||
attackDamage: 24
|
attackDamage: 24
|
||||||
warningDuration: 0.9
|
warningDuration: 0.9
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ MonoBehaviour:
|
|||||||
role: 0
|
role: 0
|
||||||
attackShape: 2
|
attackShape: 2
|
||||||
maxHealth: 38
|
maxHealth: 38
|
||||||
moveSpeed: 1.45
|
moveSpeed: 1.8125
|
||||||
attackDamage: 20
|
attackDamage: 20
|
||||||
warningDuration: 0.8
|
warningDuration: 0.8
|
||||||
activeDuration: 0.25
|
activeDuration: 0.25
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 2643d1587a2745d0b584eadd6b34978b, type: 3}
|
m_Script: {fileID: 11500000, guid: 2643d1587a2745d0b584eadd6b34978b, type: 3}
|
||||||
m_Name: PlayerConstants
|
m_Name: PlayerConstants
|
||||||
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.PlayerConstants
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.PlayerConstants
|
||||||
BaseMoveSpeed: 3
|
BaseMoveSpeed: 3.75
|
||||||
BaseMaxHealth: 100
|
BaseMaxHealth: 100
|
||||||
InvulnerabilityDuration: 1
|
InvulnerabilityDuration: 1
|
||||||
KnockbackDuration: 0.14
|
KnockbackDuration: 0.14
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b1a37a7f408649c8b975bca1bda14e86
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: f0f9bca102564c4395943d6b39c6d9f0, type: 3}
|
||||||
|
m_Name: PassiveRelicConstants
|
||||||
|
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.PassiveRelicConstants
|
||||||
|
TwinHornDoubleDamageChance: 0.15
|
||||||
|
ResonanceEchoChance: 0.08
|
||||||
|
ResonanceEchoCooldownSeconds: 3
|
||||||
|
ResonanceEchoDamageMultiplier: 0.5
|
||||||
|
RuptureSeedChance: 0.25
|
||||||
|
RuptureExplosionMaxHealthFraction: 0.1
|
||||||
|
RuptureExplosionPlayerBodyDiameters: 3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2c5773908a374e96ad132e07d5bcc421
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -20,3 +20,5 @@ MonoBehaviour:
|
|||||||
artifacts: {fileID: 11400000, guid: 619a1bb1a7bb416fbf1a4d0f4e4f66c5, type: 2}
|
artifacts: {fileID: 11400000, guid: 619a1bb1a7bb416fbf1a4d0f4e4f66c5, type: 2}
|
||||||
run: {fileID: 11400000, guid: af4f8662e3d84fbf8df4757fc7bfd22f, type: 2}
|
run: {fileID: 11400000, guid: af4f8662e3d84fbf8df4757fc7bfd22f, type: 2}
|
||||||
combatFeedback: {fileID: 11400000, guid: 6ab7e48e63b84ea3ac749bd827d1a87f, type: 2}
|
combatFeedback: {fileID: 11400000, guid: 6ab7e48e63b84ea3ac749bd827d1a87f, type: 2}
|
||||||
|
defense: {fileID: 11400000, guid: 1949e4e0aabb41ff8e66ddd2ee493ba3, type: 2}
|
||||||
|
passiveRelics: {fileID: 11400000, guid: 2c5773908a374e96ad132e07d5bcc421, type: 2}
|
||||||
|
|||||||
@@ -147,5 +147,13 @@ MonoBehaviour:
|
|||||||
ArenaPlayableHalfExtents: {x: 15, y: 8.4375}
|
ArenaPlayableHalfExtents: {x: 15, y: 8.4375}
|
||||||
ArenaPlayerPadding: {x: 0.5, y: 0.625}
|
ArenaPlayerPadding: {x: 0.5, y: 0.625}
|
||||||
ArenaCameraViewHalfExtents: {x: 10, y: 5.625}
|
ArenaCameraViewHalfExtents: {x: 10, y: 5.625}
|
||||||
|
StageCameraViewHalfExtents: {x: 7.5, y: 4.21875}
|
||||||
|
StageComparisonOriginalCameraViewHalfExtents: {x: 5, y: 2.8125}
|
||||||
|
StageComparisonCameraViewHalfExtents: {x: 7.5, y: 4.21875}
|
||||||
ArenaCameraDeadZone: {x: 0.5, y: 0.3}
|
ArenaCameraDeadZone: {x: 0.5, y: 0.3}
|
||||||
ArenaCameraSmoothTime: 0.15
|
ArenaCameraSmoothTime: 0.15
|
||||||
|
Stage1FirstCombatRoomScaleComparison: 0
|
||||||
|
StagePlayerVisibilityRadius: 6
|
||||||
|
StageExploredFogOpacity: 0.65
|
||||||
|
StageUnseenFogOpacity: 1
|
||||||
|
StageEncounterSightRangeMultiplier: 0.5
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ CircleCollider2D:
|
|||||||
m_GameObject: {fileID: 810786416157290827}
|
m_GameObject: {fileID: 810786416157290827}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_IsTrigger: 1
|
m_IsTrigger: 0
|
||||||
m_Offset: {x: 0, y: -0.28}
|
m_Offset: {x: 0, y: -0.28}
|
||||||
m_Radius: 0.22
|
m_Radius: 0.22
|
||||||
--- !u!114 &3111862855908660951
|
--- !u!114 &3111862855908660951
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ CircleCollider2D:
|
|||||||
m_CallbackLayers:
|
m_CallbackLayers:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 4294967295
|
m_Bits: 4294967295
|
||||||
m_IsTrigger: 1
|
m_IsTrigger: 0
|
||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_CompositeOperation: 0
|
m_CompositeOperation: 0
|
||||||
m_CompositeOrder: 0
|
m_CompositeOrder: 0
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ CircleCollider2D:
|
|||||||
m_UsedByEffector: 0
|
m_UsedByEffector: 0
|
||||||
m_CompositeOperation: 0
|
m_CompositeOperation: 0
|
||||||
m_CompositeOrder: 0
|
m_CompositeOrder: 0
|
||||||
m_Offset: {x: 0, y: -0.28}
|
m_Offset: {x: 0, y: -0.06}
|
||||||
m_Radius: 0.22
|
m_Radius: 0.22
|
||||||
--- !u!114 &6788187778577634304
|
--- !u!114 &6788187778577634304
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace BumpCombat.Combat
|
|||||||
public sealed class ArtifactStatusVisual : MonoBehaviour
|
public sealed class ArtifactStatusVisual : MonoBehaviour
|
||||||
{
|
{
|
||||||
public const int StatusSortingOffset = 1;
|
public const int StatusSortingOffset = 1;
|
||||||
|
public const float StatusVisualAnchorOffsetY = -13f / 32f;
|
||||||
|
|
||||||
private const int FrameCount = 6;
|
private const int FrameCount = 6;
|
||||||
private const float PixelsPerUnit = 32f;
|
private const float PixelsPerUnit = 32f;
|
||||||
@@ -213,14 +214,23 @@ namespace BumpCombat.Combat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector2 groundAnchor = enemy.GroundAnchorPosition;
|
Transform visualTransform = enemy.CurrentVisualTransform;
|
||||||
slot.Root.transform.SetPositionAndRotation(
|
if (slot.Root.transform.parent != visualTransform)
|
||||||
new Vector3(groundAnchor.x, groundAnchor.y, transform.position.z),
|
{
|
||||||
Quaternion.identity);
|
slot.Root.transform.SetParent(visualTransform, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
float anchorOffsetY = sourceRenderer.flipY
|
||||||
|
? -StatusVisualAnchorOffsetY
|
||||||
|
: StatusVisualAnchorOffsetY;
|
||||||
|
slot.Root.transform.localPosition =
|
||||||
|
new Vector3(0f, anchorOffsetY, 0f);
|
||||||
|
slot.Root.transform.localRotation = Quaternion.identity;
|
||||||
slot.Root.transform.localScale = Vector3.one;
|
slot.Root.transform.localScale = Vector3.one;
|
||||||
slot.Renderer.sortingLayerID = sourceRenderer.sortingLayerID;
|
slot.Renderer.sortingLayerID = sourceRenderer.sortingLayerID;
|
||||||
slot.Renderer.sortingOrder =
|
slot.Renderer.sortingOrder =
|
||||||
sourceRenderer.sortingOrder + StatusSortingOffset;
|
enemy.CurrentVisualSortingOrder + StatusSortingOffset;
|
||||||
|
slot.Renderer.forceRenderingOff = sourceRenderer.forceRenderingOff;
|
||||||
slot.Renderer.flipX = sourceRenderer.flipX;
|
slot.Renderer.flipX = sourceRenderer.flipX;
|
||||||
slot.Renderer.flipY = sourceRenderer.flipY;
|
slot.Renderer.flipY = sourceRenderer.flipY;
|
||||||
slot.Renderer.sharedMaterial = sourceRenderer.sharedMaterial;
|
slot.Renderer.sharedMaterial = sourceRenderer.sharedMaterial;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using BumpCombat.Core;
|
|||||||
using BumpCombat.Constants;
|
using BumpCombat.Constants;
|
||||||
using BumpCombat.Enemies;
|
using BumpCombat.Enemies;
|
||||||
using BumpCombat.Player;
|
using BumpCombat.Player;
|
||||||
|
using BumpCombat.Progression;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace BumpCombat.Combat
|
namespace BumpCombat.Combat
|
||||||
@@ -12,6 +13,7 @@ namespace BumpCombat.Combat
|
|||||||
public sealed class BumpCombatResolver : MonoBehaviour
|
public sealed class BumpCombatResolver : MonoBehaviour
|
||||||
{
|
{
|
||||||
private float baseDamage => GameplayConstants.Current.Combat.BaseDamage;
|
private float baseDamage => GameplayConstants.Current.Combat.BaseDamage;
|
||||||
|
private bool enableFrontalBumpSelfDamage => GameplayConstants.Current.Combat.EnableFrontalBumpSelfDamage;
|
||||||
private float dashDamage => GameplayConstants.Current.Combat.DashDamage;
|
private float dashDamage => GameplayConstants.Current.Combat.DashDamage;
|
||||||
private float strongDashDamage => GameplayConstants.Current.Combat.StrongDashDamage;
|
private float strongDashDamage => GameplayConstants.Current.Combat.StrongDashDamage;
|
||||||
private float minimumAttackSpeed => GameplayConstants.Current.Combat.MinimumAttackSpeed;
|
private float minimumAttackSpeed => GameplayConstants.Current.Combat.MinimumAttackSpeed;
|
||||||
@@ -35,6 +37,7 @@ namespace BumpCombat.Combat
|
|||||||
private readonly Dictionary<int, float> nextNormalHitTimes = new();
|
private readonly Dictionary<int, float> nextNormalHitTimes = new();
|
||||||
|
|
||||||
private PlayerStats playerStats;
|
private PlayerStats playerStats;
|
||||||
|
private PlayerHealth playerHealth;
|
||||||
private PlayerController playerController;
|
private PlayerController playerController;
|
||||||
private DashController dashController;
|
private DashController dashController;
|
||||||
private ActiveArtifactController activeArtifactController;
|
private ActiveArtifactController activeArtifactController;
|
||||||
@@ -43,6 +46,7 @@ namespace BumpCombat.Combat
|
|||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
playerStats = GetComponent<PlayerStats>();
|
playerStats = GetComponent<PlayerStats>();
|
||||||
|
playerHealth = GetComponent<PlayerHealth>();
|
||||||
playerController = GetComponent<PlayerController>();
|
playerController = GetComponent<PlayerController>();
|
||||||
dashController = GetComponent<DashController>();
|
dashController = GetComponent<DashController>();
|
||||||
activeArtifactController = GetComponent<ActiveArtifactController>();
|
activeArtifactController = GetComponent<ActiveArtifactController>();
|
||||||
@@ -335,6 +339,10 @@ namespace BumpCombat.Combat
|
|||||||
playerStats.CalculateDamage(rawDamage, damageTag),
|
playerStats.CalculateDamage(rawDamage, damageTag),
|
||||||
side,
|
side,
|
||||||
isOffsetHit);
|
isOffsetHit);
|
||||||
|
finalDamage = PassiveRelicController.Instance?.ApplyOrdinaryBumpDamageMultiplier(
|
||||||
|
finalDamage,
|
||||||
|
!isDash && string.IsNullOrEmpty(sourceId),
|
||||||
|
artifactColor.HasValue) ?? finalDamage;
|
||||||
Vector2 knockbackDirection = -enemyToPlayer;
|
Vector2 knockbackDirection = -enemyToPlayer;
|
||||||
bool suppressOrdinaryEventReaction = !isDash && isEventEnemy;
|
bool suppressOrdinaryEventReaction = !isDash && isEventEnemy;
|
||||||
float appliedKnockback = knockbackOverride
|
float appliedKnockback = knockbackOverride
|
||||||
@@ -361,7 +369,8 @@ namespace BumpCombat.Combat
|
|||||||
appliedKnockback,
|
appliedKnockback,
|
||||||
artifactColor.Value,
|
artifactColor.Value,
|
||||||
castIdentity,
|
castIdentity,
|
||||||
!suppressOrdinaryEventReaction);
|
!suppressOrdinaryEventReaction,
|
||||||
|
EnemyDamageSource.Player);
|
||||||
acceptedHit = artifactHitResult
|
acceptedHit = artifactHitResult
|
||||||
!= EnemyArtifactHitResult.Rejected;
|
!= EnemyArtifactHitResult.Rejected;
|
||||||
}
|
}
|
||||||
@@ -372,7 +381,8 @@ namespace BumpCombat.Combat
|
|||||||
knockbackDirection,
|
knockbackDirection,
|
||||||
appliedKnockback,
|
appliedKnockback,
|
||||||
!suppressOrdinaryEventReaction,
|
!suppressOrdinaryEventReaction,
|
||||||
!isDash);
|
!isDash,
|
||||||
|
EnemyDamageSource.Player);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acceptedHit)
|
if (!acceptedHit)
|
||||||
@@ -477,14 +487,22 @@ namespace BumpCombat.Combat
|
|||||||
|
|
||||||
if (!isDash && !protectionBegan)
|
if (!isDash && !protectionBegan)
|
||||||
{
|
{
|
||||||
if (!enemy.IsCrowd
|
if (enableFrontalBumpSelfDamage
|
||||||
|
&& !artifactColor.HasValue
|
||||||
|
&& side == HitSide.Front)
|
||||||
|
{
|
||||||
|
playerHealth?.TryTakeDamage(
|
||||||
|
enemy.ContactDamage,
|
||||||
|
enemyToPlayer,
|
||||||
|
enemy.PlayerKnockbackDistance);
|
||||||
|
}
|
||||||
|
else if (side != HitSide.Front
|
||||||
|
&& !enemy.IsCrowd
|
||||||
&& BumpCombatMath.AppliesPlayerImpactRecoil(side, isEventEnemy))
|
&& BumpCombatMath.AppliesPlayerImpactRecoil(side, isEventEnemy))
|
||||||
{
|
{
|
||||||
playerController.ApplyImpactRecoil(
|
playerController.ApplyImpactRecoil(
|
||||||
enemyToPlayer,
|
enemyToPlayer,
|
||||||
side == HitSide.Front
|
side == HitSide.Back && isEventEnemy
|
||||||
? frontPlayerImpactRecoilDistance
|
|
||||||
: side == HitSide.Back && isEventEnemy
|
|
||||||
? eventBackPlayerImpactRecoilDistance
|
? eventBackPlayerImpactRecoilDistance
|
||||||
: playerImpactRecoilDistance);
|
: playerImpactRecoilDistance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace BumpCombat.Combat
|
|||||||
private float cameraShakeDuration => GameplayConstants.Current.CombatFeedback.CameraShakeDuration;
|
private float cameraShakeDuration => GameplayConstants.Current.CombatFeedback.CameraShakeDuration;
|
||||||
private float cameraShakePixels => GameplayConstants.Current.CombatFeedback.CameraShakePixels;
|
private float cameraShakePixels => GameplayConstants.Current.CombatFeedback.CameraShakePixels;
|
||||||
private float heavyCameraShakePixels => GameplayConstants.Current.CombatFeedback.HeavyCameraShakePixels;
|
private float heavyCameraShakePixels => GameplayConstants.Current.CombatFeedback.HeavyCameraShakePixels;
|
||||||
|
private float rearHitCameraPunchDuration => GameplayConstants.Current.CombatFeedback.RearHitCameraPunchDuration;
|
||||||
|
private float rearHitCameraPunchPixels => GameplayConstants.Current.CombatFeedback.RearHitCameraPunchPixels;
|
||||||
|
private float rearHitCameraPunchRearmDelay => GameplayConstants.Current.CombatFeedback.RearHitCameraPunchRearmDelay;
|
||||||
private float heavyKnockbackThreshold => GameplayConstants.Current.CombatFeedback.HeavyKnockbackThreshold;
|
private float heavyKnockbackThreshold => GameplayConstants.Current.CombatFeedback.HeavyKnockbackThreshold;
|
||||||
[SerializeField, Min(1f)] private float assetsPixelsPerUnit = 32f;
|
[SerializeField, Min(1f)] private float assetsPixelsPerUnit = 32f;
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@ namespace BumpCombat.Combat
|
|||||||
private float flashUntil;
|
private float flashUntil;
|
||||||
private float blinkUntil;
|
private float blinkUntil;
|
||||||
private Vector3 cameraRestPosition;
|
private Vector3 cameraRestPosition;
|
||||||
|
private float lastRearHitCameraPunchAt = float.NegativeInfinity;
|
||||||
private Material effectMaterial;
|
private Material effectMaterial;
|
||||||
private Vector3 lastCycloneSamplePosition;
|
private Vector3 lastCycloneSamplePosition;
|
||||||
private Vector2 lastDashAfterimagePosition;
|
private Vector2 lastDashAfterimagePosition;
|
||||||
@@ -309,6 +313,7 @@ namespace BumpCombat.Combat
|
|||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
activeInstance = this;
|
activeInstance = this;
|
||||||
|
lastRearHitCameraPunchAt = float.NegativeInfinity;
|
||||||
CombatEvents.OnValidHit += HandleValidHit;
|
CombatEvents.OnValidHit += HandleValidHit;
|
||||||
CombatEvents.OnStunApplied += HandleStunApplied;
|
CombatEvents.OnStunApplied += HandleStunApplied;
|
||||||
CombatEvents.OnAttackCancelled += HandleAttackCancelled;
|
CombatEvents.OnAttackCancelled += HandleAttackCancelled;
|
||||||
@@ -1354,6 +1359,19 @@ namespace BumpCombat.Combat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.IsOrdinaryBump && result.HasBackBonus)
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
bool rearHitCameraPunchRearmed =
|
||||||
|
now - lastRearHitCameraPunchAt >= rearHitCameraPunchRearmDelay;
|
||||||
|
lastRearHitCameraPunchAt = now;
|
||||||
|
|
||||||
|
if (rearHitCameraPunchRearmed)
|
||||||
|
{
|
||||||
|
RequestRearHitCameraPunch(result.KnockbackDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Color color;
|
Color color;
|
||||||
float strength;
|
float strength;
|
||||||
int rayCount;
|
int rayCount;
|
||||||
@@ -2203,6 +2221,63 @@ namespace BumpCombat.Combat
|
|||||||
playerVisualCoroutine = null;
|
playerVisualCoroutine = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RequestRearHitCameraPunch(Vector2 direction)
|
||||||
|
{
|
||||||
|
if (worldCamera == null ||
|
||||||
|
rearHitCameraPunchDuration <= 0f ||
|
||||||
|
rearHitCameraPunchPixels <= 0f ||
|
||||||
|
rearHitCameraPunchRearmDelay < 0f ||
|
||||||
|
assetsPixelsPerUnit <= 0f ||
|
||||||
|
direction.sqrMagnitude <= Mathf.Epsilon ||
|
||||||
|
cameraShakeCoroutine != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraRestPosition = worldCamera.transform.position;
|
||||||
|
cameraShakeCoroutine = StartCoroutine(
|
||||||
|
RearHitCameraPunchRoutine(direction.normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator RearHitCameraPunchRoutine(Vector2 normalizedDirection)
|
||||||
|
{
|
||||||
|
float startTime = Time.unscaledTime;
|
||||||
|
|
||||||
|
while (Time.unscaledTime - startTime < rearHitCameraPunchDuration)
|
||||||
|
{
|
||||||
|
float t =
|
||||||
|
(Time.unscaledTime - startTime) / rearHitCameraPunchDuration;
|
||||||
|
Vector2 pixelOffset =
|
||||||
|
normalizedDirection * (rearHitCameraPunchPixels * (1f - t));
|
||||||
|
Vector2 offset = new Vector2(
|
||||||
|
Mathf.Round(pixelOffset.x) / assetsPixelsPerUnit,
|
||||||
|
Mathf.Round(pixelOffset.y) / assetsPixelsPerUnit);
|
||||||
|
|
||||||
|
if (cameraFollow != null)
|
||||||
|
{
|
||||||
|
cameraFollow.SetExternalShakeOffset(offset);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
worldCamera.transform.position =
|
||||||
|
cameraRestPosition + new Vector3(offset.x, offset.y, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraFollow != null)
|
||||||
|
{
|
||||||
|
cameraFollow.SetExternalShakeOffset(Vector2.zero);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
worldCamera.transform.position = cameraRestPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraShakeCoroutine = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void RequestCameraShake(float pixels)
|
private void RequestCameraShake(float pixels)
|
||||||
{
|
{
|
||||||
if (worldCamera == null || pixels <= 0f || cameraShakeDuration <= 0f)
|
if (worldCamera == null || pixels <= 0f || cameraShakeDuration <= 0f)
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ namespace BumpCombat.Constants
|
|||||||
[CreateAssetMenu(menuName = "BumpCombat/Constants/Combat")]
|
[CreateAssetMenu(menuName = "BumpCombat/Constants/Combat")]
|
||||||
public sealed class CombatConstants : ScriptableObject
|
public sealed class CombatConstants : ScriptableObject
|
||||||
{
|
{
|
||||||
|
[Header("정면 몸통박치기 접촉 피해")]
|
||||||
|
[SerializeField, Tooltip("OFF: 정면 몸통박치기 자체 피해 없음. ON: 이전 enemy.ContactDamage를 PlayerHealth.TryTakeDamage로 전달해 일반 무적·가드·피격 넉백을 적용하며 EnemyAttack는 영향 없음.")]
|
||||||
|
private bool enableFrontalBumpSelfDamage;
|
||||||
|
|
||||||
|
public bool EnableFrontalBumpSelfDamage
|
||||||
|
{
|
||||||
|
get => enableFrontalBumpSelfDamage;
|
||||||
|
set => enableFrontalBumpSelfDamage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Header("몸통박치기 방향 판정")]
|
[Header("몸통박치기 방향 판정")]
|
||||||
[Tooltip("적 정면으로 분류하는 방향 내적 기준"), Range(-1f, 1f)] public float FrontThreshold = 0.707f;
|
[Tooltip("적 정면으로 분류하는 방향 내적 기준"), Range(-1f, 1f)] public float FrontThreshold = 0.707f;
|
||||||
[Tooltip("적 후방으로 분류하는 방향 내적 기준"), Range(-1f, 1f)] public float BackThreshold = -0.5f;
|
[Tooltip("적 후방으로 분류하는 방향 내적 기준"), Range(-1f, 1f)] public float BackThreshold = -0.5f;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ namespace BumpCombat.Constants
|
|||||||
[Tooltip("흔들림 지속시간 (초)"), Min(0f)] public float CameraShakeDuration = 0.12f;
|
[Tooltip("흔들림 지속시간 (초)"), Min(0f)] public float CameraShakeDuration = 0.12f;
|
||||||
[Tooltip("일반 흔들림 크기 (픽셀)"), Min(0f)] public float CameraShakePixels = 3f;
|
[Tooltip("일반 흔들림 크기 (픽셀)"), Min(0f)] public float CameraShakePixels = 3f;
|
||||||
[Tooltip("강한 흔들림 크기 (픽셀)"), Min(0f)] public float HeavyCameraShakePixels = 5f;
|
[Tooltip("강한 흔들림 크기 (픽셀)"), Min(0f)] public float HeavyCameraShakePixels = 5f;
|
||||||
|
public float RearHitCameraPunchDuration = 0.07f;
|
||||||
|
public float RearHitCameraPunchPixels = 1f;
|
||||||
|
public float RearHitCameraPunchRearmDelay = 0.45f;
|
||||||
[Tooltip("강한 피격으로 표시하는 넉백 거리 (유닛)"), Min(0f)] public float HeavyKnockbackThreshold = 1f;
|
[Tooltip("강한 피격으로 표시하는 넉백 거리 (유닛)"), Min(0f)] public float HeavyKnockbackThreshold = 1f;
|
||||||
|
|
||||||
[Header("무적 적 몸통박치기 표시")]
|
[Header("무적 적 몸통박치기 표시")]
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using BumpCombat.Defense;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Constants
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(menuName = "BumpCombat/Constants/Defense Prototype")]
|
||||||
|
public sealed class DefenseConstants : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Prototype economy")]
|
||||||
|
[Tooltip("Initial construction currency for the test mode.")]
|
||||||
|
[Min(0)] public int InitialConstructionCurrency = 100;
|
||||||
|
[Min(0)] public int DirectTowerCost = 30;
|
||||||
|
[Min(0)] public int SummonTowerCost = 40;
|
||||||
|
[Min(0)] public int PortalRewardCurrency = 20;
|
||||||
|
|
||||||
|
[Min(0.1f)] public float BuildInteractionRadius = 1.5f;
|
||||||
|
[Header("Prototype arena")]
|
||||||
|
public Vector2 ArenaCenter = Vector2.zero;
|
||||||
|
public Vector2 ArenaPlayableHalfExtents = new(11f, 6f);
|
||||||
|
|
||||||
|
[Header("Prototype bases and routes")]
|
||||||
|
[Min(1)] public int BaseMaximumHealth = 12;
|
||||||
|
[Min(1)] public int PortalEnemyCount = 8;
|
||||||
|
[Min(0)] public int ArrivalBaseDamage = 1;
|
||||||
|
[Min(0.1f)] public float PortalSpawnInterval = 1.25f;
|
||||||
|
[Min(0.1f)] public float RouteSpeedMultiplier = 0.75f;
|
||||||
|
[Min(0f)] public float PostAttackProgressSeconds = 0.65f;
|
||||||
|
public Vector2[] PortalPositions =
|
||||||
|
{
|
||||||
|
new(-9f, 3.25f),
|
||||||
|
new(-9f, -3.25f),
|
||||||
|
};
|
||||||
|
public Vector2[] BasePositions =
|
||||||
|
{
|
||||||
|
new(9f, 0f),
|
||||||
|
new(9f, 0f),
|
||||||
|
};
|
||||||
|
public Vector2[] BuildPadPositions =
|
||||||
|
{
|
||||||
|
new(-3.25f, 3.25f),
|
||||||
|
new(3.25f, -3.25f),
|
||||||
|
};
|
||||||
|
[Tooltip("Two ordered waypoints per lane, indexed by lane * 2 + waypoint.")]
|
||||||
|
public Vector2[] WaypointPositions =
|
||||||
|
{
|
||||||
|
new(-3f, 3.25f), new(3f, 3.25f),
|
||||||
|
new(-3f, -3.25f), new(3f, -3.25f),
|
||||||
|
};
|
||||||
|
[Tooltip("The tower source kind ignored by each portal's enemies.")]
|
||||||
|
public DefenseTowerKind[] PortalImmuneTowerKinds =
|
||||||
|
{
|
||||||
|
DefenseTowerKind.Direct,
|
||||||
|
DefenseTowerKind.Summon,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Header("Prototype tower sight and damage")]
|
||||||
|
[Min(0.1f)] public float PlayerSightRadius = 6f;
|
||||||
|
[Range(0f, 1f)] public float OutsideSightDamageEfficiency = 0.7f;
|
||||||
|
[Min(0.1f)] public float TowerRange = 5.5f;
|
||||||
|
[Min(0.1f)] public float DirectTowerDamage = 10f;
|
||||||
|
[Min(0.05f)] public float DirectTowerAttackInterval = 0.75f;
|
||||||
|
[Min(0.1f)] public float SummonDamage = 6f;
|
||||||
|
[Min(0.05f)] public float SummonAttackInterval = 0.9f;
|
||||||
|
[Min(1)] public int SummonUnitCount = 2;
|
||||||
|
[Min(0.1f)] public float SummonAreaRadius = 2.25f;
|
||||||
|
[Min(0.1f)] public float SummonUnitMoveSpeed = 2.5f;
|
||||||
|
[Min(0.1f)] public float SummonAttackRange = 1.2f;
|
||||||
|
|
||||||
|
[Header("Prototype player recovery")]
|
||||||
|
[Min(0f)] public float PlayerReviveDelaySeconds = 5f;
|
||||||
|
[Min(0f)] public float PlayerReviveInvulnerabilitySeconds = 1f;
|
||||||
|
[Min(0.1f)] public float RewardPickupRadius = 1.35f;
|
||||||
|
|
||||||
|
public Vector2 GetPortalPosition(int lane)
|
||||||
|
{
|
||||||
|
return GetPosition(PortalPositions, lane, new Vector2(-9f, lane == 0 ? 3.25f : -3.25f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetBasePosition(int lane)
|
||||||
|
{
|
||||||
|
return GetPosition(BasePositions, lane, new Vector2(9f, 0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetBuildPadPosition(int index)
|
||||||
|
{
|
||||||
|
return GetPosition(BuildPadPositions, index, Vector2.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetWaypointPosition(int lane, int waypointIndex)
|
||||||
|
{
|
||||||
|
int index = lane * 2 + waypointIndex;
|
||||||
|
Vector2 fallback = new(
|
||||||
|
waypointIndex == 0 ? -3f : 3f,
|
||||||
|
lane == 0 ? 3.25f : -3.25f);
|
||||||
|
return GetPosition(WaypointPositions, index, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefenseTowerKind GetPortalImmuneTowerKind(int lane)
|
||||||
|
{
|
||||||
|
if (PortalImmuneTowerKinds != null
|
||||||
|
&& lane >= 0
|
||||||
|
&& lane < PortalImmuneTowerKinds.Length)
|
||||||
|
{
|
||||||
|
return PortalImmuneTowerKinds[lane];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (DefenseTowerKind)Mathf.Clamp(lane, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 GetPosition(Vector2[] positions, int index, Vector2 fallback)
|
||||||
|
{
|
||||||
|
return positions != null && index >= 0 && index < positions.Length
|
||||||
|
? positions[index]
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a83cd68928e48f89b011bc92f0ca8e3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -21,6 +21,17 @@ namespace BumpCombat.Constants
|
|||||||
[Tooltip("군중형 접근 기준점의 가장 안쪽 반경 (유닛)"), Min(0f)] public float CrowdApproachBaseRadius = 0.75f;
|
[Tooltip("군중형 접근 기준점의 가장 안쪽 반경 (유닛)"), Min(0f)] public float CrowdApproachBaseRadius = 0.75f;
|
||||||
[Tooltip("군중형 접근 기준점 동심원 간 간격 (유닛)"), Min(0f)] public float CrowdApproachRingSpacing = 0.15f;
|
[Tooltip("군중형 접근 기준점 동심원 간 간격 (유닛)"), Min(0f)] public float CrowdApproachRingSpacing = 0.15f;
|
||||||
[Min(0f)] public float SummonedEnemyNoAnimationLockDuration = 0.15f;
|
[Min(0f)] public float SummonedEnemyNoAnimationLockDuration = 0.15f;
|
||||||
|
[Tooltip("시야 장애물 차폐를 무시하고 무조건 감지하는 초근접 반경 (유닛)"), Min(0f)] public float ProximityAlertRange = 2.0f;
|
||||||
|
[Tooltip("적의 기본 시야 사거리 (유닛)"), Min(0f)] public float DefaultSightRange = 8.0f;
|
||||||
|
[Tooltip("일반 적이 시야를 잃은 뒤 마지막 위치를 기억하는 시간 (초)"), Min(0f)] public float LostSightMemoryDuration = 1.5f;
|
||||||
|
[Tooltip("비탐지 일반 적의 로밍 반경 최솟값 (유닛)"), Min(0f)] public float RoamRadiusMin = 2.5f;
|
||||||
|
[Tooltip("비탐지 일반 적의 로밍 반경 최댓값 (유닛)"), Min(0f)] public float RoamRadiusMax = 4.0f;
|
||||||
|
[Tooltip("로밍 지점 도착 후 최소 대기시간 (초)"), Min(0f)] public float RoamPauseMin = 0.4f;
|
||||||
|
[Tooltip("로밍 지점 도착 후 최대 대기시간 (초)"), Min(0f)] public float RoamPauseMax = 1.0f;
|
||||||
|
[Tooltip("EncounterGroup 내부 적의 최소 초기 간격 (유닛)"), Min(0f)] public float EncounterGroupMinSpacing = 1.5f;
|
||||||
|
[Tooltip("EncounterGroup 내부 적의 최대 초기 간격 (유닛)"), Min(0f)] public float EncounterGroupMaxSpacing = 2.5f;
|
||||||
|
[Tooltip("장애물 회피(Wall Slide) 원형 캐스트 반경"), Min(0.05f)] public float WallSlideCastRadius = 0.35f;
|
||||||
|
[Tooltip("장애물 회피(Wall Slide) 원형 캐스트 거리"), Min(0.05f)] public float WallSlideCastDistance = 0.4f;
|
||||||
|
|
||||||
[Header("적과 플레이어 사이 거리")]
|
[Header("적과 플레이어 사이 거리")]
|
||||||
[Tooltip("일반 적이 주는 플레이어 넉백 거리 (유닛)"), Min(0f)] public float NormalPlayerKnockbackDistance = 0.75f;
|
[Tooltip("일반 적이 주는 플레이어 넉백 거리 (유닛)"), Min(0f)] public float NormalPlayerKnockbackDistance = 0.75f;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace BumpCombat.Constants
|
|||||||
private static ArtifactConstants fallbackArtifacts;
|
private static ArtifactConstants fallbackArtifacts;
|
||||||
private static RunConstants fallbackRun;
|
private static RunConstants fallbackRun;
|
||||||
private static CombatFeedbackSettings fallbackCombatFeedback;
|
private static CombatFeedbackSettings fallbackCombatFeedback;
|
||||||
|
private static DefenseConstants fallbackDefense;
|
||||||
|
private static PassiveRelicConstants fallbackPassiveRelics;
|
||||||
|
|
||||||
[Header("설정 분류")]
|
[Header("설정 분류")]
|
||||||
[SerializeField, Tooltip("플레이어 기본 능력치, 피격, 가드 설정")]
|
[SerializeField, Tooltip("플레이어 기본 능력치, 피격, 가드 설정")]
|
||||||
@@ -33,6 +35,10 @@ namespace BumpCombat.Constants
|
|||||||
private RunConstants run;
|
private RunConstants run;
|
||||||
[SerializeField, Tooltip("타격, 피격, 가드 효과의 표시 설정")]
|
[SerializeField, Tooltip("타격, 피격, 가드 효과의 표시 설정")]
|
||||||
private CombatFeedbackSettings combatFeedback;
|
private CombatFeedbackSettings combatFeedback;
|
||||||
|
[SerializeField, Tooltip("역병형 방어전 테스트 모드 설정")]
|
||||||
|
private DefenseConstants defense;
|
||||||
|
[SerializeField, Tooltip("방어전 패시브 유물 효과 설정")]
|
||||||
|
private PassiveRelicConstants passiveRelics;
|
||||||
|
|
||||||
public static GameplayConstants Current
|
public static GameplayConstants Current
|
||||||
{
|
{
|
||||||
@@ -60,6 +66,8 @@ namespace BumpCombat.Constants
|
|||||||
public RunConstants Run => Resolve(run, ref fallbackRun);
|
public RunConstants Run => Resolve(run, ref fallbackRun);
|
||||||
public CombatFeedbackSettings CombatFeedback =>
|
public CombatFeedbackSettings CombatFeedback =>
|
||||||
Resolve(combatFeedback, ref fallbackCombatFeedback);
|
Resolve(combatFeedback, ref fallbackCombatFeedback);
|
||||||
|
public DefenseConstants Defense => Resolve(defense, ref fallbackDefense);
|
||||||
|
public PassiveRelicConstants PassiveRelics => Resolve(passiveRelics, ref fallbackPassiveRelics);
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
public static void SetCurrentForTests(GameplayConstants value)
|
public static void SetCurrentForTests(GameplayConstants value)
|
||||||
@@ -78,6 +86,8 @@ namespace BumpCombat.Constants
|
|||||||
fallbackArtifacts = null;
|
fallbackArtifacts = null;
|
||||||
fallbackRun = null;
|
fallbackRun = null;
|
||||||
fallbackCombatFeedback = null;
|
fallbackCombatFeedback = null;
|
||||||
|
fallbackDefense = null;
|
||||||
|
fallbackPassiveRelics = null;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Constants
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(menuName = "BumpCombat/Constants/Passive Relics")]
|
||||||
|
public sealed class PassiveRelicConstants : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Twin Horn Talisman")]
|
||||||
|
[Range(0f, 1f)] public float TwinHornDoubleDamageChance = 0.15f;
|
||||||
|
|
||||||
|
[Header("Resonance Core")]
|
||||||
|
[Range(0f, 1f)] public float ResonanceEchoChance = 0.08f;
|
||||||
|
[Min(0f)] public float ResonanceEchoCooldownSeconds = 3f;
|
||||||
|
[Range(0f, 1f)] public float ResonanceEchoDamageMultiplier = 0.5f;
|
||||||
|
|
||||||
|
[Header("Rupture Seed")]
|
||||||
|
[Range(0f, 1f)] public float RuptureSeedChance = 0.25f;
|
||||||
|
[Range(0f, 1f)] public float RuptureExplosionMaxHealthFraction = 0.1f;
|
||||||
|
[Min(0f)] public float RuptureExplosionPlayerBodyDiameters = 3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f0f9bca102564c4395943d6b39c6d9f0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -6,7 +6,7 @@ namespace BumpCombat.Constants
|
|||||||
public sealed class PlayerConstants : ScriptableObject
|
public sealed class PlayerConstants : ScriptableObject
|
||||||
{
|
{
|
||||||
[Header("기본 능력치")]
|
[Header("기본 능력치")]
|
||||||
[Tooltip("플레이어 기본 이동 속도 (유닛/초)"), Min(0f)] public float BaseMoveSpeed = 3f;
|
[Tooltip("플레이어 기본 이동 속도 (유닛/초)"), Min(0f)] public float BaseMoveSpeed = 3.75f;
|
||||||
[Tooltip("플레이어 기본 최대 체력"), Min(1f)] public float BaseMaxHealth = 100f;
|
[Tooltip("플레이어 기본 최대 체력"), Min(1f)] public float BaseMaxHealth = 100f;
|
||||||
|
|
||||||
[Header("피격")]
|
[Header("피격")]
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace BumpCombat.Constants
|
namespace BumpCombat.Constants
|
||||||
{
|
{
|
||||||
|
public enum RoomScaleComparisonMode
|
||||||
|
{
|
||||||
|
Disabled = 0,
|
||||||
|
Original = 1,
|
||||||
|
Expanded = 2,
|
||||||
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public struct SpawnDensityPhase
|
public struct SpawnDensityPhase
|
||||||
{
|
{
|
||||||
@@ -123,9 +130,25 @@ namespace BumpCombat.Constants
|
|||||||
public Vector2 ArenaPlayableHalfExtents = new(15f, 8.4375f);
|
public Vector2 ArenaPlayableHalfExtents = new(15f, 8.4375f);
|
||||||
public Vector2 ArenaPlayerPadding = new(0.5f, 0.625f);
|
public Vector2 ArenaPlayerPadding = new(0.5f, 0.625f);
|
||||||
public Vector2 ArenaCameraViewHalfExtents = new(10f, 5.625f);
|
public Vector2 ArenaCameraViewHalfExtents = new(10f, 5.625f);
|
||||||
|
[Tooltip("스테이지 방에서 사용할 플레이어 추적 시야의 반쪽 크기")]
|
||||||
|
public Vector2 StageCameraViewHalfExtents = new(7.5f, 4.21875f);
|
||||||
|
[Tooltip("대표 Stage 1 비교 방에서 Original 선택 시 사용할 좁은 추적 시야의 반쪽 크기")]
|
||||||
|
public Vector2 StageComparisonOriginalCameraViewHalfExtents = new(5f, 2.8125f);
|
||||||
|
[Tooltip("대표 Stage 1 비교 방에서 Expanded 선택 시 사용할 추적 시야의 반쪽 크기")]
|
||||||
|
public Vector2 StageComparisonCameraViewHalfExtents = new(7.5f, 4.21875f);
|
||||||
public Vector2 ArenaCameraDeadZone = new(0.5f, 0.3f);
|
public Vector2 ArenaCameraDeadZone = new(0.5f, 0.3f);
|
||||||
[Min(0f)] public float ArenaCameraSmoothTime = 0.15f;
|
[Min(0f)] public float ArenaCameraSmoothTime = 0.15f;
|
||||||
|
|
||||||
|
[Header("대표 Stage 1 방 크기 비교")]
|
||||||
|
[Tooltip("Disabled는 기존 카탈로그 선택을 유지하고, Original/Expanded는 입구 직결 첫 전투 방을 S1-A로 고정합니다.")]
|
||||||
|
public RoomScaleComparisonMode Stage1FirstCombatRoomScaleComparison = RoomScaleComparisonMode.Disabled;
|
||||||
|
|
||||||
|
[Header("제한 시야 교전")]
|
||||||
|
[Min(0.1f)] public float StagePlayerVisibilityRadius = 6f;
|
||||||
|
[Range(0f, 1f)] public float StageExploredFogOpacity = 0.65f;
|
||||||
|
[Range(0f, 1f)] public float StageUnseenFogOpacity = 1f;
|
||||||
|
[Range(0.1f, 1f)] public float StageEncounterSightRangeMultiplier = 0.5f;
|
||||||
|
|
||||||
public float[] EliteTimes(bool development, bool production)
|
public float[] EliteTimes(bool development, bool production)
|
||||||
{
|
{
|
||||||
float[] times = production
|
float[] times = production
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ namespace BumpCombat.Core
|
|||||||
{
|
{
|
||||||
public static ArenaBounds Instance { get; private set; }
|
public static ArenaBounds Instance { get; private set; }
|
||||||
|
|
||||||
public Vector2 BackgroundHalfExtents => GameplayConstants.Current.Run.ArenaBackgroundHalfExtents;
|
private Vector2 customCenter = Vector2.zero;
|
||||||
public Vector2 PlayableHalfExtents => GameplayConstants.Current.Run.ArenaPlayableHalfExtents;
|
private Vector2? customPlayableHalfExtents;
|
||||||
|
|
||||||
|
public Vector2 ZoneCenter => customCenter;
|
||||||
|
public Vector2 BackgroundHalfExtents => customPlayableHalfExtents ?? GameplayConstants.Current.Run.ArenaBackgroundHalfExtents;
|
||||||
|
public Vector2 PlayableHalfExtents => customPlayableHalfExtents ?? GameplayConstants.Current.Run.ArenaPlayableHalfExtents;
|
||||||
public Vector2 PlayerPadding => GameplayConstants.Current.Run.ArenaPlayerPadding;
|
public Vector2 PlayerPadding => GameplayConstants.Current.Run.ArenaPlayerPadding;
|
||||||
public Vector2 CameraViewHalfExtents => GameplayConstants.Current.Run.ArenaCameraViewHalfExtents;
|
public Vector2 CameraViewHalfExtents => GameplayConstants.Current.Run.ArenaCameraViewHalfExtents;
|
||||||
public Vector2 PlayerClampHalfExtents => new(
|
public Vector2 PlayerClampHalfExtents => new(
|
||||||
@@ -28,6 +32,18 @@ namespace BumpCombat.Core
|
|||||||
Mathf.Max(0f, BackgroundHalfExtents.x - CameraViewHalfExtents.x),
|
Mathf.Max(0f, BackgroundHalfExtents.x - CameraViewHalfExtents.x),
|
||||||
Mathf.Max(0f, BackgroundHalfExtents.y - CameraViewHalfExtents.y));
|
Mathf.Max(0f, BackgroundHalfExtents.y - CameraViewHalfExtents.y));
|
||||||
|
|
||||||
|
public void SetZoneBounds(Vector2 center, Vector2? halfExtents = null)
|
||||||
|
{
|
||||||
|
customCenter = center;
|
||||||
|
customPlayableHalfExtents = halfExtents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetToDefaultBounds()
|
||||||
|
{
|
||||||
|
customCenter = Vector2.zero;
|
||||||
|
customPlayableHalfExtents = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
@@ -50,13 +66,13 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
public Vector2 ClampPlayerPosition(Vector2 position)
|
public Vector2 ClampPlayerPosition(Vector2 position)
|
||||||
{
|
{
|
||||||
return ClampToHalfExtents(position, PlayerClampHalfExtents);
|
return ClampToHalfExtents(position, PlayerClampHalfExtents, customCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 ClampEnemyPosition(Vector2 position, float edgeInset = 0f)
|
public Vector2 ClampEnemyPosition(Vector2 position, float edgeInset = 0f)
|
||||||
{
|
{
|
||||||
Vector2 extents = GetReachableHalfExtents(edgeInset);
|
Vector2 extents = GetReachableHalfExtents(edgeInset);
|
||||||
return ClampToHalfExtents(position, extents);
|
return ClampToHalfExtents(position, extents, customCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 ClampEnemyPosition(
|
public Vector2 ClampEnemyPosition(
|
||||||
@@ -73,7 +89,7 @@ namespace BumpCombat.Core
|
|||||||
// player. The previous collider-aware clamp introduced a second,
|
// player. The previous collider-aware clamp introduced a second,
|
||||||
// scale-dependent boundary that was especially visible on elites
|
// scale-dependent boundary that was especially visible on elites
|
||||||
// and bosses at the edge of the map.
|
// and bosses at the edge of the map.
|
||||||
return ClampToHalfExtents(position, GetReachableHalfExtents(edgeInset));
|
return ClampToHalfExtents(position, GetReachableHalfExtents(edgeInset), customCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetReachableHalfExtents(float edgeInset = 0f)
|
public Vector2 GetReachableHalfExtents(float edgeInset = 0f)
|
||||||
@@ -86,13 +102,13 @@ namespace BumpCombat.Core
|
|||||||
public bool IsInsideReachableArena(Vector2 position, float inset = 0f)
|
public bool IsInsideReachableArena(Vector2 position, float inset = 0f)
|
||||||
{
|
{
|
||||||
Vector2 extents = GetReachableHalfExtents(inset);
|
Vector2 extents = GetReachableHalfExtents(inset);
|
||||||
return Mathf.Abs(position.x) <= extents.x + 0.0001f
|
return Mathf.Abs(position.x - customCenter.x) <= extents.x + 0.0001f
|
||||||
&& Mathf.Abs(position.y) <= extents.y + 0.0001f;
|
&& Mathf.Abs(position.y - customCenter.y) <= extents.y + 0.0001f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 ClampCameraCenter(Vector2 position)
|
public Vector2 ClampCameraCenter(Vector2 position)
|
||||||
{
|
{
|
||||||
return ClampToHalfExtents(position, CameraCenterHalfExtents);
|
return ClampToHalfExtents(position, CameraCenterHalfExtents, customCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 ClampCameraCenter(
|
public Vector2 ClampCameraCenter(
|
||||||
@@ -102,7 +118,7 @@ namespace BumpCombat.Core
|
|||||||
Vector2 centerHalfExtents = new(
|
Vector2 centerHalfExtents = new(
|
||||||
Mathf.Max(0f, BackgroundHalfExtents.x - Mathf.Abs(viewHalfExtents.x)),
|
Mathf.Max(0f, BackgroundHalfExtents.x - Mathf.Abs(viewHalfExtents.x)),
|
||||||
Mathf.Max(0f, BackgroundHalfExtents.y - Mathf.Abs(viewHalfExtents.y)));
|
Mathf.Max(0f, BackgroundHalfExtents.y - Mathf.Abs(viewHalfExtents.y)));
|
||||||
return ClampToHalfExtents(position, centerHalfExtents);
|
return ClampToHalfExtents(position, centerHalfExtents, customCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetCameraVisibleHalfExtents(Camera camera)
|
public Vector2 GetCameraVisibleHalfExtents(Camera camera)
|
||||||
@@ -143,10 +159,11 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
private static Vector2 ClampToHalfExtents(
|
private static Vector2 ClampToHalfExtents(
|
||||||
Vector2 position,
|
Vector2 position,
|
||||||
Vector2 halfExtents)
|
Vector2 halfExtents,
|
||||||
|
Vector2 center)
|
||||||
{
|
{
|
||||||
position.x = Mathf.Clamp(position.x, -halfExtents.x, halfExtents.x);
|
position.x = Mathf.Clamp(position.x, center.x - halfExtents.x, center.x + halfExtents.x);
|
||||||
position.y = Mathf.Clamp(position.y, -halfExtents.y, halfExtents.y);
|
position.y = Mathf.Clamp(position.y, center.y - halfExtents.y, center.y + halfExtents.y);
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Reflection;
|
||||||
using BumpCombat.Constants;
|
using BumpCombat.Constants;
|
||||||
using BumpCombat.Player;
|
using BumpCombat.Player;
|
||||||
|
using BumpCombat.Stage;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Rendering;
|
using UnityEngine.Rendering;
|
||||||
|
|
||||||
@@ -16,11 +18,41 @@ namespace BumpCombat.Core
|
|||||||
private Vector2 externalShakeOffset;
|
private Vector2 externalShakeOffset;
|
||||||
private bool initialized;
|
private bool initialized;
|
||||||
private RunManager runManager;
|
private RunManager runManager;
|
||||||
|
private Component pixelPerfectCamera;
|
||||||
|
private PropertyInfo pixelPerfectAssetsPpu;
|
||||||
|
private PropertyInfo pixelPerfectReferenceWidth;
|
||||||
|
private PropertyInfo pixelPerfectReferenceHeight;
|
||||||
|
|
||||||
|
private const int PixelPerfectAssetsPpu = 32;
|
||||||
|
|
||||||
public Vector2 CurrentCenter => currentCenter;
|
public Vector2 CurrentCenter => currentCenter;
|
||||||
public Vector2 DeadZone => GameplayConstants.Current.Run.ArenaCameraDeadZone;
|
public Vector2 DeadZone => GameplayConstants.Current.Run.ArenaCameraDeadZone;
|
||||||
public float SmoothTime => GameplayConstants.Current.Run.ArenaCameraSmoothTime;
|
public float SmoothTime => GameplayConstants.Current.Run.ArenaCameraSmoothTime;
|
||||||
public Vector2 ExternalShakeOffset => externalShakeOffset;
|
public Vector2 ExternalShakeOffset => externalShakeOffset;
|
||||||
|
public Camera WorldCamera => worldCamera != null ? worldCamera : (worldCamera = GetComponent<Camera>());
|
||||||
|
public float OrthographicSize => WorldCamera != null ? WorldCamera.orthographicSize : 5.625f;
|
||||||
|
|
||||||
|
public void SetOrthographicSize(float size)
|
||||||
|
{
|
||||||
|
ApplyPixelPerfectReference(
|
||||||
|
IsStageViewportActive(),
|
||||||
|
GetCurrentStagePreset(),
|
||||||
|
IsOriginalComparisonCurrentZone());
|
||||||
|
if (WorldCamera != null)
|
||||||
|
{
|
||||||
|
WorldCamera.orthographicSize = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float CalculateTargetOrthographicSize(
|
||||||
|
Camera cam,
|
||||||
|
Vector2 playableHalfExtents,
|
||||||
|
RoomPreset roomPreset = null,
|
||||||
|
bool originalComparison = false)
|
||||||
|
{
|
||||||
|
Vector2 stageView = GetStageView(roomPreset, originalComparison);
|
||||||
|
return stageView.y > 0.01f ? stageView.y : 2.8125f;
|
||||||
|
}
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -85,6 +117,23 @@ namespace BumpCombat.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool stageViewport = Stage.StageManager.Instance != null
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone != null;
|
||||||
|
RoomPreset roomPreset = stageViewport
|
||||||
|
? Stage.StageManager.Instance.CurrentZone.Preset
|
||||||
|
: null;
|
||||||
|
bool originalComparison = stageViewport
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone.IsStage1FirstCombatComparison;
|
||||||
|
ApplyPixelPerfectReference(stageViewport, roomPreset, originalComparison);
|
||||||
|
Vector2 stageView = GetStageView(roomPreset, originalComparison);
|
||||||
|
float configuredOrtho = stageViewport
|
||||||
|
? stageView.y
|
||||||
|
: GameplayConstants.Current.Run.ArenaCameraViewHalfExtents.y;
|
||||||
|
if (configuredOrtho > 0.01f)
|
||||||
|
{
|
||||||
|
camera.orthographicSize = configuredOrtho;
|
||||||
|
}
|
||||||
|
|
||||||
int targetWidth = camera.targetTexture != null
|
int targetWidth = camera.targetTexture != null
|
||||||
? camera.targetTexture.width
|
? camera.targetTexture.width
|
||||||
: Screen.width;
|
: Screen.width;
|
||||||
@@ -95,23 +144,98 @@ namespace BumpCombat.Core
|
|||||||
int evenHeight = targetHeight & ~1;
|
int evenHeight = targetHeight & ~1;
|
||||||
if (targetWidth < 2
|
if (targetWidth < 2
|
||||||
|| targetHeight < 2
|
|| targetHeight < 2
|
||||||
|| (evenWidth == targetWidth && evenHeight == targetHeight))
|
|| evenWidth < 2
|
||||||
|
|| evenHeight < 2)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect desiredPixelRect = new(0f, 0f, evenWidth, evenHeight);
|
||||||
Rect currentPixelRect = camera.pixelRect;
|
Rect currentPixelRect = camera.pixelRect;
|
||||||
if (Mathf.Abs(currentPixelRect.x) < 0.01f
|
bool pixelRectChanged = Mathf.Abs(currentPixelRect.x - desiredPixelRect.x) > 0.01f
|
||||||
&& Mathf.Abs(currentPixelRect.y) < 0.01f
|
|| Mathf.Abs(currentPixelRect.y - desiredPixelRect.y) > 0.01f
|
||||||
&& Mathf.Abs(currentPixelRect.width - evenWidth) < 0.01f
|
|| Mathf.Abs(currentPixelRect.width - desiredPixelRect.width) > 0.01f
|
||||||
&& Mathf.Abs(currentPixelRect.height - evenHeight) < 0.01f)
|
|| Mathf.Abs(currentPixelRect.height - desiredPixelRect.height) > 0.01f;
|
||||||
|
if (pixelRectChanged)
|
||||||
|
{
|
||||||
|
camera.pixelRect = desiredPixelRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.ResetAspect();
|
||||||
|
camera.orthographicSize = configuredOrtho * evenHeight / targetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsStageViewportActive()
|
||||||
|
{
|
||||||
|
return Stage.StageManager.Instance != null
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyPixelPerfectReference(
|
||||||
|
bool stageViewport,
|
||||||
|
RoomPreset roomPreset = null,
|
||||||
|
bool originalComparison = false)
|
||||||
|
{
|
||||||
|
if (WorldCamera == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
camera.pixelRect = new Rect(0f, 0f, evenWidth, evenHeight);
|
ResolvePixelPerfectCamera();
|
||||||
camera.ResetAspect();
|
if (pixelPerfectCamera == null)
|
||||||
camera.orthographicSize *= evenHeight / (float)targetHeight;
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 referenceView = stageViewport
|
||||||
|
? GetStageView(roomPreset, originalComparison)
|
||||||
|
: GameplayConstants.Current.Run.ArenaCameraViewHalfExtents;
|
||||||
|
int referenceWidth = Mathf.Max(
|
||||||
|
1,
|
||||||
|
Mathf.RoundToInt(Mathf.Abs(referenceView.x) * 2f * PixelPerfectAssetsPpu));
|
||||||
|
int referenceHeight = Mathf.Max(
|
||||||
|
1,
|
||||||
|
Mathf.RoundToInt(Mathf.Abs(referenceView.y) * 2f * PixelPerfectAssetsPpu));
|
||||||
|
|
||||||
|
SetPixelPerfectIntProperty(pixelPerfectAssetsPpu, PixelPerfectAssetsPpu);
|
||||||
|
SetPixelPerfectIntProperty(pixelPerfectReferenceWidth, referenceWidth);
|
||||||
|
SetPixelPerfectIntProperty(pixelPerfectReferenceHeight, referenceHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolvePixelPerfectCamera()
|
||||||
|
{
|
||||||
|
if (pixelPerfectCamera != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pixelPerfectCamera = WorldCamera.GetComponent(
|
||||||
|
"UnityEngine.Rendering.Universal.PixelPerfectCamera");
|
||||||
|
if (pixelPerfectCamera == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Type componentType = pixelPerfectCamera.GetType();
|
||||||
|
pixelPerfectAssetsPpu = componentType.GetProperty("assetsPPU");
|
||||||
|
pixelPerfectReferenceWidth = componentType.GetProperty("refResolutionX");
|
||||||
|
pixelPerfectReferenceHeight = componentType.GetProperty("refResolutionY");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPixelPerfectIntProperty(PropertyInfo property, int value)
|
||||||
|
{
|
||||||
|
if (property == null || !property.CanWrite)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object currentValue = property.GetValue(pixelPerfectCamera);
|
||||||
|
if (currentValue is int currentInt && currentInt == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
property.SetValue(pixelPerfectCamera, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
@@ -123,6 +247,12 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
ResolveBounds();
|
ResolveBounds();
|
||||||
ResolveTarget();
|
ResolveTarget();
|
||||||
|
if (Stage.StageManager.Instance != null && Stage.StageManager.Instance.IsTransitioning)
|
||||||
|
{
|
||||||
|
ApplyCameraPosition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (RunManager.GameplayInputEnabled && target != null)
|
if (RunManager.GameplayInputEnabled && target != null)
|
||||||
{
|
{
|
||||||
Vector2 desiredCenter = GetDeadZoneTargetCenter(
|
Vector2 desiredCenter = GetDeadZoneTargetCenter(
|
||||||
@@ -244,6 +374,39 @@ namespace BumpCombat.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RoomPreset GetCurrentStagePreset()
|
||||||
|
{
|
||||||
|
return Stage.StageManager.Instance != null
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone != null
|
||||||
|
? Stage.StageManager.Instance.CurrentZone.Preset
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOriginalComparisonCurrentZone()
|
||||||
|
{
|
||||||
|
return Stage.StageManager.Instance != null
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone != null
|
||||||
|
&& Stage.StageManager.Instance.CurrentZone.IsStage1FirstCombatComparison
|
||||||
|
&& GameplayConstants.Current.Run.Stage1FirstCombatRoomScaleComparison
|
||||||
|
== RoomScaleComparisonMode.Original;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 GetStageView(RoomPreset roomPreset, bool originalComparison)
|
||||||
|
{
|
||||||
|
RunConstants run = GameplayConstants.Current.Run;
|
||||||
|
if (LevelPresetCatalog.IsExpandedComparisonPreset(roomPreset))
|
||||||
|
{
|
||||||
|
return run.StageComparisonCameraViewHalfExtents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalComparison)
|
||||||
|
{
|
||||||
|
return run.StageComparisonOriginalCameraViewHalfExtents;
|
||||||
|
}
|
||||||
|
|
||||||
|
return run.StageCameraViewHalfExtents;
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyCameraPosition()
|
private void ApplyCameraPosition()
|
||||||
{
|
{
|
||||||
Vector2 finalCenter = currentCenter + externalShakeOffset;
|
Vector2 finalCenter = currentCenter + externalShakeOffset;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using BumpCombat.Constants;
|
using BumpCombat.Constants;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
@@ -17,6 +18,7 @@ namespace BumpCombat.Core
|
|||||||
{
|
{
|
||||||
Development,
|
Development,
|
||||||
Production,
|
Production,
|
||||||
|
DefensePrototype,
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RunManager : MonoBehaviour
|
public sealed class RunManager : MonoBehaviour
|
||||||
@@ -28,6 +30,9 @@ namespace BumpCombat.Core
|
|||||||
private bool guardUnlocked = true;
|
private bool guardUnlocked = true;
|
||||||
private bool artifactChargeUnlocked = true;
|
private bool artifactChargeUnlocked = true;
|
||||||
private bool gameOver;
|
private bool gameOver;
|
||||||
|
private bool gameOverPresentation;
|
||||||
|
private bool runCleared;
|
||||||
|
private int currentSeed = -1;
|
||||||
private bool titleOpen;
|
private bool titleOpen;
|
||||||
private bool pauseOpen;
|
private bool pauseOpen;
|
||||||
private bool selectionOpen;
|
private bool selectionOpen;
|
||||||
@@ -42,6 +47,8 @@ namespace BumpCombat.Core
|
|||||||
public static RunManager Instance { get; private set; }
|
public static RunManager Instance { get; private set; }
|
||||||
public static bool GameplayInputEnabled =>
|
public static bool GameplayInputEnabled =>
|
||||||
Instance == null || (!Instance.gameOver
|
Instance == null || (!Instance.gameOver
|
||||||
|
&& !Instance.gameOverPresentation
|
||||||
|
&& !Instance.runCleared
|
||||||
&& !Instance.titleOpen
|
&& !Instance.titleOpen
|
||||||
&& !Instance.pauseOpen
|
&& !Instance.pauseOpen
|
||||||
&& !Instance.selectionOpen);
|
&& !Instance.selectionOpen);
|
||||||
@@ -55,11 +62,16 @@ namespace BumpCombat.Core
|
|||||||
public event Action<bool> OnArtifactChargeAvailabilityChanged;
|
public event Action<bool> OnArtifactChargeAvailabilityChanged;
|
||||||
public event Action<bool> OnPauseChanged;
|
public event Action<bool> OnPauseChanged;
|
||||||
public event Action OnGameOver;
|
public event Action OnGameOver;
|
||||||
|
public event Action OnRunCleared;
|
||||||
|
|
||||||
public float ElapsedTime { get; private set; }
|
public float ElapsedTime { get; private set; }
|
||||||
public float ProgressionTime =>
|
public float ProgressionTime =>
|
||||||
GetProgressionTime(ElapsedTime, UseDebugEventTimes);
|
GetProgressionTime(ElapsedTime, UseDebugEventTimes);
|
||||||
public bool IsGameOver => gameOver;
|
public bool IsGameOver => gameOver;
|
||||||
|
public bool IsGameOverPresentationActive => gameOverPresentation;
|
||||||
|
public bool IsRunCleared => runCleared;
|
||||||
|
public int CurrentSeed => currentSeed > 0 ? currentSeed : 12345;
|
||||||
|
public void SetRunSeed(int seed) => currentSeed = seed;
|
||||||
public bool IsTitleScreen => titleOpen;
|
public bool IsTitleScreen => titleOpen;
|
||||||
public bool IsPaused => pauseOpen;
|
public bool IsPaused => pauseOpen;
|
||||||
public bool IsSelectionOpen => selectionOpen;
|
public bool IsSelectionOpen => selectionOpen;
|
||||||
@@ -70,13 +82,15 @@ namespace BumpCombat.Core
|
|||||||
&& selectedRunMode == RunMode.Production;
|
&& selectedRunMode == RunMode.Production;
|
||||||
public bool IsDevelopmentRun => hasSelectedRunMode
|
public bool IsDevelopmentRun => hasSelectedRunMode
|
||||||
&& selectedRunMode == RunMode.Development;
|
&& selectedRunMode == RunMode.Development;
|
||||||
|
public bool IsDefensePrototypeRun => hasSelectedRunMode
|
||||||
|
&& selectedRunMode == RunMode.DefensePrototype;
|
||||||
public bool IsGuardUnlocked => !IsProductionRun || guardUnlocked;
|
public bool IsGuardUnlocked => !IsProductionRun || guardUnlocked;
|
||||||
public bool IsArtifactChargeUnlocked => !IsProductionRun
|
public bool IsArtifactChargeUnlocked => !IsProductionRun
|
||||||
|| artifactChargeUnlocked;
|
|| artifactChargeUnlocked;
|
||||||
public int StartupArtifactSelectionCount => hasSelectedRunMode
|
public int StartupArtifactSelectionCount => hasSelectedRunMode
|
||||||
? selectedRunMode == RunMode.Development
|
? selectedRunMode == RunMode.Production
|
||||||
? GameplayConstants.Current.Run.DevelopmentStartupArtifactSelectionCount
|
? GameplayConstants.Current.Run.ProductionStartupArtifactSelectionCount
|
||||||
: GameplayConstants.Current.Run.ProductionStartupArtifactSelectionCount
|
: GameplayConstants.Current.Run.DevelopmentStartupArtifactSelectionCount
|
||||||
: GameplayConstants.Current.Run.LegacyStartupArtifactSelectionCount;
|
: GameplayConstants.Current.Run.LegacyStartupArtifactSelectionCount;
|
||||||
public int RaisedEliteEventCount => nextEliteEventIndex;
|
public int RaisedEliteEventCount => nextEliteEventIndex;
|
||||||
public bool UseDebugArtifactSelection
|
public bool UseDebugArtifactSelection
|
||||||
@@ -131,6 +145,7 @@ namespace BumpCombat.Core
|
|||||||
if (FindAnyObjectByType<BumpCombat.Progression.ArtifactRewardController>() != null)
|
if (FindAnyObjectByType<BumpCombat.Progression.ArtifactRewardController>() != null)
|
||||||
{
|
{
|
||||||
EnsurePresentationServices();
|
EnsurePresentationServices();
|
||||||
|
EnsureDefensePrototypeController();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +155,15 @@ namespace BumpCombat.Core
|
|||||||
canvas.gameObject.AddComponent<BumpCombat.Progression.ArtifactRewardController>();
|
canvas.gameObject.AddComponent<BumpCombat.Progression.ArtifactRewardController>();
|
||||||
}
|
}
|
||||||
EnsurePresentationServices();
|
EnsurePresentationServices();
|
||||||
|
EnsureDefensePrototypeController();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureDefensePrototypeController()
|
||||||
|
{
|
||||||
|
if (GetComponent<BumpCombat.Defense.DefensePrototypeController>() == null)
|
||||||
|
{
|
||||||
|
gameObject.AddComponent<BumpCombat.Defense.DefensePrototypeController>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsurePresentationServices()
|
private void EnsurePresentationServices()
|
||||||
@@ -170,6 +194,19 @@ namespace BumpCombat.Core
|
|||||||
{
|
{
|
||||||
canvas.gameObject.AddComponent<BumpCombat.UI.MissionAchievementPanel>();
|
canvas.gameObject.AddComponent<BumpCombat.UI.MissionAchievementPanel>();
|
||||||
}
|
}
|
||||||
|
if (canvas.GetComponent<BumpCombat.UI.WorldMapPanel>() == null)
|
||||||
|
{
|
||||||
|
canvas.gameObject.AddComponent<BumpCombat.UI.WorldMapPanel>();
|
||||||
|
}
|
||||||
|
if (canvas.GetComponent<BumpCombat.UI.RunClearPanel>() == null)
|
||||||
|
{
|
||||||
|
canvas.gameObject.AddComponent<BumpCombat.UI.RunClearPanel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FindAnyObjectByType<BumpCombat.Stage.StageManager>() == null)
|
||||||
|
{
|
||||||
|
gameObject.AddComponent<BumpCombat.Stage.StageManager>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
@@ -182,6 +219,11 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
|
if (gameOverPresentation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (gameOver)
|
if (gameOver)
|
||||||
{
|
{
|
||||||
if (Keyboard.current?.rKey.wasPressedThisFrame == true)
|
if (Keyboard.current?.rKey.wasPressedThisFrame == true)
|
||||||
@@ -192,6 +234,11 @@ namespace BumpCombat.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runCleared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectionOpen)
|
if (selectionOpen)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -222,7 +269,13 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
public bool TryOpenSelection(MonoBehaviour owner)
|
public bool TryOpenSelection(MonoBehaviour owner)
|
||||||
{
|
{
|
||||||
if (owner == null || titleOpen || selectionOpen || gameOver || pauseOpen)
|
if (owner == null
|
||||||
|
|| titleOpen
|
||||||
|
|| selectionOpen
|
||||||
|
|| gameOver
|
||||||
|
|| gameOverPresentation
|
||||||
|
|| runCleared
|
||||||
|
|| pauseOpen)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -308,6 +361,8 @@ namespace BumpCombat.Core
|
|||||||
public bool TryOpenPause()
|
public bool TryOpenPause()
|
||||||
{
|
{
|
||||||
if (gameOver
|
if (gameOver
|
||||||
|
|| gameOverPresentation
|
||||||
|
|| runCleared
|
||||||
|| titleOpen
|
|| titleOpen
|
||||||
|| selectionOpen
|
|| selectionOpen
|
||||||
|| pauseOpen
|
|| pauseOpen
|
||||||
@@ -365,21 +420,69 @@ namespace BumpCombat.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gameOverPresentation = false;
|
||||||
gameOver = true;
|
gameOver = true;
|
||||||
pauseOpen = false;
|
pauseOpen = false;
|
||||||
RefreshTimeScale();
|
RefreshTimeScale();
|
||||||
OnGameOver?.Invoke();
|
OnGameOver?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void BeginGameOverPresentation(float duration)
|
||||||
|
{
|
||||||
|
if (gameOver || runCleared || gameOverPresentation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOverPresentation = true;
|
||||||
|
pauseOpen = false;
|
||||||
|
StartCoroutine(CompleteGameOverPresentation(Mathf.Max(0f, duration)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator CompleteGameOverPresentation(float duration)
|
||||||
|
{
|
||||||
|
if (duration > 0f)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gameOverPresentation)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOverPresentation = false;
|
||||||
|
EndRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndRunAsClear()
|
||||||
|
{
|
||||||
|
if (gameOver || gameOverPresentation || runCleared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runCleared = true;
|
||||||
|
pauseOpen = false;
|
||||||
|
RefreshTimeScale();
|
||||||
|
OnRunCleared?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshTimeScale()
|
private void RefreshTimeScale()
|
||||||
{
|
{
|
||||||
Time.timeScale = gameOver || titleOpen || pauseOpen || selectionOpen
|
Time.timeScale = gameOver || runCleared || titleOpen || pauseOpen || selectionOpen
|
||||||
? 0f
|
? 0f
|
||||||
: 1f;
|
: 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RaiseTimedEvents()
|
private void RaiseTimedEvents()
|
||||||
{
|
{
|
||||||
|
if (BumpCombat.Stage.StageManager.IsStageModeActive
|
||||||
|
|| IsDefensePrototypeRun)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
float[] eliteTimes = CurrentEliteEventTimes;
|
float[] eliteTimes = CurrentEliteEventTimes;
|
||||||
float midBossTime = GetEventTimeForCurrentRun(RunTimedEvent.MidBoss);
|
float midBossTime = GetEventTimeForCurrentRun(RunTimedEvent.MidBoss);
|
||||||
float finalBossTime = GetEventTimeForCurrentRun(RunTimedEvent.FinalBoss);
|
float finalBossTime = GetEventTimeForCurrentRun(RunTimedEvent.FinalBoss);
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ namespace BumpCombat.Core
|
|||||||
|
|
||||||
public int CalculateSortingOrder()
|
public int CalculateSortingOrder()
|
||||||
{
|
{
|
||||||
return baseOrder - Mathf.RoundToInt(transform.position.y * precision);
|
float y = transform.position.y;
|
||||||
|
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||||
|
if (bounds != null)
|
||||||
|
{
|
||||||
|
y -= bounds.ZoneCenter.y;
|
||||||
|
}
|
||||||
|
return baseOrder - Mathf.RoundToInt(y * precision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b21501b83f844601916bda51aa90fef4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BumpCombat.Constants;
|
||||||
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Progression;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using BumpCombat.Player;
|
||||||
|
using BumpCombat.Spawning;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace BumpCombat.Defense
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class DefensePrototypeController : MonoBehaviour
|
||||||
|
{
|
||||||
|
private const int LaneCount = 2;
|
||||||
|
|
||||||
|
private static Sprite whiteSprite;
|
||||||
|
|
||||||
|
private readonly Dictionary<EnemyController, DefenseRouteAgent> activeAgents = new();
|
||||||
|
private readonly DefenseTower[] towers = new DefenseTower[LaneCount];
|
||||||
|
private readonly bool[] padsBuilt = new bool[LaneCount];
|
||||||
|
private readonly int[] suppliedPerLane = new int[LaneCount];
|
||||||
|
private readonly DefenseWaveLedger[] portalLedgers = new DefenseWaveLedger[LaneCount];
|
||||||
|
private readonly bool[] portalSupplyEnded = new bool[LaneCount];
|
||||||
|
private readonly bool[] portalRewardVisible = new bool[LaneCount];
|
||||||
|
private readonly bool[] portalRewardObjectCreated = new bool[LaneCount];
|
||||||
|
private readonly GameObject[] portalRewardObjects = new GameObject[LaneCount];
|
||||||
|
private readonly GameObject[] portalRewardLabels = new GameObject[LaneCount];
|
||||||
|
private int baseHealth;
|
||||||
|
private float nextSupplyTime;
|
||||||
|
private int nextSupplyLane;
|
||||||
|
private bool active;
|
||||||
|
private bool playerDead;
|
||||||
|
private bool failed;
|
||||||
|
private int constructionCurrency;
|
||||||
|
private int pendingPassiveRelicChoices;
|
||||||
|
private float reviveRemaining;
|
||||||
|
private GameObject worldRoot;
|
||||||
|
private Text hudText;
|
||||||
|
private RunManager runManager;
|
||||||
|
private SpawnDirector spawnDirector;
|
||||||
|
private PlayerHealth playerHealth;
|
||||||
|
private PlayerController playerController;
|
||||||
|
private DefenseConstants tuning;
|
||||||
|
|
||||||
|
public static DefensePrototypeController Instance { get; private set; }
|
||||||
|
public static Sprite WhiteSprite
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (whiteSprite == null)
|
||||||
|
{
|
||||||
|
whiteSprite = Sprite.Create(
|
||||||
|
Texture2D.whiteTexture,
|
||||||
|
new Rect(0f, 0f, 1f, 1f),
|
||||||
|
new Vector2(0.5f, 0.5f),
|
||||||
|
1f);
|
||||||
|
whiteSprite.name = "Defense Prototype White Primitive";
|
||||||
|
}
|
||||||
|
|
||||||
|
return whiteSprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefenseConstants Tuning => tuning ?? GameplayConstants.Current.Defense;
|
||||||
|
public bool IsActive => active;
|
||||||
|
public bool PlayerIsAlive => playerHealth != null && playerHealth.CurrentHealth > 0f;
|
||||||
|
public bool CanOperateTowers => DefensePrototypeRules.CanOperateTowers(
|
||||||
|
PlayerIsAlive,
|
||||||
|
active && !failed && runManager != null
|
||||||
|
&& !runManager.IsGameOver
|
||||||
|
&& !runManager.IsRunCleared
|
||||||
|
&& !runManager.IsPaused
|
||||||
|
&& !runManager.IsSelectionOpen
|
||||||
|
&& !runManager.IsTitleScreen);
|
||||||
|
public int ConstructionCurrency => constructionCurrency;
|
||||||
|
public int GetBaseHealth(int lane) => lane >= 0 && lane < LaneCount ? baseHealth : 0;
|
||||||
|
public int GetPortalRemainingSupply(int lane)
|
||||||
|
{
|
||||||
|
if (tuning == null || lane < 0 || lane >= LaneCount)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mathf.Max(
|
||||||
|
0,
|
||||||
|
tuning.PortalEnemyCount - (portalLedgers[lane]?.SuppliedCount ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RemainingPortalSupply
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (tuning == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int remaining = 0;
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
remaining += Mathf.Max(
|
||||||
|
0,
|
||||||
|
tuning.PortalEnemyCount - (portalLedgers[lane]?.SuppliedCount ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool IsRewardVisible
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
if (portalRewardVisible[lane])
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public float PlayerReviveRemaining => playerDead ? reviveRemaining : 0f;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Destroy(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
runManager = RunManager.Instance;
|
||||||
|
if (runManager != null)
|
||||||
|
{
|
||||||
|
runManager.OnRunStarted += HandleRunStarted;
|
||||||
|
runManager.OnGameOver += HandleRunEnded;
|
||||||
|
runManager.OnRunCleared += HandleRunEnded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runManager != null
|
||||||
|
&& runManager.IsDefensePrototypeRun
|
||||||
|
&& !runManager.IsTitleScreen)
|
||||||
|
{
|
||||||
|
Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (runManager != null)
|
||||||
|
{
|
||||||
|
runManager.OnRunStarted -= HandleRunStarted;
|
||||||
|
runManager.OnGameOver -= HandleRunEnded;
|
||||||
|
runManager.OnRunCleared -= HandleRunEnded;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<EnemyController, DefenseRouteAgent> pair in activeAgents)
|
||||||
|
{
|
||||||
|
if (pair.Key != null)
|
||||||
|
{
|
||||||
|
pair.Key.OnDied -= HandleEnemyDied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Instance == this)
|
||||||
|
{
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!active || runManager == null || failed || runManager.IsGameOver || runManager.IsRunCleared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runManager.IsPaused || runManager.IsSelectionOpen || runManager.IsTitleScreen)
|
||||||
|
{
|
||||||
|
UpdateHud();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePlayerRecovery();
|
||||||
|
UpdatePortalSupply();
|
||||||
|
TryOfferRewards();
|
||||||
|
TryCollectRewards();
|
||||||
|
UpdateHud();
|
||||||
|
|
||||||
|
if (Keyboard.current != null && CanOperateTowers)
|
||||||
|
{
|
||||||
|
if (Keyboard.current.digit1Key.wasPressedThisFrame
|
||||||
|
|| Keyboard.current.numpad1Key.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
TryBuildNearestTower(DefenseTowerKind.Direct);
|
||||||
|
}
|
||||||
|
else if (Keyboard.current.digit2Key.wasPressedThisFrame
|
||||||
|
|| Keyboard.current.numpad2Key.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
TryBuildNearestTower(DefenseTowerKind.Summon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Activate()
|
||||||
|
{
|
||||||
|
if (active || runManager == null || !runManager.IsDefensePrototypeRun)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tuning = GameplayConstants.Current.Defense;
|
||||||
|
spawnDirector = FindAnyObjectByType<SpawnDirector>();
|
||||||
|
playerHealth = FindAnyObjectByType<PlayerHealth>();
|
||||||
|
playerController = FindAnyObjectByType<PlayerController>();
|
||||||
|
if (playerController != null)
|
||||||
|
{
|
||||||
|
PassiveRelicController relics = playerController.GetComponent<PassiveRelicController>();
|
||||||
|
if (relics == null)
|
||||||
|
{
|
||||||
|
relics = playerController.gameObject.AddComponent<PassiveRelicController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
relics.BeginDefenseRun();
|
||||||
|
}
|
||||||
|
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||||
|
bounds?.SetZoneBounds(tuning.ArenaCenter, tuning.ArenaPlayableHalfExtents);
|
||||||
|
FindAnyObjectByType<ArenaCameraFollow>()?.ResetCenter(tuning.ArenaCenter);
|
||||||
|
active = true;
|
||||||
|
failed = false;
|
||||||
|
playerDead = false;
|
||||||
|
constructionCurrency = tuning.InitialConstructionCurrency;
|
||||||
|
pendingPassiveRelicChoices = 0;
|
||||||
|
baseHealth = tuning.BaseMaximumHealth;
|
||||||
|
activeAgents.Clear();
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
suppliedPerLane[lane] = 0;
|
||||||
|
portalLedgers[lane] = new DefenseWaveLedger(tuning.PortalEnemyCount);
|
||||||
|
portalSupplyEnded[lane] = false;
|
||||||
|
portalRewardVisible[lane] = false;
|
||||||
|
portalRewardObjectCreated[lane] = false;
|
||||||
|
portalRewardObjects[lane] = null;
|
||||||
|
portalRewardLabels[lane] = null;
|
||||||
|
padsBuilt[lane] = false;
|
||||||
|
towers[lane] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSupplyLane = 0;
|
||||||
|
nextSupplyTime = Time.time + 0.5f;
|
||||||
|
CreateWorldVisuals();
|
||||||
|
CreateHud();
|
||||||
|
UpdateHud();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryBuildTowerAtPad(int padIndex)
|
||||||
|
{
|
||||||
|
return TryBuildTowerAtPad(
|
||||||
|
padIndex,
|
||||||
|
padIndex == 0 ? DefenseTowerKind.Direct : DefenseTowerKind.Summon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryBuildTowerAtPad(int padIndex, DefenseTowerKind kind)
|
||||||
|
{
|
||||||
|
if (!CanOperateTowers
|
||||||
|
|| padIndex < 0
|
||||||
|
|| padIndex >= LaneCount
|
||||||
|
|| padsBuilt[padIndex]
|
||||||
|
|| playerController == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 position = tuning.GetBuildPadPosition(padIndex);
|
||||||
|
if (((Vector2)playerController.transform.position - position).sqrMagnitude
|
||||||
|
> tuning.BuildInteractionRadius * tuning.BuildInteractionRadius)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cost = kind == DefenseTowerKind.Direct
|
||||||
|
? tuning.DirectTowerCost
|
||||||
|
: tuning.SummonTowerCost;
|
||||||
|
if (constructionCurrency < cost)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject towerObject = new(
|
||||||
|
kind == DefenseTowerKind.Direct ? "Direct Damage Tower" : "Summon Tower");
|
||||||
|
towerObject.transform.SetParent(worldRoot.transform, false);
|
||||||
|
towerObject.transform.position = position;
|
||||||
|
DefenseTower tower = towerObject.AddComponent<DefenseTower>();
|
||||||
|
tower.Configure(this, kind, padIndex, tuning);
|
||||||
|
towers[padIndex] = tower;
|
||||||
|
padsBuilt[padIndex] = true;
|
||||||
|
constructionCurrency -= cost;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryBuildNearestTower(DefenseTowerKind kind)
|
||||||
|
{
|
||||||
|
if (playerController == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nearestPad = -1;
|
||||||
|
float nearestDistance = tuning.BuildInteractionRadius * tuning.BuildInteractionRadius;
|
||||||
|
for (int pad = 0; pad < LaneCount; pad++)
|
||||||
|
{
|
||||||
|
if (padsBuilt[pad])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float distance = ((Vector2)playerController.transform.position
|
||||||
|
- tuning.GetBuildPadPosition(pad)).sqrMagnitude;
|
||||||
|
if (distance <= nearestDistance)
|
||||||
|
{
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearestPad = pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearestPad >= 0 && TryBuildTowerAtPad(nearestPad, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAttackFromTower(DefenseTower tower)
|
||||||
|
{
|
||||||
|
if (!CanOperateTowers || tower == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DefenseRouteAgent target = FindNearestTarget(
|
||||||
|
tower.transform.position,
|
||||||
|
tuning.TowerRange,
|
||||||
|
tower.Kind);
|
||||||
|
return tower.TryAttack(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefenseRouteAgent FindNearestTarget(
|
||||||
|
Vector2 sourcePosition,
|
||||||
|
float range,
|
||||||
|
DefenseTowerKind sourceKind)
|
||||||
|
{
|
||||||
|
DefenseRouteAgent nearest = null;
|
||||||
|
float nearestDistance = range * range;
|
||||||
|
foreach (DefenseRouteAgent agent in activeAgents.Values)
|
||||||
|
{
|
||||||
|
if (agent == null
|
||||||
|
|| agent.HasArrived
|
||||||
|
|| agent.Enemy == null
|
||||||
|
|| agent.Enemy.IsDead
|
||||||
|
|| DefensePrototypeRules.IsTowerImmune(
|
||||||
|
sourceKind,
|
||||||
|
agent.ImmuneTowerKind))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float distance = ((Vector2)agent.transform.position - sourcePosition).sqrMagnitude;
|
||||||
|
if (distance <= nearestDistance)
|
||||||
|
{
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearest = agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryApplyTowerDamage(
|
||||||
|
DefenseTower tower,
|
||||||
|
DefenseRouteAgent target,
|
||||||
|
float damage)
|
||||||
|
{
|
||||||
|
if (!CanOperateTowers || tower == null || target == null || playerHealth == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float distanceToPlayer = Vector2.Distance(
|
||||||
|
playerHealth.transform.position,
|
||||||
|
tower.transform.position);
|
||||||
|
return target.TryTakeTowerDamage(
|
||||||
|
tower.Kind,
|
||||||
|
damage,
|
||||||
|
distanceToPlayer,
|
||||||
|
tuning.PlayerSightRadius,
|
||||||
|
tuning.OutsideSightDamageEfficiency,
|
||||||
|
tower.transform.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HandlePlayerDied()
|
||||||
|
{
|
||||||
|
if (!active || failed || runManager == null || runManager.IsGameOver || runManager.IsRunCleared)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerDead = true;
|
||||||
|
reviveRemaining = tuning.PlayerReviveDelaySeconds;
|
||||||
|
playerController?.SetTransitionLocked(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRunStarted()
|
||||||
|
{
|
||||||
|
if (runManager != null && runManager.IsDefensePrototypeRun)
|
||||||
|
{
|
||||||
|
Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRunEnded()
|
||||||
|
{
|
||||||
|
failed = true;
|
||||||
|
playerController?.GetComponent<PassiveRelicController>()?.EndDefenseRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlayerRecovery()
|
||||||
|
{
|
||||||
|
if (!playerDead)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reviveRemaining = Mathf.Max(0f, reviveRemaining - Time.deltaTime);
|
||||||
|
if (reviveRemaining > 0f || !PlayerCanBeRevived())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerDead = false;
|
||||||
|
playerHealth?.ReviveForDefense(tuning.PlayerReviveInvulnerabilitySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PlayerCanBeRevived()
|
||||||
|
{
|
||||||
|
return !DefensePrototypeRules.IsBaseDestroyed(baseHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePortalSupply()
|
||||||
|
{
|
||||||
|
if (spawnDirector == null || Time.time < nextSupplyTime)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < LaneCount; attempt++)
|
||||||
|
{
|
||||||
|
int lane = (nextSupplyLane + attempt) % LaneCount;
|
||||||
|
if (suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController enemy = spawnDirector.SpawnDefenseSkeleton(
|
||||||
|
tuning.GetPortalPosition(lane));
|
||||||
|
if (enemy == null)
|
||||||
|
{
|
||||||
|
nextSupplyTime = Time.time + tuning.PortalSpawnInterval;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2[] route =
|
||||||
|
{
|
||||||
|
tuning.GetPortalPosition(lane),
|
||||||
|
tuning.GetWaypointPosition(lane, 0),
|
||||||
|
tuning.GetWaypointPosition(lane, 1),
|
||||||
|
tuning.GetBasePosition(lane),
|
||||||
|
};
|
||||||
|
DefenseRouteAgent agent = enemy.gameObject.AddComponent<DefenseRouteAgent>();
|
||||||
|
agent.Configure(
|
||||||
|
enemy,
|
||||||
|
lane,
|
||||||
|
route,
|
||||||
|
enemy.MoveSpeed * tuning.RouteSpeedMultiplier,
|
||||||
|
Mathf.Max(0.1f, enemy.AttackRange + 0.35f),
|
||||||
|
tuning.PostAttackProgressSeconds,
|
||||||
|
tuning.GetPortalImmuneTowerKind(lane),
|
||||||
|
HandleEnemyArrived);
|
||||||
|
enemy.OnDied += HandleEnemyDied;
|
||||||
|
activeAgents.Add(enemy, agent);
|
||||||
|
portalLedgers[lane].RegisterSupply(enemy.SpawnLifetimeIdentity);
|
||||||
|
suppliedPerLane[lane]++;
|
||||||
|
if (suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||||
|
{
|
||||||
|
portalSupplyEnded[lane] = true;
|
||||||
|
portalLedgers[lane].MarkSupplyEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSupplyLane = (lane + 1) % LaneCount;
|
||||||
|
nextSupplyTime = Time.time + tuning.PortalSpawnInterval;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
if (!portalSupplyEnded[lane]
|
||||||
|
&& suppliedPerLane[lane] >= tuning.PortalEnemyCount)
|
||||||
|
{
|
||||||
|
portalSupplyEnded[lane] = true;
|
||||||
|
portalLedgers[lane].MarkSupplyEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void HandleEnemyDied(EnemyController enemy)
|
||||||
|
{
|
||||||
|
if (enemy == null || !activeAgents.TryGetValue(enemy, out DefenseRouteAgent agent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enemy.OnDied -= HandleEnemyDied;
|
||||||
|
activeAgents.Remove(enemy);
|
||||||
|
int lane = Mathf.Clamp(agent.Lane, 0, LaneCount - 1);
|
||||||
|
portalLedgers[lane]?.TryResolve(
|
||||||
|
enemy.SpawnLifetimeIdentity,
|
||||||
|
DefenseEnemyResolution.Death);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleEnemyArrived(DefenseRouteAgent agent)
|
||||||
|
{
|
||||||
|
if (agent == null
|
||||||
|
|| agent.Enemy == null
|
||||||
|
|| !activeAgents.ContainsKey(agent.Enemy))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController enemy = agent.Enemy;
|
||||||
|
enemy.OnDied -= HandleEnemyDied;
|
||||||
|
activeAgents.Remove(enemy);
|
||||||
|
int lane = Mathf.Clamp(agent.Lane, 0, LaneCount - 1);
|
||||||
|
portalLedgers[lane]?.TryResolve(
|
||||||
|
enemy.SpawnLifetimeIdentity,
|
||||||
|
DefenseEnemyResolution.Arrival);
|
||||||
|
baseHealth = Mathf.Max(0, baseHealth - tuning.ArrivalBaseDamage);
|
||||||
|
enemy.RetireFromRun(includeProtected: true, suppressFeedback: true);
|
||||||
|
if (DefensePrototypeRules.IsBaseDestroyed(baseHealth))
|
||||||
|
{
|
||||||
|
failed = true;
|
||||||
|
runManager.EndRun();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryOfferRewards()
|
||||||
|
{
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
DefenseWaveLedger ledger = portalLedgers[lane];
|
||||||
|
if (!portalSupplyEnded[lane]
|
||||||
|
|| ledger == null
|
||||||
|
|| portalRewardVisible[lane]
|
||||||
|
|| CountActiveAgentsForLane(lane) > 0
|
||||||
|
|| !ledger.TryOfferReward())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
portalRewardVisible[lane] = true;
|
||||||
|
if (!portalRewardObjectCreated[lane])
|
||||||
|
{
|
||||||
|
portalRewardObjectCreated[lane] = true;
|
||||||
|
Vector2 rewardPosition = tuning.GetPortalPosition(lane) + Vector2.up * 1.2f;
|
||||||
|
portalRewardObjects[lane] = CreateShape(
|
||||||
|
$"Construction Reward {lane + 1}",
|
||||||
|
rewardPosition,
|
||||||
|
new Vector2(0.85f, 0.85f),
|
||||||
|
new Color32(255, 224, 84, 255),
|
||||||
|
7);
|
||||||
|
portalRewardLabels[lane] = CreateWorldLabel(
|
||||||
|
$"Reward Label {lane + 1}",
|
||||||
|
rewardPosition + Vector2.up * 0.65f,
|
||||||
|
$"+{tuning.PortalRewardCurrency} BUILD");
|
||||||
|
}
|
||||||
|
|
||||||
|
portalRewardObjects[lane].SetActive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryCollectRewards()
|
||||||
|
{
|
||||||
|
if (playerController == null || playerHealth == null || !PlayerIsAlive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 playerPosition = playerController.transform.position;
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
if (!portalRewardVisible[lane]
|
||||||
|
|| portalRewardObjects[lane] == null
|
||||||
|
|| Vector2.Distance(
|
||||||
|
playerPosition,
|
||||||
|
portalRewardObjects[lane].transform.position)
|
||||||
|
> tuning.RewardPickupRadius
|
||||||
|
|| !portalLedgers[lane].TryCollectReward())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
portalRewardVisible[lane] = false;
|
||||||
|
portalRewardObjects[lane].SetActive(false);
|
||||||
|
portalRewardLabels[lane]?.SetActive(false);
|
||||||
|
constructionCurrency += tuning.PortalRewardCurrency;
|
||||||
|
PassiveRelicController relics = playerController.GetComponent<PassiveRelicController>();
|
||||||
|
ArtifactRewardController rewards = FindAnyObjectByType<ArtifactRewardController>();
|
||||||
|
if (relics != null && relics.AvailableCount > 0 && rewards != null)
|
||||||
|
{
|
||||||
|
pendingPassiveRelicChoices++;
|
||||||
|
rewards.QueuePassiveRelicReward();
|
||||||
|
}
|
||||||
|
else if (relics == null || relics.AvailableCount == 0 || rewards == null)
|
||||||
|
{
|
||||||
|
constructionCurrency += tuning.PortalRewardCurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TryFinishClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResolvePassiveRelicChoice()
|
||||||
|
{
|
||||||
|
pendingPassiveRelicChoices = Mathf.Max(0, pendingPassiveRelicChoices - 1);
|
||||||
|
TryFinishClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryFinishClear()
|
||||||
|
{
|
||||||
|
if (failed || activeAgents.Count > 0 || pendingPassiveRelicChoices > 0 || runManager == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
if (!portalSupplyEnded[lane]
|
||||||
|
|| portalLedgers[lane] == null
|
||||||
|
|| !portalLedgers[lane].IsRewardCollected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DefensePrototypeRules.IsBaseDestroyed(baseHealth)
|
||||||
|
&& !runManager.IsGameOver
|
||||||
|
&& !runManager.IsRunCleared)
|
||||||
|
{
|
||||||
|
runManager.EndRunAsClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CountActiveAgentsForLane(int lane)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (DefenseRouteAgent agent in activeAgents.Values)
|
||||||
|
{
|
||||||
|
if (agent != null && agent.Lane == lane)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
private void CreateWorldVisuals()
|
||||||
|
{
|
||||||
|
if (worldRoot != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldRoot = new GameObject("Defense Prototype World");
|
||||||
|
worldRoot.transform.SetParent(transform, false);
|
||||||
|
for (int lane = 0; lane < LaneCount; lane++)
|
||||||
|
{
|
||||||
|
Vector2 portal = tuning.GetPortalPosition(lane);
|
||||||
|
Vector2 basePosition = tuning.GetBasePosition(lane);
|
||||||
|
CreateLaneLine(lane, portal, tuning.GetWaypointPosition(lane, 0));
|
||||||
|
CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 0), tuning.GetWaypointPosition(lane, 1));
|
||||||
|
CreateLaneLine(lane, tuning.GetWaypointPosition(lane, 1), basePosition);
|
||||||
|
CreateShape(
|
||||||
|
$"Portal {lane + 1}",
|
||||||
|
portal,
|
||||||
|
new Vector2(0.85f, 1.25f),
|
||||||
|
new Color32(179, 84, 220, 255),
|
||||||
|
5);
|
||||||
|
CreateWorldLabel($"Portal {lane + 1} Label", portal + Vector2.right * 1.6f + Vector2.up * 0.9f, $"P{lane + 1} PORTAL\nIMMUNE {tuning.GetPortalImmuneTowerKind(lane).ToString().ToUpperInvariant()}");
|
||||||
|
if (lane == 0)
|
||||||
|
{
|
||||||
|
CreateShape(
|
||||||
|
"Shared Base",
|
||||||
|
basePosition,
|
||||||
|
new Vector2(1.15f, 2.1f),
|
||||||
|
new Color32(226, 78, 87, 255),
|
||||||
|
5);
|
||||||
|
CreateWorldLabel("Shared Base Label", basePosition + Vector2.up * 1.4f, "BASE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int pad = 0; pad < LaneCount; pad++)
|
||||||
|
{
|
||||||
|
Vector2 position = tuning.GetBuildPadPosition(pad);
|
||||||
|
CreateShape(
|
||||||
|
$"Build Pad {pad + 1}",
|
||||||
|
position,
|
||||||
|
new Vector2(1.35f, 1.35f),
|
||||||
|
new Color32(76, 104, 125, 255),
|
||||||
|
3);
|
||||||
|
CreateWorldLabel(
|
||||||
|
$"Build Pad {pad + 1} Label",
|
||||||
|
position + Vector2.down * 0.95f,
|
||||||
|
"PAD " + (pad + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateHud()
|
||||||
|
{
|
||||||
|
Canvas canvas = FindAnyObjectByType<Canvas>();
|
||||||
|
if (canvas == null || hudText != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject hudObject = new(
|
||||||
|
"Defense Prototype HUD",
|
||||||
|
typeof(RectTransform),
|
||||||
|
typeof(CanvasRenderer),
|
||||||
|
typeof(Text));
|
||||||
|
hudObject.transform.SetParent(canvas.transform, false);
|
||||||
|
RectTransform rect = hudObject.GetComponent<RectTransform>();
|
||||||
|
rect.anchorMin = new Vector2(0f, 1f);
|
||||||
|
rect.anchorMax = new Vector2(0f, 1f);
|
||||||
|
rect.pivot = new Vector2(0f, 1f);
|
||||||
|
rect.anchoredPosition = new Vector2(28f, -452f);
|
||||||
|
rect.sizeDelta = new Vector2(900f, 110f);
|
||||||
|
hudText = hudObject.GetComponent<Text>();
|
||||||
|
hudText.font = BumpCombat.UI.PresentationUiStyle.GetFont();
|
||||||
|
hudText.fontSize = 24;
|
||||||
|
hudText.color = new Color32(236, 240, 246, 255);
|
||||||
|
hudText.alignment = TextAnchor.UpperLeft;
|
||||||
|
hudText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||||
|
hudText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHud()
|
||||||
|
{
|
||||||
|
if (hudText == null || tuning == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string status = playerDead
|
||||||
|
? $"PLAYER DOWN revive {Mathf.CeilToInt(reviveRemaining)}s"
|
||||||
|
: IsRewardVisible ? "REWARD READY move to portal" : $"1 DIRECT {tuning.DirectTowerCost} / 2 SUMMON {tuning.SummonTowerCost} near a pad";
|
||||||
|
hudText.text =
|
||||||
|
$"DEFENSE TEST BASE {baseHealth}/{tuning.BaseMaximumHealth}\n"
|
||||||
|
+ $"BUILD {constructionCurrency} P1 WAIT {GetPortalRemainingSupply(0)} LIVE {CountActiveAgentsForLane(0)} P2 WAIT {GetPortalRemainingSupply(1)} LIVE {CountActiveAgentsForLane(1)}\n"
|
||||||
|
+ status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateLaneLine(int lane, Vector2 from, Vector2 to)
|
||||||
|
{
|
||||||
|
GameObject lineObject = new($"Lane {lane + 1} Segment");
|
||||||
|
lineObject.transform.SetParent(worldRoot.transform, false);
|
||||||
|
LineRenderer line = lineObject.AddComponent<LineRenderer>();
|
||||||
|
line.useWorldSpace = true;
|
||||||
|
line.positionCount = 2;
|
||||||
|
line.SetPosition(0, from);
|
||||||
|
line.SetPosition(1, to);
|
||||||
|
line.startWidth = 0.08f;
|
||||||
|
line.endWidth = 0.08f;
|
||||||
|
line.startColor = new Color32(108, 151, 177, 210);
|
||||||
|
line.endColor = new Color32(108, 151, 177, 210);
|
||||||
|
line.material = new Material(Shader.Find("Sprites/Default"));
|
||||||
|
line.sortingOrder = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject CreateShape(
|
||||||
|
string name,
|
||||||
|
Vector2 position,
|
||||||
|
Vector2 size,
|
||||||
|
Color color,
|
||||||
|
int sortingOrder)
|
||||||
|
{
|
||||||
|
GameObject shape = new(name);
|
||||||
|
shape.transform.SetParent(worldRoot.transform, false);
|
||||||
|
shape.transform.position = position;
|
||||||
|
shape.transform.localScale = new Vector3(size.x, size.y, 1f);
|
||||||
|
SpriteRenderer renderer = shape.AddComponent<SpriteRenderer>();
|
||||||
|
renderer.sprite = WhiteSprite;
|
||||||
|
renderer.color = color;
|
||||||
|
renderer.sortingOrder = sortingOrder;
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject CreateWorldLabel(string name, Vector2 position, string text)
|
||||||
|
{
|
||||||
|
GameObject labelObject = new(name);
|
||||||
|
labelObject.transform.SetParent(worldRoot.transform, false);
|
||||||
|
labelObject.transform.position = position;
|
||||||
|
TextMesh label = labelObject.AddComponent<TextMesh>();
|
||||||
|
label.text = text;
|
||||||
|
label.anchor = TextAnchor.MiddleCenter;
|
||||||
|
label.alignment = TextAlignment.Center;
|
||||||
|
label.characterSize = 0.08f;
|
||||||
|
label.fontSize = 32;
|
||||||
|
label.color = Color.white;
|
||||||
|
label.GetComponent<MeshRenderer>().sortingOrder = 12;
|
||||||
|
return labelObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a39d0992fba741809dfd6e95bc9cfd83
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Defense
|
||||||
|
{
|
||||||
|
public enum DefenseTowerKind
|
||||||
|
{
|
||||||
|
Direct,
|
||||||
|
Summon,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DefenseEnemyResolution
|
||||||
|
{
|
||||||
|
Death,
|
||||||
|
Arrival,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DefensePrototypeRules
|
||||||
|
{
|
||||||
|
public static float GetTowerEfficiency(
|
||||||
|
float distanceToPlayer,
|
||||||
|
float sightRadius,
|
||||||
|
float insideEfficiency,
|
||||||
|
float outsideEfficiency)
|
||||||
|
{
|
||||||
|
return distanceToPlayer <= sightRadius
|
||||||
|
? insideEfficiency
|
||||||
|
: outsideEfficiency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsTowerImmune(
|
||||||
|
DefenseTowerKind sourceKind,
|
||||||
|
DefenseTowerKind immuneKind)
|
||||||
|
{
|
||||||
|
return sourceKind == immuneKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanOperateTowers(bool playerAlive, bool runActive)
|
||||||
|
{
|
||||||
|
return playerAlive && runActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsBaseDestroyed(float currentHealth)
|
||||||
|
{
|
||||||
|
return currentHealth <= 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DefenseWaveLedger
|
||||||
|
{
|
||||||
|
private readonly int expectedSupplyCount;
|
||||||
|
private readonly System.Collections.Generic.HashSet<int> suppliedIds = new();
|
||||||
|
private readonly System.Collections.Generic.HashSet<int> resolvedIds = new();
|
||||||
|
private bool supplyEnded;
|
||||||
|
private bool rewardOffered;
|
||||||
|
private bool rewardCollected;
|
||||||
|
|
||||||
|
public DefenseWaveLedger(int expectedSupplyCount)
|
||||||
|
{
|
||||||
|
this.expectedSupplyCount = Mathf.Max(0, expectedSupplyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SuppliedCount => suppliedIds.Count;
|
||||||
|
public int ResolvedCount => resolvedIds.Count;
|
||||||
|
public int KillEventCount { get; private set; }
|
||||||
|
public bool IsSupplyEnded => supplyEnded;
|
||||||
|
public bool IsRewardOffered => rewardOffered;
|
||||||
|
public bool IsRewardCollected => rewardCollected;
|
||||||
|
|
||||||
|
public bool RegisterSupply(int enemyId)
|
||||||
|
{
|
||||||
|
if (supplyEnded
|
||||||
|
|| SuppliedCount >= expectedSupplyCount
|
||||||
|
|| enemyId < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return suppliedIds.Add(enemyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkSupplyEnded()
|
||||||
|
{
|
||||||
|
supplyEnded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve(int enemyId, DefenseEnemyResolution resolution)
|
||||||
|
{
|
||||||
|
if (!suppliedIds.Contains(enemyId) || !resolvedIds.Add(enemyId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolution == DefenseEnemyResolution.Death)
|
||||||
|
{
|
||||||
|
KillEventCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryOfferReward()
|
||||||
|
{
|
||||||
|
if (rewardOffered
|
||||||
|
|| !supplyEnded
|
||||||
|
|| SuppliedCount < expectedSupplyCount
|
||||||
|
|| ResolvedCount < SuppliedCount)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardOffered = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCollectReward()
|
||||||
|
{
|
||||||
|
if (!rewardOffered || rewardCollected)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardCollected = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9be2c059f93b466e9735447cb1bd2146
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
using System;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Defense
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class DefenseRouteAgent : MonoBehaviour
|
||||||
|
{
|
||||||
|
private EnemyController enemy;
|
||||||
|
private Vector2[] waypoints = Array.Empty<Vector2>();
|
||||||
|
private Action<DefenseRouteAgent> arrivalHandler;
|
||||||
|
private Transform player;
|
||||||
|
private BumpCombat.Player.PlayerHealth playerHealth;
|
||||||
|
private float routeSpeed;
|
||||||
|
private float playerEngagementRange;
|
||||||
|
private float postAttackProgressSeconds;
|
||||||
|
private float postAttackProgressRemaining;
|
||||||
|
private bool wasAttackSequenceInProgress;
|
||||||
|
private bool configured;
|
||||||
|
private int waypointIndex;
|
||||||
|
|
||||||
|
public EnemyController Enemy => enemy;
|
||||||
|
public int Lane { get; private set; }
|
||||||
|
public int LifetimeId => enemy != null ? enemy.SpawnLifetimeIdentity : -1;
|
||||||
|
public DefenseTowerKind ImmuneTowerKind { get; private set; }
|
||||||
|
public Vector2 AnchorPosition => waypoints.Length > 0 ? waypoints[0] : transform.position;
|
||||||
|
public bool HasArrived { get; private set; }
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
EnemyController configuredEnemy,
|
||||||
|
int lane,
|
||||||
|
Vector2[] route,
|
||||||
|
float speed,
|
||||||
|
float engagementRange,
|
||||||
|
float postAttackProgress,
|
||||||
|
DefenseTowerKind immuneTowerKind,
|
||||||
|
Action<DefenseRouteAgent> onArrived)
|
||||||
|
{
|
||||||
|
enemy = configuredEnemy;
|
||||||
|
Lane = lane;
|
||||||
|
waypoints = route ?? Array.Empty<Vector2>();
|
||||||
|
routeSpeed = Mathf.Max(0.1f, speed);
|
||||||
|
playerEngagementRange = Mathf.Max(0.1f, engagementRange);
|
||||||
|
postAttackProgressSeconds = Mathf.Max(0f, postAttackProgress);
|
||||||
|
postAttackProgressRemaining = 0f;
|
||||||
|
wasAttackSequenceInProgress = false;
|
||||||
|
ImmuneTowerKind = immuneTowerKind;
|
||||||
|
arrivalHandler = onArrived;
|
||||||
|
waypointIndex = 0;
|
||||||
|
configured = enemy != null && waypoints.Length > 0;
|
||||||
|
HasArrived = false;
|
||||||
|
player = FindAnyObjectByType<BumpCombat.Player.PlayerController>()?.transform;
|
||||||
|
playerHealth = player != null
|
||||||
|
? player.GetComponent<BumpCombat.Player.PlayerHealth>()
|
||||||
|
: null;
|
||||||
|
if (configured)
|
||||||
|
{
|
||||||
|
enemy.TryReposition(waypoints[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryDriveFixedUpdate(float deltaTime)
|
||||||
|
{
|
||||||
|
if (!configured || HasArrived || enemy == null || enemy.IsDead)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enemy.State == EnemyState.Spawn)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool attackSequenceActive = enemy.IsAttackSequenceInProgress;
|
||||||
|
if (attackSequenceActive)
|
||||||
|
{
|
||||||
|
wasAttackSequenceInProgress = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasAttackSequenceInProgress)
|
||||||
|
{
|
||||||
|
wasAttackSequenceInProgress = false;
|
||||||
|
postAttackProgressRemaining = postAttackProgressSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enemy.IsStunned)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
postAttackProgressRemaining = Mathf.Max(
|
||||||
|
0f,
|
||||||
|
postAttackProgressRemaining - Mathf.Max(0f, deltaTime));
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
player = FindAnyObjectByType<BumpCombat.Player.PlayerController>()?.transform;
|
||||||
|
playerHealth = player != null
|
||||||
|
? player.GetComponent<BumpCombat.Player.PlayerHealth>()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postAttackProgressRemaining <= 0f
|
||||||
|
&& player != null
|
||||||
|
&& playerHealth != null
|
||||||
|
&& playerHealth.CurrentHealth > 0f
|
||||||
|
&& (player.position - transform.position).sqrMagnitude
|
||||||
|
<= playerEngagementRange * playerEngagementRange)
|
||||||
|
{
|
||||||
|
enemy.Alert();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waypointIndex >= waypoints.Length)
|
||||||
|
{
|
||||||
|
CompleteArrival();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 current = transform.position;
|
||||||
|
Vector2 target = waypoints[waypointIndex];
|
||||||
|
Vector2 next = Vector2.MoveTowards(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
routeSpeed * Mathf.Max(0f, deltaTime));
|
||||||
|
if (!enemy.TryReposition(next))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 movementDirection = next - current;
|
||||||
|
if (movementDirection.sqrMagnitude > 0.000001f)
|
||||||
|
{
|
||||||
|
enemy.SetFacingDirection(movementDirection);
|
||||||
|
}
|
||||||
|
Animator animator = enemy.GetComponent<Animator>();
|
||||||
|
animator?.SetBool("IsMoving", (next - current).sqrMagnitude > 0.000001f);
|
||||||
|
if ((target - next).sqrMagnitude <= 0.01f)
|
||||||
|
{
|
||||||
|
waypointIndex++;
|
||||||
|
if (waypointIndex >= waypoints.Length)
|
||||||
|
{
|
||||||
|
CompleteArrival();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryTakeTowerDamage(
|
||||||
|
DefenseTowerKind sourceKind,
|
||||||
|
float requestedDamage,
|
||||||
|
float distanceToPlayer,
|
||||||
|
float sightRadius,
|
||||||
|
float outsideSightEfficiency,
|
||||||
|
Vector2 sourcePosition)
|
||||||
|
{
|
||||||
|
if (!configured
|
||||||
|
|| HasArrived
|
||||||
|
|| enemy == null
|
||||||
|
|| enemy.IsDead
|
||||||
|
|| DefensePrototypeRules.IsTowerImmune(
|
||||||
|
sourceKind,
|
||||||
|
ImmuneTowerKind))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float efficiency = DefensePrototypeRules.GetTowerEfficiency(
|
||||||
|
distanceToPlayer,
|
||||||
|
sightRadius,
|
||||||
|
1f,
|
||||||
|
outsideSightEfficiency);
|
||||||
|
Vector2 direction = ((Vector2)transform.position - sourcePosition).normalized;
|
||||||
|
return enemy.TryTakeDamage(
|
||||||
|
Mathf.Max(0f, requestedDamage) * efficiency,
|
||||||
|
direction,
|
||||||
|
0f,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
EnemyDamageSource.Tower);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteArrival()
|
||||||
|
{
|
||||||
|
if (HasArrived)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HasArrived = true;
|
||||||
|
arrivalHandler?.Invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: be1e0aa426264604ac15478324ef43aa
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Defense
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class DefenseSummonUnit : MonoBehaviour
|
||||||
|
{
|
||||||
|
private DefenseTower owner;
|
||||||
|
private DefensePrototypeController controller;
|
||||||
|
private Vector2 anchorLocalPosition;
|
||||||
|
private SpriteRenderer spriteRenderer;
|
||||||
|
private float attackCooldown;
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
DefenseTower tower,
|
||||||
|
DefensePrototypeController prototypeController,
|
||||||
|
Vector2 localAnchor)
|
||||||
|
{
|
||||||
|
owner = tower;
|
||||||
|
controller = prototypeController;
|
||||||
|
anchorLocalPosition = localAnchor;
|
||||||
|
spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
Sprite[] frames = Resources.LoadAll<Sprite>("Tutorial/Swordsman_Idle");
|
||||||
|
if (frames != null && frames.Length > 0)
|
||||||
|
{
|
||||||
|
spriteRenderer.sprite = frames[0];
|
||||||
|
}
|
||||||
|
spriteRenderer.color = new Color32(255, 234, 137, 255);
|
||||||
|
spriteRenderer.sortingOrder = 11;
|
||||||
|
transform.localScale = new Vector3(0.9f, 0.9f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (owner == null
|
||||||
|
|| !owner.isActiveAndEnabled
|
||||||
|
|| controller == null
|
||||||
|
|| !controller.CanOperateTowers)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attackCooldown = Mathf.Max(0f, attackCooldown - Time.deltaTime);
|
||||||
|
DefenseRouteAgent target = controller.FindNearestTarget(
|
||||||
|
transform.position,
|
||||||
|
controller.Tuning.SummonAreaRadius,
|
||||||
|
DefenseTowerKind.Summon);
|
||||||
|
Vector2 anchor = owner.transform.TransformPoint(anchorLocalPosition);
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
transform.position = Vector2.MoveTowards(
|
||||||
|
transform.position,
|
||||||
|
anchor,
|
||||||
|
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 targetPosition = target.transform.position;
|
||||||
|
Vector2 fromAnchor = targetPosition - anchor;
|
||||||
|
if (fromAnchor.magnitude > controller.Tuning.SummonAreaRadius)
|
||||||
|
{
|
||||||
|
targetPosition = anchor + fromAnchor.normalized * controller.Tuning.SummonAreaRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.position = Vector2.MoveTowards(
|
||||||
|
transform.position,
|
||||||
|
targetPosition,
|
||||||
|
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
|
||||||
|
if (attackCooldown <= 0f
|
||||||
|
&& (target.transform.position - transform.position).sqrMagnitude <= controller.Tuning.SummonAttackRange * controller.Tuning.SummonAttackRange)
|
||||||
|
{
|
||||||
|
if (owner.TryAttack(target))
|
||||||
|
{
|
||||||
|
attackCooldown = controller.Tuning.SummonAttackInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c2c4cbe8f78e4e11898e78be45697a12
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using BumpCombat.Constants;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Defense
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class DefenseTower : MonoBehaviour
|
||||||
|
{
|
||||||
|
private DefensePrototypeController owner;
|
||||||
|
private DefenseConstants tuning;
|
||||||
|
private float attackCooldown;
|
||||||
|
private TextMesh label;
|
||||||
|
|
||||||
|
public DefenseTowerKind Kind { get; private set; }
|
||||||
|
public int PadIndex { get; private set; }
|
||||||
|
public bool IsOperational => isActiveAndEnabled
|
||||||
|
&& owner != null
|
||||||
|
&& owner.CanOperateTowers;
|
||||||
|
|
||||||
|
public void Configure(
|
||||||
|
DefensePrototypeController controller,
|
||||||
|
DefenseTowerKind kind,
|
||||||
|
int padIndex,
|
||||||
|
DefenseConstants constants)
|
||||||
|
{
|
||||||
|
owner = controller;
|
||||||
|
Kind = kind;
|
||||||
|
PadIndex = padIndex;
|
||||||
|
tuning = constants;
|
||||||
|
CreateVisuals();
|
||||||
|
if (Kind == DefenseTowerKind.Summon)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < tuning.SummonUnitCount; i++)
|
||||||
|
{
|
||||||
|
GameObject unitObject = new($"Summon Unit {i + 1}");
|
||||||
|
unitObject.transform.SetParent(transform, false);
|
||||||
|
unitObject.transform.localPosition = new Vector3(
|
||||||
|
(i - (tuning.SummonUnitCount - 1) * 0.5f) * 0.42f,
|
||||||
|
-0.5f,
|
||||||
|
0f);
|
||||||
|
DefenseSummonUnit unit = unitObject.AddComponent<DefenseSummonUnit>();
|
||||||
|
unit.Configure(this, controller, unitObject.transform.localPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!IsOperational || Kind != DefenseTowerKind.Direct)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attackCooldown -= Time.deltaTime;
|
||||||
|
if (attackCooldown > 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float interval = Kind == DefenseTowerKind.Direct
|
||||||
|
? tuning.DirectTowerAttackInterval
|
||||||
|
: tuning.SummonAttackInterval;
|
||||||
|
if (owner.TryAttackFromTower(this))
|
||||||
|
{
|
||||||
|
attackCooldown = Mathf.Max(0.05f, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateVisuals()
|
||||||
|
{
|
||||||
|
SpriteRenderer renderer = gameObject.GetComponent<SpriteRenderer>();
|
||||||
|
if (renderer == null)
|
||||||
|
{
|
||||||
|
renderer = gameObject.AddComponent<SpriteRenderer>();
|
||||||
|
}
|
||||||
|
renderer.sprite = DefensePrototypeController.WhiteSprite;
|
||||||
|
renderer.color = Kind == DefenseTowerKind.Direct
|
||||||
|
? new Color32(65, 211, 240, 255)
|
||||||
|
: new Color32(244, 154, 74, 255);
|
||||||
|
renderer.sortingOrder = 8;
|
||||||
|
transform.localScale = new Vector3(0.72f, 0.72f, 1f);
|
||||||
|
|
||||||
|
GameObject crown = new("Tower Crown");
|
||||||
|
crown.transform.SetParent(transform, false);
|
||||||
|
crown.transform.localPosition = new Vector3(0f, 0.42f, 0f);
|
||||||
|
crown.transform.localScale = new Vector3(0.42f, 0.22f, 1f);
|
||||||
|
SpriteRenderer crownRenderer = crown.AddComponent<SpriteRenderer>();
|
||||||
|
crownRenderer.sprite = DefensePrototypeController.WhiteSprite;
|
||||||
|
crownRenderer.color = Color.white;
|
||||||
|
crownRenderer.sortingOrder = 9;
|
||||||
|
|
||||||
|
GameObject labelObject = new("Tower Label");
|
||||||
|
labelObject.transform.SetParent(transform, false);
|
||||||
|
labelObject.transform.localPosition = new Vector3(0f, 0.85f, 0f);
|
||||||
|
label = labelObject.AddComponent<TextMesh>();
|
||||||
|
label.text = Kind == DefenseTowerKind.Direct ? "DIRECT" : "SUMMON";
|
||||||
|
label.anchor = TextAnchor.MiddleCenter;
|
||||||
|
label.alignment = TextAlignment.Center;
|
||||||
|
label.characterSize = 0.16f;
|
||||||
|
label.fontSize = 32;
|
||||||
|
label.color = Color.white;
|
||||||
|
label.GetComponent<MeshRenderer>().sortingOrder = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAttack(DefenseRouteAgent target)
|
||||||
|
{
|
||||||
|
if (target == null || owner == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float damage = Kind == DefenseTowerKind.Direct
|
||||||
|
? tuning.DirectTowerDamage
|
||||||
|
: tuning.SummonDamage;
|
||||||
|
return owner.TryApplyTowerDamage(this, target, damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e82deace7a1a47e0b14c872019e51a01
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Player;
|
||||||
|
using BumpCombat.Progression;
|
||||||
|
using BumpCombat.Stage;
|
||||||
|
using BumpCombat.UI;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using UnityEngine.InputSystem.LowLevel;
|
||||||
|
|
||||||
|
[InitializeOnLoad]
|
||||||
|
internal static class AgyPlayValidationHarness
|
||||||
|
{
|
||||||
|
// A validation run starts only when run.marker explicitly contains "run".
|
||||||
|
private const string Out = "D:/project/game/output/agy-play-validation-20260920/";
|
||||||
|
private const string RunMarker = Out + "run.marker";
|
||||||
|
|
||||||
|
static AgyPlayValidationHarness()
|
||||||
|
{
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
|
{
|
||||||
|
if (IsRunRequested() && !EditorApplication.isPlayingOrWillChangePlaymode)
|
||||||
|
EditorApplication.EnterPlaymode();
|
||||||
|
};
|
||||||
|
EditorApplication.playModeStateChanged += s =>
|
||||||
|
{
|
||||||
|
if (s == PlayModeStateChange.EnteredPlayMode && IsRunRequested())
|
||||||
|
new GameObject("AgyPlayValidationRunner") { hideFlags = HideFlags.HideAndDontSave }
|
||||||
|
.AddComponent<AgyPlayValidationRunner>();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRunRequested()
|
||||||
|
{
|
||||||
|
return File.Exists(RunMarker)
|
||||||
|
&& string.Equals(File.ReadAllText(RunMarker).Trim(), "run", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AgyPlayValidationRunner : MonoBehaviour
|
||||||
|
{
|
||||||
|
private const string Out = "D:/project/game/output/agy-play-validation-20260920/";
|
||||||
|
|
||||||
|
private IEnumerator Start()
|
||||||
|
{
|
||||||
|
yield return new WaitForSecondsRealtime(0.5f);
|
||||||
|
RunManager run = RunManager.Instance;
|
||||||
|
if (run == null) { Fail("RunManager missing"); yield break; }
|
||||||
|
if (run.IsTitleScreen) run.BeginRun(RunMode.Development);
|
||||||
|
yield return ResolveBlockingSelections(run);
|
||||||
|
if (run.IsSelectionOpen || Time.timeScale <= 0f)
|
||||||
|
{
|
||||||
|
Fail($"Startup selection did not resolve. selectionOpen={run.IsSelectionOpen}, timeScale={Time.timeScale:R}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
StageManager stage = StageManager.Instance;
|
||||||
|
PlayerController player = FindAnyObjectByType<PlayerController>();
|
||||||
|
WorldMapPanel map = FindAnyObjectByType<WorldMapPanel>();
|
||||||
|
if (stage == null || stage.CurrentZone == null || player == null || map == null)
|
||||||
|
{ Fail("Stage/player/map initialization failed"); yield break; }
|
||||||
|
|
||||||
|
Capture("01-start");
|
||||||
|
Vector2 before = player.transform.position;
|
||||||
|
yield return Hold(Key.RightArrow, 0.5f);
|
||||||
|
Vector2 after = player.transform.position;
|
||||||
|
float movementDistance = Vector2.Distance(before, after);
|
||||||
|
File.WriteAllText(Out + "movement.txt", $"before={before:R}\nafter={after:R}\ndistance={movementDistance:R}\n");
|
||||||
|
Capture("02-moved");
|
||||||
|
if (movementDistance <= 0.01f)
|
||||||
|
{
|
||||||
|
Fail($"Player did not move. distance={movementDistance:R}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return Press(Key.M);
|
||||||
|
File.WriteAllText(Out + "map.txt", $"open={map.IsOpen}\ntimeScale={Time.timeScale:R}\n");
|
||||||
|
Capture("03-world-map-open");
|
||||||
|
if (!map.IsOpen || Time.timeScale > 0f)
|
||||||
|
{
|
||||||
|
Fail($"World map did not open correctly. open={map.IsOpen}, timeScale={Time.timeScale:R}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
yield return Press(Key.M);
|
||||||
|
if (map.IsOpen || Time.timeScale <= 0f)
|
||||||
|
{
|
||||||
|
Fail($"World map did not close correctly. open={map.IsOpen}, timeScale={Time.timeScale:R}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int target = -1; StageDirection exit = StageDirection.North;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int candidate = stage.CurrentZone.NodeData.GetConnection((StageDirection)i);
|
||||||
|
if (candidate >= 0) { target = candidate; exit = (StageDirection)i; break; }
|
||||||
|
}
|
||||||
|
if (target < 0) { Fail("No connected zone"); yield break; }
|
||||||
|
int oldZone = stage.CurrentZone.NodeId;
|
||||||
|
stage.RequestZoneTransition(target, exit.Opposite());
|
||||||
|
yield return null;
|
||||||
|
File.WriteAllText(Out + "transition-active.txt", $"transitioning={stage.IsTransitioning}\nlocked={player.IsTransitionLocked}\n");
|
||||||
|
Capture("04-transition-active");
|
||||||
|
float deadline = Time.realtimeSinceStartup + 4f;
|
||||||
|
while (stage.IsTransitioning && Time.realtimeSinceStartup < deadline) yield return null;
|
||||||
|
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||||
|
File.WriteAllText(Out + "transition.txt",
|
||||||
|
$"oldZone={oldZone}\nnewZone={stage.CurrentZone?.NodeId ?? -1}\ntarget={target}\n" +
|
||||||
|
$"transitioning={stage.IsTransitioning}\nlocked={player.IsTransitionLocked}\n" +
|
||||||
|
$"boundsCenter={bounds?.ZoneCenter.ToString("R")}\nzoneCenter={stage.CurrentZone?.ZoneCenter.ToString("R")}\n");
|
||||||
|
Capture("05-transition-complete");
|
||||||
|
if (stage.IsTransitioning
|
||||||
|
|| player.IsTransitionLocked
|
||||||
|
|| stage.CurrentZone == null
|
||||||
|
|| stage.CurrentZone.NodeId != target)
|
||||||
|
{
|
||||||
|
Fail($"Zone transition failed. oldZone={oldZone}, target={target}, newZone={stage.CurrentZone?.NodeId ?? -1}, transitioning={stage.IsTransitioning}, locked={player.IsTransitionLocked}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
run.EndRunAsClear();
|
||||||
|
yield return null;
|
||||||
|
RunClearPanel clear = FindAnyObjectByType<RunClearPanel>();
|
||||||
|
File.WriteAllText(Out + "clear.txt", $"runCleared={run.IsRunCleared}\ntimeScale={Time.timeScale:R}\npanel={(clear != null)}\n");
|
||||||
|
Capture("06-run-clear");
|
||||||
|
if (!run.IsRunCleared || Time.timeScale > 0f || clear == null)
|
||||||
|
{
|
||||||
|
Fail($"Run clear validation failed. runCleared={run.IsRunCleared}, timeScale={Time.timeScale:R}, panel={(clear != null)}");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
File.WriteAllText(Out + "completed.marker", DateTime.Now.ToString("O"));
|
||||||
|
DeleteRunMarker();
|
||||||
|
EditorApplication.ExitPlaymode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator ResolveBlockingSelections(RunManager run)
|
||||||
|
{
|
||||||
|
ArtifactRewardController rewards = FindAnyObjectByType<ArtifactRewardController>();
|
||||||
|
RunTutorialController tutorial = FindAnyObjectByType<RunTutorialController>();
|
||||||
|
float deadline = Time.realtimeSinceStartup + 6f;
|
||||||
|
|
||||||
|
while (Time.realtimeSinceStartup < deadline)
|
||||||
|
{
|
||||||
|
bool handled = false;
|
||||||
|
|
||||||
|
if (rewards != null && rewards.DebugIsSelectionVisible)
|
||||||
|
{
|
||||||
|
if (rewards.DebugShownCount <= 0 || !rewards.DebugChooseOption(0))
|
||||||
|
{
|
||||||
|
Fail("Could not resolve startup artifact selection.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
else if (tutorial != null && tutorial.DebugIsVisible)
|
||||||
|
{
|
||||||
|
if (!tutorial.DebugContinue())
|
||||||
|
{
|
||||||
|
Fail("Could not dismiss startup tutorial.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handled && !run.IsSelectionOpen && Time.timeScale > 0f)
|
||||||
|
{
|
||||||
|
yield return null;
|
||||||
|
if (!run.IsSelectionOpen && Time.timeScale > 0f)
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator Hold(Key key, float seconds)
|
||||||
|
{
|
||||||
|
if (Keyboard.current == null) yield break;
|
||||||
|
InputSystem.QueueStateEvent(Keyboard.current, new KeyboardState(key));
|
||||||
|
float end = Time.realtimeSinceStartup + seconds;
|
||||||
|
while (Time.realtimeSinceStartup < end) yield return null;
|
||||||
|
InputSystem.QueueStateEvent(Keyboard.current, new KeyboardState());
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator Press(Key key)
|
||||||
|
{
|
||||||
|
if (Keyboard.current == null) yield break;
|
||||||
|
InputSystem.QueueStateEvent(Keyboard.current, new KeyboardState(key));
|
||||||
|
yield return null;
|
||||||
|
InputSystem.QueueStateEvent(Keyboard.current, new KeyboardState());
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Capture(string name)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Out);
|
||||||
|
Camera camera = Camera.main;
|
||||||
|
if (camera == null) return;
|
||||||
|
RenderTexture previous = camera.targetTexture;
|
||||||
|
RenderTexture active = RenderTexture.active;
|
||||||
|
var target = new RenderTexture(1280, 720, 24);
|
||||||
|
var texture = new Texture2D(1280, 720, TextureFormat.RGB24, false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
camera.targetTexture = target; camera.Render(); RenderTexture.active = target;
|
||||||
|
texture.ReadPixels(new Rect(0, 0, 1280, 720), 0, 0); texture.Apply();
|
||||||
|
File.WriteAllBytes(Out + name + ".png", texture.EncodeToPNG());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
camera.targetTexture = previous; RenderTexture.active = active;
|
||||||
|
DestroyImmediate(texture); target.Release(); DestroyImmediate(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Fail(string message)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Out);
|
||||||
|
File.WriteAllText(Out + "validation-error.txt", message);
|
||||||
|
DeleteRunMarker();
|
||||||
|
EditorApplication.ExitPlaymode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteRunMarker()
|
||||||
|
{
|
||||||
|
string marker = Out + "run.marker";
|
||||||
|
if (File.Exists(marker))
|
||||||
|
{
|
||||||
|
File.Delete(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5ec05c0337f620543ba3d30114ee07fe
|
||||||
@@ -1271,7 +1271,7 @@ namespace BumpCombat.Editor
|
|||||||
body.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
body.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||||
|
|
||||||
CircleCollider2D collider = player.AddComponent<CircleCollider2D>();
|
CircleCollider2D collider = player.AddComponent<CircleCollider2D>();
|
||||||
collider.offset = new Vector2(0f, -0.28f);
|
collider.offset = new Vector2(0f, -0.06f);
|
||||||
collider.radius = 0.22f;
|
collider.radius = 0.22f;
|
||||||
|
|
||||||
player.AddComponent<PlayerStats>();
|
player.AddComponent<PlayerStats>();
|
||||||
@@ -1394,10 +1394,7 @@ namespace BumpCombat.Editor
|
|||||||
radius,
|
radius,
|
||||||
experience,
|
experience,
|
||||||
animationLeadTime);
|
animationLeadTime);
|
||||||
definition.ConfigureRole(
|
definition.ConfigureRole(EnemyRole.Normal);
|
||||||
kind == EnemyKind.Slime || kind == EnemyKind.Skeleton
|
|
||||||
? EnemyRole.Crowd
|
|
||||||
: EnemyRole.Normal);
|
|
||||||
AssetDatabase.CreateAsset(definition, path);
|
AssetDatabase.CreateAsset(definition, path);
|
||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace BumpCombat.Enemies
|
|||||||
private bool lancerTipNeedsRebase;
|
private bool lancerTipNeedsRebase;
|
||||||
private bool necrofireRayActive;
|
private bool necrofireRayActive;
|
||||||
private bool necrofireRayHitConfirmed;
|
private bool necrofireRayHitConfirmed;
|
||||||
|
private float necrofireRayLength;
|
||||||
private bool meleeAttackActive;
|
private bool meleeAttackActive;
|
||||||
private bool meleeHitConfirmed;
|
private bool meleeHitConfirmed;
|
||||||
private StageEnemyEffectVisual necrofireRayVisual;
|
private StageEnemyEffectVisual necrofireRayVisual;
|
||||||
@@ -392,12 +393,16 @@ namespace BumpCombat.Enemies
|
|||||||
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
|
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
|
||||||
? lockedDirection
|
? lockedDirection
|
||||||
: Vector2.right;
|
: Vector2.right;
|
||||||
float length = controller.AttackLength;
|
necrofireRayLength = GetTerrainBlockedLength(
|
||||||
|
attackOrigin,
|
||||||
|
direction,
|
||||||
|
controller.AttackLength,
|
||||||
|
controller.AttackWidth * 0.5f);
|
||||||
necrofireRayVisual = StageEnemyEffectVisual.PlayRay(
|
necrofireRayVisual = StageEnemyEffectVisual.PlayRay(
|
||||||
gameObject,
|
gameObject,
|
||||||
attackOrigin,
|
attackOrigin,
|
||||||
direction,
|
direction,
|
||||||
length,
|
necrofireRayLength,
|
||||||
controller.AttackWidth,
|
controller.AttackWidth,
|
||||||
controller.ActiveDuration);
|
controller.ActiveDuration);
|
||||||
TickNecrofireRay();
|
TickNecrofireRay();
|
||||||
@@ -982,7 +987,13 @@ namespace BumpCombat.Enemies
|
|||||||
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
|
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
|
||||||
? lockedDirection
|
? lockedDirection
|
||||||
: Vector2.right;
|
: Vector2.right;
|
||||||
float length = controller.AttackLength;
|
float length = necrofireRayLength > 0f
|
||||||
|
? necrofireRayLength
|
||||||
|
: GetTerrainBlockedLength(
|
||||||
|
attackOrigin,
|
||||||
|
direction,
|
||||||
|
controller.AttackLength,
|
||||||
|
controller.AttackWidth * 0.5f);
|
||||||
float width = Mathf.Max(0.001f, controller.AttackWidth);
|
float width = Mathf.Max(0.001f, controller.AttackWidth);
|
||||||
Vector2 center = attackOrigin + direction * (length * 0.5f);
|
Vector2 center = attackOrigin + direction * (length * 0.5f);
|
||||||
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
|
||||||
@@ -1018,5 +1029,44 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float GetTerrainBlockedLength(
|
||||||
|
Vector2 origin,
|
||||||
|
Vector2 direction,
|
||||||
|
float maxLength,
|
||||||
|
float radius)
|
||||||
|
{
|
||||||
|
float clampedLength = Mathf.Max(0f, maxLength);
|
||||||
|
if (clampedLength <= 0f || direction.sqrMagnitude <= 0.0001f)
|
||||||
|
{
|
||||||
|
return clampedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
RaycastHit2D[] hits = Physics2D.CircleCastAll(
|
||||||
|
origin,
|
||||||
|
Mathf.Max(0.01f, radius),
|
||||||
|
direction.normalized,
|
||||||
|
clampedLength,
|
||||||
|
Physics2D.AllLayers);
|
||||||
|
float blockedLength = clampedLength;
|
||||||
|
for (int i = 0; i < hits.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D collider = hits[i].collider;
|
||||||
|
if (collider == null || collider.isTrigger)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTerrain = collider.GetComponentInParent<BumpCombat.Stage.StageObstacle>() != null
|
||||||
|
|| collider.name.StartsWith("Wall_")
|
||||||
|
|| collider.gameObject.name.Contains("Wall");
|
||||||
|
if (isTerrain)
|
||||||
|
{
|
||||||
|
blockedLength = Mathf.Min(blockedLength, hits[i].distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockedLength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using BumpCombat.Constants;
|
|||||||
using BumpCombat.Core;
|
using BumpCombat.Core;
|
||||||
using BumpCombat.Player;
|
using BumpCombat.Player;
|
||||||
using BumpCombat.Progression;
|
using BumpCombat.Progression;
|
||||||
|
using BumpCombat.Stage;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace BumpCombat.Enemies
|
namespace BumpCombat.Enemies
|
||||||
@@ -49,6 +50,7 @@ namespace BumpCombat.Enemies
|
|||||||
Active,
|
Active,
|
||||||
Recovery,
|
Recovery,
|
||||||
Dead,
|
Dead,
|
||||||
|
Idle,
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D), typeof(SpriteRenderer))]
|
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D), typeof(SpriteRenderer))]
|
||||||
@@ -68,6 +70,7 @@ namespace BumpCombat.Enemies
|
|||||||
private static readonly int HurtParameter = Animator.StringToHash("Hurt");
|
private static readonly int HurtParameter = Animator.StringToHash("Hurt");
|
||||||
private static readonly int DeathState = Animator.StringToHash("Base Layer.Death");
|
private static readonly int DeathState = Animator.StringToHash("Base Layer.Death");
|
||||||
private static readonly Collider2D[] SeparationBuffer = new Collider2D[16];
|
private static readonly Collider2D[] SeparationBuffer = new Collider2D[16];
|
||||||
|
private static readonly Dictionary<int, HashSet<EnemyController>> EncounterGroups = new();
|
||||||
private static Sprite launchWhiteSprite;
|
private static Sprite launchWhiteSprite;
|
||||||
private static int nextSpawnLifetimeIdentity;
|
private static int nextSpawnLifetimeIdentity;
|
||||||
[SerializeField] private EnemyDefinition definition;
|
[SerializeField] private EnemyDefinition definition;
|
||||||
@@ -85,6 +88,9 @@ namespace BumpCombat.Enemies
|
|||||||
private EnemyAttack enemyAttack;
|
private EnemyAttack enemyAttack;
|
||||||
private EnemyModel enemyModel;
|
private EnemyModel enemyModel;
|
||||||
private ArtifactStatusVisual statusVisual;
|
private ArtifactStatusVisual statusVisual;
|
||||||
|
private readonly List<Renderer> fogRenderers = new();
|
||||||
|
private readonly List<bool> fogRendererForceOffStates = new();
|
||||||
|
private bool fogVisibilityHidden;
|
||||||
private ActiveArtifactController activeArtifactController;
|
private ActiveArtifactController activeArtifactController;
|
||||||
private Transform player;
|
private Transform player;
|
||||||
private PlayerHealth playerHealth;
|
private PlayerHealth playerHealth;
|
||||||
@@ -97,6 +103,7 @@ namespace BumpCombat.Enemies
|
|||||||
private float igniteNextTickIn;
|
private float igniteNextTickIn;
|
||||||
private float igniteTickInterval;
|
private float igniteTickInterval;
|
||||||
private float igniteTickDamage;
|
private float igniteTickDamage;
|
||||||
|
private EnemyDamageSource igniteDamageSource;
|
||||||
private float shockRemaining;
|
private float shockRemaining;
|
||||||
private float shockIncrease;
|
private float shockIncrease;
|
||||||
private float vulnerabilityRemaining;
|
private float vulnerabilityRemaining;
|
||||||
@@ -123,6 +130,7 @@ namespace BumpCombat.Enemies
|
|||||||
private IEnemyDamageGate damageGate;
|
private IEnemyDamageGate damageGate;
|
||||||
private IEnemyAttackPatternGate attackPatternGate;
|
private IEnemyAttackPatternGate attackPatternGate;
|
||||||
private IEnemyActionGate actionGate;
|
private IEnemyActionGate actionGate;
|
||||||
|
private BumpCombat.Defense.DefenseRouteAgent defenseRouteAgent;
|
||||||
private bool summoned;
|
private bool summoned;
|
||||||
private bool phaseSummon;
|
private bool phaseSummon;
|
||||||
private bool summonedEventLike;
|
private bool summonedEventLike;
|
||||||
@@ -142,6 +150,15 @@ namespace BumpCombat.Enemies
|
|||||||
private readonly HashSet<string> recentArtifactContactKeys = new(StringComparer.Ordinal);
|
private readonly HashSet<string> recentArtifactContactKeys = new(StringComparer.Ordinal);
|
||||||
private readonly Queue<string> recentArtifactContactOrder = new();
|
private readonly Queue<string> recentArtifactContactOrder = new();
|
||||||
private const int MaximumRememberedArtifactContacts = 128;
|
private const int MaximumRememberedArtifactContacts = 128;
|
||||||
|
private const int RoamTargetSelectionAttempts = 12;
|
||||||
|
private bool hasDetectedPlayer;
|
||||||
|
private int encounterGroupId = -1;
|
||||||
|
private bool hasRoamAnchor;
|
||||||
|
private Vector2 roamAnchor;
|
||||||
|
private Vector2 roamTarget;
|
||||||
|
private float roamPauseRemaining;
|
||||||
|
private float sightMemoryRemaining;
|
||||||
|
private Vector2 lastSeenPlayerPosition;
|
||||||
|
|
||||||
public static int AliveCount { get; private set; }
|
public static int AliveCount { get; private set; }
|
||||||
public static int CrowdAliveCount { get; private set; }
|
public static int CrowdAliveCount { get; private set; }
|
||||||
@@ -160,6 +177,8 @@ namespace BumpCombat.Enemies
|
|||||||
public float CurrentHealth { get; private set; }
|
public float CurrentHealth { get; private set; }
|
||||||
public float LastAppliedDamage { get; private set; }
|
public float LastAppliedDamage { get; private set; }
|
||||||
public float LastReportedDamage { get; private set; }
|
public float LastReportedDamage { get; private set; }
|
||||||
|
public EnemyDamageSource LastDamageSource { get; private set; }
|
||||||
|
public float MaximumHealthAtDeath { get; private set; }
|
||||||
public int CurrentAttackPatternIndex => attackPatternIndex;
|
public int CurrentAttackPatternIndex => attackPatternIndex;
|
||||||
public EnemyAttackPattern CurrentAttackPattern => definition != null
|
public EnemyAttackPattern CurrentAttackPattern => definition != null
|
||||||
? definition.GetAttackPattern(attackPatternIndex)
|
? definition.GetAttackPattern(attackPatternIndex)
|
||||||
@@ -216,6 +235,12 @@ namespace BumpCombat.Enemies
|
|||||||
public Vector2 GroundAnchorPosition => body != null
|
public Vector2 GroundAnchorPosition => body != null
|
||||||
? body.position
|
? body.position
|
||||||
: transform.position;
|
: transform.position;
|
||||||
|
public Transform CurrentVisualTransform => airborneVisualObject != null
|
||||||
|
? airborneVisualObject.transform
|
||||||
|
: transform;
|
||||||
|
public int CurrentVisualSortingOrder => airborneVisualObject != null
|
||||||
|
? spriteRenderer.sortingOrder + 2
|
||||||
|
: spriteRenderer.sortingOrder;
|
||||||
public bool IsKnockbackActive => knockbackVelocity.sqrMagnitude > 0.0025f;
|
public bool IsKnockbackActive => knockbackVelocity.sqrMagnitude > 0.0025f;
|
||||||
public bool IsLaunchVisualActive => launchVisualCoroutine != null;
|
public bool IsLaunchVisualActive => launchVisualCoroutine != null;
|
||||||
public bool IsAttackSequenceInProgress => State == EnemyState.Warning
|
public bool IsAttackSequenceInProgress => State == EnemyState.Warning
|
||||||
@@ -228,6 +253,11 @@ namespace BumpCombat.Enemies
|
|||||||
public EnemyController SummonOwner => summonOwner;
|
public EnemyController SummonOwner => summonOwner;
|
||||||
public bool IsProtectedFromCleanup => runEventRole.HasValue || phaseSummon;
|
public bool IsProtectedFromCleanup => runEventRole.HasValue || phaseSummon;
|
||||||
public bool HasContactDamage => definition != null && definition.HasContactDamage;
|
public bool HasContactDamage => definition != null && definition.HasContactDamage;
|
||||||
|
public bool IsFlying => definition != null && definition.IsFlying;
|
||||||
|
public Collider2D BodyCollider => bodyCollider != null ? bodyCollider : (bodyCollider = GetComponent<Collider2D>());
|
||||||
|
public bool HasDetectedPlayer => hasDetectedPlayer;
|
||||||
|
public int EncounterGroupId => encounterGroupId;
|
||||||
|
public float SightRange => enemyModel != null ? enemyModel.SightRange : (definition != null ? definition.SightRange : 8f);
|
||||||
public bool IsRangedAttackStartAllowed => definition == null
|
public bool IsRangedAttackStartAllowed => definition == null
|
||||||
|| !definition.IsRanged
|
|| !definition.IsRanged
|
||||||
|| CanStartRangedAttack();
|
|| CanStartRangedAttack();
|
||||||
@@ -369,6 +399,21 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
public void ConfigureEncounterGroup(int groupId, Vector2 anchor)
|
||||||
|
{
|
||||||
|
UnregisterEncounterGroup();
|
||||||
|
encounterGroupId = groupId;
|
||||||
|
roamAnchor = anchor;
|
||||||
|
roamTarget = anchor;
|
||||||
|
hasRoamAnchor = true;
|
||||||
|
RegisterEncounterGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureSightRangeMultiplier(float multiplier)
|
||||||
|
{
|
||||||
|
EnsureEnemyModel().ConfigureSightRangeMultiplier(multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
nextSpawnLifetimeIdentity = nextSpawnLifetimeIdentity == int.MaxValue
|
nextSpawnLifetimeIdentity = nextSpawnLifetimeIdentity == int.MaxValue
|
||||||
@@ -386,6 +431,7 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
bodyCollider = GetComponent<Collider2D>();
|
bodyCollider = GetComponent<Collider2D>();
|
||||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||||
|
CacheFogRenderers();
|
||||||
animator = GetComponent<Animator>();
|
animator = GetComponent<Animator>();
|
||||||
enemyAttack = GetComponent<EnemyAttack>();
|
enemyAttack = GetComponent<EnemyAttack>();
|
||||||
ConfigureEnemyModel();
|
ConfigureEnemyModel();
|
||||||
@@ -433,6 +479,12 @@ namespace BumpCombat.Enemies
|
|||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
|
if (fogVisibilityHidden)
|
||||||
|
{
|
||||||
|
ForceFogRenderersOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterEncounterGroup();
|
||||||
if (enemyModel != null)
|
if (enemyModel != null)
|
||||||
{
|
{
|
||||||
enemyModel.OnStatChanged += HandleCharacterStatChanged;
|
enemyModel.OnStatChanged += HandleCharacterStatChanged;
|
||||||
@@ -455,12 +507,20 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeepBodyInsideArena();
|
KeepBodyInsideArena();
|
||||||
|
if (IsFlying)
|
||||||
|
{
|
||||||
|
StageObstacle.RegisterFlyingEnemy(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
InitializeGroggyTierFromDefinition();
|
InitializeGroggyTierFromDefinition();
|
||||||
KeepBodyInsideArena();
|
KeepBodyInsideArena();
|
||||||
|
if (IsFlying)
|
||||||
|
{
|
||||||
|
StageObstacle.RegisterFlyingEnemy(this);
|
||||||
|
}
|
||||||
if (definition == null)
|
if (definition == null)
|
||||||
{
|
{
|
||||||
enabled = false;
|
enabled = false;
|
||||||
@@ -477,8 +537,16 @@ namespace BumpCombat.Enemies
|
|||||||
player = playerController.transform;
|
player = playerController.transform;
|
||||||
playerHealth = playerController.GetComponent<PlayerHealth>();
|
playerHealth = playerController.GetComponent<PlayerHealth>();
|
||||||
activeArtifactController = playerController.GetComponent<ActiveArtifactController>();
|
activeArtifactController = playerController.GetComponent<ActiveArtifactController>();
|
||||||
|
if (!hasRoamAnchor)
|
||||||
|
{
|
||||||
|
roamAnchor = body != null ? body.position : (Vector2)transform.position;
|
||||||
|
roamTarget = roamAnchor;
|
||||||
|
hasRoamAnchor = true;
|
||||||
|
}
|
||||||
InitializeInitialShieldColor();
|
InitializeInitialShieldColor();
|
||||||
CurrentHealth = MaximumHealth;
|
CurrentHealth = MaximumHealth;
|
||||||
|
LastDamageSource = EnemyDamageSource.None;
|
||||||
|
MaximumHealthAtDeath = 0f;
|
||||||
float summonDuration = summoned ? GetSummonAnimationDuration() : 0f;
|
float summonDuration = summoned ? GetSummonAnimationDuration() : 0f;
|
||||||
bool playSummonAnimation = summonDuration > 0f;
|
bool playSummonAnimation = summonDuration > 0f;
|
||||||
EnterState(
|
EnterState(
|
||||||
@@ -507,6 +575,7 @@ namespace BumpCombat.Enemies
|
|||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
|
UnregisterEncounterGroup();
|
||||||
if (enemyModel != null)
|
if (enemyModel != null)
|
||||||
{
|
{
|
||||||
enemyModel.OnStatChanged -= HandleCharacterStatChanged;
|
enemyModel.OnStatChanged -= HandleCharacterStatChanged;
|
||||||
@@ -601,6 +670,13 @@ namespace BumpCombat.Enemies
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defenseRouteAgent ??= GetComponent<BumpCombat.Defense.DefenseRouteAgent>();
|
||||||
|
if (defenseRouteAgent != null
|
||||||
|
&& defenseRouteAgent.TryDriveFixedUpdate(Time.fixedDeltaTime))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!AllowsStateMovement(State, AttackShape))
|
if (!AllowsStateMovement(State, AttackShape))
|
||||||
{
|
{
|
||||||
body.linearVelocity = Vector2.zero;
|
body.linearVelocity = Vector2.zero;
|
||||||
@@ -611,12 +687,35 @@ namespace BumpCombat.Enemies
|
|||||||
{
|
{
|
||||||
case EnemyState.Spawn:
|
case EnemyState.Spawn:
|
||||||
if (stateTimer <= 0f)
|
if (stateTimer <= 0f)
|
||||||
|
{
|
||||||
|
if (CheckPlayerDetection())
|
||||||
{
|
{
|
||||||
EnterState(EnemyState.Chase, 0f);
|
EnterState(EnemyState.Chase, 0f);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnterState(EnemyState.Idle, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EnemyState.Idle:
|
||||||
|
SetAttacking(false);
|
||||||
|
if (CheckPlayerDetection())
|
||||||
|
{
|
||||||
|
EnterState(EnemyState.Chase, 0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateRoaming();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EnemyState.Chase:
|
case EnemyState.Chase:
|
||||||
|
if (!RefreshChaseAwareness())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (actionGate != null && actionGate.IsAttackLocked)
|
if (actionGate != null && actionGate.IsAttackLocked)
|
||||||
{
|
{
|
||||||
body.linearVelocity = Vector2.zero;
|
body.linearVelocity = Vector2.zero;
|
||||||
@@ -731,7 +830,8 @@ namespace BumpCombat.Enemies
|
|||||||
Vector2 knockbackDirection,
|
Vector2 knockbackDirection,
|
||||||
float knockbackForce,
|
float knockbackForce,
|
||||||
bool playHurtAnimation = true,
|
bool playHurtAnimation = true,
|
||||||
bool isNormalBump = false)
|
bool isNormalBump = false,
|
||||||
|
EnemyDamageSource damageSource = EnemyDamageSource.None)
|
||||||
{
|
{
|
||||||
return TryTakeDamageWithEffects(
|
return TryTakeDamageWithEffects(
|
||||||
damage,
|
damage,
|
||||||
@@ -741,6 +841,7 @@ namespace BumpCombat.Enemies
|
|||||||
isNormalBump,
|
isNormalBump,
|
||||||
IsShocked,
|
IsShocked,
|
||||||
IsBumpVulnerable,
|
IsBumpVulnerable,
|
||||||
|
damageSource,
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,9 +853,11 @@ namespace BumpCombat.Enemies
|
|||||||
bool isNormalBump,
|
bool isNormalBump,
|
||||||
bool isShockedAtHit,
|
bool isShockedAtHit,
|
||||||
bool isBumpVulnerableAtHit,
|
bool isBumpVulnerableAtHit,
|
||||||
|
EnemyDamageSource damageSource,
|
||||||
out bool noDamageAfterEffects)
|
out bool noDamageAfterEffects)
|
||||||
{
|
{
|
||||||
noDamageAfterEffects = false;
|
noDamageAfterEffects = false;
|
||||||
|
Alert();
|
||||||
if (IsDead || damage <= 0f)
|
if (IsDead || damage <= 0f)
|
||||||
{
|
{
|
||||||
LastAppliedDamage = 0f;
|
LastAppliedDamage = 0f;
|
||||||
@@ -802,13 +905,16 @@ namespace BumpCombat.Enemies
|
|||||||
? damageTaken
|
? damageTaken
|
||||||
: calculatedDamage;
|
: calculatedDamage;
|
||||||
float healthBeforeDamage = CurrentHealth;
|
float healthBeforeDamage = CurrentHealth;
|
||||||
|
float maximumHealthBeforeDamage = MaximumHealth;
|
||||||
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageTaken);
|
CurrentHealth = Mathf.Max(0f, CurrentHealth - damageTaken);
|
||||||
LastAppliedDamage = healthBeforeDamage - CurrentHealth;
|
LastAppliedDamage = healthBeforeDamage - CurrentHealth;
|
||||||
|
LastDamageSource = damageSource;
|
||||||
OnDamageApplied?.Invoke(healthBeforeDamage, LastAppliedDamage);
|
OnDamageApplied?.Invoke(healthBeforeDamage, LastAppliedDamage);
|
||||||
bool damageReactionBlocked = damageGate != null
|
bool damageReactionBlocked = damageGate != null
|
||||||
&& damageGate.IsDamageBlocked;
|
&& damageGate.IsDamageBlocked;
|
||||||
if (CurrentHealth <= 0f)
|
if (CurrentHealth <= 0f)
|
||||||
{
|
{
|
||||||
|
MaximumHealthAtDeath = maximumHealthBeforeDamage;
|
||||||
Die();
|
Die();
|
||||||
}
|
}
|
||||||
else if (damageReactionBlocked)
|
else if (damageReactionBlocked)
|
||||||
@@ -817,13 +923,13 @@ namespace BumpCombat.Enemies
|
|||||||
// desperation phase. Do not append the opening hit's
|
// desperation phase. Do not append the opening hit's
|
||||||
// knockback or hurt reaction after that transition.
|
// knockback or hurt reaction after that transition.
|
||||||
knockbackVelocity = Vector2.zero;
|
knockbackVelocity = Vector2.zero;
|
||||||
body.linearVelocity = Vector2.zero;
|
if (body != null) body.linearVelocity = Vector2.zero;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (knockbackVelocity.sqrMagnitude <= 0.0025f)
|
if (knockbackVelocity.sqrMagnitude <= 0.0025f)
|
||||||
{
|
{
|
||||||
knockbackStartPosition = body.position;
|
knockbackStartPosition = body != null ? body.position : (Vector2)transform.position;
|
||||||
}
|
}
|
||||||
knockbackVelocity += knockbackDirection.normalized
|
knockbackVelocity += knockbackDirection.normalized
|
||||||
* knockbackForce
|
* knockbackForce
|
||||||
@@ -914,7 +1020,8 @@ namespace BumpCombat.Enemies
|
|||||||
float knockbackForce,
|
float knockbackForce,
|
||||||
ArtifactColor artifactColor,
|
ArtifactColor artifactColor,
|
||||||
int castIdentity,
|
int castIdentity,
|
||||||
bool playHurtAnimation = true)
|
bool playHurtAnimation = true,
|
||||||
|
EnemyDamageSource damageSource = EnemyDamageSource.None)
|
||||||
{
|
{
|
||||||
lastArtifactContactCounted = false;
|
lastArtifactContactCounted = false;
|
||||||
if (IsDead)
|
if (IsDead)
|
||||||
@@ -956,7 +1063,8 @@ namespace BumpCombat.Enemies
|
|||||||
knockbackDirection,
|
knockbackDirection,
|
||||||
knockbackForce,
|
knockbackForce,
|
||||||
playHurtAnimation,
|
playHurtAnimation,
|
||||||
false)
|
false,
|
||||||
|
damageSource)
|
||||||
? EnemyArtifactHitResult.DamageApplied
|
? EnemyArtifactHitResult.DamageApplied
|
||||||
: EnemyArtifactHitResult.Rejected;
|
: EnemyArtifactHitResult.Rejected;
|
||||||
}
|
}
|
||||||
@@ -1004,7 +1112,8 @@ namespace BumpCombat.Enemies
|
|||||||
public bool ApplyIgnite(
|
public bool ApplyIgnite(
|
||||||
float duration,
|
float duration,
|
||||||
float tickInterval,
|
float tickInterval,
|
||||||
float damagePerTick)
|
float damagePerTick,
|
||||||
|
EnemyDamageSource damageSource = EnemyDamageSource.Player)
|
||||||
{
|
{
|
||||||
if (IsDead
|
if (IsDead
|
||||||
|| duration <= 0f
|
|| duration <= 0f
|
||||||
@@ -1017,6 +1126,7 @@ namespace BumpCombat.Enemies
|
|||||||
bool wasInactive = igniteRemaining <= 0f;
|
bool wasInactive = igniteRemaining <= 0f;
|
||||||
igniteRemaining = Mathf.Max(igniteRemaining, duration);
|
igniteRemaining = Mathf.Max(igniteRemaining, duration);
|
||||||
igniteTickDamage = Mathf.Max(igniteTickDamage, damagePerTick);
|
igniteTickDamage = Mathf.Max(igniteTickDamage, damagePerTick);
|
||||||
|
igniteDamageSource = damageSource;
|
||||||
if (wasInactive)
|
if (wasInactive)
|
||||||
{
|
{
|
||||||
igniteTickInterval = tickInterval;
|
igniteTickInterval = tickInterval;
|
||||||
@@ -1085,6 +1195,7 @@ namespace BumpCombat.Enemies
|
|||||||
false,
|
false,
|
||||||
isShockedAtTick,
|
isShockedAtTick,
|
||||||
false,
|
false,
|
||||||
|
igniteDamageSource,
|
||||||
out bool noDamageAfterEffects);
|
out bool noDamageAfterEffects);
|
||||||
if ((!damageApplied && !noDamageAfterEffects) || IsDead)
|
if ((!damageApplied && !noDamageAfterEffects) || IsDead)
|
||||||
{
|
{
|
||||||
@@ -1821,19 +1932,28 @@ namespace BumpCombat.Enemies
|
|||||||
Vector2 attackSlot = GetHorizontalRootAttackSlot(
|
Vector2 attackSlot = GetHorizontalRootAttackSlot(
|
||||||
attackTarget,
|
attackTarget,
|
||||||
direction);
|
direction);
|
||||||
|
if (!IsFlying)
|
||||||
|
{
|
||||||
|
ZoneInstance currentZone = StageManager.Instance != null
|
||||||
|
? StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
|
if (currentZone != null
|
||||||
|
&& currentZone.Preset != null
|
||||||
|
&& RoomPathfinder.TryGetNextWaypoint(
|
||||||
|
currentZone,
|
||||||
|
body.position,
|
||||||
|
attackSlot,
|
||||||
|
false,
|
||||||
|
out Vector2 waypoint))
|
||||||
|
{
|
||||||
|
attackSlot = waypoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vector2 toAttackSlot = attackSlot - body.position;
|
Vector2 toAttackSlot = attackSlot - body.position;
|
||||||
movementDirection = toAttackSlot.sqrMagnitude > 0.0001f
|
movementDirection = toAttackSlot.sqrMagnitude > 0.0001f
|
||||||
? toAttackSlot.normalized
|
? toAttackSlot.normalized
|
||||||
: Vector2.zero;
|
: Vector2.zero;
|
||||||
}
|
}
|
||||||
if (IsCrowd)
|
|
||||||
{
|
|
||||||
Vector2 toApproachPoint =
|
|
||||||
(Vector2)player.position + crowdApproachOffset - body.position;
|
|
||||||
movementDirection = toApproachPoint.sqrMagnitude > 0.01f
|
|
||||||
? toApproachPoint.normalized
|
|
||||||
: Vector2.zero;
|
|
||||||
}
|
|
||||||
else if (definition.IsRanged && rangedAttackOutsideGate)
|
else if (definition.IsRanged && rangedAttackOutsideGate)
|
||||||
{
|
{
|
||||||
// A ranged attacker must enter the visible, reachable area
|
// A ranged attacker must enter the visible, reachable area
|
||||||
@@ -1845,11 +1965,45 @@ namespace BumpCombat.Enemies
|
|||||||
{
|
{
|
||||||
movementDirection = -direction;
|
movementDirection = -direction;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vector2 targetPoint = attackTarget;
|
||||||
|
if (IsCrowd)
|
||||||
|
{
|
||||||
|
float distToPlayer = (attackTarget - body.position).magnitude;
|
||||||
|
targetPoint = distToPlayer > 2.5f
|
||||||
|
? attackTarget
|
||||||
|
: (Vector2)player.position + crowdApproachOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsFlying)
|
||||||
|
{
|
||||||
|
ZoneInstance currentZone = StageManager.Instance != null ? StageManager.Instance.CurrentZone : null;
|
||||||
|
if (currentZone != null && currentZone.Preset != null)
|
||||||
|
{
|
||||||
|
if (RoomPathfinder.TryGetNextWaypoint(
|
||||||
|
currentZone,
|
||||||
|
body.position,
|
||||||
|
targetPoint,
|
||||||
|
IsFlying,
|
||||||
|
out Vector2 waypoint))
|
||||||
|
{
|
||||||
|
targetPoint = waypoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 toApproachPoint = targetPoint - body.position;
|
||||||
|
movementDirection = toApproachPoint.sqrMagnitude > 0.01f
|
||||||
|
? toApproachPoint.normalized
|
||||||
|
: Vector2.zero;
|
||||||
|
}
|
||||||
|
|
||||||
Vector2 separation = CalculateSeparation();
|
Vector2 separation = CalculateSeparation();
|
||||||
Vector2 velocity = Vector2.ClampMagnitude(
|
Vector2 desiredVelocity = Vector2.ClampMagnitude(
|
||||||
movementDirection + separation * separationStrength,
|
movementDirection + separation * separationStrength,
|
||||||
1f) * MoveSpeed;
|
1f) * MoveSpeed;
|
||||||
|
Vector2 velocity = CalculateWallSlideVelocity(desiredVelocity);
|
||||||
body.MovePosition(ClampEnemyPosition(
|
body.MovePosition(ClampEnemyPosition(
|
||||||
body.position + velocity * Time.fixedDeltaTime));
|
body.position + velocity * Time.fixedDeltaTime));
|
||||||
SetMoving(velocity.sqrMagnitude > 0.0001f);
|
SetMoving(velocity.sqrMagnitude > 0.0001f);
|
||||||
@@ -1864,10 +2018,11 @@ namespace BumpCombat.Enemies
|
|||||||
if (bounds != null)
|
if (bounds != null)
|
||||||
{
|
{
|
||||||
Vector2 actorExtents = bounds.SharedActorClampHalfExtents;
|
Vector2 actorExtents = bounds.SharedActorClampHalfExtents;
|
||||||
|
Vector2 center = bounds.ZoneCenter;
|
||||||
boundedTarget.y = Mathf.Clamp(
|
boundedTarget.y = Mathf.Clamp(
|
||||||
attackTarget.y,
|
attackTarget.y,
|
||||||
-actorExtents.y,
|
center.y - actorExtents.y,
|
||||||
actorExtents.y);
|
center.y + actorExtents.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal-root attacks measure range to the player's collider
|
// Horizontal-root attacks measure range to the player's collider
|
||||||
@@ -1919,9 +2074,10 @@ namespace BumpCombat.Enemies
|
|||||||
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
||||||
Vector2 reachable = bounds.GetReachableHalfExtents(
|
Vector2 reachable = bounds.GetReachableHalfExtents(
|
||||||
tuning.RangedApproachArenaInset);
|
tuning.RangedApproachArenaInset);
|
||||||
|
Vector2 center = bounds.ZoneCenter;
|
||||||
Vector2 interiorTarget = new(
|
Vector2 interiorTarget = new(
|
||||||
Mathf.Clamp(targetPosition.x, -reachable.x, reachable.x),
|
Mathf.Clamp(targetPosition.x, center.x - reachable.x, center.x + reachable.x),
|
||||||
Mathf.Clamp(targetPosition.y, -reachable.y, reachable.y));
|
Mathf.Clamp(targetPosition.y, center.y - reachable.y, center.y + reachable.y));
|
||||||
Camera camera = Camera.main;
|
Camera camera = Camera.main;
|
||||||
if (camera != null)
|
if (camera != null)
|
||||||
{
|
{
|
||||||
@@ -1933,10 +2089,10 @@ namespace BumpCombat.Enemies
|
|||||||
// Keep the approach point in the intersection of the
|
// Keep the approach point in the intersection of the
|
||||||
// reachable and visible interiors. This is essential when
|
// reachable and visible interiors. This is essential when
|
||||||
// the player is held against an outer map edge.
|
// the player is held against an outer map edge.
|
||||||
float minimumX = Mathf.Max(-reachable.x, cameraCenter.x - visible.x);
|
float minimumX = Mathf.Max(center.x - reachable.x, cameraCenter.x - visible.x);
|
||||||
float maximumX = Mathf.Min(reachable.x, cameraCenter.x + visible.x);
|
float maximumX = Mathf.Min(center.x + reachable.x, cameraCenter.x + visible.x);
|
||||||
float minimumY = Mathf.Max(-reachable.y, cameraCenter.y - visible.y);
|
float minimumY = Mathf.Max(center.y - reachable.y, cameraCenter.y - visible.y);
|
||||||
float maximumY = Mathf.Min(reachable.y, cameraCenter.y + visible.y);
|
float maximumY = Mathf.Min(center.y + reachable.y, cameraCenter.y + visible.y);
|
||||||
if (minimumX <= maximumX)
|
if (minimumX <= maximumX)
|
||||||
{
|
{
|
||||||
interiorTarget.x = Mathf.Clamp(
|
interiorTarget.x = Mathf.Clamp(
|
||||||
@@ -2095,6 +2251,11 @@ namespace BumpCombat.Enemies
|
|||||||
return cachedSeparation;
|
return cachedSeparation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetFacingDirection(Vector2 direction)
|
||||||
|
{
|
||||||
|
SetFacing(direction);
|
||||||
|
}
|
||||||
|
|
||||||
private void SetFacing(Vector2 direction)
|
private void SetFacing(Vector2 direction)
|
||||||
{
|
{
|
||||||
if (direction.sqrMagnitude <= 0f)
|
if (direction.sqrMagnitude <= 0f)
|
||||||
@@ -2126,6 +2287,276 @@ namespace BumpCombat.Enemies
|
|||||||
|| (state == EnemyState.Warning && attackAnimationStarted));
|
|| (state == EnemyState.Warning && attackAnimationStarted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Alert()
|
||||||
|
{
|
||||||
|
MarkPlayerDetected(false);
|
||||||
|
if (State == EnemyState.Idle)
|
||||||
|
{
|
||||||
|
EnterState(EnemyState.Chase, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckPlayerDetection()
|
||||||
|
{
|
||||||
|
if (!CanCurrentlyDetectPlayer()) return false;
|
||||||
|
MarkPlayerDetected(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanCurrentlyDetectPlayer()
|
||||||
|
{
|
||||||
|
EnsurePlayerReference();
|
||||||
|
if (player == null) return false;
|
||||||
|
|
||||||
|
Vector2 myPos = GroundAnchorPosition;
|
||||||
|
float distance = Vector2.Distance(myPos, player.position);
|
||||||
|
if (distance > SightRange) return false;
|
||||||
|
|
||||||
|
float proximityRange = GameplayConstants.Current.Enemies.ProximityAlertRange;
|
||||||
|
return distance <= proximityRange
|
||||||
|
|| RoomPathfinder.HasLineOfSight(myPos, player.position, IsFlying, bodyCollider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsurePlayerReference()
|
||||||
|
{
|
||||||
|
if (player != null) return;
|
||||||
|
|
||||||
|
PlayerController playerController = FindAnyObjectByType<PlayerController>();
|
||||||
|
if (playerController == null) return;
|
||||||
|
|
||||||
|
player = playerController.transform;
|
||||||
|
playerHealth = playerController.GetComponent<PlayerHealth>();
|
||||||
|
activeArtifactController = playerController.GetComponent<ActiveArtifactController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkPlayerDetected(bool alertGroup)
|
||||||
|
{
|
||||||
|
EnsurePlayerReference();
|
||||||
|
hasDetectedPlayer = true;
|
||||||
|
sightMemoryRemaining = GameplayConstants.Current.Enemies.LostSightMemoryDuration;
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
lastSeenPlayerPosition = player.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertGroup && encounterGroupId >= 0
|
||||||
|
&& EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||||
|
{
|
||||||
|
foreach (EnemyController member in group)
|
||||||
|
{
|
||||||
|
if (member == null || member == this || member.IsDead || member.IsEventEnemy)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
member.MarkPlayerDetected(false);
|
||||||
|
if (member.State == EnemyState.Idle)
|
||||||
|
{
|
||||||
|
member.EnterState(EnemyState.Chase, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool RefreshChaseAwareness()
|
||||||
|
{
|
||||||
|
if (IsEventEnemy)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanCurrentlyDetectPlayer())
|
||||||
|
{
|
||||||
|
MarkPlayerDetected(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sightMemoryRemaining = Mathf.Max(0f, sightMemoryRemaining - Time.fixedDeltaTime);
|
||||||
|
if (sightMemoryRemaining > 0f)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 toLastSeen = lastSeenPlayerPosition - body.position;
|
||||||
|
if (toLastSeen.sqrMagnitude <= 0.16f)
|
||||||
|
{
|
||||||
|
hasDetectedPlayer = false;
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
SetMoving(false);
|
||||||
|
EnterState(EnemyState.Idle, 0f);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveTowardPoint(lastSeenPlayerPosition, MoveSpeed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateRoaming()
|
||||||
|
{
|
||||||
|
if (IsEventEnemy || !hasRoamAnchor)
|
||||||
|
{
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
SetMoving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
||||||
|
if (roamPauseRemaining > 0f)
|
||||||
|
{
|
||||||
|
roamPauseRemaining = Mathf.Max(0f, roamPauseRemaining - Time.fixedDeltaTime);
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
SetMoving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((roamTarget - body.position).sqrMagnitude <= 0.16f)
|
||||||
|
{
|
||||||
|
if (!TryChooseRoamTarget(tuning))
|
||||||
|
{
|
||||||
|
roamTarget = body.position;
|
||||||
|
}
|
||||||
|
roamPauseRemaining = UnityEngine.Random.Range(tuning.RoamPauseMin, tuning.RoamPauseMax);
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
SetMoving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveTowardPoint(roamTarget, MoveSpeed * 0.6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryChooseRoamTarget(EnemyConstants tuning)
|
||||||
|
{
|
||||||
|
ZoneInstance currentZone = StageManager.Instance != null
|
||||||
|
? StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < RoamTargetSelectionAttempts; attempt++)
|
||||||
|
{
|
||||||
|
float radius = UnityEngine.Random.Range(tuning.RoamRadiusMin, tuning.RoamRadiusMax);
|
||||||
|
Vector2 offset = UnityEngine.Random.insideUnitCircle.normalized * radius;
|
||||||
|
Vector2 candidate = ClampEnemyPosition(roamAnchor + offset);
|
||||||
|
|
||||||
|
if (currentZone == null || currentZone.Preset == null)
|
||||||
|
{
|
||||||
|
roamTarget = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RoomPathfinder.TryGetReachableRoamPoint(
|
||||||
|
currentZone,
|
||||||
|
body.position,
|
||||||
|
candidate,
|
||||||
|
IsFlying,
|
||||||
|
out Vector2 reachableTarget))
|
||||||
|
{
|
||||||
|
roamTarget = reachableTarget;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveTowardPoint(Vector2 targetPoint, float speed)
|
||||||
|
{
|
||||||
|
if (!IsFlying)
|
||||||
|
{
|
||||||
|
ZoneInstance currentZone = StageManager.Instance != null
|
||||||
|
? StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
|
if (currentZone != null && currentZone.Preset != null
|
||||||
|
&& RoomPathfinder.TryGetNextWaypoint(
|
||||||
|
currentZone,
|
||||||
|
body.position,
|
||||||
|
targetPoint,
|
||||||
|
false,
|
||||||
|
out Vector2 waypoint))
|
||||||
|
{
|
||||||
|
targetPoint = waypoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 direction = targetPoint - body.position;
|
||||||
|
if (direction.sqrMagnitude <= 0.0001f)
|
||||||
|
{
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
SetMoving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
direction.Normalize();
|
||||||
|
SetFacing(direction);
|
||||||
|
Vector2 desiredVelocity = direction * speed;
|
||||||
|
Vector2 velocity = CalculateWallSlideVelocity(desiredVelocity);
|
||||||
|
body.MovePosition(ClampEnemyPosition(
|
||||||
|
body.position + velocity * Time.fixedDeltaTime));
|
||||||
|
SetMoving(velocity.sqrMagnitude > 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterEncounterGroup()
|
||||||
|
{
|
||||||
|
if (encounterGroupId < 0) return;
|
||||||
|
if (!EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||||
|
{
|
||||||
|
group = new HashSet<EnemyController>();
|
||||||
|
EncounterGroups.Add(encounterGroupId, group);
|
||||||
|
}
|
||||||
|
group.Add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterEncounterGroup()
|
||||||
|
{
|
||||||
|
if (encounterGroupId < 0
|
||||||
|
|| !EncounterGroups.TryGetValue(encounterGroupId, out HashSet<EnemyController> group))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Remove(this);
|
||||||
|
if (group.Count == 0)
|
||||||
|
{
|
||||||
|
EncounterGroups.Remove(encounterGroupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 CalculateWallSlideVelocity(Vector2 velocity)
|
||||||
|
{
|
||||||
|
if (velocity.sqrMagnitude < 0.0001f) return Vector2.zero;
|
||||||
|
|
||||||
|
float radius = GameplayConstants.Current.Enemies.WallSlideCastRadius;
|
||||||
|
float distance = GameplayConstants.Current.Enemies.WallSlideCastDistance;
|
||||||
|
Vector2 dir = velocity.normalized;
|
||||||
|
|
||||||
|
RaycastHit2D hit = Physics2D.CircleCast(
|
||||||
|
body.position,
|
||||||
|
radius,
|
||||||
|
dir,
|
||||||
|
distance);
|
||||||
|
|
||||||
|
if (hit.collider != null && !hit.collider.isTrigger && hit.collider != bodyCollider)
|
||||||
|
{
|
||||||
|
bool isObstacle = hit.collider.GetComponentInParent<StageObstacle>() != null;
|
||||||
|
bool isWall = hit.collider.name.StartsWith("Wall_") || hit.collider.gameObject.name.Contains("Wall");
|
||||||
|
|
||||||
|
if ((isObstacle && !IsFlying) || isWall)
|
||||||
|
{
|
||||||
|
Vector2 normal = hit.normal;
|
||||||
|
Vector2 tangent = new Vector2(-normal.y, normal.x);
|
||||||
|
if (Vector2.Dot(velocity, tangent) < 0f)
|
||||||
|
{
|
||||||
|
tangent = -tangent;
|
||||||
|
}
|
||||||
|
|
||||||
|
float forwardProjection = Vector2.Dot(velocity, tangent);
|
||||||
|
if (forwardProjection > 0.01f)
|
||||||
|
{
|
||||||
|
return tangent * (forwardProjection * 1.05f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool ShouldStartAttackAnimation(
|
public static bool ShouldStartAttackAnimation(
|
||||||
float warningTimeRemaining,
|
float warningTimeRemaining,
|
||||||
float leadTime,
|
float leadTime,
|
||||||
@@ -2294,6 +2725,89 @@ namespace BumpCombat.Enemies
|
|||||||
ReleaseSpriteVisibilityLease(reason);
|
ReleaseSpriteVisibilityLease(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetFogVisibility(bool visible)
|
||||||
|
{
|
||||||
|
if (visible)
|
||||||
|
{
|
||||||
|
if (!fogVisibilityHidden)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < fogRenderers.Count; i++)
|
||||||
|
{
|
||||||
|
if (fogRenderers[i] != null)
|
||||||
|
{
|
||||||
|
fogRenderers[i].forceRenderingOff = fogRendererForceOffStates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fogRenderers.Clear();
|
||||||
|
fogRendererForceOffStates.Clear();
|
||||||
|
fogVisibilityHidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fogVisibilityHidden)
|
||||||
|
{
|
||||||
|
CacheFogRenderers();
|
||||||
|
fogVisibilityHidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ForceFogRenderersOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheFogRenderers()
|
||||||
|
{
|
||||||
|
if (fogVisibilityHidden)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fogRenderers.Clear();
|
||||||
|
fogRendererForceOffStates.Clear();
|
||||||
|
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
|
||||||
|
for (int i = 0; i < renderers.Length; i++)
|
||||||
|
{
|
||||||
|
fogRenderers.Add(renderers[i]);
|
||||||
|
fogRendererForceOffStates.Add(renderers[i].forceRenderingOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForceFogRenderersOff()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < fogRenderers.Count; i++)
|
||||||
|
{
|
||||||
|
if (fogRenderers[i] != null)
|
||||||
|
{
|
||||||
|
fogRenderers[i].forceRenderingOff = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTransformChildrenChanged()
|
||||||
|
{
|
||||||
|
if (!fogVisibilityHidden)
|
||||||
|
{
|
||||||
|
CacheFogRenderers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
|
||||||
|
for (int i = 0; i < renderers.Length; i++)
|
||||||
|
{
|
||||||
|
if (fogRenderers.Contains(renderers[i]))
|
||||||
|
{
|
||||||
|
renderers[i].forceRenderingOff = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fogRenderers.Add(renderers[i]);
|
||||||
|
fogRendererForceOffStates.Add(renderers[i].forceRenderingOff);
|
||||||
|
renderers[i].forceRenderingOff = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AcquireSpriteVisibilityLease(string reason)
|
private void AcquireSpriteVisibilityLease(string reason)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(reason))
|
if (string.IsNullOrEmpty(reason))
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace BumpCombat.Enemies
|
||||||
|
{
|
||||||
|
public enum EnemyDamageSource
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Player,
|
||||||
|
Tower,
|
||||||
|
PassiveRelicExplosion,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c98a61b910a94e56aa5cdfa41234baf2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -297,6 +297,8 @@ namespace BumpCombat.Enemies
|
|||||||
[SerializeField] private EnemyAttackPattern[] attackPatterns;
|
[SerializeField] private EnemyAttackPattern[] attackPatterns;
|
||||||
[SerializeField, Min(0f)] private float separationRadius = 0.55f;
|
[SerializeField, Min(0f)] private float separationRadius = 0.55f;
|
||||||
[SerializeField, Min(0f)] private float separationStrength = 0.6f;
|
[SerializeField, Min(0f)] private float separationStrength = 0.6f;
|
||||||
|
[SerializeField] private bool isFlying;
|
||||||
|
[SerializeField, Min(0f)] private float sightRange = 8f;
|
||||||
|
|
||||||
public EnemyKind Kind => kind;
|
public EnemyKind Kind => kind;
|
||||||
public EnemyRole Role => role;
|
public EnemyRole Role => role;
|
||||||
@@ -326,6 +328,8 @@ namespace BumpCombat.Enemies
|
|||||||
public bool IsCrowd => role == EnemyRole.Crowd;
|
public bool IsCrowd => role == EnemyRole.Crowd;
|
||||||
public bool HasContactDamage => kind == EnemyKind.Slime
|
public bool HasContactDamage => kind == EnemyKind.Slime
|
||||||
|| kind == EnemyKind.Bat;
|
|| kind == EnemyKind.Bat;
|
||||||
|
public bool IsFlying => isFlying || kind == EnemyKind.Bat || kind == EnemyKind.Necrofire;
|
||||||
|
public float SightRange => sightRange > 0.01f ? sightRange : 8f;
|
||||||
|
|
||||||
public EnemyAttackPattern GetAttackPattern(int index)
|
public EnemyAttackPattern GetAttackPattern(int index)
|
||||||
{
|
{
|
||||||
@@ -394,6 +398,16 @@ namespace BumpCombat.Enemies
|
|||||||
{
|
{
|
||||||
attackPatterns = patterns;
|
attackPatterns = patterns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ConfigureFlying(bool flying)
|
||||||
|
{
|
||||||
|
isFlying = flying;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureSightRange(float range)
|
||||||
|
{
|
||||||
|
sightRange = range;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ namespace BumpCombat.Enemies
|
|||||||
private float maxHealthMultiplier = 1f;
|
private float maxHealthMultiplier = 1f;
|
||||||
private float moveSpeedMultiplier = 1f;
|
private float moveSpeedMultiplier = 1f;
|
||||||
private float attackDamageMultiplier = 1f;
|
private float attackDamageMultiplier = 1f;
|
||||||
|
private float sightRangeMultiplier = 1f;
|
||||||
|
|
||||||
public EnemyDefinition Definition => definition;
|
public EnemyDefinition Definition => definition;
|
||||||
public override float MaxHealth => definition == null
|
public override float MaxHealth => definition == null
|
||||||
@@ -26,6 +27,14 @@ namespace BumpCombat.Enemies
|
|||||||
? 0f
|
? 0f
|
||||||
: CalculateAttackDamage(
|
: CalculateAttackDamage(
|
||||||
definition.AttackDamage * attackDamageMultiplier);
|
definition.AttackDamage * attackDamageMultiplier);
|
||||||
|
public float SightRange => definition == null
|
||||||
|
? 8f
|
||||||
|
: definition.SightRange * sightRangeMultiplier;
|
||||||
|
|
||||||
|
public void ConfigureSightRangeMultiplier(float multiplier)
|
||||||
|
{
|
||||||
|
sightRangeMultiplier = Mathf.Max(0.1f, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
public void ConfigureDefinition(EnemyDefinition enemyDefinition)
|
public void ConfigureDefinition(EnemyDefinition enemyDefinition)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using BumpCombat.Player;
|
using BumpCombat.Player;
|
||||||
using BumpCombat.Core;
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Stage;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace BumpCombat.Enemies
|
namespace BumpCombat.Enemies
|
||||||
@@ -82,6 +83,8 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool hitTarget = false;
|
bool hitTarget = false;
|
||||||
|
float targetDistance = float.PositiveInfinity;
|
||||||
|
float blockerDistance = float.PositiveInfinity;
|
||||||
if (distance > 0f)
|
if (distance > 0f)
|
||||||
{
|
{
|
||||||
RaycastHit2D[] hits = Physics2D.CircleCastAll(
|
RaycastHit2D[] hits = Physics2D.CircleCastAll(
|
||||||
@@ -92,12 +95,28 @@ namespace BumpCombat.Enemies
|
|||||||
Physics2D.AllLayers);
|
Physics2D.AllLayers);
|
||||||
for (int i = 0; i < hits.Length; i++)
|
for (int i = 0; i < hits.Length; i++)
|
||||||
{
|
{
|
||||||
if (hits[i].collider != null
|
Collider2D collider = hits[i].collider;
|
||||||
&& hits[i].collider.GetComponentInParent<PlayerHealth>() == target)
|
if (collider == null || collider.isTrigger)
|
||||||
{
|
{
|
||||||
hitTarget = true;
|
continue;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collider.GetComponentInParent<PlayerHealth>() == target)
|
||||||
|
{
|
||||||
|
targetDistance = Mathf.Min(targetDistance, hits[i].distance);
|
||||||
|
}
|
||||||
|
else if (IsTerrainBlocker(collider))
|
||||||
|
{
|
||||||
|
blockerDistance = Mathf.Min(blockerDistance, hits[i].distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hitTarget = targetDistance < blockerDistance;
|
||||||
|
if (!hitTarget && blockerDistance < float.PositiveInfinity)
|
||||||
|
{
|
||||||
|
resolved = true;
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -134,6 +153,14 @@ namespace BumpCombat.Enemies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTerrainBlocker(Collider2D collider)
|
||||||
|
{
|
||||||
|
return collider != null
|
||||||
|
&& (collider.GetComponentInParent<StageObstacle>() != null
|
||||||
|
|| collider.name.StartsWith("Wall_")
|
||||||
|
|| collider.gameObject.name.Contains("Wall"));
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
resolved = true;
|
resolved = true;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using BumpCombat.Combat;
|
|||||||
using BumpCombat.Constants;
|
using BumpCombat.Constants;
|
||||||
using BumpCombat.Core;
|
using BumpCombat.Core;
|
||||||
using BumpCombat.Enemies;
|
using BumpCombat.Enemies;
|
||||||
|
using BumpCombat.Progression;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
|
|
||||||
@@ -339,6 +340,13 @@ namespace BumpCombat.Player
|
|||||||
{
|
{
|
||||||
UpdateTemporaryPlayerModifiers();
|
UpdateTemporaryPlayerModifiers();
|
||||||
|
|
||||||
|
if (playerHealth != null && playerHealth.CurrentHealth <= 0f)
|
||||||
|
{
|
||||||
|
CancelArtifactInput();
|
||||||
|
CancelExecution();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (playerHealth != null && playerHealth.IsHurtMovementLocked)
|
if (playerHealth != null && playerHealth.IsHurtMovementLocked)
|
||||||
{
|
{
|
||||||
CancelArtifactInput();
|
CancelArtifactInput();
|
||||||
@@ -450,6 +458,7 @@ namespace BumpCombat.Player
|
|||||||
if (definition == null
|
if (definition == null
|
||||||
|| IsExecutingArtifact
|
|| IsExecutingArtifact
|
||||||
|| (dashController != null && dashController.IsDashing)
|
|| (dashController != null && dashController.IsDashing)
|
||||||
|
|| (playerHealth != null && playerHealth.CurrentHealth <= 0f)
|
||||||
|| (playerHealth != null && playerHealth.IsHurtMovementLocked)
|
|| (playerHealth != null && playerHealth.IsHurtMovementLocked)
|
||||||
|| (charged && !IsArtifactChargeUnlocked))
|
|| (charged && !IsArtifactChargeUnlocked))
|
||||||
{
|
{
|
||||||
@@ -546,7 +555,7 @@ namespace BumpCombat.Player
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetOwnedArtifactColorCount(ArtifactColor color)
|
public int GetOwnedArtifactColorCount(ArtifactColor color)
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < ownedArtifacts.Count; i++)
|
for (int i = 0; i < ownedArtifacts.Count; i++)
|
||||||
@@ -1168,6 +1177,7 @@ namespace BumpCombat.Player
|
|||||||
float nextHitAt = 0f;
|
float nextHitAt = 0f;
|
||||||
int hitIndex = 0;
|
int hitIndex = 0;
|
||||||
int hitCount = GetScheduledHitCount(duration, hitInterval);
|
int hitCount = GetScheduledHitCount(duration, hitInterval);
|
||||||
|
bool movementBlocked = false;
|
||||||
while (elapsed < duration)
|
while (elapsed < duration)
|
||||||
{
|
{
|
||||||
visual.transform.position = body.position
|
visual.transform.position = body.position
|
||||||
@@ -1206,10 +1216,24 @@ namespace BumpCombat.Player
|
|||||||
|
|
||||||
yield return new WaitForFixedUpdate();
|
yield return new WaitForFixedUpdate();
|
||||||
elapsed += Time.fixedDeltaTime;
|
elapsed += Time.fixedDeltaTime;
|
||||||
body.MovePosition(Vector2.Lerp(
|
if (!movementBlocked
|
||||||
|
&& playerController != null
|
||||||
|
&& !playerController.IsTransitionLocked)
|
||||||
|
{
|
||||||
|
Vector2 intendedEnd = Vector2.Lerp(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
Mathf.Clamp01(elapsed / duration)));
|
Mathf.Clamp01(elapsed / duration));
|
||||||
|
Vector2 clampedEnd = playerController.ClampMovementToTerrain(
|
||||||
|
body.position,
|
||||||
|
intendedEnd);
|
||||||
|
movementBlocked = (clampedEnd - intendedEnd).sqrMagnitude > 0.0001f;
|
||||||
|
body.MovePosition(clampedEnd);
|
||||||
|
}
|
||||||
|
else if (playerController != null && playerController.IsTransitionLocked)
|
||||||
|
{
|
||||||
|
movementBlocked = true;
|
||||||
|
}
|
||||||
visual.transform.position = body.position
|
visual.transform.position = body.position
|
||||||
+ GetCycloneShadowOffsetWorld();
|
+ GetCycloneShadowOffsetWorld();
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1260,6 @@ namespace BumpCombat.Player
|
|||||||
hitIndex++;
|
hitIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.position = end;
|
|
||||||
DestroyVisual(visual);
|
DestroyVisual(visual);
|
||||||
EndExecution();
|
EndExecution();
|
||||||
}
|
}
|
||||||
@@ -2190,6 +2213,134 @@ namespace BumpCombat.Player
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryEmitPassiveRelicEcho(
|
||||||
|
Vector2 origin,
|
||||||
|
Vector2 direction,
|
||||||
|
float damageMultiplier)
|
||||||
|
{
|
||||||
|
ActiveArtifactDefinition definition = CurrentArtifact;
|
||||||
|
if (definition == null
|
||||||
|
|| definition.NormalDamage <= 0f
|
||||||
|
|| damageMultiplier <= 0f
|
||||||
|
|| definition.Effect == ActiveArtifactEffect.Pulse)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction.sqrMagnitude < MinimumDirectionSqrMagnitude)
|
||||||
|
{
|
||||||
|
direction = definition.Effect == ActiveArtifactEffect.ThunderCrash
|
||||||
|
? GetThunderCrashDirection()
|
||||||
|
: GetUseDirection();
|
||||||
|
}
|
||||||
|
direction.Normalize();
|
||||||
|
int castIdentity = ++nextArtifactCastIdentity;
|
||||||
|
float echoIgniteTickDamage = playerStats.CalculateDamage(
|
||||||
|
definition.NormalIgniteTickDamage * damageMultiplier,
|
||||||
|
definition.DamageTag);
|
||||||
|
if (definition.Effect == ActiveArtifactEffect.ChainLightning)
|
||||||
|
{
|
||||||
|
EnemyController first = FindInitialChainTarget(origin, definition.NormalRange);
|
||||||
|
if (first == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applied = false;
|
||||||
|
foreach (ChainStrike strike in PlanArcChain(definition, first, origin))
|
||||||
|
{
|
||||||
|
applied |= ApplyArtifactHit(
|
||||||
|
strike.Enemy,
|
||||||
|
definition,
|
||||||
|
strike.Damage * damageMultiplier,
|
||||||
|
(strike.End - strike.Start).normalized,
|
||||||
|
strike.Knockback,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
echoIgniteTickDamage,
|
||||||
|
castIdentity,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 center = origin;
|
||||||
|
Vector2 radii = new Vector2(definition.NormalRange, definition.NormalRange);
|
||||||
|
float rayRange = definition.NormalRange;
|
||||||
|
float rayHalfWidth = Mathf.Max(0.05f, definition.NormalWidth * 0.5f);
|
||||||
|
switch (definition.Effect)
|
||||||
|
{
|
||||||
|
case ActiveArtifactEffect.Cyclone:
|
||||||
|
center += GetCycloneHitCenterOffset(definition.NormalRange, false);
|
||||||
|
radii = GetCycloneHitRadii(definition.NormalRange, false);
|
||||||
|
break;
|
||||||
|
case ActiveArtifactEffect.ThunderCrash:
|
||||||
|
center = origin + direction * 0.45f;
|
||||||
|
radii = GetThunderCrashHitRadii(definition.NormalRange, direction);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EnemyController> enemies = FindNearestTargets(
|
||||||
|
definition.Effect == ActiveArtifactEffect.Dash
|
||||||
|
|| definition.Effect == ActiveArtifactEffect.Phoenix
|
||||||
|
? origin + direction * (rayRange * 0.5f)
|
||||||
|
: center,
|
||||||
|
Mathf.Max(radii.x, radii.y, rayRange),
|
||||||
|
null);
|
||||||
|
int hitCount = 0;
|
||||||
|
bool anyDamage = false;
|
||||||
|
foreach (EnemyController enemy in enemies)
|
||||||
|
{
|
||||||
|
if (hitCount >= definition.NormalMaxTargets)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 position = enemy.GroundAnchorPosition;
|
||||||
|
Vector2 offset = position - origin;
|
||||||
|
bool inside = definition.Effect switch
|
||||||
|
{
|
||||||
|
ActiveArtifactEffect.Dash or ActiveArtifactEffect.Phoenix =>
|
||||||
|
Vector2.Dot(offset, direction) >= 0f
|
||||||
|
&& Vector2.Dot(offset, direction) <= rayRange
|
||||||
|
&& Mathf.Abs(Vector2.Dot(offset, new Vector2(-direction.y, direction.x))) <= rayHalfWidth,
|
||||||
|
_ => GetEllipseDistance(position - center, radii) <= 1.0001f,
|
||||||
|
};
|
||||||
|
if (!inside)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 hitDirection = definition.Effect == ActiveArtifactEffect.Dash
|
||||||
|
|| definition.Effect == ActiveArtifactEffect.Phoenix
|
||||||
|
? direction
|
||||||
|
: (position - center).normalized;
|
||||||
|
if (hitDirection.sqrMagnitude < MinimumDirectionSqrMagnitude)
|
||||||
|
{
|
||||||
|
hitDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool damaged = ApplyArtifactHit(
|
||||||
|
enemy,
|
||||||
|
definition,
|
||||||
|
definition.NormalDamage * damageMultiplier,
|
||||||
|
hitDirection,
|
||||||
|
definition.NormalKnockback,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
echoIgniteTickDamage,
|
||||||
|
castIdentity,
|
||||||
|
true);
|
||||||
|
anyDamage |= damaged;
|
||||||
|
if (damaged)
|
||||||
|
{
|
||||||
|
hitCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyDamage;
|
||||||
|
}
|
||||||
private bool ApplyArtifactHit(
|
private bool ApplyArtifactHit(
|
||||||
EnemyController enemy,
|
EnemyController enemy,
|
||||||
ActiveArtifactDefinition definition,
|
ActiveArtifactDefinition definition,
|
||||||
@@ -2199,7 +2350,8 @@ namespace BumpCombat.Player
|
|||||||
bool showsHitFeedback = true,
|
bool showsHitFeedback = true,
|
||||||
bool charged = false,
|
bool charged = false,
|
||||||
float igniteTickDamage = 0f,
|
float igniteTickDamage = 0f,
|
||||||
int castIdentity = 0)
|
int castIdentity = 0,
|
||||||
|
bool passiveRelicEcho = false)
|
||||||
{
|
{
|
||||||
if (enemy == null || enemy.IsDead)
|
if (enemy == null || enemy.IsDead)
|
||||||
{
|
{
|
||||||
@@ -2219,7 +2371,9 @@ namespace BumpCombat.Player
|
|||||||
direction,
|
direction,
|
||||||
knockback,
|
knockback,
|
||||||
definition.ArtifactColor,
|
definition.ArtifactColor,
|
||||||
castIdentity);
|
castIdentity,
|
||||||
|
true,
|
||||||
|
EnemyDamageSource.Player);
|
||||||
if (result == EnemyArtifactHitResult.Rejected)
|
if (result == EnemyArtifactHitResult.Rejected)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -2233,7 +2387,7 @@ namespace BumpCombat.Player
|
|||||||
|
|
||||||
if (result == EnemyArtifactHitResult.Shielded)
|
if (result == EnemyArtifactHitResult.Shielded)
|
||||||
{
|
{
|
||||||
if (enemy.LastArtifactContactCounted)
|
if (!passiveRelicEcho && enemy.LastArtifactContactCounted)
|
||||||
{
|
{
|
||||||
CombatEvents.RaiseArtifactEffectiveHit(
|
CombatEvents.RaiseArtifactEffectiveHit(
|
||||||
enemy.gameObject,
|
enemy.gameObject,
|
||||||
@@ -2262,7 +2416,7 @@ namespace BumpCombat.Player
|
|||||||
igniteTickDamage);
|
igniteTickDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (damageWasApplied)
|
if (damageWasApplied && !passiveRelicEcho)
|
||||||
{
|
{
|
||||||
CombatEvents.RaiseArtifactEffectiveHit(
|
CombatEvents.RaiseArtifactEffectiveHit(
|
||||||
enemy.gameObject,
|
enemy.gameObject,
|
||||||
@@ -2283,7 +2437,7 @@ namespace BumpCombat.Player
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
definition.DamageTag,
|
definition.DamageTag,
|
||||||
definition.ArtifactId,
|
passiveRelicEcho ? PassiveRelicController.EchoSourceId : definition.ArtifactId,
|
||||||
false,
|
false,
|
||||||
result == EnemyArtifactHitResult.DamageApplied
|
result == EnemyArtifactHitResult.DamageApplied
|
||||||
&& showsHitFeedback,
|
&& showsHitFeedback,
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ namespace BumpCombat.Player
|
|||||||
|| IsDashing
|
|| IsDashing
|
||||||
|| playerController == null
|
|| playerController == null
|
||||||
|| playerController.IsDamageKnockbackActive
|
|| playerController.IsDamageKnockbackActive
|
||||||
|| playerController.IsHurtMovementLocked)
|
|| playerController.IsHurtMovementLocked
|
||||||
|
|| playerController.IsTransitionLocked)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -81,19 +82,29 @@ namespace BumpCombat.Player
|
|||||||
: dashDuration;
|
: dashDuration;
|
||||||
Vector2 start = body.position;
|
Vector2 start = body.position;
|
||||||
Vector2 end = playerController.ClampToArena(start + direction * distance);
|
Vector2 end = playerController.ClampToArena(start + direction * distance);
|
||||||
|
Vector2 finalPosition = start;
|
||||||
|
|
||||||
combatResolver.BeginDashPathContactSession();
|
combatResolver.BeginDashPathContactSession();
|
||||||
float elapsed = 0f;
|
float elapsed = 0f;
|
||||||
while (elapsed < duration)
|
while (elapsed < duration)
|
||||||
{
|
{
|
||||||
yield return new WaitForFixedUpdate();
|
yield return new WaitForFixedUpdate();
|
||||||
|
if (playerController.IsTransitionLocked)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
combatFeedback.EmitDashAfterimage(body.position, isStrongDash);
|
combatFeedback.EmitDashAfterimage(body.position, isStrongDash);
|
||||||
Vector2 segmentStart = body.position;
|
Vector2 segmentStart = body.position;
|
||||||
elapsed += Time.fixedDeltaTime;
|
elapsed += Time.fixedDeltaTime;
|
||||||
Vector2 segmentEnd = Vector2.Lerp(
|
Vector2 intendedEnd = Vector2.Lerp(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
Mathf.Clamp01(elapsed / duration));
|
Mathf.Clamp01(elapsed / duration));
|
||||||
|
Vector2 segmentEnd = playerController.ClampMovementToTerrain(
|
||||||
|
segmentStart,
|
||||||
|
intendedEnd);
|
||||||
|
bool blocked = (segmentEnd - intendedEnd).sqrMagnitude > 0.0001f;
|
||||||
Vector2 segment = segmentEnd - segmentStart;
|
Vector2 segment = segmentEnd - segmentStart;
|
||||||
if (segment.sqrMagnitude > 0.000001f)
|
if (segment.sqrMagnitude > 0.000001f)
|
||||||
{
|
{
|
||||||
@@ -116,9 +127,14 @@ namespace BumpCombat.Player
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.MovePosition(segmentEnd);
|
body.MovePosition(segmentEnd);
|
||||||
|
finalPosition = segmentEnd;
|
||||||
|
if (blocked)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.position = end;
|
body.position = finalPosition;
|
||||||
IsDashing = false;
|
IsDashing = false;
|
||||||
IsStrongDashing = false;
|
IsStrongDashing = false;
|
||||||
animator.SetBool(IsDashingParameter, false);
|
animator.SetBool(IsDashingParameter, false);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ namespace BumpCombat.Player
|
|||||||
Animator.StringToHash("Base Layer.BackBumpAttack");
|
Animator.StringToHash("Base Layer.BackBumpAttack");
|
||||||
private static readonly int ArtifactUseState =
|
private static readonly int ArtifactUseState =
|
||||||
Animator.StringToHash("Base Layer.ArtifactUse");
|
Animator.StringToHash("Base Layer.ArtifactUse");
|
||||||
|
private static readonly int DeathState =
|
||||||
|
Animator.StringToHash("Base Layer.Death");
|
||||||
|
|
||||||
public const string BumpAttackStateName = "BumpAttack";
|
public const string BumpAttackStateName = "BumpAttack";
|
||||||
public const string BackBumpAttackStateName = "BackBumpAttack";
|
public const string BackBumpAttackStateName = "BackBumpAttack";
|
||||||
@@ -45,6 +47,7 @@ namespace BumpCombat.Player
|
|||||||
public bool IsDamageKnockbackActive { get; private set; }
|
public bool IsDamageKnockbackActive { get; private set; }
|
||||||
public bool IsHurtMovementLocked =>
|
public bool IsHurtMovementLocked =>
|
||||||
playerHealth != null && playerHealth.IsHurtMovementLocked;
|
playerHealth != null && playerHealth.IsHurtMovementLocked;
|
||||||
|
public bool IsTransitionLocked { get; private set; }
|
||||||
public Vector2 PlayerClampHalfExtents
|
public Vector2 PlayerClampHalfExtents
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -95,6 +98,23 @@ namespace BumpCombat.Player
|
|||||||
return TryPlayCombatAnimation(ArtifactUseState, false);
|
return TryPlayCombatAnimation(ArtifactUseState, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float PlayDeathAnimation()
|
||||||
|
{
|
||||||
|
if (animator == null
|
||||||
|
|| animator.runtimeAnimatorController == null
|
||||||
|
|| !animator.isActiveAndEnabled
|
||||||
|
|| !animator.HasState(0, DeathState))
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTransitionLocked(true);
|
||||||
|
animator.SetBool(IsMovingParameter, false);
|
||||||
|
animator.Play(DeathState, 0, 0f);
|
||||||
|
animator.Update(0f);
|
||||||
|
return Mathf.Max(0f, animator.GetCurrentAnimatorStateInfo(0).length);
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryPlayCombatAnimation(
|
private bool TryPlayCombatAnimation(
|
||||||
int stateHash,
|
int stateHash,
|
||||||
bool blockActiveArtifactUse)
|
bool blockActiveArtifactUse)
|
||||||
@@ -148,7 +168,7 @@ namespace BumpCombat.Player
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!RunManager.GameplayInputEnabled || IsHurtMovementLocked)
|
if (!RunManager.GameplayInputEnabled || IsHurtMovementLocked || IsTransitionLocked)
|
||||||
{
|
{
|
||||||
SetMoveInput(Vector2.zero);
|
SetMoveInput(Vector2.zero);
|
||||||
return;
|
return;
|
||||||
@@ -172,6 +192,13 @@ namespace BumpCombat.Player
|
|||||||
private void FixedUpdate()
|
private void FixedUpdate()
|
||||||
{
|
{
|
||||||
AttackIntentVelocity = Vector2.zero;
|
AttackIntentVelocity = Vector2.zero;
|
||||||
|
if (IsTransitionLocked)
|
||||||
|
{
|
||||||
|
if (body != null) body.linearVelocity = Vector2.zero;
|
||||||
|
CurrentVelocity = Vector2.zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dashController != null && dashController.IsDashing)
|
if (dashController != null && dashController.IsDashing)
|
||||||
{
|
{
|
||||||
pendingImpactRecoil = Vector2.zero;
|
pendingImpactRecoil = Vector2.zero;
|
||||||
@@ -244,6 +271,57 @@ namespace BumpCombat.Player
|
|||||||
- GameplayConstants.Current.Player.ArenaPadding.y));
|
- GameplayConstants.Current.Player.ArenaPadding.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Vector2 ClampMovementToTerrain(Vector2 start, Vector2 desiredPosition)
|
||||||
|
{
|
||||||
|
Vector2 desired = ClampToArena(desiredPosition);
|
||||||
|
Vector2 delta = desired - start;
|
||||||
|
float distance = delta.magnitude;
|
||||||
|
if (distance <= 0.0001f)
|
||||||
|
{
|
||||||
|
return desired;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collider2D ownCollider = GetComponent<Collider2D>();
|
||||||
|
float radius = 0.2f;
|
||||||
|
if (ownCollider != null)
|
||||||
|
{
|
||||||
|
Vector2 extents = ownCollider.bounds.extents;
|
||||||
|
radius = Mathf.Max(0.05f, Mathf.Min(extents.x, extents.y) * 0.9f);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 direction = delta / distance;
|
||||||
|
RaycastHit2D[] hits = Physics2D.CircleCastAll(
|
||||||
|
start,
|
||||||
|
radius,
|
||||||
|
direction,
|
||||||
|
distance,
|
||||||
|
Physics2D.AllLayers);
|
||||||
|
float allowedDistance = distance;
|
||||||
|
for (int i = 0; i < hits.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D collider = hits[i].collider;
|
||||||
|
if (collider == null
|
||||||
|
|| collider.isTrigger
|
||||||
|
|| collider == ownCollider
|
||||||
|
|| collider.transform.IsChildOf(transform))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTerrain = collider.GetComponentInParent<BumpCombat.Stage.StageObstacle>() != null
|
||||||
|
|| collider.name.StartsWith("Wall_")
|
||||||
|
|| collider.gameObject.name.Contains("Wall");
|
||||||
|
if (isTerrain)
|
||||||
|
{
|
||||||
|
allowedDistance = Mathf.Min(
|
||||||
|
allowedDistance,
|
||||||
|
Mathf.Max(0f, hits[i].distance - 0.02f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return start + direction * allowedDistance;
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryReposition(Vector2 position)
|
public bool TryReposition(Vector2 position)
|
||||||
{
|
{
|
||||||
if (body == null)
|
if (body == null)
|
||||||
@@ -338,5 +416,36 @@ namespace BumpCombat.Player
|
|||||||
spriteRenderer.flipX = input.x < 0f;
|
spriteRenderer.flipX = input.x < 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetTransitionLocked(bool locked)
|
||||||
|
{
|
||||||
|
IsTransitionLocked = locked;
|
||||||
|
if (locked)
|
||||||
|
{
|
||||||
|
moveInput = Vector2.zero;
|
||||||
|
CurrentVelocity = Vector2.zero;
|
||||||
|
AttackIntentVelocity = Vector2.zero;
|
||||||
|
if (body != null)
|
||||||
|
{
|
||||||
|
body.linearVelocity = Vector2.zero;
|
||||||
|
}
|
||||||
|
if (animator != null && animator.isActiveAndEnabled)
|
||||||
|
{
|
||||||
|
animator.SetBool(IsMovingParameter, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetFacingDirection(Vector2 direction)
|
||||||
|
{
|
||||||
|
if (direction.sqrMagnitude > 0.0001f)
|
||||||
|
{
|
||||||
|
FacingDirection = direction.normalized;
|
||||||
|
if (spriteRenderer != null && !Mathf.Approximately(direction.x, 0f))
|
||||||
|
{
|
||||||
|
spriteRenderer.flipX = direction.x < 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using BumpCombat.Combat;
|
using BumpCombat.Combat;
|
||||||
using BumpCombat.Core;
|
using BumpCombat.Core;
|
||||||
using BumpCombat.Constants;
|
using BumpCombat.Constants;
|
||||||
|
using BumpCombat.Defense;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
using UnityEngine.InputSystem.LowLevel;
|
using UnityEngine.InputSystem.LowLevel;
|
||||||
@@ -235,8 +236,29 @@ namespace BumpCombat.Player
|
|||||||
|
|
||||||
if (CurrentHealth <= 0f)
|
if (CurrentHealth <= 0f)
|
||||||
{
|
{
|
||||||
|
float deathAnimationDuration = playerController.PlayDeathAnimation();
|
||||||
OnDied?.Invoke();
|
OnDied?.Invoke();
|
||||||
RunManager.Instance?.EndRun();
|
RunManager runManager = RunManager.Instance;
|
||||||
|
if (runManager != null
|
||||||
|
&& runManager.IsDefensePrototypeRun
|
||||||
|
&& DefensePrototypeController.Instance != null
|
||||||
|
&& DefensePrototypeController.Instance.HandlePlayerDied())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (runManager == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deathAnimationDuration > 0f)
|
||||||
|
{
|
||||||
|
runManager.BeginGameOverPresentation(deathAnimationDuration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runManager.EndRun();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -247,6 +269,26 @@ namespace BumpCombat.Player
|
|||||||
return value >= 0f ? value : configuredValue;
|
return value >= 0f ? value : configuredValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ReviveForDefense(float invulnerabilitySeconds)
|
||||||
|
{
|
||||||
|
if (CurrentHealth > 0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHealth = MaxHealth;
|
||||||
|
invulnerableUntil = Time.time + Mathf.Max(0f, invulnerabilitySeconds);
|
||||||
|
hurtMovementLockedUntil = 0f;
|
||||||
|
guardUntil = 0f;
|
||||||
|
guardCooldownUntil = 0f;
|
||||||
|
activeGuardHasBlockedAttack = false;
|
||||||
|
playerController.SetTransitionLocked(false);
|
||||||
|
animator.Rebind();
|
||||||
|
animator.Update(0f);
|
||||||
|
OnHealthChanged?.Invoke(CurrentHealth, MaxHealth);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void Heal(float amount)
|
public void Heal(float amount)
|
||||||
{
|
{
|
||||||
if (amount <= 0f || CurrentHealth <= 0f)
|
if (amount <= 0f || CurrentHealth <= 0f)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using BumpCombat.Audio;
|
|||||||
using BumpCombat.Combat;
|
using BumpCombat.Combat;
|
||||||
using BumpCombat.Core;
|
using BumpCombat.Core;
|
||||||
using BumpCombat.Player;
|
using BumpCombat.Player;
|
||||||
|
using BumpCombat.Defense;
|
||||||
using BumpCombat.Spawning;
|
using BumpCombat.Spawning;
|
||||||
using BumpCombat.UI;
|
using BumpCombat.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -30,6 +31,7 @@ namespace BumpCombat.Progression
|
|||||||
|
|
||||||
private readonly Queue<RewardSource> pendingRewards = new();
|
private readonly Queue<RewardSource> pendingRewards = new();
|
||||||
private readonly HashSet<RewardSource> queuedSources = new();
|
private readonly HashSet<RewardSource> queuedSources = new();
|
||||||
|
private readonly PassiveRelicId[] shownRelics = new PassiveRelicId[DebugOptionCount];
|
||||||
private readonly ActiveArtifactDefinition[] shownArtifacts =
|
private readonly ActiveArtifactDefinition[] shownArtifacts =
|
||||||
new ActiveArtifactDefinition[DebugOptionCount];
|
new ActiveArtifactDefinition[DebugOptionCount];
|
||||||
private ActiveArtifactController artifacts;
|
private ActiveArtifactController artifacts;
|
||||||
@@ -42,6 +44,9 @@ namespace BumpCombat.Progression
|
|||||||
private int shownCount;
|
private int shownCount;
|
||||||
private int selectedIndex;
|
private int selectedIndex;
|
||||||
private bool debugStartupSelection;
|
private bool debugStartupSelection;
|
||||||
|
private bool showingPassiveRelicReward;
|
||||||
|
private bool pendingPassiveRelicReward;
|
||||||
|
private PassiveRelicController passiveRelics;
|
||||||
private int startupSelectionCount;
|
private int startupSelectionCount;
|
||||||
private bool debugStartupSelectionPending;
|
private bool debugStartupSelectionPending;
|
||||||
private int debugStartupSelectionEarliestFrame;
|
private int debugStartupSelectionEarliestFrame;
|
||||||
@@ -148,7 +153,7 @@ namespace BumpCombat.Progression
|
|||||||
RunManager.Instance?.StartupArtifactSelectionCount ?? 3);
|
RunManager.Instance?.StartupArtifactSelectionCount ?? 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueueStartupSelection(int selectionCount)
|
public void QueueStartupSelection(int selectionCount)
|
||||||
{
|
{
|
||||||
startupSelectionCount = Mathf.Max(0, selectionCount);
|
startupSelectionCount = Mathf.Max(0, selectionCount);
|
||||||
debugStartupSelectionPending = true;
|
debugStartupSelectionPending = true;
|
||||||
@@ -157,15 +162,25 @@ namespace BumpCombat.Progression
|
|||||||
|
|
||||||
private bool ChooseOption(int optionIndex)
|
private bool ChooseOption(int optionIndex)
|
||||||
{
|
{
|
||||||
|
if (showingPassiveRelicReward)
|
||||||
|
{
|
||||||
|
return ChoosePassiveRelicOption(optionIndex);
|
||||||
|
}
|
||||||
if (panel == null
|
if (panel == null
|
||||||
|| !panel.activeSelf
|
|| !panel.activeSelf
|
||||||
|| optionIndex < 0
|
|| optionIndex < 0
|
||||||
|| optionIndex >= shownCount
|
|| optionIndex >= shownCount)
|
||||||
|| !artifacts.TryAddArtifact(shownArtifacts[optionIndex]))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!artifacts.TryAddArtifact(shownArtifacts[optionIndex]))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ArtifactRewardController] Could not add artifact {shownArtifacts[optionIndex]?.name}. Closing selection to prevent lock.");
|
||||||
|
CloseSelection();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||||
if (artifacts.OwnedArtifactCount == 1)
|
if (artifacts.OwnedArtifactCount == 1)
|
||||||
{
|
{
|
||||||
@@ -201,14 +216,55 @@ namespace BumpCombat.Progression
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void QueuePassiveRelicReward()
|
||||||
|
{
|
||||||
|
pendingPassiveRelicReward = true;
|
||||||
|
TryOpenPendingReward();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ChoosePassiveRelicOption(int optionIndex)
|
||||||
|
{
|
||||||
|
if (panel == null
|
||||||
|
|| !panel.activeSelf
|
||||||
|
|| optionIndex < 0
|
||||||
|
|| optionIndex >= shownCount
|
||||||
|
|| passiveRelics == null
|
||||||
|
|| !passiveRelics.TryAcquire(shownRelics[optionIndex]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showingPassiveRelicReward = false;
|
||||||
|
pendingPassiveRelicReward = false;
|
||||||
|
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||||
|
DefensePrototypeController.Instance?.ResolvePassiveRelicChoice();
|
||||||
|
CloseSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
private bool OpenStartupSelection()
|
private bool OpenStartupSelection()
|
||||||
{
|
{
|
||||||
|
if (artifacts == null)
|
||||||
|
{
|
||||||
|
artifacts = FindAnyObjectByType<ActiveArtifactController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel == null)
|
||||||
|
{
|
||||||
|
CreatePanel();
|
||||||
|
}
|
||||||
|
|
||||||
if (RunManager.Instance == null
|
if (RunManager.Instance == null
|
||||||
|| !RunManager.Instance.TryOpenSelection(this))
|
|| !RunManager.Instance.TryOpenSelection(this))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (artifacts == null)
|
||||||
|
{
|
||||||
|
RunManager.Instance.CloseSelection(this);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
debugSelectionsRequired = Mathf.Min(
|
debugSelectionsRequired = Mathf.Min(
|
||||||
startupSelectionCount,
|
startupSelectionCount,
|
||||||
artifacts.MaxOwnedArtifacts - artifacts.OwnedArtifactCount);
|
artifacts.MaxOwnedArtifacts - artifacts.OwnedArtifactCount);
|
||||||
@@ -243,6 +299,26 @@ namespace BumpCombat.Progression
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArtifactColor requiredColor = GetStartupColor(selectedCount);
|
ArtifactColor requiredColor = GetStartupColor(selectedCount);
|
||||||
|
if (artifacts.GetOwnedArtifactColorCount(requiredColor) >= artifacts.MaxArtifactsPerColor)
|
||||||
|
{
|
||||||
|
bool foundAvailableColor = false;
|
||||||
|
for (int c = 0; c < StartupColorCount; c++)
|
||||||
|
{
|
||||||
|
ArtifactColor candidateColor = GetStartupColor(c);
|
||||||
|
if (artifacts.GetOwnedArtifactColorCount(candidateColor) < artifacts.MaxArtifactsPerColor)
|
||||||
|
{
|
||||||
|
requiredColor = candidateColor;
|
||||||
|
foundAvailableColor = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundAvailableColor)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0;
|
for (int i = 0;
|
||||||
i < artifacts.CatalogCount && shownCount < DebugOptionCount;
|
i < artifacts.CatalogCount && shownCount < DebugOptionCount;
|
||||||
i++)
|
i++)
|
||||||
@@ -284,6 +360,12 @@ namespace BumpCombat.Progression
|
|||||||
|
|
||||||
private void HandleEventEnemyDefeated(RunTimedEvent timedEvent)
|
private void HandleEventEnemyDefeated(RunTimedEvent timedEvent)
|
||||||
{
|
{
|
||||||
|
if (Stage.StageManager.IsStageModeActive)
|
||||||
|
{
|
||||||
|
// In stage mode, StageManager.HandleMajorBossDefeated coordinates rewards
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (timedEvent == RunTimedEvent.Elite)
|
if (timedEvent == RunTimedEvent.Elite)
|
||||||
{
|
{
|
||||||
if (RunManager.Instance?.IsProductionRun == true)
|
if (RunManager.Instance?.IsProductionRun == true)
|
||||||
@@ -311,8 +393,41 @@ namespace BumpCombat.Progression
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void QueueColorReward(ArtifactColor color)
|
||||||
|
{
|
||||||
|
if (artifacts == null)
|
||||||
|
{
|
||||||
|
artifacts = FindAnyObjectByType<ActiveArtifactController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifacts == null
|
||||||
|
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifacts.GetOwnedArtifactColorCount(color) >= artifacts.MaxArtifactsPerColor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RewardSource source = color switch
|
||||||
|
{
|
||||||
|
ArtifactColor.Red => RewardSource.EliteRed,
|
||||||
|
ArtifactColor.Blue => RewardSource.EliteBlue,
|
||||||
|
_ => RewardSource.Elite,
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueReward(source);
|
||||||
|
}
|
||||||
|
|
||||||
private void QueueReward(RewardSource source)
|
private void QueueReward(RewardSource source)
|
||||||
{
|
{
|
||||||
|
if (artifacts == null)
|
||||||
|
{
|
||||||
|
artifacts = FindAnyObjectByType<ActiveArtifactController>();
|
||||||
|
}
|
||||||
|
|
||||||
if (artifacts == null
|
if (artifacts == null
|
||||||
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
|
|| artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts)
|
||||||
{
|
{
|
||||||
@@ -330,8 +445,19 @@ namespace BumpCombat.Progression
|
|||||||
|
|
||||||
private void TryOpenPendingReward()
|
private void TryOpenPendingReward()
|
||||||
{
|
{
|
||||||
if (pendingRewards.Count == 0 || artifacts == null)
|
if (artifacts == null)
|
||||||
{
|
{
|
||||||
|
artifacts = FindAnyObjectByType<ActiveArtifactController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifacts == null || (pendingRewards.Count == 0 && !pendingPassiveRelicReward))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingPassiveRelicReward)
|
||||||
|
{
|
||||||
|
TryOpenPassiveRelicReward();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +467,11 @@ namespace BumpCombat.Progression
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panel == null)
|
||||||
|
{
|
||||||
|
CreatePanel();
|
||||||
|
}
|
||||||
|
|
||||||
if (RunManager.Instance == null
|
if (RunManager.Instance == null
|
||||||
|| !RunManager.Instance.TryOpenSelection(this))
|
|| !RunManager.Instance.TryOpenSelection(this))
|
||||||
{
|
{
|
||||||
@@ -359,7 +490,8 @@ namespace BumpCombat.Progression
|
|||||||
if (definition != null
|
if (definition != null
|
||||||
&& !artifacts.OwnsArtifact(definition)
|
&& !artifacts.OwnsArtifact(definition)
|
||||||
&& (!hasRequiredColor
|
&& (!hasRequiredColor
|
||||||
|| definition.ArtifactColor == requiredColor))
|
|| definition.ArtifactColor == requiredColor)
|
||||||
|
&& artifacts.GetOwnedArtifactColorCount(definition.ArtifactColor) < artifacts.MaxArtifactsPerColor)
|
||||||
{
|
{
|
||||||
candidates.Add(definition);
|
candidates.Add(definition);
|
||||||
}
|
}
|
||||||
@@ -384,16 +516,65 @@ namespace BumpCombat.Progression
|
|||||||
|
|
||||||
titleText.text = source switch
|
titleText.text = source switch
|
||||||
{
|
{
|
||||||
RewardSource.Elite => "ELITE REWARD - CHOOSE ARTIFACT",
|
RewardSource.Elite => "ELITE REWARD - CHOOSE RED ARTIFACT",
|
||||||
RewardSource.EliteRed => "ELITE REWARD - CHOOSE RED ARTIFACT",
|
RewardSource.EliteRed => "ELITE REWARD - CHOOSE RED ARTIFACT",
|
||||||
RewardSource.EliteBlue => "ELITE REWARD - CHOOSE BLUE ARTIFACT",
|
RewardSource.EliteBlue => "ELITE REWARD - CHOOSE BLUE ARTIFACT",
|
||||||
RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE ARTIFACT",
|
RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE BLUE ARTIFACT",
|
||||||
_ => "CHOOSE ARTIFACT",
|
_ => "CHOOSE ARTIFACT",
|
||||||
};
|
};
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
ShowOptions();
|
ShowOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryOpenPassiveRelicReward()
|
||||||
|
{
|
||||||
|
passiveRelics = FindAnyObjectByType<PassiveRelicController>();
|
||||||
|
if (passiveRelics == null || RunManager.Instance == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel == null)
|
||||||
|
{
|
||||||
|
CreatePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RunManager.Instance.TryOpenSelection(this))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PassiveRelicId[] available = passiveRelics.GetAvailableRelics();
|
||||||
|
shownCount = Mathf.Min(3, available.Length);
|
||||||
|
if (shownCount == 0)
|
||||||
|
{
|
||||||
|
RunManager.Instance.CloseSelection(this);
|
||||||
|
pendingPassiveRelicReward = false;
|
||||||
|
DefensePrototypeController.Instance?.ResolvePassiveRelicChoice();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < shownCount; i++)
|
||||||
|
{
|
||||||
|
shownRelics[i] = available[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
showingPassiveRelicReward = true;
|
||||||
|
selectedIndex = 0;
|
||||||
|
titleText.text = "PASSIVE RELIC - CHOOSE ONE";
|
||||||
|
ShowOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPassiveRelicName(PassiveRelicId id)
|
||||||
|
{
|
||||||
|
return id switch
|
||||||
|
{
|
||||||
|
PassiveRelicId.TwinHornTalisman => "01 TWIN HORN TALISMAN\n15% chance: ordinary bump deals 2x damage",
|
||||||
|
PassiveRelicId.ResonanceCore => "02 RESONANCE CORE\n8% chance: echo current artifact at 50% damage",
|
||||||
|
PassiveRelicId.RuptureSeed => "03 RUPTURE SEED\n25% chance: player kill explodes for 10% max HP",
|
||||||
|
_ => id.ToString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
private void HandleSelectionChanged(bool isOpen)
|
private void HandleSelectionChanged(bool isOpen)
|
||||||
{
|
{
|
||||||
if (!isOpen && (panel == null || !panel.activeSelf))
|
if (!isOpen && (panel == null || !panel.activeSelf))
|
||||||
@@ -450,6 +631,13 @@ namespace BumpCombat.Progression
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showingPassiveRelicReward)
|
||||||
|
{
|
||||||
|
optionIconImages[i].gameObject.SetActive(false);
|
||||||
|
optionTexts[i].text = GetPassiveRelicName(shownRelics[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ActiveArtifactDefinition definition = shownArtifacts[i];
|
ActiveArtifactDefinition definition = shownArtifacts[i];
|
||||||
Sprite icon = PresentationUiStyle.GetArtifactIcon(definition);
|
Sprite icon = PresentationUiStyle.GetArtifactIcon(definition);
|
||||||
optionIconImages[i].sprite = icon;
|
optionIconImages[i].sprite = icon;
|
||||||
@@ -518,9 +706,11 @@ namespace BumpCombat.Progression
|
|||||||
{
|
{
|
||||||
switch (source)
|
switch (source)
|
||||||
{
|
{
|
||||||
|
case RewardSource.Elite:
|
||||||
case RewardSource.EliteRed:
|
case RewardSource.EliteRed:
|
||||||
color = ArtifactColor.Red;
|
color = ArtifactColor.Red;
|
||||||
return true;
|
return true;
|
||||||
|
case RewardSource.MidBoss:
|
||||||
case RewardSource.EliteBlue:
|
case RewardSource.EliteBlue:
|
||||||
color = ArtifactColor.Blue;
|
color = ArtifactColor.Blue;
|
||||||
return true;
|
return true;
|
||||||
@@ -605,6 +795,7 @@ namespace BumpCombat.Progression
|
|||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
public int DebugShownCount => shownCount;
|
public int DebugShownCount => shownCount;
|
||||||
public bool DebugIsSelectionVisible => panel != null && panel.activeSelf;
|
public bool DebugIsSelectionVisible => panel != null && panel.activeSelf;
|
||||||
|
public int DebugShownPassiveRelicCount => showingPassiveRelicReward ? shownCount : 0;
|
||||||
|
|
||||||
public ActiveArtifactDefinition DebugGetShownArtifact(int index)
|
public ActiveArtifactDefinition DebugGetShownArtifact(int index)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BumpCombat.Combat;
|
||||||
|
using BumpCombat.Constants;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using BumpCombat.Player;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Progression
|
||||||
|
{
|
||||||
|
public enum PassiveRelicId
|
||||||
|
{
|
||||||
|
TwinHornTalisman,
|
||||||
|
ResonanceCore,
|
||||||
|
RuptureSeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PassiveRelicRules
|
||||||
|
{
|
||||||
|
private static readonly PassiveRelicId[] Catalog =
|
||||||
|
{
|
||||||
|
PassiveRelicId.TwinHornTalisman,
|
||||||
|
PassiveRelicId.ResonanceCore,
|
||||||
|
PassiveRelicId.RuptureSeed,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool RollsChance(float chance, float roll) =>
|
||||||
|
chance > 0f && roll >= 0f && roll < Mathf.Clamp01(chance);
|
||||||
|
|
||||||
|
public static PassiveRelicId[] GetAvailableRelics(
|
||||||
|
IReadOnlyCollection<PassiveRelicId> owned)
|
||||||
|
{
|
||||||
|
List<PassiveRelicId> available = new(Catalog.Length);
|
||||||
|
foreach (PassiveRelicId id in Catalog)
|
||||||
|
{
|
||||||
|
if (owned == null || !owned.Contains(id))
|
||||||
|
{
|
||||||
|
available.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float ApplyTwinHornDamage(
|
||||||
|
float damage,
|
||||||
|
bool ownsRelic,
|
||||||
|
bool isOrdinaryBump,
|
||||||
|
bool hasArtifactColor,
|
||||||
|
float chance,
|
||||||
|
float roll)
|
||||||
|
{
|
||||||
|
return ownsRelic
|
||||||
|
&& isOrdinaryBump
|
||||||
|
&& !hasArtifactColor
|
||||||
|
&& RollsChance(chance, roll)
|
||||||
|
? damage * 2f
|
||||||
|
: damage;
|
||||||
|
}
|
||||||
|
public static float GetExplosionDamage(float maximumHealth, float fraction) =>
|
||||||
|
Mathf.Max(0f, maximumHealth) * Mathf.Clamp01(fraction);
|
||||||
|
|
||||||
|
public static float GetExplosionRadius(float bodyDiameter, float diameterMultiplier) =>
|
||||||
|
Mathf.Max(0f, bodyDiameter) * Mathf.Max(0f, diameterMultiplier);
|
||||||
|
|
||||||
|
public static bool IsPlayerCreditedKill(EnemyDamageSource source) =>
|
||||||
|
source == EnemyDamageSource.Player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PassiveRelicController : MonoBehaviour
|
||||||
|
{
|
||||||
|
public const string EchoSourceId = "passive_relic_echo";
|
||||||
|
|
||||||
|
private readonly HashSet<PassiveRelicId> owned = new();
|
||||||
|
private readonly HashSet<int> processedDeaths = new();
|
||||||
|
private ActiveArtifactController artifacts;
|
||||||
|
private PlayerController player;
|
||||||
|
private Collider2D playerCollider;
|
||||||
|
private PlayerHealth playerHealth;
|
||||||
|
private bool active;
|
||||||
|
private float nextEchoAllowedAt;
|
||||||
|
|
||||||
|
public static PassiveRelicController Instance { get; private set; }
|
||||||
|
public int OwnedCount => owned.Count;
|
||||||
|
public bool Owns(PassiveRelicId id) => owned.Contains(id);
|
||||||
|
public int AvailableCount => GetAvailableRelics().Length;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
public static System.Func<float> RandomRollForTests { get; set; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Destroy(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
CombatEvents.OnValidHit += HandleValidHit;
|
||||||
|
CombatEvents.OnEnemyDied += HandleEnemyDied;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
active = false;
|
||||||
|
CombatEvents.OnValidHit -= HandleValidHit;
|
||||||
|
CombatEvents.OnEnemyDied -= HandleEnemyDied;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
{
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginDefenseRun()
|
||||||
|
{
|
||||||
|
owned.Clear();
|
||||||
|
processedDeaths.Clear();
|
||||||
|
nextEchoAllowedAt = 0f;
|
||||||
|
player = GetComponent<PlayerController>();
|
||||||
|
playerCollider = GetComponent<Collider2D>();
|
||||||
|
playerHealth = GetComponent<PlayerHealth>();
|
||||||
|
artifacts = GetComponent<ActiveArtifactController>();
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndDefenseRun()
|
||||||
|
{
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PassiveRelicId[] GetAvailableRelics() =>
|
||||||
|
PassiveRelicRules.GetAvailableRelics(owned);
|
||||||
|
|
||||||
|
public bool TryAcquire(PassiveRelicId id)
|
||||||
|
{
|
||||||
|
if (!active || !System.Array.Exists(GetAvailableRelics(), candidate => candidate == id))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return owned.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float ApplyOrdinaryBumpDamageMultiplier(
|
||||||
|
float damage,
|
||||||
|
bool isOrdinaryBump,
|
||||||
|
bool hasArtifactColor)
|
||||||
|
{
|
||||||
|
if (!active || !owned.Contains(PassiveRelicId.TwinHornTalisman))
|
||||||
|
{
|
||||||
|
return damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PassiveRelicRules.ApplyTwinHornDamage(
|
||||||
|
damage,
|
||||||
|
true,
|
||||||
|
isOrdinaryBump,
|
||||||
|
hasArtifactColor,
|
||||||
|
GameplayConstants.Current.PassiveRelics.TwinHornDoubleDamageChance,
|
||||||
|
GetRandomRoll());
|
||||||
|
}
|
||||||
|
private void HandleValidHit(CombatHitResult hit)
|
||||||
|
{
|
||||||
|
if (!active
|
||||||
|
|| !owned.Contains(PassiveRelicId.ResonanceCore)
|
||||||
|
|| hit.Attacker != gameObject
|
||||||
|
|| hit.Damage <= 0f
|
||||||
|
|| playerHealth == null
|
||||||
|
|| playerHealth.CurrentHealth <= 0f
|
||||||
|
|| hit.SourceId == EchoSourceId
|
||||||
|
|| Time.time < nextEchoAllowedAt
|
||||||
|
|| !RollsChance(GameplayConstants.Current.PassiveRelics.ResonanceEchoChance)
|
||||||
|
|| artifacts == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifacts.TryEmitPassiveRelicEcho(
|
||||||
|
hit.HitPosition,
|
||||||
|
hit.KnockbackDirection,
|
||||||
|
GameplayConstants.Current.PassiveRelics.ResonanceEchoDamageMultiplier))
|
||||||
|
{
|
||||||
|
nextEchoAllowedAt = Time.time
|
||||||
|
+ GameplayConstants.Current.PassiveRelics.ResonanceEchoCooldownSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleEnemyDied(GameObject target)
|
||||||
|
{
|
||||||
|
if (!active || !owned.Contains(PassiveRelicId.RuptureSeed) || target == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController victim = target.GetComponent<EnemyController>();
|
||||||
|
if (victim == null
|
||||||
|
|| !PassiveRelicRules.IsPlayerCreditedKill(victim.LastDamageSource)
|
||||||
|
|| !processedDeaths.Add(victim.SpawnLifetimeIdentity)
|
||||||
|
|| !RollsChance(GameplayConstants.Current.PassiveRelics.RuptureSeedChance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float damage = PassiveRelicRules.GetExplosionDamage(
|
||||||
|
victim.MaximumHealthAtDeath,
|
||||||
|
GameplayConstants.Current.PassiveRelics.RuptureExplosionMaxHealthFraction);
|
||||||
|
float diameter = playerCollider != null
|
||||||
|
? Mathf.Max(playerCollider.bounds.size.x, playerCollider.bounds.size.y)
|
||||||
|
: 0f;
|
||||||
|
float radius = PassiveRelicRules.GetExplosionRadius(
|
||||||
|
diameter,
|
||||||
|
GameplayConstants.Current.PassiveRelics.RuptureExplosionPlayerBodyDiameters);
|
||||||
|
if (damage <= 0f || radius <= 0f || playerHealth == null || playerHealth.CurrentHealth <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collider2D[] hits = Physics2D.OverlapCircleAll(victim.transform.position, radius);
|
||||||
|
HashSet<EnemyController> damagedEnemies = new();
|
||||||
|
foreach (Collider2D hit in hits)
|
||||||
|
{
|
||||||
|
EnemyController enemy = hit != null ? hit.GetComponentInParent<EnemyController>() : null;
|
||||||
|
if (enemy == null
|
||||||
|
|| enemy == victim
|
||||||
|
|| enemy.IsDead
|
||||||
|
|| !damagedEnemies.Add(enemy))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 direction = ((Vector2)enemy.transform.position - (Vector2)victim.transform.position).normalized;
|
||||||
|
enemy.TryTakeDamage(
|
||||||
|
damage,
|
||||||
|
direction,
|
||||||
|
0f,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
EnemyDamageSource.PassiveRelicExplosion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float GetRandomRoll()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (RandomRollForTests != null)
|
||||||
|
{
|
||||||
|
return RandomRollForTests();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return Random.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RollsChance(float chance)
|
||||||
|
{
|
||||||
|
return PassiveRelicRules.RollsChance(chance, GetRandomRoll());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6b531569e5b6419493b2af9d6a0be4ee
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -44,6 +44,7 @@ namespace BumpCombat.Spawning
|
|||||||
// current elite is still alive and the event must wait in the queue.
|
// current elite is still alive and the event must wait in the queue.
|
||||||
private int nextEliteScheduleOrdinal;
|
private int nextEliteScheduleOrdinal;
|
||||||
private int nextEliteSpawnOrdinal;
|
private int nextEliteSpawnOrdinal;
|
||||||
|
private static int nextEncounterGroupId;
|
||||||
private const int SummonSpawnAngleSteps = 32;
|
private const int SummonSpawnAngleSteps = 32;
|
||||||
|
|
||||||
private static RunConstants Tuning => GameplayConstants.Current.Run;
|
private static RunConstants Tuning => GameplayConstants.Current.Run;
|
||||||
@@ -148,6 +149,477 @@ namespace BumpCombat.Spawning
|
|||||||
return HasTimedEventEnemy(timedEvent);
|
return HasTimedEventEnemy(timedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SpawnZoneEncounter(BumpCombat.Stage.ZoneInstance zone)
|
||||||
|
{
|
||||||
|
if (zone == null) return;
|
||||||
|
EnsureTuningLoaded();
|
||||||
|
Vector2 center = zone.ZoneCenter;
|
||||||
|
Vector2 extents = zone.PlayableHalfExtents;
|
||||||
|
BumpCombat.Stage.RoomPreset preset = zone.Preset;
|
||||||
|
|
||||||
|
if (zone.NodeType == BumpCombat.Stage.StageNodeType.Elite)
|
||||||
|
{
|
||||||
|
Vector2 spawnPos = center;
|
||||||
|
if (preset != null && preset.SpawnPoints.Count > 0)
|
||||||
|
{
|
||||||
|
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.Elite);
|
||||||
|
if (pt.Role == BumpCombat.Stage.SpawnRole.Elite)
|
||||||
|
{
|
||||||
|
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RunEventEnemyTuning tuning = GetEliteTuning(0);
|
||||||
|
EnemyController prefab = GetEventPrefab(RunTimedEvent.Elite, tuning.PrefabKind);
|
||||||
|
if (prefab != null)
|
||||||
|
{
|
||||||
|
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
|
||||||
|
enemy.name = "Stage 1 Elite";
|
||||||
|
enemy.ConfigureRunEventEnemy(RunTimedEvent.Elite, tuning);
|
||||||
|
enemy.OnDied += HandleEventEnemyDied;
|
||||||
|
GetEventList(RunTimedEvent.Elite).Add(enemy);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
OnEventEnemySpawned?.Invoke(RunTimedEvent.Elite, enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnPresetSupportMinions(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.NodeType == BumpCombat.Stage.StageNodeType.MidBoss)
|
||||||
|
{
|
||||||
|
Vector2 spawnPos = center;
|
||||||
|
if (preset != null && preset.SpawnPoints.Count > 0)
|
||||||
|
{
|
||||||
|
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.MidBoss);
|
||||||
|
if (pt.Role == BumpCombat.Stage.SpawnRole.MidBoss)
|
||||||
|
{
|
||||||
|
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RunEventEnemyTuning tuning = GetEventEnemyTuning(RunTimedEvent.MidBoss);
|
||||||
|
EnemyController prefab = GetEventPrefab(RunTimedEvent.MidBoss, tuning.PrefabKind);
|
||||||
|
if (prefab != null)
|
||||||
|
{
|
||||||
|
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
|
||||||
|
enemy.name = "Stage 2 MidBoss";
|
||||||
|
enemy.ConfigureRunEventEnemy(RunTimedEvent.MidBoss, tuning);
|
||||||
|
enemy.OnDied += HandleEventEnemyDied;
|
||||||
|
GetEventList(RunTimedEvent.MidBoss).Add(enemy);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
OnEventEnemySpawned?.Invoke(RunTimedEvent.MidBoss, enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnPresetSupportMinions(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.NodeType == BumpCombat.Stage.StageNodeType.FinalBoss)
|
||||||
|
{
|
||||||
|
Vector2 spawnPos = center;
|
||||||
|
if (preset != null && preset.SpawnPoints.Count > 0)
|
||||||
|
{
|
||||||
|
var pt = preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.FinalBoss);
|
||||||
|
if (pt.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
|
||||||
|
{
|
||||||
|
spawnPos = preset.CellToWorldPosition(center, pt.CellPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RunEventEnemyTuning tuning = GetEventEnemyTuning(RunTimedEvent.FinalBoss);
|
||||||
|
EnemyController prefab = GetEventPrefab(RunTimedEvent.FinalBoss, tuning.PrefabKind);
|
||||||
|
if (prefab != null)
|
||||||
|
{
|
||||||
|
EnemyController enemy = Instantiate(prefab, spawnPos, Quaternion.identity);
|
||||||
|
enemy.name = "Stage 3 FinalBoss";
|
||||||
|
enemy.ConfigureRunEventEnemy(RunTimedEvent.FinalBoss, tuning);
|
||||||
|
enemy.OnDied += HandleEventEnemyDied;
|
||||||
|
GetEventList(RunTimedEvent.FinalBoss).Add(enemy);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
OnEventEnemySpawned?.Invoke(RunTimedEvent.FinalBoss, enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnPresetSupportMinions(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.NodeType == BumpCombat.Stage.StageNodeType.Combat || zone.NodeType == BumpCombat.Stage.StageNodeType.Fork)
|
||||||
|
{
|
||||||
|
if (preset != null && preset.EncounterPool != null && preset.EncounterPool.Count > 0)
|
||||||
|
{
|
||||||
|
SpawnPresetEncounterPool(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset != null && preset.SpawnPoints.Count > 0)
|
||||||
|
{
|
||||||
|
SpawnPresetAnchorsFallback(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Random area spawn if no preset anchors
|
||||||
|
if (enemyPrefabs == null || enemyPrefabs.Length == 0) return;
|
||||||
|
int count = UnityEngine.Random.Range(3, 6);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
EnemyRole role = (i % 2 == 0) ? EnemyRole.Normal : EnemyRole.Crowd;
|
||||||
|
EnemyController prefab = GetRandomPrefab(role) ?? enemyPrefabs[0];
|
||||||
|
Vector2 offset = new Vector2(
|
||||||
|
UnityEngine.Random.Range(-extents.x * 0.6f, extents.x * 0.6f),
|
||||||
|
UnityEngine.Random.Range(-extents.y * 0.6f, extents.y * 0.6f));
|
||||||
|
EnemyController enemy = Instantiate(prefab, center + offset, Quaternion.identity);
|
||||||
|
enemy.transform.SetParent(zone.transform, true);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnPresetEncounterPool(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
|
||||||
|
{
|
||||||
|
if (preset.EncounterPool == null || preset.EncounterPool.Count == 0)
|
||||||
|
{
|
||||||
|
SpawnPresetAnchorsFallback(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select encounter (default Encounter A for S1-A first verification)
|
||||||
|
BumpCombat.Stage.RoomEncounter encounter = preset.EncounterPool[0];
|
||||||
|
if (encounter.Items == null || encounter.Items.Count == 0)
|
||||||
|
{
|
||||||
|
SpawnPresetAnchorsFallback(zone, center, preset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerController player = FindAnyObjectByType<PlayerController>();
|
||||||
|
Vector2 playerPos = (player != null)
|
||||||
|
? (Vector2)player.transform.position
|
||||||
|
: zone.GetEntryPosition(BumpCombat.Stage.StageDirection.West);
|
||||||
|
|
||||||
|
List<Vector2> occupiedPositions = new();
|
||||||
|
|
||||||
|
// Sort items so NormalMelee / Ranged claim authored anchors first before Crowd
|
||||||
|
List<BumpCombat.Stage.RoomEncounterItem> sortedItems = new(encounter.Items);
|
||||||
|
sortedItems.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
bool aCrowd = a.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || a.EnemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool bCrowd = b.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || b.EnemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return aCrowd.CompareTo(bCrowd);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var item in sortedItems)
|
||||||
|
{
|
||||||
|
EnemyController prefab = ResolvePrefabForKind(item.EnemyKind);
|
||||||
|
if (prefab == null) continue;
|
||||||
|
|
||||||
|
bool isFast = item.EnemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase);
|
||||||
|
bool isRanged = item.EnemyKind.Contains("Archer", StringComparison.OrdinalIgnoreCase);
|
||||||
|
float requiredSafeDist = isRanged ? 7.0f : (isFast ? 5.0f : 4.0f);
|
||||||
|
|
||||||
|
int remaining = item.Count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
int groupSize = Mathf.Min(
|
||||||
|
SelectEncounterGroupSize(preset.StageIndex, UnityEngine.Random.value),
|
||||||
|
remaining);
|
||||||
|
nextEncounterGroupId = nextEncounterGroupId == int.MaxValue
|
||||||
|
? 1
|
||||||
|
: nextEncounterGroupId + 1;
|
||||||
|
int groupId = nextEncounterGroupId;
|
||||||
|
Vector2 groupAnchor = Vector2.zero;
|
||||||
|
|
||||||
|
for (int memberIndex = 0; memberIndex < groupSize; memberIndex++)
|
||||||
|
{
|
||||||
|
Vector2 spawnWorldPos = memberIndex == 0
|
||||||
|
? FindSafeSpawnPosition(
|
||||||
|
preset,
|
||||||
|
center,
|
||||||
|
playerPos,
|
||||||
|
item.EnemyKind,
|
||||||
|
requiredSafeDist,
|
||||||
|
occupiedPositions)
|
||||||
|
: FindSafeGroupSpawnPosition(
|
||||||
|
preset,
|
||||||
|
center,
|
||||||
|
playerPos,
|
||||||
|
requiredSafeDist,
|
||||||
|
groupAnchor,
|
||||||
|
occupiedPositions);
|
||||||
|
|
||||||
|
if (memberIndex == 0)
|
||||||
|
{
|
||||||
|
groupAnchor = spawnWorldPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController enemy = Instantiate(prefab, spawnWorldPos, Quaternion.identity);
|
||||||
|
enemy.name = $"{item.EnemyKind}_{occupiedPositions.Count + 1}";
|
||||||
|
enemy.transform.SetParent(zone.transform, true);
|
||||||
|
enemy.ConfigureSightRangeMultiplier(
|
||||||
|
GameplayConstants.Current.Run.StageEncounterSightRangeMultiplier);
|
||||||
|
enemy.ConfigureEncounterGroup(groupId, groupAnchor);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
occupiedPositions.Add(spawnWorldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= groupSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SelectEncounterGroupSize(int stageIndex, float roll)
|
||||||
|
{
|
||||||
|
roll = Mathf.Clamp01(roll);
|
||||||
|
if (stageIndex <= 1)
|
||||||
|
{
|
||||||
|
if (roll < 0.55f) return 1;
|
||||||
|
return roll < 0.90f ? 2 : 3;
|
||||||
|
}
|
||||||
|
if (stageIndex == 2)
|
||||||
|
{
|
||||||
|
if (roll < 0.35f) return 1;
|
||||||
|
return roll < 0.80f ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roll < 0.20f) return 1;
|
||||||
|
return roll < 0.70f ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 FindSafeGroupSpawnPosition(
|
||||||
|
BumpCombat.Stage.RoomPreset preset,
|
||||||
|
Vector2 zoneCenter,
|
||||||
|
Vector2 playerPos,
|
||||||
|
float safeDist,
|
||||||
|
Vector2 groupAnchor,
|
||||||
|
List<Vector2> occupied)
|
||||||
|
{
|
||||||
|
EnemyConstants tuning = GameplayConstants.Current.Enemies;
|
||||||
|
Vector2 best = groupAnchor;
|
||||||
|
float bestDistance = float.MaxValue;
|
||||||
|
|
||||||
|
for (int cy = 0; cy < preset.GridHeight; cy++)
|
||||||
|
{
|
||||||
|
for (int cx = 0; cx < preset.GridWidth; cx++)
|
||||||
|
{
|
||||||
|
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 candidate = preset.CellToWorldPosition(
|
||||||
|
zoneCenter,
|
||||||
|
new Vector2Int(cx, cy));
|
||||||
|
float groupDistance = Vector2.Distance(groupAnchor, candidate);
|
||||||
|
if (groupDistance < tuning.EncounterGroupMinSpacing
|
||||||
|
|| groupDistance > tuning.EncounterGroupMaxSpacing
|
||||||
|
|| Vector2.Distance(playerPos, candidate) < safeDist
|
||||||
|
|| !IsSeparatedFromOccupied(candidate, occupied, 0.8f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float targetDistance = Mathf.Abs(
|
||||||
|
groupDistance
|
||||||
|
- (tuning.EncounterGroupMinSpacing + tuning.EncounterGroupMaxSpacing) * 0.5f);
|
||||||
|
if (targetDistance < bestDistance)
|
||||||
|
{
|
||||||
|
bestDistance = targetDistance;
|
||||||
|
best = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestDistance < float.MaxValue
|
||||||
|
? best
|
||||||
|
: FindSafeSpawnPosition(
|
||||||
|
preset,
|
||||||
|
zoneCenter,
|
||||||
|
playerPos,
|
||||||
|
string.Empty,
|
||||||
|
safeDist,
|
||||||
|
occupied);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 FindSafeSpawnPosition(BumpCombat.Stage.RoomPreset preset, Vector2 zoneCenter, Vector2 playerPos,
|
||||||
|
string enemyKind, float safeDist, List<Vector2> occupied)
|
||||||
|
{
|
||||||
|
BumpCombat.Stage.SpawnRole targetRole = BumpCombat.Stage.SpawnRole.NormalMelee;
|
||||||
|
if (enemyKind.Equals("Bat", StringComparison.OrdinalIgnoreCase) || enemyKind.Equals("Slime", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
targetRole = BumpCombat.Stage.SpawnRole.Crowd;
|
||||||
|
}
|
||||||
|
else if (enemyKind.Contains("Archer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
targetRole = BumpCombat.Stage.SpawnRole.Ranged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check matching preset anchors first
|
||||||
|
for (int i = 0; i < preset.SpawnPoints.Count; i++)
|
||||||
|
{
|
||||||
|
var pt = preset.SpawnPoints[i];
|
||||||
|
if (pt.Role != targetRole) continue;
|
||||||
|
|
||||||
|
Vector2 wPos = preset.CellToWorldPosition(zoneCenter, pt.CellPosition);
|
||||||
|
if (Vector2.Distance(playerPos, wPos) >= safeDist && IsSeparatedFromOccupied(wPos, occupied, 0.8f))
|
||||||
|
{
|
||||||
|
return wPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Search spiral from anchor or center in Walkable cells
|
||||||
|
Vector2Int originCell = (targetRole == BumpCombat.Stage.SpawnRole.Crowd && preset.SpawnPoints.Count > 0)
|
||||||
|
? preset.SpawnPoints.Find(p => p.Role == BumpCombat.Stage.SpawnRole.Crowd).CellPosition
|
||||||
|
: new Vector2Int(preset.GridWidth / 2, preset.GridHeight / 2);
|
||||||
|
|
||||||
|
if (originCell.x < 0 || originCell.x >= preset.GridWidth) originCell = new Vector2Int(preset.GridWidth / 2, preset.GridHeight / 2);
|
||||||
|
|
||||||
|
int maxRadius = Mathf.Max(preset.GridWidth, preset.GridHeight);
|
||||||
|
for (int r = 0; r <= maxRadius; r++)
|
||||||
|
{
|
||||||
|
for (int dy = -r; dy <= r; dy++)
|
||||||
|
{
|
||||||
|
for (int dx = -r; dx <= r; dx++)
|
||||||
|
{
|
||||||
|
if (Mathf.Abs(dx) != r && Mathf.Abs(dy) != r) continue;
|
||||||
|
|
||||||
|
int cx = originCell.x + dx;
|
||||||
|
int cy = originCell.y + dy;
|
||||||
|
if (cx < 0 || cx >= preset.GridWidth || cy < 0 || cy >= preset.GridHeight) continue;
|
||||||
|
|
||||||
|
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable) continue;
|
||||||
|
|
||||||
|
Vector2 candidatePos = preset.CellToWorldPosition(zoneCenter, new Vector2Int(cx, cy));
|
||||||
|
if (Vector2.Distance(playerPos, candidatePos) >= safeDist && IsSeparatedFromOccupied(candidatePos, occupied, 0.8f))
|
||||||
|
{
|
||||||
|
return candidatePos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: farthest walkable cell from player
|
||||||
|
Vector2 farthestPos = zoneCenter;
|
||||||
|
float maxDist = -1f;
|
||||||
|
for (int cy = 0; cy < preset.GridHeight; cy++)
|
||||||
|
{
|
||||||
|
for (int cx = 0; cx < preset.GridWidth; cx++)
|
||||||
|
{
|
||||||
|
if (preset.GridMap[cx, cy] != BumpCombat.Stage.PresetCellType.Walkable) continue;
|
||||||
|
Vector2 p = preset.CellToWorldPosition(zoneCenter, new Vector2Int(cx, cy));
|
||||||
|
float d = Vector2.Distance(playerPos, p);
|
||||||
|
if (d > maxDist && IsSeparatedFromOccupied(p, occupied, 0.6f))
|
||||||
|
{
|
||||||
|
maxDist = d;
|
||||||
|
farthestPos = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return farthestPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSeparatedFromOccupied(Vector2 pos, List<Vector2> occupied, float minSeparation)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < occupied.Count; i++)
|
||||||
|
{
|
||||||
|
if (Vector2.Distance(pos, occupied[i]) < minSeparation)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnPresetAnchorsFallback(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
|
||||||
|
{
|
||||||
|
foreach (var spawnPoint in preset.SpawnPoints)
|
||||||
|
{
|
||||||
|
if (spawnPoint.Role == BumpCombat.Stage.SpawnRole.Elite ||
|
||||||
|
spawnPoint.Role == BumpCombat.Stage.SpawnRole.MidBoss ||
|
||||||
|
spawnPoint.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController prefab = ResolvePrefabForRole(spawnPoint.Role);
|
||||||
|
if (prefab != null)
|
||||||
|
{
|
||||||
|
Vector2 worldPos = preset.CellToWorldPosition(center, spawnPoint.CellPosition);
|
||||||
|
EnemyController enemy = Instantiate(prefab, worldPos, Quaternion.identity);
|
||||||
|
enemy.transform.SetParent(zone.transform, true);
|
||||||
|
zone.RegisterSpawnedEnemy(enemy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnemyController ResolvePrefabForKind(string kind)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(kind)) return null;
|
||||||
|
|
||||||
|
if (Enum.TryParse<EnemyKind>(kind, true, out EnemyKind parsedKind))
|
||||||
|
{
|
||||||
|
EnemyController prefab = GetPrefab(parsedKind);
|
||||||
|
if (prefab != null) return prefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
string lower = kind.ToLowerInvariant();
|
||||||
|
if (lower.Contains("bat")) return GetPrefab(EnemyKind.Bat) ?? GetRandomPrefab(EnemyRole.Crowd);
|
||||||
|
if (lower.Contains("archer")) return GetPrefab(EnemyKind.SkeletonArcher) ?? GetRandomPrefab(EnemyRole.Normal);
|
||||||
|
if (lower.Contains("skeleton")) return GetPrefab(EnemyKind.Skeleton) ?? GetRandomPrefab(EnemyRole.Normal);
|
||||||
|
if (lower.Contains("slime")) return GetPrefab(EnemyKind.Slime) ?? GetRandomPrefab(EnemyRole.Crowd);
|
||||||
|
if (lower.Contains("golem")) return GetPrefab(EnemyKind.NecroGolem) ?? GetRandomPrefab(EnemyRole.Normal);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnPresetSupportMinions(BumpCombat.Stage.ZoneInstance zone, Vector2 center, BumpCombat.Stage.RoomPreset preset)
|
||||||
|
{
|
||||||
|
if (preset == null || preset.SpawnPoints.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var pt in preset.SpawnPoints)
|
||||||
|
{
|
||||||
|
if (pt.Role == BumpCombat.Stage.SpawnRole.Elite ||
|
||||||
|
pt.Role == BumpCombat.Stage.SpawnRole.MidBoss ||
|
||||||
|
pt.Role == BumpCombat.Stage.SpawnRole.FinalBoss)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyController prefab = ResolvePrefabForRole(pt.Role);
|
||||||
|
if (prefab != null)
|
||||||
|
{
|
||||||
|
Vector2 worldPos = preset.CellToWorldPosition(center, pt.CellPosition);
|
||||||
|
EnemyController minion = Instantiate(prefab, worldPos, Quaternion.identity);
|
||||||
|
zone.RegisterSpawnedEnemy(minion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnemyController ResolvePrefabForRole(BumpCombat.Stage.SpawnRole role)
|
||||||
|
{
|
||||||
|
switch (role)
|
||||||
|
{
|
||||||
|
case BumpCombat.Stage.SpawnRole.Ranged:
|
||||||
|
return GetPrefab(EnemyKind.SkeletonArcher)
|
||||||
|
?? GetPrefab(EnemyKind.Warlock)
|
||||||
|
?? GetPrefab(EnemyKind.Necrofire)
|
||||||
|
?? GetRandomPrefab(EnemyRole.Normal)
|
||||||
|
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
|
||||||
|
|
||||||
|
case BumpCombat.Stage.SpawnRole.Crowd:
|
||||||
|
return GetPrefab(EnemyKind.Bat)
|
||||||
|
?? GetPrefab(EnemyKind.Slime)
|
||||||
|
?? GetRandomPrefab(EnemyRole.Crowd)
|
||||||
|
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
|
||||||
|
|
||||||
|
case BumpCombat.Stage.SpawnRole.NormalMelee:
|
||||||
|
default:
|
||||||
|
return GetPrefab(EnemyKind.Skeleton)
|
||||||
|
?? GetPrefab(EnemyKind.Lancer)
|
||||||
|
?? GetPrefab(EnemyKind.GreatswordSkeleton)
|
||||||
|
?? GetRandomPrefab(EnemyRole.Normal)
|
||||||
|
?? (enemyPrefabs != null && enemyPrefabs.Length > 0 ? enemyPrefabs[0] : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
if (RunManager.Instance != null)
|
if (RunManager.Instance != null)
|
||||||
@@ -175,6 +647,11 @@ namespace BumpCombat.Spawning
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (BumpCombat.Stage.StageManager.IsStageModeActive
|
||||||
|
|| RunManager.Instance?.IsDefensePrototypeRun == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
TrySpawnPendingEvents();
|
TrySpawnPendingEvents();
|
||||||
float elapsedTime = RunManager.Instance?.ProgressionTime ?? 0f;
|
float elapsedTime = RunManager.Instance?.ProgressionTime ?? 0f;
|
||||||
bool productionRun = RunManager.Instance?.IsProductionRun == true;
|
bool productionRun = RunManager.Instance?.IsProductionRun == true;
|
||||||
@@ -732,11 +1209,21 @@ namespace BumpCombat.Spawning
|
|||||||
if (role == RunTimedEvent.MidBoss)
|
if (role == RunTimedEvent.MidBoss)
|
||||||
{
|
{
|
||||||
midBossDefeated = true;
|
midBossDefeated = true;
|
||||||
RunManager.Instance?.UnlockArtifactCharge();
|
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
|
||||||
|
BumpCombat.Stage.StageNodeType.MidBoss,
|
||||||
|
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
|
||||||
}
|
}
|
||||||
else if (role == RunTimedEvent.Elite)
|
else if (role == RunTimedEvent.Elite)
|
||||||
{
|
{
|
||||||
RunManager.Instance?.UnlockGuard();
|
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
|
||||||
|
BumpCombat.Stage.StageNodeType.Elite,
|
||||||
|
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
|
||||||
|
}
|
||||||
|
else if (role == RunTimedEvent.FinalBoss)
|
||||||
|
{
|
||||||
|
BumpCombat.Stage.StageManager.Instance?.HandleMajorBossDefeated(
|
||||||
|
BumpCombat.Stage.StageNodeType.FinalBoss,
|
||||||
|
enemy.GetComponentInParent<BumpCombat.Stage.ZoneInstance>());
|
||||||
}
|
}
|
||||||
OnEventEnemyDefeated?.Invoke(role);
|
OnEventEnemyDefeated?.Invoke(role);
|
||||||
|
|
||||||
@@ -837,6 +1324,18 @@ namespace BumpCombat.Spawning
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EnemyController SpawnDefenseSkeleton(Vector2 position)
|
||||||
|
{
|
||||||
|
EnsureTuningLoaded();
|
||||||
|
EnemyController prefab = GetPrefab(EnemyKind.Skeleton);
|
||||||
|
if (prefab == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Instantiate(prefab, position, Quaternion.identity);
|
||||||
|
}
|
||||||
|
|
||||||
private EnemyController GetPrefab(EnemyKind kind)
|
private EnemyController GetPrefab(EnemyKind kind)
|
||||||
{
|
{
|
||||||
if (enemyPrefabs == null)
|
if (enemyPrefabs == null)
|
||||||
@@ -1254,11 +1753,20 @@ namespace BumpCombat.Spawning
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnemyRole role = crowd ? EnemyRole.Crowd : EnemyRole.Normal;
|
|
||||||
EnemyController[] candidates = new EnemyController[count];
|
EnemyController[] candidates = new EnemyController[count];
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
candidates[i] = GetRandomPrefab(role);
|
if (crowd)
|
||||||
|
{
|
||||||
|
EnemyKind preferredKind = i % 2 == 0 ? EnemyKind.Bat : EnemyKind.Slime;
|
||||||
|
EnemyKind fallbackKind = preferredKind == EnemyKind.Bat ? EnemyKind.Slime : EnemyKind.Bat;
|
||||||
|
candidates[i] = GetPrefab(preferredKind) ?? GetPrefab(fallbackKind);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
candidates[i] = GetRandomPrefab(EnemyRole.Normal);
|
||||||
|
}
|
||||||
|
|
||||||
if (candidates[i] == null)
|
if (candidates[i] == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -1405,6 +1913,14 @@ namespace BumpCombat.Spawning
|
|||||||
{
|
{
|
||||||
summonedEnemies.Add(enemy);
|
summonedEnemies.Add(enemy);
|
||||||
enemy.OnDied += HandleSummonedEnemyDied;
|
enemy.OnDied += HandleSummonedEnemyDied;
|
||||||
|
|
||||||
|
EnemyController owner = enemy.SummonOwner;
|
||||||
|
BumpCombat.Stage.ZoneInstance ownerZone =
|
||||||
|
owner != null ? owner.GetComponentInParent<BumpCombat.Stage.ZoneInstance>() : null;
|
||||||
|
if (ownerZone != null)
|
||||||
|
{
|
||||||
|
ownerZone.RegisterSpawnedEnemy(enemy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSummonedEnemyDied(EnemyController enemy)
|
private void HandleSummonedEnemyDied(EnemyController enemy)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e815e053fa79d1d48937f5a46191e144
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2c003d169eb47fa4686effb16f4ea632
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
public static class RoomPathfinder
|
||||||
|
{
|
||||||
|
private static readonly int[] Dx = { 0, 1, 0, -1, 1, 1, -1, -1 };
|
||||||
|
private static readonly int[] Dy = { -1, 0, 1, 0, -1, 1, 1, -1 };
|
||||||
|
private static readonly float[] Cost = { 1f, 1f, 1f, 1f, 1.414f, 1.414f, 1.414f, 1.414f };
|
||||||
|
|
||||||
|
public static bool HasLineOfSight(Vector2 fromPos, Vector2 toPos, bool isFlying, Collider2D ignoreCollider = null)
|
||||||
|
{
|
||||||
|
RaycastHit2D[] hits = Physics2D.LinecastAll(fromPos, toPos);
|
||||||
|
for (int i = 0; i < hits.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D col = hits[i].collider;
|
||||||
|
if (col == null || col == ignoreCollider || col.isTrigger) continue;
|
||||||
|
|
||||||
|
if (col.GetComponentInParent<BumpCombat.Player.PlayerController>() != null) continue;
|
||||||
|
if (col.GetComponentInParent<EnemyController>() != null) continue;
|
||||||
|
|
||||||
|
StageObstacle obs = col.GetComponentInParent<StageObstacle>();
|
||||||
|
if (obs != null)
|
||||||
|
{
|
||||||
|
if (isFlying) continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col.name.StartsWith("Wall_") || col.gameObject.name.Contains("Wall"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetNextWaypoint(
|
||||||
|
ZoneInstance zone,
|
||||||
|
Vector2 currentPos,
|
||||||
|
Vector2 targetPos,
|
||||||
|
bool isFlying,
|
||||||
|
out Vector2 nextWaypoint)
|
||||||
|
{
|
||||||
|
nextWaypoint = targetPos;
|
||||||
|
|
||||||
|
if (isFlying || zone == null || zone.Preset == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasLineOfSight(currentPos, targetPos, isFlying))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomPreset preset = zone.Preset;
|
||||||
|
int width = preset.GridWidth;
|
||||||
|
int height = preset.GridHeight;
|
||||||
|
if (width <= 0 || height <= 0 || preset.GridMap == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 center = zone.ZoneCenter;
|
||||||
|
Vector2Int startCell = WorldToCell(preset, center, currentPos);
|
||||||
|
Vector2Int targetCell = WorldToCell(preset, center, targetPos);
|
||||||
|
|
||||||
|
startCell.x = Mathf.Clamp(startCell.x, 0, width - 1);
|
||||||
|
startCell.y = Mathf.Clamp(startCell.y, 0, height - 1);
|
||||||
|
targetCell.x = Mathf.Clamp(targetCell.x, 0, width - 1);
|
||||||
|
targetCell.y = Mathf.Clamp(targetCell.y, 0, height - 1);
|
||||||
|
|
||||||
|
if (startCell == targetCell)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2Int nextCell = FindAStarNextCell(preset, startCell, targetCell, out _);
|
||||||
|
if (nextCell != startCell)
|
||||||
|
{
|
||||||
|
nextWaypoint = preset.CellToWorldPosition(center, nextCell);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetReachableRoamPoint(
|
||||||
|
ZoneInstance zone,
|
||||||
|
Vector2 currentPos,
|
||||||
|
Vector2 candidatePos,
|
||||||
|
bool isFlying,
|
||||||
|
out Vector2 roamPoint)
|
||||||
|
{
|
||||||
|
roamPoint = currentPos;
|
||||||
|
if (zone == null || zone.Preset == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomPreset preset = zone.Preset;
|
||||||
|
if (preset.GridMap == null || preset.GridWidth <= 0 || preset.GridHeight <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 center = zone.ZoneCenter;
|
||||||
|
Vector2Int startCell = WorldToCell(preset, center, currentPos);
|
||||||
|
Vector2Int targetCell = WorldToCell(preset, center, candidatePos);
|
||||||
|
if (!IsInsideGrid(preset, startCell) || !IsInsideGrid(preset, targetCell))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PresetCellType targetType = preset.GridMap[targetCell.x, targetCell.y];
|
||||||
|
if (targetType == PresetCellType.Empty
|
||||||
|
|| (!isFlying && targetType == PresetCellType.Obstacle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 targetCenter = preset.CellToWorldPosition(center, targetCell);
|
||||||
|
if (isFlying)
|
||||||
|
{
|
||||||
|
if (!HasLineOfSight(currentPos, targetCenter, true))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
roamPoint = targetCenter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsWalkable(preset, startCell.x, startCell.y))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startCell == targetCell)
|
||||||
|
{
|
||||||
|
roamPoint = targetCenter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FindAStarNextCell(preset, startCell, targetCell, out bool reachedTarget);
|
||||||
|
if (!reachedTarget)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
roamPoint = targetCenter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsInsideGrid(RoomPreset preset, Vector2Int cell)
|
||||||
|
{
|
||||||
|
return cell.x >= 0 && cell.x < preset.GridWidth
|
||||||
|
&& cell.y >= 0 && cell.y < preset.GridHeight;
|
||||||
|
}
|
||||||
|
public static Vector2Int WorldToCell(RoomPreset preset, Vector2 zoneCenter, Vector2 worldPos)
|
||||||
|
{
|
||||||
|
Vector2 local = worldPos - zoneCenter;
|
||||||
|
int x = Mathf.RoundToInt(local.x + (preset.GridWidth - 1) * 0.5f);
|
||||||
|
int y = Mathf.RoundToInt((preset.GridHeight - 1) * 0.5f - local.y);
|
||||||
|
return new Vector2Int(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsWalkable(RoomPreset preset, int x, int y)
|
||||||
|
{
|
||||||
|
if (x < 0 || x >= preset.GridWidth || y < 0 || y >= preset.GridHeight)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PresetCellType type = preset.GridMap[x, y];
|
||||||
|
return type != PresetCellType.Obstacle && type != PresetCellType.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2Int FindAStarNextCell(
|
||||||
|
RoomPreset preset,
|
||||||
|
Vector2Int start,
|
||||||
|
Vector2Int target,
|
||||||
|
out bool reachedTarget)
|
||||||
|
{
|
||||||
|
reachedTarget = start == target;
|
||||||
|
int width = preset.GridWidth;
|
||||||
|
int height = preset.GridHeight;
|
||||||
|
int totalCells = width * height;
|
||||||
|
|
||||||
|
float[] gCost = new float[totalCells];
|
||||||
|
bool[] closed = new bool[totalCells];
|
||||||
|
int[] parent = new int[totalCells];
|
||||||
|
|
||||||
|
for (int i = 0; i < totalCells; i++)
|
||||||
|
{
|
||||||
|
gCost[i] = float.MaxValue;
|
||||||
|
parent[i] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startIndex = start.y * width + start.x;
|
||||||
|
int targetIndex = target.y * width + target.x;
|
||||||
|
|
||||||
|
gCost[startIndex] = 0f;
|
||||||
|
|
||||||
|
List<int> openList = new List<int> { startIndex };
|
||||||
|
|
||||||
|
while (openList.Count > 0)
|
||||||
|
{
|
||||||
|
int bestIdx = 0;
|
||||||
|
int current = openList[0];
|
||||||
|
int curX = current % width;
|
||||||
|
int curY = current / width;
|
||||||
|
float bestF = gCost[current] + Heuristic(curX, curY, target.x, target.y);
|
||||||
|
|
||||||
|
for (int i = 1; i < openList.Count; i++)
|
||||||
|
{
|
||||||
|
int c = openList[i];
|
||||||
|
int cx = c % width;
|
||||||
|
int cy = c / width;
|
||||||
|
float f = gCost[c] + Heuristic(cx, cy, target.x, target.y);
|
||||||
|
if (f < bestF)
|
||||||
|
{
|
||||||
|
bestF = f;
|
||||||
|
current = c;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == targetIndex)
|
||||||
|
{
|
||||||
|
reachedTarget = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
openList.RemoveAt(bestIdx);
|
||||||
|
closed[current] = true;
|
||||||
|
|
||||||
|
curX = current % width;
|
||||||
|
curY = current / width;
|
||||||
|
|
||||||
|
for (int d = 0; d < 8; d++)
|
||||||
|
{
|
||||||
|
int nx = curX + Dx[d];
|
||||||
|
int ny = curY + Dy[d];
|
||||||
|
|
||||||
|
if (!IsWalkable(preset, nx, ny)) continue;
|
||||||
|
|
||||||
|
if (d >= 4)
|
||||||
|
{
|
||||||
|
if (!IsWalkable(preset, curX, ny) || !IsWalkable(preset, nx, curY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int nIdx = ny * width + nx;
|
||||||
|
if (closed[nIdx]) continue;
|
||||||
|
|
||||||
|
float tentativeG = gCost[current] + Cost[d];
|
||||||
|
if (tentativeG < gCost[nIdx])
|
||||||
|
{
|
||||||
|
gCost[nIdx] = tentativeG;
|
||||||
|
parent[nIdx] = current;
|
||||||
|
|
||||||
|
if (!openList.Contains(nIdx))
|
||||||
|
{
|
||||||
|
openList.Add(nIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int curr = targetIndex;
|
||||||
|
if (parent[curr] == -1)
|
||||||
|
{
|
||||||
|
float bestH = float.MaxValue;
|
||||||
|
int closest = startIndex;
|
||||||
|
for (int i = 0; i < totalCells; i++)
|
||||||
|
{
|
||||||
|
if (closed[i])
|
||||||
|
{
|
||||||
|
int ix = i % width;
|
||||||
|
int iy = i / width;
|
||||||
|
float h = Heuristic(ix, iy, target.x, target.y);
|
||||||
|
if (h < bestH)
|
||||||
|
{
|
||||||
|
bestH = h;
|
||||||
|
closest = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curr = closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curr == startIndex)
|
||||||
|
{
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (parent[curr] != -1 && parent[curr] != startIndex)
|
||||||
|
{
|
||||||
|
curr = parent[curr];
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextX = curr % width;
|
||||||
|
int nextY = curr / width;
|
||||||
|
return new Vector2Int(nextX, nextY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Heuristic(int x1, int y1, int x2, int y2)
|
||||||
|
{
|
||||||
|
int dx = Math.Abs(x1 - x2);
|
||||||
|
int dy = Math.Abs(y1 - y2);
|
||||||
|
return (dx + dy) + (1.414f - 2f) * Math.Min(dx, dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b000a747520a0a24099e7fbe4ae07ff2
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
public enum PresetCellType
|
||||||
|
{
|
||||||
|
Empty = 0, // □
|
||||||
|
Walkable = 1, // ■
|
||||||
|
Obstacle = 2, // ▲
|
||||||
|
Socket = 3, // ○
|
||||||
|
PlayerAnchor = 4, // ☆
|
||||||
|
RewardAnchor = 5, // △
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SpawnRole
|
||||||
|
{
|
||||||
|
NormalMelee,
|
||||||
|
Crowd,
|
||||||
|
Ranged,
|
||||||
|
Elite,
|
||||||
|
MidBoss,
|
||||||
|
FinalBoss,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct RoomPresetSocket
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public StageDirection Direction;
|
||||||
|
public Vector2Int CellPosition;
|
||||||
|
public int Width;
|
||||||
|
|
||||||
|
public RoomPresetSocket(string id, StageDirection direction, Vector2Int cellPos, int width = 2)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Direction = direction;
|
||||||
|
CellPosition = cellPos;
|
||||||
|
Width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct RoomPresetSpawnPoint
|
||||||
|
{
|
||||||
|
public string Id;
|
||||||
|
public SpawnRole Role;
|
||||||
|
public Vector2Int CellPosition;
|
||||||
|
|
||||||
|
public RoomPresetSpawnPoint(string id, SpawnRole role, Vector2Int cellPos)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Role = role;
|
||||||
|
CellPosition = cellPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class RoomEncounterItem
|
||||||
|
{
|
||||||
|
public string EnemyKind; // "Bat", "Skeleton", "Archer", "Slime", "Golem"
|
||||||
|
public int Count;
|
||||||
|
|
||||||
|
public RoomEncounterItem(string enemyKind, int count)
|
||||||
|
{
|
||||||
|
EnemyKind = enemyKind;
|
||||||
|
Count = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class RoomEncounter
|
||||||
|
{
|
||||||
|
public string Name;
|
||||||
|
public List<RoomEncounterItem> Items = new();
|
||||||
|
|
||||||
|
public RoomEncounter(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoomEncounter Add(string kind, int count)
|
||||||
|
{
|
||||||
|
Items.Add(new RoomEncounterItem(kind, count));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RoomPreset
|
||||||
|
{
|
||||||
|
public string PresetId { get; set; }
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public int StageIndex { get; set; }
|
||||||
|
public StageNodeType NodeType { get; set; }
|
||||||
|
public int GridWidth { get; set; }
|
||||||
|
public int GridHeight { get; set; }
|
||||||
|
|
||||||
|
public PresetCellType[,] GridMap { get; set; }
|
||||||
|
public List<RoomPresetSocket> Sockets { get; } = new();
|
||||||
|
public List<RoomPresetSpawnPoint> SpawnPoints { get; } = new();
|
||||||
|
public List<RoomEncounter> EncounterPool { get; } = new();
|
||||||
|
|
||||||
|
public Vector2Int PlayerAnchor { get; set; } = new(-1, -1);
|
||||||
|
public Vector2Int RewardAnchor { get; set; } = new(-1, -1);
|
||||||
|
|
||||||
|
public Vector2 PlayableHalfExtents => new(GridWidth * 0.5f, GridHeight * 0.5f);
|
||||||
|
|
||||||
|
public Vector2 CellToLocalPosition(int x, int y)
|
||||||
|
{
|
||||||
|
return new Vector2(
|
||||||
|
x - (GridWidth - 1) * 0.5f,
|
||||||
|
(GridHeight - 1) * 0.5f - y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 CellToLocalPosition(Vector2Int cell)
|
||||||
|
{
|
||||||
|
return CellToLocalPosition(cell.x, cell.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 CellToWorldPosition(Vector2 zoneCenter, Vector2Int cell)
|
||||||
|
{
|
||||||
|
return zoneCenter + CellToLocalPosition(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoomPresetSocket? GetSocket(string socketId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(socketId)) return null;
|
||||||
|
for (int i = 0; i < Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(Sockets[i].Id, socketId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Sockets[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasSocket(StageDirection dir)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
if (Sockets[i].Direction == dir) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoomPresetSocket? GetSocket(StageDirection dir)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
if (Sockets[i].Direction == dir) return Sockets[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetSocketCenterLocalPosition(RoomPresetSocket socket)
|
||||||
|
{
|
||||||
|
float centerX = socket.CellPosition.x;
|
||||||
|
float centerY = socket.CellPosition.y;
|
||||||
|
|
||||||
|
if (socket.Direction == StageDirection.North || socket.Direction == StageDirection.South)
|
||||||
|
{
|
||||||
|
centerX += (socket.Width - 1) * 0.5f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
centerY += (socket.Width - 1) * 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector2(
|
||||||
|
centerX - (GridWidth - 1) * 0.5f,
|
||||||
|
(GridHeight - 1) * 0.5f - centerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetSocketLocalPosition(StageDirection dir)
|
||||||
|
{
|
||||||
|
RoomPresetSocket? socket = GetSocket(dir);
|
||||||
|
if (socket.HasValue)
|
||||||
|
{
|
||||||
|
return GetSocketCenterLocalPosition(socket.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir switch
|
||||||
|
{
|
||||||
|
StageDirection.North => new Vector2(0f, PlayableHalfExtents.y),
|
||||||
|
StageDirection.South => new Vector2(0f, -PlayableHalfExtents.y),
|
||||||
|
StageDirection.East => new Vector2(PlayableHalfExtents.x, 0f),
|
||||||
|
StageDirection.West => new Vector2(-PlayableHalfExtents.x, 0f),
|
||||||
|
_ => Vector2.zero,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetEntryLocalPosition(RoomPresetSocket socket)
|
||||||
|
{
|
||||||
|
Vector2 basePos = GetSocketCenterLocalPosition(socket);
|
||||||
|
int maxDepth = Math.Max(GridWidth, GridHeight);
|
||||||
|
for (int depth = 2; GridMap != null && depth < maxDepth; depth++)
|
||||||
|
{
|
||||||
|
bool clear = true;
|
||||||
|
for (int offset = 0; offset < socket.Width; offset++)
|
||||||
|
{
|
||||||
|
int x = socket.CellPosition.x;
|
||||||
|
int y = socket.CellPosition.y;
|
||||||
|
if (socket.Direction == StageDirection.North) { x += offset; y += depth; }
|
||||||
|
else if (socket.Direction == StageDirection.South) { x += offset; y -= depth; }
|
||||||
|
else if (socket.Direction == StageDirection.East) { x -= depth; y += offset; }
|
||||||
|
else if (socket.Direction == StageDirection.West) { x += depth; y += offset; }
|
||||||
|
|
||||||
|
if (!RoomPathfinder.IsWalkable(this, x, y))
|
||||||
|
{
|
||||||
|
clear = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clear) continue;
|
||||||
|
|
||||||
|
return socket.Direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => basePos + new Vector2(0f, -depth),
|
||||||
|
StageDirection.South => basePos + new Vector2(0f, depth),
|
||||||
|
StageDirection.East => basePos + new Vector2(-depth, 0f),
|
||||||
|
StageDirection.West => basePos + new Vector2(depth, 0f),
|
||||||
|
_ => basePos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const float fallbackOffset = 1.35f;
|
||||||
|
return socket.Direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => basePos + new Vector2(0f, -fallbackOffset),
|
||||||
|
StageDirection.South => basePos + new Vector2(0f, fallbackOffset),
|
||||||
|
StageDirection.East => basePos + new Vector2(-fallbackOffset, 0f),
|
||||||
|
StageDirection.West => basePos + new Vector2(fallbackOffset, 0f),
|
||||||
|
_ => basePos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetEntryLocalPosition(StageDirection entryDirection, string socketId = null)
|
||||||
|
{
|
||||||
|
RoomPresetSocket? socket = !string.IsNullOrEmpty(socketId)
|
||||||
|
? GetSocket(socketId)
|
||||||
|
: GetSocket(entryDirection);
|
||||||
|
|
||||||
|
if (socket.HasValue)
|
||||||
|
{
|
||||||
|
return GetEntryLocalPosition(socket.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 basePos = GetSocketLocalPosition(entryDirection);
|
||||||
|
float inwardOffset = 1.35f;
|
||||||
|
return entryDirection switch
|
||||||
|
{
|
||||||
|
StageDirection.North => basePos + new Vector2(0f, -inwardOffset),
|
||||||
|
StageDirection.South => basePos + new Vector2(0f, inwardOffset),
|
||||||
|
StageDirection.East => basePos + new Vector2(-inwardOffset, 0f),
|
||||||
|
StageDirection.West => basePos + new Vector2(inwardOffset, 0f),
|
||||||
|
_ => basePos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d49115a75f6ce4040a5a4ab8e6dbe013
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
public enum StageDirection
|
||||||
|
{
|
||||||
|
North = 0,
|
||||||
|
East = 1,
|
||||||
|
South = 2,
|
||||||
|
West = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StageDirectionExtensions
|
||||||
|
{
|
||||||
|
public static StageDirection Opposite(this StageDirection direction)
|
||||||
|
{
|
||||||
|
return direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => StageDirection.South,
|
||||||
|
StageDirection.East => StageDirection.West,
|
||||||
|
StageDirection.South => StageDirection.North,
|
||||||
|
StageDirection.West => StageDirection.East,
|
||||||
|
_ => StageDirection.North,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector2Int ToVector2Int(this StageDirection direction)
|
||||||
|
{
|
||||||
|
return direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => new Vector2Int(0, 1),
|
||||||
|
StageDirection.East => new Vector2Int(1, 0),
|
||||||
|
StageDirection.South => new Vector2Int(0, -1),
|
||||||
|
StageDirection.West => new Vector2Int(-1, 0),
|
||||||
|
_ => Vector2Int.zero,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector2 ToVector2(this StageDirection direction)
|
||||||
|
{
|
||||||
|
Vector2Int v = direction.ToVector2Int();
|
||||||
|
return new Vector2(v.x, v.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StageNodeType
|
||||||
|
{
|
||||||
|
Entrance,
|
||||||
|
Combat,
|
||||||
|
Fork,
|
||||||
|
Reward,
|
||||||
|
Recovery,
|
||||||
|
Elite,
|
||||||
|
MidBoss,
|
||||||
|
FinalBoss,
|
||||||
|
StageExit,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StageRewardType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
RedArtifact,
|
||||||
|
BlueArtifact,
|
||||||
|
Upgrade,
|
||||||
|
Recovery,
|
||||||
|
Guard,
|
||||||
|
ArtifactCharge,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class StageSocketConnection
|
||||||
|
{
|
||||||
|
public string FromSocketId = string.Empty;
|
||||||
|
public StageDirection Direction;
|
||||||
|
public int TargetNodeId = -1;
|
||||||
|
public string TargetSocketId = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class StageNode
|
||||||
|
{
|
||||||
|
public int Id;
|
||||||
|
public StageNodeType NodeType;
|
||||||
|
public Vector2Int GridPosition;
|
||||||
|
public StageRewardType RewardType;
|
||||||
|
public string PresetId = string.Empty;
|
||||||
|
public bool Visited;
|
||||||
|
public bool Cleared;
|
||||||
|
public bool IsCurrent;
|
||||||
|
|
||||||
|
// Direction -> Connected NodeId. (-1 if not connected)
|
||||||
|
public int[] Connections = new int[4] { -1, -1, -1, -1 };
|
||||||
|
public List<StageSocketConnection> SocketConnections = new();
|
||||||
|
|
||||||
|
public int GetConnection(StageDirection dir)
|
||||||
|
{
|
||||||
|
int index = (int)dir;
|
||||||
|
return (index >= 0 && index < Connections.Length) ? Connections[index] : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetConnection(StageDirection dir, int targetNodeId)
|
||||||
|
{
|
||||||
|
int index = (int)dir;
|
||||||
|
if (index >= 0 && index < Connections.Length)
|
||||||
|
{
|
||||||
|
Connections[index] = targetNodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasConnection(StageDirection dir)
|
||||||
|
{
|
||||||
|
return GetConnection(dir) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StageSocketConnection GetSocketConnection(string socketId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(socketId)) return null;
|
||||||
|
for (int i = 0; i < SocketConnections.Count; i++)
|
||||||
|
{
|
||||||
|
StageSocketConnection connection = SocketConnections[i];
|
||||||
|
if (connection != null
|
||||||
|
&& string.Equals(connection.FromSocketId, socketId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSocketConnection(
|
||||||
|
string fromSocketId,
|
||||||
|
StageDirection direction,
|
||||||
|
int targetNodeId,
|
||||||
|
string targetSocketId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(fromSocketId))
|
||||||
|
{
|
||||||
|
SetConnection(direction, targetNodeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StageSocketConnection connection = GetSocketConnection(fromSocketId);
|
||||||
|
if (connection == null)
|
||||||
|
{
|
||||||
|
connection = new StageSocketConnection { FromSocketId = fromSocketId };
|
||||||
|
SocketConnections.Add(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.Direction = direction;
|
||||||
|
connection.TargetNodeId = targetNodeId;
|
||||||
|
connection.TargetSocketId = targetSocketId ?? string.Empty;
|
||||||
|
if (!HasConnection(direction))
|
||||||
|
{
|
||||||
|
SetConnection(direction, targetNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ConnectionCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < Connections.Length; i++)
|
||||||
|
{
|
||||||
|
if (Connections[i] >= 0) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class StagePlan
|
||||||
|
{
|
||||||
|
public int StageIndex; // 1, 2, 3
|
||||||
|
public int Seed;
|
||||||
|
public List<StageNode> Nodes = new();
|
||||||
|
public int StartNodeId;
|
||||||
|
public int MajorEncounterNodeId;
|
||||||
|
public int ExitNodeId;
|
||||||
|
|
||||||
|
public StageNode StartNode => GetNode(StartNodeId);
|
||||||
|
public StageNode MajorEncounterNode => GetNode(MajorEncounterNodeId);
|
||||||
|
public StageNode ExitNode => GetNode(ExitNodeId);
|
||||||
|
|
||||||
|
public StageNode GetNode(int id)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Nodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (Nodes[i].Id == id) return Nodes[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StageNode GetNodeAt(Vector2Int gridPosition)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Nodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (Nodes[i].GridPosition == gridPosition) return Nodes[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectInt GetGridBounds()
|
||||||
|
{
|
||||||
|
if (Nodes.Count == 0) return new RectInt(0, 0, 1, 1);
|
||||||
|
int minX = int.MaxValue;
|
||||||
|
int minY = int.MaxValue;
|
||||||
|
int maxX = int.MinValue;
|
||||||
|
int maxY = int.MinValue;
|
||||||
|
|
||||||
|
for (int i = 0; i < Nodes.Count; i++)
|
||||||
|
{
|
||||||
|
Vector2Int p = Nodes[i].GridPosition;
|
||||||
|
if (p.x < minX) minX = p.x;
|
||||||
|
if (p.y < minY) minY = p.y;
|
||||||
|
if (p.x > maxX) maxX = p.x;
|
||||||
|
if (p.y > maxY) maxY = p.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f104d6187a441b64490a36c56c14a47b
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
public static class StageGenerator
|
||||||
|
{
|
||||||
|
public static StagePlan GenerateStage(int stageIndex, int seed, int targetNodeCount = 0)
|
||||||
|
{
|
||||||
|
if (targetNodeCount <= 0)
|
||||||
|
{
|
||||||
|
targetNodeCount = stageIndex switch
|
||||||
|
{
|
||||||
|
1 => 6,
|
||||||
|
2 => 7,
|
||||||
|
3 => 8,
|
||||||
|
_ => 6,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage-specific seed scrambling for deterministic distinct stages within the same run
|
||||||
|
int stageSeed = seed ^ (stageIndex * 7919);
|
||||||
|
System.Random rng = new System.Random(stageSeed);
|
||||||
|
|
||||||
|
StagePlan plan = new StagePlan
|
||||||
|
{
|
||||||
|
StageIndex = stageIndex,
|
||||||
|
Seed = seed,
|
||||||
|
};
|
||||||
|
|
||||||
|
Dictionary<Vector2Int, StageNode> occupied = new();
|
||||||
|
int nextId = 1;
|
||||||
|
|
||||||
|
StageNode CreateNode(StageNodeType type, Vector2Int pos, StageRewardType reward = StageRewardType.None, string presetId = "")
|
||||||
|
{
|
||||||
|
int nodeId = nextId++;
|
||||||
|
if (string.IsNullOrEmpty(presetId))
|
||||||
|
{
|
||||||
|
RoomPreset preset = LevelPresetCatalog.GetPresetForNode(stageIndex, type, nodeId, stageSeed);
|
||||||
|
presetId = preset != null ? preset.PresetId : $"{type}_Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
StageNode node = new StageNode
|
||||||
|
{
|
||||||
|
Id = nodeId,
|
||||||
|
NodeType = type,
|
||||||
|
GridPosition = pos,
|
||||||
|
RewardType = reward,
|
||||||
|
PresetId = presetId,
|
||||||
|
};
|
||||||
|
plan.Nodes.Add(node);
|
||||||
|
occupied[pos] = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connect(StageNode from, StageNode to, StageDirection dir)
|
||||||
|
{
|
||||||
|
RoomPreset fromPreset = LevelPresetCatalog.GetPreset(from.PresetId);
|
||||||
|
RoomPreset toPreset = LevelPresetCatalog.GetPreset(to.PresetId);
|
||||||
|
RoomPresetSocket? fromSocket = FindAvailableSocket(from, fromPreset, dir);
|
||||||
|
RoomPresetSocket? toSocket = FindAvailableSocket(to, toPreset, dir.Opposite());
|
||||||
|
if (fromSocket.HasValue && toSocket.HasValue)
|
||||||
|
{
|
||||||
|
from.SetSocketConnection(fromSocket.Value.Id, dir, to.Id, toSocket.Value.Id);
|
||||||
|
to.SetSocketConnection(toSocket.Value.Id, dir.Opposite(), from.Id, fromSocket.Value.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
from.SetConnection(dir, to.Id);
|
||||||
|
to.SetConnection(dir.Opposite(), from.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomPresetSocket? FindAvailableSocket(StageNode node, RoomPreset preset, StageDirection dir)
|
||||||
|
{
|
||||||
|
if (preset == null) return null;
|
||||||
|
for (int i = 0; i < preset.Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
RoomPresetSocket socket = preset.Sockets[i];
|
||||||
|
if (socket.Direction == dir && node.GetSocketConnection(socket.Id) == null)
|
||||||
|
{
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary progression directions: prefer East (+X) or North (+Y)
|
||||||
|
bool goEastFirst = rng.NextDouble() < 0.5;
|
||||||
|
StageDirection primaryDir = goEastFirst ? StageDirection.East : StageDirection.North;
|
||||||
|
StageDirection branchDir = goEastFirst ? StageDirection.North : StageDirection.East;
|
||||||
|
|
||||||
|
// 1. Start Node at (0,0)
|
||||||
|
Vector2Int currentPos = Vector2Int.zero;
|
||||||
|
StageNode startNode = CreateNode(StageNodeType.Entrance, currentPos, StageRewardType.None);
|
||||||
|
plan.StartNodeId = startNode.Id;
|
||||||
|
|
||||||
|
// 2. Initial Combat Node
|
||||||
|
currentPos += primaryDir.ToVector2Int();
|
||||||
|
StageNode firstCombat = CreateNode(StageNodeType.Combat, currentPos, StageRewardType.None);
|
||||||
|
Connect(startNode, firstCombat, primaryDir);
|
||||||
|
|
||||||
|
// 3. Fork Node
|
||||||
|
currentPos += primaryDir.ToVector2Int();
|
||||||
|
StageNode forkNode = CreateNode(StageNodeType.Fork, currentPos, StageRewardType.None);
|
||||||
|
Connect(firstCombat, forkNode, primaryDir);
|
||||||
|
|
||||||
|
// Branch A: Side Branch (Reward / Recovery)
|
||||||
|
Vector2Int branchPos = forkNode.GridPosition + branchDir.ToVector2Int();
|
||||||
|
StageRewardType rewardType = StageRewardType.None;
|
||||||
|
|
||||||
|
StageNode rewardNode = CreateNode(StageNodeType.Reward, branchPos, rewardType);
|
||||||
|
Connect(forkNode, rewardNode, branchDir);
|
||||||
|
|
||||||
|
// Optional extra node on branch if targetNodeCount allows
|
||||||
|
if (targetNodeCount >= 7)
|
||||||
|
{
|
||||||
|
Vector2Int extraBranchPos = branchPos + branchDir.ToVector2Int();
|
||||||
|
if (!occupied.ContainsKey(extraBranchPos))
|
||||||
|
{
|
||||||
|
StageNode recoveryNode = CreateNode(StageNodeType.Recovery, extraBranchPos, StageRewardType.Recovery);
|
||||||
|
Connect(rewardNode, recoveryNode, branchDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch B: Major Progression Path
|
||||||
|
currentPos = forkNode.GridPosition + primaryDir.ToVector2Int();
|
||||||
|
StageNode preBossCombat = CreateNode(StageNodeType.Combat, currentPos, StageRewardType.None);
|
||||||
|
Connect(forkNode, preBossCombat, primaryDir);
|
||||||
|
|
||||||
|
// Additional combat node for stage 3 or large count
|
||||||
|
if (stageIndex == 3 || targetNodeCount >= 8)
|
||||||
|
{
|
||||||
|
Vector2Int extraCombatPos = currentPos + primaryDir.ToVector2Int();
|
||||||
|
if (!occupied.ContainsKey(extraCombatPos))
|
||||||
|
{
|
||||||
|
StageNode extraCombat = CreateNode(StageNodeType.Combat, extraCombatPos, StageRewardType.None);
|
||||||
|
Connect(preBossCombat, extraCombat, primaryDir);
|
||||||
|
currentPos = extraCombatPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Major Encounter Node
|
||||||
|
currentPos += primaryDir.ToVector2Int();
|
||||||
|
StageNodeType majorType = stageIndex switch
|
||||||
|
{
|
||||||
|
1 => StageNodeType.Elite,
|
||||||
|
2 => StageNodeType.MidBoss,
|
||||||
|
_ => StageNodeType.FinalBoss,
|
||||||
|
};
|
||||||
|
StageRewardType majorReward = stageIndex switch
|
||||||
|
{
|
||||||
|
1 => StageRewardType.Guard,
|
||||||
|
2 => StageRewardType.ArtifactCharge,
|
||||||
|
_ => StageRewardType.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
StageNode bossNode = CreateNode(majorType, currentPos, majorReward);
|
||||||
|
StageNode lastCombat = occupied.TryGetValue(currentPos - primaryDir.ToVector2Int(), out var prev) ? prev : preBossCombat;
|
||||||
|
Connect(lastCombat, bossNode, primaryDir);
|
||||||
|
plan.MajorEncounterNodeId = bossNode.Id;
|
||||||
|
|
||||||
|
// Stage Exit (Stage 1 & 2 only)
|
||||||
|
if (stageIndex < 3)
|
||||||
|
{
|
||||||
|
currentPos += primaryDir.ToVector2Int();
|
||||||
|
StageNode exitNode = CreateNode(StageNodeType.StageExit, currentPos, StageRewardType.None);
|
||||||
|
Connect(bossNode, exitNode, primaryDir);
|
||||||
|
plan.ExitNodeId = exitNode.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Stage 3 ends with FinalBoss
|
||||||
|
plan.ExitNodeId = bossNode.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 88a6a4cfc3af80346b8d19e35e0dd000
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3a9bd30ad88a2b6439ddd24c147ba25b
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
[ExecuteAlways]
|
||||||
|
[RequireComponent(typeof(Collider2D))]
|
||||||
|
public sealed class StageObstacle : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static readonly HashSet<StageObstacle> activeObstacles = new();
|
||||||
|
public static IReadOnlyCollection<StageObstacle> AllObstacles => activeObstacles;
|
||||||
|
|
||||||
|
private Collider2D obstacleCollider;
|
||||||
|
public Collider2D Collider => obstacleCollider != null ? obstacleCollider : (obstacleCollider = GetComponent<Collider2D>());
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (obstacleCollider == null)
|
||||||
|
{
|
||||||
|
obstacleCollider = GetComponent<Collider2D>();
|
||||||
|
}
|
||||||
|
activeObstacles.Add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
activeObstacles.Add(this);
|
||||||
|
IgnoreCollisionsWithFlyingEnemies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
activeObstacles.Remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
activeObstacles.Remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IgnoreCollisionsWithFlyingEnemies()
|
||||||
|
{
|
||||||
|
if (Collider == null) return;
|
||||||
|
|
||||||
|
EnemyController[] enemies = FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < enemies.Length; i++)
|
||||||
|
{
|
||||||
|
EnemyController enemy = enemies[i];
|
||||||
|
if (enemy != null && enemy.IsFlying && enemy.BodyCollider != null)
|
||||||
|
{
|
||||||
|
Physics2D.IgnoreCollision(enemy.BodyCollider, Collider, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterFlyingEnemy(EnemyController flyingEnemy)
|
||||||
|
{
|
||||||
|
if (flyingEnemy == null || !flyingEnemy.IsFlying || flyingEnemy.BodyCollider == null) return;
|
||||||
|
|
||||||
|
activeObstacles.RemoveWhere(obs => obs == null);
|
||||||
|
|
||||||
|
foreach (StageObstacle obstacle in activeObstacles)
|
||||||
|
{
|
||||||
|
if (obstacle != null && obstacle.Collider != null)
|
||||||
|
{
|
||||||
|
Physics2D.IgnoreCollision(flyingEnemy.BodyCollider, obstacle.Collider, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 310bf4383bf41c64e85c457bc29aed17
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
using System;
|
||||||
|
using BumpCombat.Player;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
[RequireComponent(typeof(Collider2D))]
|
||||||
|
public sealed class ZoneConnector : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private string socketId = string.Empty;
|
||||||
|
[SerializeField] private StageDirection direction;
|
||||||
|
[SerializeField] private int targetNodeId = -1;
|
||||||
|
[SerializeField] private string targetSocketId = string.Empty;
|
||||||
|
[SerializeField] private bool isLocked;
|
||||||
|
|
||||||
|
[SerializeField] private float socketWidth = 2f;
|
||||||
|
[SerializeField] private float padDepth = 1f;
|
||||||
|
|
||||||
|
private const float ArrivalClearance = 0.03f;
|
||||||
|
private const float FallbackPlayerBodyExtent = 0.275f;
|
||||||
|
private Collider2D triggerCollider;
|
||||||
|
private BoxCollider2D barrierCollider;
|
||||||
|
private SpriteRenderer barrierVisual;
|
||||||
|
private Collider2D playerWaitingForSeparation;
|
||||||
|
|
||||||
|
public string SocketId => socketId;
|
||||||
|
public StageDirection Direction => direction;
|
||||||
|
public int TargetNodeId => targetNodeId;
|
||||||
|
public string TargetSocketId => targetSocketId;
|
||||||
|
public bool IsLocked => isLocked;
|
||||||
|
|
||||||
|
public bool TryGetArrivalPosition(Collider2D playerBody, out Vector2 position)
|
||||||
|
{
|
||||||
|
EnsureComponents();
|
||||||
|
position = transform.position;
|
||||||
|
if (triggerCollider == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bounds triggerBounds = triggerCollider.bounds;
|
||||||
|
Vector2 inward = GetInwardDirection(direction);
|
||||||
|
Vector2 bodyExtents = playerBody != null && playerBody.enabled
|
||||||
|
? playerBody.bounds.extents
|
||||||
|
: new Vector2(FallbackPlayerBodyExtent, FallbackPlayerBodyExtent);
|
||||||
|
Vector2 bodyOffset = playerBody != null && playerBody.enabled
|
||||||
|
? (Vector2)playerBody.bounds.center - (Vector2)playerBody.transform.position
|
||||||
|
: Vector2.zero;
|
||||||
|
Vector2 bodyCenter = triggerBounds.center;
|
||||||
|
|
||||||
|
if (inward.x > 0f)
|
||||||
|
{
|
||||||
|
bodyCenter.x = triggerBounds.max.x + bodyExtents.x + ArrivalClearance;
|
||||||
|
}
|
||||||
|
else if (inward.x < 0f)
|
||||||
|
{
|
||||||
|
bodyCenter.x = triggerBounds.min.x - bodyExtents.x - ArrivalClearance;
|
||||||
|
}
|
||||||
|
else if (inward.y > 0f)
|
||||||
|
{
|
||||||
|
bodyCenter.y = triggerBounds.max.y + bodyExtents.y + ArrivalClearance;
|
||||||
|
}
|
||||||
|
else if (inward.y < 0f)
|
||||||
|
{
|
||||||
|
bodyCenter.y = triggerBounds.min.y - bodyExtents.y - ArrivalClearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
position = bodyCenter - bodyOffset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SuppressUntilSeparated(Collider2D playerBody)
|
||||||
|
{
|
||||||
|
if (playerBody != null
|
||||||
|
&& triggerCollider != null
|
||||||
|
&& triggerCollider.bounds.Intersects(playerBody.bounds))
|
||||||
|
{
|
||||||
|
playerWaitingForSeparation = playerBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(string sockId, StageDirection dir, int targetId, string targetSockId, bool locked = false, float width = 2f, float depth = 1f)
|
||||||
|
{
|
||||||
|
socketId = sockId;
|
||||||
|
direction = dir;
|
||||||
|
targetNodeId = targetId;
|
||||||
|
targetSocketId = targetSockId ?? string.Empty;
|
||||||
|
isLocked = locked;
|
||||||
|
socketWidth = Mathf.Max(0.5f, width);
|
||||||
|
padDepth = Mathf.Max(0.4f, depth);
|
||||||
|
EnsureComponents();
|
||||||
|
UpdateVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(StageDirection dir, int targetId, bool locked = false, float width = 2f, float depth = 1f)
|
||||||
|
{
|
||||||
|
Configure(string.Empty, dir, targetId, string.Empty, locked, width, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(string sockId, StageDirection dir, int targetId, bool locked = false, float width = 2f, float depth = 1f)
|
||||||
|
{
|
||||||
|
Configure(sockId, dir, targetId, string.Empty, locked, width, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLocked(bool locked)
|
||||||
|
{
|
||||||
|
isLocked = locked;
|
||||||
|
UpdateVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
EnsureComponents();
|
||||||
|
UpdateVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject gatePadObj;
|
||||||
|
private SpriteRenderer padRenderer;
|
||||||
|
private GameObject labelObj;
|
||||||
|
private TextMesh labelTextMesh;
|
||||||
|
|
||||||
|
private void EnsureComponents()
|
||||||
|
{
|
||||||
|
if (triggerCollider == null)
|
||||||
|
{
|
||||||
|
triggerCollider = GetComponent<Collider2D>();
|
||||||
|
if (triggerCollider != null)
|
||||||
|
{
|
||||||
|
triggerCollider.isTrigger = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align trigger collider inwards inside the room boundary
|
||||||
|
if (triggerCollider is BoxCollider2D boxCol)
|
||||||
|
{
|
||||||
|
bool isHorizontal = (direction == StageDirection.North || direction == StageDirection.South);
|
||||||
|
Vector2 colSize = isHorizontal
|
||||||
|
? new Vector2(socketWidth, padDepth)
|
||||||
|
: new Vector2(padDepth, socketWidth);
|
||||||
|
|
||||||
|
Vector2 colOffset = direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => new Vector2(0f, -padDepth * 0.5f),
|
||||||
|
StageDirection.South => new Vector2(0f, padDepth * 0.5f),
|
||||||
|
StageDirection.East => new Vector2(-padDepth * 0.5f, 0f),
|
||||||
|
StageDirection.West => new Vector2(padDepth * 0.5f, 0f),
|
||||||
|
_ => Vector2.zero,
|
||||||
|
};
|
||||||
|
|
||||||
|
boxCol.size = colSize;
|
||||||
|
boxCol.offset = colOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrierCollider == null)
|
||||||
|
{
|
||||||
|
Transform barrierTransform = transform.Find("BarrierCollider");
|
||||||
|
GameObject barrierObject = barrierTransform != null
|
||||||
|
? barrierTransform.gameObject
|
||||||
|
: new GameObject("BarrierCollider");
|
||||||
|
barrierObject.transform.SetParent(transform, false);
|
||||||
|
barrierObject.transform.localPosition = Vector3.zero;
|
||||||
|
barrierCollider = barrierObject.GetComponent<BoxCollider2D>();
|
||||||
|
if (barrierCollider == null)
|
||||||
|
{
|
||||||
|
barrierCollider = barrierObject.AddComponent<BoxCollider2D>();
|
||||||
|
}
|
||||||
|
barrierCollider.isTrigger = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrierCollider != null)
|
||||||
|
{
|
||||||
|
bool isHorizontal = direction == StageDirection.North || direction == StageDirection.South;
|
||||||
|
barrierCollider.size = isHorizontal
|
||||||
|
? new Vector2(socketWidth, 0.3f)
|
||||||
|
: new Vector2(0.3f, socketWidth);
|
||||||
|
barrierCollider.offset = Vector2.zero;
|
||||||
|
barrierCollider.enabled = isLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureVisuals()
|
||||||
|
{
|
||||||
|
if (gatePadObj == null)
|
||||||
|
{
|
||||||
|
Transform padTransform = transform.Find("GatePad");
|
||||||
|
if (padTransform != null)
|
||||||
|
{
|
||||||
|
gatePadObj = padTransform.gameObject;
|
||||||
|
padRenderer = gatePadObj.GetComponent<SpriteRenderer>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gatePadObj = new GameObject("GatePad");
|
||||||
|
gatePadObj.transform.SetParent(transform, false);
|
||||||
|
padRenderer = gatePadObj.AddComponent<SpriteRenderer>();
|
||||||
|
padRenderer.sprite = GetOrCreatePadSprite();
|
||||||
|
padRenderer.sortingLayerName = "Default";
|
||||||
|
padRenderer.sortingOrder = -50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align GatePad inwards so it never protrudes past room perimeter lines
|
||||||
|
if (gatePadObj != null)
|
||||||
|
{
|
||||||
|
bool isHorizontal = (direction == StageDirection.North || direction == StageDirection.South);
|
||||||
|
Vector2 padScale = isHorizontal
|
||||||
|
? new Vector2(socketWidth * 0.5f, padDepth * 0.5f)
|
||||||
|
: new Vector2(padDepth * 0.5f, socketWidth * 0.5f);
|
||||||
|
|
||||||
|
Vector2 padOffset = direction switch
|
||||||
|
{
|
||||||
|
StageDirection.North => new Vector2(0f, -padDepth * 0.5f),
|
||||||
|
StageDirection.South => new Vector2(0f, padDepth * 0.5f),
|
||||||
|
StageDirection.East => new Vector2(-padDepth * 0.5f, 0f),
|
||||||
|
StageDirection.West => new Vector2(padDepth * 0.5f, 0f),
|
||||||
|
_ => Vector2.zero,
|
||||||
|
};
|
||||||
|
|
||||||
|
gatePadObj.transform.localScale = padScale;
|
||||||
|
gatePadObj.transform.localPosition = padOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelObj == null)
|
||||||
|
{
|
||||||
|
Transform labelTransform = transform.Find("GateLabel");
|
||||||
|
if (labelTransform != null)
|
||||||
|
{
|
||||||
|
labelObj = labelTransform.gameObject;
|
||||||
|
labelTextMesh = labelObj.GetComponent<TextMesh>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
labelObj = new GameObject("GateLabel");
|
||||||
|
labelObj.transform.SetParent(transform, false);
|
||||||
|
labelTextMesh = labelObj.AddComponent<TextMesh>();
|
||||||
|
labelTextMesh.anchor = TextAnchor.MiddleCenter;
|
||||||
|
labelTextMesh.alignment = TextAlignment.Center;
|
||||||
|
labelTextMesh.fontSize = 28;
|
||||||
|
labelTextMesh.characterSize = 0.055f;
|
||||||
|
labelTextMesh.fontStyle = FontStyle.Bold;
|
||||||
|
|
||||||
|
MeshRenderer mr = labelObj.GetComponent<MeshRenderer>();
|
||||||
|
if (mr != null)
|
||||||
|
{
|
||||||
|
mr.sortingLayerName = "Default";
|
||||||
|
mr.sortingOrder = 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelObj != null)
|
||||||
|
{
|
||||||
|
// Position label inwards past the pad threshold
|
||||||
|
float labelDist = padDepth + 0.4f;
|
||||||
|
Vector2 labelOffset = direction switch
|
||||||
|
{
|
||||||
|
StageDirection.East => new Vector2(-labelDist, 0f),
|
||||||
|
StageDirection.West => new Vector2(labelDist, 0f),
|
||||||
|
StageDirection.North => new Vector2(0f, -labelDist),
|
||||||
|
StageDirection.South => new Vector2(0f, labelDist),
|
||||||
|
_ => Vector2.zero,
|
||||||
|
};
|
||||||
|
labelObj.transform.localPosition = labelOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrierVisual == null)
|
||||||
|
{
|
||||||
|
Transform visualTransform = transform.Find("BarrierVisual");
|
||||||
|
if (visualTransform != null)
|
||||||
|
{
|
||||||
|
barrierVisual = visualTransform.GetComponent<SpriteRenderer>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisuals()
|
||||||
|
{
|
||||||
|
EnsureVisuals();
|
||||||
|
|
||||||
|
if (padRenderer != null)
|
||||||
|
{
|
||||||
|
padRenderer.color = isLocked
|
||||||
|
? new Color(0.9f, 0.25f, 0.2f, 0.8f) // Red-orange when locked
|
||||||
|
: new Color(0.2f, 0.85f, 0.95f, 0.75f); // Bright cyan when open
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelTextMesh != null)
|
||||||
|
{
|
||||||
|
string arrow = direction switch
|
||||||
|
{
|
||||||
|
StageDirection.East => "▶",
|
||||||
|
StageDirection.West => "◀",
|
||||||
|
StageDirection.North => "▲",
|
||||||
|
StageDirection.South => "▼",
|
||||||
|
_ => "▶",
|
||||||
|
};
|
||||||
|
string socketName = string.IsNullOrEmpty(socketId) ? "출구" : socketId;
|
||||||
|
labelTextMesh.text = isLocked
|
||||||
|
? $"[ 잠김 {arrow} ]"
|
||||||
|
: $"[ {socketName} 출구 {arrow} ]";
|
||||||
|
labelTextMesh.color = isLocked
|
||||||
|
? new Color(1f, 0.35f, 0.35f, 1f)
|
||||||
|
: new Color(0.4f, 1f, 0.85f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrierVisual != null)
|
||||||
|
{
|
||||||
|
barrierVisual.gameObject.SetActive(isLocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barrierCollider != null)
|
||||||
|
{
|
||||||
|
barrierCollider.enabled = isLocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Sprite padSprite;
|
||||||
|
private static Sprite GetOrCreatePadSprite()
|
||||||
|
{
|
||||||
|
if (padSprite != null) return padSprite;
|
||||||
|
|
||||||
|
int size = 32;
|
||||||
|
Texture2D tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
|
||||||
|
Color fill = new Color(1f, 1f, 1f, 0.85f);
|
||||||
|
Color border = new Color(1f, 1f, 1f, 1f);
|
||||||
|
|
||||||
|
for (int y = 0; y < size; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < size; x++)
|
||||||
|
{
|
||||||
|
bool isBorder = (x <= 1 || x >= size - 2 || y <= 1 || y >= size - 2);
|
||||||
|
tex.SetPixel(x, y, isBorder ? border : fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tex.Apply();
|
||||||
|
tex.filterMode = FilterMode.Point;
|
||||||
|
padSprite = Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 16f);
|
||||||
|
return padSprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 GetInwardDirection(StageDirection entryDirection)
|
||||||
|
{
|
||||||
|
return entryDirection switch
|
||||||
|
{
|
||||||
|
StageDirection.West => Vector2.right,
|
||||||
|
StageDirection.East => Vector2.left,
|
||||||
|
StageDirection.North => Vector2.down,
|
||||||
|
StageDirection.South => Vector2.up,
|
||||||
|
_ => Vector2.right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter2D(Collider2D other)
|
||||||
|
{
|
||||||
|
TryHandlePlayerTrigger(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerStay2D(Collider2D other)
|
||||||
|
{
|
||||||
|
TryHandlePlayerTrigger(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryHandlePlayerTrigger(Collider2D other)
|
||||||
|
{
|
||||||
|
PlayerController player = other.GetComponent<PlayerController>()
|
||||||
|
?? other.GetComponentInParent<PlayerController>();
|
||||||
|
if (player == null || playerWaitingForSeparation == other)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocked || targetNodeId < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StageManager stageManager = StageManager.Instance;
|
||||||
|
if (stageManager != null
|
||||||
|
&& (stageManager.IsTransitioning || !stageManager.CanTriggerZoneTransition))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stageManager?.RequestZoneTransition(targetNodeId, direction.Opposite(), targetSocketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerExit2D(Collider2D other)
|
||||||
|
{
|
||||||
|
if (playerWaitingForSeparation == other)
|
||||||
|
{
|
||||||
|
playerWaitingForSeparation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b932041c3dc4d3a47a4261ef873089ad
|
||||||
@@ -0,0 +1,879 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BumpCombat.Constants;
|
||||||
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Enemies;
|
||||||
|
using BumpCombat.Player;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BumpCombat.Stage
|
||||||
|
{
|
||||||
|
public sealed class ZoneInstance : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private int nodeId;
|
||||||
|
[SerializeField] private StageNodeType nodeType;
|
||||||
|
[SerializeField] private Vector2 playableHalfExtents = new(14.5f, 7.8125f);
|
||||||
|
[SerializeField] private bool lockUntilClear;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ZoneConnector> connectorsById = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<StageDirection, ZoneConnector> connectorsByDir = new();
|
||||||
|
private readonly List<EnemyController> spawnedEnemies = new();
|
||||||
|
private readonly List<GameObject> spawnedObstacles = new();
|
||||||
|
private readonly HashSet<EnemyController> fogHiddenEnemies = new();
|
||||||
|
private StageNode nodeData;
|
||||||
|
private RoomPreset preset;
|
||||||
|
private PresetCellType[,] runtimeGrid;
|
||||||
|
private bool[,] visibleCells;
|
||||||
|
private bool[,] exploredCells;
|
||||||
|
private Vector2Int lastVisibilityOrigin = new(int.MinValue, int.MinValue);
|
||||||
|
private PlayerController visibilityPlayer;
|
||||||
|
private StageFogOfWar fogOfWar;
|
||||||
|
private int visibilityRevision;
|
||||||
|
private bool hasSpawnedEncounter;
|
||||||
|
private bool isCleared;
|
||||||
|
|
||||||
|
public int NodeId => nodeId;
|
||||||
|
public StageNodeType NodeType => nodeType;
|
||||||
|
public Vector2 PlayableHalfExtents => playableHalfExtents;
|
||||||
|
public Vector2 ZoneCenter => (Vector2)transform.position;
|
||||||
|
public bool LockUntilClear => lockUntilClear;
|
||||||
|
public bool IsCleared => isCleared;
|
||||||
|
public bool IsStage1FirstCombatComparison { get; private set; }
|
||||||
|
public RoomPreset Preset => preset;
|
||||||
|
public StageNode NodeData => nodeData;
|
||||||
|
public bool HasRuntimeGrid => runtimeGrid != null;
|
||||||
|
public IReadOnlyList<EnemyController> SpawnedEnemies => spawnedEnemies;
|
||||||
|
public IReadOnlyList<GameObject> SpawnedObstacles => spawnedObstacles;
|
||||||
|
public StageFogOfWar FogOfWar => fogOfWar;
|
||||||
|
public int VisibilityRevision => visibilityRevision;
|
||||||
|
|
||||||
|
public void SetStage1FirstCombatComparison(bool value)
|
||||||
|
{
|
||||||
|
IsStage1FirstCombatComparison = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(StageNode node, Vector2 halfExtents)
|
||||||
|
{
|
||||||
|
nodeData = node;
|
||||||
|
nodeId = node.Id;
|
||||||
|
nodeType = node.NodeType;
|
||||||
|
|
||||||
|
preset = LevelPresetCatalog.GetPreset(node.PresetId);
|
||||||
|
if (preset != null)
|
||||||
|
{
|
||||||
|
playableHalfExtents = preset.PlayableHalfExtents;
|
||||||
|
runtimeGrid = BuildRuntimeGrid(node, preset);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
playableHalfExtents = halfExtents;
|
||||||
|
runtimeGrid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeGrid != null && preset != null)
|
||||||
|
{
|
||||||
|
visibleCells = new bool[preset.GridWidth, preset.GridHeight];
|
||||||
|
exploredCells = new bool[preset.GridWidth, preset.GridHeight];
|
||||||
|
if (Application.isPlaying)
|
||||||
|
{
|
||||||
|
fogOfWar = gameObject.AddComponent<StageFogOfWar>();
|
||||||
|
fogOfWar.Initialize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Major encounters are locked until cleared. Normal combat is break-through (LockUntilClear = false).
|
||||||
|
lockUntilClear = nodeType switch
|
||||||
|
{
|
||||||
|
StageNodeType.Elite => true,
|
||||||
|
StageNodeType.MidBoss => true,
|
||||||
|
StageNodeType.FinalBoss => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
isCleared = node.Cleared;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PresetCellType GetRuntimeCell(int x, int y)
|
||||||
|
{
|
||||||
|
return runtimeGrid[x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCellVisible(int x, int y)
|
||||||
|
{
|
||||||
|
return IsInsideGrid(x, y) && visibleCells != null && visibleCells[x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCellExplored(int x, int y)
|
||||||
|
{
|
||||||
|
return IsInsideGrid(x, y) && exploredCells != null && exploredCells[x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsWorldPositionVisible(Vector2 worldPosition)
|
||||||
|
{
|
||||||
|
if (preset == null || visibleCells == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2Int cell = RoomPathfinder.WorldToCell(preset, ZoneCenter, worldPosition);
|
||||||
|
return IsCellVisible(cell.x, cell.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateVisibility(Vector2 playerWorldPosition)
|
||||||
|
{
|
||||||
|
if (preset == null || runtimeGrid == null || visibleCells == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2Int origin = RoomPathfinder.WorldToCell(
|
||||||
|
preset,
|
||||||
|
ZoneCenter,
|
||||||
|
playerWorldPosition);
|
||||||
|
if (!IsInsideGrid(origin.x, origin.y))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origin == lastVisibilityOrigin)
|
||||||
|
{
|
||||||
|
RefreshEnemyVisibility();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVisibilityOrigin = origin;
|
||||||
|
float radius = Mathf.Max(
|
||||||
|
0.1f,
|
||||||
|
GameplayConstants.Current.Run.StagePlayerVisibilityRadius);
|
||||||
|
float radiusSquared = radius * radius;
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
for (int y = 0; y < preset.GridHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < preset.GridWidth; x++)
|
||||||
|
{
|
||||||
|
PresetCellType cellType = runtimeGrid[x, y];
|
||||||
|
bool wasVisible = visibleCells[x, y];
|
||||||
|
bool visible = false;
|
||||||
|
if (cellType != PresetCellType.Empty)
|
||||||
|
{
|
||||||
|
Vector2 cellPosition = preset.CellToWorldPosition(
|
||||||
|
ZoneCenter,
|
||||||
|
new Vector2Int(x, y));
|
||||||
|
bool inRadius = (cellPosition - playerWorldPosition).sqrMagnitude
|
||||||
|
<= radiusSquared;
|
||||||
|
visible = inRadius
|
||||||
|
&& HasGridLineOfSight(origin, new Vector2Int(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleCells[x, y] = visible;
|
||||||
|
changed |= wasVisible != visible;
|
||||||
|
if (visible && !exploredCells[x, y])
|
||||||
|
{
|
||||||
|
exploredCells[x, y] = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
visibilityRevision++;
|
||||||
|
fogOfWar?.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshEnemyVisibility();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshEnemyVisibility()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < spawnedEnemies.Count; i++)
|
||||||
|
{
|
||||||
|
EnemyController enemy = spawnedEnemies[i];
|
||||||
|
if (enemy == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool visible = IsWorldPositionVisible(enemy.GroundAnchorPosition);
|
||||||
|
if (visible)
|
||||||
|
{
|
||||||
|
if (fogHiddenEnemies.Remove(enemy))
|
||||||
|
{
|
||||||
|
enemy.SetFogVisibility(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fogHiddenEnemies.Add(enemy))
|
||||||
|
{
|
||||||
|
enemy.SetFogVisibility(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasGridLineOfSight(Vector2Int from, Vector2Int to)
|
||||||
|
{
|
||||||
|
int x = from.x;
|
||||||
|
int y = from.y;
|
||||||
|
int dx = Mathf.Abs(to.x - from.x);
|
||||||
|
int dy = Mathf.Abs(to.y - from.y);
|
||||||
|
int stepX = from.x < to.x ? 1 : -1;
|
||||||
|
int stepY = from.y < to.y ? 1 : -1;
|
||||||
|
int error = dx - dy;
|
||||||
|
|
||||||
|
while (x != to.x || y != to.y)
|
||||||
|
{
|
||||||
|
int twiceError = error * 2;
|
||||||
|
bool moveX = twiceError > -dy;
|
||||||
|
bool moveY = twiceError < dx;
|
||||||
|
int nextX = x + (moveX ? stepX : 0);
|
||||||
|
int nextY = y + (moveY ? stepY : 0);
|
||||||
|
if (moveX && moveY
|
||||||
|
&& (!IsTransparentCell(nextX, y)
|
||||||
|
|| !IsTransparentCell(x, nextY)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveX)
|
||||||
|
{
|
||||||
|
error -= dy;
|
||||||
|
x = nextX;
|
||||||
|
}
|
||||||
|
if (moveY)
|
||||||
|
{
|
||||||
|
error += dx;
|
||||||
|
y = nextY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x == to.x && y == to.y)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsInsideGrid(x, y)
|
||||||
|
|| runtimeGrid[x, y] == PresetCellType.Empty
|
||||||
|
|| runtimeGrid[x, y] == PresetCellType.Obstacle)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTransparentCell(int x, int y)
|
||||||
|
{
|
||||||
|
return IsInsideGrid(x, y)
|
||||||
|
&& runtimeGrid[x, y] != PresetCellType.Empty
|
||||||
|
&& runtimeGrid[x, y] != PresetCellType.Obstacle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsInsideGrid(int x, int y)
|
||||||
|
{
|
||||||
|
return preset != null
|
||||||
|
&& x >= 0
|
||||||
|
&& x < preset.GridWidth
|
||||||
|
&& y >= 0
|
||||||
|
&& y < preset.GridHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PresetCellType[,] BuildRuntimeGrid(StageNode node, RoomPreset roomPreset)
|
||||||
|
{
|
||||||
|
if (roomPreset.GridMap == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = roomPreset.GridWidth;
|
||||||
|
int height = roomPreset.GridHeight;
|
||||||
|
PresetCellType[,] resolvedGrid = new PresetCellType[width, height];
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
PresetCellType cellType = roomPreset.GridMap[x, y];
|
||||||
|
resolvedGrid[x, y] = cellType == PresetCellType.Socket
|
||||||
|
&& !IsPresetSocketActive(node, roomPreset, x, y)
|
||||||
|
? PresetCellType.Empty
|
||||||
|
: cellType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < roomPreset.Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
RoomPresetSocket socket = roomPreset.Sockets[i];
|
||||||
|
if (!IsPresetSocketActive(node, roomPreset, socket.CellPosition.x, socket.CellPosition.y))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool horizontal = socket.Direction == StageDirection.North || socket.Direction == StageDirection.South;
|
||||||
|
for (int offset = 0; offset < socket.Width; offset++)
|
||||||
|
{
|
||||||
|
int x = socket.CellPosition.x + (horizontal ? offset : 0);
|
||||||
|
int y = socket.CellPosition.y + (horizontal ? 0 : offset);
|
||||||
|
|
||||||
|
while (x >= 0 && x < width && y >= 0 && y < height)
|
||||||
|
{
|
||||||
|
resolvedGrid[x, y] = PresetCellType.Socket;
|
||||||
|
|
||||||
|
if (socket.Direction == StageDirection.North) y--;
|
||||||
|
else if (socket.Direction == StageDirection.South) y++;
|
||||||
|
else if (socket.Direction == StageDirection.East) x++;
|
||||||
|
else if (socket.Direction == StageDirection.West) x--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedGrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPresetSocketActive(StageNode node, RoomPreset roomPreset, int x, int y)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < roomPreset.Sockets.Count; i++)
|
||||||
|
{
|
||||||
|
RoomPresetSocket socket = roomPreset.Sockets[i];
|
||||||
|
bool horizontal = socket.Direction == StageDirection.North || socket.Direction == StageDirection.South;
|
||||||
|
bool containsCell = horizontal
|
||||||
|
? y == socket.CellPosition.y && x >= socket.CellPosition.x && x < socket.CellPosition.x + socket.Width
|
||||||
|
: x == socket.CellPosition.x && y >= socket.CellPosition.y && y < socket.CellPosition.y + socket.Width;
|
||||||
|
|
||||||
|
if (!containsCell) continue;
|
||||||
|
|
||||||
|
StageSocketConnection connection = node.GetSocketConnection(socket.Id);
|
||||||
|
if (connection != null && connection.TargetNodeId >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasSocketConnectionInDirection = false;
|
||||||
|
for (int j = 0; j < roomPreset.Sockets.Count; j++)
|
||||||
|
{
|
||||||
|
RoomPresetSocket directionSocket = roomPreset.Sockets[j];
|
||||||
|
if (directionSocket.Direction != socket.Direction) continue;
|
||||||
|
|
||||||
|
StageSocketConnection directionConnection = node.GetSocketConnection(directionSocket.Id);
|
||||||
|
if (directionConnection != null && directionConnection.TargetNodeId >= 0)
|
||||||
|
{
|
||||||
|
hasSocketConnectionInDirection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSocketConnectionInDirection && node.GetConnection(socket.Direction) >= 0)
|
||||||
|
{
|
||||||
|
RoomPresetSocket? legacySocket = roomPreset.GetSocket(socket.Direction);
|
||||||
|
return legacySocket.HasValue
|
||||||
|
&& string.Equals(legacySocket.Value.Id, socket.Id, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterConnector(string socketId, StageDirection dir, ZoneConnector connector)
|
||||||
|
{
|
||||||
|
if (connector == null) return;
|
||||||
|
if (!string.IsNullOrEmpty(socketId))
|
||||||
|
{
|
||||||
|
connectorsById[socketId] = connector;
|
||||||
|
}
|
||||||
|
connectorsByDir[dir] = connector;
|
||||||
|
connector.SetLocked(lockUntilClear && !isCleared);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterConnector(StageDirection dir, ZoneConnector connector)
|
||||||
|
{
|
||||||
|
RegisterConnector(string.Empty, dir, connector);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterObstacle(GameObject obstacle)
|
||||||
|
{
|
||||||
|
if (obstacle != null && !spawnedObstacles.Contains(obstacle))
|
||||||
|
{
|
||||||
|
spawnedObstacles.Add(obstacle);
|
||||||
|
Collider2D obsCollider = obstacle.GetComponent<Collider2D>();
|
||||||
|
if (obsCollider != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < spawnedEnemies.Count; i++)
|
||||||
|
{
|
||||||
|
EnemyController enemy = spawnedEnemies[i];
|
||||||
|
if (enemy != null && enemy.IsFlying && enemy.BodyCollider != null)
|
||||||
|
{
|
||||||
|
Physics2D.IgnoreCollision(enemy.BodyCollider, obsCollider, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoneConnector GetConnector(string socketId)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(socketId) && connectorsById.TryGetValue(socketId, out var connector))
|
||||||
|
{
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZoneConnector GetConnector(StageDirection dir)
|
||||||
|
{
|
||||||
|
connectorsByDir.TryGetValue(dir, out var connector);
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetEntryPosition(
|
||||||
|
StageDirection entryDirection,
|
||||||
|
string socketId = null,
|
||||||
|
Collider2D playerBody = null)
|
||||||
|
{
|
||||||
|
if (TryGetEntryPosition(entryDirection, socketId, playerBody, out Vector2 arrivalPosition))
|
||||||
|
{
|
||||||
|
return arrivalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset != null)
|
||||||
|
{
|
||||||
|
return ZoneCenter + preset.GetEntryLocalPosition(entryDirection, socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 center = ZoneCenter;
|
||||||
|
return entryDirection switch
|
||||||
|
{
|
||||||
|
StageDirection.North => center + new Vector2(0f, playableHalfExtents.y - 1.35f),
|
||||||
|
StageDirection.South => center + new Vector2(0f, -playableHalfExtents.y + 1.35f),
|
||||||
|
StageDirection.East => center + new Vector2(playableHalfExtents.x - 1.35f, 0f),
|
||||||
|
StageDirection.West => center + new Vector2(-playableHalfExtents.x + 1.35f, 0f),
|
||||||
|
_ => center,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetEntryPosition(
|
||||||
|
StageDirection entryDirection,
|
||||||
|
string socketId,
|
||||||
|
Collider2D playerBody,
|
||||||
|
out Vector2 position)
|
||||||
|
{
|
||||||
|
position = default;
|
||||||
|
ZoneConnector connector = !string.IsNullOrEmpty(socketId)
|
||||||
|
? GetConnector(socketId)
|
||||||
|
: GetConnector(entryDirection);
|
||||||
|
if (connector == null
|
||||||
|
|| !connector.TryGetArrivalPosition(playerBody, out Vector2 arrivalPosition))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryFindSafeArrivalPosition(connector, arrivalPosition, playerBody, out position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFindSafeArrivalPosition(
|
||||||
|
ZoneConnector connector,
|
||||||
|
Vector2 candidate,
|
||||||
|
Collider2D playerBody,
|
||||||
|
out Vector2 safePosition)
|
||||||
|
{
|
||||||
|
safePosition = default;
|
||||||
|
if (playerBody == null)
|
||||||
|
{
|
||||||
|
safePosition = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 inward = connector.Direction switch
|
||||||
|
{
|
||||||
|
StageDirection.West => Vector2.right,
|
||||||
|
StageDirection.East => Vector2.left,
|
||||||
|
StageDirection.North => Vector2.down,
|
||||||
|
StageDirection.South => Vector2.up,
|
||||||
|
_ => Vector2.right,
|
||||||
|
};
|
||||||
|
Vector2 bodyOffset = (Vector2)playerBody.bounds.center - (Vector2)playerBody.transform.position;
|
||||||
|
Vector2 bodyExtents = playerBody.bounds.extents;
|
||||||
|
const float arrivalSearchStep = 0.25f;
|
||||||
|
float maxDistance = Mathf.Max(playableHalfExtents.x, playableHalfExtents.y) * 2f;
|
||||||
|
|
||||||
|
for (float distance = 0f; distance <= maxDistance; distance += arrivalSearchStep)
|
||||||
|
{
|
||||||
|
Vector2 candidatePosition = candidate + inward * distance;
|
||||||
|
Vector2 candidateBodyCenter = candidatePosition + bodyOffset;
|
||||||
|
if (IsArrivalPositionClear(
|
||||||
|
candidatePosition,
|
||||||
|
candidateBodyCenter,
|
||||||
|
bodyExtents,
|
||||||
|
playerBody))
|
||||||
|
{
|
||||||
|
safePosition = candidatePosition;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsArrivalPositionClear(
|
||||||
|
Vector2 playerPosition,
|
||||||
|
Vector2 bodyCenter,
|
||||||
|
Vector2 bodyExtents,
|
||||||
|
Collider2D playerBody)
|
||||||
|
{
|
||||||
|
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||||
|
Vector2 playerPadding = bounds != null
|
||||||
|
? bounds.PlayerPadding
|
||||||
|
: GameplayConstants.Current.Run.ArenaPlayerPadding;
|
||||||
|
Vector2 localPosition = playerPosition - ZoneCenter;
|
||||||
|
Vector2 localClamp = new Vector2(
|
||||||
|
Mathf.Max(0f, playableHalfExtents.x - playerPadding.x),
|
||||||
|
Mathf.Max(0f, playableHalfExtents.y - playerPadding.y));
|
||||||
|
if (Mathf.Abs(localPosition.x) > localClamp.x + 0.0001f
|
||||||
|
|| Mathf.Abs(localPosition.y) > localClamp.y + 0.0001f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeGrid != null && preset != null)
|
||||||
|
{
|
||||||
|
for (int x = -1; x <= 1; x += 2)
|
||||||
|
{
|
||||||
|
for (int y = -1; y <= 1; y += 2)
|
||||||
|
{
|
||||||
|
Vector2 sample = bodyCenter + new Vector2(
|
||||||
|
bodyExtents.x * 0.8f * x,
|
||||||
|
bodyExtents.y * 0.8f * y);
|
||||||
|
Vector2Int cell = RoomPathfinder.WorldToCell(preset, ZoneCenter, sample);
|
||||||
|
if (!RoomPathfinder.IsWalkable(preset, cell.x, cell.y)
|
||||||
|
|| runtimeGrid[cell.x, cell.y] == PresetCellType.Empty
|
||||||
|
|| runtimeGrid[cell.x, cell.y] == PresetCellType.Obstacle)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collider2D[] overlaps = Physics2D.OverlapBoxAll(bodyCenter, bodyExtents * 2f, 0f);
|
||||||
|
for (int i = 0; i < overlaps.Length; i++)
|
||||||
|
{
|
||||||
|
Collider2D collider = overlaps[i];
|
||||||
|
if (collider == null
|
||||||
|
|| collider.isTrigger
|
||||||
|
|| collider == playerBody
|
||||||
|
|| collider.transform.IsChildOf(playerBody.transform))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collider.GetComponentInParent<StageObstacle>() != null
|
||||||
|
|| collider.GetComponentInParent<ZoneConnector>() != null
|
||||||
|
|| collider.name.StartsWith("Wall_")
|
||||||
|
|| collider.gameObject.name.Contains("Wall"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCleared()
|
||||||
|
{
|
||||||
|
isCleared = true;
|
||||||
|
if (nodeData != null)
|
||||||
|
{
|
||||||
|
nodeData.Cleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<ZoneConnector> unlocked = new();
|
||||||
|
foreach (var kvp in connectorsById)
|
||||||
|
{
|
||||||
|
if (kvp.Value != null && unlocked.Add(kvp.Value))
|
||||||
|
{
|
||||||
|
kvp.Value.SetLocked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in connectorsByDir)
|
||||||
|
{
|
||||||
|
if (kvp.Value != null && unlocked.Add(kvp.Value))
|
||||||
|
{
|
||||||
|
kvp.Value.SetLocked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ActivateZone()
|
||||||
|
{
|
||||||
|
gameObject.SetActive(true);
|
||||||
|
if (nodeData != null)
|
||||||
|
{
|
||||||
|
nodeData.Visited = true;
|
||||||
|
nodeData.IsCurrent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArenaBounds bounds = ArenaBounds.Resolve();
|
||||||
|
if (bounds != null)
|
||||||
|
{
|
||||||
|
bounds.SetZoneBounds(ZoneCenter, playableHalfExtents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactivate alive enemies belonging to this zone
|
||||||
|
for (int i = 0; i < spawnedEnemies.Count; i++)
|
||||||
|
{
|
||||||
|
if (spawnedEnemies[i] != null && !spawnedEnemies[i].IsDead)
|
||||||
|
{
|
||||||
|
spawnedEnemies[i].gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSpawnedEncounter && !isCleared)
|
||||||
|
{
|
||||||
|
SpawnZoneEncounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
visibilityPlayer = FindAnyObjectByType<PlayerController>();
|
||||||
|
if (visibilityPlayer != null)
|
||||||
|
{
|
||||||
|
lastVisibilityOrigin = new Vector2Int(int.MinValue, int.MinValue);
|
||||||
|
UpdateVisibility(visibilityPlayer.transform.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!isActiveAndEnabled
|
||||||
|
|| StageManager.Instance == null
|
||||||
|
|| StageManager.Instance.CurrentZone != this)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibilityPlayer ??= FindAnyObjectByType<PlayerController>();
|
||||||
|
if (visibilityPlayer != null)
|
||||||
|
{
|
||||||
|
UpdateVisibility(visibilityPlayer.transform.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeactivateZone()
|
||||||
|
{
|
||||||
|
if (nodeData != null)
|
||||||
|
{
|
||||||
|
nodeData.IsCurrent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup dead references and deactivate alive enemies
|
||||||
|
for (int i = spawnedEnemies.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (spawnedEnemies[i] == null || spawnedEnemies[i].IsDead)
|
||||||
|
{
|
||||||
|
spawnedEnemies.RemoveAt(i);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Keep alive enemies intact but deactivate so they don't participate in other zones
|
||||||
|
spawnedEnemies[i].gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnZoneEncounter()
|
||||||
|
{
|
||||||
|
hasSpawnedEncounter = true;
|
||||||
|
|
||||||
|
// Handled via StageManager and SpawnDirector
|
||||||
|
StageManager stageManager = StageManager.Instance;
|
||||||
|
if (stageManager != null)
|
||||||
|
{
|
||||||
|
stageManager.SpawnEncounterForZone(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSpawnedEnemy(EnemyController enemy)
|
||||||
|
{
|
||||||
|
if (enemy == null) return;
|
||||||
|
enemy.transform.SetParent(transform, true);
|
||||||
|
spawnedEnemies.Add(enemy);
|
||||||
|
enemy.OnDied += HandleEnemyDied;
|
||||||
|
|
||||||
|
if (enemy.IsFlying && enemy.BodyCollider != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < spawnedObstacles.Count; i++)
|
||||||
|
{
|
||||||
|
GameObject obstacle = spawnedObstacles[i];
|
||||||
|
if (obstacle != null)
|
||||||
|
{
|
||||||
|
Collider2D obsCollider = obstacle.GetComponent<Collider2D>();
|
||||||
|
if (obsCollider != null)
|
||||||
|
{
|
||||||
|
Physics2D.IgnoreCollision(enemy.BodyCollider, obsCollider, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StageManager.Instance != null
|
||||||
|
&& StageManager.Instance.CurrentZone == this)
|
||||||
|
{
|
||||||
|
RefreshEnemyVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleEnemyDied(EnemyController enemy)
|
||||||
|
{
|
||||||
|
spawnedEnemies.Remove(enemy);
|
||||||
|
// Record clear on all enemies dead (both for locked bosses and normal breakthrough rooms)
|
||||||
|
if (spawnedEnemies.Count == 0)
|
||||||
|
{
|
||||||
|
SetCleared();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StageFogOfWar : MonoBehaviour
|
||||||
|
{
|
||||||
|
private ZoneInstance zone;
|
||||||
|
private Mesh mesh;
|
||||||
|
private MeshRenderer meshRenderer;
|
||||||
|
private Material fogMaterial;
|
||||||
|
private Color[] vertexColors;
|
||||||
|
|
||||||
|
public void Initialize(ZoneInstance owner)
|
||||||
|
{
|
||||||
|
zone = owner;
|
||||||
|
BuildMesh();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetCellFogAlpha(int x, int y)
|
||||||
|
{
|
||||||
|
if (zone == null || zone.Preset == null)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.GetRuntimeCell(x, y) == PresetCellType.Empty)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zone.IsCellVisible(x, y))
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return zone.IsCellExplored(x, y)
|
||||||
|
? Mathf.Clamp01(GameplayConstants.Current.Run.StageExploredFogOpacity)
|
||||||
|
: Mathf.Clamp01(GameplayConstants.Current.Run.StageUnseenFogOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
if (zone == null || zone.Preset == null || mesh == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomPreset preset = zone.Preset;
|
||||||
|
for (int y = 0; y < preset.GridHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < preset.GridWidth; x++)
|
||||||
|
{
|
||||||
|
float alpha = GetCellFogAlpha(x, y);
|
||||||
|
int vertex = (y * preset.GridWidth + x) * 4;
|
||||||
|
Color fogColor = new(0f, 0f, 0f, alpha);
|
||||||
|
vertexColors[vertex] = fogColor;
|
||||||
|
vertexColors[vertex + 1] = fogColor;
|
||||||
|
vertexColors[vertex + 2] = fogColor;
|
||||||
|
vertexColors[vertex + 3] = fogColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.colors = vertexColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildMesh()
|
||||||
|
{
|
||||||
|
if (zone == null || zone.Preset == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomPreset preset = zone.Preset;
|
||||||
|
int cellCount = preset.GridWidth * preset.GridHeight;
|
||||||
|
Vector3[] vertices = new Vector3[cellCount * 4];
|
||||||
|
Vector2[] uv = new Vector2[cellCount * 4];
|
||||||
|
int[] triangles = new int[cellCount * 6];
|
||||||
|
vertexColors = new Color[cellCount * 4];
|
||||||
|
|
||||||
|
for (int y = 0; y < preset.GridHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < preset.GridWidth; x++)
|
||||||
|
{
|
||||||
|
Vector2 center = preset.CellToLocalPosition(x, y);
|
||||||
|
int vertex = (y * preset.GridWidth + x) * 4;
|
||||||
|
vertices[vertex] = new Vector3(center.x - 0.5f, center.y - 0.5f, 0f);
|
||||||
|
vertices[vertex + 1] = new Vector3(center.x - 0.5f, center.y + 0.5f, 0f);
|
||||||
|
vertices[vertex + 2] = new Vector3(center.x + 0.5f, center.y + 0.5f, 0f);
|
||||||
|
vertices[vertex + 3] = new Vector3(center.x + 0.5f, center.y - 0.5f, 0f);
|
||||||
|
uv[vertex] = Vector2.zero;
|
||||||
|
uv[vertex + 1] = Vector2.up;
|
||||||
|
uv[vertex + 2] = Vector2.one;
|
||||||
|
uv[vertex + 3] = Vector2.right;
|
||||||
|
|
||||||
|
int triangle = (y * preset.GridWidth + x) * 6;
|
||||||
|
triangles[triangle] = vertex;
|
||||||
|
triangles[triangle + 1] = vertex + 1;
|
||||||
|
triangles[triangle + 2] = vertex + 2;
|
||||||
|
triangles[triangle + 3] = vertex;
|
||||||
|
triangles[triangle + 4] = vertex + 2;
|
||||||
|
triangles[triangle + 5] = vertex + 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh = new Mesh
|
||||||
|
{
|
||||||
|
name = $"{zone.name} Fog Mesh",
|
||||||
|
hideFlags = HideFlags.HideAndDontSave,
|
||||||
|
};
|
||||||
|
mesh.vertices = vertices;
|
||||||
|
mesh.uv = uv;
|
||||||
|
mesh.triangles = triangles;
|
||||||
|
mesh.colors = vertexColors;
|
||||||
|
mesh.MarkDynamic();
|
||||||
|
|
||||||
|
MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
|
||||||
|
meshFilter.sharedMesh = mesh;
|
||||||
|
meshRenderer = gameObject.AddComponent<MeshRenderer>();
|
||||||
|
meshRenderer.sortingLayerName = "Default";
|
||||||
|
meshRenderer.sortingOrder = 32760;
|
||||||
|
|
||||||
|
Shader shader = Shader.Find("Sprites/Default");
|
||||||
|
if (shader != null)
|
||||||
|
{
|
||||||
|
fogMaterial = new Material(shader)
|
||||||
|
{
|
||||||
|
hideFlags = HideFlags.HideAndDontSave,
|
||||||
|
};
|
||||||
|
meshRenderer.sharedMaterial = fogMaterial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Application.isPlaying)
|
||||||
|
{
|
||||||
|
if (mesh != null) Destroy(mesh);
|
||||||
|
if (fogMaterial != null) Destroy(fogMaterial);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (mesh != null) DestroyImmediate(mesh);
|
||||||
|
if (fogMaterial != null) DestroyImmediate(fogMaterial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7cf2808cfbde3d84480fceef82b7f3a8
|
||||||
@@ -331,18 +331,19 @@ namespace BumpCombat.UI
|
|||||||
24,
|
24,
|
||||||
TextAnchor.MiddleLeft,
|
TextAnchor.MiddleLeft,
|
||||||
Vector2.zero,
|
Vector2.zero,
|
||||||
new Vector2(220f, 110f));
|
new Vector2(240f, 220f));
|
||||||
RectTransform hintRect = hintText.rectTransform;
|
RectTransform hintRect = hintText.rectTransform;
|
||||||
hintRect.anchorMin = new Vector2(0f, 1f);
|
hintRect.anchorMin = new Vector2(0f, 1f);
|
||||||
hintRect.anchorMax = new Vector2(0f, 1f);
|
hintRect.anchorMax = new Vector2(0f, 1f);
|
||||||
hintRect.pivot = new Vector2(0f, 1f);
|
hintRect.pivot = new Vector2(0f, 1f);
|
||||||
hintRect.anchoredPosition = new Vector2(24f, -250f);
|
hintRect.anchoredPosition = new Vector2(24f, -230f);
|
||||||
hintText.supportRichText = true;
|
hintText.supportRichText = true;
|
||||||
hintText.resizeTextForBestFit = false;
|
hintText.resizeTextForBestFit = false;
|
||||||
hintText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
hintText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||||
hintText.verticalOverflow = VerticalWrapMode.Overflow;
|
hintText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||||
hintText.text = "<size=36><color=#FFD675><b>TAB</b></color></size>\n"
|
hintText.text = "<size=32><color=#FFD675><b>TAB</b></color></size> <size=20>(미션/업적)</size>\n\n"
|
||||||
+ "<size=24>(미션 / 업적)</size>";
|
+ "<size=18><color=#79E5FF><b>조작 안내</b></color></size>\n"
|
||||||
|
+ "<size=16><color=#FFFFFF>방향키 [▲▼◀▶]: 이동\n[A]: 스킬 / [D]: 가드\n[S]: 교체 / [M]: 지도</color></size>";
|
||||||
hintText.color = Color.white;
|
hintText.color = Color.white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using BumpCombat.Audio;
|
||||||
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Progression;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace BumpCombat.UI
|
||||||
|
{
|
||||||
|
public sealed class RunClearPanel : MonoBehaviour
|
||||||
|
{
|
||||||
|
private GameObject panel;
|
||||||
|
private Text titleText;
|
||||||
|
private Text statsText;
|
||||||
|
private Text restartText;
|
||||||
|
private bool isShown;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
CreatePanel();
|
||||||
|
if (RunManager.Instance != null)
|
||||||
|
{
|
||||||
|
RunManager.Instance.OnRunCleared += HandleRunCleared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (RunManager.Instance != null)
|
||||||
|
{
|
||||||
|
RunManager.Instance.OnRunCleared -= HandleRunCleared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!isShown || Keyboard.current == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Keyboard.current.rKey.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
RunManager.Instance?.RestartRun();
|
||||||
|
}
|
||||||
|
else if (Keyboard.current.escapeKey.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
RunManager.Instance?.RestartToTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRunCleared()
|
||||||
|
{
|
||||||
|
isShown = true;
|
||||||
|
panel.SetActive(true);
|
||||||
|
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||||
|
|
||||||
|
float elapsed = RunManager.Instance != null ? RunManager.Instance.ElapsedTime : 0f;
|
||||||
|
int minutes = Mathf.FloorToInt(elapsed / 60f);
|
||||||
|
int seconds = Mathf.FloorToInt(elapsed % 60f);
|
||||||
|
|
||||||
|
ExperienceSystem exp = FindAnyObjectByType<ExperienceSystem>();
|
||||||
|
int level = exp != null ? exp.Level : 1;
|
||||||
|
|
||||||
|
statsText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nCONSTRUCTION REWARD COLLECTED\nDEFENSE TEST CLEARED" : $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nFINAL LEVEL: LV {level}\nSTAGE: 3 / 3 COMPLETED";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreatePanel()
|
||||||
|
{
|
||||||
|
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
|
||||||
|
if (canvas == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = new GameObject("RunClearPanel");
|
||||||
|
panel.transform.SetParent(canvas.transform, false);
|
||||||
|
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||||
|
panelRect.anchorMin = Vector2.zero;
|
||||||
|
panelRect.anchorMax = Vector2.one;
|
||||||
|
panelRect.offsetMin = Vector2.zero;
|
||||||
|
panelRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
// Background dark dim with gold tint
|
||||||
|
Image bg = panel.AddComponent<Image>();
|
||||||
|
bg.color = new Color(0.08f, 0.08f, 0.04f, 0.94f);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
GameObject titleObj = new GameObject("ClearTitle");
|
||||||
|
titleObj.transform.SetParent(panel.transform, false);
|
||||||
|
titleText = titleObj.AddComponent<Text>();
|
||||||
|
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
titleText.fontSize = 42;
|
||||||
|
titleText.fontStyle = FontStyle.Bold;
|
||||||
|
titleText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
titleText.color = new Color(1f, 0.85f, 0.25f);
|
||||||
|
titleText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? "★ DEFENSE TEST CLEARED ★" : "★ RUN CLEAR ★";
|
||||||
|
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
|
||||||
|
titleRect.anchorMin = new Vector2(0f, 0.6f);
|
||||||
|
titleRect.anchorMax = new Vector2(1f, 0.8f);
|
||||||
|
titleRect.offsetMin = Vector2.zero;
|
||||||
|
titleRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
GameObject statsObj = new GameObject("ClearStats");
|
||||||
|
statsObj.transform.SetParent(panel.transform, false);
|
||||||
|
statsText = statsObj.AddComponent<Text>();
|
||||||
|
statsText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
statsText.fontSize = 20;
|
||||||
|
statsText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
statsText.color = new Color(0.9f, 0.9f, 0.95f);
|
||||||
|
statsText.lineSpacing = 1.4f;
|
||||||
|
RectTransform statsRect = statsObj.GetComponent<RectTransform>();
|
||||||
|
statsRect.anchorMin = new Vector2(0.1f, 0.35f);
|
||||||
|
statsRect.anchorMax = new Vector2(0.9f, 0.55f);
|
||||||
|
statsRect.offsetMin = Vector2.zero;
|
||||||
|
statsRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
// Restart Hint
|
||||||
|
GameObject restartObj = new GameObject("ClearRestartHint");
|
||||||
|
restartObj.transform.SetParent(panel.transform, false);
|
||||||
|
restartText = restartObj.AddComponent<Text>();
|
||||||
|
restartText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
restartText.fontSize = 16;
|
||||||
|
restartText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
restartText.color = new Color(0.7f, 0.7f, 0.75f);
|
||||||
|
restartText.text = "[R] 키로 재시작 | [ESC] 키로 타이틀로 이동";
|
||||||
|
RectTransform restartRect = restartObj.GetComponent<RectTransform>();
|
||||||
|
restartRect.anchorMin = new Vector2(0f, 0.15f);
|
||||||
|
restartRect.anchorMax = new Vector2(1f, 0.25f);
|
||||||
|
restartRect.offsetMin = Vector2.zero;
|
||||||
|
restartRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
panel.SetActive(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5c63f44ba2673624d9e2fcdb546c8268
|
||||||
@@ -53,13 +53,17 @@ namespace BumpCombat.UI
|
|||||||
private Button[] debugSpawnButtons = System.Array.Empty<Button>();
|
private Button[] debugSpawnButtons = System.Array.Empty<Button>();
|
||||||
private GameObject minimapPanel;
|
private GameObject minimapPanel;
|
||||||
private RectTransform minimapMapRect;
|
private RectTransform minimapMapRect;
|
||||||
|
private RectTransform minimapTerrainRoot;
|
||||||
private RectTransform minimapMarkerRoot;
|
private RectTransform minimapMarkerRoot;
|
||||||
private RectTransform minimapViewportRoot;
|
private RectTransform minimapViewportRoot;
|
||||||
private Image minimapPlayerMarker;
|
private Image minimapPlayerMarker;
|
||||||
private Image[] minimapViewportEdges = System.Array.Empty<Image>();
|
private Image[] minimapViewportEdges = System.Array.Empty<Image>();
|
||||||
private readonly List<Image> minimapEnemyMarkers = new();
|
private readonly List<Image> minimapEnemyMarkers = new();
|
||||||
|
private readonly Dictionary<Vector2Int, Image> minimapTerrainCells = new();
|
||||||
private EnemyController[] minimapEnemies = System.Array.Empty<EnemyController>();
|
private EnemyController[] minimapEnemies = System.Array.Empty<EnemyController>();
|
||||||
private float minimapEnemyRefreshElapsed;
|
private float minimapEnemyRefreshElapsed;
|
||||||
|
private Stage.ZoneInstance minimapTerrainZone;
|
||||||
|
private int minimapTerrainVisibilityRevision = -1;
|
||||||
private ArenaBounds arenaBounds;
|
private ArenaBounds arenaBounds;
|
||||||
// The panel sprite has transparent source padding. At multiplier 1,
|
// The panel sprite has transparent source padding. At multiplier 1,
|
||||||
// 6px of canvas space keeps the map inside the visible frame.
|
// 6px of canvas space keeps the map inside the visible frame.
|
||||||
@@ -68,22 +72,9 @@ namespace BumpCombat.UI
|
|||||||
private Image[] artifactSlotIcons = System.Array.Empty<Image>();
|
private Image[] artifactSlotIcons = System.Array.Empty<Image>();
|
||||||
private readonly Stack<Text> damagePopupPool = new();
|
private readonly Stack<Text> damagePopupPool = new();
|
||||||
private readonly HashSet<Text> activeDamagePopups = new();
|
private readonly HashSet<Text> activeDamagePopups = new();
|
||||||
private static readonly RunTimedEvent[] NextBossEvents =
|
|
||||||
{
|
|
||||||
RunTimedEvent.MidBoss,
|
|
||||||
RunTimedEvent.FinalBoss,
|
|
||||||
};
|
|
||||||
private static readonly string[] NextBossLabels =
|
|
||||||
{
|
|
||||||
"다음 중간보스",
|
|
||||||
"다음 보스",
|
|
||||||
};
|
|
||||||
private int activeDamagePopupCount;
|
private int activeDamagePopupCount;
|
||||||
private int damagePopupLifecycleVersion;
|
private int damagePopupLifecycleVersion;
|
||||||
private float compactHudFadeElapsed;
|
private float compactHudFadeElapsed;
|
||||||
private int lastNextEventSecond = -1;
|
|
||||||
private bool lastNextEventUsesDebugTimes;
|
|
||||||
private bool lastNextEventIsProductionRun;
|
|
||||||
private static readonly Color PlayerDamagePopupColor =
|
private static readonly Color PlayerDamagePopupColor =
|
||||||
new(244f / 255f, 117f / 255f, 112f / 255f, 1f);
|
new(244f / 255f, 117f / 255f, 112f / 255f, 1f);
|
||||||
private static readonly Color GuardReadyColor = new Color32(255, 205, 92, 255);
|
private static readonly Color GuardReadyColor = new Color32(255, 205, 92, 255);
|
||||||
@@ -741,61 +732,25 @@ namespace BumpCombat.UI
|
|||||||
new Vector2(0f, -100f),
|
new Vector2(0f, -100f),
|
||||||
new Vector2(420f, 28f));
|
new Vector2(420f, 28f));
|
||||||
ConfigureCompactHudText(eventText, 14, 18);
|
ConfigureCompactHudText(eventText, 14, 18);
|
||||||
eventText.gameObject.SetActive(true);
|
eventText.gameObject.SetActive(false);
|
||||||
UpdateNextEventHud();
|
UpdateNextEventHud();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateNextEventHud()
|
private void UpdateNextEventHud()
|
||||||
{
|
{
|
||||||
if (eventText == null || runManager == null || showingTimedEventMessage)
|
if (eventText == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int elapsedSecond = Mathf.FloorToInt(runManager.ElapsedTime);
|
if (!showingTimedEventMessage)
|
||||||
if (elapsedSecond == lastNextEventSecond
|
|
||||||
&& runManager.UseDebugEventTimes == lastNextEventUsesDebugTimes
|
|
||||||
&& runManager.IsProductionRun == lastNextEventIsProductionRun)
|
|
||||||
{
|
{
|
||||||
|
if (eventText.gameObject.activeSelf)
|
||||||
|
{
|
||||||
|
eventText.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastNextEventSecond = elapsedSecond;
|
|
||||||
lastNextEventUsesDebugTimes = runManager.UseDebugEventTimes;
|
|
||||||
lastNextEventIsProductionRun = runManager.IsProductionRun;
|
|
||||||
float nextTime = float.PositiveInfinity;
|
|
||||||
string nextLabel = "다음 이벤트 없음";
|
|
||||||
for (int i = 0; i < runManager.CurrentEliteEventCount; i++)
|
|
||||||
{
|
|
||||||
float eliteTime = runManager.GetEliteEventTimeForCurrentRun(i);
|
|
||||||
if (eliteTime > runManager.ElapsedTime)
|
|
||||||
{
|
|
||||||
nextTime = eliteTime;
|
|
||||||
nextLabel = "다음 엘리트";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < NextBossEvents.Length; i++)
|
|
||||||
{
|
|
||||||
float eventTime = runManager.GetEventTimeForCurrentRun(
|
|
||||||
NextBossEvents[i]);
|
|
||||||
if (eventTime > runManager.ElapsedTime && eventTime < nextTime)
|
|
||||||
{
|
|
||||||
nextTime = eventTime;
|
|
||||||
nextLabel = NextBossLabels[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (float.IsPositiveInfinity(nextTime))
|
|
||||||
{
|
|
||||||
eventText.text = nextLabel;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int seconds = Mathf.FloorToInt(nextTime);
|
|
||||||
eventText.text =
|
|
||||||
$"{nextLabel} {seconds / 60:00}:{seconds % 60:00}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LayoutDebugText()
|
private void LayoutDebugText()
|
||||||
@@ -844,6 +799,20 @@ namespace BumpCombat.UI
|
|||||||
{
|
{
|
||||||
popupText = $"LAUNCH {popupText}";
|
popupText = $"LAUNCH {popupText}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnemyController popupEnemy = result.Target != null
|
||||||
|
? result.Target.GetComponentInParent<EnemyController>()
|
||||||
|
: null;
|
||||||
|
Stage.ZoneInstance popupZone = Stage.StageManager.Instance != null
|
||||||
|
? Stage.StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
|
if (popupEnemy != null
|
||||||
|
&& popupZone != null
|
||||||
|
&& !popupZone.IsWorldPositionVisible(popupEnemy.GroundAnchorPosition))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Color popupColor;
|
Color popupColor;
|
||||||
if (!TryGetArtifactPopupColor(result, out popupColor))
|
if (!TryGetArtifactPopupColor(result, out popupColor))
|
||||||
{
|
{
|
||||||
@@ -1271,7 +1240,7 @@ namespace BumpCombat.UI
|
|||||||
RunTimedEvent.MidBoss,
|
RunTimedEvent.MidBoss,
|
||||||
RunTimedEvent.FinalBoss,
|
RunTimedEvent.FinalBoss,
|
||||||
};
|
};
|
||||||
string[] labels = { "엘리트 출현", "중간보스 출현", "보스 출현" };
|
string[] labels = { "엘리트 출현", "중간보스 출현", "보스방 이동" };
|
||||||
debugSpawnButtons = new Button[events.Length];
|
debugSpawnButtons = new Button[events.Length];
|
||||||
for (int i = 0; i < events.Length; i++)
|
for (int i = 0; i < events.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -1296,6 +1265,19 @@ namespace BumpCombat.UI
|
|||||||
Button button = buttonObject.GetComponent<Button>();
|
Button button = buttonObject.GetComponent<Button>();
|
||||||
button.targetGraphic = image;
|
button.targetGraphic = image;
|
||||||
PresentationUiStyle.ApplyButton(button);
|
PresentationUiStyle.ApplyButton(button);
|
||||||
|
if (i == 2)
|
||||||
|
{
|
||||||
|
button.onClick.AddListener(() =>
|
||||||
|
{
|
||||||
|
if (Stage.StageManager.Instance != null)
|
||||||
|
{
|
||||||
|
Stage.StageManager.Instance.CheatTeleportToBossRoom();
|
||||||
|
}
|
||||||
|
UpdateDebugSpawnHud();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
RunTimedEvent requestedEvent = events[i];
|
RunTimedEvent requestedEvent = events[i];
|
||||||
button.onClick.AddListener(() =>
|
button.onClick.AddListener(() =>
|
||||||
{
|
{
|
||||||
@@ -1306,6 +1288,7 @@ namespace BumpCombat.UI
|
|||||||
}
|
}
|
||||||
UpdateDebugSpawnHud();
|
UpdateDebugSpawnHud();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
CreateRuntimeText(
|
CreateRuntimeText(
|
||||||
buttonObject.transform,
|
buttonObject.transform,
|
||||||
"Label",
|
"Label",
|
||||||
@@ -1327,7 +1310,7 @@ namespace BumpCombat.UI
|
|||||||
|
|
||||||
bool visible = runManager != null && runManager.UseDebugArtifactSelection;
|
bool visible = runManager != null && runManager.UseDebugArtifactSelection;
|
||||||
debugSpawnPanel.SetActive(visible);
|
debugSpawnPanel.SetActive(visible);
|
||||||
if (!visible || spawnDirector == null)
|
if (!visible)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1338,10 +1321,19 @@ namespace BumpCombat.UI
|
|||||||
RunTimedEvent.MidBoss,
|
RunTimedEvent.MidBoss,
|
||||||
RunTimedEvent.FinalBoss,
|
RunTimedEvent.FinalBoss,
|
||||||
};
|
};
|
||||||
|
bool inputEnabled = RunManager.GameplayInputEnabled;
|
||||||
for (int i = 0; i < debugSpawnButtons.Length; i++)
|
for (int i = 0; i < debugSpawnButtons.Length; i++)
|
||||||
|
{
|
||||||
|
if (i == 2)
|
||||||
{
|
{
|
||||||
debugSpawnButtons[i].interactable =
|
debugSpawnButtons[i].interactable =
|
||||||
spawnDirector.CanDebugSpawnEventEnemy(events[i]);
|
inputEnabled && Stage.StageManager.Instance != null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
debugSpawnButtons[i].interactable =
|
||||||
|
spawnDirector != null && spawnDirector.CanDebugSpawnEventEnemy(events[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,7 +1362,7 @@ namespace BumpCombat.UI
|
|||||||
new Vector2(panelWidth, panelHeight));
|
new Vector2(panelWidth, panelHeight));
|
||||||
Image minimapFrame = minimapPanel.GetComponent<Image>();
|
Image minimapFrame = minimapPanel.GetComponent<Image>();
|
||||||
PresentationUiStyle.ApplyPanel(minimapFrame);
|
PresentationUiStyle.ApplyPanel(minimapFrame);
|
||||||
minimapFrame.pixelsPerUnitMultiplier = 1f;
|
minimapFrame.pixelsPerUnitMultiplier = PresentationUiStyle.ThreePixelBorderMultiplier;
|
||||||
minimapFrame.fillCenter = false;
|
minimapFrame.fillCenter = false;
|
||||||
|
|
||||||
GameObject map = CreateRuntimePanel(
|
GameObject map = CreateRuntimePanel(
|
||||||
@@ -1384,6 +1376,7 @@ namespace BumpCombat.UI
|
|||||||
map.GetComponent<Image>().color = new Color(0.035f, 0.06f, 0.08f, 0.28f);
|
map.GetComponent<Image>().color = new Color(0.035f, 0.06f, 0.08f, 0.28f);
|
||||||
map.AddComponent<RectMask2D>();
|
map.AddComponent<RectMask2D>();
|
||||||
|
|
||||||
|
minimapTerrainRoot = CreateMinimapLayer(map.transform, "Minimap Terrain");
|
||||||
minimapMarkerRoot = CreateMinimapLayer(map.transform, "Markers");
|
minimapMarkerRoot = CreateMinimapLayer(map.transform, "Markers");
|
||||||
minimapViewportRoot = CreateMinimapLayer(map.transform, "Minimap Camera Viewport");
|
minimapViewportRoot = CreateMinimapLayer(map.transform, "Minimap Camera Viewport");
|
||||||
minimapMarkerRoot.SetAsLastSibling();
|
minimapMarkerRoot.SetAsLastSibling();
|
||||||
@@ -1453,6 +1446,8 @@ namespace BumpCombat.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateMinimapTerrain();
|
||||||
|
|
||||||
SetMinimapMarkerPosition(
|
SetMinimapMarkerPosition(
|
||||||
minimapPlayerMarker,
|
minimapPlayerMarker,
|
||||||
playerHealth != null ? playerHealth.transform.position : Vector3.zero,
|
playerHealth != null ? playerHealth.transform.position : Vector3.zero,
|
||||||
@@ -1469,6 +1464,9 @@ namespace BumpCombat.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
int markerIndex = 0;
|
int markerIndex = 0;
|
||||||
|
Stage.ZoneInstance visibilityZone = Stage.StageManager.Instance != null
|
||||||
|
? Stage.StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
for (int i = 0; i < minimapEnemies.Length; i++)
|
for (int i = 0; i < minimapEnemies.Length; i++)
|
||||||
{
|
{
|
||||||
EnemyController enemy = minimapEnemies[i];
|
EnemyController enemy = minimapEnemies[i];
|
||||||
@@ -1476,6 +1474,16 @@ namespace BumpCombat.UI
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (worldCamera == null
|
||||||
|
|| !arenaBounds.IsInsideCameraVisible(worldCamera, enemy.GroundAnchorPosition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (visibilityZone != null
|
||||||
|
&& !visibilityZone.IsWorldPositionVisible(enemy.GroundAnchorPosition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Image marker = GetMinimapEnemyMarker(markerIndex++);
|
Image marker = GetMinimapEnemyMarker(markerIndex++);
|
||||||
marker.gameObject.SetActive(true);
|
marker.gameObject.SetActive(true);
|
||||||
@@ -1499,6 +1507,132 @@ namespace BumpCombat.UI
|
|||||||
UpdateMinimapViewport(extents);
|
UpdateMinimapViewport(extents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateMinimapTerrain()
|
||||||
|
{
|
||||||
|
Stage.ZoneInstance currentZone = Stage.StageManager.Instance != null
|
||||||
|
? Stage.StageManager.Instance.CurrentZone
|
||||||
|
: null;
|
||||||
|
if (currentZone == minimapTerrainZone)
|
||||||
|
{
|
||||||
|
if (currentZone != null
|
||||||
|
&& currentZone.VisibilityRevision != minimapTerrainVisibilityRevision)
|
||||||
|
{
|
||||||
|
UpdateMinimapTerrainVisibility(currentZone);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
minimapTerrainZone = currentZone;
|
||||||
|
minimapTerrainVisibilityRevision = -1;
|
||||||
|
minimapTerrainCells.Clear();
|
||||||
|
if (minimapTerrainRoot == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = minimapTerrainRoot.childCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Destroy(minimapTerrainRoot.GetChild(i).gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stage.RoomPreset preset = currentZone != null ? currentZone.Preset : null;
|
||||||
|
if (preset == null || !currentZone.HasRuntimeGrid ||
|
||||||
|
preset.GridWidth <= 0 || preset.GridHeight <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 mapSize = minimapMapRect.rect.size;
|
||||||
|
float cellWidth = mapSize.x / preset.GridWidth;
|
||||||
|
float cellHeight = mapSize.y / preset.GridHeight;
|
||||||
|
float left = -mapSize.x * 0.5f;
|
||||||
|
float top = mapSize.y * 0.5f;
|
||||||
|
|
||||||
|
for (int y = 0; y < preset.GridHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < preset.GridWidth; x++)
|
||||||
|
{
|
||||||
|
Stage.PresetCellType cellType = currentZone.GetRuntimeCell(x, y);
|
||||||
|
if (cellType == Stage.PresetCellType.Empty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image cell = CreateRuntimeImage(
|
||||||
|
minimapTerrainRoot,
|
||||||
|
$"Terrain {x},{y}",
|
||||||
|
Vector2.zero,
|
||||||
|
new Vector2(cellWidth + 0.2f, cellHeight + 0.2f));
|
||||||
|
cell.raycastTarget = false;
|
||||||
|
minimapTerrainCells[new Vector2Int(x, y)] = cell;
|
||||||
|
SetRectTransform(
|
||||||
|
cell.rectTransform,
|
||||||
|
new Vector2(0.5f, 0.5f),
|
||||||
|
new Vector2(0.5f, 0.5f),
|
||||||
|
new Vector2(
|
||||||
|
left + (x + 0.5f) * cellWidth,
|
||||||
|
top - (y + 0.5f) * cellHeight),
|
||||||
|
cell.rectTransform.sizeDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMinimapTerrainVisibility(currentZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMinimapTerrainVisibility(Stage.ZoneInstance zone)
|
||||||
|
{
|
||||||
|
if (zone == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<Vector2Int, Image> entry in minimapTerrainCells)
|
||||||
|
{
|
||||||
|
Vector2Int cell = entry.Key;
|
||||||
|
if (entry.Value == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Value.color = GetMinimapTerrainColor(
|
||||||
|
zone.GetRuntimeCell(cell.x, cell.y),
|
||||||
|
zone.IsCellVisible(cell.x, cell.y),
|
||||||
|
zone.IsCellExplored(cell.x, cell.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
minimapTerrainVisibilityRevision = zone.VisibilityRevision;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color GetMinimapTerrainColor(
|
||||||
|
Stage.PresetCellType cellType,
|
||||||
|
bool visible,
|
||||||
|
bool explored)
|
||||||
|
{
|
||||||
|
Color baseColor = cellType switch
|
||||||
|
{
|
||||||
|
Stage.PresetCellType.Obstacle => new Color32(12, 18, 22, 240),
|
||||||
|
Stage.PresetCellType.Socket => new Color32(54, 103, 112, 230),
|
||||||
|
Stage.PresetCellType.RewardAnchor => new Color32(73, 88, 70, 205),
|
||||||
|
_ => new Color32(41, 57, 62, 190),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visible)
|
||||||
|
{
|
||||||
|
return baseColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explored)
|
||||||
|
{
|
||||||
|
return new Color(
|
||||||
|
baseColor.r * 0.35f,
|
||||||
|
baseColor.g * 0.35f,
|
||||||
|
baseColor.b * 0.35f,
|
||||||
|
1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Color32(2, 4, 5, 255);
|
||||||
|
}
|
||||||
|
|
||||||
private Image GetMinimapEnemyMarker(int index)
|
private Image GetMinimapEnemyMarker(int index)
|
||||||
{
|
{
|
||||||
while (minimapEnemyMarkers.Count <= index)
|
while (minimapEnemyMarkers.Count <= index)
|
||||||
@@ -1544,9 +1678,11 @@ namespace BumpCombat.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector2 center = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
|
||||||
|
Vector2 localPos = worldPosition - center;
|
||||||
Vector2 normalized = new(
|
Vector2 normalized = new(
|
||||||
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
|
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
|
||||||
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
|
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
|
||||||
Vector2 halfMarker = marker.rectTransform.rect.size * 0.5f;
|
Vector2 halfMarker = marker.rectTransform.rect.size * 0.5f;
|
||||||
float halfMapWidth = mapSize.x * 0.5f;
|
float halfMapWidth = mapSize.x * 0.5f;
|
||||||
float halfMapHeight = mapSize.y * 0.5f;
|
float halfMapHeight = mapSize.y * 0.5f;
|
||||||
@@ -1575,8 +1711,9 @@ namespace BumpCombat.UI
|
|||||||
|
|
||||||
Vector2 mapSize = minimapMapRect.rect.size;
|
Vector2 mapSize = minimapMapRect.rect.size;
|
||||||
Bounds visible = arenaBounds.GetCameraVisibleBounds(worldCamera);
|
Bounds visible = arenaBounds.GetCameraVisibleBounds(worldCamera);
|
||||||
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize);
|
Vector2 zoneCenter = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
|
||||||
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize);
|
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize, zoneCenter);
|
||||||
|
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize, zoneCenter);
|
||||||
float width = Mathf.Max(2f, max.x - min.x);
|
float width = Mathf.Max(2f, max.x - min.x);
|
||||||
float height = Mathf.Max(2f, max.y - min.y);
|
float height = Mathf.Max(2f, max.y - min.y);
|
||||||
Vector2 center = (min + max) * 0.5f;
|
Vector2 center = (min + max) * 0.5f;
|
||||||
@@ -1617,11 +1754,13 @@ namespace BumpCombat.UI
|
|||||||
private static Vector2 WorldToMinimapPosition(
|
private static Vector2 WorldToMinimapPosition(
|
||||||
Vector2 worldPosition,
|
Vector2 worldPosition,
|
||||||
Vector2 extents,
|
Vector2 extents,
|
||||||
Vector2 mapSize)
|
Vector2 mapSize,
|
||||||
|
Vector2 center = default)
|
||||||
{
|
{
|
||||||
|
Vector2 localPos = worldPosition - center;
|
||||||
Vector2 normalized = new(
|
Vector2 normalized = new(
|
||||||
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
|
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
|
||||||
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
|
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
|
||||||
return new Vector2(
|
return new Vector2(
|
||||||
(normalized.x - 0.5f) * mapSize.x,
|
(normalized.x - 0.5f) * mapSize.x,
|
||||||
(normalized.y - 0.5f) * mapSize.y);
|
(normalized.y - 0.5f) * mapSize.y);
|
||||||
@@ -1705,12 +1844,10 @@ namespace BumpCombat.UI
|
|||||||
private IEnumerator ShowEventMessage(string message)
|
private IEnumerator ShowEventMessage(string message)
|
||||||
{
|
{
|
||||||
showingTimedEventMessage = true;
|
showingTimedEventMessage = true;
|
||||||
lastNextEventSecond = -1;
|
|
||||||
eventText.text = message;
|
eventText.text = message;
|
||||||
eventText.gameObject.SetActive(true);
|
eventText.gameObject.SetActive(true);
|
||||||
yield return new WaitForSecondsRealtime(3f);
|
yield return new WaitForSecondsRealtime(3f);
|
||||||
showingTimedEventMessage = false;
|
showingTimedEventMessage = false;
|
||||||
lastNextEventSecond = -1;
|
|
||||||
UpdateNextEventHud();
|
UpdateNextEventHud();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1760,7 +1897,7 @@ namespace BumpCombat.UI
|
|||||||
"Presentation/Fonts/NanumGothic-Regular";
|
"Presentation/Fonts/NanumGothic-Regular";
|
||||||
private const string NeoFontResource = "Presentation/Fonts/neodgm";
|
private const string NeoFontResource = "Presentation/Fonts/neodgm";
|
||||||
private const string GalmuriFontResource = "Presentation/Fonts/Galmuri9";
|
private const string GalmuriFontResource = "Presentation/Fonts/Galmuri9";
|
||||||
private const float ThreePixelBorderMultiplier = 1f / 3f;
|
internal const float ThreePixelBorderMultiplier = 1f / 3f;
|
||||||
|
|
||||||
private static readonly Color PanelFallback =
|
private static readonly Color PanelFallback =
|
||||||
new(24f / 255f, 27f / 255f, 39f / 255f, 0.94f);
|
new(24f / 255f, 27f / 255f, 39f / 255f, 0.94f);
|
||||||
|
|||||||
@@ -456,6 +456,11 @@ namespace BumpCombat.UI
|
|||||||
BeginRun(RunMode.Production);
|
BeginRun(RunMode.Production);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BeginDefensePrototypeRun()
|
||||||
|
{
|
||||||
|
BeginRun(RunMode.DefensePrototype);
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenTitleModes()
|
private void OpenTitleModes()
|
||||||
{
|
{
|
||||||
SetScreen(Screen.TitleModes);
|
SetScreen(Screen.TitleModes);
|
||||||
@@ -469,6 +474,10 @@ namespace BumpCombat.UI
|
|||||||
ShowButton(index++, "개발용 시작", BeginDevelopmentRun);
|
ShowButton(index++, "개발용 시작", BeginDevelopmentRun);
|
||||||
}
|
}
|
||||||
ShowButton(index++, "기본 모드", BeginProductionRun);
|
ShowButton(index++, "기본 모드", BeginProductionRun);
|
||||||
|
if (IsDevelopmentStartVisible(Application.isEditor, Debug.isDebugBuild))
|
||||||
|
{
|
||||||
|
ShowButton(index++, "방어전 테스트", BeginDefensePrototypeRun);
|
||||||
|
}
|
||||||
ShowDisabledButton(index++, "챌린지 모드(준비 중)");
|
ShowDisabledButton(index++, "챌린지 모드(준비 중)");
|
||||||
ShowDisabledButton(index++, "하드코어 모드(준비 중)");
|
ShowDisabledButton(index++, "하드코어 모드(준비 중)");
|
||||||
ShowButton(index, "뒤로", ReturnToTitleMenu);
|
ShowButton(index, "뒤로", ReturnToTitleMenu);
|
||||||
@@ -650,7 +659,7 @@ namespace BumpCombat.UI
|
|||||||
inputHelpText.alignment = TextAnchor.MiddleLeft;
|
inputHelpText.alignment = TextAnchor.MiddleLeft;
|
||||||
inputHelpText.color = new Color32(200, 208, 218, 255);
|
inputHelpText.color = new Color32(200, 208, 218, 255);
|
||||||
CreateSettingsFocusRows(panel.transform);
|
CreateSettingsFocusRows(panel.transform);
|
||||||
for (int i = 0; i < 5; i++)
|
for (int i = 0; i < 6; i++)
|
||||||
{
|
{
|
||||||
Button button = CreateButton(panel.transform, $"Button {i + 1}", font, new Vector2(0f, 95f - i * 88f));
|
Button button = CreateButton(panel.transform, $"Button {i + 1}", font, new Vector2(0f, 95f - i * 88f));
|
||||||
button.gameObject.SetActive(false);
|
button.gameObject.SetActive(false);
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BumpCombat.Audio;
|
||||||
|
using BumpCombat.Core;
|
||||||
|
using BumpCombat.Stage;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace BumpCombat.UI
|
||||||
|
{
|
||||||
|
public sealed class WorldMapPanel : MonoBehaviour
|
||||||
|
{
|
||||||
|
private GameObject panel;
|
||||||
|
private Text titleText;
|
||||||
|
private Text seedText;
|
||||||
|
private RectTransform mapContainer;
|
||||||
|
private readonly List<GameObject> spawnedElements = new();
|
||||||
|
private bool isOpen;
|
||||||
|
private RunManager runManager;
|
||||||
|
|
||||||
|
public bool IsOpen => isOpen;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
runManager = RunManager.Instance;
|
||||||
|
CreatePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (Keyboard.current == null || runManager == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Keyboard.current.mKey.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
if (isOpen)
|
||||||
|
{
|
||||||
|
CloseMap();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OpenMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isOpen && Keyboard.current.escapeKey.wasPressedThisFrame)
|
||||||
|
{
|
||||||
|
CloseMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenMap()
|
||||||
|
{
|
||||||
|
if (isOpen || runManager == null || runManager.IsTitleScreen || runManager.IsGameOver || runManager.IsRunCleared)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runManager.TryOpenSelection(this))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen = true;
|
||||||
|
panel.SetActive(true);
|
||||||
|
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
|
||||||
|
RefreshMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseMap()
|
||||||
|
{
|
||||||
|
if (!isOpen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
panel.SetActive(false);
|
||||||
|
runManager?.SuppressPauseOpeningForCurrentFrame();
|
||||||
|
runManager?.CloseSelection(this);
|
||||||
|
BumpCombatAudioService.Instance?.PlayUi("ui_cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreatePanel()
|
||||||
|
{
|
||||||
|
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
|
||||||
|
if (canvas == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel = new GameObject("WorldMapPanel");
|
||||||
|
panel.transform.SetParent(canvas.transform, false);
|
||||||
|
RectTransform panelRect = panel.AddComponent<RectTransform>();
|
||||||
|
panelRect.anchorMin = Vector2.zero;
|
||||||
|
panelRect.anchorMax = Vector2.one;
|
||||||
|
panelRect.offsetMin = Vector2.zero;
|
||||||
|
panelRect.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
// Background dark dim
|
||||||
|
Image bg = panel.AddComponent<Image>();
|
||||||
|
bg.color = new Color(0.05f, 0.06f, 0.1f, 0.92f);
|
||||||
|
|
||||||
|
// Top Header: Title & Seed
|
||||||
|
GameObject titleObj = new GameObject("MapTitle");
|
||||||
|
titleObj.transform.SetParent(panel.transform, false);
|
||||||
|
titleText = titleObj.AddComponent<Text>();
|
||||||
|
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
titleText.fontSize = 28;
|
||||||
|
titleText.fontStyle = FontStyle.Bold;
|
||||||
|
titleText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
titleText.color = new Color(0.9f, 0.85f, 0.6f);
|
||||||
|
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
|
||||||
|
titleRect.anchorMin = new Vector2(0f, 1f);
|
||||||
|
titleRect.anchorMax = new Vector2(1f, 1f);
|
||||||
|
titleRect.pivot = new Vector2(0.5f, 1f);
|
||||||
|
titleRect.anchoredPosition = new Vector2(0f, -25f);
|
||||||
|
titleRect.sizeDelta = new Vector2(0f, 40f);
|
||||||
|
|
||||||
|
GameObject seedObj = new GameObject("MapSeed");
|
||||||
|
seedObj.transform.SetParent(panel.transform, false);
|
||||||
|
seedText = seedObj.AddComponent<Text>();
|
||||||
|
seedText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
seedText.fontSize = 16;
|
||||||
|
seedText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
seedText.color = new Color(0.7f, 0.7f, 0.8f);
|
||||||
|
RectTransform seedRect = seedObj.GetComponent<RectTransform>();
|
||||||
|
seedRect.anchorMin = new Vector2(0f, 1f);
|
||||||
|
seedRect.anchorMax = new Vector2(1f, 1f);
|
||||||
|
seedRect.pivot = new Vector2(0.5f, 1f);
|
||||||
|
seedRect.anchoredPosition = new Vector2(0f, -65f);
|
||||||
|
seedRect.sizeDelta = new Vector2(0f, 25f);
|
||||||
|
|
||||||
|
// Container for grid nodes
|
||||||
|
GameObject containerObj = new GameObject("MapContainer");
|
||||||
|
containerObj.transform.SetParent(panel.transform, false);
|
||||||
|
mapContainer = containerObj.AddComponent<RectTransform>();
|
||||||
|
mapContainer.anchorMin = new Vector2(0.1f, 0.1f);
|
||||||
|
mapContainer.anchorMax = new Vector2(0.9f, 0.85f);
|
||||||
|
mapContainer.offsetMin = Vector2.zero;
|
||||||
|
mapContainer.offsetMax = Vector2.zero;
|
||||||
|
|
||||||
|
// Bottom guide
|
||||||
|
GameObject guideObj = new GameObject("MapGuide");
|
||||||
|
guideObj.transform.SetParent(panel.transform, false);
|
||||||
|
Text guideText = guideObj.AddComponent<Text>();
|
||||||
|
guideText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
guideText.fontSize = 16;
|
||||||
|
guideText.fontStyle = FontStyle.Bold;
|
||||||
|
guideText.alignment = TextAnchor.MiddleCenter;
|
||||||
|
guideText.color = new Color(1f, 0.85f, 0.35f, 1f); // Prominent yellow
|
||||||
|
guideText.text = "※ 지도는 확인용입니다. 창을 닫고 캐릭터를 움직여 방의 [출구 ▶]로 들어가면 다음 구역으로 진입합니다.\n[M] 또는 [ESC] 키를 눌러 지도를 닫으세요.";
|
||||||
|
RectTransform guideRect = guideObj.GetComponent<RectTransform>();
|
||||||
|
guideRect.anchorMin = new Vector2(0f, 0f);
|
||||||
|
guideRect.anchorMax = new Vector2(1f, 0f);
|
||||||
|
guideRect.pivot = new Vector2(0.5f, 0f);
|
||||||
|
guideRect.anchoredPosition = new Vector2(0f, 15f);
|
||||||
|
guideRect.sizeDelta = new Vector2(0f, 45f);
|
||||||
|
|
||||||
|
panel.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshMap()
|
||||||
|
{
|
||||||
|
// Clear previous elements
|
||||||
|
for (int i = 0; i < spawnedElements.Count; i++)
|
||||||
|
{
|
||||||
|
if (spawnedElements[i] != null)
|
||||||
|
{
|
||||||
|
Destroy(spawnedElements[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spawnedElements.Clear();
|
||||||
|
|
||||||
|
StageManager stageManager = StageManager.Instance;
|
||||||
|
if (stageManager == null || stageManager.CurrentPlan == null)
|
||||||
|
{
|
||||||
|
titleText.text = "STAGE WORLD MAP";
|
||||||
|
seedText.text = "NO ACTIVE STAGE";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StagePlan plan = stageManager.CurrentPlan;
|
||||||
|
titleText.text = $"STAGE {plan.StageIndex} — {GetStageSubtitle(plan.StageIndex)}";
|
||||||
|
seedText.text = $"SEED: {stageManager.CurrentSeed}";
|
||||||
|
|
||||||
|
RectInt bounds = plan.GetGridBounds();
|
||||||
|
float cellWidth = 140f;
|
||||||
|
float cellHeight = 90f;
|
||||||
|
Vector2 origin = new Vector2(
|
||||||
|
-(bounds.x + bounds.width * 0.5f - 0.5f) * cellWidth,
|
||||||
|
-(bounds.y + bounds.height * 0.5f - 0.5f) * cellHeight);
|
||||||
|
|
||||||
|
// Render Connection Lines first
|
||||||
|
foreach (StageNode node in plan.Nodes)
|
||||||
|
{
|
||||||
|
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
|
||||||
|
for (int d = 0; d < 4; d++)
|
||||||
|
{
|
||||||
|
StageDirection dir = (StageDirection)d;
|
||||||
|
int targetId = node.GetConnection(dir);
|
||||||
|
if (targetId > node.Id) // Draw each line once
|
||||||
|
{
|
||||||
|
StageNode targetNode = plan.GetNode(targetId);
|
||||||
|
if (targetNode != null)
|
||||||
|
{
|
||||||
|
Vector2 targetPos = origin + new Vector2(targetNode.GridPosition.x * cellWidth, targetNode.GridPosition.y * cellHeight);
|
||||||
|
CreateConnectionLine(nodePos, targetPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Nodes
|
||||||
|
foreach (StageNode node in plan.Nodes)
|
||||||
|
{
|
||||||
|
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
|
||||||
|
CreateNodeCard(node, nodePos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStageSubtitle(int stageIndex)
|
||||||
|
{
|
||||||
|
return stageIndex switch
|
||||||
|
{
|
||||||
|
1 => "ELITE STAGE",
|
||||||
|
2 => "MID BOSS STAGE",
|
||||||
|
3 => "FINAL BOSS STAGE",
|
||||||
|
_ => "EXPLORATION",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateConnectionLine(Vector2 from, Vector2 to)
|
||||||
|
{
|
||||||
|
GameObject lineObj = new GameObject("MapLine");
|
||||||
|
lineObj.transform.SetParent(mapContainer, false);
|
||||||
|
Image lineImg = lineObj.AddComponent<Image>();
|
||||||
|
lineImg.color = new Color(0.3f, 0.4f, 0.55f, 0.6f);
|
||||||
|
|
||||||
|
RectTransform rt = lineObj.GetComponent<RectTransform>();
|
||||||
|
Vector2 diff = to - from;
|
||||||
|
rt.sizeDelta = new Vector2(diff.magnitude, 4f);
|
||||||
|
rt.pivot = new Vector2(0f, 0.5f);
|
||||||
|
rt.anchoredPosition = from;
|
||||||
|
float angle = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
|
||||||
|
rt.localRotation = Quaternion.Euler(0f, 0f, angle);
|
||||||
|
|
||||||
|
spawnedElements.Add(lineObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateNodeCard(StageNode node, Vector2 anchoredPos)
|
||||||
|
{
|
||||||
|
GameObject nodeObj = new GameObject($"NodeCard_{node.Id}");
|
||||||
|
nodeObj.transform.SetParent(mapContainer, false);
|
||||||
|
RectTransform rt = nodeObj.AddComponent<RectTransform>();
|
||||||
|
rt.sizeDelta = new Vector2(110f, 65f);
|
||||||
|
rt.anchoredPosition = anchoredPos;
|
||||||
|
|
||||||
|
Image bg = nodeObj.AddComponent<Image>();
|
||||||
|
bg.color = GetNodeColor(node);
|
||||||
|
|
||||||
|
// Node Text: Name / Type
|
||||||
|
GameObject textObj = new GameObject("Label");
|
||||||
|
textObj.transform.SetParent(nodeObj.transform, false);
|
||||||
|
Text label = textObj.AddComponent<Text>();
|
||||||
|
label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||||
|
label.fontSize = 12;
|
||||||
|
label.fontStyle = FontStyle.Bold;
|
||||||
|
label.alignment = TextAnchor.MiddleCenter;
|
||||||
|
label.color = Color.white;
|
||||||
|
label.text = GetNodeDisplayText(node);
|
||||||
|
|
||||||
|
RectTransform textRt = textObj.GetComponent<RectTransform>();
|
||||||
|
textRt.anchorMin = Vector2.zero;
|
||||||
|
textRt.anchorMax = Vector2.one;
|
||||||
|
textRt.offsetMin = new Vector2(4f, 4f);
|
||||||
|
textRt.offsetMax = new Vector2(-4f, -4f);
|
||||||
|
|
||||||
|
// Current Player Indicator Border
|
||||||
|
if (node.IsCurrent)
|
||||||
|
{
|
||||||
|
GameObject borderObj = new GameObject("CurrentBorder");
|
||||||
|
borderObj.transform.SetParent(nodeObj.transform, false);
|
||||||
|
Image borderImg = borderObj.AddComponent<Image>();
|
||||||
|
borderImg.color = new Color(1f, 0.85f, 0.2f, 0.9f);
|
||||||
|
RectTransform borderRt = borderObj.GetComponent<RectTransform>();
|
||||||
|
borderRt.anchorMin = Vector2.zero;
|
||||||
|
borderRt.anchorMax = Vector2.one;
|
||||||
|
borderRt.offsetMin = new Vector2(-4f, -4f);
|
||||||
|
borderRt.offsetMax = new Vector2(4f, 4f);
|
||||||
|
borderObj.transform.SetAsFirstSibling();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnedElements.Add(nodeObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color GetNodeColor(StageNode node)
|
||||||
|
{
|
||||||
|
if (node.IsCurrent)
|
||||||
|
{
|
||||||
|
return new Color(0.2f, 0.5f, 0.8f, 0.95f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.NodeType switch
|
||||||
|
{
|
||||||
|
StageNodeType.Entrance => new Color(0.25f, 0.45f, 0.35f, 0.9f),
|
||||||
|
StageNodeType.Combat => new Color(0.35f, 0.25f, 0.25f, 0.9f),
|
||||||
|
StageNodeType.Fork => new Color(0.3f, 0.3f, 0.45f, 0.9f),
|
||||||
|
StageNodeType.Reward => new Color(0.5f, 0.4f, 0.15f, 0.9f),
|
||||||
|
StageNodeType.Recovery => new Color(0.2f, 0.45f, 0.25f, 0.9f),
|
||||||
|
StageNodeType.Elite => new Color(0.6f, 0.25f, 0.15f, 0.95f),
|
||||||
|
StageNodeType.MidBoss => new Color(0.65f, 0.2f, 0.2f, 0.95f),
|
||||||
|
StageNodeType.FinalBoss => new Color(0.6f, 0.15f, 0.5f, 0.95f),
|
||||||
|
StageNodeType.StageExit => new Color(0.2f, 0.55f, 0.45f, 0.9f),
|
||||||
|
_ => new Color(0.3f, 0.3f, 0.35f, 0.9f),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetNodeDisplayText(StageNode node)
|
||||||
|
{
|
||||||
|
string currentTag = node.IsCurrent ? "[YOU]\n" : "";
|
||||||
|
string typeName = node.NodeType switch
|
||||||
|
{
|
||||||
|
StageNodeType.Entrance => "ENTRANCE",
|
||||||
|
StageNodeType.Combat => "COMBAT",
|
||||||
|
StageNodeType.Fork => "FORK",
|
||||||
|
StageNodeType.Reward => $"REWARD\n<{node.RewardType}>",
|
||||||
|
StageNodeType.Recovery => "RECOVERY\n<HEAL 50%>",
|
||||||
|
StageNodeType.Elite => "★ ELITE ★",
|
||||||
|
StageNodeType.MidBoss => "★ MID BOSS ★",
|
||||||
|
StageNodeType.FinalBoss => "☠ NECROMANCER ☠",
|
||||||
|
StageNodeType.StageExit => "STAGE EXIT",
|
||||||
|
_ => node.NodeType.ToString().ToUpper(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return currentTag + typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3ac4129f6b1ac4a4789bc13d9ad8a435
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user