Reorganized files, started splitting up unit
This commit is contained in:
16
scripts/battle/combat_engine/combat_proposal.gd
Normal file
16
scripts/battle/combat_engine/combat_proposal.gd
Normal file
@@ -0,0 +1,16 @@
|
||||
class_name CombatProposal extends Resource
|
||||
|
||||
class CombatantStats:
|
||||
var unit: Unit
|
||||
var max_hp: int
|
||||
var hp: int
|
||||
var sp: int
|
||||
var hit: int
|
||||
var atk: int
|
||||
var def: int
|
||||
var spd: int
|
||||
var available_tactics: Array[CombatTactic] = []
|
||||
var selected_tactic: CombatTactic
|
||||
|
||||
var attacker: CombatantStats
|
||||
var defender: CombatantStats
|
||||
1
scripts/battle/combat_engine/combat_proposal.gd.uid
Normal file
1
scripts/battle/combat_engine/combat_proposal.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4oatflqabi37
|
||||
141
scripts/battle/combat_engine/combat_system.gd
Normal file
141
scripts/battle/combat_engine/combat_system.gd
Normal file
@@ -0,0 +1,141 @@
|
||||
class_name CombatSystem extends Node
|
||||
|
||||
func create_proposal(attacker: Unit, defender: Unit, distance: int) -> CombatProposal:
|
||||
var proposal := CombatProposal.new()
|
||||
|
||||
var atk_tactics := _filter_tactics(attacker, distance)
|
||||
var def_tactics := _filter_tactics(defender, distance)
|
||||
|
||||
var atk_tactic := _find_default_attack(atk_tactics)
|
||||
var def_tactic := _find_default_attack(def_tactics)
|
||||
|
||||
# AI auto-selects for non-player units
|
||||
if not _is_player_controlled(defender):
|
||||
def_tactic = select_ai_tactic(defender, attacker, def_tactics)
|
||||
|
||||
proposal.attacker = _snapshot(attacker, defender, atk_tactics, atk_tactic, def_tactic)
|
||||
proposal.defender = _snapshot(defender, attacker, def_tactics, def_tactic, atk_tactic)
|
||||
|
||||
return proposal
|
||||
|
||||
|
||||
func _filter_tactics(unit: Unit, distance: int) -> Array[CombatTactic]:
|
||||
var valid: Array[CombatTactic] = []
|
||||
for tactic in unit.tactics:
|
||||
if tactic.tactic_range and tactic.tactic_range.is_valid_range(distance, unit):
|
||||
valid.append(tactic)
|
||||
return valid
|
||||
|
||||
|
||||
func _find_default_attack(tactics: Array[CombatTactic]) -> CombatTactic:
|
||||
for tactic in tactics:
|
||||
if tactic is AttackCombatTactic:
|
||||
return tactic
|
||||
return tactics[0] if tactics.size() > 0 else null
|
||||
|
||||
|
||||
func _snapshot(unit: Unit, opponent: Unit, available: Array[CombatTactic], selected: CombatTactic, opponent_selected: CombatTactic) -> CombatProposal.CombatantStats:
|
||||
var stats := CombatProposal.CombatantStats.new()
|
||||
stats.unit = unit
|
||||
stats.max_hp = unit.current_stats.max_hp
|
||||
stats.hp = unit.current_stats.current_hp
|
||||
stats.sp = unit.current_stats.current_sp
|
||||
stats.spd = unit.current_stats.spd
|
||||
stats.available_tactics = available
|
||||
stats.selected_tactic = selected
|
||||
|
||||
if selected and selected.deals_damage():
|
||||
var offensive: Dictionary = selected.get_offensive_stats(unit)
|
||||
stats.atk = offensive["atk"]
|
||||
stats.hit = offensive["hit"] - opponent.current_stats.eva
|
||||
else:
|
||||
stats.atk = 0
|
||||
stats.hit = 0
|
||||
|
||||
if opponent_selected and opponent_selected.deals_damage():
|
||||
stats.def = opponent_selected.get_relevant_defense(unit)
|
||||
else:
|
||||
stats.def = unit.current_stats.phys_def
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
func update_tactic(proposal: CombatProposal, is_attacker: bool, tactic: CombatTactic) -> void:
|
||||
var self_stats: CombatProposal.CombatantStats
|
||||
var opp_stats: CombatProposal.CombatantStats
|
||||
if is_attacker:
|
||||
self_stats = proposal.attacker
|
||||
opp_stats = proposal.defender
|
||||
else:
|
||||
self_stats = proposal.defender
|
||||
opp_stats = proposal.attacker
|
||||
|
||||
self_stats.selected_tactic = tactic
|
||||
|
||||
# Recalculate this side's offensive stats
|
||||
if tactic and tactic.deals_damage():
|
||||
var offensive: Dictionary = tactic.get_offensive_stats(self_stats.unit)
|
||||
self_stats.atk = offensive["atk"]
|
||||
self_stats.hit = offensive["hit"] - opp_stats.unit.current_stats.eva
|
||||
else:
|
||||
self_stats.atk = 0
|
||||
self_stats.hit = 0
|
||||
|
||||
# Recalculate opponent's def based on this side's new tactic
|
||||
if tactic and tactic.deals_damage():
|
||||
opp_stats.def = tactic.get_relevant_defense(opp_stats.unit)
|
||||
else:
|
||||
opp_stats.def = opp_stats.unit.current_stats.phys_def
|
||||
|
||||
|
||||
func select_ai_tactic(unit: Unit, opponent: Unit, available_tactics: Array[CombatTactic]) -> CombatTactic:
|
||||
var best_tactic: CombatTactic = null
|
||||
var best_damage := -1
|
||||
|
||||
for tactic in available_tactics:
|
||||
if not tactic.deals_damage():
|
||||
continue
|
||||
var offensive: Dictionary = tactic.get_offensive_stats(unit)
|
||||
var defense: int = tactic.get_relevant_defense(opponent)
|
||||
var damage := maxi(offensive["atk"] - defense, 0)
|
||||
if damage > best_damage:
|
||||
best_damage = damage
|
||||
best_tactic = tactic
|
||||
|
||||
if best_tactic == null or best_damage <= 0:
|
||||
for tactic in available_tactics:
|
||||
if tactic is DefendCombatTactic:
|
||||
return tactic
|
||||
return available_tactics[0] if available_tactics.size() > 0 else null
|
||||
|
||||
return best_tactic
|
||||
|
||||
|
||||
func apply_proposal(proposal: CombatProposal) -> void:
|
||||
var atk_stats := proposal.attacker
|
||||
var def_stats := proposal.defender
|
||||
var atk_unit := atk_stats.unit
|
||||
var def_unit := def_stats.unit
|
||||
|
||||
if not is_instance_valid(atk_unit) or not is_instance_valid(def_unit):
|
||||
return
|
||||
|
||||
# Attacker strikes (if their tactic deals damage)
|
||||
if atk_stats.selected_tactic and atk_stats.selected_tactic.deals_damage():
|
||||
var atk_roll := randi_range(1, 100)
|
||||
if atk_roll <= atk_stats.hit:
|
||||
var damage := maxi(atk_stats.atk - def_stats.def, 0)
|
||||
def_unit.take_damage(damage)
|
||||
|
||||
# Counterattack if defender survives and their tactic deals damage
|
||||
if is_instance_valid(def_unit) and def_unit.is_alive() \
|
||||
and is_instance_valid(atk_unit) \
|
||||
and def_stats.selected_tactic and def_stats.selected_tactic.deals_damage():
|
||||
var def_roll := randi_range(1, 100)
|
||||
if def_roll <= def_stats.hit:
|
||||
var damage := maxi(def_stats.atk - atk_stats.def, 0)
|
||||
atk_unit.take_damage(damage)
|
||||
|
||||
|
||||
func _is_player_controlled(unit: Unit) -> bool:
|
||||
return unit.current_allegiance.type == UnitAllegiance.AllegianceType.PLAYER
|
||||
1
scripts/battle/combat_engine/combat_system.gd.uid
Normal file
1
scripts/battle/combat_engine/combat_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf4ivrcbky0s3
|
||||
13
scripts/battle/combat_tactics/combat_tactic.gd
Normal file
13
scripts/battle/combat_tactics/combat_tactic.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
class_name CombatTactic extends Resource
|
||||
|
||||
@export var tactic_name: String = ""
|
||||
@export var tactic_range: CombatTacticRange
|
||||
|
||||
func get_offensive_stats(_unit: Unit) -> Variant:
|
||||
return null
|
||||
|
||||
func get_relevant_defense(unit: Unit) -> int:
|
||||
return unit.current_stats.phys_def
|
||||
|
||||
func deals_damage() -> bool:
|
||||
return false
|
||||
1
scripts/battle/combat_tactics/combat_tactic.gd.uid
Normal file
1
scripts/battle/combat_tactics/combat_tactic.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b67rtbb5gixus
|
||||
@@ -0,0 +1,5 @@
|
||||
# resources/resource_definitions/any_combat_tactic_range.gd
|
||||
class_name AnyCombatTacticRange extends CombatTacticRange
|
||||
|
||||
func is_valid_range(_distance: int, _unit: Unit) -> bool:
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://danory6304bl6
|
||||
@@ -0,0 +1,5 @@
|
||||
# resources/resource_definitions/combat_tactic_range.gd
|
||||
class_name CombatTacticRange extends Resource
|
||||
|
||||
func is_valid_range(_distance: int, _unit: Unit) -> bool:
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://5cr4kl14gvd7
|
||||
@@ -0,0 +1,7 @@
|
||||
# resources/resource_definitions/fixed_combat_tactic_range.gd
|
||||
class_name FixedCombatTacticRange extends CombatTacticRange
|
||||
|
||||
@export var tactic_range: int = 1
|
||||
|
||||
func is_valid_range(distance: int, unit: Unit) -> bool:
|
||||
return distance <= tactic_range
|
||||
@@ -0,0 +1 @@
|
||||
uid://6jxhvwrkiq6f
|
||||
@@ -0,0 +1,5 @@
|
||||
# resources/resource_definitions/unit_matching_combat_tactic_range.gd
|
||||
class_name UnitMatchingCombatTacticRange extends CombatTacticRange
|
||||
|
||||
func is_valid_range(distance: int, unit: Unit) -> bool:
|
||||
return distance <= unit.current_stats.atk_range
|
||||
@@ -0,0 +1 @@
|
||||
uid://7locjqufdkgj
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name AttackCombatTactic extends CombatTactic
|
||||
|
||||
func get_offensive_stats(unit: Unit) -> Variant:
|
||||
return {"atk": unit.current_stats.phys_atk, "hit": unit.current_stats.hit}
|
||||
|
||||
func get_relevant_defense(unit: Unit) -> int:
|
||||
return unit.current_stats.phys_def
|
||||
|
||||
func deals_damage() -> bool:
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://k8xmyrygnrcl
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name DefendCombatTactic extends CombatTactic
|
||||
|
||||
func get_offensive_stats(_unit: Unit) -> Variant:
|
||||
return null
|
||||
|
||||
func get_relevant_defense(unit: Unit) -> int:
|
||||
return unit.current_stats.phys_def
|
||||
|
||||
func deals_damage() -> bool:
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://dq74qh01wi7sy
|
||||
4
scripts/battle/deployed_units/deployed_unit.gd
Normal file
4
scripts/battle/deployed_units/deployed_unit.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name DeployedUnit extends Node2D
|
||||
|
||||
# The unit represented by this deployment
|
||||
@export var unit: Unit
|
||||
1
scripts/battle/deployed_units/deployed_unit.gd.uid
Normal file
1
scripts/battle/deployed_units/deployed_unit.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmh4lphvboggy
|
||||
14
scripts/battle/deployed_units/deployed_unit_stats.gd
Normal file
14
scripts/battle/deployed_units/deployed_unit_stats.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
class_name DeployedUnitStats extends Resource
|
||||
|
||||
@export var unit_stats: UnitStats
|
||||
@export var current_hp: int
|
||||
@export var current_sp: int
|
||||
@export var current_fs: int
|
||||
|
||||
func _init() -> void:
|
||||
_init_stats.call_deferred()
|
||||
|
||||
func _init_stats() -> void:
|
||||
current_hp = unit_stats.max_hp
|
||||
current_sp = unit_stats.max_sp
|
||||
current_fs = unit_stats.max_fs
|
||||
1
scripts/battle/deployed_units/deployed_unit_stats.gd.uid
Normal file
1
scripts/battle/deployed_units/deployed_unit_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3jekvxwi8sxi
|
||||
127
scripts/battle/map/combat_map.gd
Normal file
127
scripts/battle/map/combat_map.gd
Normal file
@@ -0,0 +1,127 @@
|
||||
class_name CombatMap
|
||||
extends Node2D
|
||||
|
||||
@export var tile_set: DLTileset
|
||||
@export var map_layout: MapLayout
|
||||
@onready var tile_map: TileMapLayer = %TerrainLayer
|
||||
@onready var highlight_map: GridOverlay = %OverlayLayer
|
||||
@onready var wall_renderer: WallRenderer = %WallRenderer
|
||||
@onready var fog_renderer: FogRenderer = %FogRenderer
|
||||
|
||||
const TILE_SIZE := 100.0
|
||||
const SOURCE_ID: int = 0
|
||||
|
||||
var _pending_layout: String
|
||||
var _pending_units: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _pending_layout:
|
||||
_apply_layout(_pending_layout)
|
||||
for entry in _pending_units:
|
||||
_apply_deploy(entry.unit, entry.coords)
|
||||
_pending_units.clear()
|
||||
if map_layout:
|
||||
apply_layout(map_layout)
|
||||
|
||||
|
||||
func snap_to_grid(pos: Vector2) -> Vector2:
|
||||
return Vector2(floorf(pos.x / TILE_SIZE), floorf(pos.y / TILE_SIZE)) * TILE_SIZE
|
||||
|
||||
func world_to_coords(pos: Vector2) -> Vector2i:
|
||||
return Vector2i(snap_to_grid(pos) / TILE_SIZE)
|
||||
|
||||
func coords_to_world(coords: Vector2i) -> Vector2:
|
||||
return Vector2(coords) * TILE_SIZE
|
||||
|
||||
func draw_wall(coords: Vector2i) -> void:
|
||||
draw_custom(coords, tile_set.wall_tile_coords)
|
||||
|
||||
func draw_floor(coords: Vector2i) -> void:
|
||||
draw_custom(coords, tile_set.floor_tile_coords)
|
||||
|
||||
func draw_custom(coords: Vector2i, tile_coords: Vector2i) -> void:
|
||||
tile_map.set_cell(coords, SOURCE_ID, tile_coords)
|
||||
|
||||
func load_map(layout: String) -> void:
|
||||
if is_node_ready():
|
||||
_apply_layout(layout)
|
||||
else:
|
||||
_pending_layout = layout
|
||||
|
||||
|
||||
func deploy_unit(unit: Unit, coords: Vector2i) -> void:
|
||||
if is_node_ready():
|
||||
_apply_deploy(unit, coords)
|
||||
else:
|
||||
_pending_units.append({unit = unit, coords = coords})
|
||||
|
||||
|
||||
func _apply_layout(layout: String) -> void:
|
||||
var rows := layout.split("\n")
|
||||
for y in rows.size():
|
||||
for x in rows[y].length():
|
||||
var coords := Vector2i(x, y)
|
||||
match rows[y][x]:
|
||||
"#":
|
||||
draw_wall(coords)
|
||||
".":
|
||||
draw_floor(coords)
|
||||
|
||||
|
||||
func _apply_deploy(unit: Unit, coords: Vector2i) -> void:
|
||||
unit.position = coords_to_world(coords)
|
||||
add_child(unit)
|
||||
|
||||
|
||||
func remove_unit(unit: Unit) -> void:
|
||||
if unit.get_parent() == self:
|
||||
remove_child(unit)
|
||||
|
||||
|
||||
func target_tile(coords: Vector2i) -> void:
|
||||
highlight_map.target_tile(coords)
|
||||
|
||||
|
||||
func apply_layout(layout: MapLayout) -> void:
|
||||
map_layout = layout
|
||||
map_layout.initialize()
|
||||
load_from_layout()
|
||||
draw_room_walls()
|
||||
draw_fog()
|
||||
|
||||
|
||||
func is_tile_passable(from: Vector2i, to: Vector2i) -> bool:
|
||||
assert(map_layout != null, "CombatMap.is_tile_passable called before map_layout was set")
|
||||
return map_layout.is_passable(from, to)
|
||||
|
||||
|
||||
func is_tile_valid(coords: Vector2i) -> bool:
|
||||
assert(map_layout != null, "CombatMap.is_tile_valid called before map_layout was set")
|
||||
return map_layout.is_tile_valid(coords)
|
||||
|
||||
|
||||
func draw_room_walls() -> void:
|
||||
if not map_layout:
|
||||
return
|
||||
wall_renderer.draw_walls_for_layout(map_layout)
|
||||
|
||||
|
||||
func draw_fog() -> void:
|
||||
if not map_layout:
|
||||
return
|
||||
fog_renderer.draw_fog_for_layout(map_layout)
|
||||
|
||||
|
||||
func get_map_rect() -> Rect2:
|
||||
if not map_layout:
|
||||
return Rect2()
|
||||
return Rect2(Vector2.ZERO, Vector2(map_layout.size) * TILE_SIZE)
|
||||
|
||||
|
||||
func load_from_layout() -> void:
|
||||
if not map_layout:
|
||||
return
|
||||
for room in map_layout.rooms:
|
||||
for tile in room.tiles:
|
||||
draw_floor(tile)
|
||||
1
scripts/battle/map/combat_map.gd.uid
Normal file
1
scripts/battle/map/combat_map.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bks7uplgjjdg0
|
||||
4
scripts/battle/map/dl_tileset.gd
Normal file
4
scripts/battle/map/dl_tileset.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name DLTileset extends Resource
|
||||
|
||||
@export var floor_tile_coords: Vector2i
|
||||
@export var wall_tile_coords: Vector2i
|
||||
1
scripts/battle/map/dl_tileset.gd.uid
Normal file
1
scripts/battle/map/dl_tileset.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6701vy8h5rfx
|
||||
39
scripts/battle/map/fog_renderer.gd
Normal file
39
scripts/battle/map/fog_renderer.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
class_name FogRenderer
|
||||
extends Node2D
|
||||
|
||||
## Renders a fog/cave texture over every tile inside the map's bounding rect
|
||||
## that is not part of any room. Future: drive visibility from map state.
|
||||
|
||||
const TILE_SIZE := 100.0
|
||||
## Fog tile region in aux_terrain.BMP
|
||||
const FOG_RECT := Rect2(53, 53, 100, 100)
|
||||
|
||||
@export var atlas_texture: Texture2D
|
||||
|
||||
var _fog_tiles: PackedVector2Array = []
|
||||
|
||||
|
||||
func draw_fog_for_layout(map_layout: MapLayout) -> void:
|
||||
_fog_tiles.clear()
|
||||
if not map_layout or not atlas_texture:
|
||||
queue_redraw()
|
||||
return
|
||||
for y in map_layout.size.y:
|
||||
for x in map_layout.size.x:
|
||||
var tile := Vector2i(x, y)
|
||||
if map_layout.is_tile_valid(tile):
|
||||
continue
|
||||
_fog_tiles.append(Vector2(tile) * TILE_SIZE)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not atlas_texture:
|
||||
return
|
||||
var dest_size := Vector2(TILE_SIZE, TILE_SIZE)
|
||||
for pos in _fog_tiles:
|
||||
draw_texture_rect_region(
|
||||
atlas_texture,
|
||||
Rect2(pos, dest_size),
|
||||
FOG_RECT,
|
||||
)
|
||||
1
scripts/battle/map/fog_renderer.gd.uid
Normal file
1
scripts/battle/map/fog_renderer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1d1nbetdvynk
|
||||
81
scripts/battle/map/map_layout.gd
Normal file
81
scripts/battle/map/map_layout.gd
Normal file
@@ -0,0 +1,81 @@
|
||||
class_name MapLayout extends Resource
|
||||
|
||||
@export var size: Vector2i = Vector2i.ZERO
|
||||
@export var rooms: Array[Room]
|
||||
## Openings are stored as a flat array of pairs: [from1, to1, from2, to2, ...].
|
||||
## Each consecutive pair of Vector2i values represents a bidirectional doorway
|
||||
## between two adjacent tiles in different rooms.
|
||||
@export var openings: Array[Vector2i]
|
||||
|
||||
var _tile_room_map: Dictionary = {}
|
||||
var _opening_set: Dictionary = {}
|
||||
|
||||
|
||||
func initialize() -> void:
|
||||
assert(openings.size() % 2 == 0, "Openings must be provided as pairs of Vector2i")
|
||||
_tile_room_map.clear()
|
||||
_opening_set.clear()
|
||||
for room in rooms:
|
||||
for tile in room.tiles:
|
||||
_tile_room_map[tile] = room
|
||||
for i in range(0, openings.size(), 2):
|
||||
var a := openings[i]
|
||||
var b := openings[i + 1]
|
||||
_opening_set[_edge_key(a, b)] = true
|
||||
|
||||
|
||||
static func _edge_key(a: Vector2i, b: Vector2i) -> String:
|
||||
if a < b:
|
||||
return "%d,%d-%d,%d" % [a.x, a.y, b.x, b.y]
|
||||
return "%d,%d-%d,%d" % [b.x, b.y, a.x, a.y]
|
||||
|
||||
|
||||
func is_tile_valid(tile: Vector2i) -> bool:
|
||||
return _tile_room_map.has(tile)
|
||||
|
||||
|
||||
func get_room_at(tile: Vector2i) -> Room:
|
||||
return _tile_room_map.get(tile, null)
|
||||
|
||||
|
||||
func is_passable(from: Vector2i, to: Vector2i) -> bool:
|
||||
if not is_tile_valid(from) or not is_tile_valid(to):
|
||||
return false
|
||||
var room_from: Room = _tile_room_map[from]
|
||||
var room_to: Room = _tile_room_map[to]
|
||||
if room_from == room_to:
|
||||
return true
|
||||
return _opening_set.has(_edge_key(from, to))
|
||||
|
||||
|
||||
func get_openings() -> Array:
|
||||
## Returns an array of [Vector2i, Vector2i] pairs representing opening edges.
|
||||
var result: Array = []
|
||||
for i in range(0, openings.size(), 2):
|
||||
result.append([openings[i], openings[i + 1]])
|
||||
return result
|
||||
|
||||
|
||||
func get_walls() -> Array:
|
||||
## Returns an array of [Vector2i, Vector2i] pairs representing wall edges.
|
||||
## A wall exists where a room tile borders void or a different room (without an opening).
|
||||
var walls: Array = []
|
||||
var directions := [Vector2i.RIGHT, Vector2i.DOWN, Vector2i.LEFT, Vector2i.UP]
|
||||
var visited_edges: Dictionary = {}
|
||||
|
||||
for room in rooms:
|
||||
for tile in room.tiles:
|
||||
for dir in directions:
|
||||
var neighbor: Vector2i = tile + dir
|
||||
var key := _edge_key(tile, neighbor)
|
||||
if visited_edges.has(key):
|
||||
continue
|
||||
visited_edges[key] = true
|
||||
|
||||
var neighbor_room: Room = _tile_room_map.get(neighbor, null)
|
||||
if neighbor_room == room:
|
||||
continue
|
||||
# Neighbor is void or different room — wall unless opening
|
||||
if not _opening_set.has(key):
|
||||
walls.append([tile, neighbor])
|
||||
return walls
|
||||
1
scripts/battle/map/map_layout.gd.uid
Normal file
1
scripts/battle/map/map_layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dj7qfdelq4ja4
|
||||
4
scripts/battle/map/room.gd
Normal file
4
scripts/battle/map/room.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
class_name Room extends Resource
|
||||
|
||||
@export var id: int
|
||||
@export var tiles: Array[Vector2i]
|
||||
1
scripts/battle/map/room.gd.uid
Normal file
1
scripts/battle/map/room.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ja34p4vpwamd
|
||||
266
scripts/battle/map/wall_renderer.gd
Normal file
266
scripts/battle/map/wall_renderer.gd
Normal file
@@ -0,0 +1,266 @@
|
||||
class_name WallRenderer
|
||||
extends Node2D
|
||||
|
||||
## Renders wall textures by sampling segments from the aux_terrain texture atlas
|
||||
## and compositing them onto tile edges. Each edge is made of two half-segments.
|
||||
|
||||
const TILE_SIZE := 100.0
|
||||
|
||||
## Source atlas rects (x, y, w, h) from aux_terrain.BMP
|
||||
## Each edge has two half-segments that together span the full tile edge.
|
||||
|
||||
# -- Left edge --
|
||||
const LEFT_UPPER_RECT := Rect2(0, 103, 20, 50)
|
||||
const LEFT_LOWER_RECT := Rect2(0, 53, 20, 50)
|
||||
|
||||
# -- Right edge --
|
||||
const RIGHT_UPPER_RECT := Rect2(186, 103, 20, 50)
|
||||
const RIGHT_LOWER_RECT := Rect2(186, 53, 20, 50)
|
||||
|
||||
# -- Top edge --
|
||||
const TOP_LEFT_RECT := Rect2(103, 0, 50, 20)
|
||||
const TOP_RIGHT_RECT := Rect2(53, 0, 50, 20)
|
||||
|
||||
# -- Bottom edge --
|
||||
const BOTTOM_LEFT_RECT := Rect2(103, 186, 50, 20)
|
||||
const BOTTOM_RIGHT_RECT := Rect2(53, 186, 50, 20)
|
||||
|
||||
# -- Inner corners (drawn where two perpendicular wall edges meet) --
|
||||
const INNER_CORNER_UPPER_LEFT_RECT := Rect2(0, 0, 50, 50)
|
||||
const INNER_CORNER_UPPER_RIGHT_RECT := Rect2(156, 0, 50, 50)
|
||||
const INNER_CORNER_LOWER_LEFT_RECT := Rect2(0, 156, 50, 50)
|
||||
const INNER_CORNER_LOWER_RIGHT_RECT := Rect2(156, 156, 50, 50)
|
||||
|
||||
# -- Openings (drawn on top of wall segments at doorway edges) --
|
||||
## Vertical opening: tiles separated on y-axis (north-south doorway through a horizontal wall)
|
||||
const VERTICAL_OPENING_RECT := Rect2(206, 36, 36, 42)
|
||||
## Horizontal opening: tiles separated on x-axis (east-west doorway through a vertical wall)
|
||||
const HORIZONTAL_OPENING_RECT := Rect2(210, 0, 41, 32)
|
||||
|
||||
## Wall thickness in game pixels (how far the border extends into the tile)
|
||||
const WALL_THICKNESS := 20.0
|
||||
## Inner corner piece size in game pixels (quarter of a tile)
|
||||
const CORNER_SIZE := 50.0
|
||||
## Half the tile edge length
|
||||
const HALF_EDGE := TILE_SIZE / 2.0
|
||||
|
||||
@export var atlas_texture: Texture2D
|
||||
|
||||
# Each entry is [dest_rect: Rect2, source_rect: Rect2]
|
||||
var _segments: Array = []
|
||||
|
||||
|
||||
func draw_walls_for_layout(map_layout: MapLayout) -> void:
|
||||
_segments.clear()
|
||||
if not map_layout or not atlas_texture:
|
||||
queue_redraw()
|
||||
return
|
||||
|
||||
var tile_edges := _collect_tile_edges(map_layout)
|
||||
for tile in tile_edges:
|
||||
var edges: Array = tile_edges[tile]
|
||||
_build_tile_walls(tile, edges)
|
||||
_build_opening_sprites(map_layout)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not atlas_texture:
|
||||
return
|
||||
for seg in _segments:
|
||||
draw_texture_rect_region(atlas_texture, seg[0], seg[1])
|
||||
|
||||
|
||||
func _collect_tile_edges(map_layout: MapLayout) -> Dictionary:
|
||||
## Returns {Vector2i: Array[StringName]} mapping each tile to its wall edge directions.
|
||||
## Includes both true walls and opening edges, so wall sprites are drawn underneath openings.
|
||||
var tile_edges: Dictionary = {}
|
||||
for edge_pair in map_layout.get_walls():
|
||||
_add_edge_pair(tile_edges, edge_pair, map_layout)
|
||||
for edge_pair in map_layout.get_openings():
|
||||
_add_edge_pair(tile_edges, edge_pair, map_layout)
|
||||
return tile_edges
|
||||
|
||||
|
||||
func _add_edge_pair(tile_edges: Dictionary, edge_pair: Array, map_layout: MapLayout) -> void:
|
||||
var tile_a: Vector2i = edge_pair[0]
|
||||
var tile_b: Vector2i = edge_pair[1]
|
||||
var diff: Vector2i = tile_b - tile_a
|
||||
|
||||
var edge_a := _direction_to_edge(diff)
|
||||
if edge_a != &"":
|
||||
if not tile_edges.has(tile_a):
|
||||
tile_edges[tile_a] = []
|
||||
tile_edges[tile_a].append(edge_a)
|
||||
|
||||
var edge_b := _direction_to_edge(-diff)
|
||||
if edge_b != &"" and map_layout.is_tile_valid(tile_b):
|
||||
if not tile_edges.has(tile_b):
|
||||
tile_edges[tile_b] = []
|
||||
tile_edges[tile_b].append(edge_b)
|
||||
|
||||
|
||||
func _direction_to_edge(dir: Vector2i) -> StringName:
|
||||
match dir:
|
||||
Vector2i.RIGHT:
|
||||
return &"right"
|
||||
Vector2i.LEFT:
|
||||
return &"left"
|
||||
Vector2i.UP:
|
||||
return &"top"
|
||||
Vector2i.DOWN:
|
||||
return &"bottom"
|
||||
return &""
|
||||
|
||||
|
||||
func _build_tile_walls(tile: Vector2i, edges: Array) -> void:
|
||||
var tile_origin := Vector2(tile) * TILE_SIZE
|
||||
|
||||
for edge in edges:
|
||||
_build_edge_segments(tile_origin, edge)
|
||||
|
||||
# TODO: Outer corner segments
|
||||
_build_outer_corners(tile, tile_origin, edges)
|
||||
|
||||
# Inner corner segments (for non-rectangular room support)
|
||||
_build_inner_corners(tile, tile_origin, edges)
|
||||
|
||||
|
||||
func _build_edge_segments(tile_origin: Vector2, edge: StringName) -> void:
|
||||
var seg_a_rect: Rect2
|
||||
var seg_b_rect: Rect2
|
||||
var seg_a_offset: Vector2
|
||||
var seg_b_offset: Vector2
|
||||
var seg_a_size: Vector2
|
||||
var seg_b_size: Vector2
|
||||
|
||||
match edge:
|
||||
&"left":
|
||||
seg_a_rect = LEFT_UPPER_RECT
|
||||
seg_b_rect = LEFT_LOWER_RECT
|
||||
seg_a_size = Vector2(WALL_THICKNESS, HALF_EDGE)
|
||||
seg_b_size = Vector2(WALL_THICKNESS, HALF_EDGE)
|
||||
seg_a_offset = Vector2(0, 0)
|
||||
seg_b_offset = Vector2(0, HALF_EDGE)
|
||||
&"right":
|
||||
seg_a_rect = RIGHT_UPPER_RECT
|
||||
seg_b_rect = RIGHT_LOWER_RECT
|
||||
seg_a_size = Vector2(WALL_THICKNESS, HALF_EDGE)
|
||||
seg_b_size = Vector2(WALL_THICKNESS, HALF_EDGE)
|
||||
seg_a_offset = Vector2(TILE_SIZE - WALL_THICKNESS, 0)
|
||||
seg_b_offset = Vector2(TILE_SIZE - WALL_THICKNESS, HALF_EDGE)
|
||||
&"top":
|
||||
seg_a_rect = TOP_LEFT_RECT
|
||||
seg_b_rect = TOP_RIGHT_RECT
|
||||
seg_a_size = Vector2(HALF_EDGE, WALL_THICKNESS)
|
||||
seg_b_size = Vector2(HALF_EDGE, WALL_THICKNESS)
|
||||
seg_a_offset = Vector2(0, 0)
|
||||
seg_b_offset = Vector2(HALF_EDGE, 0)
|
||||
&"bottom":
|
||||
seg_a_rect = BOTTOM_LEFT_RECT
|
||||
seg_b_rect = BOTTOM_RIGHT_RECT
|
||||
seg_a_size = Vector2(HALF_EDGE, WALL_THICKNESS)
|
||||
seg_b_size = Vector2(HALF_EDGE, WALL_THICKNESS)
|
||||
seg_a_offset = Vector2(0, TILE_SIZE - WALL_THICKNESS)
|
||||
seg_b_offset = Vector2(HALF_EDGE, TILE_SIZE - WALL_THICKNESS)
|
||||
|
||||
_queue_segment(tile_origin + seg_a_offset, seg_a_size, seg_a_rect)
|
||||
_queue_segment(tile_origin + seg_b_offset, seg_b_size, seg_b_rect)
|
||||
|
||||
|
||||
func _queue_segment(pos: Vector2, target_size: Vector2, source_rect: Rect2) -> void:
|
||||
_segments.append([Rect2(pos, target_size), source_rect])
|
||||
|
||||
|
||||
func _build_opening_sprites(map_layout: MapLayout) -> void:
|
||||
## Composites opening (doorway) sprites on top of the wall segments at opening edges.
|
||||
## Each opening is split in half across the shared edge, half drawn on each tile.
|
||||
for opening in map_layout.get_openings():
|
||||
var tile_a: Vector2i = opening[0]
|
||||
var tile_b: Vector2i = opening[1]
|
||||
var diff: Vector2i = tile_b - tile_a
|
||||
|
||||
# Normalize so the pair is ordered along the positive axis (tile_a < tile_b).
|
||||
if diff == Vector2i.LEFT or diff == Vector2i.UP:
|
||||
var swap := tile_a
|
||||
tile_a = tile_b
|
||||
tile_b = swap
|
||||
diff = -diff
|
||||
|
||||
var origin_a := Vector2(tile_a) * TILE_SIZE
|
||||
var origin_b := Vector2(tile_b) * TILE_SIZE
|
||||
|
||||
if diff == Vector2i.DOWN:
|
||||
_queue_vertical_opening(origin_a, origin_b)
|
||||
elif diff == Vector2i.RIGHT:
|
||||
_queue_horizontal_opening(origin_a, origin_b)
|
||||
|
||||
|
||||
func _queue_vertical_opening(origin_upper: Vector2, origin_lower: Vector2) -> void:
|
||||
# Vertical opening: tiles vertically adjacent; horizontal wall edge between them.
|
||||
var src := VERTICAL_OPENING_RECT
|
||||
var w: float = src.size.x
|
||||
var h_total: float = src.size.y
|
||||
var h_upper: float = floorf(h_total / 2.0) # 14
|
||||
var h_lower: float = h_total - h_upper # 15
|
||||
var x_offset := (TILE_SIZE - w) / 2.0
|
||||
var src_upper := Rect2(src.position, Vector2(w, h_upper))
|
||||
var src_lower := Rect2(src.position + Vector2(0, h_upper), Vector2(w, h_lower))
|
||||
_queue_segment(origin_upper + Vector2(x_offset, TILE_SIZE - h_upper), Vector2(w, h_upper), src_upper)
|
||||
_queue_segment(origin_lower + Vector2(x_offset, 0), Vector2(w, h_lower), src_lower)
|
||||
|
||||
|
||||
func _queue_horizontal_opening(origin_left: Vector2, origin_right: Vector2) -> void:
|
||||
# Horizontal opening: tiles horizontally adjacent; vertical wall edge between them.
|
||||
var src := HORIZONTAL_OPENING_RECT
|
||||
var w_total: float = src.size.x
|
||||
var h: float = src.size.y
|
||||
var w_left: float = floorf(w_total / 2.0) # 14
|
||||
var w_right: float = w_total - w_left # 14
|
||||
var y_offset := (TILE_SIZE - h) / 2.0
|
||||
var src_left := Rect2(src.position, Vector2(w_left, h))
|
||||
var src_right := Rect2(src.position + Vector2(w_left, 0), Vector2(w_right, h))
|
||||
_queue_segment(origin_left + Vector2(TILE_SIZE - w_left, y_offset), Vector2(w_left, h), src_left)
|
||||
_queue_segment(origin_right + Vector2(0, y_offset), Vector2(w_right, h), src_right)
|
||||
|
||||
|
||||
func _build_outer_corners(_tile: Vector2i, _tile_origin: Vector2, _edges: Array) -> void:
|
||||
# TODO: Implement outer corner segments
|
||||
# Check pairs of adjacent edges (e.g., "top" + "left" → top-left outer corner)
|
||||
# and draw the corner piece from the atlas.
|
||||
pass
|
||||
|
||||
|
||||
func _build_inner_corners(_tile: Vector2i, tile_origin: Vector2, edges: Array) -> void:
|
||||
## Draws decorative corner pieces wherever two perpendicular wall edges meet
|
||||
## on the same tile (e.g., a top wall + left wall produces an upper-left corner).
|
||||
var has_top := edges.has(&"top")
|
||||
var has_bottom := edges.has(&"bottom")
|
||||
var has_left := edges.has(&"left")
|
||||
var has_right := edges.has(&"right")
|
||||
var corner_size := Vector2(CORNER_SIZE, CORNER_SIZE)
|
||||
|
||||
if has_top and has_left:
|
||||
_queue_segment(
|
||||
tile_origin + Vector2(0, 0),
|
||||
corner_size,
|
||||
INNER_CORNER_UPPER_LEFT_RECT
|
||||
)
|
||||
if has_top and has_right:
|
||||
_queue_segment(
|
||||
tile_origin + Vector2(TILE_SIZE - CORNER_SIZE, 0),
|
||||
corner_size,
|
||||
INNER_CORNER_UPPER_RIGHT_RECT
|
||||
)
|
||||
if has_bottom and has_left:
|
||||
_queue_segment(
|
||||
tile_origin + Vector2(0, TILE_SIZE - CORNER_SIZE),
|
||||
corner_size,
|
||||
INNER_CORNER_LOWER_LEFT_RECT
|
||||
)
|
||||
if has_bottom and has_right:
|
||||
_queue_segment(
|
||||
tile_origin + Vector2(TILE_SIZE - CORNER_SIZE, TILE_SIZE - CORNER_SIZE),
|
||||
corner_size,
|
||||
INNER_CORNER_LOWER_RIGHT_RECT
|
||||
)
|
||||
1
scripts/battle/map/wall_renderer.gd.uid
Normal file
1
scripts/battle/map/wall_renderer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c4f1vflwd81b8
|
||||
167
scripts/battle/player_controller.gd
Normal file
167
scripts/battle/player_controller.gd
Normal file
@@ -0,0 +1,167 @@
|
||||
class_name PlayerController extends Node
|
||||
|
||||
const SPEED = 192.0
|
||||
|
||||
@export var dl_map: CombatMap
|
||||
|
||||
signal combat_requested(attacker: Unit, defender: Unit)
|
||||
signal mouse_grid_changed(coords: Vector2i)
|
||||
signal camera_drag(delta: Vector2)
|
||||
|
||||
var input_disabled := false
|
||||
|
||||
var _selected_unit: Unit = null
|
||||
var _target_pos: Vector2
|
||||
var _goal_pos: Vector2
|
||||
var _moving := false
|
||||
|
||||
var _left_pending := false
|
||||
var _drag_start := Vector2.ZERO
|
||||
var _dragging := false
|
||||
var _current_grid_coords := Vector2i(-99999, -99999)
|
||||
|
||||
const DRAG_THRESHOLD := 8.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for unit: Unit in get_tree().get_nodes_in_group("units"):
|
||||
unit.unit_died.connect(_on_unit_died)
|
||||
get_tree().node_added.connect(_on_node_added)
|
||||
|
||||
|
||||
func _on_node_added(node: Node) -> void:
|
||||
if node is Unit and node.is_in_group("units"):
|
||||
if not node.unit_died.is_connected(_on_unit_died):
|
||||
node.unit_died.connect(_on_unit_died)
|
||||
|
||||
|
||||
func _on_unit_died(unit: Unit) -> void:
|
||||
if _selected_unit == unit:
|
||||
_selected_unit = null
|
||||
_moving = false
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if input_disabled:
|
||||
return
|
||||
var mouse_pos := get_viewport().get_canvas_transform().affine_inverse() * get_viewport().get_mouse_position()
|
||||
var coords := dl_map.world_to_coords(mouse_pos)
|
||||
if coords != _current_grid_coords:
|
||||
_current_grid_coords = coords
|
||||
mouse_grid_changed.emit(coords)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if input_disabled:
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
_left_pending = true
|
||||
_drag_start = event.position
|
||||
else:
|
||||
if _dragging:
|
||||
_dragging = false
|
||||
_left_pending = false
|
||||
Input.set_default_cursor_shape(Input.CURSOR_ARROW)
|
||||
get_viewport().set_input_as_handled()
|
||||
else:
|
||||
_left_pending = false
|
||||
_handle_left_click(event.position)
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
if event.pressed:
|
||||
_dragging = true
|
||||
_drag_start = event.position
|
||||
Input.set_default_cursor_shape(Input.CURSOR_DRAG)
|
||||
else:
|
||||
_dragging = false
|
||||
Input.set_default_cursor_shape(Input.CURSOR_ARROW)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
elif event is InputEventMouseMotion:
|
||||
if _left_pending and not _dragging:
|
||||
if event.position.distance_to(_drag_start) >= DRAG_THRESHOLD:
|
||||
_dragging = true
|
||||
_left_pending = false
|
||||
Input.set_default_cursor_shape(Input.CURSOR_DRAG)
|
||||
if _dragging:
|
||||
var delta: Vector2 = _drag_start - event.position
|
||||
_drag_start = event.position
|
||||
camera_drag.emit(delta)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not _selected_unit or not _selected_unit.is_alive():
|
||||
return
|
||||
|
||||
if _moving:
|
||||
var remaining := _target_pos - _selected_unit.position
|
||||
var step := SPEED * delta
|
||||
|
||||
if remaining.length() <= step:
|
||||
_selected_unit.position = _target_pos
|
||||
_moving = false
|
||||
else:
|
||||
_selected_unit.position += remaining.normalized() * step
|
||||
return
|
||||
|
||||
if _selected_unit.position != _goal_pos:
|
||||
var diff := _goal_pos - _selected_unit.position
|
||||
var dir: Vector2
|
||||
if absf(diff.x) >= absf(diff.y):
|
||||
dir = Vector2(signf(diff.x), 0)
|
||||
else:
|
||||
dir = Vector2(0, signf(diff.y))
|
||||
|
||||
var next_pos := _selected_unit.position + dir * dl_map.TILE_SIZE
|
||||
var grid_coords := dl_map.world_to_coords(next_pos)
|
||||
var current_coords := dl_map.world_to_coords(_selected_unit.position)
|
||||
if not dl_map.is_tile_passable(current_coords, grid_coords):
|
||||
_goal_pos = _selected_unit.position
|
||||
return
|
||||
|
||||
_target_pos = next_pos
|
||||
_moving = true
|
||||
|
||||
|
||||
func _handle_left_click(screen_pos: Vector2) -> void:
|
||||
var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * screen_pos
|
||||
var clicked_unit := _get_unit_at(world_pos)
|
||||
|
||||
if clicked_unit:
|
||||
if _selected_unit and clicked_unit != _selected_unit and _selected_unit.is_alive() and clicked_unit.is_alive():
|
||||
combat_requested.emit(_selected_unit, clicked_unit)
|
||||
else:
|
||||
_select_unit(clicked_unit)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _selected_unit:
|
||||
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
||||
var grid_coords := dl_map.world_to_coords(world_pos)
|
||||
if not dl_map.is_tile_valid(grid_coords):
|
||||
return
|
||||
_goal_pos = snapped_pos
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _select_unit(unit: Unit) -> void:
|
||||
if _selected_unit:
|
||||
_selected_unit.set_selected(false)
|
||||
_selected_unit = unit
|
||||
_selected_unit.set_selected(true)
|
||||
_goal_pos = _selected_unit.position
|
||||
_target_pos = _selected_unit.position
|
||||
_moving = false
|
||||
|
||||
|
||||
func _get_unit_at(world_pos: Vector2) -> Unit:
|
||||
var snapped_coords := dl_map.snap_to_grid(world_pos)
|
||||
for unit: Unit in get_tree().get_nodes_in_group("units"):
|
||||
if not unit.is_alive():
|
||||
continue
|
||||
var unit_snapped := dl_map.snap_to_grid(unit.global_position)
|
||||
if unit_snapped == snapped_coords:
|
||||
return unit
|
||||
return null
|
||||
1
scripts/battle/player_controller.gd.uid
Normal file
1
scripts/battle/player_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dfojm3n0em4ef
|
||||
67
scripts/battle/strategy_phase.gd
Normal file
67
scripts/battle/strategy_phase.gd
Normal file
@@ -0,0 +1,67 @@
|
||||
class_name StrategyPhase extends Node2D
|
||||
|
||||
@onready var player_controller: PlayerController = $PlayerController
|
||||
@onready var combat_system: CombatSystem = $CombatSystem
|
||||
@onready var combat_ui: CombatUI = $CombatUI
|
||||
@onready var combat_map: CombatMap = $CombatMap
|
||||
@onready var camera: CameraController = $Camera2D
|
||||
|
||||
func _ready() -> void:
|
||||
# -- Test room layout (remove once map editor exists) --
|
||||
var room_a := Room.new()
|
||||
room_a.id = 0
|
||||
room_a.tiles = [
|
||||
Vector2i(2, 2), Vector2i(3, 2), Vector2i(4, 2),
|
||||
Vector2i(2, 3), Vector2i(3, 3), Vector2i(4, 3),
|
||||
Vector2i(2, 4), Vector2i(3, 4), Vector2i(4, 4),
|
||||
]
|
||||
|
||||
var room_b := Room.new()
|
||||
room_b.id = 1
|
||||
room_b.tiles = [
|
||||
Vector2i(5, 2), Vector2i(6, 2), Vector2i(7, 2),
|
||||
Vector2i(5, 3), Vector2i(6, 3), Vector2i(7, 3),
|
||||
Vector2i(5, 4), Vector2i(6, 4), Vector2i(7, 4),
|
||||
]
|
||||
|
||||
var layout := MapLayout.new()
|
||||
layout.rooms = [room_a, room_b]
|
||||
# Opening between (4,3) in room_a and (5,3) in room_b
|
||||
layout.openings = [Vector2i(4, 3), Vector2i(5, 3)]
|
||||
layout.size = Vector2i(10, 7)
|
||||
|
||||
combat_map.apply_layout(layout)
|
||||
camera.set_map_bounds(combat_map.get_map_rect())
|
||||
# -- End test room layout --
|
||||
|
||||
player_controller.combat_requested.connect(_on_combat_requested)
|
||||
player_controller.mouse_grid_changed.connect(_on_mouse_grid_changed)
|
||||
player_controller.camera_drag.connect(camera.apply_drag)
|
||||
combat_ui.fight_confirmed.connect(_on_fight_confirmed)
|
||||
combat_ui.fight_cancelled.connect(_on_fight_cancelled)
|
||||
combat_ui.combat_system = combat_system
|
||||
|
||||
|
||||
func _on_mouse_grid_changed(coords: Vector2i) -> void:
|
||||
combat_map.target_tile(coords)
|
||||
|
||||
func _on_combat_requested(attacker: Unit, defender: Unit) -> void:
|
||||
var atk_coords := combat_map.world_to_coords(attacker.position)
|
||||
var def_coords := combat_map.world_to_coords(defender.position)
|
||||
var distance := absi(atk_coords.x - def_coords.x) + absi(atk_coords.y - def_coords.y)
|
||||
var proposal := combat_system.create_proposal(attacker, defender, distance)
|
||||
_set_input_disabled(true)
|
||||
combat_ui.show_proposal(proposal)
|
||||
|
||||
|
||||
func _on_fight_confirmed(proposal: CombatProposal) -> void:
|
||||
combat_system.apply_proposal(proposal)
|
||||
_set_input_disabled(false)
|
||||
|
||||
|
||||
func _on_fight_cancelled() -> void:
|
||||
_set_input_disabled(false)
|
||||
|
||||
|
||||
func _set_input_disabled(disabled: bool) -> void:
|
||||
player_controller.input_disabled = disabled
|
||||
1
scripts/battle/strategy_phase.gd.uid
Normal file
1
scripts/battle/strategy_phase.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnsqtsx4u2hx4
|
||||
7
scripts/debug/console_command.gd
Normal file
7
scripts/debug/console_command.gd
Normal file
@@ -0,0 +1,7 @@
|
||||
@abstract class_name ConsoleCommand extends RefCounted
|
||||
|
||||
@abstract func get_command_name() -> String
|
||||
|
||||
@abstract func get_help_text() -> String
|
||||
|
||||
@abstract func run(args: Array, context: Dictionary) -> String
|
||||
1
scripts/debug/console_command.gd.uid
Normal file
1
scripts/debug/console_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b2kk8l3kumxpr
|
||||
15
scripts/debug/console_commands/help_command.gd
Normal file
15
scripts/debug/console_commands/help_command.gd
Normal file
@@ -0,0 +1,15 @@
|
||||
class_name HelpCommand extends ConsoleCommand
|
||||
|
||||
func get_command_name() -> String:
|
||||
return "help"
|
||||
|
||||
func get_help_text() -> String:
|
||||
return "Lists all available commands"
|
||||
|
||||
func run(_args: Array, context: Dictionary) -> String:
|
||||
var commands: Array = context["commands"]
|
||||
var lines: PackedStringArray = []
|
||||
for command: ConsoleCommand in commands:
|
||||
lines.append("%s - %s" % [command.get_command_name(), command.get_help_text()])
|
||||
lines.append("Any other input is evaluated as a GDScript expression.")
|
||||
return "\n".join(lines)
|
||||
1
scripts/debug/console_commands/help_command.gd.uid
Normal file
1
scripts/debug/console_commands/help_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvat5xgudptct
|
||||
14
scripts/debug/console_commands/list_scenes_command.gd
Normal file
14
scripts/debug/console_commands/list_scenes_command.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
class_name ListScenesCommand extends ConsoleCommand
|
||||
|
||||
func get_command_name() -> String:
|
||||
return "list_scenes"
|
||||
|
||||
func get_help_text() -> String:
|
||||
return "Lists available scenes for swapping"
|
||||
|
||||
func run(_args: Array, context: Dictionary) -> String:
|
||||
var registry: Array = context["scene_registry"]
|
||||
var lines: PackedStringArray = []
|
||||
for entry: Dictionary in registry:
|
||||
lines.append(entry["name"])
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b51b3np7lxd3v
|
||||
21
scripts/debug/console_commands/swap_command.gd
Normal file
21
scripts/debug/console_commands/swap_command.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
class_name SwapCommand extends ConsoleCommand
|
||||
|
||||
func get_command_name() -> String:
|
||||
return "swap"
|
||||
|
||||
func get_help_text() -> String:
|
||||
return "swap <name> - Swap to a scene by name (use list_scenes to see options)"
|
||||
|
||||
func run(args: Array, context: Dictionary) -> String:
|
||||
if args.size() == 0:
|
||||
return "Usage: swap <scene name>"
|
||||
|
||||
var search_name := " ".join(args).to_lower()
|
||||
var registry: Array = context["scene_registry"]
|
||||
for entry: Dictionary in registry:
|
||||
if entry["name"].to_lower() == search_name:
|
||||
var debug_menu: DebugMenu = context["debug_menu"]
|
||||
debug_menu.swap_scene(entry)
|
||||
return ""
|
||||
|
||||
return "Scene not found: %s" % search_name
|
||||
1
scripts/debug/console_commands/swap_command.gd.uid
Normal file
1
scripts/debug/console_commands/swap_command.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b56j4uyjiaku1
|
||||
104
scripts/debug/debug_menu.gd
Normal file
104
scripts/debug/debug_menu.gd
Normal file
@@ -0,0 +1,104 @@
|
||||
class_name DebugMenu extends CanvasLayer
|
||||
|
||||
signal close_requested
|
||||
|
||||
var active_scene_container: Node
|
||||
|
||||
var scene_registry: Array = [
|
||||
{ "name": "Battle Test", "path": "res://scenes/views/battle_view.tscn" },
|
||||
{ "name": "Main Menu", "path": "res://scenes/views/main_menu_view.tscn" },
|
||||
{ "name": "Dialogue Test", "path": "res://scenes/dialogue_scene.tscn" },
|
||||
]
|
||||
|
||||
var commands: Array[ConsoleCommand] = []
|
||||
|
||||
@onready var panel: PanelContainer = %Panel
|
||||
@onready var scene_list: VBoxContainer = %SceneList
|
||||
@onready var command_input: LineEdit = %CommandInput
|
||||
@onready var result_label: Label = %ResultLabel
|
||||
|
||||
func _ready() -> void:
|
||||
commands = [
|
||||
HelpCommand.new(),
|
||||
ListScenesCommand.new(),
|
||||
SwapCommand.new(),
|
||||
]
|
||||
_build_scene_buttons()
|
||||
command_input.text_submitted.connect(_on_command_submitted)
|
||||
result_label.text = ""
|
||||
|
||||
func _build_scene_buttons() -> void:
|
||||
for child in scene_list.get_children():
|
||||
child.queue_free()
|
||||
for entry: Dictionary in scene_registry:
|
||||
var button := Button.new()
|
||||
button.text = entry["name"]
|
||||
button.pressed.connect(swap_scene.bind(entry))
|
||||
scene_list.add_child(button)
|
||||
|
||||
func swap_scene(entry: Dictionary) -> void:
|
||||
for child in active_scene_container.get_children():
|
||||
active_scene_container.remove_child(child)
|
||||
child.queue_free()
|
||||
var scene: PackedScene = load(entry["path"])
|
||||
var instance := scene.instantiate()
|
||||
active_scene_container.add_child(instance)
|
||||
if entry.has("setup"):
|
||||
_apply_setup(entry["setup"], instance)
|
||||
close_requested.emit()
|
||||
|
||||
func _apply_setup(setup_key: String, _scene_instance: Node) -> void:
|
||||
match setup_key:
|
||||
_:
|
||||
push_warning("Unknown setup key: %s" % setup_key)
|
||||
|
||||
func _on_command_submitted(text: String) -> void:
|
||||
command_input.text = ""
|
||||
if text.strip_edges().is_empty():
|
||||
return
|
||||
var result := _execute_command(text.strip_edges())
|
||||
_show_result(result)
|
||||
|
||||
func _execute_command(input: String) -> String:
|
||||
var parts := input.split(" ", false)
|
||||
var command_name := parts[0]
|
||||
var args: Array = []
|
||||
if parts.size() > 1:
|
||||
args = Array(parts.slice(1))
|
||||
|
||||
var context := {
|
||||
"commands": commands,
|
||||
"scene_registry": scene_registry,
|
||||
"debug_menu": self,
|
||||
"active_scene_container": active_scene_container,
|
||||
}
|
||||
|
||||
for command: ConsoleCommand in commands:
|
||||
if command.get_command_name() == command_name:
|
||||
return command.run(args, context)
|
||||
|
||||
return _eval_expression(input)
|
||||
|
||||
func _eval_expression(input: String) -> String:
|
||||
var expression := Expression.new()
|
||||
var error := expression.parse(input)
|
||||
if error != OK:
|
||||
return "Parse error: %s" % expression.get_error_text()
|
||||
|
||||
var active_scene: Node = null
|
||||
if active_scene_container.get_child_count() > 0:
|
||||
active_scene = active_scene_container.get_child(0)
|
||||
|
||||
var result = expression.execute([], active_scene)
|
||||
if expression.has_execute_failed():
|
||||
return "Error: %s" % expression.get_error_text()
|
||||
|
||||
return str(result)
|
||||
|
||||
func _show_result(text: String) -> void:
|
||||
result_label.text = text
|
||||
# if _result_tween and _result_tween.is_valid():
|
||||
# _result_tween.kill()
|
||||
# _result_tween = create_tween()
|
||||
# _result_tween.tween_interval(2.0)
|
||||
# _result_tween.tween_callback(func(): result_label.text = "")
|
||||
1
scripts/debug/debug_menu.gd.uid
Normal file
1
scripts/debug/debug_menu.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c64yr8xvkb5cw
|
||||
31
scripts/game.gd
Normal file
31
scripts/game.gd
Normal file
@@ -0,0 +1,31 @@
|
||||
class_name Game extends Node
|
||||
|
||||
@onready var debug_menu: DebugMenu = $DebugMenu
|
||||
@onready var active_scene_container: Node = $ActiveSceneContainer
|
||||
|
||||
var _default_scene: PackedScene = preload("res://scenes/views/main_menu_view.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
debug_menu.active_scene_container = active_scene_container
|
||||
debug_menu.close_requested.connect(_close_debug_menu)
|
||||
debug_menu.visible = false
|
||||
var instance := _default_scene.instantiate()
|
||||
active_scene_container.add_child(instance)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("debug_toggle"):
|
||||
_toggle_debug_menu()
|
||||
|
||||
func _toggle_debug_menu() -> void:
|
||||
if debug_menu.visible:
|
||||
_close_debug_menu()
|
||||
else:
|
||||
_open_debug_menu()
|
||||
|
||||
func _open_debug_menu() -> void:
|
||||
debug_menu.visible = true
|
||||
active_scene_container.process_mode = Node.PROCESS_MODE_DISABLED
|
||||
|
||||
func _close_debug_menu() -> void:
|
||||
debug_menu.visible = false
|
||||
active_scene_container.process_mode = Node.PROCESS_MODE_INHERIT
|
||||
1
scripts/game.gd.uid
Normal file
1
scripts/game.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ifv6cww6fk6c
|
||||
56
scripts/units/unit.gd
Normal file
56
scripts/units/unit.gd
Normal file
@@ -0,0 +1,56 @@
|
||||
class_name Unit extends Node2D
|
||||
|
||||
enum UnitState { ALIVE, DEAD }
|
||||
|
||||
#region Templates
|
||||
@export var stat_template: UnitStats
|
||||
@export var info_template: UnitInfo
|
||||
@export var allegiance_template: UnitAllegiance
|
||||
@export var tactics: Array[CombatTactic] = []
|
||||
#endregion
|
||||
|
||||
var current_stats: UnitStats
|
||||
var current_info: UnitInfo
|
||||
var current_allegiance: UnitAllegiance
|
||||
var state: UnitState = UnitState.ALIVE
|
||||
|
||||
signal unit_selected_changed(unit: Unit, selected: bool)
|
||||
signal unit_allegiance_changed(unit: Unit, allegiance: UnitAllegiance)
|
||||
signal unit_died(unit: Unit)
|
||||
|
||||
func _ready() -> void:
|
||||
current_stats = stat_template.duplicate(true)
|
||||
current_info = info_template.duplicate(true)
|
||||
current_allegiance = allegiance_template.duplicate(true)
|
||||
_append_builtin_tactics()
|
||||
unit_allegiance_changed.emit(self, current_allegiance)
|
||||
|
||||
func _append_builtin_tactics() -> void:
|
||||
var attack := AttackCombatTactic.new()
|
||||
attack.tactic_name = "Attack"
|
||||
attack.tactic_range = UnitMatchingCombatTacticRange.new()
|
||||
tactics.append(attack)
|
||||
|
||||
var defend := DefendCombatTactic.new()
|
||||
defend.tactic_name = "Defend"
|
||||
defend.tactic_range = AnyCombatTacticRange.new()
|
||||
tactics.append(defend)
|
||||
|
||||
func set_selected(selected: bool) -> void:
|
||||
unit_selected_changed.emit(self, selected)
|
||||
|
||||
func is_alive() -> bool:
|
||||
return state == UnitState.ALIVE
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
if state != UnitState.ALIVE:
|
||||
return
|
||||
current_stats.current_hp -= amount
|
||||
if current_stats.current_hp <= 0:
|
||||
current_stats.current_hp = 0
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
state = UnitState.DEAD
|
||||
unit_died.emit(self)
|
||||
queue_free()
|
||||
1
scripts/units/unit.gd.uid
Normal file
1
scripts/units/unit.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c016mxgatcpse
|
||||
12
scripts/units/unit_allegiance.gd
Normal file
12
scripts/units/unit_allegiance.gd
Normal file
@@ -0,0 +1,12 @@
|
||||
class_name UnitAllegiance extends Resource
|
||||
|
||||
enum AllegianceType {
|
||||
PLAYER,
|
||||
ENEMY,
|
||||
PLAYER_ALLY,
|
||||
ENEMY_ALLY,
|
||||
UNAFFILIATED
|
||||
}
|
||||
|
||||
@export var type: AllegianceType
|
||||
@export var color: Color
|
||||
1
scripts/units/unit_allegiance.gd.uid
Normal file
1
scripts/units/unit_allegiance.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bhglsexm8dtpj
|
||||
3
scripts/units/unit_info.gd
Normal file
3
scripts/units/unit_info.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name UnitInfo extends Resource
|
||||
|
||||
@export var name: String = "Unit"
|
||||
1
scripts/units/unit_info.gd.uid
Normal file
1
scripts/units/unit_info.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d37ulss2k0bq5
|
||||
15
scripts/units/unit_stats.gd
Normal file
15
scripts/units/unit_stats.gd
Normal file
@@ -0,0 +1,15 @@
|
||||
class_name UnitStats extends Resource
|
||||
|
||||
@export var max_hp: int = 10
|
||||
@export var max_sp: int = 10
|
||||
@export var max_fs: int = 10
|
||||
@export var phys_atk: int = 10
|
||||
@export var phys_def: int = 5
|
||||
@export var magic_atk: int = 0
|
||||
@export var magic_def: int = 0
|
||||
@export var hit: int = 85
|
||||
@export var atk_range: int = 1
|
||||
@export var spd: int = 1
|
||||
@export var eva: int = 1
|
||||
@export var lck: int = 1
|
||||
@export var mov: int = 3
|
||||
1
scripts/units/unit_stats.gd.uid
Normal file
1
scripts/units/unit_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cydoey8a8nmb8
|
||||
Reference in New Issue
Block a user