saumwacht-godot/scripts/luneraria/ui/lune_lane_threat.gd
Martin Stickelbroeck 1b540f2a55 G3 (Zwischenstand): neues Ingame-HUD; Projekt wechselt nach Unity
Nicht abgenommen: Nach dem ersten Spieltest wurde entschieden, Champions of
Luneraria nach Unity 6 zu portieren. Dieser Stand ist die Referenz für das
HUD im Unity-Projekt (../luneraria-unity).

- HUD neu aus Komponenten mit eigenem Theme, Porträts aus den echten Modellen
  (einmal gerendert, zwischengespeichert), Lane-Übersicht mit deterministischer
  Bedrohung aus öffentlichem Zustand, Bossgrotte, nächste Welle, Ereignisse,
  Minimap mit zusammengefassten Armeen, Held/Items/Talente, Einheitendetails,
  Angriffschampions, Bereit-Sperre, UI-Größe, Team-Formen statt nur Farben
- Privatsphäre über LuneHudModel: fremdes Gold und fremde Entwürfe erscheinen nie
- Keine Regeländerung: Regeln 91/91, M2 73/73, Netz 43/43; verify_g3 715/715;
  verify_m1 38/38 und verify_m2 14/14 mit angepassten UI-Treibern
- Spieltest bis Welle 8 über die echte Oberfläche, Befunde in G3-HUD.md
- HELDEN-KONZEPT.md: vom Projektinhaber vorgegebene Helden (nicht umgesetzt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:55 +02:00

109 lines
5.5 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name LuneLaneThreat
extends RefCounted
## Deterministic lane threat for one team, read only from public state: building HP, the
## units on the battlefield (positions and HP, which every client also sees) and the public
## announcements of this round. It never reads drafts, gold, unannounced bookings or bot
## notes, so host and guest compute the same level. Positions are projected onto the lane,
## because a network guest has positions but no lane progress.
##
## RUHIG nothing below applies
## UMKÄMPFT a lane tower is below 85 %, an enemy attack champion is announced here, or
## enemy units stand in the own half of the lane
## UNTER DRUCK the Grenzwacht fell or is below 50 %, enemies attack the Grenzwacht, the enemy
## has more than 1.5 times the own unit HP in the own half, or the enemy boss
## is deployed on this lane
## KRITISCH both lane towers fell (base open), the Bastion is below 50 % after the
## Grenzwacht fell, enemies attack the Bastion, or the enemy boss comes while the
## Grenzwacht is already down
enum Level { CALM, CONTESTED, PRESSURE, CRITICAL }
const NAMES: Array[String] = ["RUHIG", "UMKÄMPFT", "UNTER DRUCK", "KRITISCH"]
const GLYPHS: Array[String] = ["check", "melee", "warning", "warning"]
static func _building(state: LuneMatch, id: String) -> Array:
var max_hp: float = float(state.map.building_def(id).get("max_hp", 1.0))
if state.phase == LuneMatch.Phase.COMBAT and state.combat != null and state.combat.buildings.has(id):
var unit: LuneUnit = state.combat.buildings[id]
return [unit.alive, clampf(unit.hp / maxf(1.0, max_hp), 0.0, 1.0) if unit.alive else 0.0]
var hp: float = float(state.building_hp.get(id, 0.0))
return [hp > 0.0, clampf(hp / maxf(1.0, max_hp), 0.0, 1.0)]
static func _percent(value: float) -> String:
return "%d %%" % int(round(value * 100.0))
static func evaluate(state: LuneMatch, team: int, lane: String) -> Dictionary:
var reasons: PackedStringArray = []
var level: int = Level.CALM
var prefix: String = "blau" if team == 0 else "rot"
var enemy: int = 1 - team
var outer: Array = _building(state, "%s_%s_outer" % [prefix, lane])
var inner: Array = _building(state, "%s_%s_inner" % [prefix, lane])
if not outer[0] and not inner[0]:
level = Level.CRITICAL
reasons.append("Grenzwacht und Bastion gefallen – der Weg zur Basis ist offen")
elif not outer[0]:
level = Level.PRESSURE
reasons.append("Grenzwacht gefallen")
if float(inner[1]) < .5:
level = Level.CRITICAL
reasons.append("Bastion bei " + _percent(inner[1]))
elif float(inner[1]) < .85:
reasons.append("Bastion bei " + _percent(inner[1]))
elif float(outer[1]) < .5:
level = Level.PRESSURE
reasons.append("Grenzwacht bei " + _percent(outer[1]))
elif float(outer[1]) < .85:
level = Level.CONTESTED
reasons.append("Grenzwacht bei " + _percent(outer[1]))
for entry: Dictionary in state.announcements():
if int(entry.team) != enemy or str(entry.lane) != lane:
continue
if entry.kind == "boss":
level = maxi(level, Level.CRITICAL if not outer[0] else Level.PRESSURE)
reasons.append("Gegnerische Schlundkrone greift hier an")
else:
level = maxi(level, Level.CONTESTED)
reasons.append("Angriffschampion %s angekündigt" % LuneContent.CHAMPIONS[entry.champion_id].name)
if state.phase == LuneMatch.Phase.COMBAT and state.combat != null:
var battle: Dictionary = battlefield(state, team, lane)
var length: float = float(battle.length)
if int(battle.enemy_count) > 0:
var deepest: float = float(battle.enemy_depth)
var inner_at: float = length * state.balance.inner_tower_fraction
var outer_at: float = length * state.balance.outer_tower_fraction
var reach: float = state.balance.tower_range * .8
if deepest <= inner_at + reach:
level = Level.CRITICAL
reasons.append("Gegner greifen die Bastion oder die Basis an")
elif deepest <= outer_at + reach and outer[0]:
level = maxi(level, Level.PRESSURE)
reasons.append("Gegner greifen die Grenzwacht an")
elif deepest <= length * .5:
level = maxi(level, Level.CONTESTED)
reasons.append("Gegner in der eigenen Lanehälfte")
if float(battle.enemy_hp) > float(battle.own_hp) * 1.5 and deepest <= length * .5:
level = maxi(level, Level.PRESSURE)
reasons.append("Übermacht: %d gegen %d Einheiten" % [int(battle.enemy_count), int(battle.own_count)])
return {"level": level, "name": NAMES[level], "reasons": reasons}
## Units of both sides on one lane: counts, HP sums and the deepest enemy position measured
## from the own base along the lane (from positions only).
static func battlefield(state: LuneMatch, team: int, lane: String) -> Dictionary:
var path: LuneMap.LanePath = state.map.lanes[lane]
var result: Dictionary = {"length": path.length, "own_count": 0, "enemy_count": 0, "own_hp": 0.0, "enemy_hp": 0.0, "enemy_depth": INF, "own_front": 0.0}
if state.combat == null:
return result
for unit: LuneUnit in state.combat.units:
if not unit.alive or not unit.is_mobile() or unit.zone != "lane" or unit.lane != lane or unit.team > 1:
continue
var s: float = path.project(unit.position)
var from_own_base: float = s if team == 0 else path.length - s
if unit.team == team:
result.own_count = int(result.own_count) + 1
result.own_hp = float(result.own_hp) + unit.hp
result.own_front = maxf(float(result.own_front), from_own_base)
else:
result.enemy_count = int(result.enemy_count) + 1
result.enemy_hp = float(result.enemy_hp) + unit.hp
result.enemy_depth = minf(float(result.enemy_depth), from_own_base)
return result