CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủTài liệu framework › 05 — Nhân vật, Pet & Kẻ địch
Nguồn: 05-characters-pets-enemies.md · Cập nhật lần cuối: 2026-09-06 22:42 +07 · sha256 nguồn 171917032c69

05 — Nhân vật, Pet & Kẻ địch

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


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

Màn hình idle phải đông và sống: một đàn mèo bám theo người chơi, một bầy pet lượn quanh, hàng nghìn mục tiêu chờ bị bắn. Tất cả phải tự di chuyển mà không giẫm lên nhau, và phải chạy 60 fps trên điện thoại. Đây là mô tả cách CatGunner làm điều đó.

Hai class flocking (Cat_manager, Pet_Ingame_manager) nằm trong nhóm chạy nhiều nhất toàn game: Pet_Ingame_manager 20 038 lời gọi / 40 giây, Cat_manager 9 577.


2. Stable truths

2.1 Mèo (nhân vật người chơi + đồng đội AI)

Claim Mức bằng chứng Nguồn / provenance
Cat_manager.Cat_list[0]mèo người chơi, phần còn lại là AI follower — ghi rõ trong tooltip tiếng Hàn của chính source: "0번 = 메인 플레이어 / 나머지 = AI 팔로워" metadata-proven dump.cs:593380 ([Tooltip] attribute)
Cat_manager chỉ có 3 tham số đội hình: FormationRadius @0x28, SeparationRadius @0x2C, SeparationStrength @0x30 metadata-proven dump.cs:593380
Cat_manager có 11 method: Awake, Start, Update, ComputeSeparation, SetupFormation, AddFollower, RemoveFollower, Balance_Reload, Warp, Skin_Reload, .ctor metadata-proven dump.cs:593380
Runtime: Cat_manager.Update 4 776×ComputeSeparation 4 776× trong ~40 s ⇒ separation chạy mỗi frame runtime-observed [runtime trace: runtime_ranking.json]
Cat_manager.is_Shoot_Mute là static bool @0x0 (dùng bởi Mute_Panel, doc 04) metadata-proven dump.cs:593380
Class Cat (TypeDefIndex 8154) là class lớn nhất của gameplay: ~100 field, 50+ method, có máy trạng thái AI riêng (Cat.AIState) metadata-proven dump.cs:593002
Cat có hệ spread (độ tản đạn) động: SpreadAngleMin, SpreadAngleMax, [Range(0.1,0.6)] SpreadGrowthRate, SpreadDecayMultiplier, _spreadAngle; tăng mỗi phát bắn, giảm khi ngừng bắn metadata-proven dump.cs:593002; UpdateSpreadState RVA 0x2AE80B0
Catauto-move riêng cho từng loại nội dung: HandleAutoMove, FindNearestTreeByGrid, FindNearestTreeSpeedRun, FindNearestOreMine, FindBossTarget, OverlapSphereNearestOre metadata-proven dump.cs:593002
AI follower dùng breadcrumb (vệt đường của leader): static readonly List<Vector3> Breadcrumbs, RecordDist = 0.5, MaxCrumbs = 100, CrumbLookahead = 5 metadata-proven dump.cs:593002
AI follower cũng lưu lịch sử vị trí có độ trễ: Queue<Cat.PosRecord> _leaderHistory, HistoryDuration = 2 giây, GetLaggedLeaderPos() metadata-proven dump.cs:593002
AI có chống rung trạng thái: [Range(0.05,0.3)] HysteresisMargin, [Range(0.1,1)] MinStateDuration, _stateTimer, EvaluateDesiredState(leader, distToLeader) metadata-proven dump.cs:593002
AI có làm mượt vận tốc: [Range(2,20)] VelocitySmoothSpeed, _smoothVelocity, DecelerationRange, MinSpeedRatio metadata-proven dump.cs:593002
AI có fidget (cựa quậy khi đứng yên): AIFidget(), _fidgetTimer, _fidgetInterval, _fidgetOffset metadata-proven dump.cs:593002
Hằng số camera/hình học: CameraYAngle = -45, CamPitchSin = 0.57735026 (= 1/√3, tức góc nghiêng 30°), HandBasicAngle = 130, BulletSpawnY = 2, MonsterMoveRadius = 14 metadata-proven dump.cs:593002
Cat bắn qua 3 đường riêng: TrySpawnBulletEx, TryFireShotgun, TryFireMissile, cộng ApplySpread(dir, maxAngle) metadata-proven dump.cs:593002 (RVA 0x2AEDFB0 / 0x2AEE134 / 0x2AEE32C / 0x2AEDF20)
Cat.CalculateAtkCoolMax(baseAtkSpeed, upgradeValue_Disable, isMainStage, isRaid = false) tính tốc độ đánh; có bản riêng cho speed-run và boss metadata-proven dump.cs:593002 (RVA 0x2AE5CB8)
Runtime: Cat_manager.Start , Balance_Reload — chỉ chạy khi cân bằng lại, không phải mỗi frame runtime-observed [runtime trace: runtime_ranking.json]
Đợt 3 — 21 method đầu của Cat đã dựng lại (batch A, mọi RVA đều start_ea == RVA trên IDB đã tách biên): .ctor, .cctor, Awake, Start, Skin_Reload, SpreadMaxAngle, NearObj, Balance_Reload, CalculateAtkCoolMax, AtkCool_*, Update, HandleRaycastTimer, ClearNearObj, ResolveAttackTarget, IsValidTarget, IsOnScreen, UpdateTargetIndicator, FindNearestEnemyByRaycast, FindNearestTreeByGrid, FaceTowardEnemy, UpdateFaceRotation. Tổng Cat = 42 160 B / 67 method, 5 batch A–E; OnDrawGizmosSelected (2 088 B, editor-only) cố ý không dựng disasm-proven G352, G347, G344
Cat..ctor (0x2AEF09C) mang 34 khởi tạo field bị Cpp2IL bỏ rơi: moveSpeed = 5, Attack_Length = 5, Raycast_Cool_Max = 0.1, RayCount = 36, AutoOreSearchRadius = 25 (×3 fallback), OreOverlapBufferSize = 64, AutoNearbyRadius = 8, cùng các mảng/list và một bool@0x1F8 = true disasm-proven G344, G352
Cat..cctor (0x2AEF234): BoxHalfExtents (0.3, 0.4, 0.3), LeftAngle (40, −45, 0), RightAngle (−40, 135, 0), CamForward = Euler(40, −45, 0) · forward = (−0.5417, −0.6428, 0.5417) (đính chính đợt 10 — bản trước ghi Euler(0, −45, 0), thiếu pitch 40°: Internal_FromEulerRad tại 0x2AEF350 nạp S0 = 0.6981317 rad = 40°, S1 = −0.7853982 = −45°), CamCorrection = Euler(0, 45, 0), MonsterMapCenter (−100, 0, 0), ScreenSpeedK = 1.2. Mọi static khác re-derive: đúng disasm-proven (re-derived) G352, G400
Cat.Awake: if (!is_AI) instance = this — singleton Cat.instance chỉ là mèo người chơi; follower AI không ghi đè disasm-proven G352
SpreadMaxAngleánh xạ log theo tốc độ đánh: log(AtkCool_Max / 0.05) / log(40) (chuẩn hoá về dải cool 0.05..2 s) disasm-proven G352
Cat.Balance_Reload rẽ nhánh theo chế độ (UI_manager.currentType): Mine = Damage phẳng 1, CriPer = 0; Boss / Monster / Raid = damage súng thô, không nhân upgrade vĩnh viễn, riêng Raid bỏ cả Rebirth; Main nhân đôi moveSpeed khi is_Clear_Stage disasm-proven G352
CalculateAtkCoolMax: −30 mỗi stage (nhanh dần theo tiến độ); IsOnScreen mở (trả true) khi không có camera; FindNearestTreeByGrid cắt tỉa bằng dải Z của lưới (lower-bound), cây boss cạnh tranh ở Level 4 disasm-proven G352
Cat_manager..ctor (0x2AF0024): warpOffsets = {(−6, 0, −6.4), (6, 0, −6.4)} (thiếu ⇒ NRE Warp trên thiết bị), FormationRadius = 10, SeparationRadius = 1.8, SeparationStrength = 3. Lưu ý scene serialize FormationRadius = 2.5 (doc 11 §1.1) ⇒ giá trị scene thắng; ctor chỉ là mặc định disasm-proven + scene-YAML G306; doc 11 §1.1
Pet..ctor (0x2B15E1C): _currentAnim = string.Empty; Pet_manager..ctor (0x2B1940C): AutoMerge_Degree = 6 disasm-proven G306
Batch D — AI follower (13 method, đợt 5): HandleAI 0x2AE9D18, HandleAIMonster 0x2AEA00C, EvaluateDesiredState 0x2AEBC48, AIFollow/Chase/AttackStay/Reposition/Assist/Fidget (0x2AEBEE80x2AECE2C), ApplySeparationDrift 0x2AECC08, GetApproachPosition 0x2AECA94, RecordLeaderHistory 0x2AE6A48, GetLaggedLeaderPos 0x2AEB9EC. R13: không có hệ số base-100 trong cụm này disasm-proven G364
Follow là một VỆT (trail), không phải đuổi theo điểm: lịch sử vị trí leader là static dùng chung, cắt còn 2 s; mỗi follower đọc ở độ trễ riêng _lagSeconds ∈ [0.15, 0.6] s, rút trong Start bằng Random.InitState(GetInstanceID()) (tất định theo instance) disasm-proven G364
Hai cơ chế chống rung độc lập: EvaluateDesiredState nới băng của trạng thái đang giữ thêm HysteresisMargin (hệ số +1), HandleAI ép MinStateDuration disasm-proven G364
AIAssist bị xíchFollowMaxDist × 0.85; HandleAIMonster không có leader — mỗi mèo nhận một trạm trên vành đấu trường bán kính 14 disasm-proven G364
Vì sao save mới chỉ thấy MỘT mèo (đúng): scene có Cat (is_AI 0, active) + Cat (1)/(2) (is_AI 1, inactive); Cat_manager.Balance_Reload bật một mèo cho mỗi súng ĐANG TRANG BỊ; Selected_items serialize slot0 = 0, còn lại -1. Xem follower cần save có ≥ 2 súng trang bị (chơi thật — không được đúc) disasm-proven + scene-YAML G364
Batch E — Cat HOÀN TẤT (65 marker RVA, 0 stub trừ gizmo editor-only): FindBossTarget 0x2AEADE4 (không tìm kiếm — một mục tiêu duy nhất); FindNearestOreMine 0x2AEACC0 mở rộng dần: bán kính → bán kính × hệ số fallback → quét toàn Ore_list; OverlapSphereNearestOre 0x2AEB6A8 non-alloc vào buffer 64 ô cấp trong ctor; FindNearestTreeSpeedRun 0x2AEAA88; Warp 0x2AEEF1C dời cả rigidbody (không chỉ transform); ExpectedDamage_Boss_return 0x2AE5E5C bỏ Power_return, trộn thường/chí mạng theo tỉ lệ crit = giá trị kỳ vọng mỗi phát disasm-proven G369
Bẫy coroutine (G369/G542): Bounce_Position.Bounce_Start (giật lùi khi bắn) không "cosmetic": Coroutine_NoRepeat là stub return nullCat.Attack gọi Bounce_Start mỗi phát ⇒ ship setter mà không ship coroutine là giết đường bắn. Cả hai đã dựng (MoveNext 0x2A69A64, bản localPosition của Bounce_Custom) disasm-proven G369

