2.5d test

This commit is contained in:
2026-09-27 21:08:05 +09:00
parent 84dd12414a
commit abd2da2057
201 changed files with 24297 additions and 295 deletions
+1
View File
@@ -18,6 +18,7 @@ MonoBehaviour:
m_RendererData: {fileID: 0} m_RendererData: {fileID: 0}
m_RendererDataList: m_RendererDataList:
- {fileID: 11400000, guid: 424799608f7334c24bf367e4bbfa7f9a, type: 2} - {fileID: 11400000, guid: 424799608f7334c24bf367e4bbfa7f9a, type: 2}
- {fileID: 11400000, guid: 05aadf49be3030546aafc018b71af214, type: 2}
m_DefaultRendererIndex: 0 m_DefaultRendererIndex: 0
m_RequireDepthTexture: 0 m_RequireDepthTexture: 0
m_RequireOpaqueTexture: 0 m_RequireOpaqueTexture: 0
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 323f26e40e9341819b74539ab1205e3a
folderAsset: yes
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 0.45, g: 0.77, b: 0.82, a: 1}
- _Color: {r: 0.45, g: 0.77, b: 0.82, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &6200414664021777617
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 27680ecdcb0d9c4408af43beb753bd64
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: ColliderBoundary
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 0.38, g: 0.71, b: 0.55, a: 1}
- _Color: {r: 0.37999994, g: 0.71, b: 0.54999995, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &7555243765816861683
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 96ad047810b9ae846bc2f54bf2968c31
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
%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: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
m_Name: ComparisonRenderer
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRendererData
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
probeVolumeResources:
probeVolumeDebugShader: {fileID: 0}
probeVolumeFragmentationDebugShader: {fileID: 0}
probeVolumeOffsetDebugShader: {fileID: 0}
probeVolumeSamplingDebugShader: {fileID: 0}
probeSamplingDebugMesh: {fileID: 0}
probeSamplingDebugTexture: {fileID: 0}
probeVolumeBlendStatesCS: {fileID: 0}
m_RendererFeatures: []
m_RendererFeatureMap:
m_UseNativeRenderPass: 0
xrSystemData: {fileID: 0}
postProcessData: {fileID: 0}
m_AssetVersion: 3
m_PrepassLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_TransparentLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_DefaultStencilState:
overrideStencilState: 0
stencilReference: 0
stencilCompareFunction: 8
passOperation: 2
failOperation: 0
zFailOperation: 0
m_ShadowTransparentReceive: 1
m_RenderingMode: 0
m_DepthPrimingMode: 0
m_CopyDepthMode: 1
m_DepthAttachmentFormat: 0
m_DepthTextureFormat: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 05aadf49be3030546aafc018b71af214
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
Shader "BumpCombat/Comparison Sprite Depth"
{
Properties
{
[PerRendererData] _MainTex ("Existing sprite texture", 2D) = "white" {}
_BaseColor ("Tint", Color) = (1,1,1,1)
_Cutoff ("Alpha clip", Range(0,1)) = 0.4
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Source blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Destination blend", Float) = 0
[Toggle] _ZWrite ("Depth write", Float) = 1
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="TransparentCutout" "Queue"="AlphaTest" }
Pass
{
Name "ComparisonUnlit"
Tags { "LightMode"="UniversalForward" }
Cull Off
ZTest LEqual
ZWrite [_ZWrite]
Blend [_SrcBlend] [_DstBlend]
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl"
struct Attributes
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
half4 color : COLOR;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; half4 color : COLOR; };
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
half _Cutoff;
CBUFFER_END
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
SetUpSpriteInstanceProperties();
input.positionOS = UnityFlipSprite(input.positionOS, unity_SpriteProps.xy);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
output.color = input.color;
return output;
}
half4 Frag(Varyings input) : SV_Target
{
half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * input.color * _BaseColor;
clip(color.a - _Cutoff);
return color;
}
ENDHLSL
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4aadfb1143d5401f9af3e4eb53746d77
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-4571542010389989135
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: CycloneHitBoundary
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 0.72, b: 0.18, a: 1}
- _Color: {r: 1, g: 0.72, b: 0.17999998, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eee57eaf919f7b347b8bc513b3804b89
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Ground
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 0.12, g: 0.18, b: 0.2, a: 1}
- _Color: {r: 0.11999995, g: 0.17999998, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &7274897316288786868
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0ab5f733c0b0e8a428f90a7aa1f33d1b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: GroundShadow
m_Shader: {fileID: 4800000, guid: 4aadfb1143d5401f9af3e4eb53746d77, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cutoff: 0.001
- _DstBlend: 10
- _SrcBlend: 5
- _ZWrite: 0
m_Colors:
- _BaseColor: {r: 0, g: 0, b: 0, a: 0.35}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7262b6d429f43844ebd26ac4dfcf2cd6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-8376117332504685327
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Route
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 0.22, g: 0.34, b: 0.31, a: 1}
- _Color: {r: 0.21999997, g: 0.33999997, b: 0.30999997, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 65a6bf5b1c33b80489a703cc8fe91efb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: SpriteCutout
m_Shader: {fileID: 4800000, guid: 4aadfb1143d5401f9af3e4eb53746d77, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords:
- _ZWRITE_ON
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 2450
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cutoff: 0.4
- _DstBlend: 0
- _SrcBlend: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30482bc8080f8594a92573c7e91e7d44
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: SpriteTransparent
m_Shader: {fileID: 4800000, guid: 4aadfb1143d5401f9af3e4eb53746d77, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cutoff: 0.001
- _DstBlend: 10
- _SrcBlend: 5
- _ZWrite: 0
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 0.55}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dfafc04d69db1824191ef8b87cf1295a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Wall
m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _Cull: 2
- _Cutoff: 0.5
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueOffset: 0
- _SampleGI: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 0.48, g: 0.51, b: 0.48, a: 1}
- _Color: {r: 0.47999996, g: 0.50999993, b: 0.47999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &7177300302112874480
MonoBehaviour:
m_ObjectHideFlags: 11
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: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b982e9bfd1eb9ce4c80b628b9b2b6c91
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

@@ -0,0 +1,195 @@
fileFormatVersion: 2
guid: 6d35e75312d3418ea2f58a33521b2ebb
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: Eyeball Monster_Beam-Clean_0
- first:
213: 21300002
second: Eyeball Monster_Beam-Clean_1
- first:
213: 21300004
second: Eyeball Monster_Beam-Clean_2
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: Eyeball Monster_Beam-Clean_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 3004ea3b219640239711f5b7b844b6c8
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Eyeball Monster_Beam-Clean_1
rect:
serializedVersion: 2
x: 100
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: aaf17a191ca741768f84425aa8f5ef0a
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Eyeball Monster_Beam-Clean_2
rect:
serializedVersion: 2
x: 200
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: f78d7efc0c5f4f7289005c5130b031dd
internalID: 21300004
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 3822a7d3d5c64ffe9cd15697e04623c2
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Eyeball Monster_Beam-Clean_0: 21300000
Eyeball Monster_Beam-Clean_1: 21300002
Eyeball Monster_Beam-Clean_2: 21300004
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

@@ -0,0 +1,195 @@
fileFormatVersion: 2
guid: 6fbe849b409d46c682a64fe95eaf2e8a
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: Eyeball Monster_Beam_0
- first:
213: 21300002
second: Eyeball Monster_Beam_1
- first:
213: 21300004
second: Eyeball Monster_Beam_2
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: Eyeball Monster_Beam_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 55045f58609a4086830a0380c190ac68
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Eyeball Monster_Beam_1
rect:
serializedVersion: 2
x: 100
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: ce5cb0d22820462184cee78fe93d0542
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Eyeball Monster_Beam_2
rect:
serializedVersion: 2
x: 200
y: 0
width: 100
height: 100
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: e357145bdc1c4e0cb6379be3ab7a4e10
internalID: 21300004
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 03e7593104da4d908b5a05f6ec08bb41
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Eyeball Monster_Beam_0: 21300000
Eyeball Monster_Beam_1: 21300002
Eyeball Monster_Beam_2: 21300004
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

@@ -0,0 +1,273 @@
fileFormatVersion: 2
guid: 7c04e8afcbb04d1c838e3d7b8ed4f052
TextureImporter:
internalIDToNameTable:
- first:
213: 499323505792861607
second: Blizzard-Wave-v1_0
- first:
213: 634351548535658973
second: Blizzard-Wave-v1_1
- first:
213: 753672340007810633
second: Blizzard-Wave-v1_2
- first:
213: 136483814077234494
second: Blizzard-Wave-v1_3
- first:
213: 237513938985374204
second: Blizzard-Wave-v1_4
- first:
213: 238326498544796154
second: Blizzard-Wave-v1_5
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Blizzard-Wave-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 224300080edb492d88531ed4614fde1f
internalID: 499323505792861607
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Blizzard-Wave-v1_1
rect:
serializedVersion: 2
x: 100
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 1248c85d569747eab70540ccd056a016
internalID: 634351548535658973
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Blizzard-Wave-v1_2
rect:
serializedVersion: 2
x: 200
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 9befe5b709184ebfad67cd6d02081fde
internalID: 753672340007810633
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Blizzard-Wave-v1_3
rect:
serializedVersion: 2
x: 300
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 0b12349417254db2aecdb9f222fb4046
internalID: 136483814077234494
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Blizzard-Wave-v1_4
rect:
serializedVersion: 2
x: 400
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: a36b8f4a1187414994d28eb6ade93b39
internalID: 237513938985374204
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Blizzard-Wave-v1_5
rect:
serializedVersion: 2
x: 500
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: b825c6cd44144880ae70b27a6326cf2d
internalID: 238326498544796154
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: a0051b77a00549349642a6abc5dbeeb7
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Blizzard-Wave-v1_0: 499323505792861607
Blizzard-Wave-v1_1: 634351548535658973
Blizzard-Wave-v1_2: 753672340007810633
Blizzard-Wave-v1_3: 136483814077234494
Blizzard-Wave-v1_4: 237513938985374204
Blizzard-Wave-v1_5: 238326498544796154
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 2617f115eff24ff185a333b7112250a5
TextureImporter:
internalIDToNameTable:
- first:
213: 97093349900010584
second: Shard-Blizzard-v1_0
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Shard-Blizzard-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 100
height: 100
alignment: 9
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: f6f836e9ee53460391035373f7943c1a
internalID: 97093349900010584
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 33e656590cae43ca8d0440f93b760ccb
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Shard-Blizzard-v1_0: 97093349900010584
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -59,12 +59,26 @@ MonoBehaviour:
TowerRange: 5.5 TowerRange: 5.5
DirectTowerDamage: 10 DirectTowerDamage: 10
DirectTowerAttackInterval: 0.75 DirectTowerAttackInterval: 0.75
FireT4BeamSecondTierSeconds: 1
FireT4BeamMaximumTierSeconds: 2
FireT4BeamBaseDamageMultiplier: 0.4
FireT4BeamSecondTierMultiplier: 1.5
FireT4BeamMaximumMultiplier: 2
FireT4BeamResetAfterSeconds: 1
FireT4BeamSecondTierWidthMultiplier: 1.5
FireT4BeamMaximumWidthMultiplier: 2
SummonDamage: 6 SummonDamage: 6
SummonAttackInterval: 0.9 SummonAttackInterval: 0.9
SummonUnitCount: 2 SummonUnitCount: 2
SummonUnitMaximumHealth: 30
SummonUnitCollisionRadius: 0.32
SummonUnitRespawnSeconds: 3
SummonAreaRadius: 2.25 SummonAreaRadius: 2.25
SummonUnitMoveSpeed: 2.5 SummonUnitMoveSpeed: 2.5
SummonAttackRange: 1.2 SummonAttackRange: 1.2
SummonBodySlamImpactFrame: 3
SummonBodySlamContactMargin: 0.06
SummonBodySlamSpeedMultiplier: 2
LightningDamage: 6 LightningDamage: 6
LightningInterval: 1.1 LightningInterval: 1.1
LightningRadius: 1.3 LightningRadius: 1.3
@@ -72,6 +86,8 @@ MonoBehaviour:
ColdInterval: 1.2 ColdInterval: 1.2
ColdSlowFraction: 0.35 ColdSlowFraction: 0.35
ColdSlowSeconds: 1.5 ColdSlowSeconds: 1.5
ColdProjectileBurstInterval: 0.12
FreezeboltProjectileCount: 5
SeismicDamage: 2 SeismicDamage: 2
SeismicInterval: 2 SeismicInterval: 2
SeismicStunSeconds: 0.45 SeismicStunSeconds: 0.45
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9d600a0ef58439f4480db67b07e4d705
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,128 @@
%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: 5366a103ee5d47b79aa3feb1b3ca3f4d, type: 3}
m_Name: ComparisonSettings
m_EditorClassIdentifier: BumpCombat.Runtime::BumpCombat.Constants.Defense25DComparisonSettings
GroundY: 0
MapHalfExtents: {x: 10.5, y: 5.5}
PlayerSpawn: {x: -5, y: -2}
TowerPosition: {x: 1, y: 1.8}
Route:
- {x: -9, y: 0}
- {x: -4, y: 0}
- {x: -2, y: 2.4}
- {x: 4, y: 2.4}
- {x: 9, y: 0}
SummonSlots:
- {x: -0.6, y: -0.9}
- {x: 0.6, y: -0.9}
PortalImmunity: 0
CameraPitch: 50
CameraSize: 6.5
SouthBoundaryRenderHeightFraction: 0.4
PlayerRadius: 0.275
PlayerColliderOffset: {x: 0, y: -0.075}
PlayerVisualScale: 1.25
PlayerFootOffset: -0.29296875
EnemyRadius: 0.275
EnemyColliderOffset: {x: 0, y: -0.35}
EnemyVisualScale: 1.25
EnemyFootOffset: -0.4
EnemyBodyCenterCorrectionPixels: 3
SummonVisualScale: 1.1
SummonFootOffset: -0.30937502
ActorHeight: 0.85
MovementSkin: 0.002
ContactSensorMargin: 0.025
Enemy: {fileID: 11400000, guid: 7f938c46038bd274896aa9402aadad11, type: 2}
EnemyAttackClip: {fileID: 7400000, guid: 9b8ced6bd5e565a4da13186c14ccbfbb, type: 2}
Cyclone: {fileID: 11400000, guid: 0cd054022c7b4781bc8fa2d35121a0de, type: 2}
PlayerArt:
Idle:
- {fileID: 8592238648091824432, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
- {fileID: 1250004922889109162, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
- {fileID: -5028620923284104887, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
- {fileID: 2022252980215292256, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
- {fileID: -2599092321403200321, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
- {fileID: 5488730835254470119, guid: cf5ee5ee251612148a5a3cceb77e3e09, type: 3}
Walk:
- {fileID: -510655448263928137, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: 3479404773181816430, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: -2283166651885375184, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: 3210028164496969578, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: -2276603239645387600, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: 4573818798471642851, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: 8383016689336500281, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
- {fileID: 4467538224481176286, guid: 6f1776132693af24c9999cf2e7c518c7, type: 3}
Attack:
- {fileID: 21300000, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
- {fileID: 21300001, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
- {fileID: 21300002, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
- {fileID: 21300003, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
- {fileID: 21300004, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
- {fileID: 21300005, guid: 6c25effeb9ec57a4a20f65d23369edad, type: 3}
Hurt:
- {fileID: 457371520115034197, guid: 5940cfeb72cce444fb5a7bf7c5373eca, type: 3}
- {fileID: 3404584728210486745, guid: 5940cfeb72cce444fb5a7bf7c5373eca, type: 3}
- {fileID: 7129331961366924758, guid: 5940cfeb72cce444fb5a7bf7c5373eca, type: 3}
- {fileID: 8197722601435448183, guid: 5940cfeb72cce444fb5a7bf7c5373eca, type: 3}
- {fileID: 7704259262516875320, guid: 5940cfeb72cce444fb5a7bf7c5373eca, type: 3}
Death:
- {fileID: 2811593804088240790, guid: 70b33e6f7545e6f47a46385d74aaaf83, type: 3}
- {fileID: -7828097450302239583, guid: 70b33e6f7545e6f47a46385d74aaaf83, type: 3}
- {fileID: 361189222234674287, guid: 70b33e6f7545e6f47a46385d74aaaf83, type: 3}
- {fileID: 9116452327212125477, guid: 70b33e6f7545e6f47a46385d74aaaf83, type: 3}
EnemyArt:
Idle:
- {fileID: 8488328654572304352, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
- {fileID: 3789744679490320462, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
- {fileID: 2240755264892537048, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
- {fileID: 2034087598805426463, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
- {fileID: 8271520994771697968, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
- {fileID: 5978591873814285882, guid: 07f9aef17c7d24e4399f66b931c1c71f, type: 3}
Walk:
- {fileID: -319509429510461966, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: -5704084491819767634, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: 8477824621842717129, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: 3051669912371892857, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: 7539542233003775106, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: 2788143195985776107, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: -119383558338234881, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
- {fileID: 1567333206898370016, guid: dbe397424a57ec34991241bdec4068bf, type: 3}
Attack:
- {fileID: -4931541286253664743, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
- {fileID: 6703666654972841543, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
- {fileID: 5374936872560645210, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
- {fileID: -5799585484308707375, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
- {fileID: 4528416531530387699, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
- {fileID: 3317778627300228566, guid: 017709d2bd1cb5d489eb197051a78427, type: 3}
Hurt:
- {fileID: 933120629603562079, guid: 142640e0a095a714c82d152b666b9884, type: 3}
- {fileID: -494057186547109401, guid: 142640e0a095a714c82d152b666b9884, type: 3}
- {fileID: 2785721821212994034, guid: 142640e0a095a714c82d152b666b9884, type: 3}
- {fileID: -5188259159075104939, guid: 142640e0a095a714c82d152b666b9884, type: 3}
Death:
- {fileID: 8610279347594985942, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
- {fileID: -3134359789022687535, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
- {fileID: 8399632352712144361, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
- {fileID: -2072202752667699138, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
- {fileID: 7143198583629916187, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
- {fileID: 4093069291906818420, guid: 7d5e7c2c790e42346a9f2ebbf23461e4, type: 3}
CutoutMaterial: {fileID: 2100000, guid: 30482bc8080f8594a92573c7e91e7d44, type: 2}
TransparentMaterial: {fileID: 2100000, guid: dfafc04d69db1824191ef8b87cf1295a, type: 2}
GroundMaterial: {fileID: 2100000, guid: 0ab5f733c0b0e8a428f90a7aa1f33d1b, type: 2}
WallMaterial: {fileID: 2100000, guid: b982e9bfd1eb9ce4c80b628b9b2b6c91, type: 2}
PathMaterial: {fileID: 2100000, guid: 65a6bf5b1c33b80489a703cc8fe91efb, type: 2}
ShadowMaterial: {fileID: 2100000, guid: 7262b6d429f43844ebd26ac4dfcf2cd6, type: 2}
BaseMaterial: {fileID: 2100000, guid: 27680ecdcb0d9c4408af43beb753bd64, type: 2}
BoundaryMaterial: {fileID: 2100000, guid: 96ad047810b9ae846bc2f54bf2968c31, type: 2}
HitBoundaryMaterial: {fileID: 2100000, guid: eee57eaf919f7b347b8bc513b3804b89, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fc0c99c602cf67042a4305ccbdcc8c2f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

@@ -0,0 +1,221 @@
fileFormatVersion: 2
guid: daaafe9016d35f219b2a03842f2b90cc
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: TowerImpact-Cold-Clean-v1_0
- first:
213: 21300001
second: TowerImpact-Cold-Clean-v1_1
- first:
213: 21300002
second: TowerImpact-Cold-Clean-v1_2
- first:
213: 21300003
second: TowerImpact-Cold-Clean-v1_3
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: TowerImpact-Cold-Clean-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 62887aabfb2455d6bf9ba33d6ab6ee61
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Cold-Clean-v1_1
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 1ef8844679b85eb38e016775dabbeb5c
internalID: 21300001
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Cold-Clean-v1_2
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: df4addd380a154b4ac0aede7503160b3
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Cold-Clean-v1_3
rect:
serializedVersion: 2
x: 192
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 2114fea9861751a0a42fd244ace547fd
internalID: 21300003
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: b4ebc1971c8c555ba6bce87e94485543
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TowerImpact-Cold-Clean-v1_0: 21300000
TowerImpact-Cold-Clean-v1_1: 21300001
TowerImpact-Cold-Clean-v1_2: 21300002
TowerImpact-Cold-Clean-v1_3: 21300003
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

@@ -0,0 +1,221 @@
fileFormatVersion: 2
guid: 136d80cc377d54c59d4cab2156b087c0
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: TowerImpact-Direct-Clean-v1_0
- first:
213: 21300001
second: TowerImpact-Direct-Clean-v1_1
- first:
213: 21300002
second: TowerImpact-Direct-Clean-v1_2
- first:
213: 21300003
second: TowerImpact-Direct-Clean-v1_3
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: TowerImpact-Direct-Clean-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 48954119e7eb5350add1264f92577ff3
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Direct-Clean-v1_1
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 6969f47957f454d78c21068a664e7911
internalID: 21300001
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Direct-Clean-v1_2
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: efb88da9b27c593890a8c47611dc8457
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Direct-Clean-v1_3
rect:
serializedVersion: 2
x: 192
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 72ac250e7eec588db4555181fd91dd57
internalID: 21300003
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 21ab6e30d13251e19433ff928affccb8
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TowerImpact-Direct-Clean-v1_0: 21300000
TowerImpact-Direct-Clean-v1_1: 21300001
TowerImpact-Direct-Clean-v1_2: 21300002
TowerImpact-Direct-Clean-v1_3: 21300003
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1,221 @@
fileFormatVersion: 2
guid: 1a71b5120f2055fea4503f9620cf7d26
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: TowerImpact-Lightning-Clean-v1_0
- first:
213: 21300001
second: TowerImpact-Lightning-Clean-v1_1
- first:
213: 21300002
second: TowerImpact-Lightning-Clean-v1_2
- first:
213: 21300003
second: TowerImpact-Lightning-Clean-v1_3
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: TowerImpact-Lightning-Clean-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: cd9ac527673f5338bdc9dccb1e306861
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Lightning-Clean-v1_1
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 34dea063251f5665aa3d45a5878ffa9d
internalID: 21300001
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Lightning-Clean-v1_2
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 7e65f69e56405469811e382ff9dacd41
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Lightning-Clean-v1_3
rect:
serializedVersion: 2
x: 192
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 956bbc5f98175616893445c3b4599b23
internalID: 21300003
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: b9d5f7a5978651938d2a349901de74b6
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TowerImpact-Lightning-Clean-v1_0: 21300000
TowerImpact-Lightning-Clean-v1_1: 21300001
TowerImpact-Lightning-Clean-v1_2: 21300002
TowerImpact-Lightning-Clean-v1_3: 21300003
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

@@ -0,0 +1,221 @@
fileFormatVersion: 2
guid: be2fa36cda1f59498a70c2eca27cff28
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: TowerImpact-Seismic-Clean-v1_0
- first:
213: 21300001
second: TowerImpact-Seismic-Clean-v1_1
- first:
213: 21300002
second: TowerImpact-Seismic-Clean-v1_2
- first:
213: 21300003
second: TowerImpact-Seismic-Clean-v1_3
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: TowerImpact-Seismic-Clean-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 9708f16c72a05b94b510c07e866e9269
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Clean-v1_1
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 60535391499d593eb24ee76f37793b0d
internalID: 21300001
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Clean-v1_2
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 875de6a3cda757c786c7d8e63a69b0e7
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Clean-v1_3
rect:
serializedVersion: 2
x: 192
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: 7905bdb7ff9e592a9933c01c744b578a
internalID: 21300003
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 1201601e4d9c55189882aaed409487e7
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TowerImpact-Seismic-Clean-v1_0: 21300000
TowerImpact-Seismic-Clean-v1_1: 21300001
TowerImpact-Seismic-Clean-v1_2: 21300002
TowerImpact-Seismic-Clean-v1_3: 21300003
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

@@ -0,0 +1,221 @@
fileFormatVersion: 2
guid: 7246d13b8b525c719cb6e7a2e3fef6f2
TextureImporter:
internalIDToNameTable:
- first:
213: 21300000
second: TowerImpact-Seismic-Normal-v1_0
- first:
213: 21300001
second: TowerImpact-Seismic-Normal-v1_1
- first:
213: 21300002
second: TowerImpact-Seismic-Normal-v1_2
- first:
213: 21300003
second: TowerImpact-Seismic-Normal-v1_3
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: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 32
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: 0
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:
- serializedVersion: 2
name: TowerImpact-Seismic-Normal-v1_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: fefcecbb2c555d6c977e3582d697f9ca
internalID: 21300000
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Normal-v1_1
rect:
serializedVersion: 2
x: 64
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: b7aefd876ef65b27bacaef971ecc6c57
internalID: 21300001
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Normal-v1_2
rect:
serializedVersion: 2
x: 128
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: adc350e7b0dd5b95b944af14ea921d9b
internalID: 21300002
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: TowerImpact-Seismic-Normal-v1_3
rect:
serializedVersion: 2
x: 192
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: e55b0e212cf0547bbc9f8b19f51b228e
internalID: 21300003
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 75d8c3fb144e5c81887a154320c7f74f
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TowerImpact-Seismic-Normal-v1_0: 21300000
TowerImpact-Seismic-Normal-v1_1: 21300001
TowerImpact-Seismic-Normal-v1_2: 21300002
TowerImpact-Seismic-Normal-v1_3: 21300003
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c0496bc9cbfa2af48bbfa51b8b378ee2
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -4,6 +4,7 @@ using BumpCombat.Core;
using BumpCombat.Constants; using BumpCombat.Constants;
using BumpCombat.Enemies; using BumpCombat.Enemies;
using BumpCombat.Player; using BumpCombat.Player;
using BumpCombat.Defense;
using BumpCombat.UI; using BumpCombat.UI;
using UnityEngine; using UnityEngine;
@@ -81,6 +82,7 @@ namespace BumpCombat.Combat
private readonly HashSet<GameObject> blockedBumpVisuals = new(); private readonly HashSet<GameObject> blockedBumpVisuals = new();
private readonly HashSet<GameObject> guardBlockImpactVisuals = new(); private readonly HashSet<GameObject> guardBlockImpactVisuals = new();
private readonly HashSet<GameObject> artifactImpactVisuals = new(); private readonly HashSet<GameObject> artifactImpactVisuals = new();
private readonly HashSet<GameObject> towerImpactVisuals = new();
private readonly HashSet<GameObject> playerHurtImpactVisuals = new(); private readonly HashSet<GameObject> playerHurtImpactVisuals = new();
private readonly Dictionary<string, Sprite[]> artifactImpactFrameCache = new(); private readonly Dictionary<string, Sprite[]> artifactImpactFrameCache = new();
private SpriteRenderer playerRenderer; private SpriteRenderer playerRenderer;
@@ -129,6 +131,7 @@ namespace BumpCombat.Combat
public int ActiveBlockedBumpVisualCount => blockedBumpVisuals.Count; public int ActiveBlockedBumpVisualCount => blockedBumpVisuals.Count;
public int ActiveGuardBlockImpactVisualCount => guardBlockImpactVisuals.Count; public int ActiveGuardBlockImpactVisualCount => guardBlockImpactVisuals.Count;
public int ActiveArtifactImpactVisualCount => artifactImpactVisuals.Count; public int ActiveArtifactImpactVisualCount => artifactImpactVisuals.Count;
public int ActiveTowerImpactVisualCount => towerImpactVisuals.Count;
public int ActivePlayerHurtImpactVisualCount => playerHurtImpactVisuals.Count; public int ActivePlayerHurtImpactVisualCount => playerHurtImpactVisuals.Count;
public static string GetPlayerHurtImpactResourcePath() public static string GetPlayerHurtImpactResourcePath()
@@ -189,6 +192,69 @@ namespace BumpCombat.Combat
+ "-v1"; + "-v1";
} }
public static string GetTowerImpactResourcePath(DefenseTowerKind kind)
{
return kind switch
{
DefenseTowerKind.Direct => "Combat/TowerImpact-Direct-Clean-v1",
DefenseTowerKind.Summon => null,
DefenseTowerKind.Lightning => "Combat/TowerImpact-Lightning-Clean-v1",
DefenseTowerKind.Cold => "Combat/TowerImpact-Cold-Clean-v1",
DefenseTowerKind.Seismic => "Combat/TowerImpact-Seismic-Clean-v1",
_ => null,
};
}
public static void ShowTowerImpact(
DefenseTowerKind kind, EnemyController target, Vector2 hitPosition)
{
CombatFeedback feedback = GetActive();
if (feedback != null && feedback.isActiveAndEnabled)
{
feedback.CreateTowerImpact(kind, target, hitPosition);
}
}
private void CreateTowerImpact(
DefenseTowerKind kind, EnemyController target, Vector2 hitPosition)
{
string resourcePath = GetTowerImpactResourcePath(kind);
if (string.IsNullOrEmpty(resourcePath))
{
return;
}
Sprite[] frames = LoadBumpImpactFrames(resourcePath);
if (frames == null || frames.Length != BumpImpactFrameCount)
{
return;
}
GameObject root = new($"Tower Impact {kind}");
root.transform.localScale = Vector3.one * 2f;
root.transform.position = hitPosition;
SpriteRenderer renderer = root.AddComponent<SpriteRenderer>();
renderer.sprite = frames[0];
renderer.color = Color.white;
SpriteRenderer targetRenderer = target != null
? target.GetComponent<SpriteRenderer>() : null;
if (targetRenderer != null)
{
renderer.sortingLayerID = targetRenderer.sortingLayerID;
renderer.sortingOrder = targetRenderer.sortingOrder
+ DefenseTowerArtBounds.TowerHitSortingOffset;
}
else if (playerRenderer != null)
{
renderer.sortingLayerID = playerRenderer.sortingLayerID;
renderer.sortingOrder = playerRenderer.sortingOrder
+ DefenseTowerArtBounds.TowerHitSortingOffset;
}
RegisterTransient(root, false);
towerImpactVisuals.Add(root);
StartCoroutine(AnimateArtifactImpact(root, renderer, frames));
}
public static float GetArtifactImpactFrameDuration(int frameIndex) public static float GetArtifactImpactFrameDuration(int frameIndex)
{ {
return GetBumpImpactFrameDuration(frameIndex); return GetBumpImpactFrameDuration(frameIndex);
@@ -526,6 +592,7 @@ namespace BumpCombat.Combat
ClearBlockedBumpVisuals(); ClearBlockedBumpVisuals();
ClearGuardBlockImpactVisuals(); ClearGuardBlockImpactVisuals();
ClearArtifactImpactVisuals(); ClearArtifactImpactVisuals();
ClearTowerImpactVisuals();
ClearPlayerHurtImpactVisuals(); ClearPlayerHurtImpactVisuals();
ClearCycloneAfterimages(); ClearCycloneAfterimages();
} }
@@ -1894,6 +1961,22 @@ namespace BumpCombat.Combat
artifactImpactVisuals.Clear(); artifactImpactVisuals.Clear();
} }
private void ClearTowerImpactVisuals()
{
if (towerImpactVisuals.Count == 0)
{
return;
}
GameObject[] visuals = new GameObject[towerImpactVisuals.Count];
towerImpactVisuals.CopyTo(visuals);
foreach (GameObject visual in visuals)
{
DestroyTransient(visual);
}
towerImpactVisuals.Clear();
}
private void ClearPlayerHurtImpactVisuals() private void ClearPlayerHurtImpactVisuals()
{ {
if (playerHurtImpactVisuals.Count == 0) if (playerHurtImpactVisuals.Count == 0)
@@ -2398,6 +2481,7 @@ namespace BumpCombat.Combat
blockedBumpVisuals.RemoveWhere(visual => visual == null); blockedBumpVisuals.RemoveWhere(visual => visual == null);
guardBlockImpactVisuals.RemoveWhere(visual => visual == null); guardBlockImpactVisuals.RemoveWhere(visual => visual == null);
artifactImpactVisuals.RemoveWhere(visual => visual == null); artifactImpactVisuals.RemoveWhere(visual => visual == null);
towerImpactVisuals.RemoveWhere(visual => visual == null);
playerHurtImpactVisuals.RemoveWhere(visual => visual == null); playerHurtImpactVisuals.RemoveWhere(visual => visual == null);
return; return;
} }
@@ -2406,6 +2490,7 @@ namespace BumpCombat.Combat
blockedBumpVisuals.Remove(transientObject); blockedBumpVisuals.Remove(transientObject);
guardBlockImpactVisuals.Remove(transientObject); guardBlockImpactVisuals.Remove(transientObject);
artifactImpactVisuals.Remove(transientObject); artifactImpactVisuals.Remove(transientObject);
towerImpactVisuals.Remove(transientObject);
playerHurtImpactVisuals.Remove(transientObject); playerHurtImpactVisuals.Remove(transientObject);
int instanceId = transientObject.GetInstanceID(); int instanceId = transientObject.GetInstanceID();
activeTransientIds.Remove(instanceId); activeTransientIds.Remove(instanceId);
@@ -0,0 +1,47 @@
using System;
using UnityEngine;
namespace BumpCombat.Combat
{
// Authored ground geometry shared by the XY controller and XZ comparison.
// Sprite frames and decoration consume elapsed time independently of damage ticks.
public static class CycloneGeometry
{
public const float PixelsPerUnit = 32f;
public const float ShadowLocalY = -7.5f / PixelsPerUnit;
public const float CenterOffsetY = 5f / PixelsPerUnit;
public const float FrameSeconds = .05f;
public static float VisualScale(float range, bool charged) => range / (charged ? 1.25f : .85f);
public static Vector2 Radii(float range, bool charged) =>
(charged ? new Vector2(46f, 18f) : new Vector2(38.0625f, 15.03125f))
/ PixelsPerUnit * VisualScale(range, charged);
public static Vector2 CenterOffset(float range, bool charged, float rootScaleY) =>
Vector2.up * (ShadowLocalY * rootScaleY + CenterOffsetY * VisualScale(range, charged));
public static int ScheduledHitCount(float duration, float interval) =>
duration <= 0f || interval <= 0f ? 1
: Mathf.Max(1, Mathf.FloorToInt((duration + .0001f) / interval) + 1);
public static float NormalizedDistance(Vector2 offset, Vector2 radii) =>
offset.x * offset.x / (radii.x * radii.x)
+ offset.y * offset.y / (radii.y * radii.y);
// Keep the original closest-point and 64 boundary samples, including tolerances.
// The adapter supplies Collider2D.ClosestPoint or an XZ circular footprint.
public static bool Overlaps(Vector2 center, Vector2 radii, Func<Vector2, Vector2> closestPoint)
{
if (NormalizedDistance(closestPoint(center) - center, radii) <= 1.0001f) return true;
const int samples = 64;
for (int i = 0; i < samples; i++)
{
float angle = Mathf.PI * 2f * i / samples;
Vector2 point = center + new Vector2(Mathf.Cos(angle) * radii.x, Mathf.Sin(angle) * radii.y);
if ((closestPoint(point) - point).sqrMagnitude <= .000001f) return true;
}
return false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0a2c269eb2424df68bfbd7d08ffee36d
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Player;
namespace BumpCombat.Constants
{
/// <summary>Existing authored feedback timings and paths used by the 2.5D presentation.</summary>
public static class Defense25DCombatFeedbackConstants
{
public const string ChargeResourceRoot = "Artifacts/ThreeColor-v1/Charge/";
public const string ChargeAuraSuffix = "-Aura-v1";
public const string ChargeReadyAuraSuffix = "-ReadyAura-v1";
public const string ChargeFlashSuffix = "-Flash-v1";
public const string ChargeReleaseSuffix = "-Release-v1";
public const string BlockedBumpResource = "Combat/BlockedBump-Wizard-v1";
public const float PulseGatherDuration = .1f;
public const float InteractionStreakDuration = .11f;
public static IReadOnlyList<float> BumpFrames { get; } = new[]
{
CombatFeedback.GetBumpImpactFrameDuration(0),
CombatFeedback.GetBumpImpactFrameDuration(1),
CombatFeedback.GetBumpImpactFrameDuration(2),
CombatFeedback.GetBumpImpactFrameDuration(3),
};
public static IReadOnlyList<float> ArtifactFrames { get; } = new[]
{
CombatFeedback.GetArtifactImpactFrameDuration(0),
CombatFeedback.GetArtifactImpactFrameDuration(1),
CombatFeedback.GetArtifactImpactFrameDuration(2),
CombatFeedback.GetArtifactImpactFrameDuration(3),
};
public static IReadOnlyList<float> PlayerHurtFrames { get; } = new[]
{
CombatFeedback.GetPlayerHurtImpactFrameDuration(0),
CombatFeedback.GetPlayerHurtImpactFrameDuration(1),
CombatFeedback.GetPlayerHurtImpactFrameDuration(2),
};
public static IReadOnlyList<float> ChargeAuraFrames { get; } = new[]
{ .06f, .06f, .06f, .06f, .06f, .06f };
public static IReadOnlyList<float> ChargeFlashFrames { get; } = new[]
{ .03f, .04f, .04f, .03f };
public static IReadOnlyList<float> ChargeReleaseFrames { get; } = new[]
{ .03f, .04f, .04f, .04f, .04f, .05f };
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c2a7338f9d1144eb9ab4bc9b7d29c26f
@@ -0,0 +1,25 @@
using UnityEngine;
namespace BumpCombat.Constants
{
public static class Defense25DCombatPopupConstants
{
public const int MaximumConcurrentPopups = 12;
public const float LifetimeSeconds = 0.6f;
public const float RisePixelsAt1080p = 40f;
public const float ReferenceScreenHeight = 1080f;
public const float ReferenceInternalHeight = 360f;
public const float FontPixelsAt1080p = 30f;
public const float OutlinePixels = 1f;
public const float PopupWidthInternal = 100f;
public const float PopupHeightInternal = 20f;
public static readonly Color PlayerDamageColor =
new(244f / 255f, 117f / 255f, 112f / 255f, 1f);
public static readonly Color LaunchColor = new(0.95f, 0.55f, 1f);
public static readonly Color BackHitColor = new(1f, 0.75f, 0.2f);
public static readonly Color SideHitColor = new(0.65f, 1f, 0.3f);
public static readonly Color OutlineColor = new(18f / 255f, 23f / 255f, 34f / 255f,
230f / 255f);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bdb0ea9afc64f17a25d84d1cf8a7a31
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
using System;
using BumpCombat.Defense;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Constants
{
[Serializable]
public sealed class ComparisonSpriteFrames
{
public Sprite[] Idle = Array.Empty<Sprite>();
public Sprite[] Walk = Array.Empty<Sprite>();
public Sprite[] Attack = Array.Empty<Sprite>();
public Sprite[] Hurt = Array.Empty<Sprite>();
public Sprite[] Death = Array.Empty<Sprite>();
}
[CreateAssetMenu(menuName = "BumpCombat/Constants/Defense 2.5D Comparison")]
public sealed class Defense25DComparisonSettings : ScriptableObject
{
public const float SummonBodyPixelsPerUnit = 32f;
public const float SlimeBodyCoreDiameterPixels = 11f;
public const float GiantSlimeBodyCoreDiameterPixels = 22f;
public const float LavaSlimeBodyCoreDiameterPixels = 13f;
public static readonly Vector2 SlimeBodyCenterFromFootPixels = new(-1.5f, 7.5f);
public static readonly Vector2 GiantSlimeBodyCenterFromFootPixels = new(-3f, 17f);
public static readonly Vector2 LavaSlimeBodyCenterFromFootPixels = new(-2f, 8.5f);
[Header("Independent comparison layout; not final balance")]
public float GroundY;
public Vector2 MapHalfExtents = new(10.5f, 5.5f);
public Vector2 PlayerSpawn = new(-5f, -2f);
public Vector2 TowerPosition = new(1f, 1.8f);
public Vector2[] Route = { new(-9f, 0f), new(-4f, 0f), new(-2f, 2.4f), new(4f, 2.4f), new(9f, 0f) };
public Vector2[] SummonSlots = { new(-.6f, -.9f), new(.6f, -.9f) };
public DefenseTowerKind PortalImmunity = DefenseTowerKind.Direct;
public float CameraPitch = 50f;
public float CameraSize = 6.5f;
[Range(.1f, 1f)] public float SouthBoundaryRenderHeightFraction = .4f;
[Header("XZ footprints baked from existing prefab XY colliders")]
public float PlayerRadius = .275f;
public Vector2 PlayerColliderOffset = new(0f, -.075f);
public float PlayerVisualScale = 1.25f;
public float PlayerFootOffset = -7.5f / 32f * 1.25f;
public float EnemyRadius = .275f;
public Vector2 EnemyColliderOffset = new(0f, -.35f);
public float EnemyVisualScale = 1.25f;
public float EnemyFootOffset = -.4f;
public float EnemyBodyCenterCorrectionPixels = 3f;
public float SummonVisualScale = 1.1f;
public float SummonFootOffset = -9f / 32f * 1.1f;
public float ActorHeight = .85f;
public float MovementSkin = .002f;
public float ContactSensorMargin = .025f;
// Unnamed, otherwise unused layers in the baseline project; no TagManager edits.
public const int TerrainLayer = 9;
public const int ActorLayer = 10;
public const int TerrainMask = 1 << TerrainLayer;
public const int ActorMask = 1 << ActorLayer;
[Header("Existing definitions and art; originals are not edited")]
public EnemyDefinition Enemy;
public AnimationClip EnemyAttackClip;
public ActiveArtifactDefinition Cyclone;
public ComparisonSpriteFrames PlayerArt = new();
public ComparisonSpriteFrames EnemyArt = new();
public Material CutoutMaterial;
public Material TransparentMaterial;
public Material GroundMaterial;
public Material WallMaterial;
public Material PathMaterial;
public Material ShadowMaterial;
public Material BaseMaterial;
public Material BoundaryMaterial;
public Material HitBoundaryMaterial;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5366a103ee5d47b79aa3feb1b3ca3f4d
@@ -0,0 +1,15 @@
namespace BumpCombat.Constants
{
/// <summary>Sample cadence copied from the authored Swordsman animation clips.</summary>
public static class Defense25DPlayerAnimationConstants
{
public const float IdleFrameSeconds = 1f / 8f;
public const float WalkFrameSeconds = 1f / 10f;
public const float HurtFrameSeconds = 1f / 10f;
public const float DeathFrameSeconds = 1f / 8f;
public const float DashFrameSeconds = 1f / 60f;
public const float BumpFrameSeconds = 1f / 25f;
public const float ArtifactUseFrameSeconds = 1f / 20f;
public const float NonPlayerDeathFallbackDuration = 0.5f;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0ee47e8d5cf8ceb40bf54520dc0485a1
@@ -72,12 +72,26 @@ namespace BumpCombat.Constants
[Min(0.1f)] public float TowerRange = 5.5f; [Min(0.1f)] public float TowerRange = 5.5f;
[Min(0.1f)] public float DirectTowerDamage = 10f; [Min(0.1f)] public float DirectTowerDamage = 10f;
[Min(0.05f)] public float DirectTowerAttackInterval = 0.75f; [Min(0.05f)] public float DirectTowerAttackInterval = 0.75f;
[Min(0f)] public float FireT4BeamSecondTierSeconds = 1f;
[Min(0f)] public float FireT4BeamMaximumTierSeconds = 2f;
[Min(0f)] public float FireT4BeamBaseDamageMultiplier = 0.4f;
[Min(1f)] public float FireT4BeamSecondTierMultiplier = 1.5f;
[Min(1f)] public float FireT4BeamMaximumMultiplier = 2f;
[Min(0f)] public float FireT4BeamResetAfterSeconds = 1f;
[Min(1f)] public float FireT4BeamSecondTierWidthMultiplier = 1.5f;
[Min(1f)] public float FireT4BeamMaximumWidthMultiplier = 2f;
[Min(0.1f)] public float SummonDamage = 6f; [Min(0.1f)] public float SummonDamage = 6f;
[Min(0.05f)] public float SummonAttackInterval = 0.9f; [Min(0.05f)] public float SummonAttackInterval = 0.9f;
[Min(1)] public int SummonUnitCount = 2; [Min(1)] public int SummonUnitCount = 2;
[Min(1f)] public float SummonUnitMaximumHealth = 30f;
[Min(0.05f)] public float SummonUnitCollisionRadius = 0.32f;
[Min(0f)] public float SummonUnitRespawnSeconds = 3f;
[Min(0.1f)] public float SummonAreaRadius = 2.25f; [Min(0.1f)] public float SummonAreaRadius = 2.25f;
[Min(0.1f)] public float SummonUnitMoveSpeed = 2.5f; [Min(0.1f)] public float SummonUnitMoveSpeed = 2.5f;
[Min(0.1f)] public float SummonAttackRange = 1.2f; [Min(0.1f)] public float SummonAttackRange = 1.2f;
[Range(0, 5)] public int SummonBodySlamImpactFrame = 3;
[Min(0f)] public float SummonBodySlamContactMargin = 0.06f;
[Min(1f)] public float SummonBodySlamSpeedMultiplier = 2f;
[Header("Six tower family comparison values")] [Header("Six tower family comparison values")]
[Min(0.1f)] public float LightningDamage = 6f; [Min(0.1f)] public float LightningDamage = 6f;
[Min(0.05f)] public float LightningInterval = 1.1f; [Min(0.05f)] public float LightningInterval = 1.1f;
@@ -86,6 +100,8 @@ namespace BumpCombat.Constants
[Min(0.05f)] public float ColdInterval = 1.2f; [Min(0.05f)] public float ColdInterval = 1.2f;
[Min(0f)] public float ColdSlowFraction = 0.35f; [Min(0f)] public float ColdSlowFraction = 0.35f;
[Min(0f)] public float ColdSlowSeconds = 1.5f; [Min(0f)] public float ColdSlowSeconds = 1.5f;
[Min(0.01f)] public float ColdProjectileBurstInterval = 0.12f;
[Min(1)] public int FreezeboltProjectileCount = 5;
[Min(0.1f)] public float SeismicDamage = 2f; [Min(0.1f)] public float SeismicDamage = 2f;
[Min(0.05f)] public float SeismicInterval = 2f; [Min(0.05f)] public float SeismicInterval = 2f;
[Min(0f)] public float SeismicStunSeconds = 0.45f; [Min(0f)] public float SeismicStunSeconds = 0.45f;
@@ -156,4 +172,41 @@ namespace BumpCombat.Constants
: fallback; : fallback;
} }
} }
public static class DefenseTowerHitTestLayout
{
public static readonly DefenseTowerKind[] KindsByPad =
{
DefenseTowerKind.Direct,
DefenseTowerKind.Summon,
DefenseTowerKind.Cold,
DefenseTowerKind.Lightning,
DefenseTowerKind.Empowering,
DefenseTowerKind.Seismic,
};
public static readonly Vector2[] DummyPositionsByPad =
{
new(-2.25f, 2.95f),
new(3.95f, -3.2f),
new(-4.9f, -1.4f),
new(4.9f, 1.4f),
new(1f, 2.5f),
new(0f, -1.4f),
};
public static Vector2 GetDummyPosition(DefenseTowerKind kind)
{
for (int i = 0; i < KindsByPad.Length; i++)
{
if (KindsByPad[i] == kind)
{
return DummyPositionsByPad[i];
}
}
return Vector2.zero;
}
}
} }
@@ -96,11 +96,13 @@ namespace BumpCombat.Defense
["Effects/Fire/Meteor-Impact"] = new[] { 40, 50, 60, 70, 60, 60, 60, 80 }, ["Effects/Fire/Meteor-Impact"] = new[] { 40, 50, 60, 70, 60, 60, 60, 80 },
["Sources/BlackKnight-NoSkull"] = new[] { 125, 125, 125, 125, 125, 125, 125 }, ["Sources/BlackKnight-NoSkull"] = new[] { 125, 125, 125, 125, 125, 125, 125 },
["Effects/Fire/Flamethrower"] = new[] { 80, 80, 80, 70, 65, 65, 70 }, ["Effects/Fire/Flamethrower"] = new[] { 80, 80, 80, 70, 65, 65, 70 },
["Effects/Fire/Eyeball Monster_Beam-Clean"] = new[] { 100, 100, 120 },
["Effects/Lightning/T1-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 }, ["Effects/Lightning/T1-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 },
["Effects/Lightning/T2-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 }, ["Effects/Lightning/T2-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 },
["Effects/Lightning/T3-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 }, ["Effects/Lightning/T3-Bolt"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 },
["Effects/Lightning/Arc"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 }, ["Effects/Lightning/Arc"] = new[] { 30, 40, 30, 30, 40, 40, 40, 50 },
["Effects/Lightning/ImpactWave"] = new[] { 40, 40, 50, 50, 60, 60 }, ["Effects/Lightning/ImpactWave"] = new[] { 40, 40, 50, 50, 60, 60 },
["Effects/Ice/Blizzard-Wave-v1"] = new[] { 40, 40, 50, 50, 60, 60 },
["Effects/Lightning/LightningStorm"] = new[] { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 }, ["Effects/Lightning/LightningStorm"] = new[] { 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 },
["Effects/Ice/T1-Volley"] = new[] { 125 }, ["Effects/Ice/T1-Volley"] = new[] { 125 },
["Effects/Ice/T2-Volley"] = new[] { 125 }, ["Effects/Ice/T2-Volley"] = new[] { 125 },
@@ -167,6 +169,58 @@ namespace BumpCombat.Constants
/// <summary>Offline pixel measurements for non-readable defense art and original shadows.</summary> /// <summary>Offline pixel measurements for non-readable defense art and original shadows.</summary>
public static class DefenseTowerArtBounds public static class DefenseTowerArtBounds
{ {
public const float TowerBillboardScale = 0.72f * 2.1f;
public static readonly Vector2 LightningStormGroundCenterPixels = new(61f, 34f);
public const float LightningStormGroundRadiusPixels = 55.362442f;
public const float SeismicGroundMarkerWidthPixels = 18f;
public const int FireBeamStartX = 1;
public const int FireBeamEndX = 99;
public const float FireBeamCenterY = 49.5f;
public const float FireBeamIgnitionSeconds = 0.1f;
public const float FireBeamExtinguishSeconds = 0.12f;
public const float FireT4BeamBaseWidthMultiplier = 1f;
public static Vector2 StaffTipPixels(bool attack, int frame)
{
frame = Mathf.Clamp(frame, 0, 5);
if (attack)
{
return frame switch
{
0 => new Vector2(56.5f, 44.5f),
1 => new Vector2(57.5f, 43.5f),
_ => new Vector2(58.5f, 42.5f),
};
}
return frame < 2
? new Vector2(56.5f, 44.5f)
: new Vector2(56.5f, 43.5f);
}
public const int TowerHitSortingOffset = 102;
public const int BlizzardShardCount = 8;
public const float BlizzardLaunchIntervalSeconds = 0.04f;
public const float BlizzardLandingSeconds = 0.24f;
public const float BlizzardLastLandingSeconds = BlizzardLandingSeconds
+ (BlizzardShardCount - 1) * BlizzardLaunchIntervalSeconds;
public const float BlizzardLandingRadiusFraction = 0.65f;
public const float BlizzardRangeAlpha = 0.15f;
public const float BlizzardImpactRadiusAlpha = 0.18f;
public const float BlizzardShardScale = 0.52f;
public const float BlizzardTrailScale = 0.3f;
public const float BlizzardTrailAlpha = 0.32f;
public const float BlizzardTrailOffset = 0.12f;
public const float BlizzardImpactScale = 0.16f;
public const float BlizzardWaveScale = 0.7f;
// Maximum visible half-extents measured from Blizzard-Wave-v1's alpha frames.
public const float BlizzardWaveHalfWidthPixels = 22.5f;
public const float BlizzardWaveHalfHeightPixels = 11f;
public const float BlizzardWavePixelsPerUnit = 32f;
public const float BlizzardWaveRadiusX = BlizzardWaveHalfWidthPixels
/ BlizzardWavePixelsPerUnit * BlizzardWaveScale;
public const float BlizzardWaveRadiusY = BlizzardWaveHalfHeightPixels
/ BlizzardWavePixelsPerUnit * BlizzardWaveScale;
public const float BlizzardDropHeight = 1.2f;
public const float BlizzardDropHeightStep = 0.22f;
private static readonly Vector2Int[] LightningBeamBounds = private static readonly Vector2Int[] LightningBeamBounds =
{ {
new(8, 56), new(8, 89), new(8, 89), new(8, 89), new(8, 56), new(8, 89), new(8, 89), new(8, 89),
@@ -181,6 +235,14 @@ namespace BumpCombat.Constants
public static Vector2Int LightningBeamXBounds(int frame) => public static Vector2Int LightningBeamXBounds(int frame) =>
LightningBeamBounds[Mathf.Clamp(frame, 0, LightningBeamBounds.Length - 1)]; LightningBeamBounds[Mathf.Clamp(frame, 0, LightningBeamBounds.Length - 1)];
public static Vector2 BlizzardLandingOffset(int index, float radius)
{
float fraction = BlizzardLandingRadiusFraction
* Mathf.Sqrt((index + 0.5f) / BlizzardShardCount);
float angle = index * 2.39996323f;
return radius * fraction * new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
}
public static Vector2 ImpactWaveCenterPixels(int frame, Sprite sprite) public static Vector2 ImpactWaveCenterPixels(int frame, Sprite sprite)
{ {
return frame >= ImpactWaveCenters.Length return frame >= ImpactWaveCenters.Length
@@ -103,6 +103,7 @@ namespace BumpCombat.Core
public event Action<CharacterStat> OnStatChanged; public event Action<CharacterStat> OnStatChanged;
public bool IsMinion { get; set; }
public abstract float MoveSpeed { get; } public abstract float MoveSpeed { get; }
public abstract float MaxHealth { get; } public abstract float MaxHealth { get; }
@@ -407,6 +407,7 @@ namespace BumpCombat.Core
public void RestartToTitle() public void RestartToTitle()
{ {
BumpCombat.Defense.DefensePrototypeController.CancelTowerHitTestRun();
runModeOnNextLoad = null; runModeOnNextLoad = null;
skipTitleOnNextLoad = false; skipTitleOnNextLoad = false;
Time.timeScale = 1f; Time.timeScale = 1f;
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4c2979e1dc2744f2b23a56fac198fc91
folderAsset: yes
@@ -0,0 +1,453 @@
using System;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
public enum ComparisonActorKind { Player, Enemy, Summon }
[DisallowMultipleComponent]
public sealed class Defense25DActor : CharacterModel
{
private Defense25DComparison world;
private CapsuleCollider sensor;
private readonly RaycastHit[] movementHits = new RaycastHit[64];
private Vector2 knockbackVelocity;
private Vector2 knockbackStartPosition;
private float knockbackRemaining;
private float hurtUntil;
private float invulnerableUntil;
private float shieldAmount, shieldExpiresAt;
private float guardUntil, guardCooldownUntil;
private bool guardHasBlocked;
private bool knockbackCompletionPending;
private float enemyMaximumHealthOverride;
private Func<Vector2, Vector2> closestPoint;
private Vector2 facing = Vector2.right;
private Vector2 unflippedBodyCenterOffset;
public ComparisonActorKind Kind { get; private set; }
public Rigidbody Body { get; private set; }
public CapsuleCollider BodyCollider { get; private set; }
public Defense25DSprite Visual { get; private set; }
public Vector2 Facing
{
get => facing;
set
{
bool wasLeft = facing.x < 0f;
facing = value;
if (BodyCollider != null && wasLeft != (facing.x < 0f)) ApplyColliderCenters();
}
}
public Vector2 Position => Ground(Body.position);
public Vector2 Center => Position + Offset + BodyCenterOffset;
public float Radius { get; private set; }
public Vector2 Offset { get; private set; }
public Vector2 BodyCenterOffset => Facing.x < 0f
? new Vector2(-unflippedBodyCenterOffset.x, unflippedBodyCenterOffset.y)
: unflippedBodyCenterOffset;
public float FootOffset { get; private set; }
public float CurrentHealth { get; private set; }
public float CurrentShield => world.GameTime < shieldExpiresAt ? shieldAmount : 0f;
public bool IsAlive => isActiveAndEnabled && CurrentHealth > 0f;
public bool IsHurt => world.GameTime < hurtUntil;
public bool IsGuarding => Kind == ComparisonActorKind.Player && IsAlive
&& world.GameTime < guardUntil;
public float GuardCooldownRemaining => Mathf.Max(0f, guardCooldownUntil - world.GameTime);
public float GuardReadinessNormalized => GameplayConstants.Current.Player.GuardCooldownDuration > 0f
? 1f - Mathf.Clamp01(GuardCooldownRemaining / GameplayConstants.Current.Player.GuardCooldownDuration)
: 1f;
public bool HasKnockback => knockbackVelocity.sqrMagnitude > .0025f;
public int DamageEvents { get; private set; }
public int ContactCallbacks { get; private set; }
public bool MovementBlocked { get; private set; }
public Vector2 LastMove { get; private set; }
public Collider LastMovementBlocker { get; private set; }
public Func<Vector2, Vector2> ClosestPointFunction => closestPoint;
public override float MoveSpeed => Evaluate(CharacterStat.MoveSpeed, Kind switch
{
ComparisonActorKind.Player => GameplayConstants.Current.Player.BaseMoveSpeed,
ComparisonActorKind.Enemy => world.Settings.Enemy.MoveSpeed,
_ => GameplayConstants.Current.Defense.SummonUnitMoveSpeed,
});
public override float MaxHealth => Evaluate(CharacterStat.MaxHealth, Kind switch
{
ComparisonActorKind.Player => GameplayConstants.Current.Player.BaseMaxHealth,
ComparisonActorKind.Enemy => enemyMaximumHealthOverride > 0f
? enemyMaximumHealthOverride : world.Settings.Enemy.MaxHealth,
_ => GameplayConstants.Current.Defense.SummonUnitMaximumHealth,
});
public static Vector2 Ground(Vector3 point) => new(point.x, point.z);
public static Vector3 OnGround(Vector2 point, float y = 0f) => new(point.x, y, point.y);
public void Configure(Defense25DComparison owner, ComparisonActorKind kind)
{
world = owner;
Kind = kind;
IsMinion = kind == ComparisonActorKind.Summon;
Defense25DComparisonSettings s = owner.Settings;
Radius = kind == ComparisonActorKind.Player ? s.PlayerRadius
: kind == ComparisonActorKind.Enemy ? s.EnemyRadius
: s.SummonVisualScale * Defense25DComparisonSettings.SlimeBodyCoreDiameterPixels
/ Defense25DComparisonSettings.SummonBodyPixelsPerUnit * .5f;
Offset = kind == ComparisonActorKind.Player ? s.PlayerColliderOffset
: kind == ComparisonActorKind.Enemy ? s.EnemyColliderOffset : Vector2.zero;
Vector2 initialCenterPixels = kind == ComparisonActorKind.Enemy
? new Vector2(0f, s.EnemyBodyCenterCorrectionPixels)
: kind == ComparisonActorKind.Summon
? Defense25DComparisonSettings.SlimeBodyCenterFromFootPixels
: Vector2.zero;
float initialScale = kind == ComparisonActorKind.Player ? s.PlayerVisualScale
: kind == ComparisonActorKind.Enemy ? s.EnemyVisualScale : s.SummonVisualScale;
float initialHeight = kind == ComparisonActorKind.Summon ? 2f * Radius : Mathf.Max(s.ActorHeight, 2f * Radius);
Vector2 initialScreenCenter = initialCenterPixels
* (kind == ComparisonActorKind.Enemy
? 2f * s.CameraSize / Defense25DView.Height
: initialScale / Defense25DComparisonSettings.SummonBodyPixelsPerUnit);
Camera camera = owner.View != null ? owner.View.Camera : null;
Quaternion projectionRotation = BodyProjectionRotation(camera, s.CameraPitch);
Vector3 cameraUp = projectionRotation * Vector3.up;
if (kind == ComparisonActorKind.Enemy)
initialScreenCenter.y += cameraUp.y * initialHeight * .5f;
unflippedBodyCenterOffset = kind == ComparisonActorKind.Player
? Vector2.zero
: ScreenCenterOffset(initialScreenCenter, initialHeight * .5f, camera, s.CameraPitch);
FootOffset = kind == ComparisonActorKind.Player ? s.PlayerFootOffset
: kind == ComparisonActorKind.Enemy ? s.EnemyFootOffset : s.SummonFootOffset;
gameObject.layer = Defense25DComparisonSettings.ActorLayer;
Body = gameObject.AddComponent<Rigidbody>();
Body.isKinematic = true;
Body.useGravity = false;
Body.constraints = RigidbodyConstraints.FreezePositionY | RigidbodyConstraints.FreezeRotation;
Body.interpolation = RigidbodyInterpolation.Interpolate;
Body.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
BodyCollider = gameObject.AddComponent<CapsuleCollider>();
BodyCollider.radius = Radius;
BodyCollider.height = kind == ComparisonActorKind.Summon ? 2f * Radius : Mathf.Max(s.ActorHeight, 2f * Radius);
ApplyColliderCenters();
sensor = gameObject.AddComponent<CapsuleCollider>();
sensor.center = BodyCollider.center;
sensor.radius = Radius + s.ContactSensorMargin;
sensor.height = BodyCollider.height + 2f * s.ContactSensorMargin;
sensor.isTrigger = true;
closestPoint = ClosestGroundPoint;
Visual = gameObject.AddComponent<Defense25DSprite>();
Visual.Configure(world, this);
ResetAt(Ground(transform.position));
}
public void ResetAt(Vector2 position)
{
foreach (AppliedStatModifier modifier in GetAppliedModifiers()) RemoveModifiersFromSource(modifier.SourceId);
gameObject.SetActive(true);
enabled = true;
CurrentHealth = MaxHealth;
shieldAmount = shieldExpiresAt = 0f;
guardUntil = guardCooldownUntil = 0f;
guardHasBlocked = false;
DamageEvents = ContactCallbacks = 0;
hurtUntil = invulnerableUntil = 0f;
knockbackVelocity = LastMove = Vector2.zero;
knockbackRemaining = 0f;
knockbackCompletionPending = false;
MovementBlocked = false;
Facing = Vector2.right;
Body.position = OnGround(position, world.Settings.GroundY);
Body.rotation = Quaternion.identity;
transform.position = Body.position;
BodyCollider.enabled = sensor.enabled = true;
Visual.ResetVisual();
}
public void SetEnemyMaximumHealth(float maximumHealth)
{
if (Kind == ComparisonActorKind.Enemy)
enemyMaximumHealthOverride = Mathf.Max(0f, maximumHealth);
}
public void SetSummonVariantScale(float scale, float coreDiameterPixels, Vector2 bodyCenterFromFootPixels)
{
if (Kind != ComparisonActorKind.Summon) return;
float pixelsPerUnit = Defense25DComparisonSettings.SummonBodyPixelsPerUnit;
Radius = coreDiameterPixels * scale / pixelsPerUnit * .5f;
unflippedBodyCenterOffset = ScreenCenterOffset(bodyCenterFromFootPixels * (scale / pixelsPerUnit), Radius,
world.View != null ? world.View.Camera : null, world.Settings.CameraPitch);
BodyCollider.radius = Radius;
BodyCollider.height = 2f * Radius;
ApplyColliderCenters();
sensor.radius = Radius + world.Settings.ContactSensorMargin;
sensor.height = BodyCollider.height + 2f * world.Settings.ContactSensorMargin;
}
private void ApplyColliderCenters()
{
Vector3 center = new(Offset.x + BodyCenterOffset.x, BodyCollider.height * .5f,
Offset.y + BodyCenterOffset.y);
BodyCollider.center = center;
if (sensor != null) sensor.center = center;
}
private static Vector2 ScreenCenterOffset(Vector2 screenOffset, float colliderCenterY,
Camera camera, float cameraPitch)
{
Quaternion rotation = BodyProjectionRotation(camera, cameraPitch);
Vector3 right = rotation * Vector3.right;
Vector3 up = rotation * Vector3.up;
float targetRight = screenOffset.x;
float targetUp = screenOffset.y - up.y * colliderCenterY;
float determinant = right.x * up.z - right.z * up.x;
return new Vector2(
(targetRight * up.z - right.z * targetUp) / determinant,
(right.x * targetUp - targetRight * up.x) / determinant);
}
private static Quaternion BodyProjectionRotation(Camera camera, float cameraPitch)
{
Quaternion fallback = Quaternion.Euler(cameraPitch, 0f, 0f);
if (camera == null) return fallback;
Quaternion current = camera.transform.rotation;
Vector3 right = current * Vector3.right;
Vector3 up = current * Vector3.up;
float determinant = right.x * up.z - right.z * up.x;
return Mathf.Abs(determinant) > .0001f ? current : fallback;
}
public Vector2 ClosestGroundPoint(Vector2 point) => Center + Vector2.ClampMagnitude(point - Center, Radius);
public float GapTo(Defense25DActor other) => Vector2.Distance(Center, other.Center) - Radius - other.Radius;
public bool TakeDamage(float rawDamage, Vector2 direction, float knockback = 0f,
EnemyDamageSource source = EnemyDamageSource.Player, bool ordinaryBump = false)
{
return TakeDamageWithResult(rawDamage, direction, knockback, source, ordinaryBump, out _);
}
public bool TakeDamageWithResult(float rawDamage, Vector2 direction, float knockback,
EnemyDamageSource source, bool ordinaryBump, out float actualDamage)
{
actualDamage = 0f;
if (!IsAlive || !world.IsRunning || world.Paused
|| (Kind == ComparisonActorKind.Summon && !world.TowersOperational)) return false;
Defense25DEnemy enemy = Kind == ComparisonActorKind.Enemy
? GetComponent<Defense25DEnemy>() : null;
float incoming = CalculateIncomingDamage(rawDamage);
if (enemy != null) incoming = enemy.ApplyDamageTakenEffects(incoming, ordinaryBump);
float applied = DamageCalculator.FinalizeDamage(incoming);
if (applied <= 0f) return false;
if (Kind == ComparisonActorKind.Player && IsGuarding)
{
if (!guardHasBlocked)
{
guardHasBlocked = true;
PlayerConstants player = GameplayConstants.Current.Player;
float recharge = player.GuardCooldownDuration
* Mathf.Clamp01(player.GuardSuccessCooldownRechargePercent / 100f);
guardCooldownUntil = Mathf.Max(world.GameTime, guardCooldownUntil - recharge);
}
world.CombatFeedback?.NotifyGuardImpact(direction);
return false;
}
if (Kind == ComparisonActorKind.Player && world.GameTime < invulnerableUntil)
return false;
enemy?.SetLastDamageSource(source);
if (Kind == ComparisonActorKind.Player && CurrentShield > 0f)
{
float absorbed = Mathf.Min(shieldAmount, applied);
shieldAmount -= absorbed;
applied -= absorbed;
if (applied <= 0f)
{
GrantInvulnerability(GameplayConstants.Current.Player.InvulnerabilityDuration);
return true;
}
}
float healthBefore = CurrentHealth;
CurrentHealth = Mathf.Max(0f, CurrentHealth - applied);
actualDamage = healthBefore - CurrentHealth;
DamageEvents++;
if (Kind == ComparisonActorKind.Player)
{
invulnerableUntil = world.GameTime + GameplayConstants.Current.Player.InvulnerabilityDuration;
hurtUntil = world.GameTime + GameplayConstants.Current.Player.HurtMovementLockDuration;
world.CancelCyclone();
world.NotifyPlayerDamageTaken(healthBefore - CurrentHealth);
if (actualDamage > 0f)
{
Vector3 popupPosition = transform.position;
if (world.View != null) popupPosition += world.View.transform.up * .75f;
world.NotifyPlayerDamagePopup(actualDamage, popupPosition);
world.CombatFeedback?.NotifyPlayerHurt(transform.position, direction, knockback);
}
}
ApplyKnockback(direction, knockback);
Visual.ShowHurt();
if (CurrentHealth <= 0f)
{
StopBody();
if (Kind == ComparisonActorKind.Player || Kind == ComparisonActorKind.Enemy) Visual.ShowDeath();
else Visual.Hide();
BodyCollider.enabled = sensor.enabled = false;
world.NotifyDeath(this);
}
return true;
}
public void GrantInvulnerability(float duration) => invulnerableUntil = world.GameTime + duration;
public bool TryActivateGuard()
{
PlayerConstants tuning = GameplayConstants.Current.Player;
if (Kind != ComparisonActorKind.Player || !isActiveAndEnabled || !IsAlive
|| Time.timeScale <= 0f || !world.IsRunning || world.Paused || IsHurt || IsGuarding
|| GuardCooldownRemaining > 0f)
return false;
guardHasBlocked = false;
guardUntil = world.GameTime + tuning.GuardDuration;
guardCooldownUntil = world.GameTime + tuning.GuardCooldownDuration;
return true;
}
public bool Heal(float amount)
{
if (Kind != ComparisonActorKind.Player || !IsAlive || amount <= 0f) return false;
float before = CurrentHealth;
CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
return CurrentHealth > before;
}
public bool GrantShield(float amount, float duration)
{
if (Kind != ComparisonActorKind.Player || !IsAlive || amount <= 0f || duration <= 0f)
return false;
shieldAmount = amount;
shieldExpiresAt = world.GameTime + duration;
return true;
}
public void ApplyKnockback(Vector2 direction, float magnitude)
{
if (magnitude <= 0f) return;
knockbackStartPosition = Position;
knockbackCompletionPending = true;
// Existing enemy force is a velocity; the player's value is a distance.
float speed = Kind == ComparisonActorKind.Player
? magnitude / Mathf.Max(.001f, GameplayConstants.Current.Player.KnockbackDuration) : magnitude;
knockbackVelocity = direction.normalized * speed;
knockbackRemaining = GameplayConstants.Current.Player.KnockbackDuration;
}
public Vector2 ConsumeKnockback(float dt)
{
Vector2 delta = knockbackVelocity * (Kind == ComparisonActorKind.Player ? Mathf.Min(dt, knockbackRemaining) : dt);
if (Kind == ComparisonActorKind.Player)
{
knockbackRemaining = Mathf.Max(0f, knockbackRemaining - dt);
if (knockbackRemaining <= 0f) knockbackVelocity = Vector2.zero;
}
else knockbackVelocity = Vector2.MoveTowards(knockbackVelocity, Vector2.zero,
GameplayConstants.Current.Enemies.KnockbackDeceleration * dt);
return delta;
}
public Vector2 Move(Vector2 displacement, bool slide = true)
{
Vector2 start = Position;
Vector2 next = start;
Vector2 remainder = displacement;
MovementBlocked = false;
LastMovementBlocker = null;
for (int pass = 0; pass < (slide ? 3 : 1) && remainder.sqrMagnitude > .00000001f; pass++)
{
float length = remainder.magnitude;
Vector3 direction = OnGround(remainder / length);
float cylinder = Mathf.Max(0f, BodyCollider.height * .5f - Radius);
Vector3 center = OnGround(next + Offset + BodyCenterOffset,
world.Settings.GroundY + BodyCollider.center.y);
int count = Physics.CapsuleCastNonAlloc(center + Vector3.up * cylinder,
center - Vector3.up * cylinder, Radius, direction, movementHits,
length + world.Settings.MovementSkin,
Defense25DComparisonSettings.TerrainMask | Defense25DComparisonSettings.ActorMask,
QueryTriggerInteraction.Ignore);
float allowed = length;
Vector2 normal = Vector2.zero;
for (int i = 0; i < count; i++)
{
RaycastHit hit = movementHits[i];
if (hit.collider.attachedRigidbody == Body) continue;
Defense25DActor other = hit.collider.GetComponentInParent<Defense25DActor>();
if (other != null && (Kind == ComparisonActorKind.Enemy || other.Kind == ComparisonActorKind.Enemy)) continue;
if (other != null && Ignores(other)) continue;
Vector2 hitNormal = Ground(hit.normal).normalized;
if (Vector2.Dot(remainder, hitNormal) >= -.00001f) continue;
float distance = Mathf.Max(0f, hit.distance - world.Settings.MovementSkin);
if (distance < allowed)
{
allowed = distance;
normal = hitNormal;
LastMovementBlocker = hit.collider;
}
}
next += remainder / length * allowed;
if (allowed >= length) break;
MovementBlocked = true;
Vector2 remaining = remainder / length * (length - allowed);
remainder = remaining - normal * Mathf.Min(0f, Vector2.Dot(remaining, normal));
}
Body.MovePosition(OnGround(next, world.Settings.GroundY));
LastMove = next - start;
if (knockbackCompletionPending && !HasKnockback)
{
knockbackCompletionPending = false;
world.CombatFeedback?.NotifyKnockbackCompleted(this, knockbackStartPosition, next);
}
return LastMove;
}
public bool Ignores(Defense25DActor other) =>
(Kind == ComparisonActorKind.Player && other.IsMinion)
|| (IsMinion && other.Kind == ComparisonActorKind.Player);
public void IgnorePhysicalPair(Defense25DActor other)
{
foreach (Collider mine in GetComponents<Collider>())
foreach (Collider theirs in other.GetComponents<Collider>()) Physics.IgnoreCollision(mine, theirs);
}
public void StopBody()
{
knockbackVelocity = LastMove = Vector2.zero;
knockbackRemaining = 0f;
knockbackCompletionPending = false;
BodyCollider.enabled = sensor.enabled = false;
}
public void RestoreLiveCollisionAndVisuals()
{
if (!isActiveAndEnabled || CurrentHealth <= 0f) return;
BodyCollider.enabled = sensor.enabled = true;
Visual.RestoreVisibility();
}
private void OnTriggerEnter(Collider other)
{
if (other.isTrigger || !IsAlive || !world.IsRunning || world.Paused) return;
Defense25DActor actor = other.GetComponentInParent<Defense25DActor>();
if (actor == this || (actor != null && Ignores(actor))) return;
ContactCallbacks++;
}
private void OnDisable()
{
if (Kind == ComparisonActorKind.Player) guardUntil = 0f;
if (BodyCollider != null) StopBody();
if (Visual != null) Visual.Hide();
if (world != null && Kind == ComparisonActorKind.Player) world.CancelCyclone();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38cdcdac9597465fa33d491290a6e574
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b7f4860a4f6547628f229a4ef4a81231
@@ -0,0 +1,156 @@
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
public sealed partial class Defense25DArtifactExecutor
{
public bool TryReplay(Defense25DArtifactUse use, Vector3 origin, Vector3 direction, float damageMultiplier)
{
ActiveArtifactDefinition definition = use.Definition;
if (world == null || definition == null || !world.IsRunning || world.Paused
|| world.Player == null || !world.Player.IsAlive || damageMultiplier <= 0f
|| use.Damage <= 0f || use.Effect == ActiveArtifactEffect.Pulse)
return false;
Vector2 replayOrigin = new(origin.x, origin.z);
Vector2 replayDirection = new(direction.x, direction.z);
if (replayDirection.sqrMagnitude < DirectionEpsilon)
replayDirection = use.Effect == ActiveArtifactEffect.ThunderCrash
? GetFacingSector(world.Player.Facing) : world.Player.Facing;
if (replayDirection.sqrMagnitude < DirectionEpsilon) replayDirection = Vector2.right;
replayDirection.Normalize();
var replay = new Cast
{
Use = use,
Identity = castIdentity == int.MaxValue ? 1 : castIdentity + 1,
Origin = replayOrigin,
Direction = replayDirection,
};
castIdentity = replay.Identity;
if (use.Effect == ActiveArtifactEffect.ChainLightning)
return ReplayChain(replay, damageMultiplier);
float range = use.Range;
Vector2 center = replayOrigin;
Vector2 radii = Vector2.one * range;
bool ray = use.Effect is ActiveArtifactEffect.Dash or ActiveArtifactEffect.Phoenix;
float halfWidth = Mathf.Max(.05f, use.Width * .5f);
if (use.Effect == ActiveArtifactEffect.Cyclone)
{
center += CycloneGeometry.CenterOffset(range, use.Charged, world.Settings.PlayerVisualScale);
radii = CycloneGeometry.Radii(range, use.Charged);
}
else if (use.Effect == ActiveArtifactEffect.ThunderCrash)
{
center = replayOrigin + replayDirection * .45f;
radii = new Vector2(17f, 7.25f) / 32f * (ThunderCrashScale * range);
}
int hits = 0;
float igniteDamage = world.Player.CalculateDamage(
(use.Charged ? definition.ChargedIgniteTickDamage : definition.NormalIgniteTickDamage)
* damageMultiplier, definition.DamageTag);
foreach (Defense25DActor target in GetReplayCandidates(
ray ? replayOrigin + replayDirection * (range * .5f) : center,
Mathf.Max(radii.x, radii.y, range)))
{
if (hits >= use.MaxTargets) break;
Vector2 offset = target.Position - replayOrigin;
bool inside = ray
? Vector2.Dot(offset, replayDirection) >= 0f
&& Vector2.Dot(offset, replayDirection) <= range
&& Mathf.Abs(Vector2.Dot(offset, new Vector2(-replayDirection.y, replayDirection.x))) <= halfWidth
: EllipseDistance(target.Position - center, radii) <= 1.0001f;
if (!inside) continue;
Vector2 hitDirection = ray ? replayDirection : target.Position - center;
if (hitDirection.sqrMagnitude < DirectionEpsilon) hitDirection = replayDirection;
if (ApplyReplayDamage(replay, target, use.Damage * damageMultiplier,
hitDirection.normalized, use.Knockback, igniteDamage))
hits++;
}
return hits > 0;
}
private bool ReplayChain(Cast cast, float damageMultiplier)
{
if (!PlanChain(cast)) return false;
ActiveArtifactDefinition definition = cast.Use.Definition;
float igniteDamage = world.Player.CalculateDamage(
(cast.Use.Charged ? definition.ChargedIgniteTickDamage : definition.NormalIgniteTickDamage)
* damageMultiplier, definition.DamageTag);
var strikes = new List<ChainStrike>(cast.Strikes);
bool applied = false;
foreach (ChainStrike strike in strikes)
{
Vector2 hitDirection = strike.End - strike.Start;
if (hitDirection.sqrMagnitude < DirectionEpsilon) hitDirection = cast.Direction;
applied |= ApplyReplayDamage(cast, strike.Target, strike.Damage * damageMultiplier,
hitDirection, cast.Use.Knockback, igniteDamage);
}
DestroyCast(cast);
return applied;
}
private List<Defense25DActor> GetReplayCandidates(Vector2 center, float radius)
{
var result = new List<Defense25DActor>(Query(center, radius, null));
result.Sort((left, right) =>
(left.Position - center).sqrMagnitude.CompareTo((right.Position - center).sqrMagnitude));
return result;
}
private bool ApplyReplayDamage(Cast cast, Defense25DActor target, float rawDamage,
Vector2 direction, float knockback, float igniteTickDamage)
{
if (target == null || !target.IsAlive || rawDamage <= 0f) return false;
ActiveArtifactDefinition d = cast.Use.Definition;
direction = direction.sqrMagnitude > DirectionEpsilon ? direction.normalized : cast.Direction;
float scaledDamage = world.Player.CalculateDamage(rawDamage, d.DamageTag);
Defense25DEnemy enemy = target.GetComponent<Defense25DEnemy>();
if (enemy != null && RegisterArtifactContact != null)
{
Defense25DArtifactContact contact = RegisterArtifactContact(enemy, d.ArtifactColor, cast.Identity);
if (contact.Result == EnemyArtifactContactResult.Ignored) return false;
if (contact.Result == EnemyArtifactContactResult.Shielded) return false;
}
if (!target.TakeDamage(scaledDamage, direction, knockback)) return false;
float incomingDamage = DamageCalculator.FinalizeDamage(target.CalculateIncomingDamage(scaledDamage));
if (enemy != null)
{
bool secondary = d.Effect switch
{
ActiveArtifactEffect.Phoenix =>
(cast.Use.Charged ? d.ChargedIgniteDuration : d.NormalIgniteDuration) > 0f,
ActiveArtifactEffect.ThunderCrash =>
(cast.Use.Charged ? d.ChargedVulnerabilityDuration : d.NormalVulnerabilityDuration) > 0f,
ActiveArtifactEffect.ChainLightning =>
UnityEngine.Random.value < (cast.Use.Charged ? d.ChargedShockChance : d.NormalShockChance),
_ => false,
};
if (secondary)
ApplySecondaryEffect?.Invoke(enemy, d, cast.Use.Charged, igniteTickDamage);
CombatEvents.RaiseValidHit(new CombatHitResult(
world.Player.gameObject, target.gameObject, false, HitSide.Front,
incomingDamage, direction, knockback,
target.Position, damageTag: d.DamageTag,
sourceId: Defense25DRelicRuntime.EchoSourceId, usesPositionBonus: false,
showsHitFeedback: incomingDamage > 0f, isArtifactHit: true,
artifactEffect: d.Effect, isChargedArtifact: cast.Use.Charged,
baseBumpDamage: world.Player.CalculateDamage(
GameplayConstants.Current.Combat.BaseDamage, DamageTag.Collision)));
}
target.Visual?.ShowHurt();
return true;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09c2eaf519a34c1e9e61bc3598716b7b
@@ -0,0 +1,882 @@
using System;
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Defense;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
/// <summary>Restores the original combat's sprite feedback on the Physics3D presentation.</summary>
[DisallowMultipleComponent]
public sealed class Defense25DCombatFeedback : MonoBehaviour
{
private sealed class SpriteEffect
{
public GameObject Root;
public SpriteRenderer Renderer;
public Sprite[] Frames;
public IReadOnlyList<float> Durations;
public int Frame;
public float Elapsed;
public bool Loop;
public float Alpha = 1f;
public EffectKind Kind;
public LineRenderer Line;
public float Duration;
public float Hold;
public float Fade;
public bool Heavy;
public bool Unscaled;
public bool PoolLineWhenRemoved;
public bool FollowPlayer;
public bool FollowVisualGroundAnchor;
public Vector2 FollowGroundOffset;
public bool Expand;
public Vector3 MoveStart;
public Vector3 MoveEnd;
public float FollowHeight;
public SpriteRenderer[] Ghosts;
public float[] GhostAlphas;
}
private readonly List<SpriteEffect> effects = new();
private readonly List<SpriteEffect> heavyEffects = new();
private readonly Stack<GameObject> lineEffectPool = new();
private readonly Dictionary<string, Sprite[]> frameCache = new();
private Defense25DComparison world;
private SpriteEffect aura;
private SpriteEffect readyAura;
private SpriteEffect chargeFlash;
private SpriteEffect switchPulse;
private SpriteEffect guardImpact;
private SpriteEffect blockedBump;
private SpriteRenderer playerRenderer;
private float flashRemaining;
private float blinkRemaining;
private float blinkElapsed;
private float hitStopUntil;
private float cameraShakeUntil;
private float cameraShakeDuration;
private Color flashColor = Color.white;
private bool directionalPunch;
private float lastRearPunchAt = float.NegativeInfinity;
private float nextGuardImpactAt = float.NegativeInfinity;
private float capturedTimeScale = 1f;
private bool ownsTimeFreeze;
private Vector2 shakeDirection;
private float shakePixels;
private Material transparentEffectMaterial;
private bool bound;
public int ActiveEffectCount => effects.Count;
public int ActiveHeavyEffectCount => heavyEffects.Count;
public int ActiveBumpImpactCount { get; private set; }
public int ActiveArtifactImpactCount { get; private set; }
public int ActivePlayerHurtImpactCount { get; private set; }
public bool IsChargingVisualVisible => aura != null || readyAura != null;
public bool IsReadyFlashVisible => chargeFlash != null;
public Material TransparentEffectMaterial => transparentEffectMaterial;
public void Bind(Defense25DComparison owner)
{
if (bound) Unbind();
world = owner;
if (world == null || !world.isActiveAndEnabled || world.Player == null) return;
bound = true;
capturedTimeScale = Time.timeScale > 0f ? Time.timeScale : 1f;
playerRenderer = world.Player.Visual != null ? world.Player.Visual.Renderer : null;
if (world.Settings.TransparentMaterial != null)
{
transparentEffectMaterial = new Material(world.Settings.TransparentMaterial)
{
name = "Defense25D Combat Feedback Transparent",
};
if (transparentEffectMaterial.HasProperty("_BaseColor"))
transparentEffectMaterial.SetColor("_BaseColor", Color.white);
if (transparentEffectMaterial.HasProperty("_Cutoff"))
transparentEffectMaterial.SetFloat("_Cutoff", .001f);
}
CombatEvents.OnValidHit += HandleValidHit;
}
private void OnEnable()
{
if (world != null && world.Player != null && !bound) Bind(world);
}
private void Update()
{
TickFeedback(Time.deltaTime, Time.unscaledDeltaTime);
}
/// <summary>Advances scaled sprite effects and unscaled camera/hit-stop timers.</summary>
public void TickFeedback(float scaledDeltaTime, float unscaledDeltaTime)
{
if (!bound || world == null) return;
if (!world.isActiveAndEnabled)
{
Unbind();
return;
}
scaledDeltaTime = Mathf.Max(0f, scaledDeltaTime);
if (world.Paused) scaledDeltaTime = 0f;
unscaledDeltaTime = Mathf.Max(0f, unscaledDeltaTime);
AdvanceEffects(scaledDeltaTime, unscaledDeltaTime);
AdvanceChargeVisuals(scaledDeltaTime);
if (flashRemaining > 0f) flashRemaining = Mathf.Max(0f, flashRemaining - unscaledDeltaTime);
if (blinkRemaining > 0f)
{
blinkRemaining = Mathf.Max(0f, blinkRemaining - scaledDeltaTime);
blinkElapsed += scaledDeltaTime;
}
ApplyPlayerTint();
if (hitStopUntil > 0f && Time.unscaledTime >= hitStopUntil)
{
hitStopUntil = 0f;
RestoreOwnedTimeFreeze();
}
if (cameraShakeUntil > 0f)
{
if (Time.unscaledTime >= cameraShakeUntil)
{
cameraShakeUntil = 0f;
if (world.View != null) world.View.ClearFeedbackCameraOffset();
}
else if (world.View != null)
{
float fade = Mathf.Clamp01((cameraShakeUntil - Time.unscaledTime)
/ Mathf.Max(.001f, cameraShakeDuration));
if (directionalPunch)
{
Vector2 punch = shakeDirection * shakePixels * fade;
world.View.SetFeedbackCameraOffset(new Vector2(Mathf.Round(punch.x), Mathf.Round(punch.y)));
}
else
{
Vector2 jitter = UnityEngine.Random.insideUnitCircle * shakePixels * fade;
world.View.SetFeedbackCameraOffset(new Vector2(Mathf.Round(jitter.x), Mathf.Round(jitter.y)));
}
}
}
}
public void NotifyValidHit(CombatHitResult result)
{
if (!bound || world == null || !world.isActiveAndEnabled || world.Player == null
|| result.Attacker != world.Player.gameObject || result.Target == null) return;
// Per-contact artifact flashes are deliberately emitted before this feedback gate,
// matching CombatFeedback.HandleValidHit for zero-damage/hidden contacts.
bool artifactVisualCreated = result.IsArtifactHit && CreateArtifactImpact(result);
if (!result.ShowsHitFeedback) return;
CombatFeedbackSettings tuning = GameplayConstants.Current.CombatFeedback;
if (result.IsOrdinaryBump && result.HasBackBonus)
{
float now = Time.time;
if (now - lastRearPunchAt >= tuning.RearHitCameraPunchRearmDelay)
{
if (cameraShakeUntil <= Time.unscaledTime
&& result.KnockbackDirection.sqrMagnitude > Mathf.Epsilon)
{
shakePixels = tuning.RearHitCameraPunchPixels;
shakeDirection = result.KnockbackDirection.normalized;
cameraShakeDuration = tuning.RearHitCameraPunchDuration;
cameraShakeUntil = Time.unscaledTime + tuning.RearHitCameraPunchDuration;
directionalPunch = true;
}
}
lastRearPunchAt = now;
}
Color flash = HitColor(result);
flashColor = Color.Lerp(Color.white, flash, .55f);
if (!artifactVisualCreated && result.IsOrdinaryBump)
CreateImpact(result.HitPosition, BumpFrames(result.Side), Defense25DCombatFeedbackConstants.BumpFrames,
2f, Color.white, result.KnockbackDirection, "Bump Impact", EffectKind.Bump,
true, 102);
if (result.KnockbackForce > 0f)
CreateFloorStreak(result.HitPosition, result.KnockbackDirection, flash);
flashRemaining = Mathf.Max(flashRemaining, tuning.FlashDuration);
RequestHitStop(result.IsStrongDash ? tuning.StrongDashHitStopDuration
: result.IsDash ? tuning.DashHitStopDuration
: result.HasBackBonus ? tuning.BackHitStopDuration
: tuning.NormalHitStopDuration);
if (result.IsStrongDash) RequestCameraShake(tuning.HeavyCameraShakePixels, result.KnockbackDirection);
}
public void NotifyPlayerHurt(Vector2 groundPosition, bool heavy) =>
NotifyPlayerHurt(groundPosition, Vector2.zero, heavy);
public void NotifyPlayerHurt(Vector2 groundPosition, Vector2 knockbackDirection, bool heavy)
{
if (!bound || world == null || !world.isActiveAndEnabled || world.Player == null) return;
CombatFeedbackSettings tuning = GameplayConstants.Current.CombatFeedback;
Vector2 origin = groundPosition;
if (origin == Vector2.zero)
origin = world.Player.Position;
if (knockbackDirection.sqrMagnitude > 0f) origin -= knockbackDirection.normalized * .18f;
CreateImpact(origin, LoadFrames(CombatFeedback.GetPlayerHurtImpactResourcePath()),
Defense25DCombatFeedbackConstants.PlayerHurtFrames, 1f, Color.white, Vector2.zero, "Player Hurt Impact", EffectKind.Hurt);
flashRemaining = Mathf.Max(flashRemaining, tuning.DamageFlashDuration);
flashColor = Color.Lerp(Color.white, new Color(1f, .18f, .12f, 1f), .55f);
blinkRemaining = Mathf.Max(blinkRemaining, GameplayConstants.Current.Player.InvulnerabilityDuration);
blinkElapsed = 0f;
RequestHitStop(tuning.DamageHitStopDuration);
RequestCameraShake(heavy ? tuning.HeavyCameraShakePixels : tuning.CameraShakePixels, Vector2.zero);
}
public void NotifyPlayerHurt(Vector3 worldPosition, bool heavy) =>
NotifyPlayerHurt(new Vector2(worldPosition.x, worldPosition.z), heavy);
public void NotifyPlayerHurt(Vector3 worldPosition, Vector2 knockbackDirection, float knockbackDistance)
{
float threshold = GameplayConstants.Current.CombatFeedback.HeavyKnockbackThreshold;
NotifyPlayerHurt(new Vector2(worldPosition.x, worldPosition.z), knockbackDirection,
knockbackDistance >= threshold);
}
public void NotifyChargeChanged(ActiveArtifactDefinition definition, float normalized, bool becameFull)
{
if (!bound || world == null || !world.isActiveAndEnabled || definition == null || world.Player == null) return;
normalized = Mathf.Clamp01(normalized);
if (normalized <= 0f)
{
ClearChargeVisuals();
return;
}
if (normalized >= 1f)
{
ClearEffect(ref aura);
EnsureChargeSlot(ref readyAura, definition, Defense25DCombatFeedbackConstants.ChargeReadyAuraSuffix, true, Defense25DCombatFeedbackConstants.ChargeAuraFrames, -1);
if (becameFull)
{
EnsureChargeSlot(ref chargeFlash, definition, Defense25DCombatFeedbackConstants.ChargeFlashSuffix, false, Defense25DCombatFeedbackConstants.ChargeFlashFrames, 1,
ArtifactChargeVisual.FlashFrameCount);
if (chargeFlash != null) chargeFlash.Loop = false;
}
return;
}
ClearEffect(ref readyAura);
ClearEffect(ref chargeFlash);
EnsureChargeSlot(ref aura, definition, Defense25DCombatFeedbackConstants.ChargeAuraSuffix, true, Defense25DCombatFeedbackConstants.ChargeAuraFrames, -1,
ArtifactChargeVisual.FrameCount);
if (aura != null) aura.Alpha = Mathf.Lerp(.55f, .85f, normalized);
}
public void NotifyArtifactReleased(ActiveArtifactDefinition definition, Vector2 groundOrigin, bool charged)
{
if (!bound || world == null || !world.isActiveAndEnabled || definition == null) return;
string path = Defense25DCombatFeedbackConstants.ChargeResourceRoot + definition.Effect + Defense25DCombatFeedbackConstants.ChargeReleaseSuffix;
Sprite[] frames = LoadFrames(path);
if (frames.Length == ArtifactChargeVisual.FrameCount)
CreateImpact(groundOrigin, frames, Defense25DCombatFeedbackConstants.ChargeReleaseFrames, 1f, Color.white,
Vector2.zero, "Artifact Release", EffectKind.Release, false, 1);
ClearChargeVisuals();
}
public void NotifyArtifactSwitched(ActiveArtifactDefinition definition)
{
if (!bound || world == null || !world.isActiveAndEnabled || definition == null || playerRenderer == null) return;
EnsureChargeSlot(ref switchPulse, definition, Defense25DCombatFeedbackConstants.ChargeReleaseSuffix, false, Defense25DCombatFeedbackConstants.ChargeAuraFrames, 1,
ArtifactChargeVisual.FrameCount);
if (switchPulse != null) switchPulse.Loop = false;
}
public void NotifyGuardImpact(Vector2 incomingDirection)
{
if (!bound || world == null || !world.isActiveAndEnabled || world.Player == null || playerRenderer == null
|| Time.time < nextGuardImpactAt) return;
CombatFeedbackSettings tuning = GameplayConstants.Current.CombatFeedback;
Sprite sprite = Resources.Load<Sprite>(CombatFeedback.GetGuardBlockImpactResourcePath());
if (sprite == null) return;
nextGuardImpactAt = Time.time + tuning.GuardBlockImpactInterval;
Vector2 direction = incomingDirection.sqrMagnitude > 0f
? incomingDirection.normalized : Vector2.zero;
Vector2 position = world.Player.Center - direction * tuning.GuardBlockImpactOffset;
SpriteEffect effect = CreateStaticSprite("Player Guard Block Impact", sprite,
ToWorld(position), playerRenderer.sortingOrder + 20, EffectKind.Guard);
if (effect != null)
{
effect.FollowPlayer = true;
effect.FollowGroundOffset = -direction * tuning.GuardBlockImpactOffset;
effect.Hold = tuning.GuardBlockImpactHoldDuration;
effect.Fade = tuning.GuardBlockImpactFadeDuration;
effect.Duration = effect.Hold + effect.Fade;
guardImpact = effect;
}
}
public void NotifyBlockedBump(Vector2 hitPosition, Vector2 approachDirection)
{
if (!bound || world == null || !world.isActiveAndEnabled || playerRenderer == null) return;
CombatFeedbackSettings tuning = GameplayConstants.Current.CombatFeedback;
Sprite sprite = Resources.Load<Sprite>(Defense25DCombatFeedbackConstants.BlockedBumpResource);
if (sprite == null) return;
Vector3 position = ToWorld(hitPosition) + Vector3.up * tuning.BlockedBumpVerticalOffset;
SpriteEffect effect = CreateStaticSprite("Blocked Bump", sprite, position,
playerRenderer.sortingOrder + 102, EffectKind.Blocked);
if (effect != null)
{
effect.Renderer.flipX = approachDirection.x < 0f;
effect.Hold = tuning.BlockedBumpHoldDuration;
effect.Fade = tuning.BlockedBumpFadeDuration;
effect.Duration = effect.Hold + effect.Fade;
blockedBump = effect;
}
}
public void NotifyTowerImpact(DefenseTowerKind kind, Defense25DEnemy target, Vector2 hitPosition)
{
if (!bound || world == null || !world.isActiveAndEnabled || target == null) return;
string path = CombatFeedback.GetTowerImpactResourcePath(kind);
Sprite[] frames = LoadFrames(path);
if (frames.Length != CombatFeedback.BumpImpactFrameCount) return;
SpriteEffect effect = new();
if (!CreateImpact(hitPosition, frames, Defense25DCombatFeedbackConstants.ArtifactFrames, 2f, Color.white,
Vector2.zero, $"Tower Impact {kind}", EffectKind.Tower, false,
DefenseTowerArtBounds.TowerHitSortingOffset, effect)) return;
SpriteRenderer targetRenderer = target.Actor != null && target.Actor.Visual != null
? target.Actor.Visual.Renderer : null;
if (targetRenderer != null && effect.Renderer != null)
{
effect.Renderer.sortingLayerID = targetRenderer.sortingLayerID;
effect.Renderer.sortingOrder = targetRenderer.sortingOrder
+ DefenseTowerArtBounds.TowerHitSortingOffset;
}
}
public void NotifyShield(float duration)
{
if (!bound || world == null || !world.isActiveAndEnabled || duration <= 0f) return;
SpriteEffect ring = CreateRing(PlayerVisualGroundPosition, .6f,
new Color(.15f, .95f, .95f, 1f), duration, true);
if (ring != null)
{
ring.FollowPlayer = true;
ring.FollowVisualGroundAnchor = true;
}
}
public void NotifyPulseGather(Defense25DEnemy target, Vector2 previousPosition, Vector2 finalPosition)
{
if (!bound || world == null || !world.isActiveAndEnabled || target == null
|| target.Actor == null || target.Actor.Visual == null) return;
float distance = Vector2.Distance(previousPosition, finalPosition);
if (!CombatFeedback.ShouldShowPulseStreak(distance))
{
CreateRing(finalPosition, .26f, new Color(1f, .39f, .39f, 1f), .12f, true);
return;
}
SpriteRenderer source = target.Actor.Visual.Renderer;
if (source == null || source.sprite == null) return;
GameObject root = new("Pulse Gather Afterimage");
root.transform.rotation = world.View != null ? world.View.transform.rotation : Quaternion.identity;
root.transform.localScale = source.transform.lossyScale;
float height = source.transform.position.y - world.Settings.GroundY;
root.transform.position = ToWorld(previousPosition) + Vector3.up * height;
SpriteRenderer first = root.AddComponent<SpriteRenderer>();
CopyGhost(source, first, 2, new Color(1f, .39f, .39f, .65f));
GameObject secondObject = new("Pulse Gather Afterimage 2");
secondObject.transform.SetParent(root.transform, false);
secondObject.transform.localPosition = Vector3.left * .08f;
SpriteRenderer second = secondObject.AddComponent<SpriteRenderer>();
CopyGhost(source, second, 1, new Color(1f, .65f, .65f, .4f));
LineRenderer streak = AcquireLine("Pulse Gather Streak");
CombatFeedback.ConfigureWorldStreakRenderer(streak, transparentEffectMaterial,
new Color(1f, .39f, .39f, .8f), .055f, ToWorld(previousPosition), ToWorld(finalPosition));
SpriteEffect effect = new()
{
Root = root, Line = streak, Frames = Array.Empty<Sprite>(),
Durations = new[] { Defense25DCombatFeedbackConstants.PulseGatherDuration },
Duration = Defense25DCombatFeedbackConstants.PulseGatherDuration,
Unscaled = true, Heavy = true,
MoveStart = ToWorld(previousPosition) + Vector3.up * height,
MoveEnd = ToWorld(finalPosition) + Vector3.up * height,
FollowHeight = height,
Ghosts = new[] { first, second },
GhostAlphas = new[] { .65f, .4f },
};
AddEffect(effect, true);
}
public void NotifyKnockbackCompleted(Defense25DActor target, Vector2 start, Vector2 end)
{
if (!bound || world == null || !world.isActiveAndEnabled || target == null
|| target.Kind != ComparisonActorKind.Enemy
|| !IsOwnedEnemy(target)) return;
if (CombatFeedback.ShouldShowKnockbackDust(Vector2.Distance(start, end)))
CreateRing(end, .2f, new Color(.9f, .8f, 1f, 1f), .13f, true);
}
public void ResetFeedback()
{
ClearAllEffects();
ClearChargeVisuals();
flashRemaining = blinkRemaining = blinkElapsed = hitStopUntil = cameraShakeUntil = 0f;
ActiveBumpImpactCount = ActiveArtifactImpactCount = ActivePlayerHurtImpactCount = 0;
directionalPunch = false;
shakeDirection = Vector2.zero;
shakePixels = cameraShakeDuration = 0f;
if (world != null && world.Player != null && world.Player.Visual != null)
world.Player.Visual.ClearFeedbackTint();
if (world != null && world.View != null) world.View.ClearFeedbackCameraOffset();
RestoreOwnedTimeFreeze();
lastRearPunchAt = float.NegativeInfinity;
nextGuardImpactAt = float.NegativeInfinity;
}
private void HandleValidHit(CombatHitResult result) => NotifyValidHit(result);
private void Unbind()
{
if (bound) CombatEvents.OnValidHit -= HandleValidHit;
bound = false;
ResetFeedback();
while (lineEffectPool.Count > 0)
{
GameObject pooled = lineEffectPool.Pop();
if (pooled != null) Destroy(pooled);
}
frameCache.Clear();
playerRenderer = null;
if (transparentEffectMaterial != null)
{
Destroy(transparentEffectMaterial);
transparentEffectMaterial = null;
}
}
private void OnDisable() => Unbind();
private void OnDestroy() => Unbind();
private void RestoreOwnedTimeFreeze()
{
if (!ownsTimeFreeze) return;
Time.timeScale = capturedTimeScale;
ownsTimeFreeze = false;
}
private enum EffectKind { Generic, Bump, Artifact, Hurt, Release, Charge, Guard, Blocked, Tower }
private bool CreateArtifactImpact(CombatHitResult result)
{
Sprite[] frames = LoadFrames(CombatFeedback.GetArtifactImpactResourcePath(
result.ArtifactEffect, result.IsChargedArtifact));
if (frames.Length != CombatFeedback.ArtifactImpactFrameCount) return false;
Vector2 direction = result.KnockbackDirection.sqrMagnitude > 0f
? result.KnockbackDirection.normalized : Vector2.right;
bool created = CreateImpact(result.HitPosition, frames, Defense25DCombatFeedbackConstants.ArtifactFrames, 2f,
Color.white, direction, "Artifact Impact " + result.ArtifactEffect,
EffectKind.Artifact, true, 102);
return created;
}
private Sprite[] BumpFrames(HitSide side)
{
ActiveArtifactDefinition selected = world.Loadout != null ? world.Loadout.CurrentArtifact : null;
string path = selected != null
? $"Artifacts/ThreeColor-v1/Combat/BumpImpact-{(side == HitSide.Back ? "Strong" : side == HitSide.Side ? "Medium" : "Weak")}-{selected.ArtifactColor}-v1"
: CombatFeedback.GetBumpImpactResourcePath(side);
Sprite[] frames = LoadFrames(path);
return frames.Length == CombatFeedback.BumpImpactFrameCount
? frames : LoadFrames(CombatFeedback.GetBumpImpactResourcePath(side));
}
private bool CreateImpact(Vector2 groundPosition, Sprite[] frames, IReadOnlyList<float> durations,
float scale, Color color, Vector2 direction, string label, EffectKind kind,
bool rotate = false, int sortingOffset = 102, SpriteEffect capture = null)
{
if (frames == null || frames.Length == 0 || world == null || world.Player == null) return false;
SpriteEffect effect = new()
{
Root = new GameObject(label), Frames = frames, Durations = durations,
Loop = false, Renderer = null,
};
effect.Root.transform.position = ToWorld(groundPosition);
effect.Root.transform.localScale = Vector3.one * scale;
if (world.View != null)
{
effect.Root.transform.rotation = world.View.transform.rotation;
if (rotate && direction.sqrMagnitude > .0001f)
{
Vector3 worldDirection = new(direction.x, 0f, direction.y);
Vector3 projected = new(Vector3.Dot(worldDirection, world.View.transform.right),
Vector3.Dot(worldDirection, world.View.transform.up), 0f);
if (projected.sqrMagnitude > .0001f)
effect.Root.transform.rotation *= Quaternion.AngleAxis(
Mathf.Atan2(projected.y, projected.x) * Mathf.Rad2Deg, Vector3.forward);
}
}
effect.Renderer = effect.Root.AddComponent<SpriteRenderer>();
effect.Renderer.sprite = frames[0];
effect.Renderer.color = color;
if (transparentEffectMaterial != null) effect.Renderer.sharedMaterial = transparentEffectMaterial;
if (playerRenderer != null)
{
effect.Renderer.sortingLayerID = playerRenderer.sortingLayerID;
effect.Renderer.sortingOrder = playerRenderer.sortingOrder + sortingOffset;
}
effects.Add(effect);
effect.Kind = kind;
if (capture != null)
{
capture.Root = effect.Root;
capture.Renderer = effect.Renderer;
capture.Frames = effect.Frames;
capture.Durations = effect.Durations;
capture.Kind = effect.Kind;
}
if (kind == EffectKind.Bump) ActiveBumpImpactCount++;
if (kind == EffectKind.Artifact) ActiveArtifactImpactCount++;
if (kind == EffectKind.Hurt) ActivePlayerHurtImpactCount++;
return true;
}
private SpriteEffect CreateStaticSprite(string label, Sprite sprite, Vector3 position,
int sortingOffset, EffectKind kind)
{
if (sprite == null || world == null) return null;
GameObject root = new(label);
root.transform.position = position;
if (world.View != null) root.transform.rotation = world.View.transform.rotation;
SpriteRenderer renderer = root.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.color = Color.white;
if (transparentEffectMaterial != null) renderer.sharedMaterial = transparentEffectMaterial;
if (playerRenderer != null)
{
renderer.sortingLayerID = playerRenderer.sortingLayerID;
renderer.sortingOrder = playerRenderer.sortingOrder + sortingOffset;
}
SpriteEffect effect = new() { Root = root, Renderer = renderer,
Frames = Array.Empty<Sprite>(), Durations = new[] { .001f }, Kind = kind };
effects.Add(effect);
return effect;
}
private void CreateFloorStreak(Vector2 position, Vector2 direction, Color color)
{
if (direction.sqrMagnitude < .0001f || world == null) return;
LineRenderer line = AcquireLine("Interaction Streak");
GameObject root = line.gameObject;
CombatFeedback.ConfigureWorldStreakRenderer(line, transparentEffectMaterial,
color, .04f, ToWorld(position), ToWorld(position + direction.normalized * .34f));
SpriteEffect effect = new() { Root = root, Line = line,
Duration = Defense25DCombatFeedbackConstants.InteractionStreakDuration,
Durations = new[] { Defense25DCombatFeedbackConstants.InteractionStreakDuration },
Frames = Array.Empty<Sprite>(), Loop = false, Unscaled = true, Heavy = true,
PoolLineWhenRemoved = true };
AddEffect(effect, true);
}
private SpriteEffect CreateRing(Vector2 position, float radius, Color color,
float duration, bool useScaledTime)
{
if (!bound || world == null || duration <= 0f) return null;
LineRenderer line = AcquireLine("Interaction Ring");
GameObject root = line.gameObject;
root.transform.SetPositionAndRotation(ToWorld(position) + Vector3.up * .025f, Quaternion.identity);
root.transform.localScale = Vector3.one * .72f;
line.enabled = true;
line.loop = true;
line.useWorldSpace = false;
line.positionCount = 24;
line.startWidth = line.endWidth = 0f;
line.widthMultiplier = .045f;
line.numCornerVertices = 0;
line.numCapVertices = 2;
line.sharedMaterial = transparentEffectMaterial;
line.startColor = line.endColor = color;
line.sortingOrder = 210;
for (int i = 0; i < line.positionCount; i++)
{
float angle = i * Mathf.PI * 2f / line.positionCount;
line.SetPosition(i, new Vector3(Mathf.Cos(angle) * radius, 0f,
Mathf.Sin(angle) * radius));
}
SpriteEffect effect = new() { Root = root, Line = line, Duration = duration,
Durations = new[] { duration }, Frames = Array.Empty<Sprite>(),
Unscaled = !useScaledTime, Heavy = true, PoolLineWhenRemoved = true,
Expand = true };
AddEffect(effect, true);
return effects.Contains(effect) ? effect : null;
}
private LineRenderer AcquireLine(string label)
{
GameObject root = lineEffectPool.Count > 0 ? lineEffectPool.Pop() : new GameObject(label);
root.name = label;
root.SetActive(true);
LineRenderer line = root.GetComponent<LineRenderer>();
if (line == null) line = root.AddComponent<LineRenderer>();
line.enabled = true;
line.loop = false;
return line;
}
private void AddEffect(SpriteEffect effect, bool heavy)
{
effects.Add(effect);
effect.Heavy = heavy;
if (!heavy) return;
heavyEffects.Add(effect);
int limit = CombatFeedback.MaxConcurrentHeavyEffects;
while (heavyEffects.Count > limit)
{
SpriteEffect oldest = heavyEffects[0];
int index = effects.IndexOf(oldest);
if (index >= 0) RemoveEffectAt(index, oldest);
else heavyEffects.RemoveAt(0);
}
}
private bool IsOwnedEnemy(Defense25DActor actor)
{
foreach (Defense25DEnemy enemy in world.Enemies)
if (enemy != null && enemy.Actor == actor) return true;
return false;
}
private void CopyGhost(SpriteRenderer source, SpriteRenderer ghost, int sortingOffset, Color color)
{
ghost.sprite = source.sprite;
ghost.flipX = source.flipX;
ghost.flipY = source.flipY;
ghost.color = color;
ghost.sharedMaterial = transparentEffectMaterial;
ghost.sortingLayerID = source.sortingLayerID;
ghost.sortingOrder = source.sortingOrder + sortingOffset;
}
private void EnsureChargeSlot(ref SpriteEffect slot, ActiveArtifactDefinition definition,
string suffix, bool loop, IReadOnlyList<float> durations, int sortingOffset, int expectedFrameCount = ArtifactChargeVisual.FrameCount)
{
if (slot != null) return;
Sprite[] frames = LoadFrames(Defense25DCombatFeedbackConstants.ChargeResourceRoot + definition.Effect + suffix);
if (frames.Length != expectedFrameCount || playerRenderer == null) return;
GameObject root = new("Artifact Charge " + suffix);
root.transform.SetParent(playerRenderer.transform, false);
SpriteRenderer renderer = root.AddComponent<SpriteRenderer>();
renderer.sprite = frames[0];
renderer.color = Color.white;
renderer.sharedMaterial = playerRenderer.sharedMaterial;
renderer.sortingLayerID = playerRenderer.sortingLayerID;
renderer.sortingOrder = playerRenderer.sortingOrder + sortingOffset;
if (transparentEffectMaterial != null) renderer.sharedMaterial = transparentEffectMaterial;
slot = new SpriteEffect { Root = root, Renderer = renderer, Frames = frames,
Durations = durations, Loop = loop, Kind = EffectKind.Charge };
effects.Add(slot);
}
private void AdvanceChargeVisuals(float dt)
{
if (aura != null) aura.Renderer.color = new Color(1f, 1f, 1f, aura.Alpha);
}
private void AdvanceEffects(float scaledDeltaTime, float unscaledDeltaTime)
{
for (int i = effects.Count - 1; i >= 0; i--)
{
SpriteEffect effect = effects[i];
if (effect == null || effect.Root == null) { effects.RemoveAt(i); continue; }
float dt = effect.Unscaled ? unscaledDeltaTime : scaledDeltaTime;
if (dt <= 0f) continue;
if (effect.Frames.Length == 0)
{
effect.Elapsed += dt;
float progress = Mathf.Clamp01(effect.Elapsed / Mathf.Max(.001f, effect.Duration));
if (effect.FollowPlayer && world.Player != null)
{
Vector2 followPosition = effect.FollowVisualGroundAnchor
? PlayerVisualGroundPosition : world.Player.Position;
effect.Root.transform.position = ToWorld(followPosition + effect.FollowGroundOffset)
+ Vector3.up * .025f;
}
if (effect.MoveEnd != Vector3.zero)
effect.Root.transform.position = Vector3.Lerp(effect.MoveStart, effect.MoveEnd, progress);
if (effect.Expand)
effect.Root.transform.localScale = Vector3.one * Mathf.Lerp(.72f, 1.2f, progress);
if (effect.Ghosts != null)
for (int g = 0; g < effect.Ghosts.Length; g++)
if (effect.Ghosts[g] != null)
{
Color ghostColor = effect.Ghosts[g].color;
ghostColor.a = effect.GhostAlphas[g] * (1f - progress);
effect.Ghosts[g].color = ghostColor;
}
if (effect.Renderer != null && effect.Duration > 0f)
{
float fadeProgress = effect.Fade > 0f
? Mathf.Clamp01((effect.Elapsed - effect.Hold) / effect.Fade) : 1f;
Color spriteColor = effect.Renderer.color;
spriteColor.a = 1f - fadeProgress;
effect.Renderer.color = spriteColor;
if (effect.Elapsed >= effect.Duration) RemoveEffectAt(i, effect);
continue;
}
if (effect.Line != null)
{
Color color = effect.Line.startColor;
color.a = 1f - progress;
effect.Line.startColor = effect.Line.endColor = color;
}
if (effect.Elapsed >= effect.Duration) RemoveEffectAt(i, effect);
continue;
}
effect.Elapsed += dt;
while (effect.Elapsed >= effect.Durations[Mathf.Clamp(effect.Frame, 0, effect.Durations.Count - 1)])
{
effect.Elapsed -= effect.Durations[Mathf.Clamp(effect.Frame, 0, effect.Durations.Count - 1)];
int next = effect.Frame + 1;
if (next >= effect.Frames.Length)
{
if (!effect.Loop) { RemoveEffectAt(i, effect); break; }
next = 0;
}
effect.Frame = next;
if (effect.Renderer != null) effect.Renderer.sprite = effect.Frames[next];
}
}
}
private void RemoveEffectAt(int index, SpriteEffect effect)
{
if (effect == aura) aura = null;
if (effect == readyAura) readyAura = null;
if (effect == chargeFlash) chargeFlash = null;
if (effect == switchPulse) switchPulse = null;
if (effect == guardImpact) guardImpact = null;
if (effect == blockedBump) blockedBump = null;
if (effect.Kind == EffectKind.Bump) ActiveBumpImpactCount = Mathf.Max(0, ActiveBumpImpactCount - 1);
if (effect.Kind == EffectKind.Artifact) ActiveArtifactImpactCount = Mathf.Max(0, ActiveArtifactImpactCount - 1);
if (effect.Kind == EffectKind.Hurt) ActivePlayerHurtImpactCount = Mathf.Max(0, ActivePlayerHurtImpactCount - 1);
heavyEffects.Remove(effect);
if (effect.PoolLineWhenRemoved && effect.Root != null)
{
RecycleLineRoot(effect.Root, effect.Line);
}
else if (effect.Root != null) Destroy(effect.Root);
effects.RemoveAt(index);
}
private void RecycleLineRoot(GameObject root, LineRenderer line)
{
if (line != null) line.enabled = false;
root.SetActive(false);
lineEffectPool.Push(root);
}
private void ApplyPlayerTint()
{
if (world == null || world.Player == null || world.Player.Visual == null) return;
Color tint = Color.white;
if (blinkRemaining > 0f && GameplayConstants.Current.CombatFeedback.InvulnerabilityBlinkInterval > 0f
&& Mathf.FloorToInt(blinkElapsed / GameplayConstants.Current.CombatFeedback.InvulnerabilityBlinkInterval) % 2 == 0)
{
float alpha = GameplayConstants.Current.CombatFeedback.InvulnerabilityBlinkAlpha;
tint = new Color(1f, 1f, 1f, alpha);
}
if (flashRemaining > 0f)
{
tint.r = flashColor.r;
tint.g = flashColor.g;
tint.b = flashColor.b;
}
world.Player.Visual.SetFeedbackTint(tint);
}
private Color HitColor(CombatHitResult? hit)
{
if (hit.HasValue && hit.Value.IsArtifactHit)
return ActiveArtifactDefinition.GetArtifactPaletteColor(ActiveArtifactDefinition.GetArtifactColor(hit.Value.ArtifactEffect));
if (world.Loadout != null && world.Loadout.CurrentArtifact != null)
return ActiveArtifactDefinition.GetArtifactPaletteColor(world.Loadout.CurrentArtifact.ArtifactColor);
if (!hit.HasValue) return new Color(1f, .18f, .12f, 1f);
if (hit.Value.IsStrongDash) return new Color(.95f, .5f, 1f, 1f);
if (hit.Value.IsDash) return new Color(.2f, .95f, 1f, 1f);
if (hit.Value.HasBackBonus) return new Color(1f, .55f, .12f, 1f);
if (hit.Value.HasPositionBonus) return new Color(.65f, 1f, .3f, 1f);
return new Color(.78f, .72f, 1f, 1f);
}
private void RequestHitStop(float duration)
{
if (duration <= 0f) return;
if (!ownsTimeFreeze)
{
if (Time.timeScale <= 0f) return;
capturedTimeScale = Time.timeScale;
ownsTimeFreeze = true;
Time.timeScale = 0f;
}
hitStopUntil = Mathf.Max(hitStopUntil, Time.unscaledTime + duration);
}
private void RequestCameraShake(float pixels, Vector2 direction)
{
if (pixels <= 0f || world.View == null) return;
shakePixels = pixels;
shakeDirection = direction.sqrMagnitude > 0f ? direction.normalized : Vector2.zero;
cameraShakeDuration = GameplayConstants.Current.CombatFeedback.CameraShakeDuration;
cameraShakeUntil = Time.unscaledTime + cameraShakeDuration;
directionalPunch = false;
}
private void ClearChargeVisuals()
{
ClearEffect(ref aura);
ClearEffect(ref readyAura);
ClearEffect(ref chargeFlash);
ClearEffect(ref switchPulse);
}
private void ClearEffect(ref SpriteEffect effect)
{
if (effect == null) return;
if (effect.Root != null) Destroy(effect.Root);
effects.Remove(effect);
effect = null;
}
private void ClearAllEffects()
{
foreach (SpriteEffect effect in effects)
if (effect != null && effect.Root != null) Destroy(effect.Root);
effects.Clear();
aura = readyAura = chargeFlash = switchPulse = null;
guardImpact = blockedBump = null;
}
private Sprite[] LoadFrames(string path)
{
if (string.IsNullOrEmpty(path)) return Array.Empty<Sprite>();
if (!frameCache.TryGetValue(path, out Sprite[] frames))
{
frames = Resources.LoadAll<Sprite>(path);
Array.Sort(frames, (a, b) => string.CompareOrdinal(a.name, b.name));
frameCache.Add(path, frames);
}
return frames;
}
private Vector2 PlayerVisualGroundPosition
{
get
{
if (world == null || world.Player == null || world.Player.Visual == null)
return world != null && world.Player != null ? world.Player.Center : Vector2.zero;
Vector3 foot = world.Player.Visual.FootPosition;
return new Vector2(foot.x, foot.z);
}
}
private Vector3 ToWorld(Vector2 point) => new(point.x, world.Settings.GroundY + .015f, point.y);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a149c72e9544596b216326c1b27fbe4
@@ -0,0 +1,971 @@
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Player;
using UnityEngine;
using UnityEngine.InputSystem;
namespace BumpCombat.Defense.Comparison
{
// An independent Physics3D experiment. No RunManager, PlayerController or 2D proxy.
public sealed partial class Defense25DComparison : MonoBehaviour
{
private const int PortalLaneCount = 2;
[SerializeField] private Defense25DComparisonSettings settings;
[SerializeField] private Defense25DView view;
private readonly List<Defense25DEnemy> enemies = new();
private readonly List<Defense25DSummon> summons = new();
private readonly Collider[] overlap = new Collider[128];
private readonly HashSet<Defense25DActor> unique = new();
private readonly List<Defense25DActor> candidates = new();
private readonly Dictionary<Defense25DActor, float> nextBump = new();
private readonly List<Transform> adjustedArenaTransforms = new();
private readonly List<Vector3> originalArenaPositions = new();
private readonly List<Vector3> originalArenaScales = new();
private readonly DefenseWaveLedger[] portalLedgers = new DefenseWaveLedger[PortalLaneCount];
private readonly int[] suppliedPerLane = new int[PortalLaneCount];
private readonly bool[] portalSupplyEnded = new bool[PortalLaneCount];
private readonly GameObject[] portalRewards = new GameObject[PortalLaneCount];
private readonly Vector2[][] routes = new Vector2[PortalLaneCount][];
private readonly List<ActiveArtifactDefinition> startupArtifactOptions = new();
private Defense25DCycloneVisual cycloneVisual;
private Defense25DCombatFeedback combatFeedback;
private Defense25DPlayerArt playerArt;
private float previousChargeProgress = -1f;
private MeshFilter adjustedSouthBoundary;
private Mesh originalSouthBoundaryMesh;
private Mesh adjustedSouthBoundaryMesh;
private Vector2 input;
private Vector2 aim = Vector2.right;
private Vector2 cycloneDirection;
private float nextSpawn, reviveAt, cycloneDuration, cycloneInterval;
private float cycloneDistance, cycloneRange, buffUntil;
private bool cycloneCharged, cycloneBlocked, initialized;
private int nextCycloneTick, scheduledCycloneTicks;
private int nextSupplyLane;
private int startupSelectionsRemaining, startupSelectionTotal, startupSelectionIndex;
private ArtifactColor startupArtifactColor;
private bool startupSelectionPending;
private float fallbackGauge;
private const string CycloneBuff = "defense-25d-cyclone";
public Defense25DComparisonSettings Settings => settings;
public Vector2 ArenaHalfExtents { get; private set; }
public Defense25DView View => view;
public Defense25DActor Player { get; private set; }
public IReadOnlyList<Defense25DEnemy> Enemies => enemies;
public IReadOnlyList<Defense25DSummon> Summons => summons;
public DefenseWaveLedger Ledger { get; private set; }
public Defense25DTowerManager TowerManager { get; private set; }
public Defense25DLoadout Loadout { get; private set; }
public Defense25DArtifactExecutor ArtifactExecutor { get; private set; }
public Defense25DRelicRuntime RelicRuntime { get; private set; }
public float GameTime { get; private set; }
public float Gauge => Loadout != null ? Loadout.Gauge : fallbackGauge;
public float BaseHealth { get; private set; }
public int KillCurrency { get; private set; }
public bool IsRunning { get; private set; }
public bool RunCleared { get; private set; }
public bool RunFailed { get; private set; }
public bool Paused { get; private set; }
public bool AutoSpawn { get; set; } = true;
public bool ReadInput { get; set; } = true;
public bool ScreenRelativeMovement { get; private set; } = true;
public bool CycloneActive { get; private set; }
public float CycloneElapsed { get; private set; }
public int CycloneTicks { get; private set; }
public int CycloneDamageEvents { get; private set; }
public int BumpEvents { get; private set; }
public bool ShowBoundaries { get; set; } = true;
public bool TowersOperational => !Paused && DefensePrototypeRules.CanOperateTowers(
Player != null && Player.IsAlive, IsRunning);
public float TowerEfficiency
{
get
{
if (Player == null || TowerManager == null) return 0f;
float best = 0f;
foreach (Defense25DTower tower in TowerManager.Towers)
if (tower != null) best = Mathf.Max(best, tower.SightEfficiency);
return best;
}
}
public Defense25DEnemy CommandTarget { get; private set; }
public Defense25DCycloneVisual CycloneVisual => cycloneVisual;
public Defense25DCombatFeedback CombatFeedback => combatFeedback;
public Defense25DPlayerArt PlayerArt => playerArt;
private DefenseConstants Tuning => GameplayConstants.Current.Defense;
public Vector2[] GetRouteForLane(int lane) => routes[Mathf.Clamp(lane, 0, PortalLaneCount - 1)];
public bool IsTowerImmuneToEnemy(DefenseTowerKind kind, Defense25DEnemy enemy) =>
enemy == null || DefensePrototypeRules.IsTowerImmune(kind, Tuning.GetPortalImmuneTowerKind(enemy.Lane));
private void Start()
{
if (initialized || settings == null) return;
// This scene has no RunManager to resume a paused project's clock.
Time.timeScale = 1f;
Initialize(settings, view);
}
public void Initialize(Defense25DComparisonSettings configuration, Defense25DView display = null)
{
if (initialized) return;
settings = configuration;
view = display;
initialized = true;
ArenaHalfExtents = settings.MapHalfExtents;
ReduceSouthBoundaryRenderHeight();
Player = CreateActor(ComparisonActorKind.Player, "Player (Physics3D)", settings.PlayerSpawn);
combatFeedback = gameObject.AddComponent<Defense25DCombatFeedback>();
combatFeedback.Bind(this);
playerArt = gameObject.AddComponent<Defense25DPlayerArt>();
playerArt.Bind(this);
for (int lane = 0; lane < PortalLaneCount; lane++)
{
routes[lane] = new[]
{
Tuning.GetPortalPosition(lane),
Tuning.GetWaypointPosition(lane, 0),
Tuning.GetWaypointPosition(lane, 1),
Tuning.GetBasePosition(lane),
};
for (int i = 0; i < Tuning.PortalEnemyCount; i++)
{
int id = lane * Tuning.PortalEnemyCount + i;
Defense25DActor actor = CreateActor(ComparisonActorKind.Enemy,
$"Portal {lane + 1} Enemy {i + 1}", routes[lane][0]);
Defense25DEnemy enemy = actor.gameObject.AddComponent<Defense25DEnemy>();
enemy.Configure(this, actor, id, lane);
AttachEnemyStatusVisual(enemy);
enemies.Add(enemy);
}
}
CreateRuntimeRouteVisuals();
TowerManager = gameObject.AddComponent<Defense25DTowerManager>();
TowerManager.Configure(this);
ArtifactConstants artifactTuning = GameplayConstants.Current.Artifacts;
Loadout = new Defense25DLoadout(artifactTuning.StartingGauge, artifactTuning.MaximumGauge,
artifactTuning.MaximumOwnedArtifacts, artifactTuning.MaximumArtifactsPerColor);
Loadout.ConfigureCatalog(artifactTuning.Definitions);
ArtifactExecutor = new Defense25DArtifactExecutor();
ArtifactExecutor.Configure(this, Loadout);
ConfigureRelicRuntime();
ConfigureRelicEffectCallbacks();
ConfigureRelicReplayCallbacks();
CreatePortalRewards();
cycloneVisual = new GameObject("Cyclone comparison").AddComponent<Defense25DCycloneVisual>();
cycloneVisual.transform.SetParent(transform, false);
cycloneVisual.Configure(this);
ResetRun();
}
private void ReduceSouthBoundaryRenderHeight()
{
GameObject boundary = GameObject.Find("South boundary");
if (boundary == null) return;
ExpandArenaForSummonSlots(boundary);
MeshFilter meshFilter = boundary != null ? boundary.GetComponent<MeshFilter>() : null;
if (meshFilter == null || meshFilter.sharedMesh == null) return;
originalSouthBoundaryMesh = meshFilter.sharedMesh;
adjustedSouthBoundaryMesh = Instantiate(originalSouthBoundaryMesh);
adjustedSouthBoundaryMesh.name = originalSouthBoundaryMesh.name + " (comparison render height)";
adjustedSouthBoundary = meshFilter;
Mesh mesh = adjustedSouthBoundaryMesh;
Vector3[] vertices = mesh.vertices;
float floor = mesh.bounds.min.y;
float fraction = Mathf.Clamp01(settings.SouthBoundaryRenderHeightFraction);
for (int i = 0; i < vertices.Length; i++)
vertices[i].y = floor + (vertices[i].y - floor) * fraction;
mesh.vertices = vertices;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
meshFilter.sharedMesh = mesh;
}
private void ExpandArenaForSummonSlots(GameObject southBoundary)
{
BoxCollider collider = southBoundary.GetComponent<BoxCollider>();
if (collider == null) return;
DefenseConstants tuning = GameplayConstants.Current.Defense;
int maximumSummons = tuning.SummonUnitCount + 4;
int lastSlot = Mathf.Max(0, maximumSummons - 1);
float lastSlotOffset = -.55f - .8f * (lastSlot / 3);
float southernmostSlot = float.PositiveInfinity;
for (int pad = 0; pad < 6; pad++)
southernmostSlot = Mathf.Min(southernmostSlot,
tuning.GetBuildPadPosition(pad).y + lastSlotOffset);
float maximumVariantRadius = Defense25DComparisonSettings.GiantSlimeBodyCoreDiameterPixels
* 1.55f / Defense25DComparisonSettings.SummonBodyPixelsPerUnit * .5f;
float requiredNorthFace = southernmostSlot - maximumVariantRadius - settings.MovementSkin;
float shiftSouth = Mathf.Min(0f, requiredNorthFace - collider.bounds.max.z);
if (shiftSouth >= 0f) return;
float extension = -shiftSouth;
float wallThickness = collider.bounds.size.z;
AdjustArenaTransform(southBoundary.transform,
southBoundary.transform.position + Vector3.forward * shiftSouth,
southBoundary.transform.localScale);
AdjustArenaObject("North boundary", Vector3.forward * extension, 0f);
float perimeterScaleOffset = extension * 2f + wallThickness;
AdjustArenaObject("XZ ground", Vector3.zero, perimeterScaleOffset);
AdjustArenaObject("East boundary", Vector3.zero, perimeterScaleOffset);
AdjustArenaObject("West boundary", Vector3.zero, perimeterScaleOffset);
ArenaHalfExtents += Vector2.up * extension;
}
private void AdjustArenaObject(string objectName, Vector3 positionOffset, float zScaleOffset)
{
GameObject target = GameObject.Find(objectName);
if (target == null) return;
Vector3 scale = target.transform.localScale;
scale.z += zScaleOffset;
AdjustArenaTransform(target.transform, target.transform.position + positionOffset, scale);
}
private void AdjustArenaTransform(Transform target, Vector3 position, Vector3 scale)
{
int index = adjustedArenaTransforms.IndexOf(target);
if (index < 0)
{
adjustedArenaTransforms.Add(target);
originalArenaPositions.Add(target.position);
originalArenaScales.Add(target.localScale);
}
target.position = position;
target.localScale = scale;
}
private void OnDestroy()
{
for (int i = 0; i < adjustedArenaTransforms.Count; i++)
{
Transform target = adjustedArenaTransforms[i];
if (target == null) continue;
target.position = originalArenaPositions[i];
target.localScale = originalArenaScales[i];
}
if (adjustedSouthBoundary != null
&& adjustedSouthBoundary.sharedMesh == adjustedSouthBoundaryMesh)
adjustedSouthBoundary.sharedMesh = originalSouthBoundaryMesh;
if (adjustedSouthBoundaryMesh != null) Destroy(adjustedSouthBoundaryMesh);
}
private Defense25DActor CreateActor(ComparisonActorKind kind, string label, Vector2 position)
{
GameObject go = new(label);
go.transform.SetParent(transform, false);
go.transform.position = Defense25DActor.OnGround(position, settings.GroundY);
Defense25DActor actor = go.AddComponent<Defense25DActor>();
actor.Configure(this, kind);
return actor;
}
public void RegisterSummon(Defense25DSummon summon)
{
if (summon != null && !summons.Contains(summon)) summons.Add(summon);
}
public void UnregisterSummon(Defense25DSummon summon) => summons.Remove(summon);
internal static void AttachEnemyStatusVisual(Defense25DEnemy enemy)
{
if (enemy == null) return;
Defense25DStatusVisual visual = enemy.GetComponent<Defense25DStatusVisual>();
if (visual == null) visual = enemy.gameObject.AddComponent<Defense25DStatusVisual>();
visual.Bind(enemy);
}
private void CreatePortalRewards()
{
for (int lane = 0; lane < PortalLaneCount; lane++)
{
GameObject reward = GameObject.CreatePrimitive(PrimitiveType.Sphere);
reward.name = $"Portal {lane + 1} Construction Reward";
reward.transform.SetParent(transform, false);
reward.transform.localScale = Vector3.one * .38f;
reward.transform.position = Defense25DActor.OnGround(
Tuning.GetPortalPosition(lane) + Vector2.up * 1.2f, settings.GroundY + .3f);
Destroy(reward.GetComponent<Collider>());
reward.GetComponent<Renderer>().material.color = new Color32(255, 224, 84, 255);
reward.SetActive(false);
portalRewards[lane] = reward;
}
}
private void CreateRuntimeRouteVisuals()
{
for (int segment = 1; segment <= 4; segment++)
GameObject.Find($"Route segment {segment}")?.SetActive(false);
GameObject.Find("Portal")?.SetActive(false);
GameObject.Find("Base - arrival marker")?.SetActive(false);
GameObject.Find("South obstacle")?.SetActive(false);
Transform root = new GameObject("Defense routes and build pads").transform;
root.SetParent(transform, false);
void Box(string label, Vector2 center, Vector3 size, Material material, float height)
{
GameObject item = GameObject.CreatePrimitive(PrimitiveType.Cube);
item.name = label;
item.transform.SetParent(root, false);
item.transform.position = Defense25DActor.OnGround(center, settings.GroundY + height);
item.transform.localScale = size;
item.GetComponent<Renderer>().sharedMaterial = material;
Destroy(item.GetComponent<Collider>());
}
for (int lane = 0; lane < PortalLaneCount; lane++)
{
for (int i = 0; i < routes[lane].Length - 1; i++)
{
Vector2 start = routes[lane][i], end = routes[lane][i + 1];
Vector2 delta = end - start;
GameObject segment = GameObject.CreatePrimitive(PrimitiveType.Cube);
segment.name = $"Portal {lane + 1} route segment {i + 1}";
segment.transform.SetParent(root, false);
segment.transform.position = Defense25DActor.OnGround((start + end) * .5f, settings.GroundY - .005f);
segment.transform.localScale = new Vector3(.65f, .015f, delta.magnitude);
segment.transform.rotation = Quaternion.LookRotation(Defense25DActor.OnGround(delta.normalized));
segment.GetComponent<Renderer>().sharedMaterial = settings.PathMaterial;
Destroy(segment.GetComponent<Collider>());
}
Box($"Portal {lane + 1} marker", routes[lane][0], new Vector3(1f, .12f, 1f), settings.PathMaterial, .06f);
}
Box("Shared base marker", Tuning.GetBasePosition(0), new Vector3(1.25f, .4f, 1.25f), settings.BaseMaterial, .2f);
for (int pad = 0; pad < 6; pad++)
Box($"Tower build pad {pad + 1}", Tuning.GetBuildPadPosition(pad),
new Vector3(.8f, .08f, .8f), settings.BaseMaterial, .035f);
}
public void ResetRun()
{
if (!initialized) return;
ResetPresentationRewards();
combatFeedback?.ResetFeedback();
playerArt?.ResetVisuals();
previousChargeProgress = -1f;
ArtifactExecutor.Cancel();
Loadout.ResetRun();
RelicRuntime?.BeginRun();
if (view != null) BeginStartupArtifactSelection();
CancelCyclone();
GameTime = nextSpawn = reviveAt = buffUntil = 0f;
BaseHealth = Tuning.BaseMaximumHealth;
fallbackGauge = GameplayConstants.Current.Artifacts.StartingGauge;
BumpEvents = CycloneTicks = CycloneDamageEvents = KillCurrency = 0;
input = Vector2.zero;
aim = Vector2.right;
Paused = startupSelectionPending;
IsRunning = true;
RunCleared = RunFailed = false;
CommandTarget = null;
nextBump.Clear();
nextSupplyLane = 0;
Ledger = new DefenseWaveLedger(enemies.Count);
for (int lane = 0; lane < PortalLaneCount; lane++)
{
portalLedgers[lane] = new DefenseWaveLedger(Tuning.PortalEnemyCount);
suppliedPerLane[lane] = 0;
portalSupplyEnded[lane] = false;
portalRewards[lane]?.SetActive(false);
}
TowerManager.ResetForRun();
Player.ResetAt(settings.PlayerSpawn);
foreach (Defense25DEnemy enemy in enemies) enemy.ResetForRun();
foreach (Defense25DSummon summon in summons) summon.ResetForRun();
Physics.SyncTransforms();
}
private void Update()
{
if (!initialized || !ReadInput || Keyboard.current == null) return;
Keyboard k = Keyboard.current;
if (k.rKey.wasPressedThisFrame) ResetRun();
if (k.pKey.wasPressedThisFrame && !startupSelectionPending
&& !HasPendingFieldRelicReward && !showingArtifactSelection)
SetPaused(!Paused);
if (k.escapeKey.wasPressedThisFrame) CancelCyclone();
if (k.bKey.wasPressedThisFrame) ShowBoundaries = !ShowBoundaries;
if (k.cKey.wasPressedThisFrame) ScreenRelativeMovement = !ScreenRelativeMovement;
if (k.tabKey.wasPressedThisFrame) cycloneVisual.Ribbon = !cycloneVisual.Ribbon;
if (k.mKey.wasPressedThisFrame) cycloneVisual.Transparent = !cycloneVisual.Transparent;
if (k.hKey.wasPressedThisFrame) cycloneVisual.Height = cycloneVisual.Height < .4f ? .65f : .15f;
if (k.vKey.wasPressedThisFrame) cycloneVisual.DecorationSpeed = cycloneVisual.DecorationSpeed < 2f ? 4f : 1f;
if (k.nKey.wasPressedThisFrame) cycloneVisual.RadiusScale = cycloneVisual.RadiusScale < 1.1f ? 1.35f : 1f;
if (k.gKey.wasPressedThisFrame && IsRunning && !Paused) DebugFillGauge();
if (startupSelectionPending)
{
if (k.leftArrowKey.wasPressedThisFrame || k.upArrowKey.wasPressedThisFrame)
startupSelectionIndex = (startupSelectionIndex + startupArtifactOptions.Count - 1)
% startupArtifactOptions.Count;
else if (k.rightArrowKey.wasPressedThisFrame || k.downArrowKey.wasPressedThisFrame)
startupSelectionIndex = (startupSelectionIndex + 1) % startupArtifactOptions.Count;
if (k.spaceKey.wasPressedThisFrame) ChooseStartupArtifact(startupSelectionIndex);
return;
}
if (IsTowerShowcaseSimulationActive)
{
input = Vector2.zero;
return;
}
if (!IsRunning || Paused || !Player.IsAlive) { input = Vector2.zero; return; }
Mouse mouse = Mouse.current;
bool pointerOverUI = mouse != null && IsPointerOverDefenseUI(mouse.position.ReadValue());
if (HasPendingFieldRelicReward || showingArtifactSelection
|| (pointerOverUI && mouse != null
&& (mouse.leftButton.wasPressedThisFrame || mouse.leftButton.wasReleasedThisFrame)))
{
input = Vector2.zero;
return;
}
if (k.dKey.wasPressedThisFrame) Player.TryActivateGuard();
if (k.sKey.wasPressedThisFrame && !ArtifactExecutor.IsExecutingArtifact
&& !ArtifactExecutor.IsDashing && Loadout.OwnedArtifactCount > 0)
{
int current = 0;
for (int i = 0; i < Loadout.OwnedArtifacts.Count; i++)
if (ReferenceEquals(Loadout.OwnedArtifacts[i], Loadout.CurrentArtifact)) { current = i; break; }
if (Loadout.Select((current + 1) % Loadout.OwnedArtifactCount))
{
combatFeedback?.NotifyArtifactSwitched(Loadout.CurrentArtifact);
playerArt?.Bind(this);
}
}
if (k.aKey.wasPressedThisFrame) ArtifactExecutor.OnArtifactButtonPressed();
if (k.aKey.wasReleasedThisFrame) ArtifactExecutor.OnArtifactButtonReleased();
if (TowersOperational)
{
if (k.digit1Key.wasPressedThisFrame || k.numpad1Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Direct);
else if (k.digit2Key.wasPressedThisFrame || k.numpad2Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Summon);
else if (k.digit3Key.wasPressedThisFrame || k.numpad3Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Lightning);
else if (k.digit4Key.wasPressedThisFrame || k.numpad4Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Cold);
else if (k.digit5Key.wasPressedThisFrame || k.numpad5Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Seismic);
else if (k.digit6Key.wasPressedThisFrame || k.numpad6Key.wasPressedThisFrame)
TryBuildNearestTower(DefenseTowerKind.Empowering);
else if (k.uKey.wasPressedThisFrame)
TryUpgradeNearestTower(DefenseTowerBranch.None);
else if (k.jKey.wasPressedThisFrame)
TryUpgradeNearestTower(DefenseTowerBranch.First);
else if (k.kKey.wasPressedThisFrame)
TryUpgradeNearestTower(DefenseTowerBranch.Second);
}
// D is the original guard binding; movement remains on the original arrow-key layout.
Vector2 keys = new((k.rightArrowKey.isPressed ? 1 : 0)
- (k.leftArrowKey.isPressed ? 1 : 0),
(k.wKey.isPressed || k.upArrowKey.isPressed ? 1 : 0)
- (k.downArrowKey.isPressed ? 1 : 0));
input = view != null && ScreenRelativeMovement ? view.KeyboardToGround(keys) : Vector2.ClampMagnitude(keys, 1f);
if (view != null && mouse != null && view.TryScreenToGround(mouse.position.ReadValue(), settings.GroundY, out Vector2 point))
{
Vector2 direction = point - Player.Position;
if (direction.sqrMagnitude > .0001f) aim = direction.normalized;
}
}
private bool TryBuildNearestTower(DefenseTowerKind kind)
{
int nearestPad = -1;
float nearestDistance = Tuning.BuildInteractionRadius * Tuning.BuildInteractionRadius;
for (int pad = 0; pad < 6; pad++)
{
if (TowerManager.GetTowerAtPad(pad) != null) continue;
float distance = (Player.Position - Tuning.GetBuildPadPosition(pad)).sqrMagnitude;
if (distance > nearestDistance) continue;
nearestDistance = distance;
nearestPad = pad;
}
return nearestPad >= 0 && TowerManager.TryBuildTowerAtPad(nearestPad, kind);
}
private void BeginStartupArtifactSelection()
{
startupSelectionsRemaining = Mathf.Min(3, GameplayConstants.Current.Artifacts.MaximumOwnedArtifacts);
startupSelectionTotal = startupSelectionsRemaining;
startupSelectionPending = startupSelectionsRemaining > 0;
startupSelectionIndex = 0;
if (startupSelectionPending) PopulateStartupArtifactOptions();
}
private void PopulateStartupArtifactOptions()
{
startupArtifactOptions.Clear();
int selectionIndex = Mathf.Min(3 - startupSelectionsRemaining, 2);
startupArtifactColor = selectionIndex switch
{
0 => ArtifactColor.Green,
1 => ArtifactColor.Red,
_ => ArtifactColor.Blue,
};
for (int i = 0; i < Loadout.Catalog.Count; i++)
{
ActiveArtifactDefinition definition = Loadout.Catalog[i];
if (definition != null && definition.ArtifactColor == startupArtifactColor
&& !Loadout.OwnsArtifact(definition.Effect))
startupArtifactOptions.Add(definition);
}
if (startupArtifactOptions.Count == 0) startupSelectionPending = false;
}
private bool ChooseStartupArtifact(int index)
{
if (!startupSelectionPending || index < 0 || index >= startupArtifactOptions.Count
|| !Loadout.TryAddArtifact(startupArtifactOptions[index])) return false;
startupSelectionsRemaining--;
if (startupSelectionsRemaining <= 0)
{
startupSelectionPending = false;
Paused = false;
startupArtifactOptions.Clear();
return true;
}
PopulateStartupArtifactOptions();
startupSelectionIndex = 0;
if (!startupSelectionPending) Paused = false;
return true;
}
private bool TryUpgradeNearestTower(DefenseTowerBranch branch)
{
Defense25DTower nearest = null;
float nearestDistance = Tuning.BuildInteractionRadius * Tuning.BuildInteractionRadius;
foreach (Defense25DTower tower in TowerManager.TowersInConstructionOrder)
{
if (tower == null) continue;
float distance = (Player.Position - tower.Position).sqrMagnitude;
if (distance > nearestDistance) continue;
nearestDistance = distance;
nearest = tower;
}
return nearest != null && TowerManager.TryUpgradeTowerAtPad(nearest.PadIndex, branch);
}
public void SetMovementInput(Vector2 direction) => input = Vector2.ClampMagnitude(direction, 1f);
public void DebugFillGauge()
{
if (Loadout != null) Loadout.AddGauge(Loadout.MaximumGauge);
else fallbackGauge = GameplayConstants.Current.Artifacts.MaximumGauge;
}
public void SetPaused(bool value) { Paused = value; input = Vector2.zero; }
private void FixedUpdate()
{
if (!initialized || !IsRunning || Paused) return;
float dt = Time.fixedDeltaTime;
if (IsTowerShowcaseSimulationActive)
{
AdvanceTowerShowcase(dt);
TowerManager.Step(dt);
return;
}
GameTime += dt;
RelicRuntime?.Advance(GameTime, Player.CurrentHealth);
if (buffUntil > 0f && GameTime >= buffUntil)
{
Player.RemoveModifiersFromSource(CycloneBuff);
buffUntil = 0f;
}
if (!Player.IsAlive && GameTime >= reviveAt)
{
Player.ResetAt(settings.PlayerSpawn);
Player.GrantInvulnerability(Tuning.PlayerReviveInvulnerabilitySeconds);
}
if (AutoSpawn && GameTime >= nextSpawn && Ledger.SuppliedCount < enemies.Count)
{
SpawnNextEnemy();
nextSpawn = GameTime + Tuning.PortalSpawnInterval;
}
if (Player.IsAlive)
{
if (CycloneActive) StepCyclone(dt);
else if (Player.HasKnockback || Player.IsHurt) Player.Move(Player.ConsumeKnockback(dt));
else
{
Vector2 velocity = input * Player.MoveSpeed;
ResolveBumps(velocity);
if (Player.IsAlive && !Player.IsHurt)
{
Vector2 moved = Player.Move(velocity * dt);
if (moved.sqrMagnitude > .0000001f)
{
AddGauge(GameplayConstants.Current.Artifacts.MovementGaugeGainPerSecond
* Mathf.Min(dt, moved.magnitude / Mathf.Max(.001f, Player.MoveSpeed)));
RelicRuntime?.AddVoluntaryMovement(moved.magnitude,
Player.Radius * 2f, Player.CurrentHealth);
}
}
}
if (input.sqrMagnitude > .001f) Player.Facing = input.normalized;
}
CommandTarget = null;
ArtifactExecutor.Step(dt);
float chargeProgress = Loadout != null && Loadout.IsCharging ? Loadout.ChargeProgress : 0f;
if (Loadout?.CurrentArtifact != null)
combatFeedback?.NotifyChargeChanged(Loadout.CurrentArtifact, chargeProgress,
chargeProgress >= 1f && previousChargeProgress < 1f);
previousChargeProgress = chargeProgress;
StepRelicHealingOrbs();
TowerManager.Step(dt);
foreach (Defense25DEnemy enemy in enemies) enemy.Step(dt);
foreach (Defense25DSummon summon in summons) summon.Step(dt);
OfferPortalRewards();
TryCollectPortalRewards();
TryFinishRunClear();
}
public Defense25DEnemy SpawnNextEnemy()
{
if (Ledger.SuppliedCount >= enemies.Count) return null;
for (int attempt = 0; attempt < PortalLaneCount; attempt++)
{
int lane = (nextSupplyLane + attempt) % PortalLaneCount;
if (suppliedPerLane[lane] >= Tuning.PortalEnemyCount) continue;
Defense25DEnemy enemy = null;
foreach (Defense25DEnemy candidate in enemies)
{
if (candidate.Lane == lane && candidate.CanSpawn) { enemy = candidate; break; }
}
if (enemy == null || !Ledger.RegisterSupply(enemy.Id)
|| !portalLedgers[lane].RegisterSupply(enemy.Id)) return null;
suppliedPerLane[lane]++;
enemy.Spawn(routes[lane][0]);
if (suppliedPerLane[lane] >= Tuning.PortalEnemyCount)
{
portalSupplyEnded[lane] = true;
portalLedgers[lane].MarkSupplyEnded();
}
nextSupplyLane = (lane + 1) % PortalLaneCount;
if (Ledger.SuppliedCount >= enemies.Count) Ledger.MarkSupplyEnded();
return enemy;
}
return null;
}
public bool TryCollectRewardAtLane(int lane)
{
if (lane < 0 || lane >= PortalLaneCount || !IsRunning || Paused
|| Player == null || !Player.IsAlive || portalRewards[lane] == null
|| !portalLedgers[lane].IsRewardOffered
|| Vector2.Distance(Player.Position, Tuning.GetPortalPosition(lane) + Vector2.up * 1.2f)
> Tuning.RewardPickupRadius
|| !portalLedgers[lane].TryCollectReward()) return false;
portalRewards[lane].SetActive(false);
TowerManager.AddCurrency(Tuning.PortalRewardCurrency);
QueueFieldRelicRewardForCollectedPortal(lane);
TryFinishRunClear();
return true;
}
private void OfferPortalRewards()
{
for (int lane = 0; lane < PortalLaneCount; lane++)
{
if (!portalSupplyEnded[lane] || !portalLedgers[lane].TryOfferReward()) continue;
if (portalRewards[lane] == null)
{
GameObject reward = GameObject.CreatePrimitive(PrimitiveType.Sphere);
reward.name = $"Portal {lane + 1} Construction Reward";
reward.transform.SetParent(transform, false);
reward.transform.localScale = Vector3.one * .38f;
reward.transform.position = Defense25DActor.OnGround(
Tuning.GetPortalPosition(lane) + Vector2.up * 1.2f, settings.GroundY + .3f);
Destroy(reward.GetComponent<Collider>());
reward.GetComponent<Renderer>().material.color = new Color32(255, 224, 84, 255);
portalRewards[lane] = reward;
}
portalRewards[lane].SetActive(true);
}
if (portalSupplyEnded[0] && portalSupplyEnded[1]) Ledger.TryOfferReward();
}
private void TryCollectPortalRewards()
{
for (int lane = 0; lane < PortalLaneCount; lane++)
if (portalRewards[lane] != null && portalRewards[lane].activeSelf)
TryCollectRewardAtLane(lane);
}
private void TryFinishRunClear()
{
if (!IsRunning || RunFailed || Ledger.ResolvedCount < enemies.Count
|| !portalSupplyEnded[0] || !portalSupplyEnded[1]
|| !portalLedgers[0].IsRewardCollected || !portalLedgers[1].IsRewardCollected
|| !CanFinishRunClearPresentation) return;
Ledger.TryOfferReward();
Ledger.TryCollectReward();
RunCleared = true;
EndRun();
}
private void AddGauge(float gain)
{
if (CycloneActive || !Player.IsAlive || !IsRunning || Paused) return;
float adjusted = Player.Evaluate(CharacterStat.ArtifactGaugeGain, gain);
if (Loadout != null) Loadout.AddGauge(adjusted);
else fallbackGauge = Mathf.Clamp(fallbackGauge + adjusted,
0f, GameplayConstants.Current.Artifacts.MaximumGauge);
}
// A Physics3D broadphase only. The final footprint/ellipse checks below use XZ.
public IReadOnlyList<Defense25DActor> QueryEnemies(Vector2 center, Vector2 radii)
{
candidates.Clear();
unique.Clear();
Vector3 low = Defense25DActor.OnGround(center, settings.GroundY + .05f);
Vector3 high = low + Vector3.up * settings.ActorHeight;
int count = Physics.OverlapBoxNonAlloc((low + high) * .5f,
new Vector3(radii.x + 1f, settings.ActorHeight, radii.y + 1f), overlap,
Quaternion.identity, Defense25DComparisonSettings.ActorMask, QueryTriggerInteraction.Ignore);
for (int i = 0; i < count; i++)
{
Defense25DActor actor = overlap[i].GetComponentInParent<Defense25DActor>();
if (actor != null && actor.Kind == ComparisonActorKind.Enemy && actor.IsAlive && unique.Add(actor))
candidates.Add(actor);
}
candidates.Sort((a, b) => (a.Position - center).sqrMagnitude.CompareTo((b.Position - center).sqrMagnitude));
return candidates;
}
public void ResolveBumps(Vector2 attackVelocity)
{
CombatConstants combat = GameplayConstants.Current.Combat;
if (!IsRunning || Paused || !Player.IsAlive || Player.IsHurt || Player.HasKnockback || CycloneActive
|| attackVelocity.magnitude < combat.MinimumAttackSpeed) return;
QueryEnemies(Player.Center, Vector2.one * (Player.Radius + .5f));
var bumpTargets = new List<Defense25DActor>(candidates);
foreach (Defense25DActor enemy in bumpTargets)
{
if (Player.GapTo(enemy) > settings.ContactSensorMargin
|| (nextBump.TryGetValue(enemy, out float next) && GameTime < next)) continue;
Vector2 toward = (enemy.Position - Player.Position).normalized;
if (Vector2.Dot(attackVelocity.normalized, toward) < combat.MinimumApproachDot) continue;
HitSide side = BumpCombatMath.ClassifySide(enemy.Facing, Player.Position - enemy.Position);
bool offset = BumpCombatMath.IsOffsetHit(side, attackVelocity, Player.Position, enemy.Position, combat.MinimumOffsetDistance);
float baseDamage = Player.CalculateDamage(combat.BaseDamage, DamageTag.Collision);
float damage = BumpCombatMath.ApplySideMultiplier(baseDamage, side, offset);
damage = RelicRuntime?.ApplyTrophyBumpDamage(
RelicRuntime.ApplyOrdinaryBumpDamage(damage, true, false), true) ?? damage;
float force = combat.KnockbackForce * (side == HitSide.Front ? combat.FrontKnockbackMultiplier : 1f);
if (!enemy.TakeDamage(damage, toward, force,
BumpCombat.Enemies.EnemyDamageSource.Player, ordinaryBump: true))
{
combatFeedback?.NotifyBlockedBump(enemy.ClosestGroundPoint(Player.Center),
attackVelocity.normalized);
continue;
}
nextBump[enemy] = GameTime + combat.NormalHitInterval;
BumpEvents++;
AddGauge(GameplayConstants.Current.Artifacts.NormalBumpGaugeGain);
Player.Visual.ShowAttack(side == HitSide.Back);
if (side == HitSide.Back) enemy.GetComponent<Defense25DEnemy>()?.InterruptAttackFromRearHit();
if (side == HitSide.Front && !offset && combat.EnableFrontalBumpSelfDamage)
Player.TakeDamage(settings.Enemy.AttackDamage, -toward, GameplayConstants.Current.Enemies.NormalPlayerKnockbackDistance);
CombatEvents.RaiseValidHit(new CombatHitResult(
Player.gameObject,
enemy.gameObject,
false,
side,
damage,
toward,
force,
enemy.ClosestGroundPoint(Player.Center),
offset,
damageTag: DamageTag.Collision,
sourceId: null,
baseBumpDamage: damage));
RelicRuntime?.NotifyHit(CreateRelicHit(enemy.GetComponent<Defense25DEnemy>(), damage,
toward, baseDamage, true, false), Player.CurrentHealth, Player.MaxHealth);
}
}
public bool TryUseCyclone(bool charged, Vector2 direction)
{
if (!IsRunning || Paused || !Player.IsAlive || Player.IsHurt || CycloneActive) return false;
var definition = settings.Cyclone;
float cost = charged ? definition.ChargedGaugeCost : definition.NormalGaugeCost;
if (Gauge < cost) return false;
if (Loadout != null) Loadout.AddGauge(-cost);
else fallbackGauge = Mathf.Max(0f, fallbackGauge - cost);
CycloneActive = true;
cycloneCharged = charged;
cycloneBlocked = false;
CycloneElapsed = 0f;
CycloneTicks = CycloneDamageEvents = nextCycloneTick = 0;
cycloneDuration = Mathf.Max(Time.fixedDeltaTime, charged ? definition.ChargedDuration : definition.NormalDuration);
cycloneInterval = Mathf.Max(Time.fixedDeltaTime, definition.HitInterval);
cycloneDistance = charged ? definition.ChargedMoveDistance : definition.NormalMoveDistance;
cycloneRange = charged ? definition.ChargedRange : definition.NormalRange;
cycloneDirection = direction.sqrMagnitude > .0001f ? direction.normalized : Player.Facing;
scheduledCycloneTicks = CycloneGeometry.ScheduledHitCount(cycloneDuration, cycloneInterval);
float invulnerability = charged ? definition.ChargedInvulnerabilityDuration : definition.NormalInvulnerabilityDuration;
if (invulnerability > 0f) Player.GrantInvulnerability(invulnerability);
cycloneVisual.SetVisible(true);
return true;
}
public Vector2 CycloneCenter => Player.Position + CycloneGeometry.CenterOffset(cycloneRange, cycloneCharged, settings.PlayerVisualScale);
public Vector2 CycloneRadii => CycloneGeometry.Radii(cycloneRange, cycloneCharged);
public bool IsCycloneCharged => cycloneCharged;
private void StepCyclone(float dt)
{
while (nextCycloneTick < scheduledCycloneTicks && CycloneElapsed + .0001f >= nextCycloneTick * cycloneInterval)
{
DealCycloneTick(nextCycloneTick == scheduledCycloneTicks - 1);
nextCycloneTick++;
CycloneTicks++;
}
if (CycloneElapsed >= cycloneDuration)
{
// Includes a tick exactly on the duration boundary, without losing it to a frame step.
while (nextCycloneTick < scheduledCycloneTicks)
{
DealCycloneTick(nextCycloneTick == scheduledCycloneTicks - 1);
nextCycloneTick++;
CycloneTicks++;
}
CycloneActive = false;
cycloneVisual.SetVisible(false);
Player.RemoveModifiersFromSource(CycloneBuff);
float increase = cycloneCharged ? settings.Cyclone.ChargedMoveSpeedIncrease : settings.Cyclone.NormalMoveSpeedIncrease;
Player.AddModifier(new StatModifier(CycloneBuff, CharacterStat.MoveSpeed, ModifierOperation.Increased, increase));
buffUntil = GameTime + (cycloneCharged ? settings.Cyclone.ChargedBuffDuration : settings.Cyclone.NormalBuffDuration);
return;
}
float nextTime = Mathf.Min(cycloneDuration, CycloneElapsed + dt);
if (!cycloneBlocked)
{
Player.Move(cycloneDirection * (cycloneDistance * (nextTime - CycloneElapsed) / cycloneDuration), false);
cycloneBlocked = Player.MovementBlocked;
}
CycloneElapsed = nextTime;
}
private void DealCycloneTick(bool final)
{
Vector2 center = CycloneCenter;
Vector2 radii = CycloneRadii;
QueryEnemies(center, radii);
int cap = cycloneCharged ? settings.Cyclone.ChargedMaxTargets : settings.Cyclone.NormalMaxTargets;
int hit = 0;
foreach (Defense25DActor enemy in candidates)
{
if (hit >= cap) break;
if (!CycloneGeometry.Overlaps(center, radii, enemy.ClosestPointFunction)) continue;
float damage = Player.CalculateDamage(cycloneCharged ? settings.Cyclone.ChargedDamage : settings.Cyclone.NormalDamage,
settings.Cyclone.DamageTag);
float knockback = final ? (cycloneCharged ? settings.Cyclone.ChargedKnockback : settings.Cyclone.NormalKnockback) : 0f;
if (!enemy.TakeDamage(damage, enemy.Position - center, knockback)) continue;
hit++;
CycloneDamageEvents++;
}
}
public void CancelCyclone()
{
CycloneActive = false;
cycloneBlocked = false;
if (cycloneVisual != null) cycloneVisual.SetVisible(false);
}
public void NotifyDeath(Defense25DActor actor)
{
if (actor == Player)
{
CancelCyclone();
Player.RemoveModifiersFromSource(CycloneBuff);
buffUntil = 0f;
reviveAt = GameTime + Tuning.PlayerReviveDelaySeconds;
RelicRuntime?.Advance(GameTime, 0f);
foreach (Defense25DSummon summon in summons) summon.SuspendAttack();
CommandTarget = null;
}
else if (actor.Kind == ComparisonActorKind.Enemy)
{
Defense25DEnemy enemy = actor.GetComponent<Defense25DEnemy>();
if (enemy != null && enemy.IsShowcaseDummy) NotifyTowerShowcaseDummyDeath(enemy);
else ResolveEnemy(enemy, DefenseEnemyResolution.Death);
}
else actor.GetComponent<Defense25DSummon>()?.BeginRespawn();
}
public void ResolveEnemy(Defense25DEnemy enemy, DefenseEnemyResolution resolution)
{
if (enemy == null || !Ledger.TryResolve(enemy.Id, resolution)) return;
if (resolution == DefenseEnemyResolution.Death)
RelicRuntime?.NotifyEnemyDeath(CreateRelicTarget(enemy), Player.CurrentHealth);
portalLedgers[Mathf.Clamp(enemy.Lane, 0, PortalLaneCount - 1)]?.TryResolve(enemy.Id, resolution);
if (resolution == DefenseEnemyResolution.Death) KillCurrency += Tuning.EnemyKillCurrency;
if (resolution == DefenseEnemyResolution.Death) TowerManager.AddCurrency(Tuning.EnemyKillCurrency);
else BaseHealth = Mathf.Max(0f, BaseHealth - Tuning.ArrivalBaseDamage);
if (resolution == DefenseEnemyResolution.Death) enemy.RetireForDeathVisual();
else enemy.Retire();
if (DefensePrototypeRules.IsBaseDestroyed(BaseHealth))
{
RunFailed = true;
EndRun();
}
}
public void EndRun()
{
IsRunning = false;
ArtifactExecutor?.Cancel();
RelicRuntime?.EndRun();
CancelCyclone();
input = Vector2.zero;
CommandTarget = null;
Player.RemoveModifiersFromSource(CycloneBuff);
TowerManager?.CancelAll();
Player.StopBody();
foreach (Defense25DEnemy enemy in enemies) enemy.Actor.StopBody();
foreach (Defense25DSummon summon in summons) { summon.SuspendAttack(); summon.Actor.StopBody(); }
}
private void OnDisable()
{
CleanupPresentationDamagePopups();
combatFeedback?.ResetFeedback();
if (combatFeedback != null) combatFeedback.enabled = false;
if (initialized) EndRun();
}
private void RestoreRuntimeFeedbackAfterEnable()
{
if (!initialized) return;
if (combatFeedback != null)
{
if (!combatFeedback.enabled) combatFeedback.enabled = true;
else if (combatFeedback.TransparentEffectMaterial == null) combatFeedback.Bind(this);
}
if (playerArt != null)
{
playerArt.ResetVisuals();
playerArt.Bind(this);
}
}
private void NotifyPlayerArtifactUseVisual(Defense25DArtifactUse use)
{
if (Player == null || use.Definition == null) return;
if (use.Effect == ActiveArtifactEffect.Dash) Player.Visual.ShowDash();
else Player.Visual.ShowArtifactUse();
combatFeedback?.NotifyArtifactReleased(use.Definition,
Player.Position, use.Charged);
}
private void OnGUI()
{
DrawHud(Defense25DView.GetViewport(Screen.width, Screen.height));
}
private void DrawHud(Rect viewport) => DrawDefensePresentationGUI(viewport);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3da342f87d5c43a5905256f7b5515c7e
@@ -0,0 +1,731 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BumpCombat.Combat;
using System.Text.RegularExpressions;
using BumpCombat.Constants;
using BumpCombat.Defense;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.UI;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
public sealed partial class Defense25DComparison
{
private static readonly Rect RunStatusRect = new(7, 5, 626, 20);
private static readonly Rect ArtifactBarRect = new(7, 28, 626, 25);
private static readonly Rect BottomControlsRect = new(7, 320, 626, 37);
private static readonly Rect FieldRelicPanelRect = new(112, 96, 416, 168);
private static readonly Rect StartupArtifactPanelRect = new(107, 88, 426, 184);
private static readonly Rect ArtifactPickerRect = new(378, 61, 250, 151);
private static readonly Rect[] PersistentDefenseUiRects =
{ RunStatusRect, ArtifactBarRect, BottomControlsRect };
private static readonly DefenseTowerKind[] PresentationTowerKinds =
{
DefenseTowerKind.Direct, DefenseTowerKind.Summon, DefenseTowerKind.Lightning,
DefenseTowerKind.Cold, DefenseTowerKind.Seismic, DefenseTowerKind.Empowering,
};
private DefenseTowerKind selectedBuildKind = DefenseTowerKind.Direct;
private int selectedBuildPad = -1;
private int startupArtifactPage;
private readonly Queue<PassiveRelicId[]> earnedRelicChoices = new();
private readonly bool[] relicChoiceQueuedForLane = new bool[2];
private bool resumeAfterRelicChoice;
private Func<bool> enterValidationShowcase;
private Action exitValidationShowcase;
private bool validationShowcaseActive;
private readonly CombatDamagePopup[] combatDamagePopups =
new CombatDamagePopup[Defense25DCombatPopupConstants.MaximumConcurrentPopups];
private int activeCombatDamagePopupCount;
private Defense25DCombatPopupLifecycle combatPopupLifecycle;
private bool combatPopupEventsSubscribed;
private bool hasPresentationMessage;
private string presentationMessage;
private float presentationMessageUntil;
private GUIStyle compactStatusStyle;
private GUIStyle combatDamagePopupStyle;
private struct CombatDamagePopup
{
public string Text;
public Color Color;
public Vector3 Position;
public float StartedAt;
public bool IsTowerPopup;
public void Set(string text, Color color, Vector3 position, bool isTowerPopup)
{
Text = text;
Color = color;
Position = position;
StartedAt = Time.unscaledTime;
IsTowerPopup = isTowerPopup;
}
}
public IReadOnlyList<DefenseTowerKind> TowerShowcaseKinds => PresentationTowerKinds;
public int SelectedBuildPad => selectedBuildPad;
public DefenseTowerKind SelectedBuildKind => selectedBuildKind;
public IReadOnlyList<PassiveRelicId> PendingFieldRelicChoices =>
earnedRelicChoices.Count == 0 ? Array.Empty<PassiveRelicId>() : earnedRelicChoices.Peek();
public bool HasPendingFieldRelicReward => earnedRelicChoices.Count > 0;
public bool CanFinishRunClearPresentation => !HasPendingFieldRelicReward;
public bool IsValidationShowcaseActive => validationShowcaseActive;
public int ActiveTowerDamagePopupCount
{
get
{
PruneCombatDamagePopups();
int count = 0;
for (int i = 0; i < activeCombatDamagePopupCount; i++)
if (combatDamagePopups[i].IsTowerPopup) count++;
return count;
}
}
public int ActiveCombatDamagePopupCount
{
get
{
PruneCombatDamagePopups();
return activeCombatDamagePopupCount;
}
}
public Rect[] GetPersistentDefenseUiHitRects() => (Rect[])PersistentDefenseUiRects.Clone();
public Rect GetSelectedPadDetailsRect()
{
if (selectedBuildPad < 0 || TowerManager == null) return Rect.zero;
float x = 8f;
if (view != null && view.Camera != null)
{
Vector2 pad = GameplayConstants.Current.Defense.GetBuildPadPosition(selectedBuildPad);
Vector3 projected = view.Camera.WorldToViewportPoint(
new Vector3(pad.x, settings.GroundY, pad.y));
if (projected.x < .5f) x = 422f;
}
return new Rect(x, 119, 210, 122);
}
public void RecordTowerDamagePopup(float actualDamage, Color color, Vector3 worldPosition)
{
if (actualDamage > 0f)
AddCombatDamagePopup($"-{DamageCalculator.FormatDamage(actualDamage)}",
color, worldPosition, isTowerPopup: true);
}
public void NotifyPlayerDamagePopup(float actualDamage, Vector3 worldPosition)
{
if (actualDamage > 0f)
AddCombatDamagePopup($"-{DamageCalculator.FormatDamage(actualDamage)}",
Defense25DCombatPopupConstants.PlayerDamageColor, worldPosition,
isTowerPopup: false);
}
private void HandleComparisonValidHit(CombatHitResult result)
{
if (Player == null || result.Attacker != Player.gameObject || result.Target == null) return;
Defense25DEnemy target = result.Target.GetComponentInParent<Defense25DEnemy>();
if (target == null || !enemies.Contains(target)) return;
string damageText = DamageCalculator.FormatDamage(result.Damage);
string popupText = !string.IsNullOrEmpty(result.SourceId)
? $"{GetArtifactHitDisplayName(result.SourceId)} {damageText}"
: result.HasBackBonus
? $"BACK ×1.5 {damageText}"
: result.IsOffsetHit
? $"OFFSET ×1.2 {damageText}"
: result.HasPositionBonus
? $"SIDE ×1.2 {damageText}"
: damageText;
if (result.LaunchSucceeded) popupText = $"LAUNCH {popupText}";
Color color = TryGetArtifactPopupColor(result, out Color artifactColor)
? artifactColor
: result.LaunchSucceeded
? Defense25DCombatPopupConstants.LaunchColor
: result.HasBackBonus
? Defense25DCombatPopupConstants.BackHitColor
: result.HasPositionBonus
? Defense25DCombatPopupConstants.SideHitColor
: Color.white;
AddCombatDamagePopup(popupText, color, result.Target.transform.position,
isTowerPopup: false);
}
private static bool TryGetArtifactPopupColor(CombatHitResult result, out Color color)
{
if (result.IsArtifactHit)
{
color = GetArtifactPopupColor(result.ArtifactEffect);
return true;
}
if (TryGetLegacyArtifactEffect(result.SourceId, out ActiveArtifactEffect effect))
{
color = GetArtifactPopupColor(effect);
return true;
}
color = Color.white;
return false;
}
private static Color GetArtifactPopupColor(ActiveArtifactEffect effect)
{
if (effect < ActiveArtifactEffect.Dash || effect > ActiveArtifactEffect.ChainLightning)
return Color.white;
return ActiveArtifactDefinition.GetArtifactPaletteColor(
ActiveArtifactDefinition.GetArtifactColor(effect));
}
private static bool TryGetLegacyArtifactEffect(string sourceId, out ActiveArtifactEffect effect)
{
if (string.Equals(sourceId, "dash", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.Dash;
else if (string.Equals(sourceId, "pulse", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.Pulse;
else if (string.Equals(sourceId, "phoenix", StringComparison.OrdinalIgnoreCase)
|| string.Equals(sourceId, "scorching_ray", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.Phoenix;
else if (string.Equals(sourceId, "cyclone", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.Cyclone;
else if (string.Equals(sourceId, "thunder_crash", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.ThunderCrash;
else if (string.Equals(sourceId, "chain_lightning", StringComparison.OrdinalIgnoreCase)
|| string.Equals(sourceId, "arc", StringComparison.OrdinalIgnoreCase))
effect = ActiveArtifactEffect.ChainLightning;
else
{
effect = default;
return false;
}
return true;
}
private string GetArtifactHitDisplayName(string sourceId)
{
if (Loadout != null)
foreach (ActiveArtifactDefinition definition in Loadout.Catalog)
if (string.Equals(definition.ArtifactId, sourceId,
StringComparison.OrdinalIgnoreCase)) return definition.DisplayName;
return sourceId.ToUpperInvariant();
}
private void AddCombatDamagePopup(string text, Color color, Vector3 worldPosition,
bool isTowerPopup)
{
if (!isActiveAndEnabled) return;
PruneCombatDamagePopups();
if (activeCombatDamagePopupCount >= combatDamagePopups.Length) return;
combatDamagePopups[activeCombatDamagePopupCount++].Set(
text, color, worldPosition, isTowerPopup);
}
private void PruneCombatDamagePopups()
{
float now = Time.unscaledTime;
for (int i = activeCombatDamagePopupCount - 1; i >= 0; i--)
{
if (now - combatDamagePopups[i].StartedAt
< Defense25DCombatPopupConstants.LifetimeSeconds) continue;
activeCombatDamagePopupCount--;
combatDamagePopups[i] = combatDamagePopups[activeCombatDamagePopupCount];
combatDamagePopups[activeCombatDamagePopupCount] = default;
}
}
private void InitializePresentationDamagePopups()
{
if (combatPopupLifecycle == null)
{
combatPopupLifecycle = GetComponent<Defense25DCombatPopupLifecycle>();
if (combatPopupLifecycle == null)
combatPopupLifecycle = gameObject.AddComponent<Defense25DCombatPopupLifecycle>();
}
combatPopupLifecycle.Bind(this);
SetCombatDamagePopupEventsSubscribed(true);
}
private void CleanupPresentationDamagePopups()
{
SetCombatDamagePopupEventsSubscribed(false);
}
private void OnEnable()
{
if (!initialized) return;
InitializePresentationDamagePopups();
RestoreRuntimeFeedbackAfterEnable();
}
internal void SetCombatDamagePopupEventsSubscribed(bool subscribed)
{
if (combatPopupEventsSubscribed == subscribed) return;
if (subscribed)
CombatEvents.OnValidHit += HandleComparisonValidHit;
else
{
CombatEvents.OnValidHit -= HandleComparisonValidHit;
activeCombatDamagePopupCount = 0;
Array.Clear(combatDamagePopups, 0, combatDamagePopups.Length);
}
combatPopupEventsSubscribed = subscribed;
}
public bool IsPointerOverDefenseUI(Vector2 screenPosition)
{
Rect viewport = Defense25DView.GetViewport(Screen.width, Screen.height);
if (viewport.width <= 0f) return false;
float scale = viewport.width / Defense25DView.Width;
Vector2 gui = new((screenPosition.x - viewport.x) / scale,
(Screen.height - screenPosition.y - viewport.y) / scale);
if (startupSelectionPending) return StartupArtifactPanelRect.Contains(gui);
if (HasPendingFieldRelicReward) return FieldRelicPanelRect.Contains(gui);
if (IsValidationShowcaseActive) return new Rect(7, 5, 626, 28).Contains(gui);
if (RunStatusRect.Contains(gui) || ArtifactBarRect.Contains(gui)
|| BottomControlsRect.Contains(gui)) return true;
Rect details = GetSelectedPadDetailsRect();
return details.width > 0f && details.Contains(gui)
|| showingArtifactSelection && ArtifactPickerRect.Contains(gui);
}
public void BindValidationShowcase(Func<bool> enter, Action exit)
{
enterValidationShowcase = enter;
exitValidationShowcase = exit;
}
public bool EnterValidationShowcase()
{
if (validationShowcaseActive || enterValidationShowcase == null
|| HasPendingFieldRelicReward || !enterValidationShowcase()) return false;
validationShowcaseActive = true;
showingArtifactSelection = false;
return true;
}
public bool ExitValidationShowcase()
{
if (!validationShowcaseActive || exitValidationShowcase == null) return false;
exitValidationShowcase();
validationShowcaseActive = false;
return true;
}
public Sprite GetTowerShowcaseSprite(DefenseTowerKind kind)
{
if (!PresentationTowerKinds.Contains(kind)) return null;
Sprite[] frames = DefenseTowerArt.Load(DefenseTowerArt.BodyKey(kind, 1, "Idle"));
return frames.Length == 0 ? null : frames[0];
}
public bool SelectBuildKind(DefenseTowerKind kind)
{
if (!PresentationTowerKinds.Contains(kind)) return false;
selectedBuildKind = kind;
return true;
}
public bool SelectBuildPad(int padIndex)
{
if (TowerManager == null || padIndex < 0 || padIndex >= TowerManager.Towers.Count) return false;
selectedBuildPad = padIndex;
return true;
}
public bool BuildSelectedTower()
{
bool built = selectedBuildPad >= 0
&& TowerManager.TryBuildTowerAtPad(selectedBuildPad, selectedBuildKind);
SetPresentationMessage(built ? $"{TowerName(selectedBuildKind)} built" : "Move closer to the selected pad");
return built;
}
public bool UpgradeSelectedTower(DefenseTowerBranch branch)
{
bool upgraded = selectedBuildPad >= 0
&& TowerManager.TryUpgradeTowerAtPad(selectedBuildPad, branch);
SetPresentationMessage(upgraded ? "Tower upgraded" : "Upgrade unavailable at this pad");
return upgraded;
}
public bool SelectOwnedArtifact(int index)
{
if (Loadout == null || !Loadout.Select(index)) return false;
ArtifactExecutor?.Cancel();
return true;
}
public bool QueueFieldRelicRewardForCollectedPortal(int lane)
{
if (lane < 0 || lane >= relicChoiceQueuedForLane.Length || relicChoiceQueuedForLane[lane]
|| Loadout == null || portalLedgers[lane] == null || !portalLedgers[lane].IsRewardCollected)
return false;
PassiveRelicId[] available = Loadout.GetAvailableRelics();
if (available.Length == 0) return false;
for (int i = available.Length - 1; i > 0; i--)
{
int swap = UnityEngine.Random.Range(0, i + 1);
(available[i], available[swap]) = (available[swap], available[i]);
}
earnedRelicChoices.Enqueue(available.Take(Mathf.Min(3, available.Length)).ToArray());
relicChoiceQueuedForLane[lane] = true;
if (earnedRelicChoices.Count == 1)
{
resumeAfterRelicChoice = IsRunning && !Paused && !startupSelectionPending;
SetPaused(true);
}
return true;
}
public bool ResolveFieldRelicChoice(PassiveRelicId id)
{
if (!HasPendingFieldRelicReward || !earnedRelicChoices.Peek().Contains(id)
|| !Loadout.TryAcquireRelic(id)) return false;
earnedRelicChoices.Dequeue();
if (earnedRelicChoices.Count == 0)
{
if (resumeAfterRelicChoice && IsRunning && Player != null && Player.IsAlive)
SetPaused(false);
resumeAfterRelicChoice = false;
TryFinishRunClear();
}
return true;
}
public void ResetPresentationRewards()
{
InitializePresentationDamagePopups();
activeCombatDamagePopupCount = 0;
Array.Clear(combatDamagePopups, 0, combatDamagePopups.Length);
earnedRelicChoices.Clear();
Array.Clear(relicChoiceQueuedForLane, 0, relicChoiceQueuedForLane.Length);
resumeAfterRelicChoice = false;
if (validationShowcaseActive) ExitValidationShowcase();
}
public void DrawDefensePresentationGUI(Rect viewport)
{
if (!initialized || viewport.width <= 0f || viewport.height <= 0f) return;
float scale = viewport.width / Defense25DView.Width;
Matrix4x4 previous = GUI.matrix;
try
{
GUI.matrix = Matrix4x4.TRS(new Vector3(viewport.x, viewport.y, 0f),
Quaternion.identity, new Vector3(scale, scale, 1f));
DrawRunStatus();
DrawCombatDamagePopups(scale);
if (IsValidationShowcaseActive)
{
GUI.Box(new Rect(7, 5, 626, 28), "Six-family tower combat showcase");
if (GUI.Button(new Rect(548, 7, 76, 24), "General run")) ExitValidationShowcase();
return;
}
DrawArtifactBar();
DrawTowerCatalog();
DrawPadAndUpgradePanel();
DrawFieldRelicReward();
DrawStartupArtifactPicker();
if (hasPresentationMessage && GameTime < presentationMessageUntil)
GUI.Label(new Rect(245, 317, 150, 20), presentationMessage, CenteredStyle());
if (Paused && !startupSelectionPending && !HasPendingFieldRelicReward)
GUI.Box(new Rect(264, 159, 112, 32), "PAUSED");
if (RunFailed || RunCleared)
GUI.Box(new Rect(224, 151, 192, 42), RunCleared ? "DEFENSE CLEARED" : "BASE DESTROYED");
}
finally { GUI.matrix = previous; }
}
private void DrawCombatDamagePopups(float scale)
{
PruneCombatDamagePopups();
if (view == null || view.Camera == null) return;
Color previousColor = GUI.color;
try
{
combatDamagePopupStyle ??= new GUIStyle(GUI.skin.label)
{
font = PresentationUiStyle.GetGalmuriFont(),
fontSize = Mathf.RoundToInt(Defense25DCombatPopupConstants.FontPixelsAt1080p
* Defense25DCombatPopupConstants.ReferenceInternalHeight
/ Defense25DCombatPopupConstants.ReferenceScreenHeight),
alignment = TextAnchor.MiddleCenter,
wordWrap = false,
clipping = TextClipping.Overflow,
};
for (int i = 0; i < activeCombatDamagePopupCount; i++)
{
CombatDamagePopup popup = combatDamagePopups[i];
Vector3 projected = view.Camera.WorldToViewportPoint(popup.Position);
if (projected.z <= 0f || projected.x < 0f || projected.x > 1f
|| projected.y < 0f || projected.y > 1f) continue;
float age = Mathf.Max(0f, Time.unscaledTime - popup.StartedAt);
float x = projected.x * Defense25DView.Width;
float y = (1f - projected.y) * Defense25DView.Height
- age * Defense25DCombatPopupConstants.RisePixelsAt1080p
* Defense25DCombatPopupConstants.ReferenceInternalHeight
/ Defense25DCombatPopupConstants.ReferenceScreenHeight;
Rect rect = new(
x - Defense25DCombatPopupConstants.PopupWidthInternal * 0.5f,
y - Defense25DCombatPopupConstants.PopupHeightInternal * 0.5f,
Defense25DCombatPopupConstants.PopupWidthInternal,
Defense25DCombatPopupConstants.PopupHeightInternal);
Color faded = popup.Color;
faded.a *= 1f - age / Defense25DCombatPopupConstants.LifetimeSeconds;
Color outline = Defense25DCombatPopupConstants.OutlineColor;
outline.a *= faded.a;
GUI.color = outline;
float outlineOffset = Defense25DCombatPopupConstants.OutlinePixels
/ Mathf.Max(1f, scale);
GUI.Label(new Rect(rect.x - outlineOffset, rect.y, rect.width, rect.height),
popup.Text, combatDamagePopupStyle);
GUI.Label(new Rect(rect.x + outlineOffset, rect.y, rect.width, rect.height),
popup.Text, combatDamagePopupStyle);
GUI.Label(new Rect(rect.x, rect.y - outlineOffset, rect.width, rect.height),
popup.Text, combatDamagePopupStyle);
GUI.Label(new Rect(rect.x, rect.y + outlineOffset, rect.width, rect.height),
popup.Text, combatDamagePopupStyle);
GUI.color = faded;
GUI.Label(rect, popup.Text, combatDamagePopupStyle);
}
}
finally { GUI.color = previousColor; }
}
private void DrawRunStatus()
{
GUI.Box(RunStatusRect, "");
if (Player == null || TowerManager == null || Ledger == null) return;
compactStatusStyle ??= new GUIStyle(GUI.skin.label) { fontSize = 9 };
GUI.Label(new Rect(14, 6, 612, 18),
$"HP {Player.CurrentHealth:0}/{Player.MaxHealth:0} Base {BaseHealth:0} " +
$"Wave {Ledger.ResolvedCount}/{enemies.Count} Build {TowerManager.ConstructionCurrency} " +
$"{GameTime:0.0}s Relics {Loadout.OwnedRelicCount}/20 " +
$"Efficiency {TowerEfficiency:P0} {(IsRunning ? "DEFENDING" : "RUN ENDED")}", compactStatusStyle);
}
private void DrawArtifactBar()
{
GUI.Box(ArtifactBarRect, "");
GUI.Label(new Rect(13, 29, 105, 23), $"Gauge {Gauge:0}");
if (Loadout.CurrentArtifact == null)
{
GUI.Label(new Rect(120, 29, 220, 23), "No artifact equipped");
}
else
{
GUI.Label(new Rect(120, 29, 225, 23), Loadout.CurrentArtifact.DisplayName);
GUI.Label(new Rect(347, 29, 113, 23), Loadout.IsCharging
? $"Charge {Loadout.ChargeProgress:P0}" : "Ready");
}
if (GUI.Button(new Rect(466, 29, 76, 23), "Artifacts")) showingArtifactSelection = !showingArtifactSelection;
if (HasPendingFieldRelicReward) GUI.Label(new Rect(548, 29, 77, 23), "RELIC REWARD");
else if (GUI.Button(new Rect(548, 29, 77, 23), IsValidationShowcaseActive ? "General run" : "Showcase"))
{
if (IsValidationShowcaseActive) ExitValidationShowcase();
else EnterValidationShowcase();
}
if (showingArtifactSelection) DrawOwnedArtifactPicker();
}
private bool showingArtifactSelection;
private void DrawOwnedArtifactPicker()
{
GUI.Box(ArtifactPickerRect, "Choose artifact");
for (int i = 0; i < Loadout.OwnedArtifacts.Count; i++)
{
ActiveArtifactDefinition artifact = Loadout.OwnedArtifacts[i];
if (GUI.Button(new Rect(390, 83 + i * 28, 226, 24), artifact.DisplayName)
&& SelectOwnedArtifact(i)) showingArtifactSelection = false;
}
}
private void DrawTowerCatalog()
{
GUI.Box(BottomControlsRect, "");
for (int i = 0; i < PresentationTowerKinds.Length; i++)
{
DefenseTowerKind kind = PresentationTowerKinds[i];
Rect button = new(12 + i * 51, 326, 49, 25);
string label = selectedBuildKind == kind ? $"*{TowerShortName(kind)}" : TowerShortName(kind);
if (GUI.Button(button, label))
SelectBuildKind(kind);
}
}
private void DrawPadAndUpgradePanel()
{
for (int pad = 0; pad < 6; pad++)
{
Defense25DTower tower = TowerManager.GetTowerAtPad(pad);
string label = tower == null ? $"P{pad + 1}" : $"P{pad + 1} T{tower.Tier}";
if (GUI.Button(new Rect(322 + pad * 51, 326, 49, 25), label)) SelectBuildPad(pad);
}
Rect panel = GetSelectedPadDetailsRect();
if (panel.width <= 0f) return;
Defense25DTower selected = selectedBuildPad >= 0 ? TowerManager.GetTowerAtPad(selectedBuildPad) : null;
GUI.Box(panel, "");
if (GUI.Button(new Rect(panel.xMax - 24, panel.y + 3, 20, 19), "×"))
{
selectedBuildPad = -1;
return;
}
if (selected == null)
{
int cost = BuildCost(selectedBuildKind);
GUI.Label(new Rect(panel.x + 10, panel.y + 20, panel.width - 20, 23),
$"Pad {selectedBuildPad + 1}: {TowerName(selectedBuildKind)}");
if (GUI.Button(new Rect(panel.x + 10, panel.y + 54, 112, 27), $"Build {cost}")
&& selectedBuildPad >= 0)
BuildSelectedTower();
return;
}
GUI.Label(new Rect(panel.x + 10, panel.y + 20, panel.width - 20, 24),
$"Pad {selectedBuildPad + 1}: {TowerName(selected.Kind)} T{selected.Tier}");
if (selected.Tier < 3)
{
if (GUI.Button(new Rect(panel.x + 10, panel.y + 54, 180, 27),
$"Upgrade {UpgradeCost(selected.Tier)}"))
UpgradeSelectedTower(DefenseTowerBranch.None);
}
else if (selected.Tier == 3)
{
string first = DefenseTower.GetBranchName(selected.Kind, DefenseTowerBranch.First);
string second = DefenseTower.GetBranchName(selected.Kind, DefenseTowerBranch.Second);
GUI.Label(new Rect(panel.x + 10, panel.y + 45, panel.width - 20, 27),
$"I {first} II {second}", compactStatusStyle);
if (GUI.Button(new Rect(panel.x + 10, panel.y + 76, 86, 27), $"I {UpgradeCost(3)}"))
UpgradeSelectedTower(DefenseTowerBranch.First);
if (GUI.Button(new Rect(panel.x + 104, panel.y + 76, 86, 27), $"II {UpgradeCost(3)}"))
UpgradeSelectedTower(DefenseTowerBranch.Second);
}
else GUI.Label(new Rect(panel.x + 10, panel.y + 54, 190, 42),
$"{TowerName(selected.Kind)} IV\n{DefenseTower.GetBranchName(selected.Kind, selected.Branch)}");
}
private void DrawFieldRelicReward()
{
if (!HasPendingFieldRelicReward) return;
PassiveRelicId[] choices = earnedRelicChoices.Peek();
GUI.Box(FieldRelicPanelRect, $"Portal field reward {Loadout.OwnedRelicCount}/20");
for (int i = 0; i < choices.Length; i++)
{
Rect button = new(132, 126 + i * 36, 376, 30);
if (GUI.Button(button, RelicName(choices[i]))) ResolveFieldRelicChoice(choices[i]);
}
}
private void DrawStartupArtifactPicker()
{
if (!startupSelectionPending) return;
GUI.Box(StartupArtifactPanelRect,
$"Choose {startupArtifactColor} artifact {Loadout.OwnedArtifactCount + 1}/{startupSelectionTotal}");
int pageCount = Mathf.Max(1, Mathf.CeilToInt(startupArtifactOptions.Count / 4f));
startupArtifactPage = Mathf.Clamp(startupArtifactPage, 0, pageCount - 1);
int firstChoice = startupArtifactPage * 4;
int lastChoice = Mathf.Min(startupArtifactOptions.Count, firstChoice + 4);
for (int i = firstChoice; i < lastChoice; i++)
{
Rect button = new(132, 119 + (i - firstChoice) * 32, 376, 28);
if (GUI.Button(button, startupArtifactOptions[i].DisplayName)) ChooseStartupArtifact(i);
}
if (startupArtifactPage > 0 && GUI.Button(new Rect(132, 250, 70, 21), "Previous")) startupArtifactPage--;
if (startupArtifactPage + 1 < pageCount && GUI.Button(new Rect(438, 250, 70, 21), "Next")) startupArtifactPage++;
}
private void SetPresentationMessage(string message)
{
presentationMessage = message;
presentationMessageUntil = GameTime + 2f;
hasPresentationMessage = true;
}
private int BuildCost(DefenseTowerKind kind)
{
DefenseConstants tuning = GameplayConstants.Current.Defense;
return kind switch
{
DefenseTowerKind.Direct => tuning.DirectTowerCost,
DefenseTowerKind.Summon => tuning.SummonTowerCost,
DefenseTowerKind.Lightning => tuning.LightningTowerCost,
DefenseTowerKind.Cold => tuning.ColdTowerCost,
DefenseTowerKind.Seismic => tuning.SeismicTowerCost,
DefenseTowerKind.Empowering => tuning.EmpoweringTowerCost,
_ => 0,
};
}
private int UpgradeCost(int tier) => tier switch
{
1 => GameplayConstants.Current.Defense.Tier2UpgradeCost,
2 => GameplayConstants.Current.Defense.Tier3UpgradeCost,
_ => GameplayConstants.Current.Defense.Tier4UpgradeCost,
};
private static string TowerName(DefenseTowerKind kind) => kind switch
{
DefenseTowerKind.Direct => "Fire",
DefenseTowerKind.Summon => "Summon",
DefenseTowerKind.Lightning => "Lightning",
DefenseTowerKind.Cold => "Cold",
DefenseTowerKind.Seismic => "Seismic",
DefenseTowerKind.Empowering => "Support",
_ => kind.ToString(),
};
private static string TowerShortName(DefenseTowerKind kind) => kind switch
{
DefenseTowerKind.Direct => "Fire",
DefenseTowerKind.Summon => "Summon",
DefenseTowerKind.Lightning => "Storm",
DefenseTowerKind.Cold => "Cold",
DefenseTowerKind.Seismic => "Earth",
DefenseTowerKind.Empowering => "Support",
_ => kind.ToString(),
};
private static string RelicName(PassiveRelicId id) =>
Regex.Replace(id.ToString(), "(?<=[a-z])(?=[A-Z])", " ");
private static GUIStyle CenteredStyle()
{
GUIStyle style = new(GUI.skin.label) { alignment = TextAnchor.MiddleCenter };
return style;
}
}
internal sealed class Defense25DCombatPopupLifecycle : MonoBehaviour
{
private Defense25DComparison owner;
public void Bind(Defense25DComparison comparison)
{
if (owner == comparison) return;
if (owner != null) owner.SetCombatDamagePopupEventsSubscribed(false);
owner = comparison;
if (isActiveAndEnabled && owner != null)
owner.SetCombatDamagePopupEventsSubscribed(true);
}
private void OnEnable()
{
if (owner != null) owner.SetCombatDamagePopupEventsSubscribed(true);
}
private void OnDisable()
{
if (owner != null) owner.SetCombatDamagePopupEventsSubscribed(false);
}
private void OnDestroy()
{
if (owner != null) owner.SetCombatDamagePopupEventsSubscribed(false);
owner = null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f8db4b736f6478fb580f7fe926c70c1
@@ -0,0 +1,13 @@
namespace BumpCombat.Defense.Comparison
{
public sealed partial class Defense25DComparison
{
private void ConfigureRelicReplayCallbacks()
{
relicEffects.EmitEcho = (use, origin, direction, multiplier) =>
ArtifactExecutor.TryReplay(use, origin, direction, multiplier);
relicEffects.EmitRepeat = (use, origin, direction, multiplier) =>
ArtifactExecutor.TryReplay(use, origin, direction, multiplier);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b7c43d5582674f77a9befa672a24bd6c
@@ -0,0 +1,212 @@
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Defense;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
public sealed partial class Defense25DComparison
{
private sealed class RelicHealingOrb
{
public GameObject Object;
public float ExpiresAt;
}
private readonly List<RelicHealingOrb> relicHealingOrbs = new();
private Defense25DRelicEffects relicEffects;
private float relicOrbLastStepTime;
private void ConfigureRelicRuntime()
{
relicEffects = new Defense25DRelicEffects();
RelicRuntime = new Defense25DRelicRuntime(
Loadout, GameplayConstants.Current.PassiveRelics, relicEffects);
Loadout.ArtifactUseSucceeded += NotifyRelicArtifactUse;
Loadout.ArtifactUseSucceeded += NotifyPlayerArtifactUseVisual;
ArtifactExecutor.NotifyRelicHit = NotifyRelicHit;
RelicRuntime.BeginRun(GameTime);
}
private void ConfigureRelicEffectCallbacks()
{
Defense25DRelicEffects effects = relicEffects;
effects.QueryEnemies = QueryRelicTargets;
effects.DamageEnemy = ApplyRelicDamage;
effects.IgniteEnemy = ApplyRelicIgnite;
effects.CalculatePlayerDamage = (damage, tag) => Player.CalculateDamage(damage, tag);
effects.ApplyVulnerability = (target, duration, increase) =>
FindRelicEnemy(target.Identity)?.ApplyBumpVulnerability(duration, increase) == true;
effects.ApplySlow = (target, duration, fraction) =>
FindRelicEnemy(target.Identity)?.ApplySlow(duration, fraction) == true;
effects.HealPlayer = amount => Player.Heal(amount);
effects.GrantShield = (amount, duration) => Player.GrantShield(amount, duration);
effects.RestoreGauge = fraction =>
{
if (!IsRunning || !Player.IsAlive || fraction <= 0f) return false;
float before = Gauge;
Loadout.AddGauge(Loadout.MaximumGauge * fraction);
return Gauge > before;
};
effects.BoostVisibleTowers = BoostVisibleRelicTowers;
effects.AwardCurrency = AwardRelicCurrency;
effects.SpawnHealingOrb = SpawnRelicHealingOrb;
effects.SetTimedPlayerModifier = (modifier, duration) =>
{
if (Player.IsAlive && duration > 0f) Player.AddModifier(modifier);
};
effects.RemovePlayerModifier = source => Player.RemoveModifiersFromSource(source);
}
private IReadOnlyList<Defense25DRelicTarget> QueryRelicTargets(Vector3 center, float radius)
{
var results = new List<Defense25DRelicTarget>();
float radiusSquared = radius * radius;
foreach (Defense25DEnemy enemy in enemies)
{
if (enemy == null || !enemy.IsAvailable) continue;
Vector2 closest = enemy.Actor.ClosestGroundPoint(new Vector2(center.x, center.z));
Vector2 delta = closest - new Vector2(center.x, center.z);
if (delta.sqrMagnitude <= radiusSquared) results.Add(CreateRelicTarget(enemy));
}
return results;
}
private bool ApplyRelicDamage(Defense25DRelicTarget target, float damage,
Vector3 direction, EnemyDamageSource source)
{
Defense25DEnemy enemy = FindRelicEnemy(target.Identity);
if (enemy == null || !enemy.IsAvailable || damage <= 0f) return false;
return enemy.Actor.TakeDamage(damage, new Vector2(direction.x, direction.z), 0f, source);
}
private bool ApplyRelicIgnite(Defense25DRelicTarget target, float duration,
float interval, float damage, EnemyDamageSource source)
{
return FindRelicEnemy(target.Identity)?.ApplyIgnite(duration, interval, damage, source) == true;
}
private Defense25DEnemy FindRelicEnemy(int identity)
{
foreach (Defense25DEnemy enemy in enemies)
if (enemy != null && enemy.Id == identity) return enemy;
return null;
}
private bool BoostVisibleRelicTowers(float duration)
{
if (!TowersOperational || duration <= 0f) return false;
bool applied = false;
foreach (Defense25DTower tower in TowerManager.Towers)
{
if (tower == null || tower.Kind is DefenseTowerKind.Empowering or DefenseTowerKind.Summon
|| Vector2.Distance(Player.Position, tower.Position) > Tuning.PlayerSightRadius) continue;
tower.ApplyBattleBaton(duration,
GameplayConstants.Current.PassiveRelics.BattleBatonTowerDamageIncrease);
applied = true;
}
return applied;
}
private bool AwardRelicCurrency(int amount)
{
if (!IsRunning || Player == null || !Player.IsAlive || amount <= 0) return false;
TowerManager.AddCurrency(amount);
return true;
}
private void SpawnRelicHealingOrb(Vector3 position, float healFraction, float lifetime)
{
if (!IsRunning || lifetime <= 0f) return;
GameObject orb = GameObject.CreatePrimitive(PrimitiveType.Sphere);
orb.name = "Pilgrim Healing Orb";
orb.transform.SetParent(transform, true);
orb.transform.position = position + Vector3.up * .2f;
orb.transform.localScale = Vector3.one * .3f;
Collider collider = orb.GetComponent<Collider>();
if (collider != null) Destroy(collider);
Renderer renderer = orb.GetComponent<Renderer>();
if (renderer != null) renderer.material.color = new Color(.35f, 1f, .55f);
relicHealingOrbs.Add(new RelicHealingOrb
{
Object = orb,
ExpiresAt = GameTime + lifetime,
});
}
private void StepRelicHealingOrbs()
{
if (GameTime < relicOrbLastStepTime)
{
foreach (RelicHealingOrb oldOrb in relicHealingOrbs)
if (oldOrb.Object != null) Destroy(oldOrb.Object);
relicHealingOrbs.Clear();
RelicRuntime?.NotifyHealingOrbExpired();
}
relicOrbLastStepTime = GameTime;
float pickupRadius = GameplayConstants.Current.Items.ExperienceOrbPickupRadius;
for (int i = relicHealingOrbs.Count - 1; i >= 0; i--)
{
RelicHealingOrb orb = relicHealingOrbs[i];
if (orb.Object == null || GameTime >= orb.ExpiresAt)
{
if (orb.Object != null) Destroy(orb.Object);
RelicRuntime?.NotifyHealingOrbExpired();
relicHealingOrbs.RemoveAt(i);
continue;
}
if (Player == null || !Player.IsAlive) continue;
Vector2 orbPosition = Defense25DActor.Ground(orb.Object.transform.position);
if ((Player.Position - orbPosition).sqrMagnitude > pickupRadius * pickupRadius) continue;
bool pickedUp = RelicRuntime?.NotifyHealingOrbPickedUp(Player.MaxHealth) == true;
Destroy(orb.Object);
relicHealingOrbs.RemoveAt(i);
}
}
private void NotifyRelicArtifactUse(Defense25DArtifactUse use)
{
if (Player == null) return;
RelicRuntime?.NotifyArtifactUse(use,
Defense25DActor.OnGround(Player.Position, settings.GroundY),
Defense25DActor.OnGround(Player.Facing), Player.CurrentHealth);
}
private void NotifyRelicHit(Defense25DRelicHit hit) =>
RelicRuntime?.NotifyHit(hit, Player != null ? Player.CurrentHealth : 0f,
Player != null ? Player.MaxHealth : 0f);
internal Defense25DRelicTarget CreateRelicTarget(Defense25DEnemy enemy)
{
Defense25DActor actor = enemy.Actor;
return new Defense25DRelicTarget(enemy.Id,
Defense25DActor.OnGround(actor.Position, settings.GroundY), actor.CurrentHealth,
actor.MaxHealth, enemy.LastDamageSource, !actor.IsAlive,
igniteRemaining: enemy.IgniteRemaining, igniteInterval: enemy.IgniteInterval,
igniteTickDamage: enemy.IgniteTickDamage);
}
public void NotifyPlayerDamageTaken(float actualLoss) =>
RelicRuntime?.NotifyPlayerDamageTaken(actualLoss, Player.CurrentHealth,
Player.MaxHealth, Defense25DActor.OnGround(Player.Position, settings.GroundY));
private Defense25DRelicHit CreateRelicHit(Defense25DEnemy enemy, float damage,
Vector2 direction, float baseBumpDamage, bool ordinaryBump, bool artifactHit)
{
Defense25DActor actor = enemy.Actor;
Defense25DRelicTarget target = new(enemy.Id,
Defense25DActor.OnGround(actor.Position, settings.GroundY), actor.CurrentHealth,
actor.MaxHealth, enemy.LastDamageSource, !actor.IsAlive,
igniteRemaining: enemy.IgniteRemaining, igniteInterval: enemy.IgniteInterval,
igniteTickDamage: enemy.IgniteTickDamage);
return new Defense25DRelicHit(target,
Defense25DActor.OnGround(actor.ClosestGroundPoint(Player.Center), settings.GroundY),
Defense25DActor.OnGround(direction), damage, baseBumpDamage, ordinaryBump,
artifactHit);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 71a94f1e2f6e49ca9a4c44b77f51054e
@@ -0,0 +1,262 @@
using System.Collections.Generic;
using System.Linq;
using BumpCombat.Constants;
using BumpCombat.Defense;
using UnityEngine;
namespace BumpCombat.Defense.Comparison
{
public sealed partial class Defense25DComparison
{
private static DefenseTowerKind[] ShowcaseKinds => DefenseTowerHitTestLayout.KindsByPad;
private readonly Dictionary<Defense25DEnemy, DefenseTowerKind> showcaseDummyKinds = new();
private readonly Dictionary<Defense25DEnemy, Vector2> showcaseDummyPositions = new();
private readonly Dictionary<Defense25DEnemy, int> showcaseObservedDamageEvents = new();
private readonly Dictionary<DefenseTowerKind, int> showcaseHitCounts = new();
private readonly HashSet<Defense25DEnemy> showcaseSuspendedEnemies = new();
private Defense25DTowerManager runTowerManager;
private Defense25DTowerManager showcaseTowerManager;
private GameObject showcaseRoot;
private Vector2 runPlayerPosition;
private float runGameTime;
private bool towerShowcaseActive;
public bool IsTowerShowcaseSimulationActive => towerShowcaseActive;
public IReadOnlyCollection<Defense25DEnemy> TowerShowcaseDummies => showcaseDummyKinds.Keys;
public int TowerShowcaseCurrency => showcaseTowerManager != null
? showcaseTowerManager.ConstructionCurrency : 0;
public IEnumerable<Defense25DEnemy> GetTowerTargets(DefenseTowerKind sourceKind)
{
foreach (Defense25DEnemy enemy in enemies)
if (enemy != null && !enemy.IsShowcaseDummy)
yield return enemy;
if (!towerShowcaseActive) yield break;
foreach (KeyValuePair<Defense25DEnemy, DefenseTowerKind> pair in showcaseDummyKinds)
if (pair.Value == sourceKind && pair.Key != null && pair.Key.IsAvailable)
yield return pair.Key;
}
public bool IsTowerShowcaseTarget(DefenseTowerKind sourceKind, Defense25DEnemy target)
{
if (target == null) return false;
if (!target.IsShowcaseDummy) return enemies.Contains(target);
return towerShowcaseActive
&& showcaseDummyKinds.TryGetValue(target, out DefenseTowerKind assignedKind)
&& assignedKind == sourceKind;
}
public Defense25DEnemy GetTowerShowcaseDummy(DefenseTowerKind kind)
{
foreach (KeyValuePair<Defense25DEnemy, DefenseTowerKind> pair in showcaseDummyKinds)
if (pair.Value == kind) return pair.Key;
return null;
}
public int GetTowerShowcaseHitCount(DefenseTowerKind kind) =>
showcaseHitCounts.TryGetValue(kind, out int count) ? count : 0;
public float GetTowerShowcaseSupportBonus(DefenseTowerKind kind)
{
if (showcaseTowerManager == null) return 0f;
foreach (Defense25DTower tower in showcaseTowerManager.Towers)
if (tower != null && tower.Kind == kind) return tower.ShowcaseSupportBonus;
return 0f;
}
public bool EnterTowerShowcase()
{
if (towerShowcaseActive || !IsRunning || Paused || RunCleared || RunFailed
|| Player == null || !Player.IsAlive || TowerManager == null
|| HasPendingFieldRelicReward || startupSelectionPending || showingArtifactSelection
|| CycloneActive || ArtifactExecutor.IsExecutingArtifact || ArtifactExecutor.IsDashing)
return false;
runTowerManager = TowerManager;
runPlayerPosition = Player.Position;
runGameTime = GameTime;
towerShowcaseActive = true;
showcaseSuspendedEnemies.Clear();
runTowerManager.SetShowcaseSuspended(true);
foreach (Defense25DEnemy enemy in enemies)
{
if (enemy == null || !enemy.gameObject.activeSelf) continue;
showcaseSuspendedEnemies.Add(enemy);
enemy.gameObject.SetActive(false);
}
showcaseRoot = new GameObject("Isolated tower showcase");
showcaseRoot.transform.SetParent(transform, false);
GameObject managerObject = new("Showcase construction economy");
managerObject.transform.SetParent(showcaseRoot.transform, false);
showcaseTowerManager = managerObject.AddComponent<Defense25DTowerManager>();
showcaseTowerManager.Configure(this);
TowerManager = showcaseTowerManager;
showcaseHitCounts.Clear();
int totalBuildCost = 0;
foreach (DefenseTowerKind kind in ShowcaseKinds) totalBuildCost += ShowcaseBuildCost(kind);
showcaseTowerManager.AddCurrency(totalBuildCost);
for (int i = 0; i < ShowcaseKinds.Length; i++)
{
Vector2 pad = Tuning.GetBuildPadPosition(i);
MovePlayerForShowcaseBuild(pad);
if (!showcaseTowerManager.TryBuildTowerAtPad(i, ShowcaseKinds[i]))
{
ExitTowerShowcase();
return false;
}
}
RestoreShowcasePlayerPosition();
for (int i = 0; i < ShowcaseKinds.Length; i++)
{
DefenseTowerKind kind = ShowcaseKinds[i];
Vector2 pad = Tuning.GetBuildPadPosition(i);
Vector2 position = DefenseTowerHitTestLayout.GetDummyPosition(kind);
GameObject actorObject = new($"Showcase Skeleton - {kind}");
actorObject.transform.SetParent(transform, false);
actorObject.transform.position = Defense25DActor.OnGround(position, settings.GroundY);
Defense25DActor actor = actorObject.AddComponent<Defense25DActor>();
actor.Configure(this, ComparisonActorKind.Enemy);
actor.SetEnemyMaximumHealth(Mathf.Max(1f, settings.Enemy.MaxHealth));
Defense25DEnemy dummy = actorObject.AddComponent<Defense25DEnemy>();
dummy.Configure(this, actor, int.MinValue + i, FindNonImmuneShowcaseLane(kind));
AttachEnemyStatusVisual(dummy);
dummy.ConfigureShowcaseDummy();
dummy.Spawn(position);
showcaseDummyKinds.Add(dummy, kind);
showcaseDummyPositions.Add(dummy, position);
showcaseObservedDamageEvents.Add(dummy, 0);
showcaseHitCounts[kind] = 0;
}
Physics.SyncTransforms();
return true;
}
public void ExitTowerShowcase()
{
if (!towerShowcaseActive) return;
towerShowcaseActive = false;
RestoreShowcasePlayerPosition();
if (showcaseTowerManager != null)
{
showcaseTowerManager.CancelAll();
showcaseTowerManager.ResetForRun();
}
TowerManager = runTowerManager;
GameTime = runGameTime;
runTowerManager?.SetShowcaseSuspended(false);
foreach (Defense25DEnemy enemy in showcaseSuspendedEnemies)
if (enemy != null)
{
enemy.gameObject.SetActive(true);
enemy.Actor.RestoreLiveCollisionAndVisuals();
}
foreach (Defense25DEnemy dummy in showcaseDummyKinds.Keys)
if (dummy != null)
{
dummy.gameObject.SetActive(false);
Destroy(dummy.gameObject);
}
if (showcaseRoot != null) Destroy(showcaseRoot);
showcaseDummyKinds.Clear();
showcaseDummyPositions.Clear();
showcaseObservedDamageEvents.Clear();
showcaseHitCounts.Clear();
showcaseSuspendedEnemies.Clear();
showcaseTowerManager = null;
showcaseRoot = null;
runTowerManager = null;
Physics.SyncTransforms();
}
public void AdvanceTowerShowcase(float dt)
{
if (!towerShowcaseActive || dt <= 0f) return;
GameTime += dt;
foreach (Defense25DEnemy dummy in showcaseDummyKinds.Keys.ToArray())
{
AccumulateShowcaseDamage(dummy);
}
if (showcaseTowerManager == null) return;
foreach (Defense25DTower tower in showcaseTowerManager.Towers)
if (tower != null && tower.Kind == DefenseTowerKind.Summon)
tower.StepShowcaseSummons(dt);
}
public void NotifyTowerShowcaseDummyDeath(Defense25DEnemy enemy)
{
if (!towerShowcaseActive || enemy == null
|| !showcaseDummyKinds.ContainsKey(enemy)) return;
AccumulateShowcaseDamage(enemy);
enemy.ConfigureShowcaseDummy();
enemy.Spawn(showcaseDummyPositions[enemy]);
showcaseObservedDamageEvents[enemy] = 0;
Physics.SyncTransforms();
}
private void AccumulateShowcaseDamage(Defense25DEnemy enemy)
{
if (!showcaseDummyKinds.TryGetValue(enemy, out DefenseTowerKind kind)) return;
int current = enemy.Actor.DamageEvents;
int previous = showcaseObservedDamageEvents.TryGetValue(enemy, out int observed) ? observed : 0;
if (current >= previous) showcaseHitCounts[kind] += current - previous;
showcaseObservedDamageEvents[enemy] = current;
}
private void MovePlayerForShowcaseBuild(Vector2 position)
{
Player.Body.position = Defense25DActor.OnGround(position, settings.GroundY);
Player.transform.position = Player.Body.position;
Physics.SyncTransforms();
}
private void RestoreShowcasePlayerPosition()
{
if (Player == null || Player.Body == null) return;
Player.Body.position = Defense25DActor.OnGround(runPlayerPosition, settings.GroundY);
Player.transform.position = Player.Body.position;
}
private int ShowcaseBuildCost(DefenseTowerKind kind) => kind switch
{
DefenseTowerKind.Direct => Tuning.DirectTowerCost,
DefenseTowerKind.Summon => Tuning.SummonTowerCost,
DefenseTowerKind.Lightning => Tuning.LightningTowerCost,
DefenseTowerKind.Cold => Tuning.ColdTowerCost,
DefenseTowerKind.Seismic => Tuning.SeismicTowerCost,
DefenseTowerKind.Empowering => Tuning.EmpoweringTowerCost,
_ => 0,
};
private int FindNonImmuneShowcaseLane(DefenseTowerKind kind)
{
for (int lane = 0; lane < 2; lane++)
if (!DefensePrototypeRules.IsTowerImmune(kind, Tuning.GetPortalImmuneTowerKind(lane)))
return lane;
return 0;
}
}
public sealed partial class Defense25DEnemy
{
public void ConfigureShowcaseDummy() => showcaseDummy = true;
}
public sealed partial class Defense25DTower
{
public float ShowcaseSupportBonus => Tuning.SupportDamageBonusPerTier * supportAuraStrength;
public void StepShowcaseSummons(float dt)
{
foreach (Defense25DSummon summon in ownedSummons)
if (summon != null) summon.Step(dt);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3408d4c9c47b4639b0c6d601915cc820

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