LuneSession hält den autoritativen Matchzustand; Clients senden nur Absichten. Fremde Sitze, veraltete Befehle und Doppelkäufe sind geprüft, Disconnect führt zur Bot-Übernahme statt zum Kernverlust. Exporte und entpackbare Demos 0.16.0-m4 für Linux und Windows.
245 lines
9.3 KiB
GDScript
245 lines
9.3 KiB
GDScript
extends Node3D
|
|
## One Champions of Luneraria match on screen: authoritative LuneMatch, the 3D world,
|
|
## unit views and the HUD. Offline, planning waits for the human; pause and speed are local.
|
|
|
|
signal exit_requested
|
|
signal rematch_requested
|
|
|
|
const World = preload("res://scripts/luneraria/view/lune_world.gd")
|
|
const UnitView = preload("res://scripts/luneraria/view/lune_unit_view.gd")
|
|
const CameraRig = preload("res://scripts/luneraria/view/lune_camera.gd")
|
|
const Hud = preload("res://scripts/luneraria/ui/lune_hud.gd")
|
|
const SPEEDS: Array[int] = [1, 2, 4]
|
|
|
|
var match_state: LuneMatch
|
|
var session: Node
|
|
var world: Node3D
|
|
var camera: Camera3D
|
|
var hud: Control
|
|
var human_slot: int = 1
|
|
var speed_index: int = 0
|
|
var paused: bool = false
|
|
var options: Dictionary = {}
|
|
var _units_root: Node3D
|
|
var _views: Dictionary = {}
|
|
var _accumulator: float = 0.0
|
|
var _last_phase: int = -1
|
|
var _preview: LuneCombat
|
|
var _time: float = 0.0
|
|
var _lane_cycle: int = 0
|
|
|
|
func start(setup_options: Dictionary) -> void:
|
|
options = setup_options
|
|
name = "LuneGame"
|
|
human_slot = int(options.get("human_slot", 1))
|
|
if session != null and is_instance_valid(session) and session.match_state != null:
|
|
match_state = session.match_state
|
|
else:
|
|
match_state = LuneMatch.new()
|
|
match_state.setup(options)
|
|
world = World.new()
|
|
add_child(world)
|
|
world.build(match_state.map)
|
|
_units_root = Node3D.new()
|
|
_units_root.name = "Units"
|
|
add_child(_units_root)
|
|
camera = CameraRig.new()
|
|
camera.reduced_motion = Preferences.reduced_motion
|
|
add_child(camera)
|
|
var layer := CanvasLayer.new()
|
|
add_child(layer)
|
|
hud = Hud.new()
|
|
hud.match_state = match_state
|
|
hud.human_slot = maxi(0, human_slot)
|
|
layer.add_child(hud)
|
|
hud.command_requested.connect(_on_command)
|
|
hud.undo_requested.connect(func() -> void: _feedback(match_state.undo(human_slot)))
|
|
hud.ready_requested.connect(_on_ready)
|
|
hud.starter_requested.connect(_starter)
|
|
hud.speed_requested.connect(func(_v: int) -> void: _cycle_speed())
|
|
hud.pause_requested.connect(_toggle_pause)
|
|
hud.lane_focus_requested.connect(focus_lane)
|
|
hud.map_focus_requested.connect(func(point: Vector3) -> void: camera.look_at_point(point))
|
|
hud.menu_requested.connect(func() -> void: exit_requested.emit())
|
|
hud.rematch_requested.connect(func() -> void: rematch_requested.emit())
|
|
if human_slot >= 0:
|
|
focus_lane(match_state.players[human_slot].lane, true)
|
|
else:
|
|
camera.overview(true)
|
|
_enter_planning()
|
|
|
|
func focus_lane(lane: String, instant: bool = false) -> void:
|
|
var team: int = int(match_state.players[maxi(0, human_slot)].team)
|
|
var path: LuneMap.LanePath = match_state.map.lanes[lane]
|
|
# Planning looks at the own deployment and outer tower, combat at the contested middle.
|
|
var fraction: float = .2 if match_state.phase == LuneMatch.Phase.PLANNING else .42
|
|
var point: Vector2 = match_state.map.lane_point(team, lane, path.length * fraction)
|
|
camera.look_at_point(Vector3(point.x, 0, point.y), 24.0, instant)
|
|
|
|
func _feedback(result: Dictionary) -> void:
|
|
hud.notify(str(result.message), not bool(result.ok))
|
|
Soundscape.play("place" if result.ok else "error")
|
|
_rebuild_preview()
|
|
|
|
func _on_command(command: Dictionary) -> void:
|
|
if session != null and is_instance_valid(session):
|
|
_feedback(session.submit(command))
|
|
else:
|
|
_feedback(match_state.submit(human_slot, command))
|
|
|
|
func _starter() -> void:
|
|
var player: Dictionary = match_state.players[human_slot]
|
|
var ids: Array[String] = match_state.base_ids(player.bund)
|
|
for index: int in [1, 0, 2]:
|
|
var result: Dictionary = match_state.submit(human_slot, {"type": "buy", "type_id": ids[index]})
|
|
if not result.ok:
|
|
_feedback(result)
|
|
return
|
|
_feedback({"ok": true, "message": "Startaufstellung: Front, Nahkampf und Fernkampf gekauft."})
|
|
|
|
func _on_ready() -> void:
|
|
if match_state.phase != LuneMatch.Phase.PLANNING:
|
|
return
|
|
if human_slot >= 0:
|
|
match_state.set_ready(human_slot)
|
|
if match_state.all_ready():
|
|
_start_combat()
|
|
|
|
func _enter_planning() -> void:
|
|
for slot: int in range(6):
|
|
if slot != human_slot:
|
|
LuneBot.plan(match_state, slot)
|
|
_rebuild_preview()
|
|
if human_slot < 0:
|
|
_start_combat()
|
|
|
|
func _start_combat() -> void:
|
|
_clear_views()
|
|
_preview = null
|
|
match_state.begin_combat()
|
|
_accumulator = 0.0
|
|
Soundscape.play("wave")
|
|
|
|
func _clear_views() -> void:
|
|
for view: Node in _units_root.get_children():
|
|
view.free()
|
|
_views.clear()
|
|
|
|
## Shows the human player's current draft army in its formation during planning.
|
|
func _rebuild_preview() -> void:
|
|
if match_state.phase != LuneMatch.Phase.PLANNING or human_slot < 0:
|
|
return
|
|
_clear_views()
|
|
_preview = LuneCombat.new(match_state.map, match_state.balance, match_state.round_number)
|
|
var player: Dictionary = match_state.players[human_slot]
|
|
var per_lane: Dictionary = {}
|
|
var entries: Array = []
|
|
for entry: Dictionary in player.draft.roster:
|
|
entries.append([DataCatalog.get_unit(entry.type_id), str(entry.lane), str(entry.uid), "troop"])
|
|
var hero: Dictionary = player.draft.hero
|
|
if not hero.is_empty():
|
|
entries.append([LuneContent.hero_definition(hero), str(hero.lane), str(hero.uid), "hero"])
|
|
for entry: Array in entries:
|
|
var lane: String = entry[1]
|
|
var index: int = int(per_lane.get(lane, 0))
|
|
per_lane[lane] = index + 1
|
|
var unit: LuneUnit
|
|
if lane == LuneMatch.GROTTO:
|
|
# Grotto orders preview at the team's grotto entrance.
|
|
var at: Vector2 = match_state.map.grotto_entry(int(player.team)) + Vector2(float(index % 3) - 1.0, float(index / 3)) * 1.1
|
|
unit = _preview.spawn_at(entry[3], int(player.team), "grotto", entry[0], at, human_slot, entry[2])
|
|
else:
|
|
unit = _preview.spawn(entry[3], int(player.team), lane, entry[0], maxf(.4, 7.2 - float(index / 4) * 1.5), (float(index % 4) - 1.5) * 1.2, human_slot, entry[2])
|
|
_add_view(unit)
|
|
|
|
func _add_view(unit: LuneUnit) -> void:
|
|
var view: Node3D = UnitView.new()
|
|
_units_root.add_child(view)
|
|
view.bind(unit)
|
|
_views[unit.id] = view
|
|
|
|
func _cycle_speed() -> void:
|
|
speed_index = (speed_index + 1) % SPEEDS.size()
|
|
hud.set_speed_label(SPEEDS[speed_index], paused)
|
|
|
|
func _toggle_pause() -> void:
|
|
if match_state.phase == LuneMatch.Phase.COMBAT:
|
|
paused = not paused
|
|
hud.set_speed_label(SPEEDS[speed_index], paused)
|
|
|
|
func _process(delta: float) -> void:
|
|
_time += delta
|
|
var step: float = match_state.balance.sim_step
|
|
if match_state.phase == LuneMatch.Phase.COMBAT and not paused:
|
|
_accumulator += minf(delta, .1) * SPEEDS[speed_index]
|
|
while _accumulator >= step and match_state.phase == LuneMatch.Phase.COMBAT:
|
|
match_state.step_combat()
|
|
_accumulator -= step
|
|
if match_state.phase == LuneMatch.Phase.COMBAT and match_state.combat != null:
|
|
for unit: LuneUnit in match_state.combat.units:
|
|
if unit.is_mobile() and not _views.has(unit.id):
|
|
_add_view(unit)
|
|
if match_state.phase != _last_phase:
|
|
var previous: int = _last_phase
|
|
_last_phase = match_state.phase
|
|
if previous == LuneMatch.Phase.COMBAT:
|
|
_after_combat()
|
|
var alpha: float = _accumulator / step if match_state.phase == LuneMatch.Phase.COMBAT else 1.0
|
|
var sim_delta: float = 0.0 if paused else delta * SPEEDS[speed_index]
|
|
for view: Node3D in _views.values():
|
|
view.sync(alpha, sim_delta if match_state.phase == LuneMatch.Phase.COMBAT else delta)
|
|
var hp: Dictionary = match_state.building_hp.duplicate()
|
|
if match_state.phase == LuneMatch.Phase.COMBAT and match_state.combat != null:
|
|
for id: String in match_state.combat.buildings:
|
|
hp[id] = (match_state.combat.buildings[id] as LuneUnit).hp
|
|
var max_hp: Dictionary = {}
|
|
for definition: Dictionary in match_state.map.buildings:
|
|
max_hp[definition.id] = definition.max_hp
|
|
world.sync_buildings(hp, max_hp, _time, delta)
|
|
var claim: Dictionary = match_state.boss.claim
|
|
world.sync_grotto(match_state.grotto_available(), int(claim.team) if not claim.is_empty() else -1)
|
|
hud.minimap.camera_focus = camera.focus
|
|
hud.minimap.camera_span = camera.zoom
|
|
|
|
func _after_combat() -> void:
|
|
if match_state.phase == LuneMatch.Phase.OVER:
|
|
paused = false
|
|
var won: bool = match_state.winner == int(match_state.players[maxi(0, human_slot)].team)
|
|
Soundscape.play("victory" if won else ("settle" if match_state.winner == LuneMatch.DRAW else "defeat"))
|
|
camera.look_at_point(Vector3(match_state.map.core_position(0 if match_state.combat.destroyed_cores.has(0) else 1).x, 0, match_state.map.core_position(0 if match_state.combat.destroyed_cores.has(0) else 1).y), 26.0)
|
|
return
|
|
Soundscape.play("settle")
|
|
_enter_planning()
|
|
|
|
var input_blocked: Callable = func() -> bool: return false
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not input_blocked.call() and camera.handle_world_input(event):
|
|
get_viewport().set_input_as_handled()
|
|
|
|
## Game keys run before the GUI so Space/Tab keep their meaning even when a button has focus.
|
|
func _input(event: InputEvent) -> void:
|
|
if input_blocked.call() or not (event is InputEventKey and event.pressed and not event.echo):
|
|
return
|
|
var handled: bool = true
|
|
match event.keycode:
|
|
KEY_SPACE:
|
|
if match_state.phase == LuneMatch.Phase.PLANNING:
|
|
_on_ready()
|
|
else:
|
|
_toggle_pause()
|
|
KEY_P:
|
|
_toggle_pause()
|
|
KEY_TAB:
|
|
_lane_cycle = (_lane_cycle + 1) % 3
|
|
focus_lane(LuneMap.LANES[_lane_cycle])
|
|
KEY_O:
|
|
camera.overview()
|
|
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5:
|
|
if match_state.phase == LuneMatch.Phase.PLANNING and human_slot >= 0:
|
|
var ids: Array[String] = match_state.base_ids(match_state.players[human_slot].bund)
|
|
_on_command({"type": "buy", "type_id": ids[event.keycode - KEY_1]})
|
|
_:
|
|
handled = false
|
|
if handled:
|
|
get_viewport().set_input_as_handled()
|