From 49562ba9cecd2373fd8e9e5e3635d0f53f85e91d Mon Sep 17 00:00:00 2001 From: Layla Manley Date: Tue, 23 Jan 2024 21:45:52 +0100 Subject: [PATCH] General code improvements, progress step tracker implementation for worldgen, and work towards enemy behavior --- assets/tiles.tres | 3 - scripts/v2/creature.gd | 87 ++++++-- scripts/v2/enemy.gd | 89 ++++++-- scripts/v2/loading/progress_step_tracker.gd | 32 +-- scripts/v2/loading/progress_tracker.gd | 49 +++-- scripts/v2/status_effect.gd | 27 +++ scripts/v2/world.gd | 21 +- scripts/v2/worldgen/standard/entity_marker.gd | 68 +++--- scripts/v2/worldgen/standard/room.gd | 146 +++++++++---- scripts/v2/worldgen/standard_generator.gd | 204 +++++++++++------- scripts/v2/worldgen/world_generator.gd | 5 +- 11 files changed, 517 insertions(+), 214 deletions(-) create mode 100644 scripts/v2/status_effect.gd diff --git a/assets/tiles.tres b/assets/tiles.tres index f51cb4d..e78d70b 100644 --- a/assets/tiles.tres +++ b/assets/tiles.tres @@ -72,11 +72,8 @@ texture = ExtResource("3_ca38s") [sub_resource type="TileSetScenesCollectionSource" id="TileSetScenesCollectionSource_mxjkl"] resource_name = "Entity Markers" scenes/1/scene = ExtResource("4_skp8u") -scenes/1/display_placeholder = ExtResource("4_skp8u") scenes/2/scene = ExtResource("5_5hygb") -scenes/2/display_placeholder = ExtResource("5_5hygb") scenes/3/scene = ExtResource("6_m5a4l") -scenes/3/display_placeholder = ExtResource("6_m5a4l") [resource] physics_layer_0/collision_layer = 1 diff --git a/scripts/v2/creature.gd b/scripts/v2/creature.gd index f818754..fac9b03 100644 --- a/scripts/v2/creature.gd +++ b/scripts/v2/creature.gd @@ -1,21 +1,32 @@ class_name Creature -extends Node +extends CharacterBody2D -signal damaged(amount) -signal healed(amount) -signal death() +## Emitted when the creature is damaged +signal damaged(amount: int) +## Emitted when the creature is healed +signal healed(amount: int) +## Emitted when the creature dies +signal death +## Emitted when a status effect is applied to the creature +signal status_effect_applied(effect: StatusEffect) +## Emitted when a status effect is removed from the creature +signal status_effect_removed(effect: StatusEffect) @export_category("Creature") -@export -var max_hp: int = 1 -@export -var death_sound: AudioStream = null +## The creature's maximum health +@export var max_hp: int = 1 -@export -var free_on_death: bool = true +## The sound to play when the creature dies +@export var death_sound: AudioStream = null -var hp: int : +## If true, the creature will be freed when it dies +@export var free_on_death: bool = true + +@export var uses_gravity: bool = true + +## The creature's current health +var hp: int: get: return hp set(value): @@ -27,24 +38,47 @@ var hp: int : self.damaged.emit(old_hp - hp) elif hp > old_hp: self.healed.emit(hp - old_hp) - + if hp == 0: self.death.emit() +var _status_effects: Array = [] + +@onready var default_gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity") + + func _ready() -> void: self.hp = self.max_hp self.death.connect(self._creature_on_death) if self.has_method("_on_ready"): self.call("_on_ready") + +func _physics_process(delta: float) -> void: + if self.uses_gravity: + self.velocity.y += self.default_gravity * delta + if self.is_on_floor(): + self.velocity.y = 0 + self.move_and_slide() + + +func _process(delta: float) -> void: + for status_effect in self._status_effects: + status_effect.process(self, delta) + + +## Damage the creature by the given amount func take_damage(damage: int) -> void: self.hp -= damage + +## Heal the creature by the given amount func heal(amount: int) -> void: self.hp += amount + +## Callback for when the creature dies func _creature_on_death() -> void: - # Play a sound on creature death if self.death_sound: var audio_player = AudioStreamPlayer2D.new() @@ -57,3 +91,30 @@ func _creature_on_death() -> void: # Destroy on death if self.free_on_death: self.queue_free() + + +## Returns true if the creature's health is greater than 0 +func is_alive() -> bool: + return hp > 0 + + +## Reset the creature's health to its maximum +func reset_health() -> void: + self.hp = self.max_hp + + +## Apply a status effect to the creature +func apply_status_effect(status_effect: StatusEffect) -> void: + status_effect.apply(self) + + +## Remove a status effect from the creature +func remove_status_effect(status_effect: StatusEffect) -> void: + status_effect.remove(self) + self._status_effects.erase(status_effect) + + +## Remove all status effects from the creature +func clear_status_effects() -> void: + for status_effect in self._status_effects: + self.remove_status_effect(status_effect) diff --git a/scripts/v2/enemy.gd b/scripts/v2/enemy.gd index e087ab6..c959689 100644 --- a/scripts/v2/enemy.gd +++ b/scripts/v2/enemy.gd @@ -1,59 +1,108 @@ class_name Enemy extends Creature -enum State { - IDLE, - CHASE, - ATTACK -} +enum State { IDLE, CHASE, ATTACK } -@export -var state: State = State.IDLE +@export_category("Behavior") + +@export var state: State = State.IDLE + +## Speed the enemy will chase the player +@export var chase_speed: float = 25 + +## Distance the enemy will chase the player before attacking +@export var attack_distance: float = 10 + +## Maximum distance the enemy will chase the player before giving up +@export var max_chase_distance: float = 200 @export_category("Parts") -@export -var animation_player: AnimationPlayer +## The area that will detect players +@export var animation_player: AnimationPlayer -@export -var sprite: Sprite2D +## The area that will detect players +@export var sprite: Sprite2D -@export -var detection_area: Area2D +@export var detection_area: Area2D var _chase_target: Node2D var _target_is_left: bool = false + func _play_animation(animation: String) -> void: if not animation_player: return if not animation_player.has_animation(animation): return - + if animation_player.is_playing() and animation_player.current_animation == animation: return - + animation_player.play(animation) + func _on_ready() -> void: self.detection_area.body_entered.connect(self._on_detection_area_entered) -func _process(delta: float) -> void: + +func _process(_delta: float) -> void: var animation_name = str(State.keys()[self.state]).to_lower() self._play_animation(animation_name) sprite.flip_h = self._target_is_left + var distance: float = 0 + if self._chase_target: + var target_position: Vector2 = self._chase_target.position + self._target_is_left = target_position.x < self.position.x + distance = target_position.distance_to(self.position) + + match self.state: + State.IDLE: + pass + State.CHASE: + if not self._chase_target: + self.state = State.IDLE + return + + if distance < self.attack_distance: + self.state = State.ATTACK + return + + if distance > self.max_chase_distance: + self._chase_target = null + self.state = State.IDLE + return + + State.ATTACK: + if distance > self.attack_distance: + self.state = State.CHASE + return + + func _physics_process(delta: float) -> void: match self.state: State.CHASE: if not self._chase_target: - self.state = State.IDLE - - self._target_is_left = self._chase_target.position.x < self.position.x + return + + # lmanley: This is super basic movement logic for test + # it will need to be updated to actually be able to + # path, jump, etc. + var target_position: Vector2 = self._chase_target.position + var direction: Vector2 = target_position - self.position + direction = direction.normalized() + self.velocity += direction * self.chase_speed * delta + State.IDLE: + self.velocity.x = 0 + + super._physics_process(delta) + func _on_detection_area_entered(body: CollisionObject2D) -> void: match state: State.IDLE: - if body is Player: + # If not already chasing a target and the body is a player, start chasing + if not self._chase_target and body is Player: self._chase_target = body self.state = State.CHASE diff --git a/scripts/v2/loading/progress_step_tracker.gd b/scripts/v2/loading/progress_step_tracker.gd index 47e1785..0e97d35 100644 --- a/scripts/v2/loading/progress_step_tracker.gd +++ b/scripts/v2/loading/progress_step_tracker.gd @@ -8,43 +8,51 @@ var _subset_count: int = 0 var _tracker: ProgressTracker = null + func _init(tracker: ProgressTracker, step_name: String): self._tracker = tracker self._step_name = step_name -# set_substeps: set the number of substeps for this step + +## Sets the number of substeps for this step func set_substeps(substeps: int): self._subset_count = substeps -# substep: should be called when a substep is started + +## Progresses to the next substep func substep(message: String = ""): self._subset_index += 1 self._message = message self._tracker.progress_update.emit() -# complete: should be called when this step is done + +## Completes this step func complete(): self._tracker.step_complete.emit() -# get_step_name: get the name of this step -func get_step_name() -> String: - return self._step_name -# get_message: get the message for this step +## Returns the name of this step +func get_step_name() -> String: + return self._step_name + + +## Returns the message for this step func get_message() -> String: return self._message -# get_substep_index: get the index of the current substep + +## Returns the index of the current substep func get_substep_index() -> int: return self._subset_index -# get_substep_count: get the number of substeps for this step + +## Returns the number of substeps for this step func get_substep_count() -> int: return self._subset_count -# get_progress: get the progress of this step + +## Returns the progress of this step func get_progress() -> float: if self._subset_count == 0: return 0.0 - else: - return float(self._subset_index) / float(self._subset_count) + return float(self._subset_index) / float(self._subset_count) diff --git a/scripts/v2/loading/progress_tracker.gd b/scripts/v2/loading/progress_tracker.gd index 3c90cb8..bc58a97 100644 --- a/scripts/v2/loading/progress_tracker.gd +++ b/scripts/v2/loading/progress_tracker.gd @@ -1,25 +1,25 @@ class_name ProgressTracker extends Object -signal progress_update() +## Signal emitted when there is a progress update +signal progress_update +## Signal emitted when the current step changes signal step_change(step: int) -signal complete() -signal step_complete() +## Signal emitted when the all steps are completed +signal complete +## Signal emitted when a step is completed +signal step_complete var _current_step: ProgressStepTracker = null var _current_step_index: int = -1 var _steps_count: int + func _init(step_count: int = 0) -> void: _steps_count = step_count - #self.substep_complete.connect(self, "_on_substep_complete") -func _on_step_complete() -> void: - if _current_step_index == _steps_count: - self.complete.emit() - return -# next_step: should be called before the step is actually started +## Progresses to the next step func next_step(step_name: String = "") -> ProgressStepTracker: _current_step_index += 1 if _current_step_index > _steps_count: @@ -30,21 +30,36 @@ func next_step(step_name: String = "") -> ProgressStepTracker: self.progress_update.emit() return self._current_step - -# step_complete: returns the progress of all steps + + +## Returns the progress of the current step func get_step_progress() -> float: return float(_current_step_index) / float(_steps_count) -# get_total_progress_percentage: returns the progress of all steps and substeps -func get_total_progress_percentage() -> float: - return self.get_step_progress() -# get_progress_data: returns a dictionary with data about the current progress for the UI +## Returns the progress of the current step +func get_total_progress_percentage() -> float: + var progress: float = self.get_step_progress() + if self._current_step: + return progress + (self._current_step.get_progress() / self._steps_count) + return progress + + +## Returns a dictionary with the following keys: +## - step_name: the name of the current step +## - message: the message of the current step +## - substeps: the number of substeps in the current step +## - current_substep: the index of the current substep +## - total_progress: the progress of all steps and substeps func get_progress_data() -> Dictionary: + if not _current_step: + return { + "step_name": "", "message": "", "substeps": 0, "current_substep": 0, "total_progress": 0 + } return { "step_name": self._current_step.get_step_name(), "message": self._current_step.get_message(), - "substeps": self._current_step.get_substeps(), + "substeps": self._current_step.get_substep_count(), "current_substep": self._current_step.get_substep_index(), - "total_progress":self.get_total_progress_percentage() + "total_progress": self.get_total_progress_percentage() } diff --git a/scripts/v2/status_effect.gd b/scripts/v2/status_effect.gd new file mode 100644 index 0000000..3e05793 --- /dev/null +++ b/scripts/v2/status_effect.gd @@ -0,0 +1,27 @@ +class_name StatusEffect +extends Resource +## A status effect is a temporary effect that can be applied to a creature. + +## Duration of the status effect in seconds. +@export var duration: float = 0.0 + +## Magnitude of the status effect. +@export var magnitude: float = 0.0 + + +## Apply the status effect to a creature. +func apply(creature: Creature) -> void: + if self.has_method("_apply"): + self.call("_apply", creature) + + +## Remove the status effect from a creature. +func remove(creature: Creature) -> void: + if self.has_method("_remove"): + self.call("_remove", creature) + + +## Tick the status effect on a creature. +func tick(creature: Creature, delta: float) -> void: + if self.has_method("_tick"): + self.call("_tick", creature, delta) diff --git a/scripts/v2/world.gd b/scripts/v2/world.gd index ae51c30..b4f3e2b 100644 --- a/scripts/v2/world.gd +++ b/scripts/v2/world.gd @@ -1,10 +1,23 @@ extends Node -@export -var generator: WorldGenerator +@export var generator: WorldGenerator + +@export var map: TileMap + +var _progress_tracker: ProgressTracker = null -@export -var map: TileMap func _ready() -> void: + self._progress_tracker = self.generator.get_progress_tracker() + self._progress_tracker.progress_update.connect(self._on_progress) self.generator.generate(map) + + +func _on_progress() -> void: + var status = self._progress_tracker.get_progress_data() + print( + ( + "%s (%s): %s" + % [status.step_name, str(int(status.total_progress * 100.0)) + "%", status.message] + ) + ) diff --git a/scripts/v2/worldgen/standard/entity_marker.gd b/scripts/v2/worldgen/standard/entity_marker.gd index 33324ac..3891b9c 100644 --- a/scripts/v2/worldgen/standard/entity_marker.gd +++ b/scripts/v2/worldgen/standard/entity_marker.gd @@ -1,59 +1,67 @@ class_name StandardEntityMarker extends Node2D -enum SPAWN_METHOD { - ALL, - FURTHEST, - ONCE, - NONE -} +## The method used to spawn entities +## +## ALL: Spawns all entities in the list +## FURTHEST: Spawns the entity furthest from the player +## ONCE: Spawns the entity once at a random marker +## NONE: Does not spawn any entities +enum EntitySpawnMethod { ALL, FURTHEST, ONCE, NONE } -enum ENTITY_SELECTION_METHOD { - RANDOM, - POP -} +## The method used to select entities +## +## RANDOM: Selects a random entity from the list +## POP: Removes the entity from the list after it is selected +enum EntitySelectionMethod { RANDOM, POP } @export_category("Entity Spawning") -@export -var marker_id: String +## The ID representing the type of this marker +@export var marker_id: String -@export -var entities: Array[PackedScene] = [] +## The entities that can be spawned by this marker +@export var entities: Array[PackedScene] = [] -@export -var spawn_method: SPAWN_METHOD +## The method used to spawn entities +@export var spawn_method: EntitySpawnMethod -@export_range(0, 1.0) -var spawn_chance: float = 1.0 +## The chance that an entity will be spawned +@export_range(0, 1.0) var spawn_chance: float = 1.0 -@export -var entity_selection_method: ENTITY_SELECTION_METHOD +## The method used to select entities +@export var entity_selection_method: EntitySelectionMethod @export_category("Marker Debug") -@export -var debug_color: Color = Color.WHITE +## The color of the debug sprite +@export var debug_color: Color = Color.WHITE + +## The size of the debug sprite +@export var marker_size: int = 16 -@export -var marker_size: int = 16 func _ready() -> void: self.add_to_group("genv2:entity_marker") - + + ## Create a debug sprite if one doesn't exist var sprite: Sprite2D = self.get_node_or_null("Sprite2D") if not sprite: sprite = Sprite2D.new() sprite.texture = _get_debug_texture() self.add_child(sprite) - -func _register(gen: StandardWorldGenerator) -> void: + + +## Registers this marker with the world generator +func register(gen: StandardWorldGenerator) -> void: if self.marker_id == "": push_error("Marker ID is empty, this marker will not be used for spawning entities.") return - gen._register_marker("genv2:entity_marker:%s" % self.marker_id, self) + gen.register_marker("genv2:entity_marker:%s" % self.marker_id, self) + +## Generates a texture for the debug sprite func _get_debug_texture() -> Texture2D: var texture = GradientTexture2D.new() - + var gradient = Gradient.new() gradient.colors = [self.debug_color] @@ -61,5 +69,5 @@ func _get_debug_texture() -> Texture2D: texture.width = self.marker_size texture.height = self.marker_size - + return texture diff --git a/scripts/v2/worldgen/standard/room.gd b/scripts/v2/worldgen/standard/room.gd index daf451d..cb2ece3 100644 --- a/scripts/v2/worldgen/standard/room.gd +++ b/scripts/v2/worldgen/standard/room.gd @@ -9,18 +9,27 @@ const TILEMAP_LAYER = 0 @export var top: bool = false @export var bottom: bool = false -var room_size: Vector2i -var cell_size: Vector2i +## The rect that the room occupies in the map +## from 0, 0 to room_size.x, room_size.y +var room_size: Vector2i + +## The size of each tile in the room +var cell_size: Vector2i + func _init() -> void: self.room_size = self.get_used_rect().size self.cell_size = self.tile_set.tile_size + +## Returns true when the room has no exits func is_block() -> bool: if self.left or self.right or self.top or self.bottom: return false return true + +## Get the position of the exit in the room func get_exits() -> Array[Vector2i]: var exits: Array[Vector2i] = [] if self.left: @@ -33,6 +42,8 @@ func get_exits() -> Array[Vector2i]: exits.append(Vector2i.DOWN) return exits + +## Get the position of the exit in the room func get_exit_pos(exit: Vector2i) -> Vector2i: var pos: Vector2i = Vector2i.ZERO if exit == Vector2i.LEFT: @@ -45,75 +56,122 @@ func get_exit_pos(exit: Vector2i) -> Vector2i: pos = Vector2i(self.room_size.x / 2, self.room_size.y - 1) return pos + +## Get the offset of the exit position from the edge of the room func get_exit_pos_offset(edge_pos: Vector2i, exit: Vector2i) -> Vector2i: match exit: Vector2i.UP: - return edge_pos + Vector2i(self.room_size.x/-2, self.room_size.y * -1) + return edge_pos + Vector2i(self.room_size.x / -2, self.room_size.y * -1) Vector2i.DOWN: - return edge_pos + Vector2i(self.room_size.x/-2, 1) + return edge_pos + Vector2i(self.room_size.x / -2, 1) Vector2i.LEFT: - return edge_pos + Vector2i(-self.room_size.x, -self.room_size.y/2) + return edge_pos + Vector2i(-self.room_size.x, -self.room_size.y / 2) Vector2i.RIGHT: - return edge_pos + Vector2i(1, -self.room_size.y/2) + return edge_pos + Vector2i(1, -self.room_size.y / 2) _: return Vector2i.ZERO + +## Copy the room to the TileMap at a given position func copy_to_map(map: TileMap) -> void: for x in range(0, self.room_size.x): for y in range(0, self.room_size.y): var tile = self.get_cell_source_id(TILEMAP_LAYER, Vector2i(x, y)) var alt = self.get_cell_alternative_tile(TILEMAP_LAYER, Vector2i(x, y)) - - if tile != -1: - map.set_cell(TILEMAP_LAYER, Vector2i(position.x + x, position.y + y), tile, Vector2i(0, 0), alt) - else: - map.set_cell(TILEMAP_LAYER, Vector2i(position.x + x, position.y + y), -1, Vector2i(0, 0)) + if tile != -1: + map.set_cell( + TILEMAP_LAYER, + Vector2i(position.x + x, position.y + y), + tile, + Vector2i(0, 0), + alt + ) + else: + map.set_cell( + TILEMAP_LAYER, Vector2i(position.x + x, position.y + y), -1, Vector2i(0, 0) + ) + + +## Seal the exit of a room by placing a tile over it func seal_exit(map: TileMap, pos: Vector2i, exit: Vector2i) -> void: var exit_pos = get_exit_pos(exit) var seal_tile_id = 0 - + match exit: Vector2i.UP: - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x - 1, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x + 1, pos.y), seal_tile_id, Vector2i(0, 0)) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x - 1, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x + 1, pos.y), seal_tile_id, Vector2i(0, 0) + ) Vector2i.DOWN: - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x - 1, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x + 1, pos.y), seal_tile_id, Vector2i(0, 0)) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x - 1, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x + 1, pos.y), seal_tile_id, Vector2i(0, 0) + ) Vector2i.RIGHT: - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y - 1), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y + 1), seal_tile_id, Vector2i(0, 0)) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y - 1), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y + 1), seal_tile_id, Vector2i(0, 0) + ) Vector2i.LEFT: - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y - 1), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0)) - map.set_cell(TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y + 1), seal_tile_id, Vector2i(0, 0)) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y - 1), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y), seal_tile_id, Vector2i(0, 0) + ) + map.set_cell( + TILEMAP_LAYER, exit_pos + Vector2i(pos.x, pos.y + 1), seal_tile_id, Vector2i(0, 0) + ) + +## Check if the room overlaps with any tiles in the map func is_overlapping(map: TileMap) -> bool: for x in range(0, self.room_size.x): for y in range(0, self.room_size.y): - var tile = map.get_cell_source_id(TILEMAP_LAYER, Vector2i(self.position.x + x, self.position.y + y)) + var tile = map.get_cell_source_id( + TILEMAP_LAYER, Vector2i(self.position.x + x, self.position.y + y) + ) if tile != -1: return true return false -# Validate room by checking that the top left tile is at 0, 0 and all exits are valid -func _validate() -> bool: - # Check for tiles where x is negative - pass - - # Check for tiles where y is negative - pass - +## Validate room by checking that the top left tile is at 0, 0 and all exits are valid +func _validate() -> bool: # If exit on left, check that the middle tile on the left is empty - if self.left and self.get_cell_source_id(TILEMAP_LAYER, Vector2i(0, self.room_size.y / 2)) != -1: + if ( + self.left + and self.get_cell_source_id(TILEMAP_LAYER, Vector2i(0, self.room_size.y / 2)) != -1 + ): push_error("Room with exit on left must have empty middle tile on left") return false # If exit on right, check that the middle tile on the right is empty - if self.right and self.get_cell_source_id(TILEMAP_LAYER, Vector2i(self.room_size.x - 1, self.room_size.y / 2)) != -1: + if ( + self.right + and ( + self.get_cell_source_id( + TILEMAP_LAYER, Vector2i(self.room_size.x - 1, self.room_size.y / 2) + ) + != -1 + ) + ): push_error("Room with exit on right must have empty middle tile on right") return false # If exit on top, check that the middle tile on the top is empty @@ -121,18 +179,32 @@ func _validate() -> bool: push_error("Room with exit on top must have empty middle tile on top") return false # If exit on bottom, check that the middle tile on the bottom is empty - if self.bottom and self.get_cell_source_id(TILEMAP_LAYER, Vector2i(self.room_size.x / 2, self.room_size.y - 1)) != -1: + if ( + self.bottom + and ( + self.get_cell_source_id( + TILEMAP_LAYER, Vector2i(self.room_size.x / 2, self.room_size.y - 1) + ) + != -1 + ) + ): push_error("Room with exit on bottom must have empty middle tile on bottom") return false - + return true + +## Flip a vector func flip_vector(vector: Vector2i) -> Vector2i: return Vector2i(vector.x * -1, vector.y * -1) + +## Disable a room exit by setting the corresponding exit variable to false func disable_room_exit_opposite(exit: Vector2i) -> void: self.disable_room_exit(self.flip_vector(exit)) + +## Disable a room exit by setting the corresponding exit variable to false func disable_room_exit(exit: Vector2i) -> void: if exit == Vector2i.LEFT: self.left = false diff --git a/scripts/v2/worldgen/standard_generator.gd b/scripts/v2/worldgen/standard_generator.gd index 937f0fd..f0aa6b2 100644 --- a/scripts/v2/worldgen/standard_generator.gd +++ b/scripts/v2/worldgen/standard_generator.gd @@ -1,14 +1,14 @@ class_name StandardWorldGenerator extends WorldGenerator -@export -var rooms: Array[PackedScene] = [] +## Rooms used to generate the map +@export var rooms: Array[PackedScene] = [] -@export -var max_room_path_length: int = 10 +## Maximum length of a path of rooms +@export var max_room_path_length: int = 10 -@export -var spawn_room: PackedScene = null +## Room used to spawn the player +@export var spawn_room: PackedScene = null # Rooms are sorted into these arrays based on their exits # Each PackedScene is a StandardRoom @@ -18,33 +18,48 @@ var _rooms_top: Array[PackedScene] = [] var _rooms_bottom: Array[PackedScene] = [] var _rooms_blocking: Array[PackedScene] = [] +## Number of total generated rooms +var _room_count: int = 0 + +## Progress tracker var _progress_tracker: ProgressTracker +## Marker groups var _markers: Dictionary = {} -func _generate(map: TileMap) -> void: - self._progress_tracker = ProgressTracker.new(1) +func _init() -> void: + self._progress_tracker = ProgressTracker.new(4) + + +## Generates a map +func _generate(map: TileMap) -> void: self._sort_rooms(_progress_tracker.next_step("Sorting Rooms")) # Create initial room - var init_room = self._create_initial_room(map, self.spawn_room, _progress_tracker.next_step("Creating Initial Room")) + var init_room = self._create_initial_room( + map, self.spawn_room, _progress_tracker.next_step("Creating Initial Room") + ) init_room.queue_free.call_deferred() # Create rooms - self._create_rooms(map, init_room, self.max_room_path_length, _progress_tracker.next_step("Creating Rooms")) + self._create_rooms( + map, init_room, self.max_room_path_length, _progress_tracker.next_step("Creating Rooms") + ) map.update_internals() - + self._spawn_entities(map, _progress_tracker.next_step("Spawning Entities")) map.update_internals() + +## Sorts rooms into arrays based on their exits func _sort_rooms(step_tracker: ProgressStepTracker) -> void: step_tracker.complete.call_deferred() - step_tracker.set_substeps(len(self.rooms)) + for room_scene in rooms: - step_tracker.substep(room_scene.get_name()) var room: StandardRoom = room_scene.instantiate() + step_tracker.substep(room.name) if room.left: self._rooms_left.append(room_scene) if room.right: @@ -55,9 +70,9 @@ func _sort_rooms(step_tracker: ProgressStepTracker) -> void: self._rooms_bottom.append(room_scene) if room.is_block(): self._rooms_blocking.append(room_scene) - + room.queue_free() - + if len(self._rooms_left) == 0: push_error("0 left rooms!") if len(self._rooms_right) == 0: @@ -69,27 +84,34 @@ func _sort_rooms(step_tracker: ProgressStepTracker) -> void: if len(self._rooms_blocking) == 0: push_warning("0 blocking rooms!") -# Create initial room -func _create_initial_room(map: TileMap, room: PackedScene, step_tracker: ProgressStepTracker) -> StandardRoom: + +## Create initial room +func _create_initial_room( + map: TileMap, room: PackedScene, step_tracker: ProgressStepTracker +) -> StandardRoom: step_tracker.complete.call_deferred() # Instantiate spawn room var init_room: StandardRoom = room.instantiate() - + # Copy spawn room into map init_room.copy_to_map(map) + self._room_count += 1 return init_room -# Create rooms by randomly selecting from the available rooms in every direction with openings from the current room -# where the maximum path length has not been exceeded and the room does not overlap with any other room. -# -# Each room is marked with boolean flags for each direction that it has an opening to another room. -# -# This is done recursively until the maximum path length is exceeded or there are no more rooms to add -func _create_rooms(map: TileMap, parent_room: StandardRoom, max_path_length: int, step_tracker: ProgressStepTracker) -> void: - step_tracker.complete.call_deferred() +## Creates rooms from the exits of the given room +func _create_rooms( + map: TileMap, parent_room: StandardRoom, max_path_length: int, step_tracker: ProgressStepTracker +) -> void: + if step_tracker.get_substep_count() == 0: + step_tracker.complete.call_deferred() + + ## This is an approximation of the number of rooms that will be created + step_tracker.set_substeps( + (max_path_length * (max_path_length + 1) / 2) * len(parent_room.get_exits()) + ) var exits: Array[Vector2i] = parent_room.get_exits() @@ -98,30 +120,37 @@ func _create_rooms(map: TileMap, parent_room: StandardRoom, max_path_length: int for exit in exits: parent_room.seal_exit(map, parent_room.position, exit) return - + # If there are no more exits, stop if len(exits) == 0: return - + # If there are no more rooms to add, stop - if len(self._rooms_left) == 0 and len(self._rooms_right) == 0 and len(self._rooms_top) == 0 and len(self._rooms_bottom) == 0: + if ( + len(self._rooms_left) == 0 + and len(self._rooms_right) == 0 + and len(self._rooms_top) == 0 + and len(self._rooms_bottom) == 0 + ): push_warning("No more rooms to add!") return - + # For every exit, try to add a room for exit in exits: # Find exit pos in current room var exit_pos: Vector2i = parent_room.get_exit_pos(exit) var room_edge_pos: Vector2i = exit_pos + Vector2i(parent_room.position) - + # Get the rooms that can be added in this direction - var possible_rooms: Array[PackedScene] = self._get_rooms(parent_room.flip_vector(exit)).duplicate() - + var possible_rooms: Array[PackedScene] = ( + self._get_rooms(parent_room.flip_vector(exit)).duplicate() + ) + # If there are no rooms that can be added in this direction, skip it if len(possible_rooms) == 0: continue - + # Get the room to add var room_instance: StandardRoom = null while len(possible_rooms) != 0 and room_instance == null: @@ -133,9 +162,8 @@ func _create_rooms(map: TileMap, parent_room: StandardRoom, max_path_length: int if possible_room.is_overlapping(map): continue - room_instance = possible_room - + if room_instance == null: parent_room.seal_exit(map, parent_room.position, exit) continue @@ -144,63 +172,76 @@ func _create_rooms(map: TileMap, parent_room: StandardRoom, max_path_length: int # Disable direction that the room was added from # since the parent room will already be there room_instance.disable_room_exit_opposite(exit) - + # Copy the room into the map room_instance.copy_to_map(map) - + self._room_count += 1 + step_tracker.substep("%s rooms" % self._room_count) + # Create rooms from the exits of the room that was just added - step_tracker.substep("Creating Rooms") self._create_rooms(map, room_instance, max_path_length - 1, step_tracker) - + # If there are no more exits, stop return -func _get_rooms(exit: Vector2i) -> Array[PackedScene]: - if exit == Vector2i.LEFT: - return self._rooms_left - elif exit == Vector2i.RIGHT: - return self._rooms_right - elif exit == Vector2i.UP: - return self._rooms_top - elif exit == Vector2i.DOWN: - return self._rooms_bottom - else: - push_error("Invalid exit: " + str(exit)) - return [] -func _register_marker(group: String, marker: Node2D) -> void: +## Returns the rooms that can be added in the given direction +func _get_rooms(exit: Vector2i) -> Array[PackedScene]: + match exit: + Vector2i.LEFT: + return self._rooms_left + Vector2i.RIGHT: + return self._rooms_right + Vector2i.UP: + return self._rooms_top + Vector2i.DOWN: + return self._rooms_bottom + push_error("Invalid exit: " + str(exit)) + return [] + + +## Registers a marker with the world generator +func register_marker(group: String, marker: Node2D) -> void: if not group in self._markers: self._markers[group] = [marker] else: self._markers[group].append(marker) -func _spawn_entities(map: TileMap, step_tracker: ProgressStepTracker) -> void: + +## Spawns entities in the map +func _spawn_entities(map: TileMap, _step_tracker: ProgressStepTracker) -> void: + _step_tracker.complete.call_deferred() var tree = map.get_tree() - + # Register entity markers for entity_marker in tree.get_nodes_in_group("genv2:entity_marker"): if entity_marker is StandardEntityMarker: - entity_marker._register(self) - + entity_marker.register(self) + + _step_tracker.set_substeps(len(self._markers.keys())) + for marker_id in self._markers: + _step_tracker.substep.call_deferred(marker_id) var sample_marker: StandardEntityMarker = self._markers[marker_id][0] - + match sample_marker.spawn_method: - StandardEntityMarker.SPAWN_METHOD.ALL: + StandardEntityMarker.EntitySpawnMethod.ALL: for marker in self._markers[marker_id]: map.erase_cell.call_deferred(0, map.local_to_map(marker.position)) - - var packed_entity: PackedScene = _select_entity(marker.entity_selection_method, marker.entities) + + var packed_entity: PackedScene = _select_entity( + marker.entity_selection_method, marker.entities + ) if packed_entity == null: continue - + var entity: Node2D = packed_entity.instantiate() entity.position = marker.position map.add_child(entity) - StandardEntityMarker.SPAWN_METHOD.FURTHEST: + StandardEntityMarker.EntitySpawnMethod.FURTHEST: var furthest_marker: StandardEntityMarker = self._markers[marker_id].pop_back() var furthest_distance: float = furthest_marker.position.distance_to(Vector2.ZERO) - + # Calculate furthest marker and destroy others for marker in self._markers[marker_id]: var calc_distance: float = marker.position.distance_to(Vector2.ZERO) @@ -215,48 +256,57 @@ func _spawn_entities(map: TileMap, step_tracker: ProgressStepTracker) -> void: map.erase_cell.call_deferred(0, map.local_to_map(furthest_marker.position)) # Get entity - var packed_entity: PackedScene = _select_entity(furthest_marker.entity_selection_method, furthest_marker.entities) + var packed_entity: PackedScene = _select_entity( + furthest_marker.entity_selection_method, furthest_marker.entities + ) if packed_entity == null: continue var entity: Node2D = packed_entity.instantiate() entity.position = furthest_marker.position map.add_child(entity) - - - StandardEntityMarker.SPAWN_METHOD.ONCE: + + StandardEntityMarker.EntitySpawnMethod.ONCE: var i = randi() % len(self._markers[marker_id]) var marker = self._markers[marker_id][i] - - var packed_entity: PackedScene = _select_entity(marker.entity_selection_method, marker.entities) + + var packed_entity: PackedScene = _select_entity( + marker.entity_selection_method, marker.entities + ) if packed_entity != null: var entity: Node2D = packed_entity.instantiate() entity.position = marker.position map.add_child(entity) - + for _marker in self._markers[marker_id]: map.erase_cell(0, map.local_to_map(_marker.position)) - - StandardEntityMarker.SPAWN_METHOD.NONE: + + StandardEntityMarker.EntitySpawnMethod.NONE: for marker in self._markers[marker_id]: map.erase_cell(0, map.local_to_map(marker.position)) _: push_error("Invalid spawning method!") continue -func _select_entity(mode: StandardEntityMarker.ENTITY_SELECTION_METHOD, entities: Array) -> PackedScene: + +## Selects an entity from the given array based on the given selection method +func _select_entity( + mode: StandardEntityMarker.EntitySelectionMethod, entities: Array +) -> PackedScene: if len(entities) == 0: return null - + match mode: - StandardEntityMarker.ENTITY_SELECTION_METHOD.RANDOM: + StandardEntityMarker.EntitySelectionMethod.RANDOM: var i = randi() % len(entities) return entities[i] - StandardEntityMarker.ENTITY_SELECTION_METHOD.POP: + StandardEntityMarker.EntitySelectionMethod.POP: var i = randi() % len(entities) return entities.pop_at(i) return null + +## Returns the progress tracker for this world generator func _get_progress_tracker() -> ProgressTracker: return self._progress_tracker diff --git a/scripts/v2/worldgen/world_generator.gd b/scripts/v2/worldgen/world_generator.gd index 1bd3c7a..4bc17b0 100644 --- a/scripts/v2/worldgen/world_generator.gd +++ b/scripts/v2/worldgen/world_generator.gd @@ -1,6 +1,7 @@ class_name WorldGenerator extends Resource +## Generates a world to the given TileMap. func generate(map: TileMap) -> void: if not self.has_method("_generate"): push_error("Generator missing `_generate` method") @@ -8,9 +9,11 @@ func generate(map: TileMap) -> void: self.call("_generate", map) + +## Returns the progress tracker for this generator. func get_progress_tracker() -> ProgressTracker: if not self.has_method("_get_progress_tracker"): push_error("Generator missing `_get_progress_tracker` method") return null - + return self.call("_get_progress_tracker")