Skip to content

Commit

Permalink
Merge pull request #676 from snipercup/spawn-mob
Browse files Browse the repository at this point in the history
Spawn mob
  • Loading branch information
snipercup authored Feb 14, 2025
2 parents 016378e + 87ce69e commit 19afba0
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 37 deletions.
12 changes: 8 additions & 4 deletions Scenes/ContentManager/Custom_Editors/QuestEditor.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ Enter map: Select the id of a map. This step completes when the player has enter
Kill x mobs of type: This step completes when the player has killed x amount of the mob you selected
Spawn item: Spawns an item on the chunk that the player is on. First it will try a furniture container and if that's not possible, a free tile."
Spawn item: Spawns an item on the chunk that the player is on. First it will try a furniture container and if that's not possible, a free tile.
Spawn mob: Spawns the selected mob on the nearest map of the selected id. The nearest map with that id will be selected from coordinates that the player hasn't visited."
selected = 0
item_count = 6
item_count = 7
popup/item_0/text = "Craft item"
popup/item_1/text = "Collect x amount of item"
popup/item_1/id = 1
Expand All @@ -171,6 +173,8 @@ popup/item_4/text = "Kill x mobs of type"
popup/item_4/id = 4
popup/item_5/text = "Spawn item (current map)"
popup/item_5/id = 5
popup/item_6/text = "Spawn mob (on target map)"
popup/item_6/id = 6

[node name="AddStepButton" type="Button" parent="VBoxContainer/FormGrid/StepControlsHBoxContainer"]
layout_mode = 2
Expand Down Expand Up @@ -236,8 +240,8 @@ anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 4.0
offset_top = 4.0
offset_right = -4.0
offset_bottom = -4.0
offset_right = 796.0
offset_bottom = 273.0
grow_horizontal = 2
grow_vertical = 2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func _on_sprite_selector_sprite_selected_ok(clicked_sprite) -> void:
# "id": selected_item_id,
# "text": selected_item_text,
# "mod_id": mod_id,
# "contentType": contentType
# "contentType": contentType # an DMod.ContentType
# }
func _can_drop_data(_newpos, data) -> bool:
# Check if the data dictionary has the 'id' property
Expand Down
97 changes: 73 additions & 24 deletions Scenes/ContentManager/Custom_Editors/Scripts/QuestEditor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ func _on_save_button_button_up() -> void:
step["type"] = "spawn_item"
step["item"] = (hbox.get_child(1)).get_text()
step["amount"] = (hbox.get_child(2) as SpinBox).value
elif step_type_label.text == "Spawn mob:":
step["type"] = "spawn_mob"
step["mob"] = (hbox.get_child(1)).get_text()
step["map_id"] = (hbox.get_child(2)).get_text()
elif step_type_label.text == "Collect:":
step["type"] = "collect"
step["item"] = (hbox.get_child(1)).get_text()
Expand Down Expand Up @@ -198,6 +202,8 @@ func _on_add_step_button_button_up():
empty_step = {"type": "kill", "mob": "", "amount": 1}
5: # Spawn item on map
empty_step = {"type": "spawn_item", "item": "", "amount": 1}
6: # Spawn mob on map
empty_step = {"type": "spawn_mob", "mob": "", "map_id": ""}

add_step_from_data(empty_step)

Expand Down Expand Up @@ -379,6 +385,42 @@ func add_spawn_item_step(step: Dictionary) -> HBoxContainer:

return hbox


# Adds a spawn mob step. The user configures a mob id and a map id where it will spawn
func add_spawn_mob_step(step: Dictionary) -> HBoxContainer:
var hbox = HBoxContainer.new()
var labelinstance: Label = Label.new()
labelinstance.text = "Spawn mob:"
hbox.add_child(labelinstance)

