初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
## EnemyManager — 敌人管理器 AutoloadGDScript 接口层)
## 热路径由 C# EnemyManagerCs 子节点驱动。
## 权威来源:implementation_plan.md §2.3.C
##
## SoA 布局(ENEMY_STRIDE=8):
## [ x, y, vx, vy, hp, hp_max, enemy_type, state ]
## 0 1 2 3 4 5 6 7
extends Node
const ENEMY_STRIDE: int = 8 # 禁止裸整数 8P6-N54
const MAX_ENEMIES: int = 1024
const OFFSCREEN_SEPARATION_INTERVAL: int = 10 # 屏外低频 Boid 分离力更新频率(P6-N1)
# SoA 热数组
var _data: PackedFloat32Array = PackedFloat32Array()
var _active_count: int = 0
# entity_id ↔ SoA 槽位 双向映射
var _entity_index_map: Dictionary = {} # { entity_id → slot_idx }
var _slot_entity_map: Dictionary = {} # { slot_idx → entity_id }
var _next_entity_id: int = 1
# LOD:屏内可见标志(PackedByteArray0=屏外,1=屏内
var _visible_flags: PackedByteArray = PackedByteArray()
var _offscreen_sep_counter: int = 0
var _cs_node: Node = null
## ADR-A4:精英寻路。W114 杂鱼永远 BoidW15+ Elite/Boss 按需挂 NavigationAgent2D
## 并发寻路上限 MAX_PATHFINDING_ENEMIES=20,超出降级 BoidP-S5-AI-01)。
const MAX_PATHFINDING_ENEMIES: int = 20
const ELITE_SPEED: float = 90.0
## 敌人原型(enemy_type 存于 SoA[+6])。S6 内容:基础/快速/护甲/精英/Boss
enum Type { BASIC = 0, FAST = 1, ARMORED = 2, ELITE = 3, MINIBOSS = 4, BOSS = 5 }
## 各原型数值:纯数据驱动,唯一来源 data/enemies.json(游戏设计器「敌人」面板维护)
## get() 的单值默认仅为防崩溃,非内容副本
var _SPEED: Dictionary = {} # type → 移动速度
var _ARMOR: Dictionary = {} # type → 护甲
var _RENDER_SIZE: Dictionary = {} # type → 渲染直径
var _RENDER_COLOR: Dictionary = {} # type → 渲染颜色
const ARCHETYPE_JSON: String = "res://data/enemies.json"
## 数据驱动:从 data/enemies.json 加载原型数值(速度/护甲/尺寸/颜色)
func _load_json_archetypes() -> void:
if not FileAccess.file_exists(ARCHETYPE_JSON):
push_error("EnemyManager: 缺失 res://data/enemies.json(敌人原型数值)")
return
var data = JSON.parse_string(FileAccess.get_file_as_string(ARCHETYPE_JSON))
if not (data is Dictionary):
push_error("EnemyManager: enemies.json 格式错误")
return
for k in data:
var t: int = int(k)
var d: Dictionary = data[k]
_SPEED[t] = float(d.get("speed", 80.0))
_ARMOR[t] = float(d.get("armor", 0.0))
_RENDER_SIZE[t] = float(d.get("size", 14.0))
var c: Array = d.get("color", [0.9, 0.3, 0.3])
_RENDER_COLOR[t] = Color(float(c[0]), float(c[1]), float(c[2]))
var _nav_region: NavigationRegion2D = null # 程序化矩形导航区
var _pathfinders: Dictionary = {} # entity_id → {"agent":NavigationAgent2D, "host":Node2D}
var _pathfinder_order: Array = [] # FIFO,用于超上限淘汰
func _ready() -> void:
_load_json_archetypes() # 数据驱动:覆盖原型数值(游戏设计器面板维护)
_data.resize(MAX_ENEMIES * ENEMY_STRIDE)
_data.fill(0.0)
_visible_flags.resize(MAX_ENEMIES)
_visible_flags.fill(0)
func _physics_process(delta: float) -> void:
_offscreen_sep_counter += 1
var do_sep: bool = (_offscreen_sep_counter >= OFFSCREEN_SEPARATION_INTERVAL)
if do_sep:
_offscreen_sep_counter = 0
if not _cs_node:
_gd_update_movement(delta)
# ADR-A4:精英寻路移动(NavigationAgent2D 跟随),仅当存在 pathfinder 时执行
if not _pathfinders.is_empty():
_update_pathfinding_movement(delta)
# S1: 移动后更新 SpatialGrid(碰撞检测依赖)
_update_spatial_grid()
func _update_spatial_grid() -> void:
for i in _active_count:
var base: int = i * ENEMY_STRIDE
var eid: int = _slot_entity_map.get(i, -1)
if eid >= 0:
SpatialGrid.insert(eid, Vector2(_data[base + 0], _data[base + 1]))
# GDScript 回退:直线追玩家(C# 没就绪时使用)
func _gd_update_movement(delta: float) -> void:
var player_pos: Vector2 = PlayerManager.get_position()
var has_pf: bool = not _pathfinders.is_empty() # 无精英时零额外开销
for i in _active_count:
if has_pf and _pathfinders.has(_slot_entity_map.get(i, -1)):
continue # 寻路精英由 _update_pathfinding_movement 处理
var base: int = i * ENEMY_STRIDE
var ex: float = _data[base + 0]
var ey: float = _data[base + 1]
var spd: float = _SPEED.get(int(_data[base + 6]), 80.0) # 按原型查速度
var dx: float = player_pos.x - ex
var dy: float = player_pos.y - ey
var dist: float = sqrt(dx * dx + dy * dy)
if dist > 1.0:
_data[base + 2] = (dx / dist) * spd
_data[base + 3] = (dy / dist) * spd
_data[base + 0] += _data[base + 2] * delta
_data[base + 1] += _data[base + 3] * delta
# ── 对外接口 ───────────────────────────────────────────────────
func spawn_enemy(pos: Vector2, hp: float, enemy_type: int = 0, has_pathfinding: bool = false) -> int:
if _active_count >= MAX_ENEMIES:
return -1
var entity_id: int = _next_entity_id
_next_entity_id += 1
var slot: int = _active_count
var base: int = slot * ENEMY_STRIDE
_data[base + 0] = pos.x
_data[base + 1] = pos.y
_data[base + 2] = 0.0
_data[base + 3] = 0.0
_data[base + 4] = hp # hp
_data[base + 5] = hp # hp_max
_data[base + 6] = float(enemy_type)
_data[base + 7] = 0.0 # state: 0=alive
_entity_index_map[entity_id] = slot
_slot_entity_map[slot] = entity_id
_active_count += 1
# ADR-A4W15+ Elite 在并发上限内挂寻路(超限降级 Boid)
if has_pathfinding and _pathfinders.size() < MAX_PATHFINDING_ENEMIES:
_register_pathfinder(entity_id, pos)
return entity_id
# ── ADR-A4 精英寻路子系统 ──────────────────────────────────────
func _ensure_nav_region() -> void:
if _nav_region != null:
return
_nav_region = NavigationRegion2D.new()
var poly := NavigationPolygon.new()
# 覆盖竞技场的单个凸矩形导航多边形(无障碍占位;P-S5-AI-01 仅测寻路开销)
var ext: float = 4000.0
poly.vertices = PackedVector2Array([
Vector2(-ext, -ext), Vector2(-ext, ext), Vector2(ext, ext), Vector2(ext, -ext),
])
poly.add_polygon(PackedInt32Array([0, 1, 2, 3]))
_nav_region.navigation_polygon = poly
add_child(_nav_region)
func _register_pathfinder(entity_id: int, pos: Vector2) -> void:
_ensure_nav_region()
var host := Node2D.new()
host.position = pos
add_child(host)
var agent := NavigationAgent2D.new()
agent.path_desired_distance = 8.0
agent.target_desired_distance = 8.0
agent.avoidance_enabled = false # 与 SoA Boid 分离力共存,避免双重避障
host.add_child(agent)
_pathfinders[entity_id] = {"agent": agent, "host": host}
_pathfinder_order.append(entity_id)
func _unregister_pathfinder(entity_id: int) -> void:
var pf: Dictionary = _pathfinders.get(entity_id, {})
if pf.is_empty():
return
(pf["host"] as Node).queue_free() # agent 是 host 子节点,随之释放
_pathfinders.erase(entity_id)
_pathfinder_order.erase(entity_id)
## 寻路精英移动:host 同步到 SoA 坐标,agent 朝玩家求下一路径点并转向
func _update_pathfinding_movement(delta: float) -> void:
var player_pos: Vector2 = PlayerManager.get_position()
for eid in _pathfinders:
var slot: int = _entity_index_map.get(eid, -1)
if slot < 0:
continue
var base: int = slot * ENEMY_STRIDE
var pos: Vector2 = Vector2(_data[base + 0], _data[base + 1])
var pf: Dictionary = _pathfinders[eid]
var agent: NavigationAgent2D = pf["agent"]
(pf["host"] as Node2D).global_position = pos
agent.target_position = player_pos
var nxt: Vector2 = agent.get_next_path_position()
var dir: Vector2 = nxt - pos
if dir.length() > 1.0:
dir = dir.normalized()
_data[base + 2] = dir.x * ELITE_SPEED
_data[base + 3] = dir.y * ELITE_SPEED
_data[base + 0] += _data[base + 2] * delta
_data[base + 1] += _data[base + 3] * delta
func get_pathfinder_count() -> int:
return _pathfinders.size()
func despawn_enemy(entity_id: int) -> void:
var slot: int = _entity_index_map.get(entity_id, -1)
if slot < 0:
return
StatusManager.remove_all_for_entity(entity_id)
SpatialGrid.remove_entity(entity_id)
if _pathfinders.has(entity_id):
_unregister_pathfinder(entity_id)
var last: int = _active_count - 1
if slot != last:
var base_slot: int = slot * ENEMY_STRIDE
var base_last: int = last * ENEMY_STRIDE
for s in ENEMY_STRIDE:
_data[base_slot + s] = _data[base_last + s]
var moved_id: int = _slot_entity_map[last]
_entity_index_map[moved_id] = slot
_slot_entity_map[slot] = moved_id
_entity_index_map.erase(entity_id)
_slot_entity_map.erase(last)
_active_count -= 1
## 获取指定 entity_id 的世界坐标(找不到返回 Vector2(-9999,-9999)
func get_pos_by_id(entity_id: int) -> Vector2:
var slot: int = _entity_index_map.get(entity_id, -1)
if slot < 0:
return Vector2(-9999.0, -9999.0)
var base: int = slot * ENEMY_STRIDE
return Vector2(_data[base + 0], _data[base + 1])
## S1 完整伤害接口:从 DamageContextPool 取 context,计算公式后扣 HP
## 公式(S1 简化):final_dmg = base_damage * mult
## S4 起补充 resistance / armor(参见 combat_mechanics_depth.md §4
func apply_damage_from_context(entity_id: int, ctx_id: int) -> bool:
var ctx: DamageContext = DamageContextPool.get_context(ctx_id)
if ctx == null:
return false
var final_dmg: float = ctx.calc_damage()
return apply_damage(entity_id, final_dmg, ctx.owner_id, true)
## 直接扣血接口;source_id 用于击杀归属,record_dps 控制是否计入 DPS
func apply_damage(entity_id: int, damage: float, source_id: int = -1, record_dps: bool = true) -> bool:
var slot: int = _entity_index_map.get(entity_id, -1)
if slot < 0:
return false
var combo_stacks: int = StatusManager.get_stacks(entity_id, StatusID.COMBO_MARK)
var combo_bonus: float = 1.0 + float(combo_stacks) * 0.02 # 每层 +2% 伤害
var base: int = slot * ENEMY_STRIDE
var armor: float = _ARMOR.get(int(_data[base + 6]), 0.0) # 原型护甲,扣减后保底 1
var final_dmg: float = maxf(1.0, damage * combo_bonus - armor)
_data[base + 4] -= final_dmg
if record_dps:
DpsTracker.record_damage(final_dmg)
if _data[base + 4] <= 0.0:
StatusManager.remove_all_for_entity(entity_id)
EventBus.emit(EventID.ENEMY_KILLED, {"entity_id": entity_id, "killer_id": source_id})
despawn_enemy(entity_id)
return true
return false
func has_entity(entity_id: int) -> bool:
return _entity_index_map.has(entity_id)
## 返回敌人血量百分比(权威来源:arch §5.2 get_hp_percent
func get_hp_percent(entity_id: int) -> float:
var slot: int = _entity_index_map.get(entity_id, -1)
if slot < 0:
return 0.0
var base: int = slot * ENEMY_STRIDE
var hp: float = _data[base + 4] # slot +4: hp
var hp_max: float = _data[base + 5] # slot +5: hp_max
return hp / max(hp_max, 0.001)
func get_active_count() -> int:
return _active_count
## 渲染:活跃敌人 SoA 位置 → MultiMesh(按原型设尺寸/颜色,Boss 大且醒目)
func sync_multimesh(mm: MultiMesh) -> void:
if mm == null:
return
var n: int = min(_active_count, mm.instance_count)
mm.visible_instance_count = n
for i in n:
var base: int = i * ENEMY_STRIDE
var etype: int = int(_data[base + 6])
var sz: float = _RENDER_SIZE.get(etype, 14.0)
mm.set_instance_transform_2d(i, Transform2D(
0.0, Vector2(sz, sz), 0.0,
Vector2(_data[base + 0], _data[base + 1])))
mm.set_instance_color(i, _RENDER_COLOR.get(etype, Color(0.9, 0.3, 0.3)))
## 返回最近敌人位置
func get_nearest_pos(origin: Vector2, max_dist: float = 9999.0) -> Vector2:
var best_sq: float = max_dist * max_dist
var best: Vector2 = origin
for i in _active_count:
var base: int = i * ENEMY_STRIDE
var dx: float = _data[base + 0] - origin.x
var dy: float = _data[base + 1] - origin.y
var dsq: float = dx * dx + dy * dy
if dsq < best_sq:
best_sq = dsq
best = Vector2(_data[base + 0], _data[base + 1])
return best
## 建立敌人位置快照(BulletManager homing 共用)
func fill_pos_snapshot(out_arr: PackedVector2Array) -> void:
out_arr.resize(_active_count)
for i in _active_count:
var base: int = i * ENEMY_STRIDE
out_arr[i] = Vector2(_data[base + 0], _data[base + 1])
## 同步 Node2D 显示位置(屏内 dirty sync
func sync_node_positions(enemy_nodes: Array) -> void:
for i in min(_active_count, enemy_nodes.size()):
if _visible_flags[i] == int(1):
var base: int = i * ENEMY_STRIDE
enemy_nodes[i].position = Vector2(_data[base + 0], _data[base + 1])
func reset() -> void:
_active_count = 0
_data.fill(0.0)
_visible_flags.fill(0)
_entity_index_map.clear()
_slot_entity_map.clear()
_next_entity_id = 1
# ADR-A4:释放所有寻路精英节点
for eid in _pathfinders.keys():
(_pathfinders[eid]["host"] as Node).queue_free()
_pathfinders.clear()
_pathfinder_order.clear()