2.2 Pet (bầy boids)

Claim Mức bằng chứng Nguồn / provenance
Pet_Ingame_manager là class chạy nhiều thứ hai toàn game: 20 038 lời gọi / 40 s, 11 method runtime-observed [runtime trace: runtime_ranking.json]
Vòng lặp mỗi bước vật lý: FixedUpdate 4 006×GetMainCat 4 007×, ComputePetSeparation 4 006×, ComputeCatAvoidance 4 006×, MovePets 4 006× runtime-observed như trên
GetMainCat() = Cat_manager.instance?.Cat_list != null && Length > 0 ? Cat_list[0] : null disasm-proven [evidence codegen: README.md]
Tốc độ mèo được làm mượt hàm mũ với hệ số 0.12: inst = Distance(pos, _prevMainCatPos) / Time.fixedDeltaTime; _smoothedCatSpeed += (inst - _smoothedCatSpeed) * 0.12f; disasm-proven Pet_Ingame_manager.FixedUpdate; _evidence/Pet_Ingame_manager/README.md
Tham số bầy (offset đã xác minh): FormationRadius @0x28, FormationSpreadDeg @0x2C, StartMoveDistance @0x30, StopMoveDistance @0x34, MinSpeed @0x38, MaxSpeed @0x3C, FullSpeedDistance @0x40, CatSpeedMultiplier @0x44, PetSeparationRadius @0x48, PetSeparationStrength @0x4C, CatAvoidRadius @0x50, CatAvoidStrength @0x54 metadata-proven _evidence/Pet_Ingame_manager/README.md; dump.cs
Pet (TypeDefIndex 8251) giữ 3 vector lực [HideInInspector]: SeparationForce @0x40, CatAvoidanceForce @0x4C, FormationOffset @0x58, cùng IsMoving @0x64 và _currentAnim @0x68 metadata-proven dump.cs:597811
Pet._animSkeletonAnimation (Spine), không phải Animator metadata-proven dump.cs:597811
Pet có 5 method: Reload(int SkinNum), Warp(Vector3), SetAnimation(string), SetFacing(Vector3 moveDir), .ctor metadata-proven dump.cs:597811
Spine runtime = 4.1.24, định dạng JSON, 13 skeleton + 13 atlas, đường ảnh gốc ./Pet/ asset-census [report: asset_lane.md] §0
13 pet: Acorn, Bee, BlackCat, Chicken, Dove, Frog, Ghost, Griffin, Pigeon, PillowGhost, PinkRabbit, Slime, Slime2 — tất cả "spine": "4.1.24", không có ngoại lệ asset-census như trên
Tên dự án nội bộ đọc được từ header skeleton: BardCat asset-census [report: asset_lane.md] §0

2.3 Kẻ địch