# Mob ID (dropable)
var mob_dropabletextedit_instance: HBoxContainer = dropabletextedit.instantiate()
mob_dropabletextedit_instance.set_text(step["mob"])
mob_dropabletextedit_instance.set_meta("step_type", "spawn_mob")
mob_dropabletextedit_instance.myplaceholdertext = "Drop a mob from the left menu"
mob_dropabletextedit_instance.tooltip_text = "The mob that will spawn on the \n" + \
"selected map. The mob will spawn the moment the player gets close enough. \n" + \
"Then the quest will move on to the next step."
set_drop_functions(mob_dropabletextedit_instance)
hbox.add_child(mob_dropabletextedit_instance)

# Map ID (dropable)
var map_dropabletextedit_instance: HBoxContainer = dropabletextedit.instantiate()
map_dropabletextedit_instance.set_text(step["map_id"])
map_dropabletextedit_instance.set_meta("step_type", "spawn_mob_map")
map_dropabletextedit_instance.myplaceholdertext = "Drop a map from the left menu"
map_dropabletextedit_instance.tooltip_text = "The map that the mob will spawn on. \n" + \
"This will select the nearest overmap cell with this map id that's either \n" + \
"hidden or revealed (meaning the player hasn't seen it or hasn't been close \n" + \
"enough). The player will see a marker on the map for the target location. \n" + \
"When the player gets close enough, the marker will disappear, the mob will \n" + \
"spawn and the quest moves on to the next step."
set_drop_functions(map_dropabletextedit_instance)
hbox.add_child(map_dropabletextedit_instance)

return hbox


# This function adds the move up, move down, and delete controls to a step
func add_step_controls(hbox: HBoxContainer, step: Dictionary):
# Create the settings button (⚙️)
Expand Down Expand Up @@ -420,7 +462,9 @@ func add_step_from_data(step: Dictionary):
"kill":
hbox = add_kill_step(step)
"spawn_item":
hbox = add_spawn_item_step(step) # ✅ NEW CASE FOR SPAWN ITEM STEP
hbox = add_spawn_item_step(step)
"spawn_mob":
hbox = add_spawn_mob_step(step)
add_step_controls(hbox, step)

# **Store tip and description in metadata**
Expand Down Expand Up @@ -463,49 +507,54 @@ func get_child_index(container: VBoxContainer, child: Control) -> int:


# Called when the user has successfully dropped data onto the texteditcontrol
# We are expecting a dictionary like this:
# {
# "id": selected_item_id,
# "text": selected_item_text,
# "mod_id": mod_id,
# "contentType": contentType # an DMod.ContentType
# }
func entity_drop(dropped_data: Dictionary, texteditcontrol: HBoxContainer) -> void:
if dropped_data and "id" in dropped_data:
var step_type = texteditcontrol.get_meta("step_type")
var valid_data = false
var entity_type = "" # To store whether it is mob or mobgroup

match step_type:
"craft", "collect", "spawn_item":
valid_data = Gamedata.mods.by_id(dropped_data["mod_id"]).items.has_id(dropped_data["id"])
"kill":
if dropped_data["contentType"] == DMod.ContentType.MOBS:
valid_data = true
entity_type = "mob"
elif dropped_data["contentType"] == DMod.ContentType.MOBGROUPS:
valid_data = true
entity_type = "mobgroup"
"enter":
valid_data = Gamedata.mods.by_id(dropped_data["mod_id"]).maps.has_id(dropped_data["id"])
var content_type: DMod.ContentType = dropped_data.get("contentType", -1)
var mymod: String = dropped_data.get("mod_id", "")
var datainstance: RefCounted = Gamedata.mods.by_id(mymod).get_data_of_type(content_type)

if content_type == DMod.ContentType.MOBS:
entity_type = "mob"
elif content_type == DMod.ContentType.MOBGROUPS:
entity_type = "mobgroup"

if valid_data:
if datainstance.has_id(dropped_data["id"]):
texteditcontrol.set_text(dropped_data["id"])
if step_type == "kill":
# Set metadata to specify if this is a mob or mobgroup
texteditcontrol.set_meta("entity_type", entity_type)


