Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spawn mob #676

Merged
merged 12 commits into from
Feb 14, 2025
18 changes: 13 additions & 5 deletions Scenes/ContentManager/Custom_Editors/QuestEditor.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,13 @@ Call function: Select a function and add parameters. When the quest reaches this

Enter map: Select the id of a map. This step completes when the player has entered a chunk that is generated from the map with this id

Kill x mobs of type: This step completes when the player has killed x amount of the mob you selected"
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 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 = 5
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 @@ -167,6 +171,10 @@ popup/item_3/text = "Enter map"
popup/item_3/id = 3
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 @@ -227,13 +235,13 @@ size = Vector2i(800, 277)
theme_override_styles/panel = SubResource("StyleBoxFlat_fhs5k")

[node name="StepPropertiesVBoxContainer" type="VBoxContainer" parent="StepPropertiesPopupPanel"]
anchors_preset = -1
anchors_preset = 15
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
132 changes: 109 additions & 23 deletions Scenes/ContentManager/Custom_Editors/Scripts/QuestEditor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ func _on_save_button_button_up() -> void:
if step_type_label.text == "Craft item:":
step["type"] = "craft"
step["item"] = (hbox.get_child(1)).get_text()
elif step_type_label.text == "Spawn item:":
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 @@ -192,6 +200,10 @@ func _on_add_step_button_button_up():
empty_step = {"type": "enter", "map_id": ""}
4: # Kill x mobs of type
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 @@ -345,6 +357,70 @@ func add_kill_step(step: Dictionary) -> HBoxContainer:
return hbox


# Adds a spawn item step. The user configures an item id and an amount
# When this step is reached in-game, the item will spawn on the map where the player is at
func add_spawn_item_step(step: Dictionary) -> HBoxContainer:
var hbox = HBoxContainer.new()
var labelinstance: Label = Label.new()
labelinstance.text = "Spawn item:"
hbox.add_child(labelinstance)

var dropabletextedit_instance: HBoxContainer = dropabletextedit.instantiate()
dropabletextedit_instance.set_text(step["item"])
dropabletextedit_instance.set_meta("step_type", "spawn_item")
dropabletextedit_instance.myplaceholdertext = "Drop an item from the left menu"
dropabletextedit_instance.tooltip_text = "The id of the item to spawn. The item \n" + \
" will spawn on the map that the player is currently on. So if the player is \n" + \
" at the overmap coordinate (-4,2), the item will spawn on the chunk that \n" + \
" represents that coordinate. First, it will try to spawn in a static \n" + \
" furniture container on the same y level as the player. If that's not \n" + \
" available, it will find an empty tile and spawn the item there."
set_drop_functions(dropabletextedit_instance)
hbox.add_child(dropabletextedit_instance)

var spinbox = SpinBox.new()
spinbox.min_value = 1
spinbox.value = step["amount"]
hbox.add_child(spinbox)

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 @@ -385,6 +461,10 @@ func add_step_from_data(step: Dictionary):
hbox = add_enter_step(step)
"kill":
hbox = add_kill_step(step)
"spawn_item":
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 All @@ -396,6 +476,7 @@ func add_step_from_data(step: Dictionary):
steps_container.add_child(hbox)



# Function to handle moving a step up
func _on_move_up_button_pressed(hbox: HBoxContainer):
var index = get_child_index(steps_container, hbox)
Expand Down Expand Up @@ -426,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":
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":
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
95 changes: 95 additions & 0 deletions Scripts/Chunk.gd
Original file line number Diff line number Diff line change
Expand Up @@ -1415,3 +1415,98 @@ func spawn_furniture(furniture_data: Dictionary):
furniture_blueprint_spawner.spawn_furniture(furniture_data)
else:
furniture_static_spawner.spawn_furniture(furniture_data)


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
16 changes: 16 additions & 0 deletions Scripts/FurnitureStaticSpawner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,19 @@ func _on_melee_attacked_rid(body_rid: RID, attack: Dictionary):
var furniturenode: FurnitureStaticSrv = collider_to_furniture[body_rid]
if furniturenode.has_method("get_hit"):
furniturenode.get_hit(attack)

# Function to get all furniture at the specified y level
func get_furniture_at_y_level(target_y_level: float) -> Array[FurnitureStaticSrv]:
var matching_furniture: Array[FurnitureStaticSrv] = [] # Array to store matching furniture

# Loop over all tracked furniture instances
for furniture in collider_to_furniture.values():
if is_instance_valid(furniture):
# Get furniture's snapped y position
var furniture_y = furniture.get_y_position(true)

# If y positions match, add to the array
if furniture_y == target_y_level:
matching_furniture.append(furniture)

return matching_furniture # Return the list of furniture at the specified y level
15 changes: 15 additions & 0 deletions Scripts/FurnitureStaticSrv.gd
Original file line number Diff line number Diff line change
Expand Up @@ -1410,3 +1410,18 @@ func transform_into():

# Remove the instance afterwards and don't add a corpse
_die(false)


# Takes an item_id and quantity and adds it to the inventory
func add_item_to_inventory(item_id: String, quantity: int) -> bool:
# Check if inventory has items and insert them into the new item
if is_container() and container.get_inventory():
container.add_item_to_inventory(item_id, quantity)
return true
return false

# Returns the y position of the furniture.
# If 'snapped' is true, it returns the y position snapped to the nearest integer.
func get_y_position(is_snapped: bool = false) -> float:
var y_pos = furniture_transform.posy
return round(y_pos) if is_snapped else y_pos
Loading