Claim Mức bằng chứng Nguồn / provenance
Enemy.Type enum 6 giá trị: Tree=0, Tree_SpeedRun=1, BossMonster=2, Monster=3, Ore=4, RaidBoss=5 disasm-proven [reconstructed: Enemy.cs]
Enemythành phần chung, còn hành vi riêng nằm ở component anh em, dispatch qua Die_Reload() theo _type disasm-proven Enemy.Die_Reload RVA 0x2AFF354
Số instance trong scene: Enemy 1 933 · EnemyColl 1 933 · Enemy_Slider 1 931 · SpriteFill 1 931 · Ore 1 000 · Tree 501 · Monster 240 · Tree_SpeedRun 190 scene-census [index: scene_census.json]
Toàn bộ kẻ địch đã tồn tại sẵn trong scene (không Instantiate lúc chạy) — .ctor của chúng chạy đúng bằng số instance ngay lúc load scene-census + runtime-observed Enemy..ctor 1 933×, Ore..ctor 1 000×, Tree..ctor 501×
Monster hằng số: NormalSpeed = 1.2, BigSpeed = 0.9, BarrierRadius = 20, ReferenceSpawnDist = 65, MapCenter static metadata-proven dump.cs:594503
Monster dùng MaterialPropertyBlock với 3 property id static: ID_TimeOffset, ID_AnimSpeed, ID_ScaleAmount — animation chạy trên shader, không phải Animator metadata-proven dump.cs:594503
Monster.Spawn(InfVal Health, Vector3 Pos, Sprite SkinSprite, bool is_Big) + hai kích thước Size_Big / Size_Small metadata-proven dump.cs:594503
Monster.Apply_Knockback(float damageRatio) khởi động Knockback_Coroutine(damageRatio), lưu trong _knockbackCoroutine để huỷ chồng metadata-proven dump.cs:594503 (RVA 0x2AF7D70 / 0x2AF7DE8)
Chỉ Enemy.Type.Monster nhận knockback; tỉ lệ = (float)(Damage / Health_Max) disasm-proven Enemy.Damaged RVA 0x2AFEF9C
Boss đi lang thang bằng coroutine: WanderRoutine, MoveToPoint, PickWanderPoint, RandomPointInMap, PathCrossesCat(origin, target, catPos, radius); MapRadius = 60 metadata-proven dump.cs:593531
Boss né đường đi của mèo: catAvoidRadius + maxPointRetries — chọn lại điểm đến nếu đường cắt qua mèo metadata-proven dump.cs:593531
Raid_Boss tối giản: chỉ Spawn(Health, SkinNum), Reload(Health), Die_Reload(), Game_End() metadata-proven dump.cs:595047
Trạng thái wave nằm ở Tree_manager: TreeCount_Now @0x90, TreeCount_Max @0x94, GameTime_Now @0x98, GameTime_Max @0x9C metadata-proven dump.cs:602135
Tiến độ stage: Stage_Now @0x28, Level_Now @0x2C, Stage_Value_Max @0x30, cùng StageAdvance(ref int stage, ref int level) metadata-proven dump.cs:602135
Máu/tiền theo stage được tính trước 5 000 stage: const int PrecomputedStageCount = 5000, cộng Dictionary<int,InfVal> _healthCache / _moneyCachestatic InfVal PowOfThree(int exp) metadata-proven dump.cs:602135 (GetStageHealth RVA 0x2B4111C, GetStageMoney 0x2B431B8)
Cây đổ giảm TreeCount_Now, cập nhật gameSlider theo tỉ lệ (Max - Now) / Max, và gọi Tree_manager.Game_Win() khi TreeCount_Now <= 0logic này nằm trong Tree_manager.Tree_Destroyed (0x2B439BC), được Tree.Die_Reload (0x2B43904) gọi; bản trước gán cả khối cho Die_Reload vì IDA gộp hai hàm (G346) disasm-proven Tree_manager.Tree_Destroyed RVA 0x2B439BC; G346
Ore là mục tiêu đếm nhát đánh: máu 3 (skin 0) hoặc 5 (skin ≠ 0), mỗi nhát trừ đúng (InfVal)1 disasm-proven Ore.Spawn RVA 0x2AF70B8; Damage_Tag RVA 0x2A503F0
Tham số int skinNum của Ore.Spawn được dùng như BOOLEAN: khác 0 ⇒ 5 HP / 10 dia, bằng 0 ⇒ 3 HP / 1 dia. Ore không trả Money qua Enemy (đối số 0) — thưởng là _diaValue, cấp riêng khi chết. Chữ ký int mời gọi coi nó là chỉ số skin và âm thầm phá thưởng quặng disasm-proven G343
Tree.Spawn (0x2B437FC) = enemy.Spawn(Health, Money) + localPosition + sprite. Đặt cây theo đường cong Morton (Z-order), KHÔNG xáo trộn ngẫu nhiên: Tree_Grid.Spawn (0x2B43D28) sắp chỉ số ô bằng comparer <Spawn>b__3_0 (0x2B44344) so Morton(i % 10, i / 10) với Morton = Spread(x) \| (Spread(z) << 1) (0x2B441EC, Spread 0x2B44254 = đan bit); rồi lấy TreeCount ô cách đều trên thứ tự đó (acc += total; index = acc / want). Kết quả: cây phủ đều toàn lưới, tất định; ngẫu nhiên duy nhất là jitter ±randomRange từng cây. Lưới 10 cột: x bước 5 trong hàng, mỗi hàng x −50, z +5 disasm-proven G343
Tree_manager..ctor (0x2B45F74): _healthCache / _moneyCache = new Dictionary<int, InfVal>() (thiếu ⇒ GetStageHealth/Money NRE ngay stage đầu ngoài bảng serialize), _sentWaveRatioKeys = new HashSet<string>(), _trackingStageValue = −1, GameTime_Max = 180f (để 0 ⇒ Game_Cor gọi Game_Fail ngay frame đầu — lỗi im lặng), CatSpawnPos = (−100, 0, 0) disasm-proven G306
Đồng hồ màn đo được trên thiết bị: Time_Fill (SlicedFilledImage) mép phải 713 px (99.03 %) → 387 px (53.75 %) sau 81.5 s → 23 px (3.19 %) sau 172.4 s ⇒ 4.00 px/s, chu kỳ 180.0 / 179.9 s (sai 0.06 %); thanh đếm xuống (remaining / 180) build-measured (device) G351
Tree.Die_Reload (0x2B43904) chỉ có 184 B = 2 dòng: Particle_Polling.instance.Spawn(MoneySpawn_trans.position, is_Boss ? 15 : 5); Tree_manager.instance.Tree_Destroyed();. IDA từng gộp nó với Tree_manager.Tree_Destroyed (0x2B439BC, 864 B) qua tail call B + helper ném null noreturn rơi thẳng vào prologue kế — mọi logic wave bên dưới thuộc Tree_Destroyed disasm-proven (sau tách biên) G346
Tree_manager.Tree_Destroyed: giảm TreeCount_Now, cập nhật slider, phễu analytics SendLog_WaveRatio chỉ khi Stage_Now == 0 && Level_Now >= Stage_Value_Max (lần đầu chơi stage 1), mốc 10 % (decile), khử trùng bằng khoá "{stage}_{level}_{percent}"; TreeCount_Now <= 0 ⇒ Game_Win(); MissionValue_Increase(TreeCount) disasm-proven G346
Game_Winprivate (đúng như dump.cs khai báo) — G190 từng mở rộng thành internal vì tưởng Tree.Die_Reload gọi xuyên class; đã rút lại khi tách biên: người gọi là Tree_Destroyed cùng class disasm-proven (đính chính) G346
Tree_Grid.degreeRates = float[5,4] (.cctor 0x2B44288, blob metadata 0x92ABF8, SHA MATCH), hàng-chính: {80, 20, 0, 0} / {50, 45, 5, 0} / {20, 55, 25, 0} / {5, 35, 60, 0} / {0, 15, 85, 0} — mỗi hàng = 100 % (trọng số roulette), 4 cột = 4 bậc cây; hàng = Level_Now (0..4) theo Degree_Random_return(Level) (0x2B440D8, doc 11 §5.2). Cùng blob khởi tạo OfflineProgressSimulator.DegreeRates. Đóng G199 metadata-proven (blob) + disasm-proven (consumer) G353, G701; doc 11 §5.2
Monster_manager (.cctor 0x2AFB518): MonsterCountPerLevel = int[5] {90, 120, 160, 200, 240}, SpawnIntervalPerLevel = float[5] {0.35, 0.2, 0.17, 0.12, 0.08} (giây). 5 phần tử gợi ý 5 level/stage nhưng consumer chưa đọc ⇒ chỉ số hàng UNKNOWN metadata-proven (blob SHA MATCH) G353 (TABLE.md hàng 9-10)
Mine_manager..ctor (đợt 8): GameTime_Max = 60f (màn Mine 60 s, khác 180 s của Main), orePerFrame = 100, CatSpawnPos (−100, 0, 0), outerRadius 20, innerRadius 8, center 0, noiseScale 0.15, noiseThreshold 0.45 (lưu ý scene serialize 0.085 / 0.517 — doc 11 §6.1 — với field serialize thì scene thắng, G902) disasm-proven + scene-YAML G385
Bảng máu/tiền theo stage — hàm sinh đã đọc (G395): Tree_manager.Balance_Reload xây hai InfVal[5000] với seed 250 (máu) và 20 (tiền), mỗi stage ×3 rồi cắt về 2 chữ số có nghĩa, xoá dictionary memo. Mảng scene khớp quy tắc này: máu 250, 750, 2200, 6600, 19000, 57000, 170000, 510000… (2250→2200, 19800→19000, 171000→170000), tiền 20, 60, 180, 540, 1600, 4800, 14000, 42000…. (Balance_Reload vẫn là dead code lúc chạy — mảng scene là nguồn sự thật; công thức giải thích vì sao mảng trông như thế.) SpeeRun_Stage_Power_return = GetStageHealth(Stage) × 5 (lỗi chính tả là của SOT) disasm-proven + scene-YAML (đối chiếu 8 mục đầu) G395; đóng UNKNOWN §5

