CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủTài liệu framework › 02 — Chiến đấu & đạn (Combat / Projectiles)
Nguồn: 02-combat-projectiles.md · Cập nhật lần cuối: 2026-09-05 08:39 +07 · sha256 nguồn 5d187ad4dc35

02 — Chiến đấu & đạn (Combat / Projectiles)

Nguồn: chưng cất từ restore SOT com.Chodun.CatGunner 1.1.59 (BardCat). Mọi C# ở đây là L1 ANALYSIS — dựng lại clean-room từ ARM64 disassembly, không phải source gốc.


1. Câu hỏi tài liệu này trả lời

Một idle-shooter phải bắn liên tục, không cấp phát, va chạm với hàng nghìn mục tiêu, và hỗ trợ nhiều "loại súng" có ngữ nghĩa va chạm khác nhau — tất cả trong một class đạn duy nhất. Bullet.Update chạy 14 199 lần / 40 giây trong trace runtime; nó là hot path thật.


2. Stable truths

Claim Mức bằng chứng Nguồn / provenance Phạm vi
Bullet là class đạn thật của game (22 method); BulletShooter không bao giờ chạy trong gameplay bình thường — là shooter legacy runtime-verified (negative) [runtime trace: runtime_ranking.json]: Bullet.Update 14 199×, BulletShooter.* Cảnh báo: đừng chọn target theo tên
27 instance Bullet trong scene ⇒ pool cố định, không Instantiate mỗi phát scene-census [index: scene_census.json] (Bullet = 27) Kiến trúc pooling
Enum GunType (TypeDefIndex 8226) có đúng 7 giá trị: SINGLE=0, LASER=1, PIERCE=2, SHOTGUN=3, EXPLOSIVE=4, MISSILE=5, BLAST_SNIPER=6 metadata-proven dump.cs:596519 Mô hình vũ khí
EXPLOSIVE = 4 và MISSILE = 5 — suy ra từ test bit ((int)gunType & ~1) == 4 trong Bullet.Destroy, đã được metadata xác nhận độc lập disasm-proven + metadata-proven Bullet.cs RVA 0x2B0954C; dump.cs:596519 Giá trị enum
Chỉ số vũ khí sống trong Gun_manager.Gun_Info (nested, TypeDefIndex 8227): Name, type, InfVal Damage, AtkSpeed, ShotgunPelletCount, ShotgunSpreadAngle, ExplosionRadius, MissileExplosionRadius, MissileSpeed, MissileElevationMin/Max, BlastSniperExplosionRadius, PenetratingBulletLifetime metadata-proven dump.cs:596533 Schema tuning vũ khí
Bên bắn là Cat, không phải Gun: Cat.TrySpawnBulletEx(pos, dir, GunType, expRadius, lifetime, dmg, dmgStr, isCri) (RVA 0x2AEDFB0), Cat.TryFireShotgun(...) (0x2AEE134), Cat.TryFireMissile(spawnPos, target, spreadAngle, ...) (0x2AEE32C), Cat.ApplySpread(dir, maxAngle) (0x2AEDF20) metadata-proven dump.cs:593002 (class Cat) Đường bắn
Dispatch va chạm nằm trong OnTriggerEnter (RVA 0x2B089C8); 5 method Hit* bị compiler inline vào nó disasm-proven G130 trong gap list (nội bộ); [disasm: Bullet.c] Đọc disasm
Đạn thẳng bay bằng Rigidbody.linearVelocity; chỉ missile được di chuyển thủ công trong Update disasm-proven Bullet.SetupLinear RVA 0x2B07CF8, Bullet.Update RVA 0x2B0816C Chọn mô hình chuyển động
Quỹ đạo missile là parabol y = lerp(y0,y1,t) + (1-t)·t·(arcHeight·4), đỉnh tại t = 0.5 disasm-proven Bullet.Update RVA 0x2B0816C Công thức bay
_msArcHeight = khoảng cách ngang (x,z) × arcHeightMultiplier (mặc định 0.4f) disasm-proven Bullet.SpawnMissile RVA 0x2B07E80 Tỉ lệ cung
Điểm va chạm cho đạn thẳng được tua ngược một bước vật lý: transform.position - rb.linearVelocity * Time.fixedDeltaTime, rồi other.ClosestPoint(...) disasm-proven Bullet.OnTriggerEnter RVA 0x2B089C8 Chống lệch VFX
Chống trúng lặp bằng HashSet<int> _hitIds chứa Collider.GetInstanceID() disasm-proven field @0xD0; dùng trong OnTriggerEnter, HitLaser, HitPierce, Explode Pierce/AoE
AoE = Physics.OverlapSphere(center, explosionRadius, enemyLayer), bỏ qua collider không activeInHierarchy và collider đã có trong _hitIds disasm-proven Bullet.Explode RVA 0x2B08404 Nổ
PIERCE: xuyên tối đa 2 mục tiêu; giết được thì đi tiếp, không giết được và đã _pierceCount >= 2 thì dừng disasm-proven (nhánh có rủi ro đảo, cần runtime-verify) Bullet.HitPierce RVA 0x2B08E70; _evidence/Bullet/README.md Ngữ nghĩa xuyên
LASER: gây damage mỗi enemy đúng 1 lần, sinh FX 1 lần, không bao giờ chết vì trúng enemy; chỉ chết khi trúng non-enemy disasm-proven Bullet.HitLaser RVA 0x2B08D1C Ngữ nghĩa laser
BLAST_SNIPER: cú trúng đầu tiên nổ; mục tiêu sống sót ⇒ dừng, mục tiêu chết ⇒ chuyển sang hành vi pierce disasm-proven Bullet.HitBlastSniper RVA 0x2B09124 Ngữ nghĩa lai
Damage KHÔNG đi thẳng vào Enemy: Bullet.Hit* → Gamemanager.Damage_Collider → Damage_Tag → Enemy.Damaged disasm-proven + runtime-verified _evidence/Gamemanager/combat_damage.cs.evidence; trace: Damage_Collider 207×, Damage_Tag 207×, Enemy.Damaged 202× Kiến trúc pipeline
Phân loại mục tiêu bằng tag: "Enemy" nhận Damage của đạn; "Ore" nhận cố định (InfVal)1 mỗi lần trúng disasm-proven Damage_Tag RVA 0x2A503F0; literal @0x5E8A1E0 / 0x5E91930 Mục tiêu không phải quái
Gamemanager.is_Enemy (static @0x168) được ghi lại sau mỗi lần Damage_Collider disasm-proven RVA 0x2A50048 Trạng thái chia sẻ
Enemy.DamagedEnemy.Die đều có guard if (!Gamemanager.is_gaming) return; disasm-proven (đã sửa bug reconstruct) G132b trong gap list (nội bộ); Enemy.cs Bài học then chốt — xem §3.5
Enemy.Type enum: Tree=0, Tree_SpeedRun=1, BossMonster=2, Monster=3, Ore=4, RaidBoss=5 disasm-proven [reconstructed: Enemy.cs] Phân loại mục tiêu
Chỉ Type.Monster nhận knockback: damageRatio = (float)(Damage / Health_Max)Monster.Apply_Knockback(damageRatio) disasm-proven Enemy.Damaged RVA 0x2AFEF9C Game feel (xem doc 06)
Vòng đời đạn dùng Invoke("Destroy", t) / Invoke("Deactivate", delay), KHÔNG dùng coroutine disasm-proven SpawnEx RVA 0x2B07AD0, DieAndWait RVA 0x2B08858 Vòng đời
Thời gian sống đạn thẳng = min(50 / max(Speed, 0.1), lifetime); Speed mặc định 45f ⇒ ~1.11 s disasm-proven .ctor + SpawnEx Tuning
Scene ghi đè Speed = 70 (ctor 45 chỉ là mặc định — quy tắc G902) ⇒ đạn thẳng sống 50/70 ≈ 0.71 s; _deactivateDelay = 1f scene-YAML + disasm-proven G385 Giá trị ship
Thiết lập trigger đạn ↔ địch (đọc từ scene + binary): Bullet layer 8, SphereCollider trigger r 0.6, Rigidbody không kinematic, interpolate, va chạm DISCRETE; Enemy_Coll layer 7, BoxCollider tĩnh ~20×12×14; EnemyColl không có method (chỉ giữ enemyScript+0x20); Damage_Tag đọc tag của enemyScript (932 Enemy / 1000 Ore / 1 boss Untagged); Bullet.Update không raycast quét — chỉ trigger. Ma trận va chạm SOT: Bullet(8)↔Enemy(7) bật (SOT[8] = 0xFFFFF881, SOT[7] = 0xFFFFF7FF), Cat(6)↔Enemy(7), Bullet(8)↔Monster(9) scene-YAML + disasm-proven + build-measured G385, G383 Cặp trigger hợp lệ
Bullet.OnTriggerEnter (0x2B089C8) đối chiếu từng lệnh với bản dựng lại: cổng is_Die, _hitIds.Contains, ClosestPoint, CompareTag, dispatch 7 nhánh theo gunType (SINGLE = 0) — khớp disasm-proven G385 Loại trừ code
Đạn thẳng đi bằng rb.linearVelocity (SetupLinear 0x2B07CF8: isKinematic = false, đặt vị trí + hướng, velocity = forward × Speed, SetActive, particle); SpawnEx hẹn Invoke("Destroy", min(50/Speed, lifetime)); ResetState (0x2B07C18) bật lại collider; Update thoát ngay trừ khi _isMissilemissile kinematic, dời bằng transform. Bản dựng lại khớp; xuyên hầm khó xảy ra (box ~20×12×14 vs 1.4 u/bước) disasm-proven G391 Loại trừ code
Enemy/Tree/Ore..ctor là tail-call 8 byte vào base ctor ⇒ không có khởi tạo bị bỏ rơi; Enemy.Damaged (0x2AFEF9C) có đúng 2 thoát sớm (is_Die, !is_gaming); Enemy.Spawn đặt is_Die = false; Tree không có Damaged (damage đi Enemy.Damaged → Die → Die_Reload → Tree.Die_Reload) disasm-proven G391 Loại trừ code
Ba cổng "sức mạnh" theo chế độ (G395): Power_Boss_return = Σ mèo active ExpectedDamage_Boss_return() × 100 / (int)(AtkCool_Boss_return() × 100), ToPrecision(5); Power_Monster_return = như trên × hệ số mục tiêu theo loại súng; Power_SpeedRun_return (mỗi phát) = (Damage × (100 − criPct) + CriDmg × criPct) / 100 với criPct = clamp(Math.Round(CriPer × 100) [làm tròn NGÂN HÀNG], 0, 100) disasm-proven G395 Sức mạnh hiển thị theo chế độ
GunTargetMultiplier_return: SINGLE 1 · LASER 7 · PIERCE 5 · SHOTGUN max(pellets, 1) · EXPLOSIVE 5 · MISSILE 5 · BLAST_SNIPER 10khác bảng MultiTargetCount của simulator offline (Laser/Pierce 10, nổ 5, sniper 5): hai bảng cho hai mục đích (hiển thị sức mạnh vs mô phỏng clear) disasm-proven G395; so doc 01 §5.8 luật 8 Hai bảng, đừng gộp
Trong cửa sổ trace, mọi phát bắn đều là HitSingle (207×) ⇒ súng khởi đầu là SINGLE; các nhánh khác chưa được runtime-verify runtime-observed (negative) runtime_ranking.json Giới hạn bằng chứng
Cụm bắn của Cat đã dựng lại (batch B, đợt 4): Attack 0x2AE9624, UpdateHandRotation 0x2AE7184, TryFireMissile 0x2AEE32C, TryFireShotgun 0x2AEE134, ApplyLeftHandTowardGunPivot 0x2AED960, UpdateSpreadState 0x2AE80B0, TrySpawnBulletEx 0x2AEDFB0, CalcBulletDir 0x2AEDDF4, GetFirePosition 0x2AEDD04, CalcBulletSpawnPos 0x2AEE644, ApplySpread 0x2AEDF20, ApplyHandZ 0x2AE64E8 — 12 method, 45 marker RVA disasm-proven G363 Đường bắn hoàn chỉnh
R1 (phủ định, đã quét 12 RVA): không có nhánh weapon_02 / AD_Type nào trong cụm bắn ⇒ không gì route qua IAdService; súng quảng cáo chỉ chạm Gun_manager (doc 10 §4.6) disasm-proven (negative) G363 Ranh giới R1
Pool đạn — hai lượt "cướp ô": Polling_manager.Object_Polling_Smart (0x2AC6900) + Object_struct.Object_Polling_Smart (0x2AC6934): con trỏ xoay vòng, lượt 1 lấy object inactive đầu tiên; không có ⇒ lượt 2 cướp ô Bullet đã chết đầu tiên (huỷ Invoke("Deactivate") đang chờ của nó); chỉ trả null khi mọi viên còn sốngCat.Update hoàn lại cooldown (không mất phát bắn). Hai method này từng là return null — không viên đạn nào có thể spawn mà không một ngoại lệ (R13) disasm-proven G363 Pooling
Spread = bloom: tăng mỗi phát (trừ shotgun), rỉ dần khi không có mục tiêu; mục tiêu mới ⇒ reset spread và cooldown disasm-proven UpdateSpreadState 0x2AE80B0; G363 Cảm giác bắn
Missile trượt bằng tán xạ ĐIỂM CHẠM, không phải hướng bay: điểm nổ được rải trong bán kính range × tan(spread) quanh mục tiêu disasm-proven TryFireMissile; G363 Ngữ nghĩa spread cho đạn dẫn
Nòng súng được chiếu dọc CamForward lên mặt phẳng y = BulletSpawnY = 2 (chiều cao màn hình cố định) ⇒ đạn xuất phát cùng độ cao dù mèo đứng đâu: spawn = pos + CamForward × ((2 − pos.y) / CamForward.y) disasm-proven CalcBulletSpawnPos 0x2AEE644; G363 Hình học iso
VÌ SAO ĐẠN KHÔNG TRÚNG (G424 → G400): phép chiếu trên chia cho CamForward.y. Bản dựng lại có CamForward = Euler(0, −45, 0)·forward (y = 0) thay vì Euler(40, −45, 0)·forward = (−0.5417, **−0.6428**, 0.5417) của binary ⇒ chia cho 0 ⇒ mọi viên đạn spawn ở ±∞, bay ngoài mọi collider, OnTriggerEnter không bao giờ được gọi (khớp 0 dòng trace), rồi hết hạn theo lifetime. Đường này chạy mỗi phátShoot_Random_trans rỗng trên cả 3 mèo trong scene. Một hằng số sai một thành phần qua được mọi guard (compile, singleton, meta, event) và mọi loại trừ (ma trận va chạm, thứ tự script, ctor, rb.velocity) — xem doc 08 §3.6 disasm-proven (Internal_FromEulerRad 0x2AEF350: S0 = 40°, S1 = −45°) + build-measured (batch 20) G400, G405 G424 ĐÃ ĐÓNG: SetupLinear y = 2.000 hữu hạn; 104/104 OnTriggerEnter → Damage_Tag → Enemy.Damaged trong 30 s; cây đổ (fresh TreeCount 29→26→22→14, gần sạch ở 65 s; save thật 141→121→103→85), tờ tiền + hạt lá, thanh máu trên xương rồng

