Files
zeling_v2/Docs_Dev/superpowers/plans/2026-07-10-enemy-sprite-driven-box-colliders.md
T
2026-07-21 10:22:43 +08:00

380 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 敌人碰撞体 Sprite 驱动 + 统一 Box + 全员接触伤害区 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让脚手架 `SceneObjectPlacerTool` 生成的每个敌人(含 Boss)的主体/HurtBox/ContactDamageZone 三者统一为 BoxCollider2D、同尺寸、底部对齐 y=0,尺寸由向导默认 Sprite 的包围盒推导(留空回退硬编码),且每个敌人都有 ContactDamageZone。
**Architecture:**`SceneObjectPlacerTool` 新增两个静态助手(`SpriteSizeOr``SetupHurtAndContactBoxes`),把各 `PlaceExxx` 里参差的 Capsule/Circle HurtBox/接触伤害创建统一替换为"主体 Box(size) + 助手建 HurtBox/ContactDamageZone(Box, size, trigger, 底部对齐)"`CharacterWizardWindow` 加一个可选默认 Sprite 字段并透传。只改这两个编辑器脚本,不动任何 prefab。
**Tech Stack:** Unity 2022.3 (C# 9)、UnityEditor、UI Toolkit(向导)、Unity MCP(端口 7890)做编译门与放置验证。
**设计文档:** `Docs/superpowers/specs/2026-07-10-enemy-sprite-driven-box-colliders-design.md`
**全局约定:**
- **编译门**`unity_execute_code`(port 7890) 跑 `AssetDatabase.Refresh(); CompilationPipeline.RequestScriptCompilation(); return "ok";` → Bash `sleep 9``unity_get_compilation_errors` severity=error 期望 count=0。桥接超时则 `unity_editor_ping` 等恢复再重试;长会话保持 Unity 窗口前台(失焦会卡死队列)。
- 每个 Task 结束提交一次(中文 + 类型前缀)。分支已在 `fix/e001-chase-facing-and-cd-patrol`(或按需另开)。
- **不修改任何 prefab**(用户后续手动重生成替换)。
- **放置验证**`PlaceExxx` 会在活动场景创建一个敌人 GameObject。验证时先确认活动场景是 TestRoomA(或任意可写场景),放置后读该对象的碰撞体,验证完 `DestroyImmediate` 清理,避免污染场景。
---
## 关键事实(实现前必读,均已核实)
- `EnemyBodyColliderType { Box, Capsule, Circle }`(SceneObjectPlacerTool.cs:44)`CreateBodyCollider(GameObject, EnemyBodyColliderType, Vector2 size)` 建主体碰撞体并 `AlignColliderBottomToPivot`
- `AlignColliderBottomToPivot(Collider2D)`Box/Capsule 设 `offset.y = size.y*0.5`Circle 设 `offset.y = radius` —— 底部对齐 y=0。
- `EnsureCollidersAreTriggers(GameObject)` 已存在:把节点全部 Collider2D 设 `isTrigger=true`
- `GetOrCreateChild(Transform, string)``GetOrAddComponent<T>(GameObject)``SetLayer(GameObject, string, List<string>)``AssignReference(Object, string, Object, List<string>)``FindFirstAsset(params string[])` 均已存在。
- `SetupSpriteRenderer(GameObject) → SpriteRenderer`:若 sr.sprite 为空则填一个默认 Square;返回 sr。
- 各敌人现有 fallback 主体尺寸(Vector2)E001=0.6×0.8、E002=0.5×0.7、E003=0.5×0.6、E004=0.8×1.2、E005=0.9×1.0、E006=0.7×1.0、ChaoFeng=1.2×2.0、PlaceEnemy(通用)=0.7×0.9、PlaceBossEnemy(通用)=1.5×2.5。
- E001 接触伤害现有接线:`BodyContactDamage.enabled=false`(伪装期不伤,追击能力开)`ContactChaseAbility._contactDamage` 绑该 BodyContactDamageContactDamageZone 的 `HitBox._defaultSource``FindFirstAsset("CMB_DS_EnemyBody","DS_EnemyBody")`
- 命名空间:`BaseGames.Combat`(HurtBox/HitBox)、`BaseGames.Enemies`(BodyContactDamage)。SceneObjectPlacerTool 顶部已 `using` 这些。
---
## Task 1: 新增两个助手 `SpriteSizeOr` / `SetupHurtAndContactBoxes`
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs`(紧邻现有 `EnsureCollidersAreTriggers` 助手之后)
- [ ] **Step 1: 加两个助手**
`EnsureCollidersAreTriggers` 方法之后插入:
```csharp
/// <summary>Sprite 有则用其世界包围盒尺寸(sprite.bounds.size),否则回退。</summary>
private static Vector2 SpriteSizeOr(Sprite s, Vector2 fallback)
=> s != null ? (Vector2)s.bounds.size : fallback;
/// <summary>
/// 建/更新 HurtBox 与 ContactDamageZone 两个子节点:均为 BoxCollider2D、同尺寸 size、
/// 底部对齐 y=0、isTrigger=true。ContactDamageZone 带 HitBox + BodyContactDamage。
/// contactEnabledBodyContactDamage 初始启用状态(不需要接触伤害的敌人可传 false 或后续置节点非激活)。
/// 返回 (hurtBox, bodyContact, contactHitBox) 供上层按需接线(如伤害源/能力引用)。
/// </summary>
private static (HurtBox hurt, BodyContactDamage contact, HitBox contactHitBox)
SetupHurtAndContactBoxes(GameObject root, Vector2 size, bool contactEnabled, List<string> report)
{
// HurtBox 子节点
var hurtT = GetOrCreateChild(root.transform, "HurtBox");
SetLayer(hurtT.gameObject, "EnemyHurtBox", report);
var hurtCol = GetOrAddComponent<BoxCollider2D>(hurtT.gameObject);
hurtCol.size = size;
hurtCol.isTrigger = true;
AlignColliderBottomToPivot(hurtCol);
var hurtBox = GetOrAddComponent<HurtBox>(hurtT.gameObject);
EnsureCollidersAreTriggers(hurtT.gameObject);
// ContactDamageZone 子节点
var contactT = GetOrCreateChild(root.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
var contactCol = GetOrAddComponent<BoxCollider2D>(contactT.gameObject);
contactCol.size = size;
contactCol.isTrigger = true;
AlignColliderBottomToPivot(contactCol);
var contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
var bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
bodyContact.enabled = contactEnabled;
EnsureCollidersAreTriggers(contactT.gameObject);
return (hurtBox, bodyContact, contactHitBox);
}
```
注意:`HurtBox`/`HitBox``BaseGames.Combat``BodyContactDamage``BaseGames.Enemies`,文件顶部已 using。`GetOrAddComponent<BoxCollider2D>` 若节点已有别的 Collider2D 不会移除——但本任务不改现有 prefab,全新放置的节点只会有这一个 Box,无残留。
- [ ] **Step 2: 编译门** → count=0(此时助手未被调用,只验证语法/引用)。
- [ ] **Step 3: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
git commit -m "feat(scaffold): 新增 SpriteSizeOr / SetupHurtAndContactBoxes 助手(Box 统一 HurtBox/接触伤害区)"
```
---
## Task 2: E001 改用助手 + sprite 参数(参考样板)
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs``PlaceE001_CaoZhi`
- [ ] **Step 1: 改签名加 sprite 参数**
把重载:
```csharp
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box);
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider)
```
改为:
```csharp
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box, null);
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
```
- [ ] **Step 2: 用 sprite 推导 size、赋 SpriteRenderer、走助手**
E001 现有片段(约 457-489 行)大致为:
```csharp
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.6f, 0.8f));
...
SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject);
...
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
hurtCap.isTrigger = true;
hurtCap.size = new Vector2(0.55f, 0.75f);
AlignColliderBottomToPivot(hurtCap);
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
EnsureCollidersAreTriggers(hurtBoxT.gameObject);
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.4f;
AlignColliderBottomToPivot(contactCol);
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
bodyContact.enabled = false;
EnsureCollidersAreTriggers(contactT.gameObject);
```
改为(size 用 sprite,主体 Box(size)HurtBox/ContactDamageZone 走助手;E001 接触伤害仍禁用+由能力控制):
```csharp
Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(0.6f, 0.8f));
Collider2D body = CreateBodyCollider(go, bodyCollider, size);
...
SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject);
if (defaultSprite != null) sr1.sprite = defaultSprite;
...
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, contactEnabled: false, report); // E001 伪装期不伤,追击能力开
```
保留后续对 `bodyContact` / `contactHitBox` / `hurtBox` 的接线不变,即:
- `AssignReference(enemyBase, "_hurtBox", hurtBox, report)` 若原有(用返回的 hurtBox)。
- `AssignReference(chaseAbility, "_contactDamage", bodyContact, report)` 保留。
- 伤害源:`Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody", "DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);` 保留。
> 实现前 Read 当前 `PlaceE001_CaoZhi` 全文,把上面被替换段之外的接线原样保留(只换碰撞体创建方式与 size 来源)。`sr1` 变量名沿用现有。
- [ ] **Step 3: 编译门** → count=0。
- [ ] **Step 4: 放置验证(MCP)**
`unity_execute_code`(port 7890):反射调用 `PlaceE001_CaoZhi(EnemyBodyColliderType.Box, null)`(无 sprite→回退 0.6×0.8),然后校验并清理:
```csharp
var t = System.AppDomain.CurrentDomain.GetAssemblies().SelectMany(a=>{try{return a.GetTypes();}catch{return new System.Type[0];}}).FirstOrDefault(x=>x.Name=="SceneObjectPlacerTool");
var m = t.GetMethod("PlaceE001_CaoZhi", new[]{ t.GetNestedType("EnemyBodyColliderType") ?? typeof(int), typeof(UnityEngine.Sprite) });
// 若上面拿不到,改用带默认参数的 2-参重载:
m = t.GetMethods().First(x=>x.Name=="PlaceE001_CaoZhi" && x.GetParameters().Length==2);
var enumType = m.GetParameters()[0].ParameterType;
m.Invoke(null, new object[]{ System.Enum.ToObject(enumType, 0), null });
var go = UnityEngine.GameObject.Find("ENM_CaoZhi");
var sb=new System.Text.StringBuilder();
foreach(var name in new[]{"","HurtBox","ContactDamageZone"}){
var node = name==""? go.transform : go.transform.Find(name);
var col = node.GetComponent<UnityEngine.Collider2D>();
string label = name==""? "BODY" : name;
if(col is UnityEngine.BoxCollider2D b)
sb.AppendLine(label+": Box size="+b.size.ToString("F2")+" isTrigger="+b.isTrigger+" offset.y="+b.offset.y.ToString("F3")+" (期望 offset.y=size.y/2="+(b.size.y*0.5f).ToString("F3")+")");
else sb.AppendLine(label+": "+(col!=null?col.GetType().Name:"NO COLLIDER"));
}
sb.AppendLine("ContactDamageZone.BodyContactDamage.enabled="+go.transform.Find("ContactDamageZone").GetComponent<BaseGames.Enemies.BodyContactDamage>().enabled+" (E001 期望 false)");
UnityEngine.Object.DestroyImmediate(go); // 清理
return sb.ToString();
```
期望:BODY/HurtBox/ContactDamageZone 都是 Box、size=(0.60,0.80)、HurtBox/Contact isTrigger=True、BODY isTrigger=False、三者 offset.y=size.y/2、ContactDamage.enabled=False。
- [ ] **Step 5: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
git commit -m "refactor(scaffold): E001 用统一助手建 Box HurtBox/接触伤害区 + sprite 尺寸参数"
```
---
## Task 3: E002-E006 改用助手 + sprite 参数
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs``PlaceE002_HuangZhi``PlaceE003_YouZhi_Enemy``PlaceE004_ZhiMu_Enemy``PlaceE005_FeiZhi_Enemy``PlaceE006_Huan`
对每个敌人应用**相同变换**(下方 fallback size 逐敌人不同):
- [ ] **Step 1: 每个敌人改签名加 sprite 参数**
例(E002)
```csharp
public static void PlaceE002_HuangZhi() => PlaceE002_HuangZhi(EnemyBodyColliderType.Box, null);
public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
```
E003/E004/E005/E006 同理(保持各自方法名)。
- [ ] **Step 2: 每个敌人替换碰撞体创建**
对每个 `PlaceExxx`Read 其当前实现,然后:
1. 主体:`Vector2 size = SpriteSizeOr(defaultSprite, <fallback>);``CreateBodyCollider(go, bodyCollider, size);`(替换原 `new Vector2(...)` 硬编码)。
2. SpriteRenderer:找到该敌人的 `SetupSpriteRenderer(...)` 调用(变量如 sr/sr3/sr5),其后加 `if (defaultSprite != null) <sr>.sprite = defaultSprite;`
3. HurtBox + ContactDamageZone:删除该敌人现有的 HurtBox(Capsule) 与(若有)hit/ContactDamageZone(Circle) 创建代码,改为:
`var (hurtBox, bodyContact, contactHitBox) = SetupHurtAndContactBoxes(go, size, contactEnabled: true, report);`
E002/E004/E005 原本**没有** ContactDamageZone——现在统一补上,contactEnabled=true。)
4. 保留该敌人原有对 `hurtBox` 的接线(如 `AssignReference(enemyBase, "_hurtBox", hurtBox, report)`;用返回的 hurtBox 替换原局部变量名)。若该敌人的某个攻击能力原本引用了旧 HurtBox 变量,改用返回的 `hurtBox`
5. 伤害源接线(统一加,若该敌人原本没有):`Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody", "DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);`
**逐敌人 fallback size**
- E002 `PlaceE002_HuangZhi``new Vector2(0.5f, 0.7f)`
- E003 `PlaceE003_YouZhi_Enemy``new Vector2(0.5f, 0.6f)`
- E004 `PlaceE004_ZhiMu_Enemy``new Vector2(0.8f, 1.2f)`
- E005 `PlaceE005_FeiZhi_Enemy``new Vector2(0.9f, 1.0f)`
- E006 `PlaceE006_Huan``new Vector2(0.7f, 1.0f)`
> 注意:某些敌人(如 E002)的攻击能力有独立的 `_attackHitBox` / `_hurtBox` 引用——这些是**能力自己的 HitBox**,与本任务的 HurtBox/ContactDamageZone 无关,勿动;只替换"主体受击 HurtBox"与"接触伤害 ContactDamageZone"两处。实现前务必 Read 每个 PlaceExxx 全文分清。
- [ ] **Step 3: 编译门** → count=0。
- [ ] **Step 4: 放置验证(MCP)**
对 E002-E006 各放置一次并校验(同 Task 2 的验证代码,替换方法名与期望 GameObject 名 ENM_HuangZhi/ENM_YouZhi/ENM_ZhiMu/ENM_FeiZhi/ENM_Huan,期望 size=各 fallback、三者 Box+底部对齐、HurtBox/Contact isTrigger=True、ContactDamage.enabled=True)。每次验证后 `DestroyImmediate` 清理。
- [ ] **Step 5: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
git commit -m "refactor(scaffold): E002-E006 统一 Box HurtBox/接触伤害区 + sprite 尺寸(补全缺失的接触伤害区)"
```
---
## Task 4: ChaoFeng(Boss) + 通用 PlaceEnemy/PlaceBossEnemy 改用助手 + sprite
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs``PlaceChaoFeng``PlaceEnemy``PlaceBossEnemy`
- [ ] **Step 1: PlaceChaoFeng 加 sprite 参数 + 助手**
```csharp
public static void PlaceChaoFeng() => PlaceChaoFeng(EnemyBodyColliderType.Box, null);
public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider, Sprite defaultSprite = null)
```
体内:`Vector2 size = SpriteSizeOr(defaultSprite, new Vector2(1.2f, 2.0f));``CreateBodyCollider(go, bodyCollider, size);`SpriteRenderer 后加 sprite 赋值;HurtBox 段(Capsule)替换为 `var (hurtBox, bodyContact, contactHitBox) = SetupHurtAndContactBoxes(go, size, contactEnabled: true, report);`(Boss 原**无** ContactDamageZone,现补上,默认启用,策划按需置非激活);保留 Boss 原有 hurtBox 接线;加伤害源接线(同上)。
- [ ] **Step 2: 通用 PlaceEnemy / PlaceBossEnemy 同样处理**
`PlaceEnemy(EnemyBodyColliderType, Sprite defaultSprite=null)` fallback `new Vector2(0.7f, 0.9f)``PlaceBossEnemy(...)` fallback `new Vector2(1.5f, 2.5f)`。各自:size 用 sprite、CreateBodyCollider(size)、sprite 赋 Sr、`SetupHurtAndContactBoxes(go, size, contactEnabled: true, report)`(若原本没有 HurtBox/ContactDamageZone 则新增;有则替换)。加无参重载 `=> PlaceXxx(EnemyBodyColliderType.Box, null)` 若需要。
- [ ] **Step 3: 编译门** → count=0。
- [ ] **Step 4: 放置验证(MCP)**:放置 ChaoFeng(ENM_ChaoFeng),校验三者 Box、size=(1.20,2.00)、HurtBox/Contact isTrigger=True、底部对齐、ContactDamageZone 存在。`DestroyImmediate` 清理。
- [ ] **Step 5: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
git commit -m "refactor(scaffold): ChaoFeng + 通用 Place 统一 Box HurtBox/接触伤害区 + sprite 尺寸"
```
---
## Task 5: 向导 CharacterWizardWindow 加默认 Sprite 字段并透传
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs`
- [ ] **Step 1: 加字段**
在字段区(约 `_enemyBodyCollider` 声明附近)加:
```csharp
private Sprite _defaultSprite; // 敌人/Boss 默认外观 Sprite:碰撞体尺寸依据(可留空)
```
- [ ] **Step 2: 小怪 Tab 加 ObjectField**
`RefreshEnemyTabContent` 里的 `colliderField`(EnumField "主体碰撞器类型")之后加:
```csharp
var spriteField = new UnityEditor.UIElements.ObjectField("默认外观 Sprite(碰撞体尺寸依据,可留空)")
{ objectType = typeof(Sprite), allowSceneObjects = false, value = _defaultSprite };
spriteField.RegisterValueChangedCallback(evt => _defaultSprite = evt.newValue as Sprite);
container.Add(spriteField);
```
- [ ] **Step 3: 小怪放置调用透传 sprite**
`PlaceSpecificEnemy(string id, EnemyBodyColliderType bodyCollider)` 改签名加 `Sprite defaultSprite`
```csharp
private static void PlaceSpecificEnemy(string id, SceneObjectPlacerTool.EnemyBodyColliderType bodyCollider, Sprite defaultSprite)
{
switch (id)
{
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(bodyCollider, defaultSprite); break;
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(bodyCollider, defaultSprite); break;
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(bodyCollider, defaultSprite); break;
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(bodyCollider, defaultSprite); break;
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(bodyCollider, defaultSprite); break;
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(bodyCollider, defaultSprite); break;
default:
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}'。");
SceneObjectPlacerTool.PlaceEnemy(bodyCollider, defaultSprite);
break;
}
}
```
并把调用处 `PlaceSpecificEnemy(id, _enemyBodyCollider)` 改为 `PlaceSpecificEnemy(id, _enemyBodyCollider, _defaultSprite)``RefreshEnemyTabContent` 里的 `MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id, _enemyBodyCollider))`)。
- [ ] **Step 4: Boss Tab 加 ObjectField + 透传**
`BuildBossTab` 的 "放置嘲风到场景并绑定 SO" 按钮之前加同款 spriteField(绑 `_defaultSprite`);把该按钮改为 `MakeSceneButton("放置嘲风到场景并绑定 SO", () => SceneObjectPlacerTool.PlaceChaoFeng(SceneObjectPlacerTool.EnemyBodyColliderType.Box, _defaultSprite))`
- [ ] **Step 5: 编译门** → count=0。
- [ ] **Step 6: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs
git commit -m "feat(wizard): 敌人/Boss 加默认外观 Sprite 字段并透传给脚手架(碰撞体尺寸依据)"
```
---
## Task 6: 端到端 Sprite 驱动验证 + 收尾
**Files:** 无(仅 MCP 验证)
- [ ] **Step 1: 用一张真实 Sprite 走一次放置**,验证碰撞体尺寸 = sprite 包围盒
`unity_execute_code`(port 7890)
```csharp
// 取任意一张 sprite 资产
var spriteGuid = UnityEditor.AssetDatabase.FindAssets("t:Sprite").FirstOrDefault();
var sprite = spriteGuid!=null ? UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(UnityEditor.AssetDatabase.GUIDToAssetPath(spriteGuid)) : null;
if(sprite==null) return "no sprite asset found";
var t = System.AppDomain.CurrentDomain.GetAssemblies().SelectMany(a=>{try{return a.GetTypes();}catch{return new System.Type[0];}}).FirstOrDefault(x=>x.Name=="SceneObjectPlacerTool");
var m = t.GetMethods().First(x=>x.Name=="PlaceE002_HuangZhi" && x.GetParameters().Length==2);
var enumType = m.GetParameters()[0].ParameterType;
m.Invoke(null, new object[]{ System.Enum.ToObject(enumType, 0), sprite });
var go = UnityEngine.GameObject.Find("ENM_HuangZhi");
var body = go.GetComponent<UnityEngine.BoxCollider2D>();
var sb=new System.Text.StringBuilder();
sb.AppendLine("sprite.bounds.size="+((UnityEngine.Vector2)sprite.bounds.size).ToString("F3"));
sb.AppendLine("body Box size="+body.size.ToString("F3")+" (期望=sprite.bounds.size)");
sb.AppendLine("HurtBox size="+go.transform.Find("HurtBox").GetComponent<UnityEngine.BoxCollider2D>().size.ToString("F3"));
sb.AppendLine("Contact size="+go.transform.Find("ContactDamageZone").GetComponent<UnityEngine.BoxCollider2D>().size.ToString("F3"));
sb.AppendLine("SpriteRenderer.sprite="+(go.GetComponentInChildren<UnityEngine.SpriteRenderer>()?.sprite?.name ?? "null"));
UnityEngine.Object.DestroyImmediate(go);
return sb.ToString();
```
期望:body/HurtBox/Contact size 三者一致且 = `sprite.bounds.size`SpriteRenderer.sprite = 该 sprite 名。
- [ ] **Step 2: 自检**`BaseGames/Tools/Maintenance/Physics2D Layer Matrix/Check` 期望无新增不符(层未变)。
- [ ] **Step 3: grep 确认无残留 Capsule/Circle HurtBox/接触伤害创建**
```
rg -n "CapsuleCollider2D hurt|CircleCollider2D contact|CircleCollider2D hit" Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
```
期望:0 命中(HurtBox/接触伤害区的 Capsule/Circle 创建都已换成助手;主体的 CreateBodyCollider 里 Capsule/Circle 分支保留属正常)。
- [ ] **Step 4: 更新记忆** `editor_scaffold_tools.md` / `scaffold_tools_required.md`:记录敌人碰撞体现由 sprite 驱动、三者统一 Box+底部对齐、全员有 ContactDamageZone。
---
## Self-Review(作者已核对)
- **Spec 覆盖**:§A 向导 sprite 字段→Task 5;§B 助手→Task 1、各敌人改造→Task 2/3/4(含补全 4 个缺失 ContactDamageZone、E001 contactEnabled=false 特例、伤害源接线);§C 不动 prefab→全程无 prefab 操作;§D 验证→各 Task Step 4 + Task 6。主体默认 Box→CreateBodyCollider 默认分支;底部对齐→助手内 AlignColliderBottomToPivot。
- **占位符**:无 TBD;助手给完整代码;per-enemy 改造给"读现有→套模式+具体 fallback size+接线说明",因每个 PlaceExxx 的能力接线各异,显式要求实现前 Read 全文(属机械变换)。
- **类型一致**`SpriteSizeOr(Sprite,Vector2)→Vector2``SetupHurtAndContactBoxes(GameObject,Vector2,bool,List<string>)→(HurtBox,BodyContactDamage,HitBox)` 全程一致;`EnemyBodyColliderType` 参数名/默认值一致;`PlaceExxx(bodyCollider, defaultSprite=null)` 签名贯穿 Task 2-5。
- **已知风险**:各 PlaceExxx 内的攻击能力自带 HitBox(如 E002 `_attackHitBox`)与本任务的主体 HurtBox/ContactDamageZone 不同,Task 3 已显式提示勿混淆——实现前必须 Read 每个方法全文。