2.4 Camera, khung nhìn và "vòng chơi" đo trên thiết bị (đợt 4)

Claim Mức bằng chứng Nguồn / provenance
Camera orthographic, size = 40, góc isometric yaw −45° / pitch 35.26° (= atan(1/√2), khớp CamPitchSin 0.57735), Camera_manager.Follow → Cat disasm-proven + scene-YAML G354
Nửa bề rộng nhìn thấy trên 1080×2400 = size × (1080/2400) = 40 × 0.45 = 18 đơn vị thế giới derived (đã đối chiếu ảnh) G354
Lưới cây grid[0]: x −122.6..−76.5, z 27.5..73.6, tâm (−100, 0, 50.5); CatSpawnPos (−100, 0, 0) ⇒ tâm khối chiếu lên màn hình ở x = +35.6 ⇒ lúc spawn chỉ 5/30 cây chọn nằm trong khung (góc trên phải). Trước batch 10 cả 500 cây scene đều active (15 trong khung = "bóng cây" từng thấy) derived + build-measured G354
Tree_List (−22.5, 0, −22.5), Tree_Grid.Start = RET trần (đúng), 40 Tree_sprites resolve, Tutorial_manager không chạm root map disasm-proven + scene-YAML G354
Vòng chơi đo được (batch 12/13, fresh save): round bắt đầu t = 8.00 s sau khi launch; timer 180 s (4.000 px/s); hết giờ ⇒ round tự bắt đầu lại và RE-ROLL bố cục (Morton + jitter + bốc bậc mới); canopy tròn (bậc cao) xuất hiện sau khi Degree_Random_return được dựng (trước đó mọi cây bậc 0); splash #583F21 = SOT build-measured (device) G358, G362
Tree_Grid.Degree_Random_return (0x2B440D8) từng là return 0 ⇒ mọi cây bậc 0; nay = roulette có trọng số trên degreeRates[Level, i]. Chốt ngữ nghĩa: hàng = level wave 0..4, cột = bậc cây 0..3; bậc 3 không bao giờ được bốc — dành cho cây boss (Tree_Batch phần tử [3]) disasm-proven G354

3. Mô hình hành vi

3.1 Đàn mèo — leader + follower, tách ra hai tầng

Cat_manager (11 method, 1 instance)          <- tầng ĐÀN
    Cat_list[0]      = mèo người chơi
    Cat_list[1..n]   = follower AI
    Update()         -> ComputeSeparation()   (mỗi frame, 4 776x/40s)
    SetupFormation() -> tính FormationOffset cho từng follower quanh leader
    AddFollower / RemoveFollower / Warp / Skin_Reload / Balance_Reload

Cat (~100 field, ~50 method, n instance)     <- tầng CÁ THỂ
    máy trạng thái AIState: Follow / Chase / AttackStay / Reposition / Assist / Fidget
    hệ ngắm: FindNearestEnemyByRaycast (RayCount 8..72) -> ResolveAttackTarget -> Attack
    hệ bắn:  ApplySpread -> TrySpawnBulletEx | TryFireShotgun | TryFireMissile
    hệ tay/mặt: UpdateHandRotation / UpdateFaceRotation / FaceTowardEnemy

Sự phân chia này là điểm học chính: manager chỉ lo quan hệ giữa các cá thể (đội hình, đẩy nhau), cá thể lo hành vi của chính nó (ngắm, bắn, đi). Không có "AI trung tâm" điều khiển từng con.

Chống rung trạng thái là chi tiết chuyên nghiệp đáng chép lại:

HysteresisMargin  [Range(0.05, 0.3)]   ngưỡng đổi trạng thái có vùng chết
MinStateDuration  [Range(0.1, 1.0)]    thời gian tối thiểu ở một trạng thái
_stateTimer                            đếm ngược

Không có hai thứ này, một follower đứng đúng ranh giới FollowIdealDist sẽ nhấp nháy giữa FollowAttackStay mỗi frame. Tên field tiếng Hàn trong code (버벅거림 방지 = "chống giật") cho thấy đây là bản vá cho một bug thật.

Breadcrumb + lịch sử có độ trễ giải bài toán "follower đi xuyên tường":

static List<Vector3> Breadcrumbs      // leader thả vệt mỗi RecordDist = 0.5 đơn vị
const int MaxCrumbs      = 100        // giữ 100 điểm gần nhất
const int CrumbLookahead = 5          // nhắm tới điểm thứ 5 phía trước, không phải điểm ngay sau
Queue<PosRecord> _leaderHistory       // + HistoryDuration = 2 s
GetLaggedLeaderPos()                  // vị trí leader cách đây X giây

Follower đi theo đường mà leader đã thực sự đi qua, không đi theo đường thẳng tới leader. Đó là lý do đàn mèo trông như một đoàn rắn chứ không như một đám xúm lại. CrumbLookahead = 5 cho phép cắt góc nhẹ để không trông máy móc.

Fidget (AIFidget, _fidgetTimer, _fidgetInterval, _fidgetOffset) là thứ biến một đàn đứng yên từ "tượng" thành "sinh vật": khi không có việc gì, mỗi con dịch một chút theo chu kỳ ngẫu nhiên.

3.1b Auto-move — mèo tự đi tới mục tiêu (batch C, đợt 4)

12 method (HandleAutoMove 0x2AE8278 4 404 B, HandleMovement 0x2AE6BF0, SmoothMoveTowardWithAvoidance 0x2AEA4A4, ClampToMonsterRadius 0x2AE93AC, RecordBreadcrumb 0x2AE67F0, GetBreadcrumbTarget 0x2AED0C4, ScreenSpeedMultiplier 0x2AE5020, …) — disasm-proven, dự đoán trước khi build và khớp trên thiết bị (G356 → G358).

HandleAutoMove:                                        // mode-aware
    Main      : mục tiêu = cây gần nhất theo lưới; đi tới, DỪNG CÁCH 8 đơn vị (AutoZOffsetMax = -8)
    SpeedRun  : khoá mục tiêu bắn (không đổi cây giữa chừng)
    Raid      : mục tiêu = _boss
    Monster   : đứng trên VÀNH đấu trường bán kính 14 (ClampToMonsterRadius); lùi khi trong MinEnemyDist tới +0.5
    tốc độ thực ~14.4 u/s  ->  tới cây (-102.5, 0, 27.5) từ (-100, 0, 0) mất ~1.4 s (đo: 5 -> 12 cây vào khung)

ScreenSpeedMultiplier:                                 // triệt tiêu foreshortening isometric (CamPitchSin 0.57735)
    stick.x / ScreenSpeedK ; stick.z * ScreenSpeedK    // ánh xạ BẤT ĐỐI XỨNG: ngang chia, dọc nhân