| Tiếng bắn bị chặn bởi hai cờ: Cat_manager.is_Shoot_Mute AND Setting.is_eco | disasm-proven | Attack 0x2AE9624; G363 | Mute kép (doc 04) | | Chuỗi 100 sát thương của súng khởi đầu — xác minh từ số scene: Gun_list[0].Damage 100 × Power_balance[0] 100/100 × Rebirth 100/100 × Skin 100/100 × (Fish 0 + 100)/100 × Relic 100/100 = 100; đường TrySpawnBulletEx → Bullet.SpawnEx → OnTriggerEnter → HitSingle → Damage_Collider đã đầy đủ. Hai bẫy base-100 (Rebirth_manager.Damage_return = 0, Relic.Dmg_Value = 0) từng đưa tích về 0 | disasm-proven + scene-YAML (derived) | G363, G361; R13 | Bài học then chốt |


3. Mô hình hành vi

3.1 Kiến trúc tổng thể — một class đạn, dispatch theo enum

Gun_manager / Cat_manager  (quyết định bắn)
        |
        v
Polling_manager.Object_Polling(Objects_Type.Bullet)   -> lấy Bullet từ pool
        |
        v
Bullet.SpawnEx(...)          hoặc   Bullet.SpawnMissile(...)
   Skin_Reload(gunNum)  -> chọn ParticleSystem theo skin súng
   Mesh_Reload(gunNum)  -> bật Bullet_Mesh tương ứng
   ResetState()         -> xoá _hitIds, _pierceCount, cờ; bật lại collider
   SetupLinear()        -> rb.linearVelocity = forward * Speed
   Invoke("Destroy", t)
        |
        v (va chạm)
