
Godot Genre Party
- 127 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-party for development tasks
About
godot-genre-party: A skill for development. This provides functionality for development workflows.
- godot-genre-party
Godot Genre Party by the numbers
- 127 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,750 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-partyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 127 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-party for development tasks
Files
Genre: Party / Minigame Collection
Expert blueprint for party games balancing accessibility, variety, and social fun.
NEVER Do (Expert Anti-Patterns)
Multiplayer & Input
- NEVER hardcode player inputs to specific joypad IDs (e.g., 0 or 1); strictly query dynamically via
Input.get_connected_joypads(). - NEVER bake player-IDs into the input map (e.g., "p1_jump"); strictly use a Dynamic Input Router to map physical controllers to players at runtime.
- NEVER use
Input.is_action_pressed()for assigning new player joins; strictly parse rawInputEventJoypadButtonin_unhandled_input()for device metadata. - NEVER allow inconsistent controls between games; strictly standardize across all minigames (A = Accept/Action, B = Back/Cancel, Joystick = Move).
- NEVER assume a disconnected joypad removes a player; strictly connect to the
joy_connection_changedsignal to pause and handle dropouts gracefully. - NEVER use boolean polling for analog sticks; strictly use
Input.get_vector()for precision and deadzones.
User Experience & Feedback
- NEVER use long text-based tutorials; strictly use a 3-second looping GIF + a single-sentence instruction overlay (e.g., "Mash A to fly!").
- NEVER ignore "Asymmetric" balance in 1v3 games; strictly provide the "One" with unique abilities or increased HP/speed to offset the numerical disadvantage.
- NEVER neglect Accessibility and Handicap systems; strictly implement optional support (e.g., speed boosts for lower-skilled players) to keep the competition social.
- NEVER leave UI Control nodes with
FOCUS_NONEfor gamepad menus; strictly set toFOCUS_ALLwith explicit focus neighbors for accessible navigation.
Rendering & Architecture
- NEVER use heavy scene transitions; strictly keep minigame assets light and use Threaded Background Loading while the instructions screen is active.
- NEVER draw global
CanvasLayerUI for individual split-screen players; strictly use per-viewportCanvasLayerchildren. - NEVER manually set sizes on
SubViewportchildren; strictly useGridContainerorBoxContainerfor automatic split-screen layout. - NEVER store tournament state or scores inside minigame scenes; strictly use a Persistent Autoload (Singleton).
- NEVER use a static
Camera2Dfor shared-room games; strictly use a dynamic group camera that zooms/pans to fit all players in frame. - NEVER overlap
SubViewportContainernodes without settingmouse_filtertoPASS; otherwise, top viewports will block input.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- party_input_router.gd - Professional local multiplayer solution mapping
device_idtoplayer_id.
Modular Components
- player_join_manager.gd - Slot mapping logic using raw JoypadButton event parsing.
- split_screen_manager.gd - SubViewport synchronization for shared physics worlds.
- tournament_state.gd - Persistent Autoload singleton for cross-scene state.
---
Core Loop
1. Lobby: Players join and select characters/colors. 2. Meta: Players move on a board or vote for the next game. 3. Play: Short, intense minigame (30s - 2m). 4. Score: Winners get points/coins. 5. Repeat: Cycle continues until a turn limit or score limit.
Skill Chain
| Phase | Skills | Purpose |
|---|---|---|
| 1. Input | input-mapping | Handling 2-4 local controllers dynamically |
| 2. Scene | godot-scene-management | Loading/Unloading minigames cleanly |
| 3. Data | godot-resource-data-patterns | Defining minigames via Resource files |
| 4. UI | godot-ui-containers | Scoreboards, instructions screens |
| 5. Logic | godot-turn-system | Managing the "Board Game" phase |
Architecture Overview
1. Minigame Definition
Using Resources to define what a minigame is.
# minigame_data.gd
class_name MinigameData extends Resource
@export var title: String
@export var scene_path: String
@export var instructions: String
@export var is_1v3: bool = false
@export var thumbnail: Texture2D2. The Party Manager
Singleton that persists between minigames.
# party_manager.gd
extends Node
var players: Array[PlayerData] = [] # Tracks score, input_device_id, color
var current_round: int = 1
var max_rounds: int = 10
func start_minigame(minigame: MinigameData) -> void:
# 1. Show instructions scene
await show_instructions(minigame)
# 2. Transition to actual game
get_tree().change_scene_to_file(minigame.scene_path)
# 3. Pass player data to the new scene
# (The minigame scene must look up PartyManager in _ready)3. Minigame Base Class
Every minigame inherits from this to ensure compatibility.
# minigame_base.gd
class_name Minigame extends Node
signal game_ended(results: Dictionary)
func _ready() -> void:
setup_players(PartyManager.players)
start_countdown()
func end_game() -> void:
# Calculate winner
game_ended.emit(results)
PartyManager.handle_minigame_end(results)Key Mechanics Implementation
Local Multiplayer Input
Handling dynamic device assignment.
# player_controller.gd
@export var player_id: int = 0 # 0, 1, 2, 3
func _physics_process(delta: float) -> void:
var device = PartyManager.players[player_id].device_id
# Use the specific device ID for input
var direction = Input.get_vector("p%s_left" % player_id, ...)
# Better approach: Remap InputMap actions at runtime explicitlyAsymmetric Gameplay (1v3)
Balancing the "One" vs the "Many".
- The One: Powerful, high HP, unique abilities (e.g., Bowser suit).
- The Many: Weak individually, must cooperate to survive/win.
Godot-Specific Tips
- SubViewport: Powerful for 4-player split-screen. Each player gets a camera, all rendering the same world (or different worlds!).
- InputEventJoypadButton: Use
Input.get_connected_joypads()to auto-detect controllers on the Lobby screen. - Remapping: Godot's
InputMapsystem can be modified at runtime usingInputMap.action_add_event(). Creating "p1_jump", "p2_jump" dynamically is a common pattern.
Common Pitfalls
1. Long Tutorials: Players just want to play. Fix: 3-second looping GIF + 1 sentence instruction overlay before the game starts. 2. Downtime: Loading times between 10-second minigames. Fix: Keep minigame assets light. Use a "Board" scene that stays loaded in the background if possible, or use creating Thread loading. 3. Confusing Controls: Minigame A uses "A" to jump, Minigame B uses "B". Fix: Standardize. "A" is always Accept/Action. "B" is always Back/Cancel.
---
🚀 Elite Technical Implementations (Batch 09)
1. Local-Input-Remapping Pattern (4+ Controllers)
To support dynamic local multiplayer, generate player-specific input actions at runtime using the InputMap singleton. This avoids hardcoding "p1_jump" and allows any connected joypad to be assigned to any player slot.
class_name PartyInputManager extends Node
## Dynamically registers input actions for a specific player and device.
func register_player_device(player_index: int, device_id: int) -> void:
var base_actions: Array[String] = ["jump", "dash", "interact"]
for action in base_actions:
var player_action: StringName = StringName("p%d_%s" % [player_index, action])
if not InputMap.has_action(player_action):
InputMap.add_action(player_action)
InputMap.action_erase_events(player_action)
var joy_event := InputEventJoypadButton.new()
joy_event.device = device_id
joy_event.button_index = JOY_BUTTON_A # Map based on action...
InputMap.action_add_event(player_action, joy_event)2. Minigame-Orchestrator Pattern (Scene Switching)
Party games rapidly cycle between minigames. Use an Autoload to securely free() the current scene and load the next PackedScene using call_deferred to prevent crashes during physics/logic execution.
class_name MinigameOrchestrator extends Node
var _current_scene: Node
func _ready() -> void:
_current_scene = get_tree().root.get_child(-1)
func transition_to_minigame(scene_path: String) -> void:
call_deferred("_deferred_transition", scene_path)
func _deferred_transition(scene_path: String) -> void:
_current_scene.free()
var next_scene := ResourceLoader.load(scene_path) as PackedScene
_current_scene = next_scene.instantiate()
get_tree().root.add_child(_current_scene)
get_tree().current_scene = _current_scene3. Screen-Shake-Global Pattern (Impact Observer)
Decouple camera effects from gameplay logic using the Observer pattern. Gameplay nodes broadcast impact intensity via a global signal, and the camera script listens to apply decaying noise to its h_offset and v_offset.
class_name ImpactCamera3D extends Camera3D
@export var decay_rate: float = 5.0
var _current_shake_strength: float = 0.0
func _process(delta: float) -> void:
if _current_shake_strength > 0.01:
_current_shake_strength = lerpf(_current_shake_strength, 0.0, decay_rate * delta)
h_offset = randf_range(-_current_shake_strength, _current_shake_strength)
v_offset = randf_range(-_current_shake_strength, _current_shake_strength)
else:
h_offset = 0.0
v_offset = 0.0
func _on_shake_requested(intensity: float) -> void:
_current_shake_strength = max(_current_shake_strength, intensity)- Master Skill: godot-master
# character_select_grid.gd
extends GridContainer
class_name CharacterSelectGrid
# Gamepad UI Navigation Flow
# Programmatically sets focus neighbors for seamless D-pad navigation.
func setup_focus_routing() -> void:
var buttons := get_children()
# Assume 2 columns for this example.
for i in buttons.size():
var btn := buttons[i] as Control
btn.focus_mode = Control.FOCUS_ALL
# Route left/right.
if i % 2 == 0 and i + 1 < buttons.size():
btn.set_focus_neighbor(SIDE_RIGHT, buttons[i + 1].get_path())
elif i % 2 != 0:
btn.set_focus_neighbor(SIDE_LEFT, buttons[i - 1].get_path())
# Route up/down.
if i >= 2:
btn.set_focus_neighbor(SIDE_TOP, buttons[i - 2].get_path())
if i + 2 < buttons.size():
btn.set_focus_neighbor(SIDE_BOTTOM, buttons[i + 2].get_path())
if not buttons.is_empty():
(buttons[0] as Control).grab_focus()
# connection_monitor.gd
extends Node
class_name ConnectionMonitor
# Controller Disconnect Handling
# Pauses the game if a controller battery dies mid-minigame.
func _ready() -> void:
Input.joy_connection_changed.connect(_on_joy_changed)
func _on_joy_changed(device: int, connected: bool) -> void:
if not connected:
# Cross-reference with active tournament players.
# if TournamentState.active_players.values().has(device):
get_tree().paused = true
# Notify UI to show reconnect prompt.
get_tree().call_group(&"ui_overlays", &"show_reconnect", device)
# deferred_scene_switcher.gd
extends Node
class_name DeferredSceneSwitcher
# Safe Deferred Scene Switcher
# Transitions between minigames without crashing due to logic mid-execution.
func switch_to_hub(path: String) -> void:
# NEVER free a scene immediately during its own logic execution.
call_deferred(&"_deferred_switch", path)
func _deferred_switch(path: String) -> void:
if get_tree().current_scene:
get_tree().current_scene.free()
var next := ResourceLoader.load(path) as PackedScene
var inst := next.instantiate()
get_tree().root.add_child(inst)
get_tree().current_scene = inst
extends Node
## Expert Local Input Manager (Godot 4.6).
## Gamepad hotplugging and player ID mapping.
var player_map = {} # PlayerID -> DeviceID
func _ready() -> void:
Input.joy_connection_changed.connect(_on_joy_changed)
for id in Input.get_connected_joypads():
_on_joy_changed(id, true)
func _on_joy_changed(device: int, connected: bool) -> void:
if connected:
var p_id = player_map.size() + 1
player_map[p_id] = device
print("P%d connected to Device %d" % [p_id, device])
else:
# Handle disconnection logic (pause game, etc)
pass
func is_player_action(p_id: int, action: StringName, event: InputEvent) -> bool:
return event.device == player_map.get(p_id, -1) and event.is_action(action)
## [SKILL NOTICE]: Use 'joy_connection_changed' to handle mid-game
## controller swaps. Validate 'event.device' in '_unhandled_input'.
# minigame_async_loader.gd
extends Node
class_name MinigameAsyncLoader
# Asynchronous Minigame Loader
# Preloads scenes in the background to prevent frame-rate drops during transitions.
var _target_path: String
func load_minigame(path: String) -> void:
_target_path = path
# Pattern: Request threaded load to keep UI responsive.
ResourceLoader.load_threaded_request(path)
func _process(_delta: float) -> void:
if _target_path.is_empty(): return
var status := ResourceLoader.load_threaded_get_status(_target_path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var scene := ResourceLoader.load_threaded_get(_target_path) as PackedScene
_target_path = ""
_swap_scene(scene)
func _swap_scene(scene: PackedScene) -> void:
# Safely swap the current scene.
get_tree().current_scene.queue_free()
var instance := scene.instantiate()
get_tree().root.add_child(instance)
get_tree().current_scene = instance
extends Node
## Expert Minigame Orchestrator (Godot 4.6).
## State-driven transitions between minigames.
enum Phase { INTRO, PLAY, RESULTS }
var current_phase = Phase.INTRO
func start_game() -> void:
current_phase = Phase.PLAY
# Start gameplay timer...
func end_game(winner_id: int) -> void:
current_phase = Phase.RESULTS
# Update persistent score (Autoload)
# ScoreManager.add_score(winner_id, 1)
# Transition back to main board or next game
get_tree().change_scene_to_file("res://scenes/results_screen.tscn")
## [SKILL NOTICE]: Use 'get_tree().change_scene_to_file()' to
## ensure clean memory teardown between unrelated minigames.
# minigame_player_controller.gd
extends CharacterBody3D
class_name MinigamePlayerController
# Raw Device Input Polling
# Reads analog data directly from a specific joypad ID for local multiplayer.
@export var device_id: int = -1
@export var speed := 10.0
func _physics_process(_delta: float) -> void:
if device_id == -1: return
# Pattern: Bypassing InputMap for precise multi-device polling.
var x := Input.get_joy_axis(device_id, JOY_AXIS_LEFT_X)
var y := Input.get_joy_axis(device_id, JOY_AXIS_LEFT_Y)
var dir := Vector3(x, 0, y)
if dir.length() > 0.2: # Deadzone
velocity = dir * speed
else:
velocity = Vector3.ZERO
move_and_slide()
# godot-master/scripts/party_party_input_router.gd
extends Node
## Party Input Router Expert Pattern
## Isolates inputs for 4 local players using device-ID prefixing.
signal player_joined(player_id: int, device_id: int)
signal player_input_received(player_id: int, action: String, strength: float)
var _player_assignments: Dictionary = {} # player_id -> device_id
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
var device_id = event.device
# 1. Join Logic
if event.is_action_pressed("ui_accept"):
_handle_join_request(device_id)
# 2. Input Isolation
var player_id = _get_player_from_device(device_id)
if player_id != -1:
_route_input(player_id, event)
func _handle_join_request(device_id: int) -> void:
if device_id not in _player_assignments.values():
var new_player_id = _player_assignments.size() + 1
if new_player_id <= 4:
_player_assignments[new_player_id] = device_id
player_joined.emit(new_player_id, device_id)
print("Player ", new_player_id, " Joined on Device ", device_id)
func _get_player_from_device(device_id: int) -> int:
for p_id in _player_assignments:
if _player_assignments[p_id] == device_id:
return p_id
return -1
func _route_input(player_id: int, event: InputEvent) -> void:
# 3. Action Prefixing
# In a professional party game, you shouldn't just check 'Input.is_action_pressed'.
# You must map the device-specific events to player-abstracted signals.
# Placeholder for action-mapping logic
# var action_name = _map_event_to_action(event)
# player_input_received.emit(player_id, action_name, event.get_action_strength(action_name))
pass
## EXPERT NOTE:
## Use 'InputMap.action_add_event()' at runtime to dynamically create
## "p1_jump", "p2_jump" actions maped to specific Device IDs.
## NEVER share a single "jump" action across players in local multiplayer.
# player_haptic_feedback.gd
extends Node
class_name PlayerHapticFeedback
# Localized Haptic Feedback
# Triggers rumble strictly on the controller of the player who was hit.
func trigger_vibration(device_id: int, weak: float, strong: float, duration: float) -> void:
# Pattern: Finite duration is mandatory to prevent infinite rumble on pause.
if device_id != -1:
Input.start_joy_vibration(device_id, weak, strong, duration)
func _on_eliminated(device_id: int) -> void:
trigger_vibration(device_id, 1.0, 1.0, 0.5)
# player_join_manager.gd
extends Node
class_name PlayerJoinManager
# Dynamic Player "Join" System (Local Multiplayer)
# Listens for any controller pressing "Start" and assigns device IDs.
signal player_joined(player_index: int, device_id: int)
var active_players: Dictionary = {} # Maps player_index (0-3) to device_id
var max_players := 4
func _unhandled_input(event: InputEvent) -> void:
# Pattern: Use raw InputEventJoypadButton to identify the EXACT physical device.
if event is InputEventJoypadButton and event.is_pressed() and not event.is_echo():
if not active_players.values().has(event.device):
if active_players.size() < max_players:
var next_player_index = active_players.size()
active_players[next_player_index] = event.device
player_joined.emit(next_player_index, event.device)
# Mark as handled to prevent multiple joins from one press.
get_viewport().set_input_as_handled()
# shared_party_camera.gd
extends Camera2D
class_name SharedPartyCamera
# Shared-Screen Dynamic Camera (2D)
# Manages zoom and positioning based on the bounding box of all players.
@export var min_zoom := 0.5
@export var max_zoom := 2.0
@export var margin := Vector2(100, 100)
func _process(_delta: float) -> void:
var players := get_tree().get_nodes_in_group(&"players")
if players.is_empty(): return
# Calculate bounding box for all players.
var bounds := Rect2(players[0].global_position, Vector2.ZERO)
for p in players:
bounds = bounds.expand(p.global_position)
# Center camera on the party.
global_position = bounds.get_center()
# Update zoom to fit everyone.
var screen_size := get_viewport_rect().size
var target_size := bounds.size + margin * 2.0
var zoom_f := minf(screen_size.x / target_size.x, screen_size.y / target_size.y)
zoom = Vector2.ONE * clampf(zoom_f, min_zoom, max_zoom)
# split_screen_manager.gd
extends GridContainer
class_name SplitScreenManager
# Dynamic Split-Screen Viewport Synchronizer
# Ensures all viewports share the same 3D physics and rendering world.
@export var main_world_viewport: SubViewport
func add_player_viewport(player_id: int, camera_target: Node3D) -> SubViewport:
var container := SubViewportContainer.new()
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
var viewport := SubViewport.new()
# CRITICAL: Share the same World3D so players see each other.
if main_world_viewport:
viewport.world_3d = main_world_viewport.world_3d
var camera := Camera3D.new()
viewport.add_child(camera)
container.add_child(viewport)
add_child(container)
return viewport
extends GridContainer
## Expert Split-Screen Layout (Godot 4.6).
## Standard 2-4 player grid configuration.
func setup(players: int) -> void:
# Clear existing
for c in get_children(): c.queue_free()
columns = 2 if players > 1 else 1
for i in range(players):
var container = SubViewportContainer.new()
container.stretch = true
container.size_flags_horizontal = SIZE_EXPAND_FILL
container.size_flags_vertical = SIZE_EXPAND_FILL
var viewport = SubViewport.new()
var cam = Camera3D.new() # Or Camera2D
add_child(container)
container.add_child(viewport)
viewport.add_child(cam)
## [SKILL NOTICE]: Use 'SubViewportContainer' with 'stretch=true'.
## For adaptive split-screen (merging), use a custom shader on a ColorRect.
# tournament_state.gd
extends Node
# Configured as an Autoload named 'TournamentState'
# Global Tournament State (Autoload)
# Persists scores and round progression across minigame scene switches.
var player_scores: Dictionary = {} # player_id -> score
var current_round: int = 1
var active_players: Dictionary = {} # device_id mapping
func award_points(player_id: int, points: int) -> void:
player_scores[player_id] = player_scores.get(player_id, 0) + points
func reset_tournament() -> void:
player_scores.clear()
current_round = 1