Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9689ac3cd | |||
| 6982ded2a1 | |||
| 7b1b09e0c1 |
131
.agents/skills/godot-dev/SKILL.md
Normal file
131
.agents/skills/godot-dev/SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: godot-dev
|
||||
description: Develops Godot 4.5+ games with GDScript. Creates scenes, scripts, resources, and nodes. Use when working on Godot projects, writing GDScript, or building game features.
|
||||
---
|
||||
|
||||
# Godot 4.5+ Development Skill
|
||||
|
||||
## Capabilities
|
||||
- Create and modify GDScript files with proper Godot 4.5 syntax
|
||||
- Design scene hierarchies and node structures
|
||||
- Implement common game patterns (state machines, singletons, signals)
|
||||
- Debug and fix GDScript errors
|
||||
- Create custom resources and exported properties
|
||||
|
||||
## GDScript Quick Reference
|
||||
|
||||
### Script Template
|
||||
```gdscript
|
||||
class_name MyClass
|
||||
extends Node
|
||||
|
||||
signal my_signal(value: int)
|
||||
|
||||
@export var speed: float = 100.0
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
|
||||
var _private_var: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
pass
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
pass
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
pass
|
||||
```
|
||||
|
||||
### Common Node Types
|
||||
- **Node2D/Node3D**: Base spatial nodes
|
||||
- **CharacterBody2D/3D**: Player/enemy controllers
|
||||
- **RigidBody2D/3D**: Physics objects
|
||||
- **Area2D/3D**: Triggers and detection zones
|
||||
- **Control**: UI elements (Button, Label, Panel, etc.)
|
||||
- **AudioStreamPlayer**: Sound effects and music
|
||||
|
||||
### Signal Patterns
|
||||
```gdscript
|
||||
# Declare
|
||||
signal health_changed(new_value: int)
|
||||
|
||||
# Emit
|
||||
health_changed.emit(current_health)
|
||||
|
||||
# Connect in code
|
||||
other_node.health_changed.connect(_on_health_changed)
|
||||
|
||||
# Await
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
var result = await some_signal
|
||||
```
|
||||
|
||||
### Input Handling
|
||||
```gdscript
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("jump"):
|
||||
jump()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var direction = Input.get_vector("left", "right", "up", "down")
|
||||
velocity = direction * speed
|
||||
```
|
||||
|
||||
### Resource Creation
|
||||
```gdscript
|
||||
class_name ItemData
|
||||
extends Resource
|
||||
|
||||
@export var name: String = ""
|
||||
@export var icon: Texture2D
|
||||
@export var value: int = 0
|
||||
@export_multiline var description: String = ""
|
||||
```
|
||||
|
||||
### State Machine Pattern
|
||||
```gdscript
|
||||
enum State { IDLE, WALK, JUMP, ATTACK }
|
||||
var current_state: State = State.IDLE
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
match current_state:
|
||||
State.IDLE:
|
||||
_handle_idle(delta)
|
||||
State.WALK:
|
||||
_handle_walk(delta)
|
||||
State.JUMP:
|
||||
_handle_jump(delta)
|
||||
```
|
||||
|
||||
### Tween Animations
|
||||
```gdscript
|
||||
func fade_out() -> void:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "modulate:a", 0.0, 0.5)
|
||||
await tween.finished
|
||||
queue_free()
|
||||
```
|
||||
|
||||
## Scene Structure Best Practices
|
||||
- Root node named after the scene purpose (Player, Enemy, MainMenu)
|
||||
- Group related nodes under organizational Node2D/Node3D
|
||||
- Use `%UniqueNodeName` syntax for unique node access
|
||||
- Prefer composition over inheritance
|
||||
|
||||
## Debugging Commands
|
||||
```bash
|
||||
# Validate project
|
||||
godot --headless --quit
|
||||
|
||||
# Run with verbose logging
|
||||
godot --verbose
|
||||
|
||||
# Run specific scene
|
||||
godot --path . res://scenes/test.tscn
|
||||
```
|
||||
|
||||
## Common Fixes
|
||||
- **Null reference**: Check `@onready` nodes exist, use `is_instance_valid()`
|
||||
- **Signal not found**: Ensure signal is declared before connecting
|
||||
- **Type errors**: Add explicit type hints, check return types
|
||||
- **Physics issues**: Verify collision layers/masks are set correctly
|
||||
139
AGENTS.md
Normal file
139
AGENTS.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Godot 4.5.1 Development - Agent Configuration
|
||||
|
||||
## Project Info
|
||||
- **Engine**: Godot 4.5.1
|
||||
- **Language**: GDScript (primary), C# (optional)
|
||||
- **Project Root**: Contains `project.godot`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Run project
|
||||
godot --path .
|
||||
|
||||
# Run headless (for validation/testing)
|
||||
godot --headless --quit --script res://path/to/script.gd
|
||||
|
||||
# Export project
|
||||
godot --headless --export-release "preset_name" output_path
|
||||
|
||||
# Validate/check project
|
||||
godot --headless --quit
|
||||
|
||||
# Run specific scene
|
||||
godot --path . res://scenes/main.tscn
|
||||
|
||||
# Generate documentation
|
||||
godot --doctool docs/ --gdscript-docs res://
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### GDScript Conventions
|
||||
- Use `snake_case` for variables, functions, signals
|
||||
- Use `PascalCase` for class names and node types
|
||||
- Use `SCREAMING_SNAKE_CASE` for constants
|
||||
- Prefix private members with `_`
|
||||
- Use type hints: `var speed: float = 100.0`
|
||||
- Use `@export` for editor-exposed variables
|
||||
- Use `@onready` for node references
|
||||
|
||||
### File Naming
|
||||
- Scripts: `snake_case.gd` (e.g., `player_controller.gd`)
|
||||
- Scenes: `snake_case.tscn` (e.g., `main_menu.tscn`)
|
||||
- Resources: `snake_case.tres` (e.g., `player_stats.tres`)
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
res://
|
||||
├── scenes/ # .tscn scene files
|
||||
├── scripts/ # .gd script files
|
||||
├── assets/
|
||||
│ ├── sprites/
|
||||
│ ├── audio/
|
||||
│ └── fonts/
|
||||
├── resources/ # .tres resource files
|
||||
├── autoload/ # Singleton scripts
|
||||
└── addons/ # Third-party plugins
|
||||
```
|
||||
|
||||
## Godot 4.5 Specifics
|
||||
- Use `@export` instead of `export`
|
||||
- Use `@onready` instead of `onready`
|
||||
- Use `super()` instead of `.call()`
|
||||
- Signals: `signal_name.emit()` instead of `emit_signal("signal_name")`
|
||||
- Await: `await signal_name` instead of `yield`
|
||||
- String formatting: `"Value: %s" % value` or `"Value: {val}".format({"val": value})`
|
||||
|
||||
## Testing
|
||||
- Use GUT (Godot Unit Test) for unit testing if available
|
||||
- Test scripts in `res://tests/`
|
||||
- Run tests: `godot --headless --script res://addons/gut/gut_cmdln.gd`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Singleton/Autoload
|
||||
```gdscript
|
||||
# In Project Settings > AutoLoad
|
||||
extends Node
|
||||
|
||||
var game_state: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
pass
|
||||
```
|
||||
|
||||
### Signal Pattern
|
||||
```gdscript
|
||||
signal health_changed(new_health: int)
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
health -= amount
|
||||
health_changed.emit(health)
|
||||
```
|
||||
|
||||
### Resource Pattern
|
||||
```gdscript
|
||||
class_name WeaponData extends Resource
|
||||
|
||||
@export var damage: int = 10
|
||||
@export var fire_rate: float = 0.5
|
||||
```
|
||||
|
||||
## MCP Server (Recommended)
|
||||
Install [Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp) for:
|
||||
- Launch editor programmatically
|
||||
- Run projects and capture debug output
|
||||
- Scene management (create/modify scenes)
|
||||
- UID management (Godot 4.4+)
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
git clone https://github.com/Coding-Solo/godot-mcp
|
||||
cd godot-mcp
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Configuration (add to MCP settings)
|
||||
```json
|
||||
{
|
||||
"godot": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/godot-mcp/build/index.js"],
|
||||
"env": {
|
||||
"GODOT_PATH": "/path/to/godot"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation MCP (Optional)
|
||||
For up-to-date Godot docs access:
|
||||
```json
|
||||
{
|
||||
"godot-docs": {
|
||||
"url": "https://doc-mcp.fly.dev/mcp/"
|
||||
}
|
||||
}
|
||||
```
|
||||
119
TETROMINO_SELECTOR.md
Normal file
119
TETROMINO_SELECTOR.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Tetromino Selector System
|
||||
|
||||
## Overview
|
||||
Adds interactive tetromino selection and repositioning during gameplay. Players can select already-
|
||||
placed tetrominoes, move them, rotate them, and place them in new positions.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. TetrominoSelector (scripts/tetromino_selector.gd)
|
||||
|
||||
Main controller for tetromino selection logic.
|
||||
|
||||
**Features:**
|
||||
- Raycast-based tetromino detection on left-click from camera to world
|
||||
- Real-time position tracking with mouse movement
|
||||
|
||||
- Ghost mode visualization (semi-transparent)
|
||||
- Placement validation using board manager
|
||||
- Rotation with spacebar (45° increments)
|
||||
- Cancellation with right-click or ESC key
|
||||
|
||||
**Signals:**
|
||||
- `tetromino_selected(tetromino: Tetromino)` - Fired when a tetromino is selected
|
||||
- `tetromino_deselected(tetromino: Tetromino)` - Fired when selection is cancelled
|
||||
- `selection_placed(tetromino: Tetromino)` - Fired when tetromino is placed
|
||||
|
||||
**Controls:**
|
||||
- **Left Click** - Select tetromino or place selected tetromino
|
||||
- **Right Click / ESC** - Cancel selection
|
||||
- **Spacebar** - Rotate selected tetromino 45°
|
||||
- **Mouse Movement** - Move ghost tetromino (updates position on Y=0 plane)
|
||||
|
||||
### 2. Tetromino (scripts/tetromino.gd) - Updated
|
||||
Added ghost mode support to tetromino script.
|
||||
|
||||
**New Methods:**
|
||||
- `set_ghost_mode(enabled: bool)` - Enables/disables ghost appearance
|
||||
- When enabled: tetromino becomes 50% transparent with original color
|
||||
- When disabled: tetromino returns to original appearance
|
||||
|
||||
**New Variables:**
|
||||
- `_is_ghost: bool` - Tracks if tetromino is in ghost mode
|
||||
- `_original_color: Color` - Stores original color for restoration
|
||||
|
||||
### 3. Board Scene (scenes/board/board.tscn) - Updated
|
||||
Added TetrominoSelector node to board hierarchy.
|
||||
|
||||
**Changes:**
|
||||
- Added TetrominoSelector as child of Board node
|
||||
- Selector has access to BoardManager3D parent
|
||||
- Integrated into existing board scene structure
|
||||
|
||||
### 4. Tetromino Scene (scenes/board/tetromino.tscn) - Updated
|
||||
Added collision detection for raycast selection.
|
||||
|
||||
**Changes:**
|
||||
- Added SelectionArea (Area3D) node
|
||||
- Added CollisionShape3D with BoxShape3D for raycast detection
|
||||
- Enables click detection on tetromino mesh
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Selection Phase**
|
||||
- Player left-clicks on a placed tetromino
|
||||
- Selector raycasts from camera through mouse position
|
||||
- If tetromino hit, it enters ghost mode (semi-transparent)
|
||||
- Signals emit `tetromino_selected`
|
||||
|
||||
2. **Movement Phase**
|
||||
- Player moves mouse to new position
|
||||
- Ghost tetromino tracks mouse position in real-time
|
||||
- Position snaps to grid coordinates
|
||||
- Clamped to valid board bounds
|
||||
|
||||
3. **Rotation Phase**
|
||||
- Player presses spacebar to rotate ghost 45°
|
||||
- Rotation applied around Y-axis
|
||||
- Updates visual representation
|
||||
|
||||
4. **Placement Phase**
|
||||
- Player left-clicks to place
|
||||
- Selector validates placement using BoardManager3D._can_place_tetromino()
|
||||
- If valid:
|
||||
- Updates board manager occupied cells
|
||||
- Tetromino exits ghost mode
|
||||
- Signals emit `selection_placed`
|
||||
- If invalid:
|
||||
- Tetromino snaps back to original position
|
||||
- Reverts to original rotation
|
||||
|
||||
5. **Cancellation**
|
||||
- Player right-clicks or presses ESC
|
||||
- Tetromino reverts to original position and rotation
|
||||
- Tetromino exits ghost mode
|
||||
- Signals emit `tetromino_deselected`
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Board Manager
|
||||
- Uses `_can_place_tetromino()` for validation
|
||||
- Modifies `_occupied_cells` dictionary directly
|
||||
- Respects grid bounds and collision rules
|
||||
|
||||
### Tetromino Script
|
||||
- Calls `set_ghost_mode()` for visual feedback
|
||||
- Tracks grid_position and rotation_quat
|
||||
- Provides `get_grid_cells()` for collision checking
|
||||
|
||||
### Input System
|
||||
- Listens to input events (_input callback)
|
||||
- Handles mouse clicks, movement, and keyboard input
|
||||
- Calls `get_tree().root.set_input_as_handled()` to consume inputs
|
||||
|
||||
## Notes
|
||||
- Ghost mode requires material initialization to display transparency
|
||||
- Position clamping prevents out-of-bounds placement attempts
|
||||
- Rotation validation happens before placement
|
||||
- Only one tetromino can be selected at a time
|
||||
- Original position/rotation saved for cancellation
|
||||
BIN
assets/models/MSH_1x1.res
Normal file
BIN
assets/models/MSH_1x1.res
Normal file
Binary file not shown.
BIN
assets/models/MSH_1x2_Orizzontale.res
Normal file
BIN
assets/models/MSH_1x2_Orizzontale.res
Normal file
Binary file not shown.
BIN
assets/models/MSH_1x2_Verticale.res
Normal file
BIN
assets/models/MSH_1x2_Verticale.res
Normal file
Binary file not shown.
BIN
assets/models/MSH_1x3.res
Normal file
BIN
assets/models/MSH_1x3.res
Normal file
Binary file not shown.
BIN
assets/models/MSH_T_Corta.res
Normal file
BIN
assets/models/MSH_T_Corta.res
Normal file
Binary file not shown.
101
assets/tetra-tactics.md
Normal file
101
assets/tetra-tactics.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Tetra Tactics
|
||||
|
||||
## Summary
|
||||
|
||||
Tetra Tactics is a strategic roguelike tower-defense.
|
||||
The player defends their position by building modular structures (tetraminos) and
|
||||
**exploiting their sinergies** while waves of enemies fall from above.
|
||||
|
||||
## Experience
|
||||
|
||||
**Primary Feeling**: The satisfaction of strategic planning combined with the
|
||||
thrill of narrowly avoiding disaster ("just barely made it"). The player should
|
||||
feel **clever** and **resourceful**.
|
||||
|
||||
A single encounter has a duration of ~10 minutes, or in general a short duration.
|
||||
|
||||
Depth emerges from the interaction between shape, positioning, timing, and build
|
||||
choices.
|
||||
|
||||
## Inspiration
|
||||
|
||||
- Ball Pit
|
||||
- 9 Kings
|
||||
- Drop Duchy
|
||||
- The King Is Watching
|
||||
- Vampire Survivor
|
||||
|
||||
## Design Pillars
|
||||
|
||||
- **Exploiting Sinergies**: breaking the game is fun
|
||||
- **Modular Depth**: simple tetromino rules combine in unexpected ways;
|
||||
complexity emerges naturally from interaction
|
||||
- **Consequence & Clarity**: threats are telegraphed clearly; every placement
|
||||
has visible impact; failure is immediate and understandable
|
||||
- **Momentum & Escalation**: each successful wave grows your toolkit and unlocks
|
||||
new synergies; pressure mounts steadily without overwhelming
|
||||
|
||||
## Encounter Loop
|
||||
|
||||
1. **Telegraph**: Warning indicators show where next wave enemies will fall.
|
||||
2. **Build & Adapt**:
|
||||
- Draft new shapes (randomly offered).
|
||||
- Place/Rotate units on persistent board.
|
||||
- **Trigger Synergies**: Adjacency bonuses light up (Visual Feedback).
|
||||
3. **Resolution**:
|
||||
- Enemies fall/attack.
|
||||
- Towers fire.
|
||||
- **Player repositions shapes in real-time** to dodge incoming fire.
|
||||
- **Player can active a the main tower skill**
|
||||
4. **Outcome**:
|
||||
- **Defense Breached**: Lose Health (Punishment).
|
||||
- **Defense Holds**: Gain Gold/XP.
|
||||
5. **Evolution**: Buy upgrades or Mutate blocks (Synergy scaling). Threats
|
||||
escalate automatically.
|
||||
6. **Repeat** until Death or Victory.
|
||||
|
||||
## Progression & Motivation
|
||||
|
||||
### Encounter Goals
|
||||
- Encounters consist of a **variable number of waves** (RNG-driven difficulty/length)
|
||||
- Some encounters feature **mini-bosses** mid-run
|
||||
- The **final encounter of a scenario** is a **boss fight**
|
||||
|
||||
### Player Motivation Arc
|
||||
- **Moment-to-Moment**: Hit synergies, exploit combinations, survive threats ("feeling clever")
|
||||
- **Single Run**: Build scaling through gold; adapt to random shapes; defeat
|
||||
mini-bosses and scenario bosses
|
||||
- **Long-Term (Meta)**: Unlock new block types and permanent perks across runs;
|
||||
expand synergy possibilities; enable new strategies in future runs
|
||||
|
||||
### Permanent Progression
|
||||
- **Unlockable Blocks**: New tetromino types unlock over time, creating fresh
|
||||
synergy combinations
|
||||
- **Unlockable Perks**: Passive bonuses that persist across runs, increasing
|
||||
player power and enabling riskier strategies
|
||||
- Progression feeds back into motivation: "What can I break with this new block?"
|
||||
|
||||
## Core Mechanics
|
||||
|
||||
- **Tetromino Placement & Rotation**: Spatial puzzle element; player chooses
|
||||
positioning and orientation on grid
|
||||
- **Adjacency Synergies**: Shapes trigger bonuses when placed next to
|
||||
complementary pieces; core depth driver
|
||||
- **Persistent Board State**: Board carries between waves; placement decisions
|
||||
compound and escalate over time
|
||||
- **Shape Drafting (Random)**: Player selects from randomly offered tetrominos
|
||||
or mutations; forces adaptive strategy
|
||||
- **Real-Time Repositioning**: During enemy attack, player moves shapes to dodge
|
||||
and optimize defense in slow motion
|
||||
- **Encounter Market**: Shapes or Upgrades purchased with gold; modify base
|
||||
stats and effects;
|
||||
- **Resource Economy**: Gold collection from successful waves → purchases upgrades/mutations;
|
||||
paces progression
|
||||
- **Automatic Wave Escalation**: Threats increase autonomously each wave;
|
||||
creates growing pressure
|
||||
- **Enemy Telegraph**: Visible threat indicators before waves;
|
||||
enables defensive planning
|
||||
- **Mini-Bosses & Bosses**: Special encounters with unique attacks that twist
|
||||
standard wave mechanics
|
||||
- **Perk System**: Permanent unlocks that affect all game state (damage, costs,
|
||||
synergy bonuses, etc.); unlock through meta-progression
|
||||
@@ -14,6 +14,45 @@ config/name="tetra-tactics"
|
||||
config/features=PackedStringArray("4.5", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
EventBus="*res://scenes/event_bus.tscn"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1280
|
||||
window/size/viewport_height=720
|
||||
|
||||
[input]
|
||||
|
||||
movement={
|
||||
"deadzone": 0.21,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":65,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
rotate={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
confirm={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
3d_physics/layer_1="ground"
|
||||
3d_physics/layer_2="tetrominos"
|
||||
3d_physics/layer_3="enemies"
|
||||
3d_physics/layer_4="projectiles"
|
||||
3d_physics/layer_5="selection"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
|
||||
9
resources/data/tetrominos/i_piece.tres
Normal file
9
resources/data/tetrominos/i_piece.tres
Normal file
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=3 format=3 uid="uid://curn7voye0ewx"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_suu2q"]
|
||||
[ext_resource type="ArrayMesh" uid="uid://dev1jigsm4i4e" path="res://assets/models/MSH_1x2_Orizzontale.res" id="1_tot0g"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_suu2q")
|
||||
mesh = ExtResource("1_tot0g")
|
||||
metadata/_custom_type_script = "uid://p168a1urs4im"
|
||||
8
resources/data/tetrominos/o_piece.tres
Normal file
8
resources/data/tetrominos/o_piece.tres
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=2 format=3 uid="uid://cot0k1cs02ds3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_s1xhx"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_s1xhx")
|
||||
shape_type = &"O"
|
||||
metadata/_custom_type_script = "uid://p168a1urs4im"
|
||||
9
resources/data/tetrominos/t_piece.tres
Normal file
9
resources/data/tetrominos/t_piece.tres
Normal file
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="TetrominoDefinition" load_steps=3 format=3 uid="uid://cqgsjnof4dly0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://p168a1urs4im" path="res://scripts/tetromino_definition.gd" id="1_0geyk"]
|
||||
[ext_resource type="ArrayMesh" uid="uid://5jb2cluw5746" path="res://assets/models/MSH_T_Corta.res" id="1_il264"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_0geyk")
|
||||
mesh = ExtResource("1_il264")
|
||||
metadata/_custom_type_script = "uid://p168a1urs4im"
|
||||
7
resources/data/waves/wave_1.tres
Normal file
7
resources/data/waves/wave_1.tres
Normal file
@@ -0,0 +1,7 @@
|
||||
[gd_resource type="Resource" script_class="WaveConfig" load_steps=2 format=3 uid="uid://ccwgq3vxplge1"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0xlr04kjhr22" path="res://scripts/wave_config.gd" id="1_vphdf"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_vphdf")
|
||||
metadata/_custom_type_script = "uid://c0xlr04kjhr22"
|
||||
7
resources/data/waves/wave_2.tres
Normal file
7
resources/data/waves/wave_2.tres
Normal file
@@ -0,0 +1,7 @@
|
||||
[gd_resource type="Resource" script_class="WaveConfig" load_steps=2 format=3 uid="uid://bau2lnnbf88ih"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0xlr04kjhr22" path="res://scripts/wave_config.gd" id="1_sn1sd"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_sn1sd")
|
||||
metadata/_custom_type_script = "uid://c0xlr04kjhr22"
|
||||
7
resources/data/waves/wave_3.tres
Normal file
7
resources/data/waves/wave_3.tres
Normal file
@@ -0,0 +1,7 @@
|
||||
[gd_resource type="Resource" script_class="WaveConfig" load_steps=2 format=3 uid="uid://owybnsnvwjbf"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0xlr04kjhr22" path="res://scripts/wave_config.gd" id="1_g8gee"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_g8gee")
|
||||
metadata/_custom_type_script = "uid://c0xlr04kjhr22"
|
||||
32
scenes/board/board.tscn
Normal file
32
scenes/board/board.tscn
Normal file
@@ -0,0 +1,32 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://j3vihw63lw7q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bvxt8mmibr1uj" path="res://scripts/board_manager.gd" id="1_jnyf0"]
|
||||
[ext_resource type="PackedScene" uid="uid://btdnshtrnejt" path="res://scenes/board/tetromino.tscn" id="2_jnyf0"]
|
||||
[ext_resource type="Script" uid="uid://bv7xi75mklk7d" path="res://scripts/tetromino_selector.gd" id="3_jnyf0"]
|
||||
[ext_resource type="Resource" uid="uid://cqgsjnof4dly0" path="res://resources/data/tetrominos/t_piece.tres" id="3_q07he"]
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_s78fa"]
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[node name="Board" type="Node3D"]
|
||||
script = ExtResource("1_jnyf0")
|
||||
|
||||
[node name="GridVisuals" type="Node3D" parent="."]
|
||||
|
||||
[node name="GridPlane" type="Node3D" parent="GridVisuals"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="GridVisuals/GridPlane"]
|
||||
transform = Transform3D(8, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0)
|
||||
mesh = SubResource("PlaneMesh_s78fa")
|
||||
|
||||
[node name="GridLines" type="Node3D" parent="GridVisuals"]
|
||||
|
||||
[node name="MultiMeshInstance3D" type="MultiMeshInstance3D" parent="GridVisuals/GridLines"]
|
||||
|
||||
[node name="TetrominoContainer" type="Node3D" parent="."]
|
||||
|
||||
[node name="Tetromino" parent="TetrominoContainer" instance=ExtResource("2_jnyf0")]
|
||||
resource = ExtResource("3_q07he")
|
||||
|
||||
[node name="TetrominoSelector" type="Node3D" parent="."]
|
||||
script = ExtResource("3_jnyf0")
|
||||
23
scenes/board/board.tscn916401494.tmp
Normal file
23
scenes/board/board.tscn916401494.tmp
Normal file
@@ -0,0 +1,23 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://j3vihw63lw7q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bvxt8mmibr1uj" path="res://scripts/board_manager.gd" id="1_jnyf0"]
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_s78fa"]
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[node name="Board" type="Node3D"]
|
||||
script = ExtResource("1_jnyf0")
|
||||
|
||||
[node name="GridVisuals" type="Node3D" parent="."]
|
||||
|
||||
[node name="GridPlane" type="Node3D" parent="GridVisuals"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="GridVisuals/GridPlane"]
|
||||
transform = Transform3D(8, 0, 0, 0, 4, 0, 0, 0, 14, 0, 0, 0)
|
||||
mesh = SubResource("PlaneMesh_s78fa")
|
||||
|
||||
[node name="GridLines" type="Node3D" parent="GridVisuals"]
|
||||
|
||||
[node name="MultiMeshInstance3D" type="MultiMeshInstance3D" parent="GridVisuals/GridLines"]
|
||||
|
||||
[node name="TetrominoContainer" type="Node3D" parent="."]
|
||||
29
scenes/board/tetromino.tscn
Normal file
29
scenes/board/tetromino.tscn
Normal file
@@ -0,0 +1,29 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://btdnshtrnejt"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c8041v2usigk4" path="res://scripts/tetromino.gd" id="1_hprdj"]
|
||||
[ext_resource type="Resource" uid="uid://curn7voye0ewx" path="res://resources/data/tetrominos/i_piece.tres" id="2_f3wyc"]
|
||||
[ext_resource type="ArrayMesh" uid="uid://cp721yqfdga1n" path="res://assets/models/MSH_T_Corta.res" id="3_f3wyc"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_f3wyc"]
|
||||
|
||||
[node name="Tetromino" type="Node3D"]
|
||||
script = ExtResource("1_hprdj")
|
||||
resource = ExtResource("2_f3wyc")
|
||||
|
||||
[node name="Visuals" type="Node3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Visuals"]
|
||||
layers = 2
|
||||
mesh = ExtResource("3_f3wyc")
|
||||
skeleton = NodePath("../..")
|
||||
|
||||
[node name="SelectionArea" type="Area3D" parent="."]
|
||||
collision_layer = 16
|
||||
collision_mask = 0
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="SelectionArea"]
|
||||
shape = SubResource("BoxShape3D_f3wyc")
|
||||
|
||||
[connection signal="input_event" from="SelectionArea" to="." method="_on_selection_area_input_event"]
|
||||
[connection signal="mouse_entered" from="SelectionArea" to="." method="_on_selection_area_mouse_entered"]
|
||||
[connection signal="mouse_exited" from="SelectionArea" to="." method="_on_selection_area_mouse_exited"]
|
||||
19
scenes/encounter.tscn
Normal file
19
scenes/encounter.tscn
Normal file
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://di7iohkgtosmm"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dv2k6b3b3tfa6" path="res://scripts/encounter_manager.gd" id="1_lp88p"]
|
||||
[ext_resource type="PackedScene" uid="uid://j3vihw63lw7q" path="res://scenes/board/board.tscn" id="2_eu4s7"]
|
||||
|
||||
[node name="Encounter" type="Node3D"]
|
||||
script = ExtResource("1_lp88p")
|
||||
|
||||
[node name="Board" parent="." instance=ExtResource("2_eu4s7")]
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.64278764, 0.76604444, 0, -0.76604444, 0.64278764, 0, 7.431855, 7.5)
|
||||
fov = 60.0
|
||||
near = 0.1
|
||||
far = 1000.0
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.90507954, 0.3257545, -0.27334046, 0, 0.64278764, 0.76604444, 0.42524227, -0.6933311, 0.58177394, 7.1817727, 10, 5)
|
||||
shadow_enabled = true
|
||||
6
scenes/event_bus.tscn
Normal file
6
scenes/event_bus.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ibl8sdj1wp6t"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cbus31k0tae6i" path="res://scripts/event_bus.gd" id="1_7xt3l"]
|
||||
|
||||
[node name="Node" type="Node"]
|
||||
script = ExtResource("1_7xt3l")
|
||||
285
scripts/board_manager.gd
Normal file
285
scripts/board_manager.gd
Normal file
@@ -0,0 +1,285 @@
|
||||
## Manages the 3D game board state, tetromino placement, and grid operations.
|
||||
##
|
||||
## Provides 3D grid representation, tetromino placement, collision detection,
|
||||
## rotation logic, and real-time re positioning during combat.
|
||||
class_name BoardManager3D
|
||||
extends Node3D
|
||||
|
||||
# Signals
|
||||
signal tetromino_placed(tetromino: Tetromino)
|
||||
signal tetromino_moved(tetromino: Tetromino, old_position: Vector3, new_position: Vector3)
|
||||
signal tetromino_rotated(tetromino: Tetromino, rotation: Quaternion)
|
||||
signal placement_invalid(reason: String)
|
||||
|
||||
# Grid configuration
|
||||
@export var grid_size: Vector3i = Vector3i(8, 5, 8) # Width (X), Height (Y), Depth (Z)
|
||||
@export var cell_size: float = 1.0
|
||||
@export var placement_plane_y: float = 0.0 # Y-position where tetrominoes are placed
|
||||
|
||||
# Internal state
|
||||
var _grid: Dictionary = {} # Key: Vector3i, Value: GridCell3D
|
||||
var _tetrominoes: Array[Tetromino] = []
|
||||
var _occupied_cells: Dictionary = {} # Key: Vector3i, Value: Tetromino3D reference
|
||||
var selected_tetromino: Tetromino
|
||||
|
||||
# Ray casting for placement validation
|
||||
var _space_state: PhysicsDirectSpaceState3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_space_state = get_world_3d().direct_space_state
|
||||
_initialize_grid()
|
||||
|
||||
# Connect to every tetrominos already present on board
|
||||
var curr_tetrominos = $TetrominoContainer.get_children();
|
||||
for t in curr_tetrominos:
|
||||
t.selected.connect(_on_tetromino_selected)
|
||||
t.deselected.connect(_on_tetromino_deselected)
|
||||
|
||||
func _on_tetromino_selected(tetromino: Tetromino):
|
||||
print("Tetromino selected")
|
||||
selected_tetromino = tetromino
|
||||
|
||||
func _on_tetromino_deselected(tetromino: Tetromino):
|
||||
print("Tetromino deselected")
|
||||
selected_tetromino = null
|
||||
|
||||
|
||||
## Initializes the 3D grid structure with empty cells.
|
||||
func _initialize_grid() -> void:
|
||||
for x in range(grid_size.x):
|
||||
for y in range(grid_size.y):
|
||||
for z in range(grid_size.z):
|
||||
var cell_pos = Vector3i(x, y, z)
|
||||
var cell = GridCell.new()
|
||||
cell.position = cell_pos
|
||||
_grid[cell_pos] = cell
|
||||
|
||||
|
||||
## Converts world position to grid coordinates.
|
||||
func world_to_grid(world_pos: Vector3) -> Vector3i:
|
||||
var local_pos = world_pos - (Vector3(grid_size) * cell_size / 2.0)
|
||||
return Vector3i(
|
||||
int(local_pos.x / cell_size),
|
||||
int(local_pos.y / cell_size),
|
||||
int(local_pos.z / cell_size)
|
||||
)
|
||||
|
||||
|
||||
## Converts grid coordinates to world position.
|
||||
func grid_to_world(grid_pos: Vector3i) -> Vector3:
|
||||
var center_offset = Vector3(grid_size) * cell_size / 2.0
|
||||
return Vector3(grid_pos) * cell_size + center_offset
|
||||
|
||||
|
||||
## Checks if a grid position is within bounds.
|
||||
func is_within_bounds(grid_pos: Vector3i) -> bool:
|
||||
return (grid_pos.x >= 0 and grid_pos.x < grid_size.x and
|
||||
grid_pos.y >= 0 and grid_pos.y < grid_size.y and
|
||||
grid_pos.z >= 0 and grid_pos.z < grid_size.z)
|
||||
|
||||
|
||||
## Checks if a grid cell is empty and unoccupied.
|
||||
func is_cell_empty(grid_pos: Vector3i) -> bool:
|
||||
if not is_within_bounds(grid_pos):
|
||||
return false
|
||||
return grid_pos not in _occupied_cells
|
||||
|
||||
|
||||
## Attempts to place a tetromino at the given grid position with optional rotation.
|
||||
func place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rot: Quaternion = Quaternion.IDENTITY) -> bool:
|
||||
# Validate placement
|
||||
if not _can_place_tetromino(tetromino, grid_position, rot):
|
||||
placement_invalid.emit("Invalid placement: overlapping or out of bounds")
|
||||
return false
|
||||
|
||||
# Update tetromino state
|
||||
tetromino.grid_position = grid_position
|
||||
tetromino.rotation_quat = rot
|
||||
tetromino.global_position = grid_to_world(grid_position)
|
||||
|
||||
# Mark cells as occupied
|
||||
var cells = tetromino.get_grid_cells(grid_position)
|
||||
for cell_pos in cells:
|
||||
_occupied_cells[cell_pos] = tetromino
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = tetromino
|
||||
|
||||
_tetrominoes.append(tetromino)
|
||||
tetromino_placed.emit(tetromino)
|
||||
return true
|
||||
|
||||
|
||||
## Moves a tetromino to a new position (used during combat).
|
||||
func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool:
|
||||
#if tetromino not in _tetrominoes:
|
||||
#return false
|
||||
|
||||
# Clear old occupation
|
||||
var old_cells = tetromino.get_grid_cells(tetromino.grid_position)
|
||||
for cell_pos in old_cells:
|
||||
_occupied_cells.erase(cell_pos)
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = null
|
||||
|
||||
## Validate new position
|
||||
#if not _can_place_tetromino(tetromino, new_grid_position, tetromino.rotation_quat):
|
||||
## Restore old occupation
|
||||
#for cell_pos in old_cells:
|
||||
#_occupied_cells[cell_pos] = tetromino
|
||||
#if cell_pos in _grid:
|
||||
#_grid[cell_pos].owner = tetromino
|
||||
#placement_invalid.emit("Cannot move to target position")
|
||||
#return false
|
||||
|
||||
# Update position
|
||||
var old_position = tetromino.grid_position
|
||||
tetromino.grid_position = new_grid_position
|
||||
tetromino.global_position = grid_to_world(new_grid_position)
|
||||
|
||||
# Mark cells as occupied
|
||||
var new_cells = tetromino.get_grid_cells(new_grid_position)
|
||||
for cell_pos in new_cells:
|
||||
_occupied_cells[cell_pos] = tetromino
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = tetromino
|
||||
|
||||
tetromino_moved.emit(tetromino, grid_to_world(old_position), tetromino.global_position)
|
||||
return true
|
||||
|
||||
|
||||
## Rotates a tetromino around the Y-axis (XZ plane rotation).
|
||||
func rotate_tetromino_y(tetromino: Tetromino, angle: float) -> bool:
|
||||
if tetromino not in _tetrominoes:
|
||||
return false
|
||||
|
||||
var new_rotation = tetromino.rotation_quat * Quaternion.from_euler(Vector3(0, angle, 0))
|
||||
|
||||
# Validate rotation doesn't cause overlap
|
||||
if not _can_place_tetromino(tetromino, tetromino.grid_position, new_rotation):
|
||||
placement_invalid.emit("Cannot rotate: would overlap or go out of bounds")
|
||||
return false
|
||||
|
||||
# Clear old occupation
|
||||
var old_cells = tetromino.get_grid_cells(tetromino.grid_position)
|
||||
for cell_pos in old_cells:
|
||||
_occupied_cells.erase(cell_pos)
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = null
|
||||
|
||||
# Update rotation
|
||||
tetromino.rotation_quat = new_rotation
|
||||
|
||||
# Mark cells as occupied with new rotation
|
||||
var new_cells = tetromino.get_grid_cells(tetromino.grid_position)
|
||||
for cell_pos in new_cells:
|
||||
_occupied_cells[cell_pos] = tetromino
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = tetromino
|
||||
|
||||
tetromino_rotated.emit(tetromino, new_rotation)
|
||||
return true
|
||||
|
||||
|
||||
## Removes a tetromino from the board (e.g., destroyed during combat).
|
||||
func remove_tetromino(tetromino: Tetromino) -> void:
|
||||
if tetromino not in _tetrominoes:
|
||||
return
|
||||
|
||||
# Clear occupied cells
|
||||
var cells = tetromino.get_grid_cells(tetromino.grid_position)
|
||||
for cell_pos in cells:
|
||||
_occupied_cells.erase(cell_pos)
|
||||
if cell_pos in _grid:
|
||||
_grid[cell_pos].owner = null
|
||||
|
||||
_tetrominoes.erase(tetromino)
|
||||
|
||||
|
||||
## Gets all placed tetrominoes.
|
||||
func get_tetrominoes() -> Array[Tetromino]:
|
||||
return _tetrominoes.duplicate()
|
||||
|
||||
|
||||
## Gets the tetromino at a specific grid position, if any.
|
||||
func get_tetromino_at(grid_pos: Vector3i) -> Tetromino:
|
||||
return _occupied_cells.get(grid_pos)
|
||||
|
||||
|
||||
## Gets all adjacent tetrominoes within synergy radius.
|
||||
func get_adjacent_tetrominoes(tetromino: Tetromino, synergy_radius: float = 2.5) -> Array[Tetromino]:
|
||||
var adjacent: Array[Tetromino] = []
|
||||
var world_pos = tetromino.global_position
|
||||
|
||||
for other in _tetrominoes:
|
||||
if other == tetromino:
|
||||
continue
|
||||
|
||||
var distance = world_pos.distance_to(other.global_position)
|
||||
if distance <= synergy_radius:
|
||||
adjacent.append(other)
|
||||
|
||||
return adjacent
|
||||
|
||||
|
||||
## Validates whether a tetromino can be placed at the given position with rotation.
|
||||
func _can_place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rot: Quaternion) -> bool:
|
||||
# Get the cells this tetromino would occupy
|
||||
var cells = tetromino.get_grid_cells_with_rotation(grid_position, rot)
|
||||
|
||||
# Check all cells are within bounds and empty
|
||||
for cell_pos in cells:
|
||||
if not is_within_bounds(cell_pos):
|
||||
return false
|
||||
|
||||
# Check if occupied by another tetromino
|
||||
if cell_pos in _occupied_cells and _occupied_cells[cell_pos] != tetromino:
|
||||
return false
|
||||
|
||||
# Raycast check for physical obstacles
|
||||
if not _validate_placement_raycast(tetromino, grid_position):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Performs raycast validation for tetromino placement.
|
||||
## Checks for physical obstacles using raycasting.
|
||||
func _validate_placement_raycast(tetromino: Tetromino, grid_position: Vector3i) -> bool:
|
||||
if not _space_state:
|
||||
return true
|
||||
|
||||
var world_pos = grid_to_world(grid_position)
|
||||
|
||||
# Cast a ray downward from above the placement position
|
||||
var query = PhysicsRayQueryParameters3D.create(
|
||||
world_pos + Vector3.UP * 10.0,
|
||||
world_pos
|
||||
)
|
||||
query.exclude = [tetromino]
|
||||
|
||||
var result = _space_state.intersect_ray(query)
|
||||
|
||||
# If ray hits something other than the grid plane, placement is invalid
|
||||
if result and result.get("collider") and result.collider != self:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Gets a string representation of the grid for debugging.
|
||||
func get_grid_state() -> String:
|
||||
var state = "Grid State (X x Y x Z = %d x %d x %d):\n" % [grid_size.x, grid_size.y, grid_size.z]
|
||||
state += "Occupied cells: %d\n" % _occupied_cells.size()
|
||||
state += "Placed tetrominoes: %d\n" % _tetrominoes.size()
|
||||
|
||||
for tetromino in _tetrominoes:
|
||||
state += " - %s at %v\n" % [tetromino.shape_type, tetromino.grid_position]
|
||||
|
||||
return state
|
||||
|
||||
|
||||
## Clears the board (removes all tetrominoes).
|
||||
func clear_board() -> void:
|
||||
for tetromino in _tetrominoes.duplicate():
|
||||
remove_tetromino(tetromino)
|
||||
1
scripts/board_manager.gd.uid
Normal file
1
scripts/board_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvxt8mmibr1uj
|
||||
490
scripts/encounter_manager.gd
Normal file
490
scripts/encounter_manager.gd
Normal file
@@ -0,0 +1,490 @@
|
||||
## Core game state machine and encounter loop for Tetra Tactics 3D.
|
||||
##
|
||||
## Manages the encounter flow: DRAFT → PLACEMENT → TELEGRAPH → COMBAT → RESOLUTION → ESCALATION.
|
||||
## Orchestrates turn transitions, wave progression, and win/loss conditions.
|
||||
class_name EncounterManager
|
||||
extends Node
|
||||
|
||||
## Game state enumeration
|
||||
enum State {
|
||||
DRAFT, ## Draft random tetrominoes for placement
|
||||
PLACEMENT, ## Player places/rotates shapes in 3D board
|
||||
TELEGRAPH, ## Show enemy spawn points and fall paths (pre-combat warning)
|
||||
COMBAT, ## Enemies fall from above; towers track and fire projectiles
|
||||
RESOLUTION, ## Damage calculation, collect rewards, wave outcome
|
||||
ESCALATION ## Difficulty increase, preview next wave
|
||||
}
|
||||
|
||||
## Signal emitted when state changes
|
||||
signal state_changed(new_state: State, old_state: State)
|
||||
|
||||
## Signal emitted when wave starts/progresses
|
||||
signal wave_started(wave_number: int)
|
||||
signal wave_completed(wave_number: int)
|
||||
signal wave_failed
|
||||
|
||||
## Signal emitted for player feedback
|
||||
signal gold_changed(amount: int)
|
||||
signal health_changed(amount: int)
|
||||
|
||||
# Game flow configuration
|
||||
@export var waves: Array[Resource] = [] ## Array of WaveConfig resources
|
||||
@export var starting_health: int = 100
|
||||
@export var starting_gold: int = 500
|
||||
|
||||
#
|
||||
@onready var camera: Camera3D = $Camera3D
|
||||
|
||||
# State management
|
||||
var _current_state: State = State.DRAFT
|
||||
var _previous_state: State
|
||||
var _state_timer: float = 0.0
|
||||
|
||||
# Game progress
|
||||
var _current_wave: int = 0
|
||||
var _total_waves: int = 0
|
||||
var _current_health: int
|
||||
var _current_gold: int
|
||||
|
||||
# References to systems and managers
|
||||
var _event_bus: Node
|
||||
var _board_manager: Node ## BoardManager
|
||||
var _combat_system: Node ## CombatSystem
|
||||
var _synergy_system: Node ## SynergySystem
|
||||
var _enemy_spawner: Node ## EnemySpawner
|
||||
var _ui_manager: Node
|
||||
|
||||
# Draft phase data
|
||||
var _available_tetrominoes: Array[Resource] = [] ## TetrominoDefinition resources
|
||||
var _drafted_tetromino: Resource ## Currently selected tetromino
|
||||
|
||||
# Combat phase state
|
||||
var _combat_in_progress: bool = false
|
||||
var _enemies_remaining: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_total_waves = waves.size()
|
||||
_current_health = starting_health
|
||||
_current_gold = starting_gold
|
||||
|
||||
# Get references to autoload singletons and scene nodes
|
||||
_event_bus = get_tree().root.get_child(0).get_node_or_null("EventBus")
|
||||
if not _event_bus:
|
||||
_event_bus = get_node("/root/EventBus") if "/root/EventBus" in get_tree().root else null
|
||||
|
||||
# Find board manager in scene
|
||||
_board_manager = get_node_or_null("Board")
|
||||
#_combat_system = get_node_or_null("CombatSystem")
|
||||
#_synergy_system = get_node_or_null("SynergySystem")
|
||||
#_enemy_spawner = get_node_or_null("EnemySpawner")
|
||||
#_ui_manager = get_node_or_null("HUD")
|
||||
|
||||
# Connect to signal events if event bus exists
|
||||
if _event_bus:
|
||||
if _event_bus.has_signal("enemy_died"):
|
||||
_event_bus.enemy_died.connect(_on_enemy_died)
|
||||
if _event_bus.has_signal("tower_damaged"):
|
||||
_event_bus.tower_damaged.connect(_on_tower_damaged)
|
||||
|
||||
# Start first wave's draft phase
|
||||
_current_wave = 0
|
||||
_transition_to_state(State.DRAFT)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _board_manager.selected_tetromino and event is InputEventMouseMotion:
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
var ray_origin = camera.project_ray_origin(mouse_pos)
|
||||
var ray_direction = camera.project_ray_normal(mouse_pos)
|
||||
|
||||
# Cast ray to find ground position (Y = 0 plane)
|
||||
var t = -ray_origin.y / ray_direction.y if ray_direction.y != 0 else 0.0
|
||||
var world_hit = ray_origin + ray_direction * t
|
||||
|
||||
# Convert to grid position
|
||||
var new_grid_pos = _board_manager.world_to_grid(world_hit)
|
||||
_board_manager.move_tetromino(_board_manager.selected_tetromino, new_grid_pos)
|
||||
|
||||
func _on_tetromino_selected(tetromino: Tetromino):
|
||||
print("Tetromino selected")
|
||||
_board_manager.selected_tetromino = tetromino
|
||||
|
||||
func _on_tetromino_deselected(tetromino: Tetromino):
|
||||
print("Tetromino deselected")
|
||||
_board_manager.selected_tetromino = null
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_state_timer += delta
|
||||
|
||||
match _current_state:
|
||||
State.DRAFT:
|
||||
_update_draft(delta)
|
||||
State.PLACEMENT:
|
||||
_update_placement(delta)
|
||||
State.TELEGRAPH:
|
||||
_update_telegraph(delta)
|
||||
State.COMBAT:
|
||||
_update_combat(delta)
|
||||
State.RESOLUTION:
|
||||
_update_resolution(delta)
|
||||
State.ESCALATION:
|
||||
_update_escalation(delta)
|
||||
|
||||
|
||||
## Transition to a new state and emit signal
|
||||
func _transition_to_state(new_state: State) -> void:
|
||||
if new_state == _current_state:
|
||||
return
|
||||
|
||||
_previous_state = _current_state
|
||||
_current_state = new_state
|
||||
_state_timer = 0.0
|
||||
|
||||
state_changed.emit(new_state, _previous_state)
|
||||
|
||||
# Broadcast to event bus if available
|
||||
if _event_bus and _event_bus.has_signal("state_changed"):
|
||||
_event_bus.state_changed.emit(new_state, _previous_state)
|
||||
|
||||
# Execute state entry logic
|
||||
match new_state:
|
||||
State.DRAFT:
|
||||
_enter_draft()
|
||||
State.PLACEMENT:
|
||||
_enter_placement()
|
||||
State.TELEGRAPH:
|
||||
_enter_telegraph()
|
||||
State.COMBAT:
|
||||
_enter_combat()
|
||||
State.RESOLUTION:
|
||||
_enter_resolution()
|
||||
State.ESCALATION:
|
||||
_enter_escalation()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DRAFT PHASE: Present 3 random tetrominoes for player selection
|
||||
# ============================================================================
|
||||
|
||||
func _enter_draft() -> void:
|
||||
print("Entering DRAFT phase for wave %d" % (_current_wave + 1))
|
||||
|
||||
# Load tetromino definitions from resources
|
||||
_available_tetrominoes.clear()
|
||||
_load_tetromino_definitions()
|
||||
|
||||
# Shuffle and select 3 random tetrominoes for player choice
|
||||
_available_tetrominoes.shuffle()
|
||||
if _available_tetrominoes.size() > 3:
|
||||
_available_tetrominoes = _available_tetrominoes.slice(0, 3)
|
||||
|
||||
# Signal UI to show draft carousel
|
||||
if _ui_manager:
|
||||
_ui_manager.show_draft_carousel(_available_tetrominoes)
|
||||
|
||||
if _event_bus and _event_bus.has_signal("draft_started"):
|
||||
_event_bus.draft_started.emit(_available_tetrominoes)
|
||||
|
||||
|
||||
func _update_draft(delta: float) -> void:
|
||||
# Wait for player selection via input or UI callback
|
||||
# Once tetromino is selected, move to PLACEMENT phase
|
||||
pass
|
||||
|
||||
|
||||
## Called when player selects a tetromino from the draft carousel
|
||||
func select_tetromino(tetromino: Resource) -> void:
|
||||
if _current_state != State.DRAFT:
|
||||
return
|
||||
|
||||
_drafted_tetromino = tetromino
|
||||
print("Selected tetromino: %s" % tetromino.shape_type)
|
||||
|
||||
_transition_to_state(State.PLACEMENT)
|
||||
|
||||
|
||||
## Load tetromino definitions from resource files
|
||||
func _load_tetromino_definitions() -> void:
|
||||
# Load from resource directory (placeholder path)
|
||||
var tetromino_dir = "res://resources/data/tetrominoes/"
|
||||
var dir = DirAccess.open(tetromino_dir)
|
||||
|
||||
if dir:
|
||||
dir.list_dir_begin()
|
||||
var file_name = dir.get_next()
|
||||
|
||||
while file_name != "":
|
||||
if file_name.ends_with(".tres"):
|
||||
var resource_path = tetromino_dir + file_name
|
||||
var tetromino_def = load(resource_path)
|
||||
if tetromino_def is Resource:
|
||||
_available_tetrominoes.append(tetromino_def)
|
||||
file_name = dir.get_next()
|
||||
else:
|
||||
# Fallback: create sample tetrominoes if resources unavailable
|
||||
_create_sample_tetrominoes()
|
||||
|
||||
|
||||
## Create sample tetrominoes if resource files not found
|
||||
func _create_sample_tetrominoes() -> void:
|
||||
var shapes = ["I", "O", "T", "S", "Z", "L", "J"]
|
||||
for shape in shapes:
|
||||
var tetromino_def = TetrominoDefinition.new()
|
||||
tetromino_def.shape_type = StringName(shape)
|
||||
tetromino_def.base_damage = 10 + randi() % 10
|
||||
tetromino_def.fire_rate = 1.0 + randf_range(0.0, 0.5)
|
||||
tetromino_def.synergy_tags = ["fire", "ice", "light"].slice(0, randi() % 2 + 1)
|
||||
_available_tetrominoes.append(tetromino_def)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PLACEMENT PHASE: Player places/rotates tetrominoes in 3D board
|
||||
# ============================================================================
|
||||
|
||||
func _enter_placement() -> void:
|
||||
print("Entering PLACEMENT phase")
|
||||
|
||||
if _board_manager:
|
||||
_board_manager.clear_preview()
|
||||
_board_manager.set_placement_mode(true, _drafted_tetromino)
|
||||
|
||||
if _ui_manager:
|
||||
_ui_manager.show_placement_instructions()
|
||||
|
||||
if _event_bus and _event_bus.has_signal("placement_started"):
|
||||
_event_bus.placement_started.emit(_drafted_tetromino)
|
||||
|
||||
|
||||
func _update_placement(delta: float) -> void:
|
||||
# Wait for player confirmation (Space key)
|
||||
if Input.is_action_just_pressed("ui_select"):
|
||||
_on_placement_confirmed()
|
||||
|
||||
|
||||
## Called when player confirms tetromino placement
|
||||
func _on_placement_confirmed() -> void:
|
||||
if _current_state != State.PLACEMENT:
|
||||
return
|
||||
|
||||
if _board_manager and _board_manager.place_tetromino(_drafted_tetromino):
|
||||
print("Tetromino placed successfully")
|
||||
_transition_to_state(State.TELEGRAPH)
|
||||
else:
|
||||
print("Invalid placement - cannot place tetromino")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TELEGRAPH PHASE: Show enemy spawn points and fall paths
|
||||
# ============================================================================
|
||||
|
||||
func _enter_telegraph() -> void:
|
||||
print("Entering TELEGRAPH phase")
|
||||
|
||||
# Show visual indicators of where enemies will spawn and fall
|
||||
if _enemy_spawner:
|
||||
_enemy_spawner.telegraph_wave(waves[_current_wave])
|
||||
|
||||
if _ui_manager:
|
||||
_ui_manager.show_telegraph_warning()
|
||||
|
||||
if _event_bus and _event_bus.has_signal("telegraph_started"):
|
||||
_event_bus.telegraph_started.emit(waves[_current_wave])
|
||||
|
||||
|
||||
func _update_telegraph(delta: float) -> void:
|
||||
# Telegraph phase lasts 2-3 seconds, then transitions to combat
|
||||
if _state_timer >= 2.5:
|
||||
_transition_to_state(State.COMBAT)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# COMBAT PHASE: Enemies fall; towers track and fire
|
||||
# ============================================================================
|
||||
|
||||
func _enter_combat() -> void:
|
||||
print("Entering COMBAT phase for wave %d" % (_current_wave + 1))
|
||||
|
||||
_combat_in_progress = true
|
||||
|
||||
# Spawn enemies for current wave
|
||||
var wave_config: Resource = waves[_current_wave]
|
||||
if _enemy_spawner:
|
||||
_enemies_remaining = _enemy_spawner.spawn_wave(wave_config)
|
||||
|
||||
# Enable tower combat systems
|
||||
if _combat_system:
|
||||
_combat_system.enable_combat(true)
|
||||
if _synergy_system:
|
||||
_synergy_system.recalculate_synergies()
|
||||
if _board_manager:
|
||||
_board_manager.set_placement_mode(false) # Disable placement, allow repositioning
|
||||
|
||||
wave_started.emit(_current_wave + 1)
|
||||
|
||||
if _event_bus and _event_bus.has_signal("combat_started"):
|
||||
_event_bus.combat_started.emit(_current_wave + 1)
|
||||
|
||||
|
||||
func _update_combat(delta: float) -> void:
|
||||
# Combat continues until all enemies are defeated or health reaches 0
|
||||
if _enemies_remaining <= 0:
|
||||
_transition_to_state(State.RESOLUTION)
|
||||
elif _current_health <= 0:
|
||||
wave_failed.emit()
|
||||
_transition_to_state(State.RESOLUTION)
|
||||
|
||||
|
||||
## Called when an enemy is defeated
|
||||
func _on_enemy_died() -> void:
|
||||
_enemies_remaining = max(0, _enemies_remaining - 1)
|
||||
|
||||
if _enemies_remaining == 0 and _current_state == State.COMBAT:
|
||||
_transition_to_state(State.RESOLUTION)
|
||||
|
||||
|
||||
## Called when a tower takes damage from enemy impact
|
||||
func _on_tower_damaged(amount: int) -> void:
|
||||
modify_health(-amount)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# RESOLUTION PHASE: Damage calculation, collect rewards
|
||||
# ============================================================================
|
||||
|
||||
func _enter_resolution() -> void:
|
||||
print("Entering RESOLUTION phase")
|
||||
|
||||
_combat_in_progress = false
|
||||
|
||||
# Disable combat systems
|
||||
if _combat_system:
|
||||
_combat_system.enable_combat(false)
|
||||
if _board_manager:
|
||||
_board_manager.set_placement_mode(false)
|
||||
|
||||
# Calculate and award gold for wave completion
|
||||
if _enemies_remaining == 0:
|
||||
var gold_reward = 100 + (_current_wave * 50)
|
||||
modify_gold(gold_reward)
|
||||
wave_completed.emit(_current_wave + 1)
|
||||
#print("Wave %d completed! Earned %d gold" % (_current_wave + 1, gold_reward))
|
||||
else:
|
||||
print("Wave %d failed - enemies escaped" % (_current_wave + 1))
|
||||
|
||||
if _ui_manager:
|
||||
_ui_manager.show_resolution_summary(_enemies_remaining == 0)
|
||||
|
||||
|
||||
func _update_resolution(delta: float) -> void:
|
||||
# Resolution phase lasts 2 seconds, then transitions to escalation or game over
|
||||
if _state_timer >= 2.0:
|
||||
if _current_health <= 0:
|
||||
print("Game Over - Health depleted")
|
||||
_end_game(false)
|
||||
else:
|
||||
_transition_to_state(State.ESCALATION)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ESCALATION PHASE: Difficulty increase, preview next wave
|
||||
# ============================================================================
|
||||
|
||||
func _enter_escalation() -> void:
|
||||
print("Entering ESCALATION phase")
|
||||
|
||||
_current_wave += 1
|
||||
|
||||
if _current_wave >= _total_waves:
|
||||
print("All waves completed - Victory!")
|
||||
_end_game(true)
|
||||
return
|
||||
|
||||
# Prepare next wave
|
||||
if _ui_manager:
|
||||
_ui_manager.show_escalation_preview(_current_wave + 1, waves[_current_wave])
|
||||
|
||||
if _event_bus and _event_bus.has_signal("escalation_started"):
|
||||
_event_bus.escalation_started.emit(_current_wave + 1)
|
||||
|
||||
|
||||
func _update_escalation(delta: float) -> void:
|
||||
# Escalation phase lasts 2-3 seconds, then returns to draft
|
||||
if _state_timer >= 2.5:
|
||||
# Clear previous tetrominoes from board (optional: allow stacking)
|
||||
# For now, board persists across waves
|
||||
_transition_to_state(State.DRAFT)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# GAME END & UTILITIES
|
||||
# ============================================================================
|
||||
|
||||
## Called when game ends (victory or defeat)
|
||||
func _end_game(victory: bool) -> void:
|
||||
if victory:
|
||||
print("VICTORY - Campaign completed!")
|
||||
if _event_bus and _event_bus.has_signal("game_won"):
|
||||
_event_bus.game_won.emit()
|
||||
else:
|
||||
print("DEFEAT - Health depleted")
|
||||
if _event_bus and _event_bus.has_signal("game_lost"):
|
||||
_event_bus.game_lost.emit()
|
||||
|
||||
if _ui_manager:
|
||||
_ui_manager.show_game_over(victory)
|
||||
|
||||
# Pause game or transition to menu
|
||||
get_tree().paused = true
|
||||
|
||||
|
||||
## Modify player health (positive to heal, negative to damage)
|
||||
func modify_health(amount: int) -> void:
|
||||
var old_health = _current_health
|
||||
_current_health = clampi(_current_health + amount, 0, starting_health)
|
||||
|
||||
if _current_health != old_health:
|
||||
health_changed.emit(_current_health)
|
||||
if _event_bus and _event_bus.has_signal("health_changed"):
|
||||
_event_bus.health_changed.emit(_current_health)
|
||||
|
||||
|
||||
## Modify player gold (positive to gain, negative to spend)
|
||||
func modify_gold(amount: int) -> void:
|
||||
var old_gold = _current_gold
|
||||
_current_gold = maxi(_current_gold + amount, 0)
|
||||
|
||||
if _current_gold != old_gold:
|
||||
gold_changed.emit(_current_gold)
|
||||
if _event_bus and _event_bus.has_signal("gold_changed"):
|
||||
_event_bus.gold_changed.emit(_current_gold)
|
||||
|
||||
|
||||
## Get current game state
|
||||
func get_current_state() -> State:
|
||||
return _current_state
|
||||
|
||||
|
||||
## Get current wave number (1-indexed)
|
||||
func get_current_wave() -> int:
|
||||
return _current_wave + 1
|
||||
|
||||
|
||||
## Get total wave count
|
||||
func get_total_waves() -> int:
|
||||
return _total_waves
|
||||
|
||||
|
||||
## Get current health
|
||||
func get_health() -> int:
|
||||
return _current_health
|
||||
|
||||
|
||||
## Get current gold
|
||||
func get_gold() -> int:
|
||||
return _current_gold
|
||||
|
||||
|
||||
## Check if combat is active
|
||||
func is_combat_active() -> bool:
|
||||
return _combat_in_progress
|
||||
1
scripts/encounter_manager.gd.uid
Normal file
1
scripts/encounter_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dv2k6b3b3tfa6
|
||||
95
scripts/event_bus.gd
Normal file
95
scripts/event_bus.gd
Normal file
@@ -0,0 +1,95 @@
|
||||
## Global event bus for signal-based communication across systems.
|
||||
##
|
||||
## Centralizes game events to decouple systems and simplify event handling.
|
||||
## Register as autoload singleton: Project Settings → Autoload → EventBus
|
||||
extends Node
|
||||
|
||||
# ============================================================================
|
||||
# Game State Events
|
||||
# ============================================================================
|
||||
|
||||
## Emitted when game state changes
|
||||
signal state_changed(new_state: int, old_state: int)
|
||||
|
||||
## Emitted when draft phase begins
|
||||
signal draft_started(available_tetrominoes: Array[Resource])
|
||||
|
||||
## Emitted when placement phase begins
|
||||
signal placement_started(tetromino: Resource)
|
||||
|
||||
## Emitted when telegraph phase begins
|
||||
signal telegraph_started(wave_config: Resource)
|
||||
|
||||
## Emitted when combat phase begins
|
||||
signal combat_started(wave_number: int)
|
||||
|
||||
## Emitted when escalation phase begins
|
||||
signal escalation_started(wave_number: int)
|
||||
|
||||
# ============================================================================
|
||||
# Wave & Enemy Events
|
||||
# ============================================================================
|
||||
|
||||
## Emitted when enemy spawns
|
||||
signal enemy_spawned(enemy: Node3D)
|
||||
|
||||
## Emitted when enemy is defeated
|
||||
signal enemy_died(enemy: Node3D)
|
||||
|
||||
## Emitted when all enemies in wave are defeated
|
||||
signal wave_completed(wave_number: int)
|
||||
|
||||
## Emitted when wave fails (enemies reach board/health depleted)
|
||||
signal wave_failed(wave_number: int)
|
||||
|
||||
# ============================================================================
|
||||
# Combat Events
|
||||
# ============================================================================
|
||||
|
||||
## Emitted when tower fires projectile
|
||||
signal tower_fired(tower: Node3D, target: Node3D)
|
||||
|
||||
## Emitted when projectile hits enemy
|
||||
signal projectile_hit(projectile: Node3D, enemy: Node3D, damage: int)
|
||||
|
||||
## Emitted when tower takes damage from enemy
|
||||
signal tower_damaged(tower: Node3D, damage: int)
|
||||
|
||||
## Emitted when synergy is activated
|
||||
signal synergy_activated(towers: Array[Node3D], bonus_type: String, bonus_value: float)
|
||||
|
||||
# ============================================================================
|
||||
# Player Resource Events
|
||||
# ============================================================================
|
||||
|
||||
## Emitted when player gold changes
|
||||
signal gold_changed(amount: int)
|
||||
|
||||
## Emitted when player health changes
|
||||
signal health_changed(amount: int)
|
||||
|
||||
## Emitted when tower is placed
|
||||
signal tower_placed(tower: Node3D, position: Vector3)
|
||||
|
||||
## Emitted when tower is moved/repositioned
|
||||
signal tower_moved(tower: Node3D, new_position: Vector3, old_position: Vector3)
|
||||
|
||||
# ============================================================================
|
||||
# Game End Events
|
||||
# ============================================================================
|
||||
|
||||
## Emitted when player wins the encounter
|
||||
signal game_won
|
||||
|
||||
## Emitted when player loses the encounter
|
||||
signal game_lost
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Ensure this is a singleton
|
||||
if get_tree().root.get_child_count() > 1:
|
||||
for i in range(get_tree().root.get_child_count()):
|
||||
var child = get_tree().root.get_child(i)
|
||||
if child is EventBus and child != self:
|
||||
queue_free()
|
||||
return
|
||||
1
scripts/event_bus.gd.uid
Normal file
1
scripts/event_bus.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cbus31k0tae6i
|
||||
39
scripts/grid_cell.gd
Normal file
39
scripts/grid_cell.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
## Represents a single 3D grid cell on the game board.
|
||||
##
|
||||
## It's a data structure for occupancy state, owner tetromino, bounds, and
|
||||
## visual feedback.
|
||||
class_name GridCell
|
||||
extends Object
|
||||
|
||||
# Properties
|
||||
var position: Vector3i # Grid coordinates
|
||||
var owner: Tetromino = null # Tetromino occupying this cell
|
||||
var bounds: AABB = AABB() # 3D axis-aligned bounding box
|
||||
|
||||
# Visual state
|
||||
var is_highlighted: bool = false
|
||||
var is_valid_placement: bool = true
|
||||
|
||||
|
||||
## Checks if the cell is currently empty.
|
||||
func is_empty() -> bool:
|
||||
return owner == null
|
||||
|
||||
|
||||
## Marks the cell as occupied by a tetromino.
|
||||
func occupy(tetromino: Tetromino) -> void:
|
||||
owner = tetromino
|
||||
|
||||
|
||||
## Marks the cell as empty.
|
||||
func clear() -> void:
|
||||
owner = null
|
||||
|
||||
|
||||
## Returns a string representation of the cell state.
|
||||
func _to_string() -> String:
|
||||
return "GridCell3D[pos=%v, occupied=%s, valid=%s]" % [
|
||||
position,
|
||||
"yes" if owner else "no",
|
||||
"yes" if is_valid_placement else "no"
|
||||
]
|
||||
1
scripts/grid_cell.gd.uid
Normal file
1
scripts/grid_cell.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dygdtu1vjr1rt
|
||||
162
scripts/tetromino.gd
Normal file
162
scripts/tetromino.gd
Normal file
@@ -0,0 +1,162 @@
|
||||
## Represents a 3D tetromino (tower block) with shape, position, and rotation.
|
||||
##
|
||||
## Manages 3D grid cell data, mesh instances, position/rotation state,
|
||||
## synergy detection, and material effects.
|
||||
class_name Tetromino
|
||||
extends Node3D
|
||||
|
||||
signal hovered(tetromino: Tetromino)
|
||||
signal unhovered(tetromino: Tetromino)
|
||||
signal selected(tetromino: Tetromino)
|
||||
signal deselected(tetromino: Tetromino)
|
||||
|
||||
|
||||
# Data
|
||||
@export var resource: TetrominoDefinition
|
||||
|
||||
# Visual
|
||||
var _mesh_color: Color = Color.WHITE
|
||||
var _material: StandardMaterial3D
|
||||
var _is_ghost: bool = false
|
||||
var _original_color: Color = Color.WHITE
|
||||
|
||||
# Runtime
|
||||
@onready var base_damage: int = resource.base_damage
|
||||
@onready var fire_rate: float = resource.fire_rate
|
||||
@onready var synergy_radius: float = resource.synergy_radius
|
||||
@onready var synergy_tags: PackedStringArray = resource.synergy_tags
|
||||
@onready var _mesh_instance: MeshInstance3D = get_node("Visuals/MeshInstance3D")
|
||||
|
||||
# Grid state
|
||||
var grid_position: Vector3i = Vector3i.ZERO
|
||||
var rotation_quat: Quaternion = Quaternion.IDENTITY
|
||||
var _grid_cells: PackedVector3Array = [] # Relative cell coordinates
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_initialize_material()
|
||||
_set_mesh_representation()
|
||||
pass
|
||||
|
||||
|
||||
## Initializes the material for visual representation.
|
||||
func _initialize_material() -> void:
|
||||
_material = StandardMaterial3D.new()
|
||||
_material.albedo_color = Color.RED
|
||||
_material.metallic = 0.3
|
||||
_material.roughness = 0.7
|
||||
|
||||
|
||||
## Creates a simple mesh representation based on shape type.
|
||||
func _set_mesh_representation() -> void:
|
||||
_mesh_instance.mesh = resource.mesh
|
||||
_mesh_instance.set_surface_override_material(0, _material)
|
||||
$SelectionArea/CollisionShape3D.shape = _mesh_instance.mesh.create_trimesh_shape()
|
||||
_grid_cells = resource.grid_cells
|
||||
|
||||
|
||||
## Gets the grid cells occupied by this tetromino at its current position.
|
||||
func get_grid_cells(at_position: Vector3i = grid_position) -> PackedVector3Array:
|
||||
var result = PackedVector3Array()
|
||||
for cell in _grid_cells:
|
||||
result.append(at_position + Vector3i(cell.x, cell.y, cell.z))
|
||||
return result
|
||||
|
||||
|
||||
## Gets grid cells with a specific rotation applied.
|
||||
func get_grid_cells_with_rotation(at_position: Vector3i, rot: Quaternion) -> PackedVector3Array:
|
||||
var result = PackedVector3Array()
|
||||
|
||||
# Apply rotation to relative grid cells
|
||||
for cell in _grid_cells:
|
||||
var rotated = rot * Vector3(cell)
|
||||
var rotated_int = Vector3i(rotated.round())
|
||||
result.append(at_position + rotated_int)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Gets the bounds (AABB) of this tetromino.
|
||||
func get_bounds() -> AABB:
|
||||
var min_cell = Vector3i.ZERO
|
||||
var max_cell = Vector3i.ZERO
|
||||
|
||||
for cell in _grid_cells:
|
||||
min_cell = min_cell.min(cell)
|
||||
max_cell = max_cell.max(cell)
|
||||
|
||||
var size = Vector3(max_cell - min_cell) + Vector3.ONE
|
||||
var pos = grid_position + min_cell
|
||||
|
||||
return AABB(Vector3(pos), size)
|
||||
|
||||
|
||||
## Sets the color/material of the tetromino.
|
||||
func set_color(color: Color) -> void:
|
||||
_mesh_color = color
|
||||
_original_color = color
|
||||
if _material:
|
||||
_material.albedo_color = color
|
||||
|
||||
|
||||
## Enables/disables ghost mode (semi-transparent, movable state).
|
||||
func set_ghost_mode(enabled: bool) -> void:
|
||||
_is_ghost = enabled
|
||||
|
||||
if enabled:
|
||||
# Create material if needed
|
||||
if not _material:
|
||||
_initialize_material()
|
||||
|
||||
# Make semi-transparent
|
||||
var ghost_color = _original_color
|
||||
ghost_color.a = 0.5
|
||||
_material.albedo_color = ghost_color
|
||||
_mesh_instance.set_surface_override_material(0, _material)
|
||||
else:
|
||||
# Restore original appearance
|
||||
if _material:
|
||||
_material.albedo_color = _original_color
|
||||
_material.albedo_color.a = 1.0
|
||||
_mesh_instance.set_surface_override_material(0, _material)
|
||||
|
||||
|
||||
## Enables/disables highlight for visual feedback.
|
||||
func set_highlighted(enabled: bool) -> void:
|
||||
if _material:
|
||||
_material.emission_enabled = enabled
|
||||
if enabled:
|
||||
_material.emission = _mesh_color
|
||||
_material.emission_energy_multiplier = 2.0
|
||||
else:
|
||||
_material.emission_energy_multiplier = 0.0
|
||||
|
||||
|
||||
## Returns string representation for debugging.
|
||||
func _to_string() -> String:
|
||||
return "Tetromino3D[type=%s, pos=%v, cells=%d]" % [
|
||||
resource.shape_type,
|
||||
grid_position,
|
||||
_grid_cells.size()
|
||||
]
|
||||
|
||||
## Callback used to handle tetromino selection and deselection
|
||||
func _on_selection_area_input_event(camera: Node, event: InputEvent, event_position: Vector3, normal: Vector3, shape_idx: int) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
selected.emit(self)
|
||||
elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
deselected.emit(self)
|
||||
|
||||
|
||||
## Callback used to handle tetromino hovering
|
||||
func _on_selection_area_mouse_entered() -> void:
|
||||
print("Tetromino hovered")
|
||||
set_highlighted(true)
|
||||
hovered.emit(self)
|
||||
|
||||
## Callback used to handle tetromino unhovering
|
||||
func _on_selection_area_mouse_exited() -> void:
|
||||
print("Tetromino unhovered")
|
||||
set_highlighted(false)
|
||||
unhovered.emit(self)
|
||||
1
scripts/tetromino.gd.uid
Normal file
1
scripts/tetromino.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c8041v2usigk4
|
||||
123
scripts/tetromino_definition.gd
Normal file
123
scripts/tetromino_definition.gd
Normal file
@@ -0,0 +1,123 @@
|
||||
## Data class for tetromino shape definitions in 3D space.
|
||||
##
|
||||
## Stores shape geometry, combat stats, synergy tags, and cost information.
|
||||
## Designed to be saved as a Resource (.tres file) for easy editor configuration.
|
||||
class_name TetrominoDefinition
|
||||
extends Resource
|
||||
|
||||
enum ShapeType { I, O, T, S, Z, L, J }
|
||||
|
||||
## Shape type identifier
|
||||
@export var shape_type: ShapeType = ShapeType.I
|
||||
|
||||
## Base damage output per shot
|
||||
@export var base_damage: int = 10
|
||||
|
||||
## Fire rate in shots per second
|
||||
@export var fire_rate: float = 1.0
|
||||
|
||||
## Gold cost to place this tetromino
|
||||
@export var base_cost: int = 50
|
||||
|
||||
## Synergy tags for combination effects (e.g., ["fire", "ice"])
|
||||
@export var synergy_tags: PackedStringArray = []
|
||||
|
||||
## 3D mesh color/tint for visual distinction
|
||||
@export var mesh_color: Color = Color.WHITE
|
||||
|
||||
## Pre-built 3D mesh for this tetromino
|
||||
@export var mesh: Mesh
|
||||
|
||||
## Detection radius for 3D adjacency synergies
|
||||
@export var synergy_radius: float = 2.5
|
||||
|
||||
## 3D grid cells relative to anchor point
|
||||
var grid_cells: PackedVector3Array = []
|
||||
|
||||
func _init(p_shape_type: ShapeType = ShapeType.I) -> void:
|
||||
shape_type = p_shape_type
|
||||
_initialize_default_shape()
|
||||
|
||||
|
||||
## Initialize default grid cells based on shape type (Tetris standard)
|
||||
func _initialize_default_shape() -> void:
|
||||
match shape_type:
|
||||
ShapeType.I:
|
||||
# I-piece: 4 in a row
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.RIGHT, Vector3i.RIGHT * 2, Vector3i.RIGHT * 3]
|
||||
mesh_color = Color.CYAN
|
||||
base_damage = 8
|
||||
fire_rate = 0.8
|
||||
|
||||
ShapeType.O:
|
||||
# O-piece: 2x2 square
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.RIGHT, Vector3i.FORWARD, Vector3i.RIGHT + Vector3i.FORWARD]
|
||||
mesh_color = Color.YELLOW
|
||||
base_damage = 12
|
||||
fire_rate = 1.2
|
||||
|
||||
ShapeType.T:
|
||||
# T-piece: T-shape
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.FORWARD]
|
||||
mesh_color = Color.MAGENTA
|
||||
base_damage = 10
|
||||
fire_rate = 1.0
|
||||
|
||||
ShapeType.S:
|
||||
# S-piece: Zigzag
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.RIGHT, Vector3i.FORWARD, Vector3i.LEFT + Vector3i.FORWARD]
|
||||
mesh_color = Color.GREEN
|
||||
base_damage = 9
|
||||
fire_rate = 0.9
|
||||
|
||||
ShapeType.Z:
|
||||
# Z-piece: Reverse zigzag
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.LEFT, Vector3i.FORWARD, Vector3i.RIGHT + Vector3i.FORWARD]
|
||||
mesh_color = Color.RED
|
||||
base_damage = 9
|
||||
fire_rate = 0.9
|
||||
|
||||
ShapeType.L:
|
||||
# L-piece: L-shape
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.LEFT, Vector3i.FORWARD, Vector3i.LEFT + Vector3i.FORWARD]
|
||||
mesh_color = Color.ORANGE
|
||||
base_damage = 11
|
||||
fire_rate = 1.1
|
||||
|
||||
ShapeType.J:
|
||||
# J-piece: Reverse L-shape
|
||||
grid_cells = [Vector3i.ZERO, Vector3i.RIGHT, Vector3i.FORWARD, Vector3i.RIGHT + Vector3i.FORWARD]
|
||||
mesh_color = Color.BLUE
|
||||
base_damage = 11
|
||||
fire_rate = 1.1
|
||||
|
||||
|
||||
## Get the display name of this tetromino
|
||||
func get_display_name() -> String:
|
||||
match shape_type:
|
||||
ShapeType.I:
|
||||
return "I"
|
||||
ShapeType.O:
|
||||
return "O"
|
||||
ShapeType.T:
|
||||
return "T"
|
||||
ShapeType.S:
|
||||
return "S"
|
||||
ShapeType.Z:
|
||||
return "Z"
|
||||
ShapeType.L:
|
||||
return "L"
|
||||
ShapeType.J:
|
||||
return "J"
|
||||
|
||||
return "Invalid"
|
||||
|
||||
|
||||
## Get all synergy tags as a formatted string
|
||||
func get_synergy_string() -> String:
|
||||
return ", ".join(synergy_tags)
|
||||
|
||||
|
||||
## Check if this tetromino has a specific synergy tag
|
||||
func has_synergy_tag(tag: StringName) -> bool:
|
||||
return tag in synergy_tags
|
||||
1
scripts/tetromino_definition.gd.uid
Normal file
1
scripts/tetromino_definition.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://p168a1urs4im
|
||||
219
scripts/tetromino_selector.gd
Normal file
219
scripts/tetromino_selector.gd
Normal file
@@ -0,0 +1,219 @@
|
||||
## Manages tetromino selection, ghosting, and repositioning.
|
||||
##
|
||||
## Allows players to:
|
||||
## 1. Click an already-placed tetromino to select it
|
||||
## 2. Tetromino becomes "ghost" (semi-transparent, movable)
|
||||
## 3. Move and rotate the ghost tetromino
|
||||
## 4. Click to place it in the new position
|
||||
class_name TetrominoSelector
|
||||
extends Node3D
|
||||
|
||||
# Signals
|
||||
signal tetromino_selected(tetromino: Tetromino)
|
||||
signal tetromino_deselected(tetromino: Tetromino)
|
||||
signal selection_placed(tetromino: Tetromino)
|
||||
|
||||
# References
|
||||
@onready var board_manager: BoardManager3D = get_parent()
|
||||
@onready var camera: Camera3D = get_viewport().get_camera_3d()
|
||||
|
||||
# Selection state
|
||||
var _selected_tetromino: Tetromino = null
|
||||
var _ghost_position: Vector3i = Vector3i.ZERO
|
||||
var _ghost_rotation: Quaternion = Quaternion.IDENTITY
|
||||
var _original_position: Vector3i = Vector3i.ZERO
|
||||
var _original_rotation: Quaternion = Quaternion.IDENTITY
|
||||
|
||||
# Rotation speed (radians per frame)
|
||||
var rotation_speed: float = PI / 4.0 # 45 degrees
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Input is handled in _input()
|
||||
pass
|
||||
|
||||
|
||||
#func _input(event: InputEvent) -> void:
|
||||
#if event is InputEventMouseButton:
|
||||
#if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
#_handle_left_click()
|
||||
#elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
#_handle_right_click()
|
||||
#
|
||||
#if _selected_tetromino and event is InputEventMouseMotion:
|
||||
#_update_ghost_position()
|
||||
#
|
||||
#if _selected_tetromino:
|
||||
#if event.is_action_pressed("ui_select"): # Spacebar
|
||||
#_rotate_ghost()
|
||||
#elif event.is_action_pressed("ui_cancel"): # ESC
|
||||
#_cancel_selection()
|
||||
|
||||
|
||||
### Handles left mouse click for selection or placement.
|
||||
#func _handle_left_click() -> void:
|
||||
#if _selected_tetromino:
|
||||
## Try to place the ghost tetromino
|
||||
#_place_selection()
|
||||
#return
|
||||
#
|
||||
## Try to select a tetromino
|
||||
#var tetromino = _raycast_tetromino()
|
||||
#if tetromino:
|
||||
#_select_tetromino(tetromino)
|
||||
|
||||
|
||||
## Handles right mouse click to deselect.
|
||||
func _handle_right_click() -> void:
|
||||
if _selected_tetromino:
|
||||
_cancel_selection()
|
||||
|
||||
|
||||
### Raycasts from mouse position to find a tetromino.
|
||||
#func _raycast_tetromino() -> Tetromino:
|
||||
#if not camera:
|
||||
#return null
|
||||
#
|
||||
#var mouse_pos = get_viewport().get_mouse_position()
|
||||
#var from = camera.project_ray_origin(mouse_pos)
|
||||
#var to = from + camera.project_ray_normal(mouse_pos) * 10000.0
|
||||
#
|
||||
#var space_state = get_world_3d().direct_space_state
|
||||
#var collision_mask = 0xFFFFFFFF # Hit all layers
|
||||
#var query = PhysicsRayQueryParameters3D.create(from, to, collision_mask)
|
||||
#query.collide_with_areas = true;
|
||||
#
|
||||
#var result = space_state.intersect_ray(query)
|
||||
#
|
||||
#if result and result.get("collider"):
|
||||
#var collider = result["collider"]
|
||||
## Walk up the node tree to find a Tetromino
|
||||
#var node = collider
|
||||
#while node:
|
||||
#if node is Tetromino:
|
||||
#return node
|
||||
#node = node.get_parent()
|
||||
#else:
|
||||
#print_debug("Raycast hit nothing")
|
||||
#
|
||||
#return null
|
||||
|
||||
|
||||
## Selects a tetromino and converts it to ghost mode.
|
||||
func _select_tetromino(tetromino: Tetromino) -> void:
|
||||
if _selected_tetromino:
|
||||
_cancel_selection()
|
||||
|
||||
_selected_tetromino = tetromino
|
||||
_original_position = tetromino.grid_position
|
||||
_original_rotation = tetromino.rotation_quat
|
||||
_ghost_position = _original_position
|
||||
_ghost_rotation = _original_rotation
|
||||
|
||||
# Enable ghost mode
|
||||
tetromino.set_ghost_mode(true)
|
||||
|
||||
tetromino_selected.emit(tetromino)
|
||||
get_tree().root.set_input_as_handled()
|
||||
|
||||
|
||||
## Updates ghost position based on mouse position.
|
||||
func _update_ghost_position() -> void:
|
||||
if not _selected_tetromino or not camera:
|
||||
return
|
||||
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
var ray_origin = camera.project_ray_origin(mouse_pos)
|
||||
var ray_direction = camera.project_ray_normal(mouse_pos)
|
||||
|
||||
# Cast ray to find ground position (Y = 0 plane)
|
||||
var t = -ray_origin.y / ray_direction.y if ray_direction.y != 0 else 0.0
|
||||
var world_hit = ray_origin + ray_direction * t
|
||||
|
||||
# Convert to grid position
|
||||
_ghost_position = board_manager.world_to_grid(world_hit)
|
||||
|
||||
# Clamp to valid grid bounds
|
||||
_ghost_position = Vector3i(
|
||||
clampi(_ghost_position.x, 0, board_manager.grid_size.x - 1),
|
||||
clampi(_ghost_position.y, 0, board_manager.grid_size.y - 1),
|
||||
clampi(_ghost_position.z, 0, board_manager.grid_size.z - 1)
|
||||
)
|
||||
|
||||
# Update visual position of ghost
|
||||
_selected_tetromino.grid_position = _ghost_position
|
||||
_selected_tetromino.global_position = board_manager.grid_to_world(_ghost_position)
|
||||
|
||||
|
||||
## Rotates the ghost tetromino.
|
||||
func _rotate_ghost() -> void:
|
||||
if not _selected_tetromino:
|
||||
return
|
||||
|
||||
_ghost_rotation = _ghost_rotation * Quaternion.from_euler(Vector3(0, rotation_speed, 0))
|
||||
_selected_tetromino.rotation_quat = _ghost_rotation
|
||||
|
||||
get_tree().root.set_input_as_handled()
|
||||
|
||||
|
||||
## Places the ghost tetromino at its current position.
|
||||
func _place_selection() -> void:
|
||||
if not _selected_tetromino:
|
||||
return
|
||||
|
||||
var tetromino = _selected_tetromino
|
||||
|
||||
# Validate placement
|
||||
if not board_manager._can_place_tetromino(tetromino, _ghost_position, _ghost_rotation):
|
||||
# Placement invalid, snap back to original position
|
||||
tetromino.grid_position = _original_position
|
||||
tetromino.rotation_quat = _original_rotation
|
||||
tetromino.world_position = board_manager.grid_to_world(_original_position)
|
||||
return
|
||||
|
||||
# Update board manager state
|
||||
# Remove from old position
|
||||
var old_cells = tetromino.get_grid_cells(_original_position)
|
||||
for cell_pos in old_cells:
|
||||
board_manager._occupied_cells.erase(cell_pos)
|
||||
|
||||
# Place at new position
|
||||
tetromino.grid_position = _ghost_position
|
||||
tetromino.rotation_quat = _ghost_rotation
|
||||
tetromino.global_position = board_manager.grid_to_world(_ghost_position)
|
||||
|
||||
var new_cells = tetromino.get_grid_cells(_ghost_position)
|
||||
for cell_pos in new_cells:
|
||||
board_manager._occupied_cells[cell_pos] = tetromino
|
||||
|
||||
# Disable ghost mode
|
||||
tetromino.set_ghost_mode(false)
|
||||
|
||||
_selected_tetromino = null
|
||||
selection_placed.emit(tetromino)
|
||||
get_tree().root.set_input_as_handled()
|
||||
|
||||
|
||||
## Cancels current selection and reverts tetromino to original state.
|
||||
func _cancel_selection() -> void:
|
||||
if not _selected_tetromino:
|
||||
return
|
||||
|
||||
var tetromino = _selected_tetromino
|
||||
|
||||
# Restore original position and rotation
|
||||
tetromino.grid_position = _original_position
|
||||
tetromino.rotation_quat = _original_rotation
|
||||
tetromino.world_position = board_manager.grid_to_world(_original_position)
|
||||
|
||||
# Disable ghost mode
|
||||
tetromino.set_ghost_mode(false)
|
||||
|
||||
tetromino_deselected.emit(tetromino)
|
||||
_selected_tetromino = null
|
||||
get_tree().root.set_input_as_handled()
|
||||
|
||||
|
||||
## Returns the currently selected tetromino, if any.
|
||||
func get_selected_tetromino() -> Tetromino:
|
||||
return _selected_tetromino
|
||||
1
scripts/tetromino_selector.gd.uid
Normal file
1
scripts/tetromino_selector.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bv7xi75mklk7d
|
||||
68
scripts/wave_config.gd
Normal file
68
scripts/wave_config.gd
Normal file
@@ -0,0 +1,68 @@
|
||||
## Configuration data for enemy waves.
|
||||
##
|
||||
## Defines spawn count, timing, enemy stats, and difficulty modifiers for each wave.
|
||||
## Designed to be saved as a Resource (.tres file) for easy encounter design.
|
||||
class_name WaveConfig
|
||||
extends Resource
|
||||
|
||||
## Wave number (1-indexed)
|
||||
@export var wave_number: int = 1
|
||||
|
||||
## Number of enemies to spawn in this wave
|
||||
@export var enemy_count: int = 3
|
||||
|
||||
## Interval between enemy spawns in seconds
|
||||
@export var spawn_interval: float = 1.0
|
||||
|
||||
## Enemy falling speed (units per second on Y-axis)
|
||||
@export var enemy_speed: float = 5.0
|
||||
|
||||
## Base health per enemy
|
||||
@export var enemy_health: int = 10
|
||||
|
||||
## Global difficulty multiplier for this wave (scales damage, health, count)
|
||||
@export var difficulty_multiplier: float = 1.0
|
||||
|
||||
## Optional: Enemy type/archetype for variety (placeholder)
|
||||
@export var enemy_type: StringName = &"basic"
|
||||
|
||||
## Optional: Special modifiers for the wave
|
||||
@export var wave_modifiers: PackedStringArray = []
|
||||
|
||||
|
||||
func _init(p_wave_number: int = 1) -> void:
|
||||
wave_number = p_wave_number
|
||||
_scale_difficulty()
|
||||
|
||||
|
||||
## Auto-scale difficulty based on wave number
|
||||
func _scale_difficulty() -> void:
|
||||
difficulty_multiplier = 1.0 + (wave_number - 1) * 0.2
|
||||
enemy_count = max(1, int(3 + (wave_number - 1) * 0.5))
|
||||
enemy_health = int(10 * difficulty_multiplier)
|
||||
enemy_speed = 5.0 + (wave_number - 1) * 0.5
|
||||
|
||||
|
||||
## Get adjusted enemy count based on difficulty
|
||||
func get_enemy_count() -> int:
|
||||
return int(enemy_count * difficulty_multiplier)
|
||||
|
||||
|
||||
## Get adjusted enemy health based on difficulty
|
||||
func get_enemy_health() -> int:
|
||||
return int(enemy_health * difficulty_multiplier)
|
||||
|
||||
|
||||
## Get wave description for UI display
|
||||
func get_wave_description() -> String:
|
||||
return "Wave %d: %d enemies (Speed: %.1f, Health: %d)" % [
|
||||
wave_number,
|
||||
get_enemy_count(),
|
||||
enemy_speed,
|
||||
get_enemy_health()
|
||||
]
|
||||
|
||||
|
||||
## Check if wave has a specific modifier
|
||||
func has_modifier(modifier_name: StringName) -> bool:
|
||||
return modifier_name in wave_modifiers
|
||||
1
scripts/wave_config.gd.uid
Normal file
1
scripts/wave_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c0xlr04kjhr22
|
||||
101
tetra-tactics.md
Normal file
101
tetra-tactics.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Tetra Tactics
|
||||
|
||||
## Summary
|
||||
|
||||
Tetra Tactics is a strategic roguelike tower-defense.
|
||||
The player defends their position by building modular structures (tetraminos) and
|
||||
**exploiting their sinergies** while waves of enemies fall from above.
|
||||
|
||||
## Experience
|
||||
|
||||
**Primary Feeling**: The satisfaction of strategic planning combined with the
|
||||
thrill of narrowly avoiding disaster ("just barely made it"). The player should
|
||||
feel **clever** and **resourceful**.
|
||||
|
||||
A single encounter has a duration of ~10 minutes, or in general a short duration.
|
||||
|
||||
Depth emerges from the interaction between shape, positioning, timing, and build
|
||||
choices.
|
||||
|
||||
## Inspiration
|
||||
|
||||
- Ball Pit
|
||||
- 9 Kings
|
||||
- Drop Duchy
|
||||
- The King Is Watching
|
||||
- Vampire Survivor
|
||||
|
||||
## Design Pillars
|
||||
|
||||
- **Exploiting Sinergies**: breaking the game is fun
|
||||
- **Modular Depth**: simple tetromino rules combine in unexpected ways;
|
||||
complexity emerges naturally from interaction
|
||||
- **Consequence & Clarity**: threats are telegraphed clearly; every placement
|
||||
has visible impact; failure is immediate and understandable
|
||||
- **Momentum & Escalation**: each successful wave grows your toolkit and unlocks
|
||||
new synergies; pressure mounts steadily without overwhelming
|
||||
|
||||
## Encounter Loop
|
||||
|
||||
1. **Telegraph**: Warning indicators show where next wave enemies will fall.
|
||||
2. **Build & Adapt**:
|
||||
- Draft new shapes (randomly offered).
|
||||
- Place/Rotate units on persistent board.
|
||||
- **Trigger Synergies**: Adjacency bonuses light up (Visual Feedback).
|
||||
3. **Resolution**:
|
||||
- Enemies fall/attack.
|
||||
- Towers fire.
|
||||
- **Player repositions shapes in real-time** to dodge incoming fire.
|
||||
- **Player can active a the main tower skill**
|
||||
4. **Outcome**:
|
||||
- **Defense Breached**: Lose Health (Punishment).
|
||||
- **Defense Holds**: Gain Gold/XP.
|
||||
5. **Evolution**: Buy upgrades or Mutate blocks (Synergy scaling). Threats
|
||||
escalate automatically.
|
||||
6. **Repeat** until Death or Victory.
|
||||
|
||||
## Progression & Motivation
|
||||
|
||||
### Encounter Goals
|
||||
- Encounters consist of a **variable number of waves** (RNG-driven difficulty/length)
|
||||
- Some encounters feature **mini-bosses** mid-run
|
||||
- The **final encounter of a scenario** is a **boss fight**
|
||||
|
||||
### Player Motivation Arc
|
||||
- **Moment-to-Moment**: Hit synergies, exploit combinations, survive threats ("feeling clever")
|
||||
- **Single Run**: Build scaling through gold; adapt to random shapes; defeat
|
||||
mini-bosses and scenario bosses
|
||||
- **Long-Term (Meta)**: Unlock new block types and permanent perks across runs;
|
||||
expand synergy possibilities; enable new strategies in future runs
|
||||
|
||||
### Permanent Progression
|
||||
- **Unlockable Blocks**: New tetromino types unlock over time, creating fresh
|
||||
synergy combinations
|
||||
- **Unlockable Perks**: Passive bonuses that persist across runs, increasing
|
||||
player power and enabling riskier strategies
|
||||
- Progression feeds back into motivation: "What can I break with this new block?"
|
||||
|
||||
## Core Mechanics
|
||||
|
||||
- **Tetromino Placement & Rotation**: Spatial puzzle element; player chooses
|
||||
positioning and orientation on grid
|
||||
- **Adjacency Synergies**: Shapes trigger bonuses when placed next to
|
||||
complementary pieces; core depth driver
|
||||
- **Persistent Board State**: Board carries between waves; placement decisions
|
||||
compound and escalate over time
|
||||
- **Shape Drafting (Random)**: Player selects from randomly offered tetrominos
|
||||
or mutations; forces adaptive strategy
|
||||
- **Real-Time Repositioning**: During enemy attack, player moves shapes to dodge
|
||||
and optimize defense in slow motion
|
||||
- **Encounter Market**: Shapes or Upgrades purchased with gold; modify base
|
||||
stats and effects;
|
||||
- **Resource Economy**: Gold collection from successful waves → purchases upgrades/mutations;
|
||||
paces progression
|
||||
- **Automatic Wave Escalation**: Threats increase autonomously each wave;
|
||||
creates growing pressure
|
||||
- **Enemy Telegraph**: Visible threat indicators before waves;
|
||||
enables defensive planning
|
||||
- **Mini-Bosses & Bosses**: Special encounters with unique attacks that twist
|
||||
standard wave mechanics
|
||||
- **Perk System**: Permanent unlocks that affect all game state (damage, costs,
|
||||
synergy bonuses, etc.); unlock through meta-progression
|
||||
Reference in New Issue
Block a user