Bullet.OnTriggerEnter(other)
   dedup theo _hitIds
   hitPos = other.ClosestPoint(rewound position)
   switch (gunType) -> HitSingle | HitLaser | HitPierce | HitExplosive | HitBlastSniper
        |
        v
Gamemanager.Damage_Collider(coll, Damage, hitPos, Damage_str, is_Cri)
   -> Damage_Tag(coll, Damage)
        -> coll.GetComponent<EnemyColl>().enemyScript   (Enemy)
        -> tag "Enemy" ? Enemy.Damaged(Damage)
         : tag "Ore"   ? Enemy.Damaged((InfVal)1)
         : false
   -> nếu trúng: is_Cri ? Critical_Txt_Spawn(str, pos) : Damage_Txt_Spawn(str, pos)
        |
        v
Enemy.Damaged -> Health_Now -= Damage
              -> (Monster) Apply_Knockback(Damage / Health_Max)
              -> Slider_Reload()
              -> Health_Now <= 0 ? Die() : _bounce.Bounce_Start()
Enemy.Die     -> Gamemanager.Get_Money(Money, transform.position)
              -> Die_Reload()  (dispatch theo Enemy.Type)

Điểm kiến trúc quan trọng: Bullet không bao giờ gọi Enemy.Damaged trực tiếp. Nó chỉ biết Collider. Toàn bộ "collider này là gì, nhận bao nhiêu damage" nằm ở một chỗ duy nhất (Damage_Tag). Nhờ vậy thêm một loại mục tiêu mới (Ore, Tree, Boss) không cần chạm vào code đạn.

