Updates to spells, HUD, Gold, and Camera Controls. New sound effects!
This commit is contained in:
parent
689fe7be88
commit
a33d951abd
27 changed files with 578 additions and 68 deletions
8
Assets/_Project/Art/Sprites/Spells.meta
Normal file
8
Assets/_Project/Art/Sprites/Spells.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b086eebc9f3f87543bf2c98de8c8e4af
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/_Project/Art/Sprites/Spells/FireballSpell.png
(Stored with Git LFS)
Normal file
BIN
Assets/_Project/Art/Sprites/Spells/FireballSpell.png
(Stored with Git LFS)
Normal file
Binary file not shown.
117
Assets/_Project/Art/Sprites/Spells/FireballSpell.png.meta
Normal file
117
Assets/_Project/Art/Sprites/Spells/FireballSpell.png.meta
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eff45e5e3dab24f438c054a095d34578
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 0
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/_Project/Art/Sprites/Spells/SlowSpell.png
(Stored with Git LFS)
Normal file
BIN
Assets/_Project/Art/Sprites/Spells/SlowSpell.png
(Stored with Git LFS)
Normal file
Binary file not shown.
117
Assets/_Project/Art/Sprites/Spells/SlowSpell.png.meta
Normal file
117
Assets/_Project/Art/Sprites/Spells/SlowSpell.png.meta
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f5072d8c9f626c4690a7b231c7488f9
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 0
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 1
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 100
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/_Project/Audio/Sound Effects/FireballExplosion.ogg
(Stored with Git LFS)
Normal file
BIN
Assets/_Project/Audio/Sound Effects/FireballExplosion.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e54448243d0478045b6566412c54e4a7
|
||||||
|
AudioImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 8
|
||||||
|
defaultSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
loadType: 0
|
||||||
|
sampleRateSetting: 0
|
||||||
|
sampleRateOverride: 44100
|
||||||
|
compressionFormat: 1
|
||||||
|
quality: 1
|
||||||
|
conversionMode: 0
|
||||||
|
preloadAudioData: 0
|
||||||
|
platformSettingOverrides: {}
|
||||||
|
forceToMono: 0
|
||||||
|
normalize: 1
|
||||||
|
loadInBackground: 0
|
||||||
|
ambisonic: 0
|
||||||
|
3D: 1
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/_Project/Audio/Sound Effects/TimeSlow.ogg
(Stored with Git LFS)
Normal file
BIN
Assets/_Project/Audio/Sound Effects/TimeSlow.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
23
Assets/_Project/Audio/Sound Effects/TimeSlow.ogg.meta
Normal file
23
Assets/_Project/Audio/Sound Effects/TimeSlow.ogg.meta
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 038ff3eab9aa0034bac26ccb648ef2a1
|
||||||
|
AudioImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 8
|
||||||
|
defaultSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
loadType: 0
|
||||||
|
sampleRateSetting: 0
|
||||||
|
sampleRateOverride: 44100
|
||||||
|
compressionFormat: 1
|
||||||
|
quality: 1
|
||||||
|
conversionMode: 0
|
||||||
|
preloadAudioData: 0
|
||||||
|
platformSettingOverrides: {}
|
||||||
|
forceToMono: 0
|
||||||
|
normalize: 1
|
||||||
|
loadInBackground: 0
|
||||||
|
ambisonic: 0
|
||||||
|
3D: 1
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -14,18 +14,19 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.FireballSpellDefinition
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.FireballSpellDefinition
|
||||||
DisplayName: Fireball
|
DisplayName: Fireball
|
||||||
Description: Shoot a fireball
|
Description: Shoot a fireball
|
||||||
Icon: {fileID: 0}
|
Icon: {fileID: 21300000, guid: eff45e5e3dab24f438c054a095d34578, type: 3}
|
||||||
Cooldown: 5
|
Cooldown: 1
|
||||||
TargetType: 0
|
TargetType: 0
|
||||||
Radius: 3
|
Radius: 5
|
||||||
enemyLayerMask:
|
enemyLayerMask:
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 1024
|
m_Bits: 1024
|
||||||
impactVfxPrefab: {fileID: 158073311046191680, guid: 80eaad6d0b024835482b64625b63053c, type: 3}
|
impactVfxPrefab: {fileID: 158073311046191680, guid: 80eaad6d0b024835482b64625b63053c, type: 3}
|
||||||
impactVfxLifetime: 3
|
impactVfxLifetime: 3
|
||||||
|
impactDelay: 0.9
|
||||||
impactSound:
|
impactSound:
|
||||||
clip: {fileID: 8300000, guid: 4ee1b63f27ea04451802bafd35ecef89, type: 3}
|
clip: {fileID: 8300000, guid: e54448243d0478045b6566412c54e4a7, type: 3}
|
||||||
volume: 0.5
|
volume: 0.603
|
||||||
minPitch: 1.048
|
minPitch: 1.048
|
||||||
maxPitch: 0.969
|
maxPitch: 0.969
|
||||||
Damage: 50
|
Damage: 500
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ MonoBehaviour:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.SlowAreaSpellDefinition
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.SlowAreaSpellDefinition
|
||||||
DisplayName: Slow Area
|
DisplayName: Slow Area
|
||||||
Description: Slows enemies in an area
|
Description: Slows enemies in an area
|
||||||
Icon: {fileID: 0}
|
Icon: {fileID: 21300000, guid: 5f5072d8c9f626c4690a7b231c7488f9, type: 3}
|
||||||
Cooldown: 5
|
Cooldown: 10
|
||||||
TargetType: 1
|
TargetType: 1
|
||||||
Radius: 5
|
Radius: 5
|
||||||
enemyLayerMask:
|
enemyLayerMask:
|
||||||
|
|
@ -23,9 +23,9 @@ MonoBehaviour:
|
||||||
m_Bits: 1024
|
m_Bits: 1024
|
||||||
areaVfxPrefab: {fileID: 1462673130280185047, guid: bf2f1f2a9a54e9162b80673e3f7eeaf6, type: 3}
|
areaVfxPrefab: {fileID: 1462673130280185047, guid: bf2f1f2a9a54e9162b80673e3f7eeaf6, type: 3}
|
||||||
areaSound:
|
areaSound:
|
||||||
clip: {fileID: 0}
|
clip: {fileID: 8300000, guid: 038ff3eab9aa0034bac26ccb648ef2a1, type: 3}
|
||||||
volume: 0
|
volume: 0.632
|
||||||
minPitch: 0
|
minPitch: 1.14
|
||||||
maxPitch: 0
|
maxPitch: 0.96
|
||||||
SlowFactor: 0.5
|
SlowFactor: 0.5
|
||||||
EffectDuration: 3
|
EffectDuration: 8
|
||||||
|
|
|
||||||
|
|
@ -17,44 +17,40 @@ MonoBehaviour:
|
||||||
- Wave: {fileID: 11400000, guid: 65f66289ea1233b4897f46cd997d9c7a, type: 2}
|
- Wave: {fileID: 11400000, guid: 65f66289ea1233b4897f46cd997d9c7a, type: 2}
|
||||||
GoldPerEnemy: 5
|
GoldPerEnemy: 5
|
||||||
CompletionBonus: 25
|
CompletionBonus: 25
|
||||||
NoLeaksBonus: 50
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 190e39db44aa0794aa808fd60976f7c4, type: 2}
|
- Wave: {fileID: 11400000, guid: 190e39db44aa0794aa808fd60976f7c4, type: 2}
|
||||||
GoldPerEnemy: 7
|
GoldPerEnemy: 7
|
||||||
CompletionBonus: 30
|
CompletionBonus: 30
|
||||||
NoLeaksBonus: 50
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 39921b44a1a0a56478200028940c5202, type: 2}
|
- Wave: {fileID: 11400000, guid: 39921b44a1a0a56478200028940c5202, type: 2}
|
||||||
GoldPerEnemy: 10
|
GoldPerEnemy: 10
|
||||||
CompletionBonus: 35
|
CompletionBonus: 35
|
||||||
NoLeaksBonus: 50
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 50f498bc5bfc46e44b064cc96403e2cb, type: 2}
|
- Wave: {fileID: 11400000, guid: 50f498bc5bfc46e44b064cc96403e2cb, type: 2}
|
||||||
GoldPerEnemy: 15
|
GoldPerEnemy: 15
|
||||||
CompletionBonus: 40
|
CompletionBonus: 40
|
||||||
NoLeaksBonus: 50
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 9ee6dee12f0660844b4d46880f01c02f, type: 2}
|
- Wave: {fileID: 11400000, guid: 9ee6dee12f0660844b4d46880f01c02f, type: 2}
|
||||||
GoldPerEnemy: 15
|
GoldPerEnemy: 15
|
||||||
CompletionBonus: 45
|
CompletionBonus: 45
|
||||||
NoLeaksBonus: 50
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 41231de63e8f25d448f19f3816e0c22f, type: 2}
|
- Wave: {fileID: 11400000, guid: 41231de63e8f25d448f19f3816e0c22f, type: 2}
|
||||||
GoldPerEnemy: 20
|
GoldPerEnemy: 20
|
||||||
CompletionBonus: 50
|
CompletionBonus: 50
|
||||||
NoLeaksBonus: 100
|
NoLeaksBonus: 10
|
||||||
- Wave: {fileID: 11400000, guid: 6290df178cbd3144aa92a0f09833a7be, type: 2}
|
- Wave: {fileID: 11400000, guid: 6290df178cbd3144aa92a0f09833a7be, type: 2}
|
||||||
GoldPerEnemy: 20
|
GoldPerEnemy: 20
|
||||||
CompletionBonus: 55
|
CompletionBonus: 55
|
||||||
NoLeaksBonus: 100
|
NoLeaksBonus: 20
|
||||||
- Wave: {fileID: 11400000, guid: 86736fd52c18fa84e8ced40f30b514fa, type: 2}
|
- Wave: {fileID: 11400000, guid: 86736fd52c18fa84e8ced40f30b514fa, type: 2}
|
||||||
GoldPerEnemy: 25
|
GoldPerEnemy: 25
|
||||||
CompletionBonus: 60
|
CompletionBonus: 60
|
||||||
NoLeaksBonus: 100
|
NoLeaksBonus: 20
|
||||||
- Wave: {fileID: 11400000, guid: 8fdf53cfc405a5f41a00f376198b8d84, type: 2}
|
- Wave: {fileID: 11400000, guid: 8fdf53cfc405a5f41a00f376198b8d84, type: 2}
|
||||||
GoldPerEnemy: 25
|
GoldPerEnemy: 25
|
||||||
CompletionBonus: 65
|
CompletionBonus: 65
|
||||||
NoLeaksBonus: 100
|
NoLeaksBonus: 20
|
||||||
- Wave: {fileID: 11400000, guid: 4db677d2940202340841471f90a5b73a, type: 2}
|
- Wave: {fileID: 11400000, guid: 4db677d2940202340841471f90a5b73a, type: 2}
|
||||||
GoldPerEnemy: 25
|
GoldPerEnemy: 25
|
||||||
CompletionBonus: 70
|
CompletionBonus: 70
|
||||||
NoLeaksBonus: 100
|
NoLeaksBonus: 20
|
||||||
- Wave: {fileID: 11400000, guid: fc1a55fab4ce81047b4925f53a3a8b8d, type: 2}
|
|
||||||
GoldPerEnemy: 50
|
|
||||||
CompletionBonus: 75
|
|
||||||
NoLeaksBonus: 500
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ namespace TD.Gameplay
|
||||||
// Cached reference to the local TowerPaintController, looked up lazily (same
|
// Cached reference to the local TowerPaintController, looked up lazily (same
|
||||||
// rationale as the placement controller).
|
// rationale as the placement controller).
|
||||||
private TowerPaintController cachedPaintController;
|
private TowerPaintController cachedPaintController;
|
||||||
|
private BuilderSpellCastController cachedSpellCastController;
|
||||||
|
|
||||||
// ----- Lifecycle --------------------------------------------------
|
// ----- Lifecycle --------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -120,7 +121,7 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// Placement and paint are both modal: while either is active, the local
|
// Placement and paint are both modal: while either is active, the local
|
||||||
// controller for that mode owns left-/right-click, so selection here yields.
|
// controller for that mode owns left-/right-click, so selection here yields.
|
||||||
bool isModal = IsLocalPlayerPlacing() || IsLocalPlayerPainting();
|
bool isModal = IsLocalPlayerPlacing() || IsLocalPlayerPainting() || IsLocalPlayerAimingSpell();
|
||||||
Vector2 mousePos = mouse.position.ReadValue();
|
Vector2 mousePos = mouse.position.ReadValue();
|
||||||
|
|
||||||
// UI Toolkit dispatches button click events AFTER Update runs, but raw mouse
|
// UI Toolkit dispatches button click events AFTER Update runs, but raw mouse
|
||||||
|
|
@ -292,5 +293,21 @@ namespace TD.Gameplay
|
||||||
}
|
}
|
||||||
return cachedPaintController.IsPainting;
|
return cachedPaintController.IsPainting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsLocalPlayerAimingSpell()
|
||||||
|
{
|
||||||
|
if (cachedSpellCastController == null)
|
||||||
|
{
|
||||||
|
// Find lazily — controller may have been added after this component spawned.
|
||||||
|
cachedSpellCastController =
|
||||||
|
UnityEngine.Object.FindAnyObjectByType<BuilderSpellCastController>();
|
||||||
|
if (cachedSpellCastController == null) return false;
|
||||||
|
}
|
||||||
|
// IsAiming covers "aiming, click not yet made"; ConsumedCastClickThisFrame covers the
|
||||||
|
// frame the confirming click casts and exits aim mode — together they suppress the
|
||||||
|
// selection click regardless of Update order between the two controllers.
|
||||||
|
return cachedSpellCastController.IsAiming
|
||||||
|
|| cachedSpellCastController.ConsumedCastClickThisFrame;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +56,21 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// -1 when not aiming.
|
// -1 when not aiming.
|
||||||
private int activeSlot = -1;
|
private int activeSlot = -1;
|
||||||
|
|
||||||
|
// Frame on which the confirming cast click was consumed. Lets the selection controller
|
||||||
|
// ignore that click order-independently — see ConsumedCastClickThisFrame.
|
||||||
|
private int lastCastClickFrame = -1;
|
||||||
|
|
||||||
|
/// <summary>True while a spell is being aimed (awaiting the confirming click). The
|
||||||
|
/// selection input controller treats this as a modal state, so the click that confirms
|
||||||
|
/// the cast doesn't also fall through to selection and deselect the builder.</summary>
|
||||||
|
public bool IsAiming => activeSlot >= 0;
|
||||||
|
|
||||||
|
/// <summary>True during the frame the confirming cast click was consumed. Aim mode exits
|
||||||
|
/// the same frame the cast is submitted, flipping <see cref="IsAiming"/> false; this stays
|
||||||
|
/// true for the rest of that frame so the selection controller ignores the click no matter
|
||||||
|
/// which controller's Update ran first.</summary>
|
||||||
|
public bool ConsumedCastClickThisFrame => lastCastClickFrame == Time.frameCount;
|
||||||
private BuilderSpellDefinition activeDefinition;
|
private BuilderSpellDefinition activeDefinition;
|
||||||
|
|
||||||
private bool lastHitValid;
|
private bool lastHitValid;
|
||||||
|
|
@ -85,6 +100,14 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
if (activeSlot < 0) return; // idle — nothing to aim
|
if (activeSlot < 0) return; // idle — nothing to aim
|
||||||
|
|
||||||
|
// Deselecting the builder mid-aim cancels the cast, so you can't start aiming while
|
||||||
|
// selected and then deselect to sneak a cast past the gate above.
|
||||||
|
if (!LocalBuilderSelected())
|
||||||
|
{
|
||||||
|
ExitAimMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var keyboard = Keyboard.current;
|
var keyboard = Keyboard.current;
|
||||||
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
|
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
|
||||||
{
|
{
|
||||||
|
|
@ -115,16 +138,32 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
if (mouse.leftButton.wasPressedThisFrame && lastHitValid)
|
if (mouse.leftButton.wasPressedThisFrame && lastHitValid)
|
||||||
{
|
{
|
||||||
|
// Record the frame so the selection controller ignores this click even though
|
||||||
|
// TrySubmitCast exits aim mode this same frame (IsAiming flips false). Without
|
||||||
|
// this, whether the click also deselected the builder depended on Update order.
|
||||||
|
lastCastClickFrame = Time.frameCount;
|
||||||
TrySubmitCast();
|
TrySubmitCast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Hotkey scan --------------------------------------------------
|
// ----- Hotkey scan --------------------------------------------------
|
||||||
|
|
||||||
|
// True when the local player's own builder is the current selection. Casting is gated
|
||||||
|
// on this so it matches the spell HUD (shown only for the selected builder).
|
||||||
|
private static bool LocalBuilderSelected()
|
||||||
|
{
|
||||||
|
var selected = SelectionState.Instance?.SelectedObject;
|
||||||
|
return selected is Builder builder && builder.IsOwner;
|
||||||
|
}
|
||||||
|
|
||||||
private void ScanHotkeys()
|
private void ScanHotkeys()
|
||||||
{
|
{
|
||||||
if (activeSlot >= 0) return; // already aiming — hotkeys re-scanned only when idle
|
if (activeSlot >= 0) return; // already aiming — hotkeys re-scanned only when idle
|
||||||
|
|
||||||
|
// Spells cast only while the local player's OWN builder is selected — keeps casting
|
||||||
|
// in sync with the spell HUD, which is shown only for the selected builder.
|
||||||
|
if (!LocalBuilderSelected()) return;
|
||||||
|
|
||||||
var loadout = PlayerSpellLoadout.Local;
|
var loadout = PlayerSpellLoadout.Local;
|
||||||
if (loadout == null) return;
|
if (loadout == null) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
/// <para><b>Server-only resolution, client-only visual.</b> <see cref="ServerCast"/> runs on
|
/// <para><b>Server-only resolution, client-only visual.</b> <see cref="ServerCast"/> runs on
|
||||||
/// the server and applies damage/status via the same <c>Physics.OverlapSphereNonAlloc</c> +
|
/// the server and applies damage/status via the same <c>Physics.OverlapSphereNonAlloc</c> +
|
||||||
/// <c>EnemyHealth</c>/<c>EnemyStatus</c> pattern <c>TowerCombat</c> already uses.
|
/// <c>EnemyHealth</c>/<c>EnemyStatus</c> pattern <c>TowerCombat</c> already uses.
|
||||||
/// <see cref="ClientPlayVfx"/> runs on every peer (including the server) after a successful
|
/// Presentation is split into <see cref="ClientSpawnVfx"/> (on cast) and
|
||||||
/// cast, purely for presentation.</para>
|
/// <see cref="ClientPlayImpact"/> (on contact, <see cref="ImpactDelay"/> seconds later for a
|
||||||
|
/// projectile spell), both running on every peer including the server.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class BuilderSpellDefinition : ScriptableObject
|
public abstract class BuilderSpellDefinition : ScriptableObject
|
||||||
{
|
{
|
||||||
|
|
@ -82,10 +83,28 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
public abstract bool ServerCast(ulong clientId, Vector3 targetPoint);
|
public abstract bool ServerCast(ulong clientId, Vector3 targetPoint);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs on every peer (via <see cref="PlayerSpellLoadout"/>'s ClientRpc) after a
|
/// Seconds between the cast and the effect landing. 0 = instant: damage and the impact
|
||||||
/// successful <see cref="ServerCast"/>. Default no-op; override to spawn an impact VFX
|
/// sound resolve immediately on cast. A projectile-style spell (e.g. the Fireball meteor)
|
||||||
/// prefab and self-destroy it, the same idiom used elsewhere for one-off visuals.
|
/// overrides this with its fall/travel time, so <see cref="PlayerSpellLoadout"/> spawns the
|
||||||
|
/// visual on cast but holds <see cref="ServerCast"/> (damage) and <see cref="ClientPlayImpact"/>
|
||||||
|
/// (sound) until the projectile reaches the ground. Set it to match the VFX's fall time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void ClientPlayVfx(Vector3 targetPoint) { }
|
public virtual float ImpactDelay => 0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs on every peer (via <see cref="PlayerSpellLoadout"/>'s ClientRpc) the moment the
|
||||||
|
/// spell is cast. Spawns the visual: for an instant spell that's the whole effect; for a
|
||||||
|
/// delayed spell it's the projectile/travel visual that lands after <see cref="ImpactDelay"/>.
|
||||||
|
/// Default no-op.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void ClientSpawnVfx(Vector3 targetPoint) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs on every peer when the spell makes contact — immediately for an instant spell, or
|
||||||
|
/// <see cref="ImpactDelay"/> seconds after cast for a delayed one. Play the impact sound
|
||||||
|
/// (and any impact-moment visual) here so it lands with the effect, not the throw.
|
||||||
|
/// Default no-op.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void ClientPlayImpact(Vector3 targetPoint) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to sync.
|
/// Plain MonoBehaviour: identical on every peer (same assets), so there is nothing to sync.
|
||||||
/// The server reads it to resolve casts; clients read it to resolve <see
|
/// The server reads it to resolve casts; clients read it to resolve <see
|
||||||
/// cref="BuilderSpellDefinition.ClientPlayVfx"/> and to render the cast hotbar. Mirrors
|
/// cref="BuilderSpellDefinition.ClientSpawnVfx"/> / <see
|
||||||
|
/// cref="BuilderSpellDefinition.ClientPlayImpact"/> and to render the cast hotbar. Mirrors
|
||||||
/// <see cref="BuilderEffects.BuilderEffectPool"/> exactly.
|
/// <see cref="BuilderEffects.BuilderEffectPool"/> exactly.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class BuilderSpellPool : MonoBehaviour
|
public class BuilderSpellPool : MonoBehaviour
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,14 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
[Min(0f)]
|
[Min(0f)]
|
||||||
[SerializeField] private float impactVfxLifetime = 2f;
|
[SerializeField] private float impactVfxLifetime = 2f;
|
||||||
|
|
||||||
|
[Tooltip("Seconds from cast until the falling fireball reaches the ground. Damage and the " +
|
||||||
|
"impact sound are held until then so they land with the visual — SET THIS TO MATCH " +
|
||||||
|
"the meteor VFX's fall time. 0 = everything resolves instantly on cast.")]
|
||||||
|
[Min(0f)]
|
||||||
|
[SerializeField] private float impactDelay = 0.8f;
|
||||||
|
|
||||||
|
public override float ImpactDelay => impactDelay;
|
||||||
|
|
||||||
[Header("Impact Sound")]
|
[Header("Impact Sound")]
|
||||||
[Tooltip("Sound played on every peer when this spell resolves successfully. Same " +
|
[Tooltip("Sound played on every peer when this spell resolves successfully. Same " +
|
||||||
"SoundConfig struct TowerBuiltSound uses — reuse a tower's clip directly if " +
|
"SoundConfig struct TowerBuiltSound uses — reuse a tower's clip directly if " +
|
||||||
|
|
@ -59,11 +67,16 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
return hitAny;
|
return hitAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ClientPlayVfx(Vector3 targetPoint)
|
public override void ClientSpawnVfx(Vector3 targetPoint)
|
||||||
{
|
{
|
||||||
|
// The meteor VFX plays its full fall-and-crash animation from here; it reaches the
|
||||||
|
// ground after ImpactDelay, which is when ClientPlayImpact and ServerCast fire.
|
||||||
if (impactVfxPrefab != null)
|
if (impactVfxPrefab != null)
|
||||||
Destroy(Instantiate(impactVfxPrefab, targetPoint, Quaternion.identity), impactVfxLifetime);
|
Destroy(Instantiate(impactVfxPrefab, targetPoint, Quaternion.identity), impactVfxLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ClientPlayImpact(Vector3 targetPoint)
|
||||||
|
{
|
||||||
if (impactSound.clip != null)
|
if (impactSound.clip != null)
|
||||||
AudioManager.Instance?.Play(impactSound.clip, AudioCategory.Combat,
|
AudioManager.Instance?.Play(impactSound.clip, AudioCategory.Combat,
|
||||||
impactSound.RandomPitch(), impactSound.volume);
|
impactSound.RandomPitch(), impactSound.volume);
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
return hitAny;
|
return hitAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ClientPlayVfx(Vector3 targetPoint)
|
public override void ClientSpawnVfx(Vector3 targetPoint)
|
||||||
{
|
{
|
||||||
if (areaVfxPrefab != null)
|
if (areaVfxPrefab != null)
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +70,11 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
instance.transform.localScale *= Mathf.Max(Radius, 0.01f);
|
instance.transform.localScale *= Mathf.Max(Radius, 0.01f);
|
||||||
Destroy(instance, EffectDuration);
|
Destroy(instance, EffectDuration);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ClientPlayImpact(Vector3 targetPoint)
|
||||||
|
{
|
||||||
|
// Instant spell (ImpactDelay = 0), so this fires on cast alongside ClientSpawnVfx.
|
||||||
if (areaSound.clip != null)
|
if (areaSound.clip != null)
|
||||||
AudioManager.Instance?.Play(areaSound.clip, AudioCategory.Combat,
|
AudioManager.Instance?.Play(areaSound.clip, AudioCategory.Combat,
|
||||||
areaSound.RandomPitch(), areaSound.volume);
|
areaSound.RandomPitch(), areaSound.volume);
|
||||||
|
|
|
||||||
|
|
@ -227,16 +227,17 @@ namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
Vector2 dir = Vector2.zero;
|
Vector2 dir = Vector2.zero;
|
||||||
|
|
||||||
// Keyboard: WASD + arrow keys. Suppressed entirely while the player
|
// Keyboard: arrow keys only. WASD is reserved for tower-build hotkeys (the command
|
||||||
// is typing — pressing 'a' or 'w' into chat should not pan the camera.
|
// grid) — camera panning is arrow keys plus the mouse edge-pan below. Suppressed
|
||||||
// (Edge-pan below stays active since it's mouse-driven.)
|
// entirely while the player is typing so arrow keys in chat navigate text instead of
|
||||||
|
// panning. (Edge-pan below stays active since it's mouse-driven.)
|
||||||
var kb = HUDController.IsTextInputActive ? null : Keyboard.current;
|
var kb = HUDController.IsTextInputActive ? null : Keyboard.current;
|
||||||
if (kb != null)
|
if (kb != null)
|
||||||
{
|
{
|
||||||
if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) dir.x -= 1f;
|
if (kb.leftArrowKey.isPressed) dir.x -= 1f;
|
||||||
if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) dir.x += 1f;
|
if (kb.rightArrowKey.isPressed) dir.x += 1f;
|
||||||
if (kb.sKey.isPressed || kb.downArrowKey.isPressed) dir.y -= 1f;
|
if (kb.downArrowKey.isPressed) dir.y -= 1f;
|
||||||
if (kb.wKey.isPressed || kb.upArrowKey.isPressed) dir.y += 1f;
|
if (kb.upArrowKey.isPressed) dir.y += 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edge-pan: mouse near screen edge adds to keyboard direction.
|
// Edge-pan: mouse near screen edge adds to keyboard direction.
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,14 @@ namespace TD.Gameplay.Draft
|
||||||
var loadout = PlayerSpellLoadout.GetForClient(clientId);
|
var loadout = PlayerSpellLoadout.GetForClient(clientId);
|
||||||
return loadout != null && loadout.ServerGrantSpell(Kind);
|
return loadout != null && loadout.ServerGrantSpell(Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Inherits the granted spell's icon (resolved from the pool by
|
||||||
|
/// <see cref="Kind"/>) unless this option assigns an override.</summary>
|
||||||
|
public override Sprite ResolveIcon()
|
||||||
|
{
|
||||||
|
if (Icon != null) return Icon;
|
||||||
|
var def = BuilderSpellPool.Instance != null ? BuilderSpellPool.Instance.Get(Kind) : null;
|
||||||
|
return def != null ? def.Icon : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,19 @@ namespace TD.Gameplay.Draft
|
||||||
[TextArea(2, 4)]
|
[TextArea(2, 4)]
|
||||||
public string Description;
|
public string Description;
|
||||||
|
|
||||||
[Tooltip("Icon shown on the draft card. Optional for placeholder content.")]
|
[Tooltip("OPTIONAL override for the draft card icon. Leave empty to inherit the icon of " +
|
||||||
|
"the item this option grants (its tower / spell); assign one only when you want " +
|
||||||
|
"to override that inherited icon.")]
|
||||||
public Sprite Icon;
|
public Sprite Icon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The icon actually shown on the draft card. Returns the explicit <see cref="Icon"/>
|
||||||
|
/// override when one is assigned; otherwise subclasses fall back to the icon of the item
|
||||||
|
/// they grant (tower, spell). The base class has no referenced item, so it just returns
|
||||||
|
/// <see cref="Icon"/> (which may be null).
|
||||||
|
/// </summary>
|
||||||
|
public virtual Sprite ResolveIcon() => Icon;
|
||||||
|
|
||||||
[Header("Generation")]
|
[Header("Generation")]
|
||||||
[Tooltip("Relative draw weight. Higher = offered more often. Rarer rewards use " +
|
[Tooltip("Relative draw weight. Higher = offered more often. Rarer rewards use " +
|
||||||
"lower weights. Must be > 0.")]
|
"lower weights. Must be > 0.")]
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,9 @@ namespace TD.Gameplay.Draft
|
||||||
|
|
||||||
return deck.ServerGrantTower(typeId);
|
return deck.ServerGrantTower(typeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Inherits the granted tower's icon unless this option assigns an override.</summary>
|
||||||
|
public override Sprite ResolveIcon()
|
||||||
|
=> Icon != null ? Icon : (Tower != null ? Tower.Icon : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
|
// Assets/_Project/Scripts/Gameplay/PlayerSpellLoadout.cs
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -20,9 +21,12 @@ namespace TD.Gameplay
|
||||||
/// slot index and an aimed world point. The server re-validates everything (slot exists,
|
/// slot index and an aimed world point. The server re-validates everything (slot exists,
|
||||||
/// off cooldown, <see cref="MatchPhase.Playing"/>) since client-side checks in
|
/// off cooldown, <see cref="MatchPhase.Playing"/>) since client-side checks in
|
||||||
/// <see cref="BuilderSpellCastController"/> are UX-only, resolves the
|
/// <see cref="BuilderSpellCastController"/> are UX-only, resolves the
|
||||||
/// <see cref="BuilderSpellDefinition"/> via <see cref="BuilderSpellPool"/>, and calls
|
/// <see cref="BuilderSpellDefinition"/> via <see cref="BuilderSpellPool"/>. An <b>instant</b>
|
||||||
/// <see cref="BuilderSpellDefinition.ServerCast"/>. A successful cast starts the cooldown
|
/// spell (<see cref="BuilderSpellDefinition.ImpactDelay"/> == 0) resolves on cast: a hit starts
|
||||||
/// and fires <see cref="PlayVfxClientRpc"/> so every peer plays the same visual.</para>
|
/// the cooldown and fires <see cref="PlayVfxClientRpc"/>; a miss is a no-op. A <b>delayed</b>
|
||||||
|
/// spell (a projectile, e.g. the Fireball meteor) commits on the throw — cooldown + the falling
|
||||||
|
/// visual fire immediately — while its damage and impact sound are held until the projectile
|
||||||
|
/// lands <see cref="BuilderSpellDefinition.ImpactDelay"/> seconds later.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class PlayerSpellLoadout : NetworkBehaviour
|
public class PlayerSpellLoadout : NetworkBehaviour
|
||||||
{
|
{
|
||||||
|
|
@ -142,7 +146,7 @@ namespace TD.Gameplay
|
||||||
/// <paramref name="slot"/> at <paramref name="targetPoint"/>. All validation happens
|
/// <paramref name="slot"/> at <paramref name="targetPoint"/>. All validation happens
|
||||||
/// here on the server; the client only uses this to trigger an attempt.
|
/// here on the server; the client only uses this to trigger an attempt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Rpc(SendTo.Server, RequireOwnership = true)]
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||||
public void RequestCastSpellRpc(int slot, Vector3 targetPoint)
|
public void RequestCastSpellRpc(int slot, Vector3 targetPoint)
|
||||||
{
|
{
|
||||||
if (MatchState.Instance == null || MatchState.Instance.Phase != MatchPhase.Playing)
|
if (MatchState.Instance == null || MatchState.Instance.Phase != MatchPhase.Playing)
|
||||||
|
|
@ -160,18 +164,69 @@ namespace TD.Gameplay
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.ImpactDelay <= 0f)
|
||||||
|
{
|
||||||
|
// Instant spell (e.g. Slow Area): resolve on cast. A miss (hit nothing) is a no-op
|
||||||
|
// and does NOT start the cooldown — unchanged behavior.
|
||||||
if (!definition.ServerCast(OwnerClientId, targetPoint)) return;
|
if (!definition.ServerCast(OwnerClientId, targetPoint)) return;
|
||||||
|
|
||||||
|
StartCooldown(slot, definition);
|
||||||
|
PlayVfxClientRpc(slotValue.Kind, targetPoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Delayed spell (e.g. the Fireball meteor): the projectile has to fall before it
|
||||||
|
// lands, so the cast is COMMITTED on the throw — cooldown starts and the falling
|
||||||
|
// visual spawns now — while damage and the impact sound wait until it hits the
|
||||||
|
// ground (ImpactDelay later). Enemies are re-scanned at impact, so movement during
|
||||||
|
// the fall resolves correctly.
|
||||||
|
StartCooldown(slot, definition);
|
||||||
|
PlayVfxClientRpc(slotValue.Kind, targetPoint);
|
||||||
|
StartCoroutine(ServerResolveImpactAfterDelay(
|
||||||
|
slotValue.Kind, OwnerClientId, targetPoint, definition.ImpactDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only: stamp the slot's cooldown from the definition and replicate it.
|
||||||
|
private void StartCooldown(int slot, BuilderSpellDefinition definition)
|
||||||
|
{
|
||||||
|
var slotValue = spells[slot];
|
||||||
slotValue.CooldownEndServerTime = NetworkManager.ServerTime.Time + definition.Cooldown;
|
slotValue.CooldownEndServerTime = NetworkManager.ServerTime.Time + definition.Cooldown;
|
||||||
spells[slot] = slotValue;
|
spells[slot] = slotValue;
|
||||||
|
}
|
||||||
|
|
||||||
PlayVfxClientRpc(slotValue.Kind, targetPoint);
|
// Server-only: apply a delayed spell's effect once its projectile has landed. Re-resolves
|
||||||
|
// targets at impact — the enemy set may have shifted during the fall.
|
||||||
|
private IEnumerator ServerResolveImpactAfterDelay(
|
||||||
|
BuilderSpellKind kind, ulong clientId, Vector3 targetPoint, float delay)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(delay);
|
||||||
|
if (!IsServer) yield break;
|
||||||
|
BuilderSpellPool.Instance?.Get(kind)?.ServerCast(clientId, targetPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[ClientRpc]
|
[ClientRpc]
|
||||||
private void PlayVfxClientRpc(BuilderSpellKind kind, Vector3 targetPoint)
|
private void PlayVfxClientRpc(BuilderSpellKind kind, Vector3 targetPoint)
|
||||||
{
|
{
|
||||||
BuilderSpellPool.Instance?.Get(kind)?.ClientPlayVfx(targetPoint);
|
var def = BuilderSpellPool.Instance?.Get(kind);
|
||||||
|
if (def == null) return;
|
||||||
|
|
||||||
|
def.ClientSpawnVfx(targetPoint); // spawn the (possibly falling) visual now
|
||||||
|
|
||||||
|
if (def.ImpactDelay <= 0f)
|
||||||
|
def.ClientPlayImpact(targetPoint);
|
||||||
|
else
|
||||||
|
StartCoroutine(ClientPlayImpactAfterDelay(kind, targetPoint, def.ImpactDelay));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side: play the impact (sound + any contact visual) when the projectile lands.
|
||||||
|
// Timed locally off the same ImpactDelay so it stays in lockstep with this peer's own
|
||||||
|
// falling visual — no second RPC, no double latency.
|
||||||
|
private IEnumerator ClientPlayImpactAfterDelay(
|
||||||
|
BuilderSpellKind kind, Vector3 targetPoint, float delay)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(delay);
|
||||||
|
BuilderSpellPool.Instance?.Get(kind)?.ClientPlayImpact(targetPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,13 +116,17 @@ namespace TD.UI
|
||||||
private bool draftSubscribed;
|
private bool draftSubscribed;
|
||||||
private PlayerDraft subscribedDraft;
|
private PlayerDraft subscribedDraft;
|
||||||
|
|
||||||
// Spell hotbar (bottom-ui Section 6). Frame is display:none while the local player
|
// Spell hotbar (bottom-ui Section 6). Frame is visibility:hidden (but still occupies
|
||||||
|
// its reserved width, so the centered command bar never shifts) while the local player
|
||||||
// has no granted spells; rebuilt on grant (append-only, so this is rare). Cooldown
|
// has no granted spells; rebuilt on grant (append-only, so this is rare). Cooldown
|
||||||
// state changes continuously and has no change event, so it's polled every Update.
|
// state changes continuously and has no change event, so it's polled every Update.
|
||||||
private VisualElement spellHotbarFrame;
|
private VisualElement spellHotbarFrame;
|
||||||
private VisualElement spellHotbar;
|
private VisualElement spellHotbar;
|
||||||
private bool spellLoadoutSubscribed;
|
private bool spellLoadoutSubscribed;
|
||||||
private PlayerSpellLoadout subscribedSpellLoadout;
|
private PlayerSpellLoadout subscribedSpellLoadout;
|
||||||
|
// The loadout the hotbar is currently showing — the SELECTED builder's, or null when
|
||||||
|
// no builder is selected (hotbar hidden). Cooldown polling reads this.
|
||||||
|
private PlayerSpellLoadout displayedSpellLoadout;
|
||||||
private readonly List<SpellSlotUi> spellSlotUis = new List<SpellSlotUi>();
|
private readonly List<SpellSlotUi> spellSlotUis = new List<SpellSlotUi>();
|
||||||
|
|
||||||
private readonly struct SpellSlotUi
|
private readonly struct SpellSlotUi
|
||||||
|
|
@ -419,9 +423,10 @@ namespace TD.UI
|
||||||
title.style.marginBottom = 6;
|
title.style.marginBottom = 6;
|
||||||
card.Add(title);
|
card.Add(title);
|
||||||
|
|
||||||
if (option.Icon != null)
|
var iconSprite = option.ResolveIcon();
|
||||||
|
if (iconSprite != null)
|
||||||
{
|
{
|
||||||
var img = new Image { sprite = option.Icon };
|
var img = new Image { sprite = iconSprite };
|
||||||
img.style.width = 48;
|
img.style.width = 48;
|
||||||
img.style.height = 48;
|
img.style.height = 48;
|
||||||
img.style.marginBottom = 6;
|
img.style.marginBottom = 6;
|
||||||
|
|
@ -475,23 +480,41 @@ namespace TD.UI
|
||||||
|
|
||||||
// ----- Spell hotbar -------------------------------------------------
|
// ----- Spell hotbar -------------------------------------------------
|
||||||
|
|
||||||
// Rebuilds the hotbar cells from the local player's current loadout. Called on
|
// Rebuilds the hotbar cells for the SELECTED builder's loadout. Called on selection
|
||||||
// OnLoadoutChanged (a new spell granted) — spells are append-only, so this is rare,
|
// change and on OnLoadoutChanged (a new spell granted). Spells are append-only, so
|
||||||
// not a per-frame concern.
|
// this is rare, not a per-frame concern.
|
||||||
|
//
|
||||||
|
// The hotbar follows selection: it shows the selected builder's spells and is removed
|
||||||
|
// entirely when no builder is selected (spells belong to the builder). While a builder
|
||||||
|
// IS selected, the frame keeps its reserved width (visibility toggle, not display) so
|
||||||
|
// granting a spell mid-selection doesn't reflow/shift the centered command bar.
|
||||||
private void RebuildSpellHotbar()
|
private void RebuildSpellHotbar()
|
||||||
{
|
{
|
||||||
if (spellHotbar == null) return;
|
if (spellHotbar == null) return;
|
||||||
spellHotbar.Clear();
|
spellHotbar.Clear();
|
||||||
spellSlotUis.Clear();
|
spellSlotUis.Clear();
|
||||||
|
|
||||||
var loadout = PlayerSpellLoadout.Local;
|
var loadout = GetSelectedBuilderLoadout();
|
||||||
int slotCount = loadout?.SlotCount ?? 0;
|
displayedSpellLoadout = loadout;
|
||||||
|
|
||||||
if (spellHotbarFrame != null)
|
if (spellHotbarFrame != null)
|
||||||
spellHotbarFrame.style.display = slotCount > 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
{
|
||||||
|
if (loadout == null)
|
||||||
|
{
|
||||||
|
// No builder selected → remove the slot (the command grid is hidden too).
|
||||||
|
spellHotbarFrame.style.display = DisplayStyle.None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Builder selected → keep the reserved slot in layout; toggle only visibility so
|
||||||
|
// a mid-selection spell grant doesn't jump the command bar.
|
||||||
|
spellHotbarFrame.style.display = DisplayStyle.Flex;
|
||||||
|
spellHotbarFrame.style.visibility =
|
||||||
|
loadout.SlotCount > 0 ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
if (loadout == null) return;
|
if (loadout == null) return;
|
||||||
|
|
||||||
|
int slotCount = loadout.SlotCount;
|
||||||
var layout = SpellHotkeys.Layout;
|
var layout = SpellHotkeys.Layout;
|
||||||
for (int i = 0; i < slotCount; i++)
|
for (int i = 0; i < slotCount; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -531,6 +554,17 @@ namespace TD.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The loadout the spell hotbar should display: the currently-selected builder's
|
||||||
|
// (resolved via its owner, so it shows whichever builder is selected — normally the
|
||||||
|
// local player's), or null when the selection isn't a builder → hotbar hidden.
|
||||||
|
private PlayerSpellLoadout GetSelectedBuilderLoadout()
|
||||||
|
{
|
||||||
|
var selected = SelectionState.Instance?.SelectedObject;
|
||||||
|
if (selected is Builder builder)
|
||||||
|
return PlayerSpellLoadout.GetForClient(builder.OwnerClientId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Per-frame: dims each slot while on cooldown and shows the remaining whole seconds.
|
// Per-frame: dims each slot while on cooldown and shows the remaining whole seconds.
|
||||||
// Cooldown has no change event (it advances continuously with server time), so this
|
// Cooldown has no change event (it advances continuously with server time), so this
|
||||||
// has to be polled — cheap at hotbar scale (at most MaxSpellSlots cells).
|
// has to be polled — cheap at hotbar scale (at most MaxSpellSlots cells).
|
||||||
|
|
@ -538,7 +572,7 @@ namespace TD.UI
|
||||||
{
|
{
|
||||||
if (spellSlotUis.Count == 0) return;
|
if (spellSlotUis.Count == 0) return;
|
||||||
|
|
||||||
var loadout = PlayerSpellLoadout.Local;
|
var loadout = displayedSpellLoadout;
|
||||||
if (loadout == null) return;
|
if (loadout == null) return;
|
||||||
|
|
||||||
for (int i = 0; i < spellSlotUis.Count; i++)
|
for (int i = 0; i < spellSlotUis.Count; i++)
|
||||||
|
|
@ -1403,6 +1437,10 @@ namespace TD.UI
|
||||||
// hides via PopulateGridForSelection when there are no actions.
|
// hides via PopulateGridForSelection when there are no actions.
|
||||||
PopulateInfoPanel(selection);
|
PopulateInfoPanel(selection);
|
||||||
PopulateGridForSelection(selection);
|
PopulateGridForSelection(selection);
|
||||||
|
|
||||||
|
// Section 6 (spell hotbar) follows selection: show the selected builder's spells,
|
||||||
|
// hide entirely when the selection isn't a builder.
|
||||||
|
RebuildSpellHotbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -169,8 +169,11 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-end; /* short sections hug the bottom */
|
align-items: flex-end; /* short sections hug the bottom */
|
||||||
justify-content: flex-start;
|
justify-content: center; /* keep the command bar centered at ANY resolution — the row
|
||||||
padding: 0 100px; /* margin on either side — the X regions */
|
auto-centers, and re-centers symmetrically when the spell
|
||||||
|
hotbar appears/disappears */
|
||||||
|
padding: 0; /* was 0 100px; reclaimed so a smaller reference resolution
|
||||||
|
(= bigger HUD) still fits the bar without clipping */
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
<!-- Section 6: Spell Hotbar (short frame) — builder spells bound to fixed
|
<!-- Section 6: Spell Hotbar (short frame) — builder spells bound to fixed
|
||||||
hotkeys (grant order = slot order). Hidden entirely until the player has
|
hotkeys (grant order = slot order). Hidden entirely until the player has
|
||||||
been granted at least one spell. -->
|
been granted at least one spell. -->
|
||||||
<ui:VisualElement name="spell-hotbar-frame" class="spell-hotbar-frame" style="display: none;">
|
<ui:VisualElement name="spell-hotbar-frame" class="spell-hotbar-frame" style="visibility: hidden;">
|
||||||
<ui:Label text="spells" class="section-label"/>
|
<ui:Label text="spells" class="section-label"/>
|
||||||
<ui:VisualElement name="spell-hotbar" class="spell-hotbar"/>
|
<ui:VisualElement name="spell-hotbar" class="spell-hotbar"/>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ MonoBehaviour:
|
||||||
m_RenderMode: 0
|
m_RenderMode: 0
|
||||||
m_ColliderUpdateMode: 0
|
m_ColliderUpdateMode: 0
|
||||||
m_ColliderIsTrigger: 1
|
m_ColliderIsTrigger: 1
|
||||||
m_ScaleMode: 1
|
m_ScaleMode: 2
|
||||||
m_ReferenceSpritePixelsPerUnit: 100
|
m_ReferenceSpritePixelsPerUnit: 100
|
||||||
m_PixelsPerUnit: 100
|
m_PixelsPerUnit: 100
|
||||||
m_Scale: 1
|
m_Scale: 1
|
||||||
m_ReferenceDpi: 96
|
m_ReferenceDpi: 96
|
||||||
m_FallbackDpi: 96
|
m_FallbackDpi: 96
|
||||||
m_ReferenceResolution: {x: 1200, y: 800}
|
m_ReferenceResolution: {x: 1360, y: 765}
|
||||||
m_ScreenMatchMode: 0
|
m_ScreenMatchMode: 2
|
||||||
m_Match: 0
|
m_Match: 0
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_TargetDisplay: 0
|
m_TargetDisplay: 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue