Hauptszene scenes/luneraria.tscn, Saumwacht bleibt über --legacy und die alten QA-Schalter erreichbar. Karte aus Bestandsassets plus neuem Turm, HUD mit Planung, Lanes und Minimap, Menü umbenannt, Einstellungen werden aus dem alten Benutzerordner übernommen. Simulation 6,6x schneller (Raster, Caches, faire Staffelung) und exakt spiegelsymmetrisch. Regeln 91/91, echte Anwendung 38/38, Legacy unverändert grün. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
525 lines
21 KiB
GDScript
525 lines
21 KiB
GDScript
class_name LuneCombat
|
|
extends RefCounted
|
|
## One automatic combat phase on all three lanes. Every step first gathers targets,
|
|
## movement, hits, heals and effects from the state at the start of the step, then applies
|
|
## them together, so processing order never decides who strikes first. Deaths of a step
|
|
## resolve together; two Ancients falling in the same step is a draw.
|
|
|
|
var map: LuneMap
|
|
var balance: LuneBalance
|
|
var round_number: int = 1
|
|
var units: Array[LuneUnit] = []
|
|
var buildings: Dictionary = {}
|
|
var elapsed: float = 0.0
|
|
var steps: int = 0
|
|
var finished: bool = false
|
|
var end_reason: String = ""
|
|
var destroyed_cores: Array[int] = []
|
|
var events: Array[Dictionary] = []
|
|
var _next_id: int = 1
|
|
var _team_count: Array[int] = [0, 0]
|
|
## Units without a target look for one every RETARGET steps, staggered by team_index.
|
|
const RETARGET: int = 4
|
|
var _event_serial: int = 0
|
|
var _pending_damage: Dictionary = {}
|
|
var _pending_heal: Dictionary = {}
|
|
var _pending_effects: Array = []
|
|
var _pending_dots: Array = []
|
|
var _pending_shields: Array = []
|
|
# Per-step caches: building gates, march limits and a spatial grid of mobile units.
|
|
var _attackable: Dictionary = {}
|
|
var _limits: Dictionary = {}
|
|
var _grid: Dictionary = {}
|
|
const CELL: float = 4.0
|
|
## Optional section timings (microseconds) for performance work; off by default.
|
|
var profiling: bool = false
|
|
var profile: Dictionary = {}
|
|
var _mark: int = 0
|
|
|
|
func _lap(section: String) -> void:
|
|
if profiling:
|
|
var now: int = Time.get_ticks_usec()
|
|
profile[section] = int(profile.get(section, 0)) + now - _mark
|
|
_mark = now
|
|
|
|
func _init(match_map: LuneMap, config: LuneBalance, number: int) -> void:
|
|
map = match_map
|
|
balance = config
|
|
round_number = number
|
|
|
|
# ---------------------------------------------------------------- setup
|
|
func add_building(definition: Dictionary, hp: float) -> LuneUnit:
|
|
var unit := LuneUnit.new()
|
|
unit.id = _next_id
|
|
_next_id += 1
|
|
unit.kind = "core" if definition.tier == "core" else "tower"
|
|
unit.team = int(definition.team)
|
|
unit.lane = str(definition.lane)
|
|
unit.building_id = str(definition.id)
|
|
unit.team_index = _team_count[unit.team]
|
|
_team_count[unit.team] += 1
|
|
unit.def = {"id": definition.id, "display_name": definition.id, "max_hp": definition.max_hp, "damage": definition.damage,
|
|
"attack_range": definition.range, "attack_type": 1, "armor_type": 1, "speed": 0.0, "ability": "", "bounty": definition.bounty,
|
|
"tier": definition.tier}
|
|
unit.position = definition.position
|
|
unit.previous_position = unit.position
|
|
unit.max_hp = float(definition.max_hp)
|
|
unit.hp = clampf(hp, 0.0, unit.max_hp)
|
|
unit.alive = unit.hp > 0.0
|
|
units.append(unit)
|
|
buildings[unit.building_id] = unit
|
|
return unit
|
|
|
|
func spawn(kind: String, team: int, lane: String, definition: Dictionary, progress: float, lateral: float, slot: int = -1, uid: String = "") -> LuneUnit:
|
|
var unit := LuneUnit.new()
|
|
unit.id = _next_id
|
|
_next_id += 1
|
|
unit.kind = kind
|
|
unit.team = team
|
|
unit.lane = lane
|
|
unit.def = definition
|
|
unit.owner_slot = slot
|
|
unit.persistent_uid = uid
|
|
unit.team_index = _team_count[team]
|
|
_team_count[team] += 1
|
|
unit.progress = progress
|
|
unit.lateral = lateral
|
|
unit.position = map.lane_point(team, lane, progress, lateral)
|
|
unit.previous_position = unit.position
|
|
var forward: Vector2 = map.lane_forward(team, lane, progress)
|
|
unit.facing = atan2(forward.x, forward.y)
|
|
unit.max_hp = float(definition.get("max_hp", 1.0))
|
|
unit.hp = unit.max_hp
|
|
_start_ability(unit)
|
|
units.append(unit)
|
|
return unit
|
|
|
|
func alive_map() -> Dictionary:
|
|
var result: Dictionary = {}
|
|
for id: String in buildings:
|
|
result[id] = (buildings[id] as LuneUnit).alive
|
|
return result
|
|
|
|
func building_attackable(unit: LuneUnit, gates: Dictionary) -> bool:
|
|
if not unit.alive:
|
|
return false
|
|
if _attackable.has(unit.building_id):
|
|
return _attackable[unit.building_id]
|
|
return LuneMap.is_attackable(map.building_def(unit.building_id), gates)
|
|
|
|
func _prepare_caches(living: Array[LuneUnit], gates: Dictionary) -> void:
|
|
_attackable.clear()
|
|
for id: String in buildings:
|
|
_attackable[id] = LuneMap.is_attackable(map.building_def(id), gates)
|
|
_limits.clear()
|
|
for team: int in range(2):
|
|
for lane: String in LuneMap.LANES:
|
|
var path: LuneMap.LanePath = map.lanes[lane]
|
|
var limit: float = path.length
|
|
for id: String in buildings:
|
|
var building: LuneUnit = buildings[id]
|
|
if building.alive and building.kind == "tower" and building.team != team and building.lane == lane:
|
|
limit = minf(limit, path.length - float(map.building_def(id).progress) - 1.5)
|
|
_limits[team * 3 + LuneMap.LANES.find(lane)] = limit
|
|
_grid = {0: {}, 1: {}}
|
|
for unit: LuneUnit in living:
|
|
if unit.is_mobile():
|
|
var cells: Dictionary = _grid[unit.team]
|
|
var key := Vector2i(floori(unit.position.x / CELL), floori(unit.position.y / CELL))
|
|
if not cells.has(key):
|
|
cells[key] = []
|
|
(cells[key] as Array).append(unit)
|
|
|
|
## Mobile units of one team (or both with team = -1) near a point; the caller checks distance.
|
|
func _near_mobile(center: Vector2, radius: float, team: int = -1) -> Array:
|
|
var result: Array = []
|
|
var reach: int = ceili(radius / CELL)
|
|
var cx: int = floori(center.x / CELL)
|
|
var cy: int = floori(center.y / CELL)
|
|
for side: int in ([0, 1] if team < 0 else [team]):
|
|
var cells: Dictionary = _grid[side]
|
|
if cells.is_empty():
|
|
continue
|
|
for dx: int in range(-reach, reach + 1):
|
|
for dy: int in range(-reach, reach + 1):
|
|
var bucket: Variant = cells.get(Vector2i(cx + dx, cy + dy))
|
|
if bucket != null:
|
|
result.append_array(bucket)
|
|
return result
|
|
|
|
# ---------------------------------------------------------------- step
|
|
func step() -> void:
|
|
if finished:
|
|
return
|
|
var dt: float = balance.sim_step
|
|
elapsed += dt
|
|
steps += 1
|
|
_pending_damage.clear()
|
|
_pending_heal.clear()
|
|
_pending_effects.clear()
|
|
_pending_dots.clear()
|
|
_pending_shields.clear()
|
|
_mark = Time.get_ticks_usec()
|
|
var gates: Dictionary = alive_map()
|
|
var living: Array[LuneUnit] = []
|
|
for unit: LuneUnit in units:
|
|
if unit.alive:
|
|
living.append(unit)
|
|
unit.previous_position = unit.position
|
|
_prepare_caches(living, gates)
|
|
_lap("caches")
|
|
for unit: LuneUnit in living:
|
|
var dot: float = unit.advance_status(dt)
|
|
if dot > 0.0:
|
|
_queue_damage(unit, dot, gates)
|
|
_lap("status")
|
|
for unit: LuneUnit in living:
|
|
if unit.is_mobile():
|
|
_ability_step(unit, living)
|
|
_lap("abilities")
|
|
for unit: LuneUnit in living:
|
|
if unit.is_mobile() or (unit.kind == "tower"):
|
|
var keep: bool = unit.target != null and unit.target.alive
|
|
if keep or (steps + unit.team_index) % RETARGET == 0:
|
|
unit.target = _choose_target(unit, living, gates)
|
|
_lap("targets")
|
|
var moves: Dictionary = {}
|
|
for unit: LuneUnit in living:
|
|
if unit.is_mobile():
|
|
moves[unit.id] = _plan_move(unit, living, dt)
|
|
_lap("plan_move")
|
|
for unit: LuneUnit in living:
|
|
if moves.has(unit.id):
|
|
var plan: Array = moves[unit.id]
|
|
var next: Vector2 = plan[0]
|
|
var heading: Vector2 = next - unit.position
|
|
unit.position = next
|
|
if heading.length_squared() > 0.000001:
|
|
unit.facing = atan2(heading.x, heading.y)
|
|
if unit.zone == "lane":
|
|
_update_progress(unit, float(plan[1]))
|
|
_lap("apply_move")
|
|
for unit: LuneUnit in living:
|
|
_attack(unit, living, gates, dt)
|
|
_lap("attacks")
|
|
for unit: LuneUnit in living:
|
|
if _pending_heal.has(unit.id):
|
|
unit.heal(_pending_heal[unit.id])
|
|
for entry: Array in _pending_shields:
|
|
(entry[0] as LuneUnit).grant_shield(entry[1], entry[2])
|
|
for entry: Array in _pending_effects:
|
|
(entry[0] as LuneUnit).apply_effect(entry[1], entry[2], entry[3], entry[4])
|
|
for entry: Array in _pending_dots:
|
|
(entry[0] as LuneUnit).apply_dot(entry[1], entry[2], entry[3], entry[4])
|
|
for unit: LuneUnit in living:
|
|
if _pending_damage.has(unit.id):
|
|
unit.take(_pending_damage[unit.id])
|
|
for unit: LuneUnit in living:
|
|
if unit.hp <= 0.0 and unit.alive:
|
|
unit.alive = false
|
|
unit.death_time = elapsed
|
|
unit.target = null
|
|
_record_death(unit)
|
|
_lap("resolve")
|
|
_check_end()
|
|
|
|
func _check_end() -> void:
|
|
if not destroyed_cores.is_empty():
|
|
finished = true
|
|
end_reason = "core"
|
|
return
|
|
var mobile_alive: bool = false
|
|
for unit: LuneUnit in units:
|
|
if unit.alive and unit.is_mobile():
|
|
mobile_alive = true
|
|
break
|
|
if not mobile_alive:
|
|
finished = true
|
|
end_reason = "cleared"
|
|
elif elapsed >= balance.combat_time_limit - 0.000001:
|
|
finished = true
|
|
end_reason = "timeout"
|
|
|
|
func _record_death(unit: LuneUnit) -> void:
|
|
_event_serial += 1
|
|
var event: Dictionary = {"serial": _event_serial, "step": steps, "time": elapsed, "team": unit.team, "lane": unit.lane,
|
|
"zone": unit.zone, "kind": unit.kind, "unit_id": unit.id, "def_id": str(unit.def.get("id", "")), "bounty": 0,
|
|
"owner_slot": unit.owner_slot, "persistent_uid": unit.persistent_uid}
|
|
if unit.kind in ["monster", "champion", "boss"]:
|
|
event.bounty = int(unit.def.get("bounty", 0))
|
|
elif unit.is_building():
|
|
event.bounty = int(unit.def.get("bounty", 0))
|
|
event.building_id = unit.building_id
|
|
if unit.kind == "core" and not destroyed_cores.has(unit.team):
|
|
destroyed_cores.append(unit.team)
|
|
events.append(event)
|
|
|
|
# ---------------------------------------------------------------- targeting and movement
|
|
func _hostile(unit: LuneUnit, other: LuneUnit) -> bool:
|
|
return other.alive and other.team != unit.team and other.zone == unit.zone
|
|
|
|
func _choose_target(unit: LuneUnit, living: Array[LuneUnit], gates: Dictionary) -> LuneUnit:
|
|
var current: LuneUnit = unit.target
|
|
if current != null and _hostile(unit, current) and unit.position.distance_to(current.position) <= balance.leash_radius:
|
|
if not current.is_building() or building_attackable(current, gates):
|
|
if unit.kind != "tower" or unit.position.distance_to(current.position) <= unit.attack_range():
|
|
return current
|
|
var radius: float = unit.attack_range() if unit.kind == "tower" else balance.aggro_radius
|
|
var best: LuneUnit = null
|
|
var best_distance: float = INF
|
|
for other: LuneUnit in _near_mobile(unit.position, radius, 1 - unit.team):
|
|
if not _hostile(unit, other):
|
|
continue
|
|
var d: float = unit.position.distance_to(other.position)
|
|
if d <= radius and (d < best_distance - 0.0001 or (absf(d - best_distance) <= 0.0001 and other.id < best.id)):
|
|
best = other
|
|
best_distance = d
|
|
if best != null or unit.kind == "tower":
|
|
return best
|
|
# Buildings: nearby attackable ones; at the end of a lane the whole base is in reach.
|
|
for id: String in buildings:
|
|
var other: LuneUnit = buildings[id]
|
|
if not _hostile(unit, other) or not building_attackable(other, gates):
|
|
continue
|
|
var d: float = unit.position.distance_to(other.position) - balance.building_radius
|
|
if (d <= radius or unit.reached_base) and d < best_distance:
|
|
best = other
|
|
best_distance = d
|
|
return best
|
|
|
|
func _march_limit(unit: LuneUnit) -> float:
|
|
var key: int = unit.team * 3 + LuneMap.LANES.find(unit.lane)
|
|
if _limits.has(key):
|
|
return _limits[key]
|
|
var path: LuneMap.LanePath = map.lanes[unit.lane]
|
|
var limit: float = path.length
|
|
for id: String in buildings:
|
|
var building: LuneUnit = buildings[id]
|
|
if building.alive and building.kind == "tower" and building.team != unit.team and building.lane == unit.lane:
|
|
limit = minf(limit, path.length - float(map.building_def(id).progress) - 1.5)
|
|
return limit
|
|
|
|
## Returns [next position, marching progress]. Marching advances the lane parameter
|
|
## directly, so lateral formation offsets cannot stall a unit on the inside of a bend.
|
|
func _plan_move(unit: LuneUnit, living: Array[LuneUnit], dt: float) -> Array:
|
|
var step_length: float = unit.speed() * balance.march_speed_factor * unit.movement_factor() * dt
|
|
var goal: Vector2 = unit.position
|
|
var travel: float = 0.0
|
|
var marched: float = unit.progress
|
|
if unit.target != null:
|
|
var reach: float = unit.attack_range() + (balance.building_radius if unit.target.is_building() else 0.0)
|
|
var gap: float = unit.position.distance_to(unit.target.position) - reach
|
|
if gap > 0.0:
|
|
goal = unit.target.position
|
|
travel = minf(step_length, gap + 0.02)
|
|
elif unit.zone == "lane":
|
|
var limit: float = _march_limit(unit)
|
|
var anchor: Vector2 = map.lane_point(unit.team, unit.lane, unit.progress, unit.lateral)
|
|
var off: float = unit.position.distance_to(anchor)
|
|
if off > 1.2:
|
|
# Rejoin the formation line after a chase before advancing again.
|
|
goal = anchor
|
|
travel = minf(step_length, unit.position.distance_to(anchor))
|
|
elif unit.progress < limit - 0.01:
|
|
marched = minf(unit.progress + step_length, limit)
|
|
goal = map.lane_point(unit.team, unit.lane, marched, unit.lateral)
|
|
travel = unit.position.distance_to(goal)
|
|
elif off > 0.05:
|
|
goal = anchor
|
|
travel = minf(step_length, unit.position.distance_to(anchor))
|
|
var next: Vector2 = unit.position
|
|
if travel > 0.0 and goal.distance_squared_to(unit.position) > 0.000001:
|
|
next += (goal - unit.position).normalized() * minf(travel, unit.position.distance_to(goal))
|
|
# Separation from allies so bodies do not stack; opponents may still close to melee range.
|
|
var push := Vector2.ZERO
|
|
for other: LuneUnit in _near_mobile(unit.position, 1.1, unit.team):
|
|
if other == unit or other.zone != unit.zone:
|
|
continue
|
|
var offset: Vector2 = unit.position - other.position
|
|
var distance: float = offset.length()
|
|
if distance < 1.1:
|
|
if distance < 0.0001:
|
|
# Mirrored teams sidestep in mirrored directions.
|
|
offset = Vector2.RIGHT if unit.id < other.id else Vector2.LEFT
|
|
if unit.team == 1:
|
|
offset = LuneMap.mirror(offset)
|
|
distance = 0.001
|
|
push += offset.normalized() * (1.1 - distance) * 2.0
|
|
next += push.limit_length(0.9) * dt
|
|
next.x = clampf(next.x, -LuneMap.HALF_EXTENT, LuneMap.HALF_EXTENT)
|
|
next.y = clampf(next.y, -LuneMap.HALF_EXTENT, LuneMap.HALF_EXTENT)
|
|
return [next, marched]
|
|
|
|
func _update_progress(unit: LuneUnit, marched: float) -> void:
|
|
var path: LuneMap.LanePath = map.lanes[unit.lane]
|
|
var limit: float = _march_limit(unit)
|
|
if unit.target != null:
|
|
# After a chase the unit continues from where it now stands, never further back.
|
|
var blue: Vector2 = unit.position if unit.team == 0 else LuneMap.mirror(unit.position)
|
|
marched = maxf(marched, minf(path.project(blue), limit))
|
|
unit.progress = maxf(unit.progress, minf(marched, maxf(limit, unit.progress)))
|
|
if unit.progress >= path.length - 0.6:
|
|
unit.reached_base = true
|
|
|
|
# ---------------------------------------------------------------- attacks
|
|
func _attack(unit: LuneUnit, living: Array[LuneUnit], gates: Dictionary, dt: float) -> void:
|
|
unit.in_range = false
|
|
var target: LuneUnit = unit.target
|
|
if target == null or not target.alive or unit.kind == "core":
|
|
unit.attack_progress = 0.0
|
|
return
|
|
var reach: float = unit.attack_range() + (balance.building_radius if target.is_building() else 0.0) + 0.035
|
|
if unit.position.distance_to(target.position) > reach:
|
|
unit.attack_progress = 0.0
|
|
return
|
|
if target.is_building() and not building_attackable(target, gates):
|
|
unit.attack_progress = 0.0
|
|
return
|
|
unit.in_range = true
|
|
var interval: float = balance.tower_attack_interval if unit.kind == "tower" else 1.0
|
|
unit.attack_progress += dt * unit.attack_speed_factor() / interval
|
|
while unit.attack_progress >= 1.0 - 0.000001:
|
|
unit.attack_progress = maxf(0.0, unit.attack_progress - 1.0)
|
|
unit.hit_count += 1
|
|
unit.attack_serial += 1
|
|
var damage: float = float(unit.def.get("damage", 0.0)) * (1.0 + unit.damage_bonus())
|
|
if unit.hit_count % 4 == 0:
|
|
if unit.ability() == "g05":
|
|
damage *= 1.5
|
|
elif unit.ability() == "g06":
|
|
damage *= 1.75
|
|
if target.is_building():
|
|
damage *= float(unit.def.get("siege_factor", 1.0))
|
|
_queue_damage(target, target.mitigated(damage, int(unit.def.get("attack_type", 0))), gates)
|
|
_on_hit(unit, target, living, gates)
|
|
|
|
func _queue_damage(target: LuneUnit, amount: float, gates: Dictionary) -> void:
|
|
if amount <= 0.0 or not target.alive:
|
|
return
|
|
# The building gate applies to every source: ranged, splash, DoT and abilities.
|
|
if target.is_building() and not building_attackable(target, gates):
|
|
return
|
|
_pending_damage[target.id] = float(_pending_damage.get(target.id, 0.0)) + amount
|
|
|
|
func _queue_heal(target: LuneUnit, amount: float) -> void:
|
|
_pending_heal[target.id] = float(_pending_heal.get(target.id, 0.0)) + amount
|
|
|
|
# ---------------------------------------------------------------- abilities (ported from the Saumwacht rules)
|
|
func _start_ability(unit: LuneUnit) -> void:
|
|
unit.ability_ready = 6.0
|
|
match unit.ability():
|
|
"w03", "w04": unit.ability_ready = 5.0
|
|
"w09", "w10": unit.ability_ready = 4.0
|
|
"s02": unit.grant_shield(unit.max_hp * 0.15)
|
|
"e08": unit.grant_shield(unit.max_hp * 0.20)
|
|
|
|
func _nearby(unit: LuneUnit, living: Array[LuneUnit], allied: bool, center: Vector2, radius: float, maximum: int, excluded: LuneUnit = null, health_priority: bool = false) -> Array[LuneUnit]:
|
|
var result: Array[LuneUnit] = []
|
|
# Buildings can be splash victims; they are few, so check them directly.
|
|
var pool: Array = _near_mobile(center, radius, unit.team if allied else 1 - unit.team)
|
|
if not allied:
|
|
for id: String in buildings:
|
|
pool.append(buildings[id])
|
|
for other: LuneUnit in pool:
|
|
if not other.alive or other == excluded or other.zone != unit.zone:
|
|
continue
|
|
if (other.team == unit.team) != allied:
|
|
continue
|
|
if allied and other.is_building():
|
|
continue
|
|
if other.position.distance_to(center) <= radius + 0.000001:
|
|
result.append(other)
|
|
result.sort_custom(func(a: LuneUnit, b: LuneUnit) -> bool:
|
|
if health_priority and not is_equal_approx(a.hp / a.max_hp, b.hp / b.max_hp):
|
|
return a.hp / a.max_hp < b.hp / b.max_hp
|
|
var ad: float = a.position.distance_to(center)
|
|
var bd: float = b.position.distance_to(center)
|
|
return a.id < b.id if is_equal_approx(ad, bd) else ad < bd)
|
|
if result.size() > maximum:
|
|
result.resize(maximum)
|
|
return result
|
|
|
|
func _ability_step(unit: LuneUnit, living: Array[LuneUnit]) -> void:
|
|
if unit.combat_time + 0.000001 < unit.ability_ready:
|
|
return
|
|
var ability: String = unit.ability()
|
|
var success: bool = false
|
|
match ability:
|
|
"w03", "w04":
|
|
if unit.hp < unit.max_hp:
|
|
_queue_heal(unit, 100.0 if ability == "w04" else 35.0)
|
|
success = true
|
|
"w09", "w10":
|
|
var upgraded: bool = ability == "w10"
|
|
for ally: LuneUnit in _nearby(unit, living, true, unit.position, 4.5 if upgraded else 4.0, 3 if upgraded else 1, null, true):
|
|
if ally.hp < ally.max_hp:
|
|
_queue_heal(ally, 80.0 if upgraded else 65.0)
|
|
success = true
|
|
"g09", "g10":
|
|
var upgraded: bool = ability == "g10"
|
|
var allies: Array[LuneUnit] = _nearby(unit, living, true, unit.position, 4.0 if upgraded else 3.0, 4 if upgraded else 3, null, true)
|
|
for ally: LuneUnit in allies:
|
|
_pending_effects.append([ally, "hearth_warmth", "damage", 0.15 if upgraded else 0.10, 4.0])
|
|
if upgraded:
|
|
_queue_heal(ally, 45.0)
|
|
success = not allies.is_empty()
|
|
"e06":
|
|
for ally: LuneUnit in _nearby(unit, living, true, unit.position, 4.0, 100, null, true):
|
|
if ally.hp < ally.max_hp:
|
|
_queue_heal(ally, 30.0 * float(unit.def.get("hp_factor", 1.0)))
|
|
success = true
|
|
break
|
|
"s03", "e09":
|
|
var victims: Array[LuneUnit] = _nearby(unit, living, false, unit.position, 6.0 if ability == "e09" else 3.0, 3 if ability == "e09" else 4)
|
|
for victim: LuneUnit in victims:
|
|
_pending_effects.append([victim, "dissonance", "attack_slow", 0.15, 3.0])
|
|
success = not victims.is_empty()
|
|
"s05":
|
|
var victims: Array[LuneUnit] = _nearby(unit, living, false, unit.position, 5.0, 1)
|
|
for victim: LuneUnit in victims:
|
|
_pending_effects.append([victim, "rift_wound", "healing_reduction", 0.30, 4.0])
|
|
success = not victims.is_empty()
|
|
if success:
|
|
unit.ability_ready = unit.combat_time + (5.0 if ability in ["w03", "w04"] else (4.0 if ability in ["w09", "w10"] else 6.0))
|
|
unit.ability_serial += 1
|
|
|
|
func _on_hit(unit: LuneUnit, primary: LuneUnit, living: Array[LuneUnit], gates: Dictionary) -> void:
|
|
var ability: String = unit.ability()
|
|
var hits: int = unit.hit_count
|
|
match ability:
|
|
"w01", "w02":
|
|
if hits % 3 == 0:
|
|
_pending_effects.append([primary, "root_grasp", "movement_slow", 0.30 if ability == "w02" else 0.20, 3.0 if ability == "w02" else 2.0])
|
|
if ability == "w02":
|
|
_pending_effects.append([primary, "root_grasp_attack", "attack_slow", 0.10, 3.0])
|
|
"w05", "w06":
|
|
if hits % 4 == 0:
|
|
_pending_dots.append([primary, "seed_burn", 10.0 if ability == "w06" else 3.0, 4.0, int(unit.def.attack_type)])
|
|
"w07", "w08":
|
|
if hits % 3 == 0:
|
|
_splash(unit, primary, living, gates, 0.80 if ability == "w08" else 0.60, 2.5 if ability == "w08" else 2.0, 4 if ability == "w08" else 3)
|
|
"g01", "g02":
|
|
if hits % 3 == 0:
|
|
_pending_dots.append([primary, "burn", 12.0 if ability == "g02" else 4.0, 3.0, int(unit.def.attack_type)])
|
|
"g07": _splash(unit, primary, living, gates, 0.30, 1.5, 3)
|
|
"g08": _splash(unit, primary, living, gates, 0.40, 2.0, 4)
|
|
"e05":
|
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.40, 1.5, 2)
|
|
"e07":
|
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.60, 2.0, 3)
|
|
"e10":
|
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.60, 2.5, 4)
|
|
"s04":
|
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.50, 2.0, 3)
|
|
|
|
func _splash(unit: LuneUnit, primary: LuneUnit, living: Array[LuneUnit], gates: Dictionary, factor: float, radius: float, maximum: int) -> void:
|
|
for victim: LuneUnit in _nearby(unit, living, false, primary.position, radius, maximum, primary):
|
|
_queue_damage(victim, victim.mitigated(float(unit.def.damage) * factor, int(unit.def.attack_type)), gates)
|
|
|
|
# ---------------------------------------------------------------- queries for views and tests
|
|
func mobile_count(team: int = -1) -> int:
|
|
var count: int = 0
|
|
for unit: LuneUnit in units:
|
|
if unit.alive and unit.is_mobile() and (team < 0 or unit.team == team):
|
|
count += 1
|
|
return count
|