3.2 Ngữ nghĩa từng GunType

GunType Handler Ngữ nghĩa
SINGLE HitSingle Damage → FX → chết. Đạn 1 mục tiêu.
SHOTGUN HitSingle Cùng handler với SINGLE. Hiệu ứng "shotgun" nằm ở phía bắn, không ở phía va chạm: Cat.TryFireShotgun bắn Gun_Info.ShotgunPelletCount viên, mỗi viên lệch góc theo Gun_Info.ShotgunSpreadAngle qua Cat.ApplySpread(dir, maxAngle).
LASER HitLaser Trúng enemy: thêm vào _hitIds, damage, SpawnFX_Once, đi tiếp vô hạn. Trúng non-enemy: FX + chết.
PIERCE HitPierce Trúng non-enemy ⇒ chết. Trúng enemy: _pierceCount++; tính trước kill = Damage >= e.Health_Now; damage; SpawnFX_Once; nếu _pierceCount >= 2 && !kill ⇒ chết.
EXPLOSIVE HitExplosive Explode(hitPos) → FX → chết.
MISSILE HitExplosive Như EXPLOSIVE, nhưng bay parabol và cũng nổ khi hết thời gian bay (t >= 1).
BLAST_SNIPER HitBlastSniper Lần trúng đầu: _firstHitDone = true, tính survived, Explode, SpawnFX_Once; survived ⇒ chết, ngược lại tiếp tục. Các lần sau: uỷ quyền cho HitPierce.