# Determines if the dropped data can be accepted
# We are expecting a dictionary like this:
# {
# "id": selected_item_id,
# "text": selected_item_text,
# "mod_id": mod_id,
# "contentType": contentType # an DMod.ContentType
# }
func can_entity_drop(dropped_data: Dictionary, texteditcontrol: HBoxContainer) -> bool:
if not dropped_data or not dropped_data.has("id"):
return false

var step_type = texteditcontrol.get_meta("step_type")
var valid_data = false

match step_type:
"craft", "collect", "spawn_item":
valid_data = Gamedata.mods.by_id(dropped_data["mod_id"]).items.has_id(dropped_data["id"])
"kill":
valid_data = not Gamedata.mods.get_content_by_id(DMod.ContentType.MOBS, dropped_data["id"]) == null or not Gamedata.mods.get_content_by_id(DMod.ContentType.MOBGROUPS, dropped_data["id"]) == null
"enter":
valid_data = Gamedata.mods.by_id(dropped_data["mod_id"]).maps.has_id(dropped_data["id"])

return valid_data
var content_type: DMod.ContentType = dropped_data.get("contentType", -1)
var mymod: String = dropped_data.get("mod_id", "")
var datainstance: RefCounted = Gamedata.mods.by_id(mymod).get_data_of_type(content_type)

return datainstance.has_id(dropped_data["id"])


# Set the drop functions on the provided control. It should be a dropabletextedit
Expand Down
3 changes: 3 additions & 0 deletions Scenes/Overmap/Scripts/Overmap.gd
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
extends Control

# This script belongs to "overmap.tscn", which is the UI scene displaying the overmap
# This script is intended to display the data from Helper.overmap_manager

@export var positionLabel: Label = null
@export var tilesContainer: Control = null
@export var overmapTile: PackedScene = null
Expand Down
91 changes: 91 additions & 0 deletions Scripts/Chunk.gd
Original file line number Diff line number Diff line change
Expand Up @@ -1419,3 +1419,94 @@ func spawn_furniture(furniture_data: Dictionary):

func get_furniture_at_y_level(target_y_level: float) -> Array[FurnitureStaticSrv]:
return furniture_static_spawner.get_furniture_at_y_level(target_y_level)

# Takes an y cooridnate and tests random positions until a free space has been found
# or until all blocks (1024 at most) are checked.
# Position that have a block (a terrain tile) are skipped
# That leaves only tiles that could have a furniture or mob on them.
func get_free_position_on_level(y: int) -> Vector3:
# Create an array of all possible (x, z) positions on the level
var positions: Array[Vector2i] = []
for x in range(LEVEL_WIDTH):
for z in range(LEVEL_HEIGHT):
positions.append(Vector2i(x, z))

# Shuffle the positions to randomize the search order
positions.shuffle()

# Collision shape representing a 1x1x1 block
var collision_shape = BoxShape3D.new()
collision_shape.extents = Vector3(0.5, 0.5, 0.5)

# Access the physics space for collision checks
var space_state = get_world_3d().direct_space_state
var query_parameters = PhysicsShapeQueryParameters3D.new()
query_parameters.set_shape(collision_shape)
# Set collision mask to check only against layers 1, 2, 3, and 4
query_parameters.collision_mask = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3)

# Iterate over randomized positions
for pos in positions:
var x = pos.x
var z = pos.y

# Skip if a block is present
var block_key = "%s,%s,%s" % [x, y, z]
if block_positions.has(block_key):
continue

# Set the transform to the position in world space
var test_position = mypos + Vector3(x, y+0.5, z)
query_parameters.transform = Transform3D(Basis(), test_position)

# Check for collisions
var result = space_state.intersect_shape(query_parameters)
if result.is_empty():
# Found a free position
return test_position

# No free position found
return Vector3(-1, -1, -1)

