Initial MVP
This commit is contained in:
11
scripts/AudioManager.gd
Normal file
11
scripts/AudioManager.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
extends Node
|
||||
|
||||
# Load streams here
|
||||
# var sfx_hit = preload("res://assets/hit.wav")
|
||||
|
||||
func play_hit():
|
||||
# if sfx_hit: play(sfx_hit)
|
||||
print("Audio: Hit SFX")
|
||||
|
||||
func play_music():
|
||||
print("Audio: Music Started")
|
||||
1
scripts/AudioManager.gd.uid
Normal file
1
scripts/AudioManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5ge63rwtjieh
|
||||
221
scripts/GameManager.gd
Normal file
221
scripts/GameManager.gd
Normal file
@@ -0,0 +1,221 @@
|
||||
extends Node
|
||||
|
||||
signal room_changed(room_data: RoomData)
|
||||
signal log_message(message: String)
|
||||
signal stats_changed(hp: int, max_hp: int, inventory: Array)
|
||||
signal combat_started(enemy: Resource)
|
||||
signal combat_ended(won: bool)
|
||||
signal combat_log(message: String)
|
||||
signal damage_taken(amount: int)
|
||||
|
||||
var game_state: GameState
|
||||
var rooms: Dictionary = {} # id -> RoomData
|
||||
var llm_service: LLMService
|
||||
var audio_manager: Node
|
||||
var current_enemy: Resource = null
|
||||
|
||||
func _ready():
|
||||
llm_service = LLMService.new()
|
||||
add_child(llm_service)
|
||||
llm_service.response_received.connect(_on_llm_response)
|
||||
llm_service.error_occurred.connect(func(msg): emit_signal("log_message", "Error: " + msg))
|
||||
|
||||
audio_manager = load("res://scripts/AudioManager.gd").new()
|
||||
add_child(audio_manager)
|
||||
|
||||
rooms = WorldData.generate_test_world()
|
||||
start_new_game()
|
||||
|
||||
func start_new_game():
|
||||
game_state = GameState.new()
|
||||
game_state.current_room_id = "room_0_0"
|
||||
_update_stats_ui()
|
||||
_load_room(game_state.current_room_id)
|
||||
|
||||
func _load_room(room_id: String):
|
||||
if room_id in rooms:
|
||||
var room = rooms[room_id]
|
||||
|
||||
# Update Exploration
|
||||
if not room_id in game_state.explored_rooms:
|
||||
game_state.explored_rooms.append(room_id)
|
||||
|
||||
emit_signal("room_changed", room)
|
||||
|
||||
# Handle Description
|
||||
if room.generated_description != "":
|
||||
emit_signal("log_message", room.generated_description)
|
||||
else:
|
||||
_request_room_description(room)
|
||||
else:
|
||||
emit_signal("log_message", "Error: Room " + room_id + " not found.")
|
||||
|
||||
func move(direction: String):
|
||||
var current_room = rooms.get(game_state.current_room_id)
|
||||
if current_room and direction in current_room.exits:
|
||||
game_state.current_room_id = current_room.exits[direction]
|
||||
_load_room(game_state.current_room_id)
|
||||
else:
|
||||
emit_signal("log_message", "You can't go that way.")
|
||||
|
||||
func pickup_item(item_name: String):
|
||||
var current_room = rooms[game_state.current_room_id]
|
||||
var item_to_pickup = null
|
||||
|
||||
for item in current_room.items:
|
||||
if item.name.to_lower() in item_name.to_lower() or item.id == item_name.to_lower():
|
||||
item_to_pickup = item
|
||||
break
|
||||
|
||||
if item_to_pickup:
|
||||
current_room.items.erase(item_to_pickup)
|
||||
game_state.inventory.append(item_to_pickup)
|
||||
_update_stats_ui()
|
||||
emit_signal("log_message", "You picked up " + item_to_pickup.name)
|
||||
else:
|
||||
emit_signal("log_message", "There is no " + item_name + " here.")
|
||||
|
||||
func save_game():
|
||||
var error = ResourceSaver.save(game_state, "user://savegame.tres")
|
||||
if error == OK:
|
||||
emit_signal("log_message", "Game Saved.")
|
||||
else:
|
||||
emit_signal("log_message", "Error saving game: " + str(error))
|
||||
|
||||
func load_game():
|
||||
if ResourceLoader.exists("user://savegame.tres"):
|
||||
game_state = ResourceLoader.load("user://savegame.tres")
|
||||
_update_stats_ui()
|
||||
_load_room(game_state.current_room_id)
|
||||
emit_signal("log_message", "Game Loaded.")
|
||||
else:
|
||||
emit_signal("log_message", "No save file found.")
|
||||
|
||||
func _update_stats_ui():
|
||||
emit_signal("stats_changed", game_state.player_hp, game_state.player_max_hp, game_state.inventory)
|
||||
|
||||
# --- Combat System ---
|
||||
|
||||
func start_combat(enemy: Resource):
|
||||
current_enemy = enemy
|
||||
emit_signal("combat_started", enemy)
|
||||
emit_signal("log_message", "Combat started with " + enemy.name + "!")
|
||||
|
||||
func player_attack():
|
||||
if not current_enemy: return
|
||||
|
||||
# Calculate damage (simple for now)
|
||||
var damage = 5 # Base damage
|
||||
# Check for weapon
|
||||
for item in game_state.inventory:
|
||||
if item.effect_type == "DAMAGE":
|
||||
damage += item.effect_value
|
||||
|
||||
current_enemy.hp -= damage
|
||||
audio_manager.play_hit()
|
||||
emit_signal("combat_log", "You hit " + current_enemy.name + " for " + str(damage) + " damage.")
|
||||
|
||||
if current_enemy.hp <= 0:
|
||||
_win_combat()
|
||||
else:
|
||||
_enemy_turn()
|
||||
|
||||
func _enemy_turn():
|
||||
var damage = current_enemy.damage
|
||||
game_state.player_hp -= damage
|
||||
_update_stats_ui()
|
||||
audio_manager.play_hit()
|
||||
emit_signal("damage_taken", damage)
|
||||
emit_signal("combat_log", current_enemy.name + " hits you for " + str(damage) + " damage.")
|
||||
|
||||
if game_state.player_hp <= 0:
|
||||
emit_signal("log_message", "You died!")
|
||||
# Handle death (reload?)
|
||||
|
||||
func _win_combat():
|
||||
emit_signal("combat_log", "You defeated " + current_enemy.name + "!")
|
||||
# Remove enemy from room
|
||||
var room = rooms[game_state.current_room_id]
|
||||
room.enemies.erase(current_enemy)
|
||||
current_enemy = null
|
||||
emit_signal("combat_ended", true)
|
||||
|
||||
func flee():
|
||||
emit_signal("log_message", "You fled!")
|
||||
current_enemy = null
|
||||
emit_signal("combat_ended", false)
|
||||
|
||||
# --- AI Integration ---
|
||||
|
||||
func process_user_input(text: String):
|
||||
emit_signal("log_message", "> " + text)
|
||||
var prompt = _construct_system_prompt()
|
||||
llm_service.send_prompt(prompt, text)
|
||||
|
||||
func _construct_system_prompt() -> String:
|
||||
var room = rooms[game_state.current_room_id]
|
||||
var prompt = """
|
||||
You are the Game Master for a text adventure.
|
||||
Current Room: %s
|
||||
Description: %s
|
||||
Exits: %s
|
||||
|
||||
Your goal is to interpret the user's input and return a JSON object describing the outcome.
|
||||
|
||||
JSON Format:
|
||||
{
|
||||
"narrative": "Description of what happens...",
|
||||
"action": {
|
||||
"type": "MOVE" | "PICKUP" | "COMBAT" | "NONE",
|
||||
"target": "north" | "item_name" | "enemy_name" | null
|
||||
}
|
||||
}
|
||||
|
||||
Rules:
|
||||
- If user says "go north" and north is an exit, return action type "MOVE", target "north".
|
||||
- If user says "attack goblin" and goblin is in room, return action type "COMBAT", target "goblin".
|
||||
- If user says "look", just describe the room in "narrative".
|
||||
- Keep narrative concise (2-3 sentences).
|
||||
- Do NOT list player stats or inventory in the narrative.
|
||||
""" % [room.room_name, room.description, str(room.exits.keys())]
|
||||
return prompt
|
||||
|
||||
func _request_room_description(room: RoomData):
|
||||
var prompt = """
|
||||
You are a creative writer for a text adventure.
|
||||
Describe the following location.
|
||||
Name: %s
|
||||
Base Details: %s
|
||||
Exits: %s
|
||||
|
||||
Output JSON:
|
||||
{
|
||||
"narrative": "The atmospheric description...",
|
||||
"save_as_description": true
|
||||
}
|
||||
""" % [room.room_name, room.description, str(room.exits.keys())]
|
||||
|
||||
llm_service.send_prompt(prompt, "Describe this place.")
|
||||
|
||||
func _on_llm_response(response: Dictionary):
|
||||
if "narrative" in response:
|
||||
emit_signal("log_message", response["narrative"])
|
||||
|
||||
if response.get("save_as_description") == true:
|
||||
var room = rooms[game_state.current_room_id]
|
||||
room.generated_description = response["narrative"]
|
||||
|
||||
if "action" in response:
|
||||
var action = response["action"]
|
||||
match action.get("type"):
|
||||
"MOVE":
|
||||
move(action.get("target"))
|
||||
"PICKUP":
|
||||
pickup_item(action.get("target"))
|
||||
"COMBAT":
|
||||
var target = action.get("target")
|
||||
var room = rooms[game_state.current_room_id]
|
||||
for enemy in room.enemies:
|
||||
if enemy.name.to_lower() in target.to_lower() or enemy.id == target.to_lower():
|
||||
start_combat(enemy)
|
||||
break
|
||||
1
scripts/GameManager.gd.uid
Normal file
1
scripts/GameManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://5hcaskwa0y7h
|
||||
72
scripts/LLMService.gd
Normal file
72
scripts/LLMService.gd
Normal file
@@ -0,0 +1,72 @@
|
||||
class_name LLMService extends Node
|
||||
|
||||
signal response_received(response_dict: Dictionary)
|
||||
signal error_occurred(message: String)
|
||||
|
||||
var http_request: HTTPRequest
|
||||
var api_key: String = ""
|
||||
var api_url: String = "https://openrouter.ai/api/v1/chat/completions"
|
||||
var model: String = "google/gemini-2.5-flash-preview-09-2025"
|
||||
|
||||
func _ready():
|
||||
http_request = HTTPRequest.new()
|
||||
add_child(http_request)
|
||||
http_request.request_completed.connect(_on_request_completed)
|
||||
_load_api_key()
|
||||
|
||||
func _load_api_key():
|
||||
var config = ConfigFile.new()
|
||||
var err = config.load("user://secrets.cfg")
|
||||
if err == OK:
|
||||
api_key = config.get_value("auth", "openrouter_key", "")
|
||||
else:
|
||||
print("No secrets.cfg found. Please create one with [auth] openrouter_key=...")
|
||||
|
||||
func send_prompt(system_prompt: String, user_input: String):
|
||||
if api_key == "":
|
||||
emit_signal("error_occurred", "API Key missing. Please check user://secrets.cfg")
|
||||
return
|
||||
|
||||
var headers = [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer " + api_key,
|
||||
"HTTP-Referer: https://github.com/vibecoding/storyteller",
|
||||
"X-Title: Storyteller"
|
||||
]
|
||||
|
||||
var body = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_input}
|
||||
],
|
||||
"response_format": {"type": "json_object"}
|
||||
}
|
||||
|
||||
var json_body = JSON.stringify(body)
|
||||
var error = http_request.request(api_url, headers, HTTPClient.METHOD_POST, json_body)
|
||||
if error != OK:
|
||||
emit_signal("error_occurred", "HTTP Request failed: " + str(error))
|
||||
|
||||
func _on_request_completed(result, response_code, headers, body):
|
||||
if response_code != 200:
|
||||
emit_signal("error_occurred", "API Error: " + str(response_code) + " " + body.get_string_from_utf8())
|
||||
return
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(body.get_string_from_utf8())
|
||||
if parse_result != OK:
|
||||
emit_signal("error_occurred", "JSON Parse Error")
|
||||
return
|
||||
|
||||
var response = json.get_data()
|
||||
if "choices" in response and response["choices"].size() > 0:
|
||||
var content = response["choices"][0]["message"]["content"]
|
||||
# Parse the content as JSON again since the LLM returns a string containing JSON
|
||||
var content_json = JSON.new()
|
||||
if content_json.parse(content) == OK:
|
||||
emit_signal("response_received", content_json.get_data())
|
||||
else:
|
||||
emit_signal("error_occurred", "LLM returned invalid JSON: " + content)
|
||||
else:
|
||||
emit_signal("error_occurred", "Invalid API response structure")
|
||||
1
scripts/LLMService.gd.uid
Normal file
1
scripts/LLMService.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://drdjx6civ7emn
|
||||
32
scripts/Minimap.gd
Normal file
32
scripts/Minimap.gd
Normal file
@@ -0,0 +1,32 @@
|
||||
extends GridContainer
|
||||
|
||||
var cells: Array[ColorRect] = []
|
||||
|
||||
func _ready():
|
||||
columns = 4
|
||||
for i in range(16):
|
||||
var cell = ColorRect.new()
|
||||
cell.custom_minimum_size = Vector2(40, 40)
|
||||
cell.color = Color.DARK_GRAY
|
||||
add_child(cell)
|
||||
cells.append(cell)
|
||||
|
||||
func update_map(current_room_id: String, explored_rooms: Array):
|
||||
# Parse "room_x_y"
|
||||
var parts = current_room_id.split("_")
|
||||
if parts.size() == 3:
|
||||
var x = int(parts[1])
|
||||
var y = int(parts[2])
|
||||
var index = y * 4 + x
|
||||
|
||||
for i in range(cells.size()):
|
||||
var cell_x = i % 4
|
||||
var cell_y = i / 4
|
||||
var cell_id = "room_%d_%d" % [cell_x, cell_y]
|
||||
|
||||
if i == index:
|
||||
cells[i].color = Color.GREEN # Player
|
||||
elif cell_id in explored_rooms:
|
||||
cells[i].color = Color.LIGHT_GRAY # Explored
|
||||
else:
|
||||
cells[i].color = Color.DARK_GRAY # Unexplored
|
||||
1
scripts/Minimap.gd.uid
Normal file
1
scripts/Minimap.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dly6ncstyio4f
|
||||
41
scripts/WorldData.gd
Normal file
41
scripts/WorldData.gd
Normal file
@@ -0,0 +1,41 @@
|
||||
class_name WorldData
|
||||
|
||||
static func generate_test_world() -> Dictionary:
|
||||
var rooms = {}
|
||||
for x in range(4):
|
||||
for y in range(4):
|
||||
var id = "room_%d_%d" % [x, y]
|
||||
var room = RoomData.new()
|
||||
room.id = id
|
||||
room.room_name = "Room %d,%d" % [x, y]
|
||||
room.description = "A generic room at coordinates %d, %d." % [x, y]
|
||||
|
||||
# Link exits
|
||||
room.exits = {}
|
||||
if x > 0: room.exits["west"] = "room_%d_%d" % [x-1, y]
|
||||
if x < 3: room.exits["east"] = "room_%d_%d" % [x+1, y]
|
||||
if y > 0: room.exits["north"] = "room_%d_%d" % [x, y-1]
|
||||
if y < 3: room.exits["south"] = "room_%d_%d" % [x, y+1]
|
||||
|
||||
# Add test item to room 1,1
|
||||
if x == 1 and y == 1:
|
||||
var sword = ItemData.new()
|
||||
sword.id = "sword"
|
||||
sword.name = "Rusty Sword"
|
||||
sword.description = "A rusty old sword."
|
||||
sword.effect_type = "DAMAGE"
|
||||
sword.effect_value = 10
|
||||
room.items.append(sword)
|
||||
|
||||
# Add test enemy to room 2,2
|
||||
if x == 2 and y == 2:
|
||||
var goblin = load("res://scripts/resources/EnemyData.gd").new()
|
||||
goblin.id = "goblin"
|
||||
goblin.name = "Goblin"
|
||||
goblin.hp = 30
|
||||
goblin.max_hp = 30
|
||||
goblin.damage = 5
|
||||
room.enemies.append(goblin)
|
||||
|
||||
rooms[id] = room
|
||||
return rooms
|
||||
1
scripts/WorldData.gd.uid
Normal file
1
scripts/WorldData.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b5tv4wpx4bm4k
|
||||
85
scripts/main.gd
Normal file
85
scripts/main.gd
Normal file
@@ -0,0 +1,85 @@
|
||||
extends Node2D
|
||||
|
||||
@onready var game_manager = $GameManager
|
||||
@onready var room_label = $UI/MainLayout/Sidebar/RoomLabel
|
||||
@onready var log_label = $UI/MainLayout/GameView/Log
|
||||
@onready var input_field = $UI/MainLayout/GameView/Input
|
||||
@onready var minimap = $UI/MainLayout/Sidebar/Minimap
|
||||
@onready var stats_label = $UI/MainLayout/Sidebar/StatsLabel
|
||||
|
||||
@onready var btn_north = $UI/MainLayout/Sidebar/Controls/BtnNorth
|
||||
@onready var btn_south = $UI/MainLayout/Sidebar/Controls/BtnSouth
|
||||
@onready var btn_east = $UI/MainLayout/Sidebar/Controls/HBox/BtnEast
|
||||
@onready var btn_west = $UI/MainLayout/Sidebar/Controls/HBox/BtnWest
|
||||
@onready var btn_save = $UI/MainLayout/Sidebar/Controls/BtnSave
|
||||
@onready var btn_load = $UI/MainLayout/Sidebar/Controls/BtnLoad
|
||||
|
||||
@onready var combat_ui = $UI/CombatUI
|
||||
@onready var main_layout = $UI/MainLayout
|
||||
@onready var combat_enemy_label = $UI/CombatUI/Panel/VBox/EnemyLabel
|
||||
@onready var btn_attack = $UI/CombatUI/Panel/VBox/Actions/BtnAttack
|
||||
@onready var btn_flee = $UI/CombatUI/Panel/VBox/Actions/BtnFlee
|
||||
|
||||
func _ready():
|
||||
# Connect signals
|
||||
game_manager.room_changed.connect(_on_room_changed)
|
||||
game_manager.log_message.connect(_on_log_message)
|
||||
game_manager.stats_changed.connect(_on_stats_changed)
|
||||
game_manager.combat_started.connect(_on_combat_started)
|
||||
game_manager.combat_ended.connect(_on_combat_ended)
|
||||
game_manager.combat_log.connect(_on_combat_log)
|
||||
game_manager.damage_taken.connect(func(amount): shake_screen())
|
||||
|
||||
btn_north.pressed.connect(func(): game_manager.move("north"))
|
||||
btn_south.pressed.connect(func(): game_manager.move("south"))
|
||||
btn_east.pressed.connect(func(): game_manager.move("east"))
|
||||
btn_west.pressed.connect(func(): game_manager.move("west"))
|
||||
|
||||
btn_save.pressed.connect(game_manager.save_game)
|
||||
btn_load.pressed.connect(game_manager.load_game)
|
||||
|
||||
btn_attack.pressed.connect(game_manager.player_attack)
|
||||
btn_flee.pressed.connect(game_manager.flee)
|
||||
|
||||
input_field.text_submitted.connect(_on_input_submitted)
|
||||
|
||||
# Clear log
|
||||
log_label.text = ""
|
||||
|
||||
func _on_input_submitted(text: String):
|
||||
if text.strip_edges() == "":
|
||||
return
|
||||
game_manager.process_user_input(text)
|
||||
input_field.text = ""
|
||||
|
||||
func _on_room_changed(room_data: RoomData):
|
||||
room_label.text = room_data.room_name
|
||||
minimap.update_map(room_data.id, game_manager.game_state.explored_rooms)
|
||||
|
||||
func _on_log_message(message: String):
|
||||
log_label.text += message + "\n"
|
||||
|
||||
func _on_stats_changed(hp, max_hp, inventory):
|
||||
stats_label.text = "HP: %d/%d\nItems: %d" % [hp, max_hp, inventory.size()]
|
||||
|
||||
func _on_combat_started(enemy):
|
||||
main_layout.visible = false
|
||||
combat_ui.visible = true
|
||||
combat_enemy_label.text = "%s (HP: %d/%d)" % [enemy.name, enemy.hp, enemy.max_hp]
|
||||
|
||||
func _on_combat_ended(won):
|
||||
combat_ui.visible = false
|
||||
main_layout.visible = true
|
||||
|
||||
func _on_combat_log(message):
|
||||
if game_manager.current_enemy:
|
||||
var enemy = game_manager.current_enemy
|
||||
combat_enemy_label.text = "%s (HP: %d/%d)\n%s" % [enemy.name, enemy.hp, enemy.max_hp, message]
|
||||
|
||||
func shake_screen(intensity: float = 10.0, duration: float = 0.5):
|
||||
var tween = create_tween()
|
||||
var ui_node = $UI
|
||||
for i in range(10):
|
||||
var offset = Vector2(randf_range(-intensity, intensity), randf_range(-intensity, intensity))
|
||||
tween.tween_property(ui_node, "offset", offset, duration / 10)
|
||||
tween.tween_property(ui_node, "offset", Vector2.ZERO, duration / 10)
|
||||
1
scripts/main.gd.uid
Normal file
1
scripts/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b71xydu5v87jp
|
||||
8
scripts/resources/EnemyData.gd
Normal file
8
scripts/resources/EnemyData.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name EnemyData extends Resource
|
||||
|
||||
@export var id: String
|
||||
@export var name: String
|
||||
@export var hp: int
|
||||
@export var max_hp: int
|
||||
@export var damage: int
|
||||
@export var image_path: String
|
||||
1
scripts/resources/EnemyData.gd.uid
Normal file
1
scripts/resources/EnemyData.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bqfi5kki2b58t
|
||||
8
scripts/resources/GameState.gd
Normal file
8
scripts/resources/GameState.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
class_name GameState extends Resource
|
||||
|
||||
@export var current_room_id: String
|
||||
@export var player_hp: int = 100
|
||||
@export var player_max_hp: int = 100
|
||||
@export var inventory: Array[ItemData]
|
||||
@export var world_flags: Dictionary = {}
|
||||
@export var explored_rooms: Array[String] = []
|
||||
1
scripts/resources/GameState.gd.uid
Normal file
1
scripts/resources/GameState.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c1q4ns5aoocdq
|
||||
7
scripts/resources/ItemData.gd
Normal file
7
scripts/resources/ItemData.gd
Normal file
@@ -0,0 +1,7 @@
|
||||
class_name ItemData extends Resource
|
||||
|
||||
@export var id: String
|
||||
@export var name: String
|
||||
@export_multiline var description: String
|
||||
@export var effect_type: String # e.g., "HEAL", "DAMAGE"
|
||||
@export var effect_value: int
|
||||
1
scripts/resources/ItemData.gd.uid
Normal file
1
scripts/resources/ItemData.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jr376c28lhu3
|
||||
10
scripts/resources/RoomData.gd
Normal file
10
scripts/resources/RoomData.gd
Normal file
@@ -0,0 +1,10 @@
|
||||
class_name RoomData extends Resource
|
||||
|
||||
@export var id: String
|
||||
@export var room_name: String
|
||||
@export_multiline var description: String
|
||||
@export_multiline var generated_description: String = ""
|
||||
@export var image_path: String
|
||||
@export var exits: Dictionary # { "north": "room_id", ... }
|
||||
@export var items: Array[ItemData]
|
||||
@export var enemies: Array[Resource] # Array[EnemyData]
|
||||
1
scripts/resources/RoomData.gd.uid
Normal file
1
scripts/resources/RoomData.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dp8agph2wjo3a
|
||||
Reference in New Issue
Block a user