Điều đáng học: 7 loại vũ khí, 5 handler, 1 class. Sự khác biệt không nằm ở prefab riêng hay class con — nó nằm ở một switch trên enum + vài field trạng thái (_hitIds, _pierceCount, _firstHitDone, explosionRadius). Đây là lý do pool 27 viên đạn phục vụ được cả game.

3.3 Missile — parabol tường minh

// L1 ANALYSIS — RVA 0x2B0816C
_msTimer += Time.deltaTime;
float t = Mathf.Clamp01(_msTimer / _msDuration);
float x = _msStart.x + (_msEnd.x - _msStart.x) * t;
float z = _msStart.z + (_msEnd.z - _msStart.z) * t;
float linearY = _msStart.y + t * (_msEnd.y - _msStart.y);
float y = linearY + (1f - t) * (t * (_msArcHeight * 4f));   // đỉnh tại t = 0.5
transform.position = new Vector3(x, y, z);
// hướng mũi theo vector di chuyển thực tế
Vector3 move = newPos - oldPos;
if (move.sqrMagnitude > 0.0001f) transform.forward = move.normalized;
LastPos = oldPos;                         // dùng làm điểm gốc tính hitPos
if (t >= 1f) { Explode(pos); SpawnFX(pos); DieAndWait(); }

