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>
109 lines
5.5 KiB
GDScript
109 lines
5.5 KiB
GDScript
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
|