Initial MVP
This commit is contained in:
0
assets/.gitkeep
Normal file
0
assets/.gitkeep
Normal file
4
assets/placeholder_room.svg
Normal file
4
assets/placeholder_room.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="800" height="600" fill="#2c3e50"/>
|
||||
<text x="400" y="300" font-family="Arial" font-size="40" fill="white" text-anchor="middle">Room Image Placeholder</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
43
assets/placeholder_room.svg.import
Normal file
43
assets/placeholder_room.svg.import
Normal file
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cyiu5qk0icw4b"
|
||||
path="res://.godot/imported/placeholder_room.svg-f295510d91c8eade54b32d31ec77a34c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/placeholder_room.svg"
|
||||
dest_files=["res://.godot/imported/placeholder_room.svg-f295510d91c8eade54b32d31ec77a34c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
112
docs/vibecoding/Architecture.md
Normal file
112
docs/vibecoding/Architecture.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Storyteller - Technical Architecture
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
The game follows a **Client-Server** model where the Godot game client acts as the frontend and state manager, while the LLM (via OpenRouter) acts as the narrative engine and intent parser.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User Input] -->|Text| UI[UI Layer]
|
||||
UI -->|Signal| GM[GameManager]
|
||||
GM -->|Context + Input| LLM[LLM Service]
|
||||
LLM -->|OpenRouter API| Cloud[AI Model]
|
||||
Cloud -->|JSON Response| LLM
|
||||
LLM -->|Parsed Action| GM
|
||||
GM -->|Update| State[GameState]
|
||||
State -->|Signal| UI
|
||||
State -->|Signal| Visuals[Visual/Audio System]
|
||||
```
|
||||
|
||||
## 2. Core Components
|
||||
|
||||
### 2.1 LLM Service (`LLMService.gd`)
|
||||
* **Responsibility:** Handles HTTP requests to OpenRouter.
|
||||
* **Pattern:** Adapter pattern to allow future swapping to local LLM (Ollama).
|
||||
* **Key Functions:**
|
||||
* `send_prompt(system_prompt: String, user_input: String) -> Dictionary`
|
||||
* `_parse_json(response: String) -> Dictionary`
|
||||
* **Configuration:** API Key stored in `user://secrets.cfg` (not in version control).
|
||||
|
||||
### 2.2 Game Manager (`GameManager.gd`)
|
||||
* **Responsibility:** Central coordinator.
|
||||
* **Logic:**
|
||||
1. Receives user text.
|
||||
2. Constructs a prompt including:
|
||||
* Current Room Description.
|
||||
* Visible Items/Enemies.
|
||||
* Player Inventory.
|
||||
* Valid Actions (Rules).
|
||||
3. Sends to `LLMService`.
|
||||
4. Executes the returned Action (e.g., `move_player`, `pickup_item`, `start_combat`).
|
||||
|
||||
### 2.3 State Manager
|
||||
* **Responsibility:** Single source of truth.
|
||||
* **Implementation:** Godot `Resource` based system for easy saving/loading.
|
||||
|
||||
## 3. Data Architecture
|
||||
|
||||
### 3.1 Game State (`GameState.gd` - Resource)
|
||||
```gdscript
|
||||
class_name GameState extends Resource
|
||||
|
||||
@export var current_room_id: String
|
||||
@export var player_hp: int
|
||||
@export var player_max_hp: int
|
||||
@export var inventory: Array[ItemData] # List of Item Resources
|
||||
@export var world_flags: Dictionary # For tracking puzzle states
|
||||
```
|
||||
|
||||
### 3.2 Room Data (`RoomData.gd` - Resource)
|
||||
```gdscript
|
||||
class_name RoomData extends Resource
|
||||
|
||||
@export var id: String
|
||||
@export var name: String
|
||||
@export var description: String # Base description for LLM context
|
||||
@export var image_path: String # Path to static background
|
||||
@export var exits: Dictionary # { "north": "room_id_2", ... }
|
||||
@export var items: Array[ItemData]
|
||||
@export var enemies: Array[EnemyData]
|
||||
```
|
||||
|
||||
### 3.3 LLM Protocol (JSON Structure)
|
||||
The LLM will be instructed to return JSON in this format:
|
||||
```json
|
||||
{
|
||||
"narrative": "You step into the dark cave. The air is damp...",
|
||||
"action": {
|
||||
"type": "MOVE",
|
||||
"target": "north"
|
||||
},
|
||||
"state_changes": {
|
||||
"player_hp": -5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Core Systems
|
||||
|
||||
### 4.1 Navigation (The 4x4 Grid)
|
||||
* **Map:** A 4x4 grid of `RoomData` resources.
|
||||
* **Minimap:** A UI GridContainer that highlights the `current_room_id`.
|
||||
* **Movement:** Validated against `RoomData.exits`.
|
||||
|
||||
### 4.2 Combat System (Dragon Quest Style)
|
||||
* **Trigger:** When `action.type == "COMBAT"` or player encounters an enemy.
|
||||
* **View:** Switches Main Display to "Combat Mode" (Enemy sprite front & center).
|
||||
* **Flow:**
|
||||
1. **Player Turn:** Choose Attack/Item/Flee.
|
||||
2. **Resolution:** Calculate damage (Simple: `Atk - Def`).
|
||||
3. **Enemy Turn:** Enemy attacks player.
|
||||
4. **Loop:** Until HP <= 0.
|
||||
* **LLM Role:** Describes the combat moves ("You swing your sword...", "The slime wobbles aggressively..."), but the *math* is handled by Godot.
|
||||
|
||||
### 4.3 Inventory
|
||||
* **Data:** Array of `ItemData` resources.
|
||||
* **UI:** List of items. Clicking an item adds "Use [Item Name]" to the input field or triggers a context menu.
|
||||
|
||||
## 5. Tech Stack
|
||||
* **Engine:** Godot 4.2+
|
||||
* **Language:** GDScript
|
||||
* **AI Provider:** OpenRouter (Model: `google/gemini-2.0-flash-exp` or `meta-llama/llama-3-8b-instruct` for speed/cost).
|
||||
* **Version Control:** Git
|
||||
1
docs/vibecoding/NOTES.txt
Normal file
1
docs/vibecoding/NOTES.txt
Normal file
@@ -0,0 +1 @@
|
||||
Place all your vibe coding docs in here, that includes sample data, TODOs, and other notes. README.md and other user tailored docs go in the root docs folder.
|
||||
71
docs/vibecoding/ProjectPlan.md
Normal file
71
docs/vibecoding/ProjectPlan.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Storyteller - Project Plan
|
||||
|
||||
## Phase 1: Foundation (The Grid & State)
|
||||
**Goal:** A walkable 4x4 grid with state persistence, without AI or fancy graphics.
|
||||
|
||||
* [ ] **Setup:** Initialize Git, Godot Project (Done).
|
||||
* [ ] **Data Structures:** Create `RoomData`, `ItemData`, `GameState` resources.
|
||||
* [ ] **Map Creation:** Create 16 `RoomData` assets representing the 4x4 grid. Link them via exits.
|
||||
* [ ] **Game Manager:** Implement `GameManager` to handle `move(direction)` logic.
|
||||
* [ ] **Debug UI:** Simple text label showing "Current Room: [Name]" and buttons for N/S/E/W.
|
||||
* [ ] **State Management:** Implement Save/Load functionality for `GameState`.
|
||||
|
||||
**Testing:**
|
||||
* Verify player can navigate all 16 rooms correctly.
|
||||
* Verify walls (invalid exits) block movement.
|
||||
* Verify state persists after restarting the game.
|
||||
|
||||
## Phase 2: The Brain (AI Integration)
|
||||
**Goal:** Replace hardcoded text with LLM-generated narrative and parsing.
|
||||
|
||||
* [ ] **LLM Service:** Implement `LLMService.gd` with HTTPRequest node.
|
||||
* [ ] **API Integration:** Connect to OpenRouter (test with `curl` first, then Godot).
|
||||
* [ ] **Prompt Engineering:** Design the System Prompt to enforce JSON output.
|
||||
* [ ] **Input Parsing:** Connect User Input Field -> `GameManager` -> `LLMService`.
|
||||
* [ ] **Action Handling:** Parse returned JSON to trigger `move_player` or `get_item`.
|
||||
|
||||
**Testing:**
|
||||
* Verify LLM returns valid JSON.
|
||||
* Verify "Go North" results in `{"type": "MOVE", "target": "north"}`.
|
||||
* Verify narrative text is displayed in the UI.
|
||||
|
||||
## Phase 3: The Body (Visuals & UI)
|
||||
**Goal:** Implement the "Myst-style" visuals and proper UI layout.
|
||||
|
||||
* [ ] **Layout:** Create the Main Scene with `TextureRect` (Background), `RichTextLabel` (Log), `LineEdit` (Input), and `GridContainer` (Minimap).
|
||||
* [ ] **Minimap:** Implement a dynamic 4x4 grid that highlights the current room.
|
||||
* [ ] **Asset Import:** Import placeholder images for rooms.
|
||||
* [ ] **Dynamic Backgrounds:** Update `TextureRect` when changing rooms based on `RoomData.image_path`.
|
||||
|
||||
**Testing:**
|
||||
* Verify background changes on room entry.
|
||||
* Verify minimap updates correctly.
|
||||
* Verify text log scrolls properly.
|
||||
|
||||
## Phase 4: The Conflict (Combat & Items)
|
||||
**Goal:** Add gameplay depth with Inventory and Combat.
|
||||
|
||||
* [ ] **Inventory System:** Implement `pickup_item` and `use_item`. Update UI to show inventory.
|
||||
* [ ] **Combat UI:** Create a separate Control node for Combat (Enemy Sprite, HP Bars, Action Buttons).
|
||||
* [ ] **Combat Logic:** Implement turn-based logic (Player Attack -> Calc Dmg -> Enemy Attack -> Calc Dmg).
|
||||
* [ ] **LLM Combat:** Update System Prompt to handle combat narration ("You strike the goblin!").
|
||||
|
||||
**Testing:**
|
||||
* Verify items can be picked up and removed from the room.
|
||||
* Verify combat loop ends correctly (Win/Loss).
|
||||
* Verify HP updates correctly.
|
||||
|
||||
## Phase 5: Polish & Juice
|
||||
**Goal:** Audio, effects, and refinement.
|
||||
|
||||
* [ ] **Audio Manager:** Add background music and SFX (footsteps, hits).
|
||||
* [ ] **Feedback:** Add screen shake on damage, text typing effect.
|
||||
* [ ] **Bug Fixing:** Edge case handling (LLM hallucinations, invalid JSON).
|
||||
|
||||
## MVP Checklist
|
||||
* [ ] 4x4 Navigable Grid
|
||||
* [ ] LLM Narrative Generation
|
||||
* [ ] JSON Action Parsing
|
||||
* [ ] Basic Inventory (Pickup/Use)
|
||||
* [ ] Turn-based Combat (1 Enemy Type)
|
||||
* [ ] Save/Load System
|
||||
32
docs/vibecoding/The-Big-Idea.md
Normal file
32
docs/vibecoding/The-Big-Idea.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Storyteller - Brain Storming Session
|
||||
|
||||
## What I Want You To Do
|
||||
|
||||
Hello my helpful assistant. Please follow these rules and guidelines during this brain storming session.
|
||||
|
||||
Your role:
|
||||
- You are a senior software engineer with 15 years of experience.
|
||||
- You are a senior game designer with 10 years of experience.
|
||||
- You are a senior AI engineer with 5 years of experience.
|
||||
- Assume the reader is a coder but new to Godot and game development more broadly.
|
||||
- Ask questions frequently.
|
||||
- Do not make assumptions.
|
||||
- Do not make decisions without asking for clarification.
|
||||
|
||||
The Goal:
|
||||
- A comprehensive project plan with detailed architecture and solid reasoning for all decisions made.
|
||||
- The plan is for a solid MVP that demonstrates the core functionality of the game as laid out in the initial concept.
|
||||
- A comprehensive testing plan for each phase of the project.
|
||||
|
||||
## The Big Idea
|
||||
|
||||
A text adventure that uses an AI language model to generate the action. The user is still bound by rules of the game, such as a combat system and inventory etc, but the actions described are handles by the LLM. The same system is also used to turn user dialog into actions the game can take. A graphical component is used to display the game state and allow the user to interact with the game. An audio component is used to provide sound effects and music. The use of a game state should allow for consistent descriptions of items and locations.
|
||||
|
||||
What should the MVP demonstrate?
|
||||
- A core gameplay loop.
|
||||
- Text parsing and generation.
|
||||
- A 4X4 room grid that the user can navigate.
|
||||
- Items that can be picked up and used.
|
||||
- Graphical and audio feedback for actions.
|
||||
- Inventory system.
|
||||
- Basic combat system.
|
||||
1
icon.svg
Normal file
1
icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" height="124" width="124" rx="14" fill="#364d78" stroke="#a5b6d6" stroke-width="4"/><text x="64" y="84" font-size="64" text-anchor="middle" fill="#a5b6d6">GD</text></svg>
|
||||
|
After Width: | Height: | Size: 252 B |
43
icon.svg.import
Normal file
43
icon.svg.import
Normal file
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bhkbndccry064"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
25
project.godot
Normal file
25
project.godot
Normal file
@@ -0,0 +1,25 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Storyteller"
|
||||
run/main_scene="uid://c8j7k6l5m4n3"
|
||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[display]
|
||||
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
58
scenes/combat_ui.tscn
Normal file
58
scenes/combat_ui.tscn
Normal file
@@ -0,0 +1,58 @@
|
||||
[gd_scene format=3 uid="uid://combat_ui_scene"]
|
||||
|
||||
[node name="CombatUI" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Panel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -150.0
|
||||
offset_top = -200.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 200.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="EnemyImage" type="TextureRect" parent="Panel/VBox"]
|
||||
custom_minimum_size = Vector2(200, 200)
|
||||
layout_mode = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="EnemyLabel" type="Label" parent="Panel/VBox"]
|
||||
layout_mode = 2
|
||||
text = "Enemy Name (HP: 100/100)"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="Panel/VBox"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Actions" type="HBoxContainer" parent="Panel/VBox"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="BtnAttack" type="Button" parent="Panel/VBox/Actions"]
|
||||
layout_mode = 2
|
||||
text = "Attack"
|
||||
|
||||
[node name="BtnFlee" type="Button" parent="Panel/VBox/Actions"]
|
||||
layout_mode = 2
|
||||
text = "Flee"
|
||||
107
scenes/main.tscn
Normal file
107
scenes/main.tscn
Normal file
@@ -0,0 +1,107 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://c8j7k6l5m4n3"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||
[ext_resource type="Script" path="res://scripts/GameManager.gd" id="2_gm"]
|
||||
[ext_resource type="Script" path="res://scripts/Minimap.gd" id="3_minimap"]
|
||||
[ext_resource type="Texture2D" path="res://assets/placeholder_room.svg" id="4_bg"]
|
||||
[ext_resource type="PackedScene" uid="uid://combat_ui_scene" path="res://scenes/combat_ui.tscn" id="5_combat"]
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="GameManager" type="Node" parent="."]
|
||||
script = ExtResource("2_gm")
|
||||
|
||||
[node name="UI" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="CombatUI" parent="UI" instance=ExtResource("5_combat")]
|
||||
visible = false
|
||||
|
||||
[node name="MainLayout" type="HBoxContainer" parent="UI"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="GameView" type="VBoxContainer" parent="UI/MainLayout"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 0.7
|
||||
|
||||
[node name="RoomImage" type="TextureRect" parent="UI/MainLayout/GameView"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
texture = ExtResource("4_bg")
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Log" type="RichTextLabel" parent="UI/MainLayout/GameView"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
size_flags_stretch_ratio = 0.4
|
||||
text = "Log..."
|
||||
scroll_following = true
|
||||
|
||||
[node name="Input" type="LineEdit" parent="UI/MainLayout/GameView"]
|
||||
layout_mode = 2
|
||||
placeholder_text = "What do you want to do?"
|
||||
|
||||
[node name="Sidebar" type="VBoxContainer" parent="UI/MainLayout"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 0.3
|
||||
|
||||
[node name="StatsLabel" type="Label" parent="UI/MainLayout/Sidebar"]
|
||||
layout_mode = 2
|
||||
text = "HP: 100/100"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="MinimapLabel" type="Label" parent="UI/MainLayout/Sidebar"]
|
||||
layout_mode = 2
|
||||
text = "Map"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Minimap" type="GridContainer" parent="UI/MainLayout/Sidebar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
script = ExtResource("3_minimap")
|
||||
|
||||
[node name="RoomLabel" type="Label" parent="UI/MainLayout/Sidebar"]
|
||||
layout_mode = 2
|
||||
text = "Room Name"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Controls" type="VBoxContainer" parent="UI/MainLayout/Sidebar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="BtnNorth" type="Button" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
text = "North"
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="BtnWest" type="Button" parent="UI/MainLayout/Sidebar/Controls/HBox"]
|
||||
layout_mode = 2
|
||||
text = "West"
|
||||
|
||||
[node name="BtnEast" type="Button" parent="UI/MainLayout/Sidebar/Controls/HBox"]
|
||||
layout_mode = 2
|
||||
text = "East"
|
||||
|
||||
[node name="BtnSouth" type="Button" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
text = "South"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="BtnSave" type="Button" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
text = "Save Game"
|
||||
|
||||
[node name="BtnLoad" type="Button" parent="UI/MainLayout/Sidebar/Controls"]
|
||||
layout_mode = 2
|
||||
text = "Load Game"
|
||||
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
|
||||
2
secrets.example.cfg
Normal file
2
secrets.example.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[auth]
|
||||
openrouter_key="sk-or-..."
|
||||
Reference in New Issue
Block a user