Hệ số 4f là chuẩn hoá: (1-t)·t đạt cực đại 0.25 tại t = 0.5, nên ×4 khiến _msArcHeight đúng bằng độ cao đỉnh cung thực tế tính từ đường thẳng nối. Đây là cách viết parabol mà tham số có ý nghĩa vật lý trực tiếp — đáng chuẩn hoá vào framework.

Missile là isKinematic = true; đạn thẳng là isKinematic = false. Cùng một class, hai chế độ chuyển động, chọn bằng cờ _isMissile.

3.4 Điểm va chạm "tua ngược"

Vector3 basePoint = _isMissile
    ? LastPos                                                        // vị trí frame trước
    : transform.position - rb.linearVelocity * Time.fixedDeltaTime;  // lùi 1 bước vật lý
Vector3 hitPos = other.ClosestPoint(basePoint);

Với đạn nhanh, OnTriggerEnter được gọi khi đạn đã xuyên vào trong collider, nên transform.position nằm bên trong mục tiêu và VFX nổ ra ở giữa thân quái trông sai. Tua ngược đúng một bước rồi lấy ClosestPoint cho ra điểm trên bề mặt, phía đạn bay tới. Đây là một thủ thuật game-feel rẻ tiền và hiệu quả.

3.5 Bug is_gaming — vì sao nó quan trọng

Trong quá trình dựng lại, Enemy.DamagedEnemy.Die ban đầu thiếu guard if (!Gamemanager.is_gaming) return;. Bug lộ ra khi đối chiếu chéo với Ore.Die_Reload (vốn có guard rõ ràng trong disasm). Đã sửa và ghi vào G132b.

