
Godot Genre Roguelike
- 264 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-roguelike for development tasks
About
godot-genre-roguelike: A skill for development. This provides functionality for development workflows.
- godot-genre-roguelike
Godot Genre Roguelike by the numbers
- 264 all-time installs (skills.sh)
- +23 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,467 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-roguelikeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 264 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-roguelike for development tasks
Files
Genre: Roguelike
Expert blueprint for roguelikes balancing challenge, progression, and replayability.
NEVER Do (Expert Anti-Patterns)
Generation & RNG
- NEVER make runs dependent on pure RNG; strictly provide mitigation (rerolls, shops, pity timers) to ensure every run is winnable.
- NEVER use unseeded RNG for world generation; strictly initialize isolated
RandomNumberGeneratorwith a predictable seed for daily runs/debugging. - NEVER rely on
@GlobalScope.randi()for critical logic; strictly use local RNG instances to prevent global state pollution. - NEVER use
Array.pick_random()for critical content drops; strictly use a Shuffle Bag to prevent statistically unfair streaks. - NEVER generate massive dungeons on the main thread; strictly use `WorkerThreadPool.add_task()` or `add_group_task()` to distribute generation across cores and prevent frame freezes.
- NEVER interact with the SceneTree from a background thread; strictly generate dungeon data in a thread-safe Array/PackedByteArray before parsing on the main thread.
Data & State
- NEVER allow Save Scumming; strictly delete mid-run save files immediately upon loading to enforce permadeath.
- NEVER allow the player to see the "Edge of the World"; strictly use Fog of War or limited vision cones to maintain the mystery of the unknown.
- NEVER evaluate complex "Director" heuristics every frame; strictly use Frame-Slicing (`Engine.get_process_frames()`) to run heavy pacing logic only once every 60-120 frames for CPU efficiency.
- NEVER move rooms individually by pixel values during procedural generation; strictly use `Marker2D` Connection Points in pre-authored scenes to calculate exact offsets for seamless room stitching.
- NEVER allow Run State to leak into Meta State; strictly use separate singletons or Resources for
RunManagerandMetaManager. - NEVER scale meta-progression to be overpowered (+100% damage); strictly keep upgrades subtle (+5-15%) to maintain skill-based play.
- NEVER forget to call
duplicate(true)on base stat Resources; failing to deep-duplicate causes all entities to share a single health instance. - NEVER save run states to
.tscnfiles; strictly serialize to JSON or binary inuser://to prevent bloat. - NEVER rely on the
SceneTreeas the source of truth for grid logic; strictly maintain grid data in a separate Dictionary or Array.
Grid & Performance
- NEVER forget to handle Navigation re-baking; strictly rebake
NavigationRegion2DAFTER procedural tiles are placed. - NEVER use AStar2D for tile grids; strictly use `AStarGrid2D` with `jumping_enabled = true` (Jump Point Search) for O(1) queries and high-performance pathing across open areas.
- NEVER forget to call
update()onAStarGrid2Dafter modifying states; strictly ensures pathfinding queries aren't stale. - NEVER use floats (
Vector2) for discrete grid coordinates; strictly use Vector2i to prevent precision drift. - NEVER use Manhattan heuristics for 8-way movement; strictly use `HEURISTIC_CHEBYSHEV` or `HEURISTIC_OCTILE`.
- NEVER iterate over every cell coordinate (0 to W,H) in GDScript; strictly use
get_used_cells()for optimized tile access. - NEVER clear procedural levels using
free(); strictly usequeue_free()to avoid mid-frame segmentation faults. - NEVER broadcast mass state changes to a grid immediately; strictly use
call_deferred()or `call_group_flags` to avoid frame spikes during turn transitions. - NEVER use heavy TileMapLayer nodes for high-resolution Fog of War; strictly use a GPU Shader Mask via
ColorRectand anImageTextureupdated via `RenderingServer.texture_2d_update()`.
🛠 Expert Components (scripts/)
Original Expert Patterns
- meta_progression_manager.gd - Foundational meta-progression logic with secure data persistence and currency unlocks.
- roguelike_patterns.gd - 10 Essential Roguelike Expert Snippets (AStar, BSP, WorkerThreadPool, ShuffleBag, etc.).
Modular Components
- dungeon_generator_walker.gd - Drunkard's Walk algorithm for carving procedural rooms and caves.
- fov_raycast_calculator.gd - High-performance LOS checking using physics server queries.
- seeded_rng_resource.gd - RNG state persistence for deterministic and shareable replayability.
- turn_manager_decoupled.gd - Signal-driven turn coordination for decoupled entity logic.
- astar_grid_handler.gd - Specialised AStarGrid2D wrapper for optimized roguelike pathfinding.
- weighted_loot_table.gd - Native-optimized weighted random item drops with drop-rate controls.
- json_state_serializer.gd - Persistent serialization for procedural entity data and run states.
- fog_of_war_masker.gd - TileMapLayer-based visibility masking and discovery system.
- meta_progression_resource.gd - Data separation for permanent game unlocks and skill trees.
- move_command_object.gd - Command pattern implementation for reversible turn-based actions.
- dungeon_generator.gd - High-level procedural orchestrator for room-and-hallway layout generation.
Core Loop
1. Preparation: Select character, equip meta-upgrades (see meta_progression_resource.gd). 2. The Run: complete procedural levels (dungeon_generator_walker.gd), acquire temporary power-ups. 3. The Challenge: Survive increasingly difficult encounters using A pathfinding (`astar_grid_handler.gd`). 4. Death/Victory: Run ends, resources calculated. 5. Meta-Progression: Spend resources on permanent unlocks (`meta_progression_resource.gd`). 6. Repeat*: Start a new run with new capabilities.
Skill Chain
| Phase | Skills | Purpose |
|---|---|---|
| 1. Architecture | state-machines, autoloads | Managing Run State vs Meta State |
| 2. World Gen | godot-procedural-generation, tilemap, noise | Creating unique levels every run |
| 3. Combat | godot-combat-system, enemy-ai | Fast-paced, high-stakes encounters |
| 4. Progression | loot-tables, godot-inventory-system | Managing run-specific items/relics |
| 5. Persistence | save-system, resources | Saving meta-progress between runs |
Architecture Overview
Roguelikes require a strict separation between Run State (temporary) and Meta State (persistent).
1. Run Manager (AutoLoad)
Handles the lifespan of a single run. Resets completely on death.
# run_manager.gd
extends Node
signal run_started
signal run_ended(victory: bool)
signal floor_changed(new_floor: int)
var current_seed: int
var current_floor: int = 1
var player_stats: Dictionary = {}
var inventory: Array[Resource] = []
var rng: RandomNumberGenerator
func start_run(seed_val: int = -1) -> void:
rng = RandomNumberGenerator.new()
if seed_val == -1:
rng.randomize()
current_seed = rng.seed
else:
current_seed = seed_val
rng.seed = current_seed
current_floor = 1
_reset_run_state()
run_started.emit()
func _reset_run_state() -> void:
player_stats = { "hp": 100, "gold": 0 }
inventory.clear()
func next_floor() -> void:
current_floor += 1
floor_changed.emit(current_floor)
func end_run(victory: bool) -> void:
run_ended.emit(victory)
# Trigger meta-progression save here2. Meta-Progression (Resource)
Stores permanent unlocks.
# meta_progression.gd
class_name MetaProgression
extends Resource
@export var total_runs: int = 0
@export var unlocked_weapons: Array[String] = ["sword_basic"]
@export var currency: int = 0
@export var skill_tree_nodes: Dictionary = {} # node_id: level
func save() -> void:
ResourceSaver.save(self, "user://meta_progression.tres")
static func load_or_create() -> MetaProgression:
if ResourceLoader.exists("user://meta_progression.tres"):
return ResourceLoader.load("user://meta_progression.tres")
return MetaProgression.new()Key Mechanics implementation
Procedural Dungeon Generation
- Drunkard's Walk (Walker): Ideal for organic, cave-like or connected room layouts.
- Binary Space Partitioning (BSP): Best for rectangular, connected room-and-hallway dungeons.
- Wave Function Collapse (WFC): For highly detailed, rule-based tile environments and modular room assembly.
# dungeon_generator.gd
extends Node
@export var map_width: int = 50
@export var map_height: int = 50
@export var max_walkers: int = 5
@export var max_steps: int = 500
func generate_dungeon(tilemap: TileMapLayer, rng: RandomNumberGenerator) -> void:
tilemap.clear()
var walkers: Array[Vector2i] = [Vector2i(map_width/2, map_height/2)]
var floor_tiles: Array[Vector2i] = []
for step in max_steps:
var new_walkers: Array[Vector2i] = []
for walker in walkers:
floor_tiles.append(walker)
# 25% chance to destroy walker, 25% to spawn new one
if rng.randf() < 0.25 and walkers.size() > 1:
continue # Destroy
if rng.randf() < 0.25 and walkers.size() < max_walkers:
new_walkers.append(walker) # Spawn
# Move walker
var direction = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT].pick_random()
new_walkers.append(walker + direction)
walkers = new_walkers
# Set tiles
for pos in floor_tiles:
tilemap.set_cell(pos, 0, Vector2i(0,0)) # Assuming source_id 0 is floor
# Post-process: Add walls, spawn points, etc.Item/Relic System (Resource-based)
Relics modify stats or add behavior.
# relic.gd
class_name Relic
extends Resource
@export var id: String
@export var name: String
@export var icon: Texture2D
@export_multiline var description: String
# Hook system for complex interactions
func on_pickup(player: Node) -> void:
pass
func on_damage_dealt(player: Node, target: Node, damage: int) -> int:
return damage # Return modified damage
func on_kill(player: Node, target: Node) -> void:
pass# example_relic_vampirism.gd
extends Relic
func on_kill(player: Node, target: Node) -> void:
player.heal(5)
print("Vampirism triggered!")4. Director-AI (Pacing Manager)
Use frame-slicing to evaluate student performance and adjust difficulty without CPU spikes.
# director_ai.gd (Autoload)
func _process(_delta):
# Only evaluate every 60 frames
if Engine.get_process_frames() % 60 == 0:
_update_pacing_logic()
func _update_pacing_logic():
if player_health < 30:
spawn_rate -= 0.5 # Ease up
elif player_kills > 100:
spawn_rate += 1.0 # Challenge more5. Procedural Room Assembler (Markers)
Snap rooms together using connection markers for pixel-perfect stitching.
# room_assembler.gd
func add_room(new_scene: PackedScene, prev_exit: Marker2D):
var inst = new_scene.instantiate()
add_child(inst)
await inst.tree_entered # Wait for node to be ready
var entrance = inst.get_node("Entrance")
# Snap room so entrance matches previous exit
var offset = inst.global_position - entrance.global_position
inst.global_position = prev_exit.global_position + offset6. Synergy-Tag System (Relics)
Use tag aggregation on ItemData resources to trigger synergistic effects.
# synergy_manager.gd
func check_synergies(inventory: Array[ItemData]):
var tags = {}
for item in inventory:
for tag in item.synergy_tags:
tags[tag] = tags.get(tag, 0) + 1
if tags.get(&"Fire", 0) >= 1 and tags.get(&"Projectile", 0) >= 1:
activate_synergy(&"Flaming_Arrow")
## Common Pitfalls
1. **RNG Dependency**: Don't make runs entirely dependent on luck. Good roguelikes allow skill to mitigate bad RNG.
2. **Meta-progression Imbalance**: If meta-upgrades are too strong, the game becomes a "grind to win" rather than "learn to win".
3. **Lack of Variety**: Procedural generation is only as good as the content it arranges. You need *a lot* of content (rooms, enemies, items) to keep it fresh.
4. **Save Scumming**: Players will try to quit to avoid death. Save the state only on floor transition or quit, and delete the save on load (optional, but standard for strict roguelikes).
## Godot-Specific Tips
- **Seeded Runs**: Always initialize `RandomNumberGenerator` with a seed. This allows players to share specific run layouts.
- **ResourceSaver**: Use `ResourceSaver` for meta-progression, but be careful with cyclical references in deeply nested resources.
- **Scenes as Rooms**: Build your "rooms" as separate scenes (`Room1.tscn`, `Room2.tscn`) and instance them into the generated layout for handcrafted quality within procedural layouts.
- **Navigation**: Rebake `NavigationRegion2D` at runtime after generating the dungeon layout if using 2D navigation.
## Advanced Techniques
- **Synergy System**: Tag items (`fire`, `projectile`, `companion`) and check for tag combinations to create emergent power-ups.
- **Director AI**: An invisible "Director" system that tracks player health/stress and adjusts spawn rates dynamically (like *Left 4 Dead*).
## Reference
- Master Skill: [godot-master](../godot-master/SKILL.md)
class_name GridPathfinder extends Node
## Specialist AStarGrid2D handler for tile-based roguelikes.
## Prefers Manhattan heuristic for 4-way grid movement.
var _grid := AStarGrid2D.new()
func setup_grid(rect: Rect2i, cell_size: Vector2i, diagonals: bool = false) -> void:
_grid.region = rect
_grid.cell_size = cell_size
if diagonals:
_grid.default_compute_heuristic = AStarGrid2D.HEURISTIC_CHEBYSHEV
_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ALWAYS
else:
_grid.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_grid.update()
func set_cell_solid(cell: Vector2i, solid: bool = true) -> void:
if _grid.region.has_point(cell):
_grid.set_point_solid(cell, solid)
func get_cell_path(from: Vector2i, to: Vector2i) -> Array[Vector2i]:
if not _grid.region.has_point(from) or not _grid.region.has_point(to):
return []
return _grid.get_id_path(from, to)
extends Node
class_name AsyncTurnManager
## Expert Turn Manager (Godot 4.6).
## Asynchronous loop using 'await' to sync actions and animations.
var entities: Array[Node] = []
func run_turn_loop() -> void:
while true:
for entity in entities:
if not is_instance_valid(entity): continue
# Start entity turn
entity.begin_turn()
# Expert Pattern: Halt loop until entity emits 'finished'
# This ensures animations (Tweens) complete before the next turn.
await entity.turn_finished
if _check_victory(): return
## [SKILL NOTICE]: Use 'await' inside the turn loop to cleanly handle
## asynchronous actions like movement Tweens and attack animations.
class_name DrunkardsWalk extends RefCounted
## Drunkard's Walk algorithm for carving procedural dungeons.
## Uses Vector2i for discrete grid coordinates and isolated RNG for determinism.
static func generate_map(start_pos: Vector2i, steps: int, rng: RandomNumberGenerator) -> Dictionary:
var map_data: Dictionary = {}
var current_pos := start_pos
# 4-way orthogonal directions (standard top-down)
var directions: Array[Vector2i] = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT]
for i in range(steps):
# Mark current position as floor (using StringName for performance)
map_data[current_pos] = &"floor"
# Pick a random direction from the RNG instance
var dir := directions[rng.randi() % directions.size()]
current_pos += dir
return map_data
# skills/genre-roguelike/scripts/dungeon_generator.gd
extends Node
## Dungeon Generator (Expert Pattern)
## Implements a walker-based procedural generation algorithm.
## Creates organic, connected layouts suitable for roguelikes.
class_name DungeonGenerator
signal generation_complete(rooms: Array, spawn_point: Vector2)
@export var tile_map: TileMapLayer
@export var map_width: int = 50
@export var map_height: int = 50
@export var max_walkers: int = 5
@export var max_steps: int = 400
@export var room_chance: float = 0.2
var rng: RandomNumberGenerator
func generate(seed_val: int) -> void:
rng = RandomNumberGenerator.new()
rng.seed = seed_val
tile_map.clear()
var start_pos = Vector2i(map_width / 2, map_height / 2)
var floor_tiles: Array[Vector2i] = [start_pos]
var walkers: Array[Vector2i] = [start_pos]
# Drunkard's Walk
for i in range(max_steps):
var new_walkers: Array[Vector2i] = []
for walker in walkers:
# Move
var dir = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT]
var move = dir[rng.randi() % 4]
var new_pos = walker + move
# Clamp to bounds
new_pos.x = clamp(new_pos.x, 2, map_width - 2)
new_pos.y = clamp(new_pos.y, 2, map_height - 2)
if new_pos not in floor_tiles:
floor_tiles.append(new_pos)
# Fork/Kill logic
if rng.randf() < 0.2 and walkers.size() < max_walkers:
new_walkers.append(new_pos) # Clone walker
new_walkers.append(new_pos)
elif rng.randf() < 0.05 and walkers.size() > 1:
pass # Kill walker
else:
new_walkers.append(new_pos) # Keep walking
walkers = new_walkers
if walkers.is_empty(): # Safety
walkers.append(floor_tiles.pick_random())
# Set Tiles
for pos in floor_tiles:
tile_map.set_cell(pos, 0, Vector2i(0, 0)) # Assuming Atlas 0, Coords 0,0 is Floor
# Post-Processing: Walls
_generate_walls(floor_tiles)
generation_complete.emit(floor_tiles, Vector2(start_pos) * tile_map.tile_set.tile_size.x)
func _generate_walls(floor_tiles: Array[Vector2i]) -> void:
# Helper to surround floors with walls
# (Simplified for example)
pass
## EXPERT USAGE:
## Call generate(seed). Use generated floor_tiles to spawn enemies/loot.
class_name FogManager extends Node2D
## Grid-based Fog of War masker for TileMapLayer.
## Efficiently clears "fog" tiles based on FOV results.
@export var fog_layer: TileMapLayer
@export var fog_atlas_coord := Vector2i(0, 0) # Coordinate of the black tile in tileset
@export var fog_source_id := 0
func initialize_fog(region: Rect2i) -> void:
if not fog_layer: return
for x in range(region.position.x, region.end.x):
for y in range(region.position.y, region.end.y):
fog_layer.set_cell(Vector2i(x, y), fog_source_id, fog_atlas_coord)
func reveal_cells(visible_cells: Array[Vector2i]) -> void:
if not fog_layer: return
for cell in visible_cells:
# Setting source_id to -1 removes the cell (erases fog)
fog_layer.set_cell(cell, -1)
class_name FOVCalculator extends Node2D
## High-performance Field-of-View calculation using raycasts.
## Bypasses Area2D overhead by querying PhysicsDirectSpaceState2D directly.
func calculate_fov(player_pos: Vector2, radius: float, targets: Array[Vector2]) -> Array[Vector2]:
var visible_targets: Array[Vector2] = []
var space_state := get_world_2d().direct_space_state
var radius_sq := radius * radius
for target in targets:
# Early exit for distance
if player_pos.distance_squared_to(target) > radius_sq:
continue
# Construct raycast query
var query := PhysicsRayQueryParameters2D.create(player_pos, target)
# Exclude player to prevent self-collision
if get_parent() is CollisionObject2D:
query.exclude = [get_parent().get_rid()]
# If empty, path is clear (visible)
var result := space_state.intersect_ray(query)
if result.is_empty():
visible_targets.append(target)
return visible_targets
class_name StateSerializer extends Node
## JSON-based persistence system for procedurally generated entities.
## Avoids PackedScene bloat for high-variance runtime states.
const SAVE_PATH = "user://run_state.json"
func save_run_state(entities: Array[Node]) -> Error:
var save_data := []
for entity in entities:
if entity.has_method(&"get_save_dict"):
save_data.append(entity.call(&"get_save_dict"))
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if not file:
return FileAccess.get_open_error()
file.store_string(JSON.stringify(save_data))
return OK
func load_run_state() -> Array:
if not FileAccess.file_exists(SAVE_PATH):
return []
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if not file:
return []
var test_json_conv = JSON.new()
var error = test_json_conv.parse(file.get_as_text())
if error == OK:
return test_json_conv.get_data()
return []
# skills/genre-roguelike/scripts/meta_progression_manager.gd
extends Node
## Meta Progression Manager (Expert Pattern)
## Handles persistent data across runs, including currency, unlocks, and stats.
## Uses secure saving/loading to prevent casual tampering.
class_name MetaProgressionManager
signal currency_changed(new_amount: int)
signal upgrade_purchased(upgrade_id: String, new_level: int)
const SAVE_PATH = "user://meta_progression.save"
const SECRET_KEY = "CHANGE_ME_IN_PROD" # Use a proper key management strategy
# Data Structure
var save_data: Dictionary = {
"currency": 0,
"total_runs": 0,
"unlocked_items": [],
"upgrades": {} # upgrade_id: level
}
func _ready() -> void:
load_progress()
func add_currency(amount: int) -> void:
save_data["currency"] += amount
currency_changed.emit(save_data["currency"])
save_progress()
func purchase_upgrade(upgrade_id: String, cost: int) -> bool:
if save_data["currency"] >= cost:
save_data["currency"] -= cost
if not save_data["upgrades"].has(upgrade_id):
save_data["upgrades"][upgrade_id] = 0
save_data["upgrades"][upgrade_id] += 1
currency_changed.emit(save_data["currency"])
upgrade_purchased.emit(upgrade_id, save_data["upgrades"][upgrade_id])
save_progress()
return true
return false
func get_upgrade_level(upgrade_id: String) -> int:
return save_data["upgrades"].get(upgrade_id, 0)
func save_progress() -> void:
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file:
var json_str = JSON.stringify(save_data)
# Simple obfuscation (XOR or base64) to deter casual edits
# For real security, use FileAccess.open_encrypted_with_pass
var encrypted = Marshalls.utf8_to_base64(json_str)
file.store_string(encrypted)
file.close()
func load_progress() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if file:
var encrypted = file.get_as_text()
var json_str = Marshalls.base64_to_utf8(encrypted)
var json = JSON.new()
var parse_result = json.parse(json_str)
if parse_result == OK:
save_data = json.data
else:
printerr("Save file corrupted!")
file.close()
## EXPERT USAGE:
## Autoload this script. Call add_currency() at end of run.
## Check get_upgrade_level() during gameplay to apply buffs.
class_name MetaProgression extends Resource
## Global meta-progression data stored separately from session data.
## Prevents wipe of permanent unlocks upon run permadeath.
@export var global_currency: int = 0
@export var unlocked_ability_ids: Array[StringName] = []
@export var permanent_upgrades: Dictionary = {}
const META_PATH = "user://meta_progression.tres"
func save_global() -> Error:
return ResourceSaver.save(self, META_PATH)
static func load_global() -> MetaProgression:
if ResourceLoader.exists(META_PATH):
return load(META_PATH) as MetaProgression
return MetaProgression.new()
func increment_currency(amount: int) -> void:
global_currency += amount
emit_changed()
extends Resource
class_name MetaStatsResource
## Expert Meta-Progression (Godot 4.6).
## Encrypted persistent stats for cross-run upgrades.
@export var max_health_bonus: int = 0
@export var attack_multiplier: float = 1.0
@export var unlocked_classes: Array[String] = []
const SAVE_PATH = "user://meta_progression.cfg"
const CRYPTO_KEY = "expert_godot_roguelike_key"
func save_stats() -> void:
var config = ConfigFile.new()
config.set_value("Meta", "health", max_health_bonus)
config.set_value("Meta", "attack", attack_multiplier)
# Expert Pattern: Save encrypted to prevent user tampering
config.save_encrypted_pass(SAVE_PATH, CRYPTO_KEY)
func load_stats() -> void:
var config = ConfigFile.new()
if config.load_encrypted_pass(SAVE_PATH, CRYPTO_KEY) == OK:
max_health_bonus = config.get_value("Meta", "health", 0)
attack_multiplier = config.get_value("Meta", "attack", 1.0)
## [SKILL NOTICE]: Always save meta-progression to 'user://' using
## 'save_encrypted_pass()' to protect the game's economy from easy cheats.
class_name MoveCommand extends RefCounted
## Command pattern object for turn-based movement.
## Encapsulates validation and state changes for easy Undo/Redo or logging.
var _entity: Node2D
var _target_cell: Vector2i
var _previous_cell: Vector2i
var _pathfinder: AStarGrid2D
func _init(entity: Node2D, target: Vector2i, pf: AStarGrid2D) -> void:
_entity = entity
_target_cell = target
_pathfinder = pf
# Assuming entity has a grid_position property
if entity.get("grid_position"):
_previous_cell = entity.grid_position
func execute() -> bool:
# Validation: Is the cell occupied?
if _pathfinder and _pathfinder.is_point_solid(_target_cell):
return false
# Apply state
if _entity.has_method(&"set_grid_position"):
_entity.call(&"set_grid_position", _target_cell)
else:
_entity.set(&"grid_position", _target_cell)
return true
func undo() -> void:
if _entity.has_method(&"set_grid_position"):
_entity.call(&"set_grid_position", _previous_cell)
else:
_entity.set(&"grid_position", _previous_cell)
extends Node2D
class_name NoiseDungeonGenerator
## Expert Dungeon Generation (Godot 4.6).
## Uses FastNoiseLite for organic caves and AStarGrid2D for connectivity.
@export var tile_map: TileMapLayer
@export var width: int = 64
@export var height: int = 64
@export var wall_threshold: float = 0.2
var _noise := FastNoiseLite.new()
var _astar := AStarGrid2D.new()
func generate() -> void:
_noise.seed = randi()
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
_astar.region = Rect2i(0, 0, width, height)
_astar.update()
for x in range(width):
for y in range(height):
var pos = Vector2i(x, y)
if _noise.get_noise_2d(x, y) > wall_threshold:
tile_map.set_cell(pos, 0, Vector2i(0,0)) # Wall
_astar.set_point_solid(pos, true)
else:
tile_map.set_cell(pos, 1, Vector2i(1,0)) # Floor
# Expert Tip: Use _astar.get_id_path() to check connectivity
# and carve corridors between isolated noise islands.
## [SKILL NOTICE]: Use 'AStarGrid2D' to validate connectivity in noise-based
## dungeons. It is highly optimized for 2D grids and corridor carving.
# roguelike_patterns.gd
extends Node
# 1. Initializing AStarGrid2D for Dungeon Pathfinding
# EXPERT NOTE: Prefer AStarGrid2D for grid-based pathfinding; always call update() before use.
func setup_dungeon_grid(rect: Rect2i) -> AStarGrid2D:
var grid := AStarGrid2D.new()
grid.region = rect
grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
grid.update()
return grid
# 2. Typed Dictionary for Permadeath Save States
# EXPERT NOTE: Enforces strict typing for JSON serialization of your meta-progression.
var meta_progression: Dictionary[StringName, int] = {
&"total_deaths": 0,
&"currency": 0
}
# 3. Offloading Procedural Generation
# EXPERT NOTE: Frees the main thread to render loading screens smoothly.
func generate_dungeon_async() -> void:
WorkerThreadPool.add_task(_compute_dungeon_layout, true, "DungeonGen")
func _compute_dungeon_layout() -> void:
# Heavy generation logic here
pass
# 4. Turn-Based State Machine via Pattern Matching
# EXPERT NOTE: Use StringNames and match for optimized state logic.
func process_turn(state: StringName) -> void:
match state:
&"player_turn":
# await player.execute_turn()
pass
&"enemy_turn":
get_tree().call_group(&"enemies", &"take_action")
# 5. Translating Grid Coordinates to World Coordinates
# EXPERT NOTE: Precise mapping from discrete grid cells to smooth world movement.
func move_to_cell(layer: TileMapLayer, coords: Vector2i) -> void:
var world_pos := layer.map_to_local(coords)
create_tween().tween_property(self, "position", world_pos, 0.2)
# 6. Deep Duplication of Base Stats
# EXPERT NOTE: Ensures this enemy gets a unique copy of the resource data.
@export var base_stats: Resource
func setup_enemy() -> void:
if base_stats:
base_stats = base_stats.duplicate(true) # duplicate(true) is deep in Godot 4
# 7. Modifying TileMapLayers dynamically (Breaking Walls)
# EXPERT NOTE: Update both visuals and navigation logic simultaneously.
func break_wall(layer: TileMapLayer, coords: Vector2i, astar_grid: AStarGrid2D) -> void:
layer.erase_cell(coords)
astar_grid.set_point_solid(coords, false)
# 8. Decoupled Combat Logic (Signal Up, Call Down)
# EXPERT NOTE: Use duck-typing with has_method for loosely coupled combat.
func _on_attack_landed(target: Node, damage: int) -> void:
if target.has_method(&"take_damage"):
target.call(&"take_damage", damage)
# 9. Extracting Save State via Groups
# EXPERT NOTE: Use Array.map for clean, high-level extraction of save data.
func save_dungeon_state() -> Array[Dictionary]:
var entities := get_tree().get_nodes_in_group(&"Persist")
return entities.map(func(node): return node.save() if node.has_method("save") else {})
# 10. Shuffle Bag Randomization for Loot
# EXPERT NOTE: Prevents streaks and ensures variety in item drops.
var _loot_pool: Array[String] = ["potion", "sword", "shield"]
func get_random_loot() -> String:
if _loot_pool.is_empty(): return ""
_loot_pool.shuffle()
return _loot_pool.pop_back()
class_name RoguelikeRNG extends Resource
## Resource for managing seeded randomness and persistence.
## Stores the internal PCG32 state for deterministic reloads.
@export var seed_value: int = 0
var _rng := RandomNumberGenerator.new()
func initialize(new_seed: int) -> void:
seed_value = new_seed
_rng.seed = seed_value
func get_generator() -> RandomNumberGenerator:
return _rng
## Replay-safe state capturing
func save_state() -> int:
return _rng.state
func load_state(saved_state: int) -> void:
_rng.seed = seed_value
_rng.state = saved_state
## Utility for "fair" random using shuffle bag
func get_shuffled_bag(items: Array) -> Array:
var bag := items.duplicate()
bag.shuffle()
return bag
class_name TurnManager extends Node
## Signal-based coordinator for turn-based games.
## Decouples entity logic from global scheduling.
signal turn_started(entity: Node)
signal all_turns_completed
var _entities: Array[Node] = []
var _current_index: int = 0
func register_entity(entity: Node) -> void:
if not _entities.has(entity):
_entities.append(entity)
# Expects entities to have a turn_finished signal
if entity.has_signal(&"turn_finished"):
entity.connect(&"turn_finished", _on_entity_turn_finished)
func start_loop() -> void:
_current_index = 0
_trigger_turn()
func _on_entity_turn_finished() -> void:
_current_index += 1
if _current_index >= _entities.size():
_current_index = 0
all_turns_completed.emit()
# Start next turn on next frame to prevent deep recursion
_trigger_turn.call_deferred()
func _trigger_turn() -> void:
if _entities.is_empty(): return
turn_started.emit(_entities[_current_index])
class_name LootTable extends Resource
## Data-driven weighted loot table using Godot 4 native optimization.
## Avoids slow GDScript loops for probability calculation.
@export var items: Array[Resource] = []
@export var weights: PackedFloat32Array = PackedFloat32Array()
## Uses native rand_weighted for O(1) or O(log N) speed depending on size.
func roll_item(rng: RandomNumberGenerator) -> Resource:
if items.is_empty() or weights.size() != items.size():
push_error("LootTable: Weights size mismatch or empty items.")
return null
var rolled_idx := rng.rand_weighted(weights)
return items[rolled_idx]
func add_entry(item: Resource, weight: float) -> void:
items.append(item)
weights.append(weight)