Breadcrumbs (static, DÙNG CHUNG cho cả đàn):
    leader thả 1 vệt / 0.5 u, giữ tối đa 100; follower nhắm vệt thứ 5 phía trước (cắt góc nhẹ)

Ba tail byte-identical (drive / face / stop) trong HandleAutoMove được tách thành helper L1 (AutoDrive, FaceAlong, StopAndFaceTarget) — cùng mã máy, chỉ khác tên. Bài học phương pháp kèm theo: xác nhận callee từ lệnh BL, không từ nhãn trong overlay (G356).

3.1c AI follower — vệt leader có độ trễ, hai lớp chống rung (batch D, đợt 5)

RecordLeaderHistory (leader, mỗi frame):   push (pos, t) vào lịch sử STATIC dùng chung; cắt bỏ mục cũ hơn 2 s
GetLaggedLeaderPos (follower):             đọc vị trí leader tại t - _lagSeconds   // _lagSeconds ∈ [0.15, 0.6], rút 1 lần/instance
EvaluateDesiredState(leader, dist):
    băng của trạng thái ĐANG GIỮ được nới thêm HysteresisMargin (×(1+k))       // lớp 1: không đổi khi chỉ vừa chạm ngưỡng
HandleAI:
    if (_stateTimer < MinStateDuration) giữ trạng thái                          // lớp 2: tối thiểu thời gian ở một trạng thái
    Follow / Chase / AttackStay / Reposition / Assist (xích 0.85·FollowMaxDist) / Fidget
HandleAIMonster: không leader — mỗi mèo giữ một TRẠM trên vành bán kính 14 (cùng vành với auto-move §3.1b)

Vì sao là vệt chứ không phải điểm: ba follower cùng đọc một lịch sử ở ba độ trễ khác nhau tự nhiên xếp thành đoàn rắn, không cần offset đội hình phức tạp; độ trễ rút từ GetInstanceID() nên tất định giữa các lần chạy — thứ tự đoàn không đổi ngẫu nhiên mỗi lần vào game.

3.2 Hệ spread — tản đạn theo nhịp bắn

SpreadAngleMin / SpreadAngleMax      biên độ tản
SpreadGrowthRate      [Range(0.1, 0.6)]   Lerp tăng mỗi phát bắn
SpreadDecayMultiplier                     giảm khi ngừng (maxAngle * hệ số / giây)
_spreadAngle                              trạng thái hiện tại
_prevHadTarget                            biết lúc nào chuyển từ bắn sang ngừng
Debug_SpreadAngle / Debug_SpreadMaxAngle  [Header("Spread Debug (Read Only)")]

Hai điều đáng học: 1. Spread max phụ thuộc tốc độ đánh — tooltip gốc nói rõ: 스프레드 최소(가장 느린 공속) 각도 ("góc tản nhỏ nhất = tốc độ đánh chậm nhất"). Bắn càng nhanh, tản càng rộng. Đây là một cơ chế cân bằng tự nhiên: nâng cấp tốc độ đánh không phải là lợi ích thuần. 2. Có field debug read-only ngay trong inspector. Một hệ thống cảm-giác chỉ chỉnh được nếu nhìn thấy được. Framework nên chuẩn hoá [Header("... Debug (Read Only)")].

3.3 Boids pet — ba lực, tính mỗi bước vật lý

// L1 ANALYSIS — Pet_Ingame_manager.FixedUpdate (disasm-proven, chạy 4 006x/40s)
Cat mainCat = GetMainCat();
if (mainCat == null) return;
Vector3 pos = mainCat.transform.position;
float inst = Vector3.Distance(pos, _prevMainCatPos) / Time.fixedDeltaTime;
_smoothedCatSpeed += (inst - _smoothedCatSpeed) * 0.12f;      // làm mượt hàm mũ
_prevMainCatPos = mainCat.transform.position;
ComputePetSeparation();
ComputeCatAvoidance();
MovePets(mainCat);

Hệ số 0.12 là một bộ lọc thông thấp bậc một: pet phản ứng theo tốc độ trung bình của mèo trong khoảng ~8 bước vật lý, không theo tốc độ tức thời. Nếu dùng tốc độ tức thời, pet sẽ giật mỗi khi người chơi nhả joystick. Đây là hằng số cảm-giác quan trọng nhất của hệ pet.

Ba lực (thuật toán đã giải mã, xem cảnh báo ở §5):

# 1. Tách nhau giữa pet (từng cặp i<j, chỉ pet đang active)
d = Pets[i].pos.xy - Pets[j].pos.xy;  dist = |d|
if 0.001 <= dist <= PetSeparationRadius:
    f = normalize(d) * ((1 - dist/PetSeparationRadius)^2 * PetSeparationStrength)
    Pets[i].SeparationForce += f
    Pets[j].SeparationForce -= f

# 2. Né mèo (mỗi pet, với mỗi mèo đang active)
d = pet.pos.xy - cat.pos.xy;  dist = |d|
if 0.001 <= dist <= CatAvoidRadius:
    pet.CatAvoidanceForce += normalize(d) * ((1 - dist/CatAvoidRadius)^2 * CatAvoidStrength)

# 3. Di chuyển (MovePets)
    FormationOffset + SeparationForce + CatAvoidanceForce
    + trễ Start/StopMoveDistance (hysteresis)
    + kẹp [MinSpeed, MaxSpeed], FullSpeedDistance
    + CatSpeedMultiplier * _smoothedCatSpeed
    -> rb, và điều khiển animation Spine (IsMoving / _currentAnim)

Chi tiết đáng học: hàm suy giảm là bình phương (1 - dist/radius)^2, không tuyến tính. Lực gần như bằng 0 ở rìa bán kính và tăng vọt khi rất gần — nên pet không "đẩy nhau từ xa" nhưng cũng không bao giờ chồng lên nhau. Và tính toán chỉ dùng 2D (x, y) dù thế giới là 3D: mặt phẳng chiếu là đủ cho một game nhìn từ trên, và rẻ hơn hẳn.

Start/StopMoveDistance là hysteresis: pet bắt đầu đi khi cách xa hơn StartMoveDistance, nhưng chỉ dừng khi gần hơn StopMoveDistance (< StartMoveDistance). Cùng ý tưởng chống rung như ở Cat.AIState.

3.4 Kẻ địch — một Enemy chung + dispatch component

// Enemy.Die_Reload — RVA 0x2AFF354
switch (_type)
{
    case Type.Tree:          GetComponent<Tree>().Die_Reload();          gameObject.SetActive(false); break;
    case Type.Tree_SpeedRun: GetComponent<Tree_SpeedRun>().Die_Reload(); gameObject.SetActive(false); break;
    case Type.BossMonster:   GetComponent<Boss>().Die_Reload();          break;   // KHÔNG tắt
    case Type.Monster:       GetComponent<Monster>().Die_Reload();       break;   // KHÔNG tắt
    case Type.Ore:           GetComponent<Ore>().Die_Reload();           break;   // KHÔNG tắt
    case Type.RaidBoss:      GetComponent<Raid_Boss>().Die_Reload();     break;   // KHÔNG tắt
}

Enemy giữ toàn bộ thứ chung: Health_Now, Health_Max, Money, is_Die, thanh máu Slider, hiệu ứng nảy _bounce. Component anh em giữ thứ riêng: cây rơi tiền và đẩy wave, quặng nhả dust và dia, quái bị knockback, boss có coroutine đi lang thang.

Chú ý sự bất đối xứng ở trên: chỉ TreeTree_SpeedRun bị SetActive(false) ngay tại Enemy. Bốn loại còn lại tự quyết định vòng đời của chúng bên trong Die_Reload riêng (Ore.Die_Reload kết thúc bằng gameObject.SetActive(false); Monster/Boss có thể cần chạy hiệu ứng chết trước). Đây là bằng chứng đọc được từ disasm, không phải suy đoán.