Vì sao đây là bài học framework chứ không chỉ là một lỗi vặt:

  • SOT chỉ có một scene duy nhất (Main.unity, 481 MB) chứa cả gameplay lẫn toàn bộ menu/popup. Quái, đạn và collider luôn tồn tại, kể cả khi người chơi đang mở shop.
  • Không có "unload scene gameplay" để bảo vệ. Thứ duy nhất ngăn combat chạy trong menu là một cờ boolean toàn cục Gamemanager.is_gaming được kiểm tra ở từng entry-point.
  • Thiếu một guard ⇒ quái vẫn ăn damage và vẫn rơi tiền khi người chơi đang ở menu ⇒ lỗi kinh tế im lặng, không crash, cực khó phát hiện qua smoke test.

Framework rút ra: khi kiến trúc là "một scene, mọi thứ luôn sống", cờ trạng thái game phải là một guard bắt buộc ở tầng framework (ví dụ một base class hoặc một IDamageable gate), không phải một dòng if mà mỗi lập trình viên phải nhớ chép vào.

3.6 Ore = 1 damage cố định

if (tag == "Enemy") { e.Damaged(Damage); return true; }
if (tag == "Ore")   { e.Damaged((InfVal)1); return true; }

Ore (1 000 instance trong scene) là mục tiêu đếm số lần đánh, không phải mục tiêu theo sức mạnh: máu 3 (skin 0) hoặc 5 (skin ≠ 0), mỗi lần trúng trừ đúng 1. Thưởng cũng tách khỏi Enemy: Ore.Spawn truyền Money = 0 cho Enemy và giữ _diaValue riêng — 1 dia (skin 0) hoặc 10 dia (skin ≠ 0), cấp khi chết; tham số int skinNum thực chất là boolean (G343). Nghĩa là tốc độ đào phụ thuộc tốc độ bắn, không phụ thuộc damage. Đây là một cần gạt tuning riêng, cố ý tách khỏi chuỗi multiplier damage — một mẫu hay cho tài nguyên phụ trong idle (chống việc một stat làm phẳng mọi thứ).


4. Framework takeaway

  1. Một Projectile duy nhất + enum dispatch, thay vì cây kế thừa. Trạng thái riêng của từng ngữ nghĩa nằm gọn trong vài field; ResetState() phải xoá tất cả chúng khi lấy từ pool. Đây là hợp đồng quan trọng nhất của pooling: một object tái sử dụng phải trở về trạng thái xuất xưởng, không được mang theo _hitIds của lần bắn trước.

  2. Tách "va chạm" khỏi "sát thương". IProjectile -> chỉ biết Collider và điểm va chạm IDamageRouter-> biết Collider nào là gì, và bao nhiêu damage IDamageable -> biết mất máu thì làm gì Thêm loại mục tiêu = sửa router, không sửa đạn.

  3. Dedup theo GetInstanceID() trong một HashSet là đủ và rẻ cho pierce + AoE. Dùng chung một tập cho cả OnTriggerEnterOverlapSphere khiến vụ nổ không đánh trùng mục tiêu vừa bị đạn xuyên.

  4. Chuẩn hoá điểm va chạm bằng rewind + ClosestPoint. Framework nên cung cấp sẵn ResolveHitPoint(collider, previousPosition).

  5. Parabol tham số hoá theo độ cao đỉnh ((1-t)·t·h·4), vì designer nghĩ bằng "cung cao bao nhiêu", không nghĩ bằng hệ số bậc hai.

  6. Guard trạng thái game phải ở tầng framework. Trong kiến trúc một-scene, đây là lớp phòng thủ duy nhất giữa gameplay và menu.

  7. Đừng suy ra hot path từ tên class. BulletShooter nghe như trái tim của game và có 0 lần gọi; Bullet chạy 14 199 lần. Runtime ranking là oracle, tên là phỏng đoán.


