diff --git a/Assets/Resources/BalanceConfig.asset b/Assets/Resources/BalanceConfig.asset
index cd582318..2bc05e5f 100644
--- a/Assets/Resources/BalanceConfig.asset
+++ b/Assets/Resources/BalanceConfig.asset
@@ -67,7 +67,11 @@ MonoBehaviour:
bossRadiusMult: 1.25
cavalryKnockback: 0.35
archerRangeRatio: 0.2
+ archerMeleeDealtRatio: 0.5
+ archerMeleeTakenRatio: 1.5
+ collisionPushRatio: 1
unitArtHeight: 1.5
+ boardArt: battlefield_01
minDragDistance: 0.35
finalHitTolerance: 0.15
moveSpeed: 9
diff --git a/Assets/Resources/Board.meta b/Assets/Resources/Board.meta
new file mode 100644
index 00000000..6ac7ba5c
--- /dev/null
+++ b/Assets/Resources/Board.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a74463feafb491c4dafe8d1133c7f3cc
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Board/battlefield_01.png b/Assets/Resources/Board/battlefield_01.png
new file mode 100644
index 00000000..4917d126
Binary files /dev/null and b/Assets/Resources/Board/battlefield_01.png differ
diff --git a/Assets/Resources/Board/battlefield_01.png.meta b/Assets/Resources/Board/battlefield_01.png.meta
new file mode 100644
index 00000000..06f5e2b7
--- /dev/null
+++ b/Assets/Resources/Board/battlefield_01.png.meta
@@ -0,0 +1,169 @@
+fileFormatVersion: 2
+guid: 6e3a6ea6e68311c4683b0c9f0944480f
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: 6962303912857251952
+ second: battlefield_01_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ 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: 4096
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: battlefield_01_0
+ rect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 2000
+ height: 3200
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 0703b2855821f9060800000000000000
+ internalID: 6962303912857251952
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable:
+ battlefield_01_0: 6962303912857251952
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_archer.meta b/Assets/Resources/Units/dz_archer.meta
new file mode 100644
index 00000000..c41dee7d
--- /dev/null
+++ b/Assets/Resources/Units/dz_archer.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c762694df723e0449b223cfd4674c673
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_archer/front_idle.meta b/Assets/Resources/Units/dz_archer/front_idle.meta
new file mode 100644
index 00000000..e459f2dc
--- /dev/null
+++ b/Assets/Resources/Units/dz_archer/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b61c180460f762d45a53cbc320967d18
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_archer/front_idle/f000.png b/Assets/Resources/Units/dz_archer/front_idle/f000.png
new file mode 100644
index 00000000..b7c92872
Binary files /dev/null and b/Assets/Resources/Units/dz_archer/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/dz_archer/front_idle/f000.png.meta b/Assets/Resources/Units/dz_archer/front_idle/f000.png.meta
new file mode 100644
index 00000000..8f6fce35
--- /dev/null
+++ b/Assets/Resources/Units/dz_archer/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: 9948907966e189148b1268d13d76c475
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02064}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 18
+ width: 530
+ height: 932
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_archer/meta.json b/Assets/Resources/Units/dz_archer/meta.json
new file mode 100644
index 00000000..933eecc8
--- /dev/null
+++ b/Assets/Resources/Units/dz_archer/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 568,
+ 969
+ ],
+ "pivot": [
+ 0.5,
+ 0.02064
+ ],
+ "charHeightPx": 929,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/dz_archer/meta.json.meta b/Assets/Resources/Units/dz_archer/meta.json.meta
new file mode 100644
index 00000000..38118640
--- /dev/null
+++ b/Assets/Resources/Units/dz_archer/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 68b84df74057f3f47a3c33b6b3342573
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_cavalry.meta b/Assets/Resources/Units/dz_cavalry.meta
new file mode 100644
index 00000000..2503af16
--- /dev/null
+++ b/Assets/Resources/Units/dz_cavalry.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c58e18725c9a5514da41526a47391814
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_cavalry/front_idle.meta b/Assets/Resources/Units/dz_cavalry/front_idle.meta
new file mode 100644
index 00000000..badf3e6d
--- /dev/null
+++ b/Assets/Resources/Units/dz_cavalry/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5769dc3460a78ca47a6b782fc06ebd7c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_cavalry/front_idle/f000.png b/Assets/Resources/Units/dz_cavalry/front_idle/f000.png
new file mode 100644
index 00000000..4fef8f45
Binary files /dev/null and b/Assets/Resources/Units/dz_cavalry/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/dz_cavalry/front_idle/f000.png.meta b/Assets/Resources/Units/dz_cavalry/front_idle/f000.png.meta
new file mode 100644
index 00000000..c0234fc6
--- /dev/null
+++ b/Assets/Resources/Units/dz_cavalry/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: e1b356e27b2ca84409a21237e725c1ad
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02092}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 676
+ height: 918
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_cavalry/meta.json b/Assets/Resources/Units/dz_cavalry/meta.json
new file mode 100644
index 00000000..99512cc1
--- /dev/null
+++ b/Assets/Resources/Units/dz_cavalry/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 714,
+ 956
+ ],
+ "pivot": [
+ 0.5,
+ 0.02092
+ ],
+ "charHeightPx": 916,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/dz_cavalry/meta.json.meta b/Assets/Resources/Units/dz_cavalry/meta.json.meta
new file mode 100644
index 00000000..6ef4b4e2
--- /dev/null
+++ b/Assets/Resources/Units/dz_cavalry/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d3a20275187eda3458bc205f153411ae
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_infantry.meta b/Assets/Resources/Units/dz_infantry.meta
new file mode 100644
index 00000000..cf559dbe
--- /dev/null
+++ b/Assets/Resources/Units/dz_infantry.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8264d82aab0a4ec4da8daef6b607a1d4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_infantry/front_idle.meta b/Assets/Resources/Units/dz_infantry/front_idle.meta
new file mode 100644
index 00000000..9442e48f
--- /dev/null
+++ b/Assets/Resources/Units/dz_infantry/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 64bd26411189d0149a15c532ddb851d1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_infantry/front_idle/f000.png b/Assets/Resources/Units/dz_infantry/front_idle/f000.png
new file mode 100644
index 00000000..c73e5626
Binary files /dev/null and b/Assets/Resources/Units/dz_infantry/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/dz_infantry/front_idle/f000.png.meta b/Assets/Resources/Units/dz_infantry/front_idle/f000.png.meta
new file mode 100644
index 00000000..6dee01df
--- /dev/null
+++ b/Assets/Resources/Units/dz_infantry/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: 82e9825e5f8895c4d88848505ca6a5b9
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.0221}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 528
+ height: 867
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_infantry/meta.json b/Assets/Resources/Units/dz_infantry/meta.json
new file mode 100644
index 00000000..61e67eea
--- /dev/null
+++ b/Assets/Resources/Units/dz_infantry/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 566,
+ 905
+ ],
+ "pivot": [
+ 0.5,
+ 0.0221
+ ],
+ "charHeightPx": 865,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/dz_infantry/meta.json.meta b/Assets/Resources/Units/dz_infantry/meta.json.meta
new file mode 100644
index 00000000..b839eea1
--- /dev/null
+++ b/Assets/Resources/Units/dz_infantry/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 9ce640825d5c4e74fbfb0c09a8d7fced
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_spear.meta b/Assets/Resources/Units/dz_spear.meta
new file mode 100644
index 00000000..7ae7efcc
--- /dev/null
+++ b/Assets/Resources/Units/dz_spear.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8a518d38fe2e823429d906d3681919ce
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_spear/front_idle.meta b/Assets/Resources/Units/dz_spear/front_idle.meta
new file mode 100644
index 00000000..12858da4
--- /dev/null
+++ b/Assets/Resources/Units/dz_spear/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 21750aef097feac448849e0781b4d12f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_spear/front_idle/f000.png b/Assets/Resources/Units/dz_spear/front_idle/f000.png
new file mode 100644
index 00000000..6deb4855
Binary files /dev/null and b/Assets/Resources/Units/dz_spear/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/dz_spear/front_idle/f000.png.meta b/Assets/Resources/Units/dz_spear/front_idle/f000.png.meta
new file mode 100644
index 00000000..638392df
--- /dev/null
+++ b/Assets/Resources/Units/dz_spear/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: 4b581a6de17271047ba0feb63ea70500
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02033}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 540
+ height: 960
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/dz_spear/meta.json b/Assets/Resources/Units/dz_spear/meta.json
new file mode 100644
index 00000000..ff2d5238
--- /dev/null
+++ b/Assets/Resources/Units/dz_spear/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 578,
+ 984
+ ],
+ "pivot": [
+ 0.5,
+ 0.02033
+ ],
+ "charHeightPx": 957,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/dz_spear/meta.json.meta b/Assets/Resources/Units/dz_spear/meta.json.meta
new file mode 100644
index 00000000..e7f74a55
--- /dev/null
+++ b/Assets/Resources/Units/dz_spear/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c406e2a385c03a74f87aa25e6a6f3c09
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_archer.meta b/Assets/Resources/Units/hgj_archer.meta
new file mode 100644
index 00000000..71fa3be2
--- /dev/null
+++ b/Assets/Resources/Units/hgj_archer.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6c3636902d4db564792989f078754f48
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_archer/front_idle.meta b/Assets/Resources/Units/hgj_archer/front_idle.meta
new file mode 100644
index 00000000..be704426
--- /dev/null
+++ b/Assets/Resources/Units/hgj_archer/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b3ec43f64b3072d4abd386d5e1895eb6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_archer/front_idle/f000.png b/Assets/Resources/Units/hgj_archer/front_idle/f000.png
new file mode 100644
index 00000000..432c2acb
Binary files /dev/null and b/Assets/Resources/Units/hgj_archer/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/hgj_archer/front_idle/f000.png.meta b/Assets/Resources/Units/hgj_archer/front_idle/f000.png.meta
new file mode 100644
index 00000000..136359a0
--- /dev/null
+++ b/Assets/Resources/Units/hgj_archer/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: e0d9e04563411c949a226789ed94534f
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02157}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 557
+ height: 890
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_archer/meta.json b/Assets/Resources/Units/hgj_archer/meta.json
new file mode 100644
index 00000000..f5ee38cd
--- /dev/null
+++ b/Assets/Resources/Units/hgj_archer/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 595,
+ 927
+ ],
+ "pivot": [
+ 0.5,
+ 0.02157
+ ],
+ "charHeightPx": 887,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/hgj_archer/meta.json.meta b/Assets/Resources/Units/hgj_archer/meta.json.meta
new file mode 100644
index 00000000..37e8e2fe
--- /dev/null
+++ b/Assets/Resources/Units/hgj_archer/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d046c89bed998a340bd6f9ecefbfc4f0
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao.meta b/Assets/Resources/Units/hgj_boss_zhangjiao.meta
new file mode 100644
index 00000000..c832be53
--- /dev/null
+++ b/Assets/Resources/Units/hgj_boss_zhangjiao.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 09f436c99abc85c40903215547c36eaf
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle.meta b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle.meta
new file mode 100644
index 00000000..22d2ee34
--- /dev/null
+++ b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d343dbcb4505382479e05bdfc7ab2ffe
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png
new file mode 100644
index 00000000..ce1214a9
Binary files /dev/null and b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png.meta b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png.meta
new file mode 100644
index 00000000..325f9076
--- /dev/null
+++ b/Assets/Resources/Units/hgj_boss_zhangjiao/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: 17f44627146649542b114aa9b63c9dd1
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02119}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 608
+ height: 906
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json b/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json
new file mode 100644
index 00000000..fbae8e4e
--- /dev/null
+++ b/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 646,
+ 944
+ ],
+ "pivot": [
+ 0.5,
+ 0.02119
+ ],
+ "charHeightPx": 904,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json.meta b/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json.meta
new file mode 100644
index 00000000..0ce6d6c9
--- /dev/null
+++ b/Assets/Resources/Units/hgj_boss_zhangjiao/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c4fddb826eaa7874cba807c3f1cc1903
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_cavalry.meta b/Assets/Resources/Units/hgj_cavalry.meta
new file mode 100644
index 00000000..b739352b
--- /dev/null
+++ b/Assets/Resources/Units/hgj_cavalry.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e8fa108f337384f4fa47e6a21d775d21
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_cavalry/front_idle.meta b/Assets/Resources/Units/hgj_cavalry/front_idle.meta
new file mode 100644
index 00000000..585dfa2f
--- /dev/null
+++ b/Assets/Resources/Units/hgj_cavalry/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 07ca2b82651d3184f845bf4709a9692d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png b/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png
new file mode 100644
index 00000000..cbda342d
Binary files /dev/null and b/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png.meta b/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png.meta
new file mode 100644
index 00000000..9b41093e
--- /dev/null
+++ b/Assets/Resources/Units/hgj_cavalry/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: 9fed149a8ac54b94eb620fe7bf0ad83c
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02083}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 687
+ height: 922
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_cavalry/meta.json b/Assets/Resources/Units/hgj_cavalry/meta.json
new file mode 100644
index 00000000..0bf1cf6f
--- /dev/null
+++ b/Assets/Resources/Units/hgj_cavalry/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 725,
+ 960
+ ],
+ "pivot": [
+ 0.5,
+ 0.02083
+ ],
+ "charHeightPx": 920,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/hgj_cavalry/meta.json.meta b/Assets/Resources/Units/hgj_cavalry/meta.json.meta
new file mode 100644
index 00000000..afd31545
--- /dev/null
+++ b/Assets/Resources/Units/hgj_cavalry/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a7092a9a34597e54d90b46dbbde76bd4
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_infantry.meta b/Assets/Resources/Units/hgj_infantry.meta
new file mode 100644
index 00000000..a1eb43e4
--- /dev/null
+++ b/Assets/Resources/Units/hgj_infantry.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: da646cd4bb2c34f418b81a081dac92ed
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_infantry/front_idle.meta b/Assets/Resources/Units/hgj_infantry/front_idle.meta
new file mode 100644
index 00000000..9d253b7e
--- /dev/null
+++ b/Assets/Resources/Units/hgj_infantry/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6cad2bd92c97c484f8fc94ded468c7bb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_infantry/front_idle/f000.png b/Assets/Resources/Units/hgj_infantry/front_idle/f000.png
new file mode 100644
index 00000000..4b7044d3
Binary files /dev/null and b/Assets/Resources/Units/hgj_infantry/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/hgj_infantry/front_idle/f000.png.meta b/Assets/Resources/Units/hgj_infantry/front_idle/f000.png.meta
new file mode 100644
index 00000000..9fa290a4
--- /dev/null
+++ b/Assets/Resources/Units/hgj_infantry/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: fe11dbe17cc3fe840820473f273cf926
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02291}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 532
+ height: 835
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_infantry/meta.json b/Assets/Resources/Units/hgj_infantry/meta.json
new file mode 100644
index 00000000..308aae0b
--- /dev/null
+++ b/Assets/Resources/Units/hgj_infantry/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 570,
+ 873
+ ],
+ "pivot": [
+ 0.5,
+ 0.02291
+ ],
+ "charHeightPx": 833,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/hgj_infantry/meta.json.meta b/Assets/Resources/Units/hgj_infantry/meta.json.meta
new file mode 100644
index 00000000..6913281d
--- /dev/null
+++ b/Assets/Resources/Units/hgj_infantry/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3294e2cca1df12e4a82604ec8c6ad12d
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_spear.meta b/Assets/Resources/Units/hgj_spear.meta
new file mode 100644
index 00000000..994d14ec
--- /dev/null
+++ b/Assets/Resources/Units/hgj_spear.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c1131a44c41c90e4483cfcd59e05716f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_spear/front_idle.meta b/Assets/Resources/Units/hgj_spear/front_idle.meta
new file mode 100644
index 00000000..c3d9c57b
--- /dev/null
+++ b/Assets/Resources/Units/hgj_spear/front_idle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 84f1180717c2745438b1b87279a68ce1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_spear/front_idle/f000.png b/Assets/Resources/Units/hgj_spear/front_idle/f000.png
new file mode 100644
index 00000000..e96e711c
Binary files /dev/null and b/Assets/Resources/Units/hgj_spear/front_idle/f000.png differ
diff --git a/Assets/Resources/Units/hgj_spear/front_idle/f000.png.meta b/Assets/Resources/Units/hgj_spear/front_idle/f000.png.meta
new file mode 100644
index 00000000..40c79b3d
--- /dev/null
+++ b/Assets/Resources/Units/hgj_spear/front_idle/f000.png.meta
@@ -0,0 +1,168 @@
+fileFormatVersion: 2
+guid: dc29d8b6aee0ae141800576aff7e4518
+TextureImporter:
+ internalIDToNameTable:
+ - first:
+ 213: -166343660977832731
+ second: f000_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: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 1
+ wrapV: 1
+ wrapW: 1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 1
+ spriteExtrude: 1
+ spriteMeshType: 0
+ alignment: 9
+ spritePivot: {x: 0.5, y: 0.02232}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 8
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 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
+ - serializedVersion: 4
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: WebGL
+ 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: f000_0
+ rect:
+ serializedVersion: 2
+ x: 19
+ y: 19
+ width: 500
+ height: 858
+ alignment: 0
+ pivot: {x: 0, y: 0}
+ border: {x: 0, y: 0, z: 0, w: 0}
+ customData:
+ outline: []
+ physicsShape: []
+ tessellationDetail: -1
+ bones: []
+ spriteID: 5ec37c8525701bdf0800000000000000
+ internalID: -166343660977832731
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID: 5e97eb03825dee720800000000000000
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Units/hgj_spear/meta.json b/Assets/Resources/Units/hgj_spear/meta.json
new file mode 100644
index 00000000..c99032ef
--- /dev/null
+++ b/Assets/Resources/Units/hgj_spear/meta.json
@@ -0,0 +1,20 @@
+{
+ "fps": 1,
+ "ppu": 100,
+ "frame": [
+ 538,
+ 896
+ ],
+ "pivot": [
+ 0.5,
+ 0.02232
+ ],
+ "charHeightPx": 856,
+ "clips": [
+ {
+ "name": "front_idle",
+ "frames": 1,
+ "loop": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Assets/Resources/Units/hgj_spear/meta.json.meta b/Assets/Resources/Units/hgj_spear/meta.json.meta
new file mode 100644
index 00000000..4551d8bb
--- /dev/null
+++ b/Assets/Resources/Units/hgj_spear/meta.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 35d4a4ca7503dc84e896e62dbc609093
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Editor/BalanceDefaults.cs b/Assets/Scripts/Editor/BalanceDefaults.cs
index 440ec598..2b4c902c 100644
--- a/Assets/Scripts/Editor/BalanceDefaults.cs
+++ b/Assets/Scripts/Editor/BalanceDefaults.cs
@@ -16,7 +16,7 @@ namespace TKS.EditorTools
{
Stats(UnitType.Infantry, 100, 30, 10, 4.5f, 0f, 0.45f),
Stats(UnitType.Cavalry, 110, 28, 8, 6.5f, 0f, 0.45f),
- Stats(UnitType.Archer, 70, 26, 4, 4.0f, 0f, 0.42f), // 궁병 사거리 = slingMaxDistance × archerRangeRatio (attackRange 미사용)
+ Stats(UnitType.Archer, 70, 26, 4, 4.0f, 0f, 0.42f), // 궁병 사거리 = slingMaxDistance (attackRange 미사용 · #1080)
Stats(UnitType.Spear, 100, 30, 10, 4.5f, 2.2f, 0.45f),
};
@@ -41,8 +41,12 @@ namespace TKS.EditorTools
c.bossHpMult = 1.6f;
c.bossRadiusMult = 1.25f;
c.cavalryKnockback = 0.35f;
- c.archerRangeRatio = 0.2f; // PD 2026-11-04: 슬링샷 범위 + 20 %
+ c.archerRangeRatio = 0.2f; // PD 2026-11-04: 사거리(슬링샷 범위) 밖 +20 % 안의 적에게 다가가 사격
+ c.archerMeleeDealtRatio = 0.5f; // PD 2026-11-04 #1080: 궁수 근접 전투 피해 50 %
+ c.archerMeleeTakenRatio = 1.5f; // PD 2026-11-04 #1080: 궁수 근접 피격 +50 %
+ c.collisionPushRatio = 1f; // PD 2026-11-04 #1080: 보병 알까기 밀어내기 = 남은 거리 그대로
c.unitArtHeight = 1.5f;
+ c.boardArt = "battlefield_01"; // 원화 세션 1차 납품(2026-11-04) · Resources/Board/
c.minDragDistance = 0.35f;
c.finalHitTolerance = 0.15f;
c.moveSpeed = 9f;
diff --git a/Assets/Scripts/Editor/UnitArtImporter.cs b/Assets/Scripts/Editor/UnitArtImporter.cs
index 0652bce5..cb3a3a6b 100644
--- a/Assets/Scripts/Editor/UnitArtImporter.cs
+++ b/Assets/Scripts/Editor/UnitArtImporter.cs
@@ -11,10 +11,30 @@ namespace TKS.EditorTools
public class UnitArtImporter : AssetPostprocessor
{
public const string Root = "Assets/Resources/Units/";
+ /// 전장 배경(원화 세션 납품 · 5:8 불투명 · PPU 100 = 전장 20×32 월드).
+ public const string BoardRoot = "Assets/Resources/Board/";
void OnPreprocessTexture()
{
string path = assetPath.Replace('\\', '/');
+ if (path.StartsWith(BoardRoot))
+ {
+ var bi = (TextureImporter)assetImporter;
+ bi.textureType = TextureImporterType.Sprite;
+ bi.spriteImportMode = SpriteImportMode.Single;
+ bi.mipmapEnabled = false;
+ bi.filterMode = FilterMode.Bilinear;
+ bi.alphaIsTransparency = false;
+ bi.textureCompression = TextureImporterCompression.Compressed;
+ bi.maxTextureSize = 4096;
+ bi.spritePixelsPerUnit = 100f;
+ var bs = new TextureImporterSettings();
+ bi.ReadTextureSettings(bs);
+ bs.spriteMeshType = SpriteMeshType.FullRect;
+ bs.spriteAlignment = (int)SpriteAlignment.Center;
+ bi.SetTextureSettings(bs);
+ return;
+ }
if (!path.StartsWith(Root)) return;
var ti = (TextureImporter)assetImporter;
ti.textureType = TextureImporterType.Sprite;
@@ -59,15 +79,18 @@ namespace TKS.EditorTools
[MenuItem("TKS/Reimport Unit Art (유닛 아트 재임포트)")]
public static void ReimportAll()
{
- string dir = Root.TrimEnd('/');
- if (!AssetDatabase.IsValidFolder(dir))
+ foreach (var root in new[] { Root, BoardRoot })
{
- Debug.Log("[TKS] " + dir + " 없음");
- return;
+ string dir = root.TrimEnd('/');
+ if (!AssetDatabase.IsValidFolder(dir))
+ {
+ Debug.Log("[TKS] " + dir + " 없음");
+ continue;
+ }
+ AssetDatabase.ImportAsset(dir, ImportAssetOptions.ImportRecursive | ImportAssetOptions.ForceUpdate);
+ Debug.Log("[TKS] 아트 재임포트 완료 · " + dir);
}
- AssetDatabase.ImportAsset(dir, ImportAssetOptions.ImportRecursive | ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
- Debug.Log("[TKS] 유닛 아트 재임포트 완료 · " + dir);
}
}
}
diff --git a/Assets/Scripts/Runtime/ActionResolver.cs b/Assets/Scripts/Runtime/ActionResolver.cs
index ed12e804..b555fb77 100644
--- a/Assets/Scripts/Runtime/ActionResolver.cs
+++ b/Assets/Scripts/Runtime/ActionResolver.cs
@@ -11,22 +11,26 @@ namespace TKS
public bool Returned; // 기병: 최종 목표점 명중 → 제자리 복귀
public bool Overwatch; // 창병: 최종 목표점 명중 → 대기(견제)
public bool Crit; // 궁병: 최종 목표점 명중 → 크리티컬
- public bool MovedOnly; // 공격 없이 이동만
+ public bool MovedOnly; // 행동 유닛 자신은 공격하지 않음(이동 · 밀어내기만)
public int DamageDealt;
public int CounterTaken;
public List Attacked = new List();
+ /// 충돌로 밀려난 유닛(아군·적군 · PD 2026-11-04 #1080).
+ public List Pushed = new List();
+ /// 밀린 아군이 적과 닿아 얻은 공격 등 부가 설명(HUD 문구).
+ public List Notes = new List();
/// 실제로 지나간 경로(시작 포함). 창병 견제 판정용.
public List Path = new List();
}
///
- /// 슬링샷 벡터(시작점 → 목표점)를 병종 규칙대로 이동·공격으로 해석한다.
- /// ① 보병: 경로 위 첫 적까지 이동 후 공격 · 목표점 명중 시 추가 이동 1회
- /// ② 기병: 경로 위 적을 모두 관통 피해 · 목표점까지 이동 · 목표점 명중 시 제자리 복귀
- /// ③ 창병: 보병과 동일 · 목표점 명중 시 대기 → 반경 안에서 움직이는 적에게 피해
- /// ④ 궁병: 조준 방향 슬링샷 거리 + 사거리(슬링샷 최대 × archerRangeRatio) 안에 적이 있으면
- /// 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬 ·
- /// 적이 없으면 목표점까지 이동만(PD 2026-11-04 2차 지시 · 1차의 「행동 취소」 폐기)
+ /// 슬링샷 벡터(시작점 → 목표점)를 병종 규칙대로 이동·공격으로 해석한다 (PD 2026-11-04 #1080 충돌 규칙).
+ /// ① 보병: 경로 위 첫 유닛(아군·적군)에 부딪히면 알까기처럼 그 유닛을 남은 힘만큼 밀어내고 자신은 멈춤 · 적이면 공격(반격 받음) · 목표점 명중 시 추가 이동 1회
+ /// ② 기병: 유닛을 무시하고 관통 · 경로 위 적은 모두 피해(옆으로 밀림) · 아군은 지나칠 거리가 안 나오면 그 아군 직전에 멈춤 · 목표점 명중 시 제자리 복귀
+ /// ③ 창병: 첫 유닛에 부딪히면 튕기지 않고 남은 거리만큼 밀며 함께 전진 · 적이면 피해(반격 받음) · 아군이면 밀기만 · 목표점 명중 시 대기(견제)
+ /// ④ 궁병: 사거리 = 슬링샷 최대 거리 · 조준 방향 사거리 안의 적은 제자리에서 사격 · 사거리 밖 ~ ×(1+비율) 안이면 들어올 만큼만 다가가 사격 ·
+ /// 더 멀면 목표점까지 이동 · 반격 없음 · 목표점 명중 시 크리티컬 · 근접 전투는 피해 50 % · 피격 +50 %(Combat.Damage)
+ /// ⑤ 밀린 아군(행동 유닛과 같은 진영)이 상대 유닛과 닿으면 그 자리에서 공격 1회(조작마다 · 자기 행동은 소모하지 않음)
///
public class ActionResolver
{
@@ -53,8 +57,10 @@ namespace TKS
switch (u.Type)
{
case UnitType.Infantry:
+ yield return InfantryRoutine(u, origin, dir, dist, target, moveOnly, o);
+ break;
case UnitType.Spear:
- yield return MeleeRoutine(u, origin, dir, dist, target, moveOnly, o);
+ yield return SpearRoutine(u, origin, dir, dist, target, moveOnly, o);
break;
case UnitType.Cavalry:
yield return CavalryRoutine(u, origin, dir, dist, target, moveOnly, o);
@@ -67,7 +73,7 @@ namespace TKS
// ---------- 미리보기(드라이런) ----------
- /// 조작 중 표시용: 이 슬링샷이 공격하게 될 적 목록.
+ /// 조작 중 표시용: 이 슬링샷으로 행동 유닛이 직접 공격하게 될 적 목록(밀린 아군의 공격은 예측하지 않음).
public List PredictTargets(Unit u, Vector2 target, bool moveOnly)
{
var result = new List();
@@ -83,20 +89,21 @@ namespace TKS
case UnitType.Infantry:
case UnitType.Spear:
{
- var first = Combat.FirstEnemyOnPath(u, origin, dir, dist, out hitDist);
- if (first != null && Combat.FreeTravel(u, origin, dir, hitDist, false) >= hitDist - 0.05f) result.Add(first);
+ var first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist);
+ if (first != null && first.Faction != u.Faction) result.Add(first);
break;
}
case UnitType.Cavalry:
{
- float travel = Combat.FreeTravel(u, origin, dir, dist, false);
+ float travel = Combat.FreeTravel(u, origin, dir, dist, false, false);
+ travel = StopBeforeUnclearedAllies(u, origin, dir, travel);
var dists = new List();
result.AddRange(Combat.EnemiesOnPath(u, origin, dir, travel, dists));
break;
}
case UnitType.Archer:
{
- var first = Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist);
+ var first = Combat.FirstEnemyOnPath(u, origin, dir, u.AttackRange + u.ArcherMoveAllow, out hitDist);
if (first != null) result.Add(first);
break;
}
@@ -104,48 +111,134 @@ namespace TKS
return result;
}
- // ---------- 보병 · 창병 ----------
+ // ---------- 공통 질의 ----------
- IEnumerator MeleeRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
+ /// 경로 위 첫 유닛. 벽이 그보다 먼저 있으면 null.
+ Unit FirstUnitBeforeWall(Unit u, Vector2 origin, Vector2 dir, float dist, out float hitDist)
{
- float hitDist = 0f;
- Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, dist, out hitDist);
+ Unit first = Combat.FirstUnitOnPath(u, origin, dir, dist, out hitDist);
+ if (first == null) return null;
+ float wall = Combat.FreeTravel(u, origin, dir, dist, false, false);
+ if (hitDist > wall + Combat.Epsilon)
+ {
+ hitDist = 0f;
+ return null;
+ }
+ return first;
+ }
+
+ // ---------- 보병: 알까기 충돌 ----------
+
+ IEnumerator InfantryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
+ {
+ if (moveOnly)
+ {
+ yield return MoveFree(u, origin, dir, dist, o);
+ yield break;
+ }
+
+ float hitDist;
+ Unit first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist);
if (first == null)
{
- float free = Combat.FreeTravel(u, origin, dir, dist, true);
- yield return Move(u, origin + dir * free, o);
- o.MovedOnly = true;
+ yield return MoveFree(u, origin, dir, dist, o);
yield break;
}
- float freeToEnemy = Combat.FreeTravel(u, origin, dir, hitDist, false);
- if (freeToEnemy < hitDist - 0.05f)
+ Vector2 contact = origin + dir * hitDist;
+ yield return Move(u, contact, o); // 부딪힌 지점에서 멈춤
+ float remaining = Mathf.Max(0f, dist - hitDist);
+ bool enemy = first.Faction != u.Faction;
+ bool isFinal = enemy && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
+
+ if (enemy)
+ {
+ yield return Attack(u, first, false, false, o, true);
+ if (!u.IsAlive) yield break;
+ }
+ else
{
- // 아군·벽이 먼저 막는다 → 거기까지만 이동
- yield return Move(u, origin + dir * freeToEnemy, o);
o.MovedOnly = true;
+ }
+
+ if (first.IsAlive)
+ {
+ // 알까기: 접촉 방향(행동 유닛 중심 → 부딪힌 유닛 중심)으로 남은 힘만큼 밀어낸다
+ Vector2 n = first.Position - contact;
+ n = n.sqrMagnitude > 1e-6f ? n.normalized : dir;
+ yield return Push(first, n, remaining * cfg.collisionPushRatio, u, o);
+ if (!enemy) yield return PushedAllyAttack(first, o);
+ }
+
+ if (isFinal && u.IsAlive) o.BonusMove = true;
+ }
+
+ // ---------- 창병: 밀며 함께 전진 ----------
+
+ IEnumerator SpearRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
+ {
+ if (moveOnly)
+ {
+ yield return MoveFree(u, origin, dir, dist, o);
yield break;
}
- yield return Move(u, origin + dir * hitDist, o);
- Unit finalHit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
- bool isFinal = finalHit == first;
- yield return Attack(u, first, false, false, o);
- if (!u.IsAlive) yield break;
+ float hitDist;
+ Unit first = FirstUnitBeforeWall(u, origin, dir, dist, out hitDist);
+ if (first == null)
+ {
+ yield return MoveFree(u, origin, dir, dist, o);
+ yield break;
+ }
- if (u.Type == UnitType.Infantry && isFinal) o.BonusMove = true;
- if (u.Type == UnitType.Spear && isFinal)
+ Vector2 contact = origin + dir * hitDist;
+ yield return Move(u, contact, o);
+ float remaining = Mathf.Max(0f, dist - hitDist);
+ bool enemy = first.Faction != u.Faction;
+ bool isFinal = enemy && Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
+
+ if (enemy)
+ {
+ yield return Attack(u, first, false, false, o, true);
+ if (!u.IsAlive) yield break;
+ }
+ else
+ {
+ o.MovedOnly = true;
+ }
+
+ if (first.IsAlive && remaining > 0.01f)
+ {
+ // 튕기지 않고 앞 유닛을 밀며 함께 전진 · 앞 유닛이 다른 유닛·벽에 막히면 둘 다 멈춤 · 접촉 간격 유지
+ float travel = Combat.FreeTravel(first, first.Position, dir, remaining, true, true, u);
+ if (travel > 0.01f)
+ {
+ Vector2 firstEnd = first.Position + dir * travel;
+ Vector2 spearEnd = contact + dir * travel;
+ Fx.Run(Fx.MoveAlong(first.transform, first.Position, firstEnd, cfg.moveSpeed, null));
+ yield return Fx.MoveAlong(u.transform, contact, spearEnd, cfg.moveSpeed, null);
+ first.Position = firstEnd;
+ u.Position = spearEnd;
+ o.Path.Add(spearEnd);
+ battle.NotifyChanged();
+ }
+ o.Pushed.Add(first);
+ if (!enemy) yield return PushedAllyAttack(first, o);
+ }
+
+ if (isFinal && u.IsAlive)
{
u.SetOverwatch(true);
o.Overwatch = true;
}
}
- // ---------- 기병 ----------
+ // ---------- 기병: 관통 ----------
IEnumerator CavalryRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
{
- float travel = Combat.FreeTravel(u, origin, dir, dist, false);
+ float travel = Combat.FreeTravel(u, origin, dir, dist, false, false); // 유닛은 무시 · 벽만
+ travel = StopBeforeUnclearedAllies(u, origin, dir, travel); // 지나칠 수 없는 아군 직전에서 멈춤
var dists = new List();
var enemies = moveOnly ? new List() : Combat.EnemiesOnPath(u, origin, dir, travel, dists);
Unit finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance);
@@ -209,7 +302,7 @@ namespace TKS
void PierceHit(Unit cav, Unit e, Vector2 dir, ActionOutcome o)
{
if (!e.IsAlive) return;
- int dmg = Combat.Damage(cfg, cav, e, false, rng);
+ int dmg = Combat.Damage(cfg, cav, e, false, rng, true);
e.TakeDamage(dmg);
o.Attacked.Add(e);
o.DamageDealt += dmg;
@@ -225,6 +318,33 @@ namespace TKS
battle.NotifyChanged();
}
+ /// 아군은 관통(통과)하되 travel 안에 완전히 지나칠 수 없는 아군이 있으면 그 아군 직전까지로 줄인다.
+ float StopBeforeUnclearedAllies(Unit u, Vector2 origin, Vector2 dir, float travel)
+ {
+ float t = travel;
+ var allies = battle.AliveUnits(u.Faction);
+ for (int pass = 0; pass < 4; pass++)
+ {
+ float before = t;
+ foreach (var a in allies)
+ {
+ if (a == u) continue;
+ Vector2 rel = a.Position - origin;
+ float along = Vector2.Dot(rel, dir);
+ float lat = Mathf.Abs(rel.x * dir.y - rel.y * dir.x);
+ float sum = u.Radius + a.Radius;
+ if (lat >= sum || along + sum <= 0f) continue; // 경로와 겹치지 않음 · 뒤쪽
+ float half = Mathf.Sqrt(sum * sum - lat * lat);
+ float touch = along - half;
+ float clear = along + half;
+ if (t >= clear || t <= touch) continue; // 완전히 지나침 · 닿기 전에 멈춤
+ t = Mathf.Min(t, Mathf.Max(0f, touch - Combat.Epsilon));
+ }
+ if (Mathf.Approximately(before, t)) break;
+ }
+ return t;
+ }
+
/// 종착점이 적과 겹치면 경로를 되짚어 겹치지 않는 지점까지 물러선다.
Vector2 BackOffFromEnemies(Unit u, Vector2 origin, Vector2 dir, float travel)
{
@@ -251,27 +371,25 @@ namespace TKS
IEnumerator ArcherRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o)
{
- // 조준 방향으로 슬링샷 거리 + 사거리 안의 첫 적. hitDist = 그 적 가장자리에 닿기까지의 이동 거리.
+ // 조준 방향으로 사거리 + 접근 허용 거리 안의 첫 적. hitDist = 그 적 가장자리에 닿기까지의 이동 거리(= 가장자리 간격).
float hitDist = 0f;
- Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist);
+ Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, u.AttackRange + u.ArcherMoveAllow, out hitDist);
if (first == null)
{
- // 적이 없으면 목표점까지 이동(PD 2026-11-04 2차: 「슬링샷 범위 내 적이 없을 경우 해당 위치까지 이동」)
- float free = Combat.FreeTravel(u, origin, dir, dist, true);
- yield return Move(u, origin + dir * free, o);
- o.MovedOnly = true;
+ // 공격 가능 범위에 적이 없으면 목표점까지 이동 (PD 2026-11-04 2차 지시)
+ yield return MoveFree(u, origin, dir, dist, o);
yield break;
}
- // 적 가장자리까지 남은 간격이 사거리 안에 들어올 때까지만 이동(슬링샷 거리 상한)
- float need = Mathf.Clamp(hitDist - u.AttackRange, 0f, dist);
- float freeMove = Combat.FreeTravel(u, origin, dir, need, true);
+ // 사거리 안이면 제자리 · 밖이면 가장자리가 사거리에 들어올 만큼만 다가간다(접근 허용 거리 상한)
+ float need = Mathf.Clamp(hitDist - u.AttackRange, 0f, u.ArcherMoveAllow);
+ float freeMove = need > 0.01f ? Combat.FreeTravel(u, origin, dir, need, true) : 0f;
if (freeMove > 0.01f) yield return Move(u, origin + dir * freeMove, o);
if (hitDist - freeMove <= u.AttackRange + 0.05f)
{
bool crit = Combat.EnemyAtPoint(u, target, cfg.finalHitTolerance) == first;
- yield return Attack(u, first, true, crit, o);
+ yield return Attack(u, first, true, crit, o, true);
}
else
{
@@ -281,6 +399,13 @@ namespace TKS
// ---------- 공통 ----------
+ IEnumerator MoveFree(Unit u, Vector2 origin, Vector2 dir, float dist, ActionOutcome o)
+ {
+ float free = Combat.FreeTravel(u, origin, dir, dist, true, true);
+ yield return Move(u, origin + dir * free, o);
+ o.MovedOnly = true;
+ }
+
IEnumerator Move(Unit u, Vector2 to, ActionOutcome o)
{
yield return Fx.MoveAlong(u.transform, u.Position, to, cfg.moveSpeed, null);
@@ -288,32 +413,67 @@ namespace TKS
o.Path.Add(to);
}
- IEnumerator Attack(Unit a, Unit d, bool ranged, bool crit, ActionOutcome o)
+ /// 부딪힌 유닛을 n 방향으로 force 만큼 밀어낸다(다른 유닛·벽에 닿으면 멈춤). 밀어 주는 유닛(pusher)은 막는 것으로 보지 않는다.
+ IEnumerator Push(Unit target, Vector2 n, float force, Unit pusher, ActionOutcome o)
+ {
+ if (target == null || !target.IsAlive || force < 0.01f) yield break;
+ float slide = Combat.FreeTravel(target, target.Position, n, force, true, true, pusher);
+ if (slide > 0.01f)
+ {
+ Vector2 end = battle.Board.ClampInside(target.Position + n * slide, target.Radius);
+ yield return Fx.MoveAlong(target.transform, target.Position, end, cfg.moveSpeed, null);
+ target.Position = end;
+ }
+ o.Pushed.Add(target);
+ battle.NotifyChanged();
+ }
+
+ /// 규칙 ⑤: 밀린 아군이 상대 유닛과 닿아 있으면 그 자리에서 공격 1회(근접 · 자기 행동 소모 없음).
+ IEnumerator PushedAllyAttack(Unit ally, ActionOutcome o)
+ {
+ if (ally == null || !ally.IsAlive) yield break;
+ Unit foe = Combat.TouchingOpponent(ally);
+ if (foe == null) yield break;
+ yield return Fx.Wait(0.1f);
+ yield return Attack(ally, foe, false, false, o, false);
+ }
+
+ /// 공격 1회. byActor = 행동 유닛의 공격(결과 요약에 집계) · false = 밀린 아군의 공격(Notes 에 기록).
+ IEnumerator Attack(Unit a, Unit d, bool ranged, bool crit, ActionOutcome o, bool byActor)
{
if (a.View != null) a.View.PlayAttack();
if (ranged) yield return Fx.Arrow(a.Position, d.Position);
else yield return Fx.Lunge(a, d.Position);
- int dmg = Combat.Damage(cfg, a, d, crit, rng);
+ int dmg = Combat.Damage(cfg, a, d, crit, rng, !ranged);
d.TakeDamage(dmg);
- o.Attacked.Add(d);
- o.DamageDealt += dmg;
- o.Crit |= crit;
+ if (byActor)
+ {
+ o.Attacked.Add(d);
+ o.DamageDealt += dmg;
+ o.Crit |= crit;
+ }
Fx.Popup(d.Position, crit ? dmg + "!" : dmg.ToString(), crit ? new Color(1f, 0.7f, 0.2f) : Color.white);
battle.NotifyChanged();
yield return Fx.Wait(0.2f);
+ int counter = 0;
if (!ranged && d.IsAlive && a.IsAlive)
{
- int c = Combat.CounterDamage(cfg, d, a, rng);
+ counter = Combat.CounterDamage(cfg, d, a, rng);
if (d.View != null) d.View.PlayAttack();
yield return Fx.Lunge(d, a.Position);
- a.TakeDamage(c);
- o.CounterTaken += c;
- Fx.Popup(a.Position, c.ToString(), new Color(1f, 0.45f, 0.45f));
+ a.TakeDamage(counter);
+ if (byActor) o.CounterTaken += counter;
+ Fx.Popup(a.Position, counter.ToString(), new Color(1f, 0.45f, 0.45f));
battle.NotifyChanged();
yield return Fx.Wait(0.2f);
}
+
+ if (!byActor)
+ {
+ o.Notes.Add("밀린 " + a.DisplayName + " → " + d.DisplayName + " 공격 " + dmg + (counter > 0 ? " · 반격 " + counter : ""));
+ }
}
}
}
diff --git a/Assets/Scripts/Runtime/BalanceConfig.cs b/Assets/Scripts/Runtime/BalanceConfig.cs
index c8b65c75..72b536eb 100644
--- a/Assets/Scripts/Runtime/BalanceConfig.cs
+++ b/Assets/Scripts/Runtime/BalanceConfig.cs
@@ -20,7 +20,7 @@ namespace TKS
public int def;
[Tooltip("슬링샷으로 지정할 수 있는 최대 거리(월드 단위)")]
public float slingMaxDistance;
- [Tooltip("창병 대기(견제) 반경 · 궁병은 쓰지 않음(사거리 = slingMaxDistance × archerRangeRatio)")]
+ [Tooltip("창병 대기(견제) 반경 · 궁병은 쓰지 않음(사거리 = slingMaxDistance)")]
public float attackRange;
public float radius;
}
@@ -81,13 +81,23 @@ namespace TKS
[Tooltip("기병 관통 시 적을 옆으로 밀어내는 거리")]
public float cavalryKnockback = 0.35f;
- [Header("궁병 규칙 (PD 2026-11-04)")]
- [Tooltip("궁병 사거리 = 슬링샷 최대 거리 × 비율. 조준 방향의 슬링샷 거리 + 사거리 안에 적이 있을 때만 행동 가능(없으면 놓아도 취소) · 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격")]
+ [Header("궁병 규칙 (PD 2026-11-04 · #1080)")]
+ [Tooltip("궁병 사거리 = 슬링샷 최대 거리(얇은 원 · 안의 적은 제자리 사격). 그 밖 ~ 사거리 × (1 + 비율) 안(붉은 도넛)의 적은 사거리에 들어올 만큼만 다가가 사격 · 더 멀면 목표점까지 이동")]
public float archerRangeRatio = 0.2f;
+ [Tooltip("궁병이 근접 유닛(보병·기병·창병)과 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격)할 때 주는 피해 배율 · 원거리 사격은 1")]
+ public float archerMeleeDealtRatio = 0.5f;
+ [Tooltip("궁병이 근접 유닛에게 근접으로 맞을 때 받는 피해 배율(상성 배율과 곱)")]
+ public float archerMeleeTakenRatio = 1.5f;
+
+ [Header("충돌 (PD 2026-11-04 · #1080)")]
+ [Tooltip("보병 알까기: 부딪힌 유닛(아군·적군)이 밀려나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 · 창병은 비율 없이 남은 거리만큼 밀며 함께 전진")]
+ public float collisionPushRatio = 1f;
[Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f;
+ [Tooltip("Resources/Board/<이름> 전장 배경 스프라이트(원화 세션 납품 · 5:8) · 비면 도형·격자 바닥")]
+ public string boardArt = "battlefield_01";
[Header("조작")]
[Tooltip("이보다 짧게 당기면 취소")]
diff --git a/Assets/Scripts/Runtime/BattleHUD.cs b/Assets/Scripts/Runtime/BattleHUD.cs
index 9a000624..15c5d197 100644
--- a/Assets/Scripts/Runtime/BattleHUD.cs
+++ b/Assets/Scripts/Runtime/BattleHUD.cs
@@ -189,7 +189,7 @@ namespace TKS
string line1 = side + " · " + u.DisplayName + " · " + UnitTypeInfo.Korean(u.Type) + (u.IsBoss ? " (장수)" : "");
string line2 = "HP " + u.Hp + "/" + u.MaxHp + " 공 " + u.Atk + " 방 " + u.Def;
string line3 = "슬링 " + u.SlingMaxDistance.ToString("0.#");
- if (u.Type == UnitType.Archer) line3 += " 사거리 " + u.AttackRange.ToString("0.#") + " (슬링 +" + Mathf.RoundToInt(battle.Config.archerRangeRatio * 100f) + " %)";
+ if (u.Type == UnitType.Archer) line3 += " 사거리 " + u.AttackRange.ToString("0.#") + " (접근 +" + u.ArcherMoveAllow.ToString("0.#") + ")";
if (u.Type == UnitType.Spear) line3 += " 견제 반경 " + u.AttackRange.ToString("0.#");
string state = "";
if (u.Faction == Faction.Player)
diff --git a/Assets/Scripts/Runtime/BattleManager.cs b/Assets/Scripts/Runtime/BattleManager.cs
index 58e1aeec..edc8b99f 100644
--- a/Assets/Scripts/Runtime/BattleManager.cs
+++ b/Assets/Scripts/Runtime/BattleManager.cs
@@ -184,11 +184,11 @@ namespace TKS
return Phase == BattlePhase.PlayerTurn && u != null && u.Faction == Faction.Player && u.IsAlive && !u.ActedThisTurn;
}
- /// 궁병 공격 가능 범위(슬링샷 최대 + 사거리 · 가장자리 간격 기준) 안에 상대 유닛이 하나라도 있는가 — 도넛 색 표시용.
+ /// 궁병 공격 가능 범위(사거리 + 접근 허용 거리 · 가장자리 간격 기준) 안에 상대 유닛이 하나라도 있는가 — 도넛 색 표시용.
public bool AnyEnemyWithinArcherReach(Unit u)
{
if (u == null) return false;
- float reach = u.SlingMaxDistance + u.AttackRange;
+ float reach = u.AttackRange + u.ArcherMoveAllow;
Faction other = u.Faction == Faction.Player ? Faction.Enemy : Faction.Player;
foreach (var e in AliveUnits(other))
{
@@ -383,18 +383,32 @@ namespace TKS
static string Describe(Unit u, ActionOutcome o, bool moveOnly)
{
string who = u.DisplayName + "(" + UnitTypeInfo.Korean(u.Type) + ")";
+ string s;
if (o.Attacked.Count == 0)
{
- return who + (moveOnly ? " 추가 이동" : " 이동");
+ s = who + (moveOnly ? " 추가 이동" : " 이동");
}
- var names = new List();
- foreach (var a in o.Attacked)
+ else
{
- if (!names.Contains(a.DisplayName)) names.Add(a.DisplayName);
+ var names = new List();
+ foreach (var a in o.Attacked)
+ {
+ if (!names.Contains(a.DisplayName)) names.Add(a.DisplayName);
+ }
+ s = who + " → " + string.Join(", ", names.ToArray()) + " 공격 " + o.DamageDealt;
+ if (o.Crit) s += " 크리티컬!";
+ if (o.CounterTaken > 0) s += " · 반격 " + o.CounterTaken;
}
- string s = who + " → " + string.Join(", ", names.ToArray()) + " 공격 " + o.DamageDealt;
- if (o.Crit) s += " 크리티컬!";
- if (o.CounterTaken > 0) s += " · 반격 " + o.CounterTaken;
+ if (o.Pushed.Count > 0)
+ {
+ var pushed = new List();
+ foreach (var p in o.Pushed)
+ {
+ if (!pushed.Contains(p.DisplayName)) pushed.Add(p.DisplayName);
+ }
+ s += " · " + string.Join(", ", pushed.ToArray()) + " 밀어냄";
+ }
+ foreach (var n in o.Notes) s += " · " + n;
if (o.BonusMove) s += " · 추가 이동!";
if (o.Returned) s += " · 제자리 복귀";
if (o.Overwatch) s += " · 대기(견제) 발동";
diff --git a/Assets/Scripts/Runtime/Board.cs b/Assets/Scripts/Runtime/Board.cs
index 9799f2db..31482d44 100644
--- a/Assets/Scripts/Runtime/Board.cs
+++ b/Assets/Scripts/Runtime/Board.cs
@@ -7,48 +7,61 @@ namespace TKS
{
}
- /// 전장(바닥·격자·테두리·4면 벽 콜라이더). 화면보다 넓어 카메라로 스크롤한다.
+ /// 전장(바닥 = 원화 배경 또는 도형·격자 · 테두리 · 4면 벽 콜라이더). 화면보다 넓어 카메라로 스크롤한다.
public class Board : MonoBehaviour
{
public Vector2 Size { get; private set; }
+ /// Resources/Board/<boardArt> 원화 배경을 썼는가(없으면 도형·격자).
+ public bool HasArt { get; private set; }
public Rect Rect
{
get { return new Rect(-Size.x * 0.5f, -Size.y * 0.5f, Size.x, Size.y); }
}
- public void Build(Vector2 size)
+ public void Build(Vector2 size, string boardArt = null)
{
Size = size;
float w = size.x;
float h = size.y;
- var bg = ProceduralSprites.NewSprite(transform, "Background", ProceduralSprites.Square, new Color(0.17f, 0.24f, 0.17f), 1f, -20);
- bg.transform.localScale = new Vector3(w, h, 1f);
-
- // 아군·적군 진영 구분용 옅은 띠
- var enemyHalf = ProceduralSprites.NewSprite(transform, "EnemyHalf", ProceduralSprites.Square, new Color(0.45f, 0.2f, 0.2f, 0.18f), 1f, -19);
- enemyHalf.transform.localScale = new Vector3(w, h * 0.5f, 1f);
- enemyHalf.transform.localPosition = new Vector3(0f, h * 0.25f, 0f);
-
- // 격자(4 단위) — 스크롤 체감용
- var gridColor = new Color(1f, 1f, 1f, 0.07f);
- const float gridStep = 4f;
- for (float x = -w * 0.5f + gridStep; x < w * 0.5f - 0.01f; x += gridStep)
+ Sprite art = string.IsNullOrEmpty(boardArt) ? null : Resources.Load("Board/" + boardArt);
+ HasArt = art != null;
+ if (HasArt)
{
- var g = ProceduralSprites.NewSprite(transform, "GridV", ProceduralSprites.Square, gridColor, 1f, -18);
- g.transform.localPosition = new Vector3(x, 0f, 0f);
- g.transform.localScale = new Vector3(0.04f, h, 1f);
+ // 원화 세션 납품 배경(5:8 · 위 = 적 진영) — 전장 크기에 맞춰 늘린다 · 격자·진영 띠는 그리지 않음
+ var bg = ProceduralSprites.NewSprite(transform, "Background", art, Color.white, 1f, -20);
+ bg.transform.localScale = new Vector3(w / art.bounds.size.x, h / art.bounds.size.y, 1f);
}
- for (float y = -h * 0.5f + gridStep; y < h * 0.5f - 0.01f; y += gridStep)
+ else
{
- var g = ProceduralSprites.NewSprite(transform, "GridH", ProceduralSprites.Square, gridColor, 1f, -18);
- g.transform.localPosition = new Vector3(0f, y, 0f);
- g.transform.localScale = new Vector3(w, 0.04f, 1f);
- }
+ var bg = ProceduralSprites.NewSprite(transform, "Background", ProceduralSprites.Square, new Color(0.17f, 0.24f, 0.17f), 1f, -20);
+ bg.transform.localScale = new Vector3(w, h, 1f);
- var mid = ProceduralSprites.NewSprite(transform, "MidLine", ProceduralSprites.Square, new Color(1f, 1f, 1f, 0.14f), 1f, -17);
- mid.transform.localScale = new Vector3(w, 0.06f, 1f);
+ // 아군·적군 진영 구분용 옅은 띠
+ var enemyHalf = ProceduralSprites.NewSprite(transform, "EnemyHalf", ProceduralSprites.Square, new Color(0.45f, 0.2f, 0.2f, 0.18f), 1f, -19);
+ enemyHalf.transform.localScale = new Vector3(w, h * 0.5f, 1f);
+ enemyHalf.transform.localPosition = new Vector3(0f, h * 0.25f, 0f);
+
+ // 격자(4 단위) — 스크롤 체감용
+ var gridColor = new Color(1f, 1f, 1f, 0.07f);
+ const float gridStep = 4f;
+ for (float x = -w * 0.5f + gridStep; x < w * 0.5f - 0.01f; x += gridStep)
+ {
+ var g = ProceduralSprites.NewSprite(transform, "GridV", ProceduralSprites.Square, gridColor, 1f, -18);
+ g.transform.localPosition = new Vector3(x, 0f, 0f);
+ g.transform.localScale = new Vector3(0.04f, h, 1f);
+ }
+ for (float y = -h * 0.5f + gridStep; y < h * 0.5f - 0.01f; y += gridStep)
+ {
+ var g = ProceduralSprites.NewSprite(transform, "GridH", ProceduralSprites.Square, gridColor, 1f, -18);
+ g.transform.localPosition = new Vector3(0f, y, 0f);
+ g.transform.localScale = new Vector3(w, 0.04f, 1f);
+ }
+
+ var mid = ProceduralSprites.NewSprite(transform, "MidLine", ProceduralSprites.Square, new Color(1f, 1f, 1f, 0.14f), 1f, -17);
+ mid.transform.localScale = new Vector3(w, 0.06f, 1f);
+ }
// 테두리
var borderColor = new Color(0.85f, 0.75f, 0.45f);
diff --git a/Assets/Scripts/Runtime/Combat.cs b/Assets/Scripts/Runtime/Combat.cs
index 67171dd3..29653fa6 100644
--- a/Assets/Scripts/Runtime/Combat.cs
+++ b/Assets/Scripts/Runtime/Combat.cs
@@ -8,27 +8,39 @@ namespace TKS
{
/// 경로 질의에 쓰는 원 반지름 배율(살짝 작게 캐스트해 스침을 관대하게).
public const float CastRadiusScale = 0.9f;
- const float Epsilon = 0.02f;
+ public const float Epsilon = 0.02f;
+ /// 두 유닛 가장자리 간격이 이 이하면 「닿아 있다」(밀린 아군의 공격 기회 판정).
+ public const float ContactTolerance = 0.12f;
// ---------- 피해 공식 ----------
- public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng)
+ ///
+ /// 피해 = (공격 × 상성 × 크리 − 방어 × defenseFactor) × 궁병 근접 배율 × 난수.
+ /// melee = 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격). 궁병이 근접 유닛(보병·기병·창병)과 근접 전투하면
+ /// 주는 피해 × archerMeleeDealtRatio · 받는 피해 × archerMeleeTakenRatio (PD 2026-11-04 #1080) · 원거리 사격은 패널티 없음.
+ ///
+ public static int Damage(BalanceConfig cfg, Unit attacker, Unit defender, bool crit, System.Random rng, bool melee)
{
float mult = cfg.Matchup(attacker.Type, defender.Type);
float raw = attacker.Atk * mult * (crit ? cfg.critMultiplier : 1f) - defender.Def * cfg.defenseFactor;
+ if (melee)
+ {
+ if (attacker.Type == UnitType.Archer && UnitTypeInfo.IsMelee(defender.Type)) raw *= cfg.archerMeleeDealtRatio;
+ if (defender.Type == UnitType.Archer && UnitTypeInfo.IsMelee(attacker.Type)) raw *= cfg.archerMeleeTakenRatio;
+ }
float variance = 1f + ((float)rng.NextDouble() * 2f - 1f) * cfg.damageVariance;
return Mathf.Max(1, Mathf.RoundToInt(raw * variance));
}
public static int CounterDamage(BalanceConfig cfg, Unit defender, Unit attacker, System.Random rng)
{
- int baseDmg = Damage(cfg, defender, attacker, false, rng);
+ int baseDmg = Damage(cfg, defender, attacker, false, rng, true);
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.counterRatio));
}
public static int OverwatchDamage(BalanceConfig cfg, Unit spear, Unit mover, System.Random rng)
{
- int baseDmg = Damage(cfg, spear, mover, false, rng);
+ int baseDmg = Damage(cfg, spear, mover, false, rng, true);
return Mathf.Max(1, Mathf.RoundToInt(baseDmg * cfg.overwatchDamageRatio));
}
@@ -61,6 +73,40 @@ namespace TKS
return null;
}
+ /// 경로(원 캐스트) 위 가장 가까운 유닛(아군·적군 불문). hitDist = 닿기까지 이동 거리. 벽은 무시한다(호출 쪽에서 FreeTravel 과 비교).
+ public static Unit FirstUnitOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, out float hitDist)
+ {
+ hitDist = 0f;
+ foreach (var h in Cast(actor, origin, dir, dist))
+ {
+ var u = UnitOf(h.collider);
+ if (u == null || u == actor || !u.IsAlive) continue;
+ hitDist = h.distance;
+ return u;
+ }
+ return null;
+ }
+
+ /// u 와 가장자리가 닿아 있는(ContactTolerance 이내) 가장 가까운 상대 진영 유닛.
+ public static Unit TouchingOpponent(Unit u)
+ {
+ Physics2D.SyncTransforms();
+ Unit best = null;
+ float bestGap = float.MaxValue;
+ foreach (var c in Physics2D.OverlapCircleAll(u.Position, u.Radius + ContactTolerance + 0.5f))
+ {
+ var o = UnitOf(c);
+ if (o == null || o == u || !o.IsAlive || o.Faction == u.Faction) continue;
+ float gap = Vector2.Distance(u.Position, o.Position) - u.Radius - o.Radius;
+ if (gap <= ContactTolerance && gap < bestGap)
+ {
+ bestGap = gap;
+ best = o;
+ }
+ }
+ return best;
+ }
+
/// 경로 위의 모든 적(가까운 순)과 각 접촉 거리.
public static List EnemiesOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, List distances)
{
@@ -77,8 +123,8 @@ namespace TKS
return list;
}
- /// 벽·(옵션) 유닛에 막히기 전까지 실제로 나아갈 수 있는 거리.
- public static float FreeTravel(Unit actor, Vector2 origin, Vector2 dir, float dist, bool enemiesBlock, bool friendsBlock = true)
+ /// 벽·(옵션) 유닛에 막히기 전까지 실제로 나아갈 수 있는 거리. ignore = 밀어 주는 유닛처럼 출발 지점에서 닿아 있어도 무시할 유닛.
+ public static float FreeTravel(Unit actor, Vector2 origin, Vector2 dir, float dist, bool enemiesBlock, bool friendsBlock = true, Unit ignore = null)
{
float min = dist;
foreach (var h in Cast(actor, origin, dir, dist))
@@ -89,7 +135,7 @@ namespace TKS
continue;
}
var u = UnitOf(h.collider);
- if (u == null || u == actor || !u.IsAlive) continue;
+ if (u == null || u == actor || u == ignore || !u.IsAlive) continue;
bool enemy = u.Faction != actor.Faction;
if (enemy && !enemiesBlock) continue;
if (!enemy && !friendsBlock) continue;
diff --git a/Assets/Scripts/Runtime/GameBootstrap.cs b/Assets/Scripts/Runtime/GameBootstrap.cs
index d6db3287..77362cb2 100644
--- a/Assets/Scripts/Runtime/GameBootstrap.cs
+++ b/Assets/Scripts/Runtime/GameBootstrap.cs
@@ -39,7 +39,7 @@ namespace TKS
cam.transparencySortAxis = Vector3.up;
var board = new GameObject("Board").AddComponent();
- board.Build(Config.boardSize);
+ board.Build(Config.boardSize, Config.boardArt);
CameraCtrl = cam.GetComponent();
if (CameraCtrl == null) CameraCtrl = cam.gameObject.AddComponent();
diff --git a/Assets/Scripts/Runtime/Unit.cs b/Assets/Scripts/Runtime/Unit.cs
index fbaaf7f5..3cd04ac3 100644
--- a/Assets/Scripts/Runtime/Unit.cs
+++ b/Assets/Scripts/Runtime/Unit.cs
@@ -17,8 +17,10 @@ namespace TKS
public int Def;
public float Radius;
public float SlingMaxDistance;
- /// 궁병 = 슬링샷 최대 거리 × archerRangeRatio (적 가장자리까지의 간격 기준) · 창병 = 대기(견제) 반경.
+ /// 궁병 = 사거리 = 슬링샷 최대 거리(적 가장자리까지의 간격 기준 · 제자리 사격) · 창병 = 대기(견제) 반경.
public float AttackRange;
+ /// 궁병이 사거리 밖(~ 사거리 × (1 + archerRangeRatio))의 적에게 다가갈 수 있는 최대 거리. 다른 병종 = 0.
+ public float ArcherMoveAllow;
/// Resources/Units/<ArtId>/ 아트 폴더 · 비면 도형.
public string ArtId = "";
/// 아트 세로 크기(월드) = unitArtHeight × 반지름 배율.
@@ -61,8 +63,9 @@ namespace TKS
Radius = s.radius * radiusMult;
ArtHeight = cfg.unitArtHeight * radiusMult;
SlingMaxDistance = s.slingMaxDistance;
- // PD 2026-11-04: 궁병 사거리 = 슬링샷 범위의 비율(+20 %) · 창병은 자산의 대기 반경
- AttackRange = type == UnitType.Archer ? s.slingMaxDistance * cfg.archerRangeRatio : s.attackRange;
+ // PD 2026-11-04 #1080: 궁병 사거리 = 슬링샷 범위(제자리 사격) · 그 밖 +비율 안의 적은 들어올 만큼만 다가가 사격 · 창병은 자산의 대기 반경
+ AttackRange = type == UnitType.Archer ? s.slingMaxDistance : s.attackRange;
+ ArcherMoveAllow = type == UnitType.Archer ? s.slingMaxDistance * cfg.archerRangeRatio : 0f;
gameObject.name = (faction == Faction.Player ? "P_" : "E_") + displayName;
Collider = GetComponent();
diff --git a/Assets/Tests/PlayMode/BattleFlowTests.cs b/Assets/Tests/PlayMode/BattleFlowTests.cs
index 7d1a9a1a..2e93fcf8 100644
--- a/Assets/Tests/PlayMode/BattleFlowTests.cs
+++ b/Assets/Tests/PlayMode/BattleFlowTests.cs
@@ -7,7 +7,7 @@ using UnityEngine.TestTools;
namespace TKS.Tests
{
- /// 병종 4종 규칙·턴 흐름·승리 판정을 실제 씬에서 검증한다(헤드리스 실행 가능).
+ /// 병종 4종 규칙(PD 2026-11-04 #1080 충돌 규칙 포함)·턴 흐름·승리 판정을 실제 씬에서 검증한다(헤드리스 실행 가능).
public class BattleFlowTests
{
BattleManager bm;
@@ -40,6 +40,17 @@ namespace TKS.Tests
yield return null; // 파괴된 콜라이더가 물리 월드에서 빠질 시간
}
+ /// 경로 질의가 쓰는 접촉 거리(행동 유닛 캐스트 반지름 + 상대 반지름).
+ static float CastContact(Unit mover, Unit other)
+ {
+ return mover.Radius * Combat.CastRadiusScale + other.Radius;
+ }
+
+ static float Gap(Unit a, Unit b)
+ {
+ return Vector2.Distance(a.Position, b.Position) - a.Radius - b.Radius;
+ }
+
[UnityTest]
public IEnumerator Bootstrap_SpawnsHeroesAndStageEnemies()
{
@@ -64,19 +75,24 @@ namespace TKS.Tests
Assert.Greater(c.Matchup(UnitType.Spear, UnitType.Archer), 1f);
}
+ // ---------- 보병 ----------
+
[UnityTest]
- public IEnumerator Infantry_AttacksFirstEnemyOnPath_BonusMoveOnFinalHit()
+ public IEnumerator Infantry_HitsEnemy_StopsAtContact_PushesIt_BonusMoveOnFinalHit()
{
yield return Fresh();
var inf = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -3f), "보병");
var foe = bm.SpawnUnit(UnitType.Spear, Faction.Enemy, new Vector2(0f, 0f), "적창병");
+ float contactY = -3f + (3f - CastContact(inf, foe));
var o = new ActionOutcome();
yield return bm.ResolveForTest(inf, foe.Position, false, o);
Assert.Contains(foe, o.Attacked);
- Assert.Less(foe.Hp, foe.MaxHp, "경로 위 첫 적을 공격");
+ Assert.Less(foe.Hp, foe.MaxHp, "부딪힌 적을 공격");
Assert.IsTrue(o.BonusMove, "최종 목표점 명중 → 추가 이동");
- Assert.Less(Vector2.Distance(inf.Position, foe.Position), inf.Radius + foe.Radius + 0.2f, "적에 닿는 지점까지 이동");
+ Assert.AreEqual(contactY, inf.Position.y, 0.1f, "보병은 부딪힌 지점에서 멈춘다(알까기)");
+ Assert.Contains(foe, o.Pushed, "부딪힌 유닛은 밀려난다");
+ Assert.Greater(foe.Position.y, 0.3f, "적이 남은 힘만큼 밀려남");
Assert.Less(inf.Hp, inf.MaxHp, "근접 공격은 반격 피해를 받는다");
int foeHp = foe.Hp;
@@ -99,6 +115,39 @@ namespace TKS.Tests
Assert.IsFalse(o.BonusMove);
}
+ [UnityTest]
+ public IEnumerator Infantry_PushesAlly_AllyAttacksTouchingEnemy_OncePerPush()
+ {
+ yield return Fresh();
+ // A(보병) 가 B(아군 창병)를 +x 로 밀어 적에게 붙이면 B 가 공격 · C 가 B 를 다시 밀면 B 가 또 공격 (규칙 ⑤)
+ var a = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(-3f, -2f), "보병A");
+ var b = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -2f), "창병B");
+ float pushX = CastContact(a, b); // A 는 3 − 접촉거리 만큼 가서 닿으므로 남는 힘(= 밀리는 거리) = 접촉거리
+ var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(pushX, -2f + b.Radius + 0.45f + 0.05f), "적보병");
+ Assert.AreEqual(b.MaxHp, b.Hp);
+
+ var o = new ActionOutcome();
+ yield return bm.ResolveForTest(a, new Vector2(0f, -2f), false, o);
+ Assert.AreEqual(0, o.Attacked.Count, "아군에 부딪힌 보병 자신은 공격하지 않음");
+ Assert.Contains(b, o.Pushed);
+ Assert.AreEqual(pushX, b.Position.x, 0.1f, "B 가 남은 힘만큼 밀려남");
+ Assert.LessOrEqual(Gap(b, foe), Combat.ContactTolerance + 0.05f, "밀려서 적과 닿음");
+ Assert.Less(foe.Hp, foe.MaxHp, "밀린 아군 B 가 적을 공격");
+ Assert.AreEqual(a.MaxHp, a.Hp, "A 는 피해 없음");
+ Assert.IsFalse(b.ActedThisTurn, "B 의 공격은 자기 행동을 소모하지 않음");
+ Assert.AreEqual(1, o.Notes.Count);
+
+ int hp = foe.Hp;
+ var c = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(pushX, -5f), "보병C");
+ var o2 = new ActionOutcome();
+ yield return bm.ResolveForTest(c, new Vector2(pushX, -2.2f), false, o2);
+ Assert.Contains(b, o2.Pushed, "C 가 B 를 다시 밀었다");
+ Assert.Less(foe.Hp, hp, "B 가 C 의 기회로 또 공격");
+ Assert.AreEqual(0, o2.Attacked.Count);
+ }
+
+ // ---------- 기병 ----------
+
[UnityTest]
public IEnumerator Cavalry_PiercesAllOnPath_AndReturnsOnFinalHit()
{
@@ -129,41 +178,82 @@ namespace TKS.Tests
}
[UnityTest]
- public IEnumerator Archer_RangeIsSlingTimesRatio_MovesIntoRange_ShootsWithoutCounter()
+ public IEnumerator Cavalry_PassesThroughAlly_OrStopsJustBeforeIt()
+ {
+ yield return Fresh();
+ var cav = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(0f, -4f), "기병");
+ var ally = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -1.5f), "아군보병");
+ var o = new ActionOutcome();
+ yield return bm.ResolveForTest(cav, new Vector2(0f, 2f), false, o);
+ Assert.AreEqual(2f, cav.Position.y, 0.1f, "아군을 관통해 목표점까지");
+ Assert.AreEqual(-1.5f, ally.Position.y, 0.01f, "아군은 밀리지 않음");
+ Assert.AreEqual(0, o.Pushed.Count);
+
+ var cav2 = bm.SpawnUnit(UnitType.Cavalry, Faction.Player, new Vector2(4f, -4f), "기병2");
+ var ally2 = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(4f, -1.5f), "아군2");
+ float sum = cav2.Radius + ally2.Radius;
+ var o2 = new ActionOutcome();
+ yield return bm.ResolveForTest(cav2, new Vector2(4f, -1.5f + sum * 0.5f), false, o2); // 지나칠 거리가 안 나옴
+ Assert.AreEqual(-1.5f - sum, cav2.Position.y, 0.1f, "통과할 거리가 안 나오면 아군 직전에 멈춤");
+ Assert.GreaterOrEqual(Vector2.Distance(cav2.Position, ally2.Position), sum - 0.05f, "아군과 겹치지 않음");
+ Assert.AreEqual(-1.5f, ally2.Position.y, 0.01f);
+ }
+
+ // ---------- 궁병 ----------
+
+ [UnityTest]
+ public IEnumerator Archer_ShootsInPlace_WhenEnemyWithinSlingRange()
{
yield return Fresh();
var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병");
- Assert.AreEqual(arc.SlingMaxDistance * cfg.archerRangeRatio, arc.AttackRange, 0.001f, "궁병 사거리 = 슬링샷 최대 × 비율 (PD 2026-11-04 · +20 %)");
- // 적 가장자리까지 4.8 − 반지름 합 ≈ 4.0 ≤ 슬링샷 4.0 + 사거리 0.8 → 행동 가능 · 목표점 (0,−1) 은 적 원 밖(크리 아님)
- var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -0.2f), "적보병");
+ Assert.AreEqual(arc.SlingMaxDistance, arc.AttackRange, 0.001f, "궁병 사거리 = 슬링샷 범위 (PD #1080)");
+ Assert.AreEqual(arc.SlingMaxDistance * cfg.archerRangeRatio, arc.ArcherMoveAllow, 0.001f, "접근 허용 = 슬링샷 × 비율");
+ var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -1.5f), "적보병");
+ Assert.Less(3.5f - CastContact(arc, foe), arc.AttackRange, "전제: 적 가장자리가 사거리 안");
var o = new ActionOutcome();
- yield return bm.ResolveForTest(arc, new Vector2(0f, -1f), false, o);
- Assert.Contains(foe, o.Attacked, "조준 방향의 적을 사거리까지 이동 후 사격");
+ yield return bm.ResolveForTest(arc, new Vector2(0f, -2.5f), false, o);
+ Assert.Contains(foe, o.Attacked, "사거리 안의 적을 사격");
Assert.Less(foe.Hp, foe.MaxHp);
- Assert.AreEqual(arc.MaxHp, arc.Hp, "궁병은 반격을 받지 않는다");
- float gap = Vector2.Distance(arc.Position, foe.Position) - arc.Radius * Combat.CastRadiusScale - foe.Radius;
- Assert.LessOrEqual(gap, arc.AttackRange + 0.1f, "적 가장자리가 사거리에 들어올 때까지만 이동");
- Assert.Greater(arc.Position.y, -5f + 2.5f, "사거리에 들어오기 위해 실제로 이동");
+ Assert.Less(Vector2.Distance(arc.Position, new Vector2(0f, -5f)), 0.01f, "움직이지 않고 제자리에서");
+ Assert.AreEqual(arc.MaxHp, arc.Hp, "원거리 사격은 반격을 받지 않는다");
Assert.IsFalse(o.Crit);
}
[UnityTest]
- public IEnumerator Archer_MovesToTarget_WhenNoEnemyWithinSlingPlusRatio()
+ public IEnumerator Archer_StepsInOnlyAsNeeded_WhenEnemyInDonut()
+ {
+ yield return Fresh();
+ var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병");
+ var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0.3f), "적보병");
+ float hitDist = 5.3f - CastContact(arc, foe);
+ Assert.Greater(hitDist, arc.AttackRange, "전제: 사거리 밖");
+ Assert.LessOrEqual(hitDist, arc.AttackRange + arc.ArcherMoveAllow, "전제: 공격 가능 범위(도넛) 안");
+ float need = hitDist - arc.AttackRange;
+ var o = new ActionOutcome();
+ yield return bm.ResolveForTest(arc, new Vector2(0f, -1f), false, o);
+ Assert.Contains(foe, o.Attacked, "사거리에 들어올 만큼만 다가가 사격");
+ Assert.AreEqual(-5f + need, arc.Position.y, 0.1f, "필요한 만큼만 이동");
+ Assert.AreEqual(arc.MaxHp, arc.Hp);
+ Assert.IsFalse(o.Crit);
+ }
+
+ [UnityTest]
+ public IEnumerator Archer_MovesToTarget_WhenNoEnemyWithinReach()
{
yield return Fresh();
var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병");
- // 적 가장자리까지 6.0 − 반지름 합 ≈ 5.2 > 슬링샷 + 사거리 → 공격 없이 목표점까지 이동(PD 2026-11-04 2차 지시)
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 1f), "적보병");
+ Assert.Greater(6f - CastContact(arc, foe), arc.AttackRange + arc.ArcherMoveAllow, "전제: 공격 가능 범위 밖");
Assert.IsFalse(bm.AnyEnemyWithinArcherReach(arc), "공격 가능 범위 안에 적 없음(도넛 회색)");
var o = new ActionOutcome();
var target = new Vector2(0f, -1f);
yield return bm.ResolveForTest(arc, target, false, o);
- Assert.IsTrue(o.MovedOnly, "슬링샷 범위 + 비율 안에 적이 없으면 궁병은 목표점까지 이동만 한다");
+ Assert.IsTrue(o.MovedOnly, "공격 가능 범위에 적이 없으면 목표점까지 이동만 (PD 2026-11-04 2차 지시)");
Assert.AreEqual(0, o.Attacked.Count);
Assert.AreEqual(foe.MaxHp, foe.Hp);
Assert.Less(Vector2.Distance(arc.Position, target), 0.05f, "목표 지점까지 이동");
- var near = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -0.4f), "가까운적");
+ var near = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(2f, -1f), "가까운적");
Assert.IsTrue(bm.AnyEnemyWithinArcherReach(arc), "범위 안에 적이 생기면 도넛 붉게");
Assert.IsTrue(near.IsAlive);
}
@@ -182,13 +272,42 @@ namespace TKS.Tests
}
[UnityTest]
- public IEnumerator Spear_OverwatchOnFinalHit_DamagesMovingEnemy()
+ public IEnumerator Archer_MeleePenalty_DealsRatio_TakesRatio_RangedUnaffected()
+ {
+ yield return Fresh();
+ var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병");
+ var arc2 = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(2f, -5f), "궁병2");
+ var inf = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(4f, -5f), "적보병");
+
+ int ranged = Combat.Damage(cfg, arc, inf, false, new System.Random(7), false);
+ int melee = Combat.Damage(cfg, arc, inf, false, new System.Random(7), true);
+ Assert.AreEqual(ranged * cfg.archerMeleeDealtRatio, melee, 1f, "궁병이 근접 유닛과 근접 전투하면 주는 피해 × 비율(기본 50 %)");
+
+ int takenRanged = Combat.Damage(cfg, inf, arc, false, new System.Random(7), false);
+ int takenMelee = Combat.Damage(cfg, inf, arc, false, new System.Random(7), true);
+ Assert.AreEqual(takenRanged * cfg.archerMeleeTakenRatio, takenMelee, 1f, "근접 유닛에게 근접으로 맞으면 받는 피해 × 비율(기본 +50 %)");
+
+ int aa = Combat.Damage(cfg, arc, arc2, false, new System.Random(7), true);
+ int ab = Combat.Damage(cfg, arc, arc2, false, new System.Random(7), false);
+ Assert.AreEqual(ab, aa, "궁병끼리는 근접 패널티 없음(근접 유닛 상대만)");
+ yield return null;
+ }
+
+ // ---------- 창병 ----------
+
+ [UnityTest]
+ public IEnumerator Spear_PlowsEnemyForward_OverwatchOnFinalHit_DamagesMovingEnemy()
{
yield return Fresh();
var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -3f), "창병");
var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 0f), "적보병");
var o = new ActionOutcome();
yield return bm.ResolveForTest(spear, foe.Position, false, o);
+ Assert.Contains(foe, o.Attacked);
+ Assert.Less(foe.Hp, foe.MaxHp, "부딪힌 적에게 피해");
+ Assert.Contains(foe, o.Pushed, "튕기지 않고 밀며 함께 전진");
+ Assert.Greater(foe.Position.y, 0.5f, "적이 남은 거리만큼 앞으로 밀림");
+ Assert.AreEqual(CastContact(spear, foe), foe.Position.y - spear.Position.y, 0.1f, "창병은 접촉 간격을 유지하며 따라감");
Assert.IsTrue(o.Overwatch && spear.Overwatch, "최종 목표점 명중 → 대기(견제)");
int hp = foe.Hp;
@@ -203,6 +322,29 @@ namespace TKS.Tests
Assert.AreEqual(farHp, far.Hp);
}
+ [UnityTest]
+ public IEnumerator Spear_PlowsAlly_NoDamage_AllyAttacksWhenPushedOntoEnemy()
+ {
+ yield return Fresh();
+ var spear = bm.SpawnUnit(UnitType.Spear, Faction.Player, new Vector2(0f, -4f), "창병");
+ var ally = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -2f), "아군보병");
+ var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 1.5f), "적보병");
+ int allyHp = ally.Hp;
+ var o = new ActionOutcome();
+ yield return bm.ResolveForTest(spear, new Vector2(0f, 0f), false, o);
+ Assert.AreEqual(0, o.Attacked.Count, "아군에 부딪힌 창병 자신은 공격하지 않음");
+ Assert.Contains(ally, o.Pushed);
+ Assert.Greater(ally.Position.y, 0.4f, "아군을 밀며 함께 전진(적에 닿을 때까지)");
+ Assert.LessOrEqual(Gap(ally, foe), Combat.ContactTolerance + 0.05f, "밀린 아군이 적과 닿음");
+ Assert.AreEqual(CastContact(spear, ally), ally.Position.y - spear.Position.y, 0.1f, "창병은 아군 바로 뒤까지 따라옴");
+ Assert.Less(foe.Hp, foe.MaxHp, "밀린 아군이 적을 공격(규칙 ⑤)");
+ Assert.AreEqual(spear.MaxHp, spear.Hp, "창병은 피해 없음");
+ Assert.AreEqual(1, o.Notes.Count);
+ Assert.IsTrue(allyHp >= ally.Hp, "아군은 밀려서 피해를 받지 않는다(근접 공격 반격만 가능)");
+ }
+
+ // ---------- 턴 · 승패 · AI · 카메라 · 표시 ----------
+
[UnityTest]
public IEnumerator EnemyTurn_RunsAndReturnsToPlayerTurn()
{
@@ -313,7 +455,7 @@ namespace TKS.Tests
[UnityTest]
public IEnumerator UnitArt_HeroWithArt_PlaysBackIdle_UnitWithoutArt_IsShape()
{
- // 부트스트랩 스테이지 1: 유비·관우·장비 = 스켈레탈 세션 아트(Resources/Units/liubei·guanyu·zhangfei) · 조운·황충·적 = 도형
+ // 부트스트랩 스테이지 1: 유비·관우·장비 = 스켈레탈 세션 아트(Resources/Units/liubei·guanyu·zhangfei) · 조운·황충 = 도형
Unit liubei = null;
Unit shape = null;
foreach (var u in bm.AliveUnits(Faction.Player))
@@ -332,6 +474,29 @@ namespace TKS.Tests
liubei.View.PlayAttack();
Assert.AreEqual("back_action", liubei.View.Anim.Current, "공격 자세 클립");
+
+ // 원화 세션 1차 납품(2026-11-04): 황건적 5종 정지 그림(front_idle 1장) + 전장 배경
+ int enemyArt = 0;
+ foreach (var e in bm.AliveUnits(Faction.Enemy))
+ {
+ if (e.View.HasArt)
+ {
+ enemyArt++;
+ Assert.AreEqual("front_idle", e.View.Anim.Current, "적은 앞모습 정지 그림");
+ }
+ }
+ Assert.AreEqual(bm.AliveCount(Faction.Enemy), enemyArt, "스테이지 1 황건적은 전원 원화(hgj_*)로 표시");
+ Assert.IsTrue(bm.Board.HasArt, "전장 배경 = Resources/Board/" + cfg.boardArt);
+
+ // 원화 2차 납품(2026-11-04): 동탁군 보·창·기·궁 4종 반영 · 여포(dz_boss_lvbu)는 보류 → 도형
+ bm.StartStage(1);
+ yield return null;
+ foreach (var e in bm.AliveUnits(Faction.Enemy))
+ {
+ if (e.IsBoss) continue;
+ Assert.IsTrue(e.View.HasArt, "스테이지 2 동탁군 병종 4종은 원화(" + e.ArtId + ")");
+ }
+ bm.StartStage(0);
yield return null;
}
}
diff --git a/CLAUDE.md b/CLAUDE.md
index 93291931..5df6470f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -44,9 +44,10 @@ Unity.exe -projectPath -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
## 코드 규칙
- 수치·영웅·스테이지 SOT = `Assets/Resources/BalanceConfig.asset`(Inspector 조정 · 최초 시드만 `Assets/Scripts/Editor/BalanceDefaults.cs`). 코드·테스트에 수치를 직접 적지 않는다(C45).
-- 규칙 해석 변경 = `Assets/Scripts/Runtime/ActionResolver.cs`(병종 4종) · `BattleManager.cs`(턴·승패·창병 대기) · `Combat.cs`(피해 공식·경로 질의). 변경 시 `Assets/Tests/PlayMode/BattleFlowTests.cs` 에 테스트 추가 후 14종 이상 전부 통과 확인.
+- 규칙 해석 변경 = `Assets/Scripts/Runtime/ActionResolver.cs`(병종 4종) · `BattleManager.cs`(턴·승패·창병 대기) · `Combat.cs`(피해 공식·경로 질의). 변경 시 `Assets/Tests/PlayMode/BattleFlowTests.cs` 에 테스트 추가 후 22종 이상 전부 통과 확인.
- 콜라이더 질의 전 `Physics2D.SyncTransforms()` · 유닛 제거는 `SetActive(false)` 후 `Destroy` · 자식 렌더러가 있는 표시물은 GameObject 통째로 끈다.
-- **궁병 규칙(PD 2026-11-04)**: 사거리 = `slingMaxDistance × archerRangeRatio`(자산 · 기본 0.2 = 슬링샷 범위 +20 %). 조준 방향 「슬링샷 거리 + 사거리」 안에 적이 있으면 적 **가장자리**가 사거리에 들어올 때까지만 이동 후 사격 · 적이 없으면 **목표점까지 이동만**(PD 2026-11-04 2차 지시 · 1차의 「행동 취소」 폐기 · AI 적 궁병도 같음). `UnitStats.attackRange` 는 창병 견제 반경만.
+- **궁병 규칙(PD 2026-11-04 · #1080)**: 사거리 = `slingMaxDistance`(얇은 원). 조준 방향 사거리 안의 적(가장자리 간격 기준) = **제자리 사격** · 사거리 밖 ~ ×(1 + `archerRangeRatio`)(붉은 도넛) 안의 적 = 들어올 만큼만(최대 `ArcherMoveAllow` = 슬링 × 비율) 다가가 사격 · 더 멀면 **목표점까지 이동만**(#1079). 반격 없음 · 목표점 명중 크리. **근접 전투**(돌진·반격·관통·견제·밀려 닿은 공격)에서 궁병은 근접 유닛 상대 주는 피해 × `archerMeleeDealtRatio`(0.5) · 받는 피해 × `archerMeleeTakenRatio`(1.5) — `Combat.Damage(…, melee)` · 원거리 사격은 패널티 없음.
+- **충돌 규칙(PD 2026-11-04 · #1080)** — `ActionResolver`: 보병 = 경로 첫 유닛(아군·적군)에 부딪히면 **알까기**(자신은 접촉점에 멈춤 · 상대는 접촉 방향으로 남은 거리 × `collisionPushRatio` 만큼 밀림 · 적이면 공격+반격 뒤 밀림) · 기병 = 유닛 무시 관통(적 피해 · 아군은 지나칠 거리가 안 나오면 **그 아군 직전 정지** `StopBeforeUnclearedAllies`) · 창병 = 첫 유닛을 **밀며 함께 전진**(접촉 간격 유지 · 앞 유닛이 막히면 둘 다 정지 · 적이면 피해 · 아군은 밀기만) · **규칙 ⑤** = 밀린 같은 진영 유닛이 상대와 닿으면(`Combat.TouchingOpponent` · 간격 ≤ 0.12) 그 자리에서 근접 공격 1회(조작마다 · 자기 행동 소모 없음 · `ActionOutcome.Notes`). 적 AI 도 같은 코드(대칭). `UnitStats.attackRange` 는 창병 견제 반경만.
- **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색).
- 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만.
@@ -54,7 +55,8 @@ Unity.exe -projectPath -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
- 아트 위치 = `Assets/Resources/Units///f000.png …` + `meta.json`(fps · ppu · pivot(발) · charHeightPx · clips). `` 는 `BalanceConfig.asset` 의 `heroes[].art` / `stages[].enemies[].art`(비면 도형). 아군 = `liubei`·`guanyu`·`zhangfei`(스켈레탈 세션 리그 프레임) · 적 = `<세력>_<병종>`(`hgj_infantry` …) · 보스 = `<세력>_boss_<로마자>`(`hgj_boss_zhangjiao` …).
- 스켈레탈 리그 → 프레임: 조직 레포 스킬 `skeletal-anim-pipeline/files/scripts/rig_frames_export.py rig <리그 패키지> --parts <작업>/parts --out Assets/Resources/Units/ --clips back_idle,back_ready,back_action,back_damage,back_stop,front_idle,front_ready,front_action,front_damage,front_stop,front_victory,front_failed --px 180 --step 2`(ComfyUI venv python · 실측 482×470 · 15 fps · 3인 11 MB).
-- 정지 그림(적 SD 전신 `_rgba.png`) → `rig_frames_export.py static --out Assets/Resources/Units/`(front_idle 1장 · 발 = 불투명 상자 아래).
+- 정지 그림(적 SD 전신 `_rgba.png`) → `rig_frames_export.py static --out Assets/Resources/Units/`(front_idle 1장 · 발 = 불투명 상자 아래). 원화 세션 납품 = `C:\ArtRnD\samguk_game\manifest.json` · 파일명 `_rgba.png` → `` 폴더(2026-11-04 1차 `hgj_*` 5종 + 2차 `dz_infantry·spear·cavalry·archer` 반영 완료 · 여포 `dz_boss_lvbu`·`cc_*`·`wei_*` 는 납품 시 같은 명령).
+- 전장 배경 = `Assets/Resources/Board/.png`(자산 `boardArt` · 5:8 불투명 · PPU 100 = 전장 20×32) → `Board.Build` 가 있으면 바닥으로 쓰고 격자·진영 띠는 생략. 임포트 규칙(스프라이트 · 4096 · 압축)은 같은 `UnitArtImporter` 의 `Board/` 분기 · 재임포트 메뉴가 두 폴더 처리.
- 임포트 설정은 `Editor/UnitArtImporter.cs`(AssetPostprocessor · meta 의 pivot·ppu) 가 자동 적용 · 프레임을 넣거나 meta 를 바꾼 뒤 = 메뉴 `TKS/Reimport Unit Art`(Build/BuildFresh 가 함께 호출).
- 재생 = `UnitArt.cs`(Resources 로더 · 캐시) + `FrameAnimator`(대기 loop · 준비 = 조준 중 · 공격·피격 = 1프레임 자세 유지 후 대기 · 승리 = 끝 프레임 유지) · `UnitView` 가 아군 = `back_*` · 적군 = `front_*` 로 고르고 없으면 다른 보기 → 대기. 세로 크기 = `unitArtHeight`(자산) × 반지름 배율 · 발 = 원 중심 아래 0.55 R.
diff --git a/README.md b/README.md
index 7292896e..89ec0ee2 100644
--- a/README.md
+++ b/README.md
@@ -22,12 +22,12 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
| 병종 | 경로 위 적 | 최종 목표점 명중 시 |
|---|---|---|
-| 보병 | 첫 적까지 이동 후 공격(반격 받음) | 추가 이동 1회(이동만) |
-| 기병 | 경로 위 모든 적 관통 피해 · 목표점까지 이동 | 제자리로 복귀 |
-| 창병 | 보병과 동일 | 대기 — 적군 턴에 반경 안에서 움직이는 적에게 피해 |
-| 궁병 | 목표 방향 「슬링샷 거리 + 사거리」 안의 적을 향해 사거리까지 이동 후 사격(반격 없음) · 적이 없으면 목표점까지 이동 | 무조건 크리티컬 |
+| 보병 | 첫 유닛(아군·적군)에 부딪히면 알까기처럼 남은 힘만큼 밀어내고 자신은 멈춤 · 적이면 공격(반격 받음) | 추가 이동 1회(이동만) |
+| 기병 | 유닛을 무시하고 관통 · 경로 위 적은 모두 피해 · 아군은 지나칠 거리가 안 나오면 직전에 멈춤 | 제자리로 복귀 |
+| 창병 | 첫 유닛을 튕기지 않고 밀며 함께 전진 · 적이면 피해(반격 받음) · 아군은 밀기만 | 대기 — 적군 턴에 반경 안에서 움직이는 적에게 피해 |
+| 궁병 | 사거리(= 슬링샷 범위) 안의 적은 제자리 사격 · 사거리 밖 +20 % 안의 적은 들어올 만큼만 다가가 사격(반격 없음) · 더 멀면 목표점까지 이동 | 무조건 크리티컬 |
-상성: 보병 > 창병 > 기병 > 보병. 궁병은 모두에게 약함(받는 피해 1.5배).
+밀린 아군이 적과 닿으면 그 자리에서 공격 1회(조작마다 · 자기 행동 소모 없음). 상성: 보병 > 창병 > 기병 > 보병. 궁병은 모두에게 약함(받는 피해 1.5배) · 근접 유닛과 근접 전투하면 주는 피해 50 % · 받는 피해 +50 %(원거리 사격은 패널티 없음).
## 데이터(SOT)