# Spawns a mob by its ID at a random free position on the specified Y level.
# Uses `get_free_position_on_level` to find the position.
func spawn_mob_at_free_position(mob_id: String, y: int) -> bool:
var free_position: Vector3 = get_free_position_on_level(y)
if free_position.y == -1:
print_debug("No free position found on level %s for mob %s" % [y, mob_id])
return false

# Construct the mob JSON as expected by Mob.new() (assuming it takes ID and other params)
var mob_json: Dictionary = {"id": mob_id}

# Create the mob and position it correctly
var new_mob: Mob = Mob.new(free_position, mob_json)

# Parent to level_manager because mobs can move across chunks
level_manager.add_child.call_deferred(new_mob)
return true


# Spawns a single item by its ID at a random free position on the specified Y level.
func spawn_item_at_free_position(item_id: String, quantity: int, y: int) -> bool:
var free_position := get_free_position_on_level(y)
if free_position.y == -1:
print_debug("No free position found on level %s for item %s" % [y, item_id])
return false

# Prepare the position data for the item
var item_json: Dictionary = {
"global_position_x": free_position.x,
"global_position_y": free_position.y + 1.01, # Slightly above the ground
"global_position_z": free_position.z
}

# Create the item and add it to the mapitems group
var new_item := ContainerItem.new(item_json)
# Add the actual item to the container's inventory
new_item.add_item(item_id, quantity)
new_item.add_to_group("mapitems")

# Add the item to the scene tree
Helper.map_manager.level_generator.get_tree().get_root().add_child.call_deferred(new_item)
return true
6 changes: 6 additions & 0 deletions Scripts/Gamedata/DQuest.gd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ extends RefCounted
# "tip": "You can find them in town",
# "type": "kill"
# },
# { #spawns a mob as soon as the player gets close to the map with the selected id
# "mob": "big_boss",
# "map_id": "city_square",
# "tip": "The target must be near the city square",
# "type": "spawn_mob"
# },
# {
# "amount": 3,
# "mobgroup": "bandits", # Example mobgroup kill step
Expand Down
18 changes: 17 additions & 1 deletion Scripts/Helper/map_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func get_chunk_from_overmap_coordinate(coordinate: Vector2) -> Chunk:
# the map that the player is currently on. It will make two attempts:
# 1. spawn the item at a random static furniture on the same y level as the player
# We spawn at the same y level to increase the chance that it is reachable
# 2. TODO: Spawn the item on a free tile on the chunk as an ContainerItem
# 2. Spawn the item on a free tile on the chunk as an ContainerItem
func spawn_item_at_current_player_map(item_id: String, quantity: int) -> bool:
var player: Player = Helper.overmap_manager.player
var player_coordinate: Vector2 = Helper.overmap_manager.player_current_cell
Expand All @@ -38,7 +38,23 @@ func spawn_item_at_current_player_map(item_id: String, quantity: int) -> bool:
if container_furniture.size() > 0:
var random_furniture = container_furniture.pick_random()
return random_furniture.add_item_to_inventory(item_id, quantity)

# Attempt to spawn the item on an empty tile
return chunk.spawn_item_at_free_position(item_id,quantity,current_player_y)
return false

# Takes an mob_id (of an RMob) and spawns it onto
# the map that is indicated by the coordinates.
# We use the same Y coordinate as the player but we can change this if it is unreliable
func spawn_mob_at_nearby_map(mob_id: String, coordinates: Vector2) -> bool:
var player: Player = Helper.overmap_manager.player
var chunk: Chunk = get_chunk_from_overmap_coordinate(coordinates)
if not chunk:
return false
var current_player_y: float = player.get_y_position(true)

# Attempt to spawn the item on an empty tile
return chunk.spawn_mob_at_free_position(mob_id, current_player_y)
return false


Expand Down
Loading

0 comments on commit 19afba0

Please sign in to comment.