5. Negative evidence & UNKNOWN

Mục Trạng thái
Nhánh PIERCE / BLAST_SNIPER ("giết ⇒ đi tiếp, sống ⇒ dừng") Chưa runtime-verify. _evidence/Bullet/README.md đánh dấu đây là rủi ro đảo nhánh tinh vi nhất. Cách khoá: trang bị súng pierce trên thiết bị, hook Damage_Collider + _pierceCount.
LASER, EXPLOSIVE, MISSILE, BLAST_SNIPER, SHOTGUN Chưa quan sát chạy. Cửa sổ trace 40 s chỉ có súng SINGLE (HitSingle 207×).
Bullet.cs (22 method) Chưa compile-test, chưa runtime-verify. Đang ở _evidence/, không nằm trong tập shipped (G124).
Giá trị số cụ thể của Gun_Info (Damage, AtkSpeed, ExplosionRadius… của từng khẩu) UNKNOWNschema đã biết đầy đủ (metadata), nhưng dữ liệu nằm trong scene Main.unity + Gun_manager.Balance_Reload() (chưa dựng lại) và có thể bị ghi đè bởi bảng từ xa (§6).
enemyLayer / OreLayer cụ thể UNKNOWN — là LayerMask serialize trên Cat, chưa trích từ scene YAML.
Damage_Collider_list (bản batch cho AoE) Chữ ký đã biết (RVA 0x2A4FF04 / 0x2A5017C), thân là vòng for hiển nhiên nhưng chưa dựng lại. Explode dùng bản per-collider.
Không quan sát thấy BulletShooter không hề chạy — đây là negative evidence có giá trị, đã kiểm bằng hook native offset trên toàn bộ 1 279 method mục tiêu.
FXThrottle.CanSpawn Có tồn tại và được Bullet.SpawnFX gọi, nhưng thân method chưa dựng lại — xem doc 03.
Bắn đã chạy trên thiết bị (batch 15): súng lục bắn liên tục, tracer chạm mục tiêu — nhưng trên save đã nạp không quái nào mất máu / không tiền / không Game_Win (120 s) MỞ (G424). G423 (save mới 0 damage) đã đóngRelic_manager.Start/Skin_manager.Start (G374). Probe batch 16 (G381): is_gaming = true 32/32, đạn sống, mục tiêu có, không enemy nào mất máu. Batch 17 (G386): ma trận va chạm layer đã sửa về SOT — vẫn damaged = 0 ở 32/32 mẫu ⇒ cả hai hướng cài đặt đã loại trừ; code phía trigger/dispatch cũng khớp từng lệnh (G385). ĐÃ ĐÓNG (G405, batch 20): sau khi sửa CamForward, 104/104 va chạm đi trọn chuỗi OnTriggerEnter → Damage_Tag → Enemy.Damaged trong 30 s, cây đổ, tiền rơi — game chơi được. Chuỗi sát thương giờ xác minh trên thiết bị (qua số cây đổ), không chỉ từ số.

6. Raw evidence

  • [evidence codegen: README.md], Bullet.cs} — dựng lại đủ 22 method
  • [evidence codegen: combat_damage.cs.evidence]Damage_Collider × 2 + Damage_Tag
  • [disasm: Bullet.asm], Bullet_Update.c, Bullet_Update.decompiled.c}
  • [disasm: Bullet.c], Bullet_fixed.c, Enemy.c}
  • [reconstructed: Enemy.cs], EnemyColl.cs, Bullet_Mesh.cs, Gun.cs, BulletShooter.cs}
  • [evidence codegen: rule_statics.cs.evidence] — enum GunType đầy đủ
  • [runtime trace: runtime_ranking.json] — 505 method đã chạy, kèm số lần
  • Gaps: G124, G130, G132b, G133, G134 trong gap list (nội bộ)