Ba component data-holder thuần (EnemyColl, Enemy_Slider, Gun) chỉ có .ctor — chúng tồn tại để liên kết ngược: EnemyColl.enemyScript cho phép một collider con tìm ra Enemy cha mà không cần GetComponentInParent (vốn đắt và chạy mỗi lần trúng đạn).

3.5 Quái vật — animation trên GPU

// Monster — dump.cs:594503
private MaterialPropertyBlock _mpb;                    // @0x40
private static readonly int ID_TimeOffset;             // @0xC
private static readonly int ID_AnimSpeed;              // @0x10
private static readonly int ID_ScaleAmount;            // @0x14

240 quái vật trên màn hình, không Animator nào. Thay vào đó mỗi con nhận một MaterialPropertyBlock với ba tham số: lệch pha thời gian, tốc độ animation, biên độ co giãn. Shader tự làm phần còn lại.

Vì sao đây là mẫu quan trọng cho idle: Animator tốn ~vài chục µs mỗi instance mỗi frame và không batch được. MaterialPropertyBlock giữ nguyên batching (cùng material) và chi phí gần bằng 0. ID_TimeOffset khác nhau cho từng con là thứ khiến 240 quái không đập cánh đồng loạt — một số ngẫu nhiên duy nhất thay thế cho 240 state machine.

3.6 Trạng thái wave và đường cong stage

Tree_manager:
    Stage_Now / Level_Now / Stage_Value_Max         tiến độ
    TreeCount_Now / TreeCount_Max                   wave hiện tại
    GameTime_Now / GameTime_Max                     đồng hồ đếm ngược màn
    gameSlider (GameSlider)                         thanh tiến độ
    _healthCache / _moneyCache  Dictionary<int, InfVal>
    const int PrecomputedStageCount = 5000
    static InfVal PowOfThree(int exp)
    GetStageHealth(stage) / GetStageMoney(stage)
    StageAdvance(ref int stage, ref int level)
    Game_Start / Game_Win / Game_Fail / Game_Restart / Game_Exit

Vòng đời một wave (disasm-proven; đợt 3 tách đúng biên hàm — G346):

// Tree.Die_Reload  0x2B43904  (184 B — chỉ hai dòng)
    Particle_Polling.instance.Spawn(MoneySpawn_trans.position, is_Boss ? 15 : 5)   // KHÔNG null-guard: binary dereference thẳng
    Tree_manager.instance.Tree_Destroyed()

// Tree_manager.Tree_Destroyed  0x2B439BC  (864 B — toàn bộ logic wave)
    if (!Gamemanager.is_gaming) return;                  <- guard bắt buộc (doc 02 §3.5)
    TreeCount_Now--
    ratio = (TreeCount_Max - TreeCount_Now) / TreeCount_Max
    gameSlider.Value_Reload(ratio)
    [R1 analytics: SendLog_WaveRatio CHỈ khi Stage_Now == 0 && Level_Now >= Stage_Value_Max,
     mốc 10 % (decile), dedup HashSet<string> khoá "{stage}_{level}_{percent}"]
    if (TreeCount_Now <= 0) Game_Win()                   // private, cùng class
    Mission_manager.instance.MissionValue_Increase(Mission_Type.TreeCount)

Game_Win (0x2B452E4, 1 144 B — G347):

    if (!is_gaming) return;  is_gaming = false
    _stageTimeAll += realtimeSinceStartup - _roundStartTime
    gameSlider.Value_Reload(1f)
    (stage0, level0) = (Stage_Now, Level_Now)                     // chụp trước khi tiến
    StageAdvance(ref Stage_Now, ref Level_Now)                    // 5 level / stage
    // RELIC "SKIP": Relic_manager.Skip_Value (static +0x6C) là % cơ hội
    if (Random.Range(0, 100) < Skip_Value && Level_Now + 10*Stage_Now < Stage_Value_Max)
        StageAdvance(...)  lần 2                                   // skip KHÔNG BAO GIỜ vượt mốc cao nhất từng đạt
    _pendingGameSkipUI = <kết quả skip>
    if (Level_Now + 10*Stage_Now >= Stage_Value_Max):             // lập kỷ lục
        Stage_Value_Max = Level_Now + 10*Stage_Now
        if (!is_ProgramDestroyed) Rank_manager.SubmitScore(false, null)   // [R1]
        Contents_Lock_Reload()
        SendLog_WaveClear();  if (level0 == 4) SendLog_StageClear(_stageTryCount)   // [R1]
    Sound_manager.PlaySfx(Win);  MissionValue_Increase(StageClear)
    GameWin_UI.SetActive(true);  Loading_Then_Start()

Hai chi tiết đáng học: (1) relic Skip chỉ nhảy trong vùng đã chinh phục (< Stage_Value_Max) — nó rút ngắn cày lại sau rebirth chứ không đẩy tiến độ mới; (2) _stageTimeAll cộng dồn thời gian thực mỗi màn — nguồn cho analytics, không phải cho kinh tế.

Đặt cây (Tree_Grid.Spawn 0x2B43D28 — G343):

    order = [0..99].OrderBy(i => Morton(i % 10, i / 10))         // Z-order curve: Spread(x) | (Spread(z) << 1)
    acc = 0; for k in 0..want-1: acc += total; pick order[acc / want]   // lấy `want` ô CÁCH ĐỀU trên đường cong
    mỗi cây: localPosition = (5*j - 50*(j/10) + jitter, 0, 5*(j/10) + jitter), jitter ∈ ±randomRange

Đọc nhầm comparer thành "xáo trộn" rồi thay bằng Random sẽ cho cây tụ đám — đúng thứ SOT cố tránh.

Hai chi tiết kiến trúc: bộ đếm wave là một số nguyên duy nhất (TreeCount_Now), và tỉ lệ tiến độ được tính lại từ nó mỗi lần — không có state trùng lặp. Còn _sentWaveRatioKeys (HashSet chuỗi) đảm bảo mỗi mốc analytics chỉ gửi một lần cho mỗi (stage, level, percent).

Đường cong stage được tính trước 5 000 stage và cache trong dictionary. Kết hợp với PowOfThree(int) (luỹ thừa 3), điều này gợi ý máu/tiền tăng theo bậc 3 — nhưng công thức chính xác là UNKNOWN (xem §5).


4. Framework takeaway

  1. Tách "quan hệ giữa các cá thể" khỏi "hành vi của cá thể". IFlockManager : đội hình, lực tách nhau, thêm/bớt thành viên (chạy mỗi frame/bước vật lý) IFlockMember : máy trạng thái riêng, nhận lực, tự di chuyển Đây là lý do một class 11 method (Cat_manager) quản được một đàn mà không thành God object.

  2. Chống rung là hạ tầng, không phải bản vá. Mọi ngưỡng chuyển trạng thái cần hysteresisMargin + minStateDuration; mọi ngưỡng khoảng cách cần một cặp start/stop. Framework nên phơi ra một kiểu HysteresisThreshold thay vì để mỗi người tự nhớ.

  3. Làm mượt hàm mũ cho mọi tín hiệu đầu vào của chuyển động. smoothed += (instant - smoothed) * k với k = 0.12 là một hằng số cảm-giác đã được kiểm chứng trên một game đã ship. Đừng dùng giá trị tức thời.

  4. Đi theo vệt đường, không đi theo đường thẳng. Breadcrumb (RecordDist, MaxCrumbs, CrumbLookahead) là cách rẻ nhất để follower trông thông minh mà không cần pathfinding.

  5. Lực suy giảm bình phương, tính trong 2D. (1 - dist/radius)^2 cho ranh giới mềm ở rìa và đẩy mạnh khi sát nhau. Với game nhìn từ trên, chiếu về 2 chiều và tiết kiệm một nửa chi phí.

  6. MaterialPropertyBlock + lệch pha thời gian thay cho Animator khi có hàng trăm instance. Ba tham số (timeOffset, animSpeed, scaleAmount) đủ để một đám đông trông sống động. Đây là kỹ thuật có tỉ lệ hiệu quả/chi phí cao nhất trong toàn bộ tài liệu này.

  7. Một component "kẻ địch" chung + dispatch tới component chuyên biệt. Máu/tiền/thanh máu/hiệu ứng nảy là chung; vòng đời và phần thưởng là riêng. Cho phép loại chung uỷ quyền vòng đời cho loại riêng, đừng ép mọi loại chết giống nhau.

  8. Component "liên kết ngược" (EnemyColl.enemyScript) thay cho GetComponentInParent trên hot path.

  9. Field debug read-only trong inspector cho mọi hệ thống cảm-giác. Không nhìn thấy thì không chỉnh được.

  10. Tính trước và cache đường cong tiến độ. PrecomputedStageCount = 5000 + Dictionary<int, InfVal> — đường cong idle được truy vấn liên tục; đừng tính lại luỹ thừa mỗi lần.

  11. "Bể" thực thể trang trí = POOL CỐ ĐỊNH bằng đúng trần kho, không phải Instantiate/Destroy. Mẫu đọc được ở Fish_Tank (bể cá 20 ô, docs/functions/ca-fish.md): fishPool là mảng dựng sẵn trong scene, đúng MAX_FISH phần tử; Awake gọi Init(this) + Deactivate() cho từng ô; thêm một thực thể = GetInactiveItem() rồi Reload(grade, invIndex); xoá = Deactivate(). Không có Instantiate nào ở runtime, không có GC spike khi người chơi rút liên tục, và trần kho tự thực thi bằng kích thước mảngGetInactiveItem() trả null là hết chỗ, không cần một if thứ hai. Đổi lại: pool và trần phải khớp nhau, nên trần nên là một const duy nhất mà cả hai cùng đọc. Kèm theo: chỉ số kho (inventoryIndex) là dữ liệu, tham chiếu item là tạm bợ. Sau mỗi lần xoá, SyncInventoryIndices() gán lại chỉ số cho mọi item đang sống bằng cách bắt cặp theo hạng với một mảng bool[] used (mỗi ô kho chỉ dùng một lần); item không tìm được ô nào thì ở lại -1. Đây là cách an toàn khi lưới có thể bị dựng lại dưới chân coroutine — cùng bài học với mục 1 của docs/functions/pet-drag-fuse.md §6.

  12. Rải chỗ trống bằng "thử N lần, giữ chỗ thoáng nhất" — đừng dùng lưới, đừng giải va chạm. best = randomPoint(); bestDist = -1 for (i = 0; i < maxAttempts; i++) { // Fish_Tank: 30 p = randomPoint() d = distanceToNearestActive(p, exclude: self) if (d >= minDistance) return p // Fish_Tank: 100 px — đủ thoáng, nhận NGAY if (d > bestDist) { bestDist = d; best = p } } return best // không nhánh thất bại, không vòng vô hạn Ba tính chất khiến mẫu này đáng chép: (a) luôn trả về một điểm hợp lệ kể cả khi bể chật — chất lượng suy giảm mượt thay vì hỏng; (b) chi phí có trần cứng O(maxAttempts × N); (c) hai tham số (minDistance, maxAttempts) là núm chỉnh cảm giác, không phải hằng số kỹ thuật. Nhớ seed best bằng một điểm ngẫu nhiên TRƯỚC vòng lặp — nếu seed bằng Vector2.zero thì bể chật sẽ dồn mọi thứ về tâm. Cùng khuôn "kẹp vào khung": if (min > max) { min = 0; max = 0; } — khi thực thể lớn hơn khung chứa thì ghim về tâm thay vì cho ra một Random.Range ngược biên.

  13. Sắp lớp bằng MỘT thang order mang HAI trục + một dải "luôn trên cùng" nằm ngoài Clamp. csharp order = Clamp(RoundToInt(yOrigin − y) + step * (maxGrade − grade), 0, 2000); // step = 10 if (isDragging) order = 9999; // vật thể người chơi đang cầm else if (isAnimatingToTarget) order = 9998; // vật thể đang trong hoạt ảnh Trục hạng chiếm bước 10, trục toạ độ Y chiếm bước 1 ⇒ hạng vẫn thắng, nhưng trong cùng hạng thì ai ở thấp nổi lên trên (phối cảnh nhìn ngang). Hai giá trị đặc biệt thoát khỏi cả hai trục, và vì chúng nằm ngoài Clamp nên không bao giờ bị trục nội dung đuổi kịp. Đi kèm là một quyết định thẩm mỹ đáng ghi: Fish_Tank.SortHierarchyByGrade sắp hạng giảm dần trong hierarchy ⇒ thực thể bậc CAO (sprite to) bị bậc THẤP vẽ đè. Nghe ngược, nhưng nó giữ cho một con to không nuốt mất mặt tiền của cả đàn nhỏ — người chơi vẫn đếm được tài sản của mình. Thực thể đang tắt bị SetAsLastSibling() để không chiếm chỗ sibling.


5. Negative evidence & UNKNOWN

Mục Trạng thái Bằng chứng khoá được nó
Toán NEON của 3 method flocking (ComputePetSeparation, ComputeCatAvoidance, MovePets) CHƯA XÁC MINH BYTE-EXACT (G131). Thuật toán đã giải mã (§3.3) nhưng ánh xạ lane vector → thành phần và cách cộng dồn lực chưa được oracle xác nhận. Một lỗi lane cho ra chuyển động sai một cách tinh vi. Không được ship ở mức 1:1 khi chưa xác minh. Cần phiên có nhiều pet ĐANG hoạt động và tụ lại
Kết quả chạy flock-oracle Đã chạy, KHÔNG kết luận được. Mảng Pets đọc đúng (3 slot, offset đã xác minh) nhưng toàn bộ SeparationForce/CatAvoidanceForce = 0 và moving = 0 — pet không active trong phiên early-game, nên guard activeSelf bỏ qua chúng (nhất quán với bản giải mã). Đây là bằng chứng phủ định hợp lệ, không phải thất bại. [runtime trace: flock_samples.json]; G131
Cat_manager.ComputeSeparation CHƯA dựng lại + cần sửa biên hàm trong IDA trước (G130). Tương tự Pet_Ingame_manager nhưng chưa giải mã. Sửa biên hàm rồi disasm RVA 0x2AEF830
Cat (67 method, 42 160 B) HOÀN TẤT (đợt 5): A 21 + C 12 + B 12 + D 13 + E 7 = 65 marker RVA; chỉ OnDrawGizmosSelected (editor-only) cố ý bỏ G352, G356, G363, G364, G369
~~Mèo không có thân (sprite) trên save đã nạp~~ (G425) ĐÃ ĐÓNG (G374): Skin_Bone_return / Skin_Color_return / is_Unlock là return-stub ⇒ sprite thân null + màu tay trong suốt sau Skin_Reload; đã dựng cả ba (skin 0 luôn mở). Trên save mới lỗi chưa lộ vì Skin_manager.Start cũng còn stub (G423) — hai lỗi ship chung chờ build kế
Đạn trúng nhưng không gây gì trên save đã nạp (Stage 3 desert, Upgrade [36,36,35], Dmg_Value 100) VẪN MỞ (G374 loại trừ tĩnh xong): Bullet layer 8 SphereCollider trigger + RB non-kinematic; EnemyColl layer 7 BoxCollider, 1 933 tag "Enemy"; SpawnEx → SetupLinear SetActive(true); Damage_Tag kiểm tag của enemy; toàn chuỗi Damage_Collider → Damaged → Die → Slider_Reload → Die_Reload → Get_Money → Money_Txt_Spawn đã dựng; số học ~1 600 dmg vs máu Stage-2 2 200 × bậc ⇒ ~3 phát/xương rồng. Probe batch 16 (G381) — giả thuyết is_gaming BỊ BÁC: is_gaming = true ở cả 32 mẫu; đã có mục tiêu (nearObj), 2–6 đạn sống, Damage (100 mới / 1 490 nạp) vs máu 450 / 3 960, nhưng không enemy nào có Health_Now ≠ Health_Max trong 30 s bắn ⇒ đứt giữa đường bay của đạn và Enemy.Damaged (OnTriggerEnter / Hit* / EnemyColl chuyển tiếp / thiết lập trigger). Cả hai nghi phạm cài đặt ĐÃ KIỂM (G383) và LOẠI TRỪ (G386): (1) ma trận va chạm layer trong export thật sự hỏng — ký tự / ở chữ số hex thứ 7 của cả 32 mục uint32 (lỗi mã hoá của AssetRipper, giá trị còn lại = SOT); đã khôi phục từ PhysicsManager/Physics2DSettings ship (Bullet(8)↔Enemy(7) bật) — nhưng batch 17 vẫn damaged = 0 32/32 ⇒ không phải nguyên nhân; (2) thứ tự thực thi script khớp SOT (không script game nào ≠ 0) ⇒ TutorialStep kẹt là lỗi code (SetStep stub, đã dựng — G385). GỐC RỄ TÌM RA (G400): hằng CamForward trong Cat..cctor thiếu pitch 40° (y = 0) ⇒ CalcBulletSpawnPos = pos + CamForward × ((2 − pos.y) / CamForward.y) chia cho 0mọi viên đạn spawn ở ±∞, không bao giờ chạm trigger, hết hạn theo lifetime — khớp 0 dòng trace. Đường này chạy mỗi phátShoot_Random_trans rỗng trên cả 3 Cat trong scene (GetFirePosition rơi về nhánh chiếu). Nghi phạm cuối cùng bị loại vĩnh viễn: Physics.IgnoreLayerCollision/IgnoreCollision 0 call site; Collider.set_enabled 5 caller, không cái nào chạm Enemy_Coll; 1 933 Enemy_Coll active + collider bật. Batch 20 xác nhận (G405): 104/104 hit, cây đổ, tiền + lá rơi, thanh máu trên xương rồng, hàng nút HUD + bàn tay tutorial ĐÃ ĐÓNG (G424 → G400 → G405)
~~Save mới bắn 0 sát thương cả round~~ (G423) ĐÃ ĐÓNG (G374): Relic_manager.Start = Value_Reload, Skin_manager.Start = UI_First_Reload + Skin_Value_Reload + Cat Skin_Reload (doc 07 §2.4) — chờ build kế
Bắn đã chạy thật trên thiết bị: súng lục bắn liên tục, tracer tới mục tiêu, chớp nòng; thanh nâng cấp (SlicedFilledImage hướng Up) đo với tiền thật 47.7 / 48.5 / 57.2 % chiều cao panel, ổn định; danh tính PlayFab round-trip build-measured (device) — xác nhận, không phải UNKNOWN G371
Cây/quặng chưa hiện trên thiết bị ĐÓNG (G354/G358): cây được spawn đúng nhưng nằm ngoài camera orthographic (size 40, iso yaw −45°, pitch 35.26°; nửa bề rộng nhìn thấy trên 1080×2400 = 40 × 0.45 = 18 đơn vị); khối grid[0] tâm (−100, 0, 50.5) trong khi mèo spawn (−100, 0, 0) ⇒ chỉ 5/30 cây trong khung. SOT: HandleAutoMove đưa mèo tới cây trong ~1 s và Camera_manager.Follow kéo camera theo — thiếu batch C. Sau batch C trên thiết bị: 5 → 12 cây trong khung, mèo đi và quay mặt về mục tiêu, chỉ báo bám cây (đúng dự đoán). Quặng (Mine) cần batch riêng (Mine_manager 21 stub) §2.4
Công thức Tree_manager.GetStageHealth / GetStageMoney ĐÃ ĐÓNG (G395): hàm sinh = seed 250 / 20, ×3 mỗi stage, cắt 2 chữ số có nghĩa (§2.3); mảng scene 5 000 mục khớp 8 mục đầu đã đối chiếu; PowOfThree + cache phục vụ stage > 5 000 (ngoài bảng) Đối chiếu toàn bộ 5 000 mục (script)
Giá trị thực tế của mọi tham số đội hình/boids ĐÓNG MỘT PHẦN: Cat_manager scene = FormationRadius 2.5, SeparationRadius 1.8, SeparationStrength 3 (doc 11 §1.1); mặc định ctor FormationRadius 10 (G306) — scene thắng. Tham số Pet_Ingame_manager vẫn chưa trích Trích Pet_Ingame_manager từ scene YAML
Ánh xạ 13 skeleton Spine → pet nào trong game UNKNOWN — biết 13 skeleton và biết Pet.Skin_list / _skinNum, chưa nối được. Trích Pet_manager từ scene
Cat.AIState có bao nhiêu trạng thái và giá trị nào UNKNOWN một phần — suy được từ tên method (AIFollow, AIChase, AIAttackStay, AIReposition, AIAssist, AIFidget) nhưng enum chưa trích. Grep Cat.AIState trong dump.cs
Monster.Freeze()Barrier_Touched() CHƯA dựng lại — biết chữ ký và BarrierRadius = 20, không biết hành vi. Disasm RVA 0x2AF82E8 / 0x2AF7904
Không quan sát thấy Không có NavMeshAgent nào trong Cat, Pet, Monster hay Boss — mọi di chuyển là thủ công qua Rigidbody + lực. Package com.unity.ai.navigation trong manifest nhưng không dùng ở các class này. dump.cs field list các class trên
Không quan sát thấy Pet khôngAnimator — chỉ SkeletonAnimation (Spine). Monster cũng không — chỉ MaterialPropertyBlock. BossRaid_Boss thì có Animator. dump.cs:597811, :594503, :593531, :595047

6. Raw evidence

  • dump.cs:593380Cat_manager (tooltip tiếng Hàn xác nhận Cat_list[0])
  • dump.cs:593002Cat (~100 field, ~50 method, hằng số)
  • dump.cs:597811Pet
  • dump.cs:594503Monster (MaterialPropertyBlock, knockback)
  • dump.cs:593531 / :595047Boss / Raid_Boss
  • dump.cs:602135Tree_manager (wave + đường cong stage)
  • [reconstructed: Cat.cs], Tree_Grid.cs, Tree.cs, Ore.cs, Tree_manager.cs} — thân đã dựng lại đợt 3 (marker // L1 RVA)
  • [evidence codegen: TABLE.md]degreeRates, MonsterCountPerLevel, SpawnIntervalPerLevel (SHA MATCH)
  • [report: t300.png] — đo đồng hồ màn 180 s (G351)
  • Gaps đợt 3: G306, G343, G344, G346, G347, G351, G352, G353 trong gap list (nội bộ)
  • [evidence codegen: README.md], Pet_Ingame_manager.c} — boids đã giải mã
  • [runtime trace: flock_oracle.js], run_flock.py, flock_samples.json} — oracle lực (bằng chứng phủ định)
  • [reconstructed: Enemy.cs], EnemyColl.cs, Enemy_Slider.cs, Ore.cs, Tree_SpeedRun.cs, Pet_Dummy.cs}
  • [evidence codegen: Tree.cs] — vòng đời wave
  • [report: asset_lane.md] §0, §5 — Spine 4.1.24, 13 skeleton, tên dự án BardCat
  • [index: scene_census.json] — số instance
  • Gaps: G130, G131 trong gap list (nội bộ)