Files
wicked-quest/levels/README.md

22 KiB

Creating Custom Levels for Wicked Quest

Want to add your own levels? It's simple! Every level is a JSON file, and you can create as many as you want.

Getting Started

  1. Create a directory under levels/ with your level pack name:

    levels/My Cool Levels/
    
  2. Start with 1.json in your new directory. Each level after that is numbered in order: 2.json, 3.json, etc.

Basic Level Structure

Every level needs these essential properties:

{
    "level_id": 1,
    "name": "My Level",
    "description": "Set the scene for your level.",
    "locked": false,
    "player_start": {
        "x": 0,
        "y": 0
    },
    "boundaries": {
        "left": 0,
        "right": 200
    },
    "ambience": "Graveyard Blitz.ogg",
    "footstep_sound": "footstep_stone"
}

If you set locked to true, the player cannot leave the level until all enemies have been defeated. Drop custom ambience files (e.g., music or creepy soundtracks) in sounds/ambience/. Add custom footstep sounds into the sounds/ directory.

Dialogue System

Instead of using a simple "description" field, levels can now include interactive dialogue sequences with the new "dialog" property. This creates immersive story-driven introductions with character conversations:

{
    "level_id": 1,
    "name": "Story Level",
    "dialog": {
        "allow_skip": true,
        "entries": [
            {
                "speaker": "Character Name",
                "text": "This is what the character says."
            },
            {
                "speaker": "Billy Bones",
                "text": "This is the player character's response."
            },
            {
                "text": "A narrative description of what happens.",
                "narrative": true
            },
            {
                "speaker": "Character Name",
                "text": "More dialogue with optional sound effect.",
                "sound": "character_sound"
            }
        ]
    },
    "player_start": {
        "x": 0,
        "y": 0
    }
}

Dialogue Properties:

  • allow_skip: Set to true to let players skip the dialogue sequence
  • entries: Array of dialogue entries that play in sequence
  • speaker: Name of the character speaking (optional for narrative entries)
  • text: The dialogue text to be spoken
  • narrative: Set to true for descriptive text entries (no speaker)
  • sound: Optional sound file to play with this dialogue entry. If no sound is specified, the system will automatically play sounds/dialogue.ogg if it exists.

Note: Levels can include both "description" and "dialog". When both are present, the dialogue plays first, followed by the standard level description message format ("Level X, Name. Description.").

Adding Objects

All objects go in an "objects" list. Here are examples of what you can add:

Object Positioning

Objects can be positioned using either:

  • "x": 15 - Single position for objects like coffins, graves, catapults
  • "x_range": [10, 20] - Range for objects that span multiple positions like bone dust collections, enemy patrol areas, or hazards

The y coordinate determines the vertical layer:

  • y: 0 - Ground level (enemies, graves, catapults)
  • y: 3 - Elevated level (bone dust, coffins)
  • y: 12 - High level (skull storms)

Collectibles

Bone Dust

{
    "x_range": [5, 8],
    "y": 3,
    "sound": "bone_dust",
    "collectible": true,
    "static": true
}

The static property means objects don't move - they stay in their fixed positions. Static objects like bone dust and graves remain stationary, while enemies without this property can move and patrol.

Interactive Objects

Coffins

{
    "x": 15,
    "y": 3,
    "sound": "coffin",
    "type": "coffin",
    "item": "extra_life"
}

Items available for coffins:

  • "extra_life" - Grants an extra life
  • "hand_of_glory" - Provides temporary invincibility
  • "jack_o_lantern" - Throwable projectile
  • "guts" - Increases max health or restores health if at max
  • "cauldron" - Special item
  • "witch_broom" - Weapon upgrade
  • "random" - Randomly selects from eligible items (default if not specified)

Coffin Behavior: When broken with any weapon, coffins drop their specified item 1-2 tiles away in a random direction. The item bounces and can be collected by walking over it.

Graves

{
    "x": 35,
    "y": 0,
    "type": "grave",
    "sound": "grave",
    "static": true,
    "zombie_spawn_chance": 20,
    "item": "shin_bone"
}

Grave items available:

  • "shin_bone" - Currency for extra lives (100 needed)
  • "guts" - Health upgrade/restoration
  • Any item available for coffins

Grave behaviors:

  • zombie_spawn_chance: 0-100, higher means more zombies spawn when walked over
  • Item collection: Duck while walking (not running) with the rusty shovel equipped to safely collect items
  • Grave filling: Duck with shovel over empty graves (no item) to fill them and remove the hazard

Enemies

{
    "x_range": [20, 35],
    "y": 0,
    "enemy_type": "goblin",
    "health": 4,
    "damage": 2,
    "attack_range": 1.5,
    "attack_pattern": {
        "type": "hunter",
        "turn_threshold": 5
    }
}

Attack patterns:

  • "patrol": Enemy moves back and forth within their x_range
  • "hunter": Enemy will leave their area to pursue the player once entered
    • turn_threshold: How quickly hunting enemies turn around to attack (lower = more aggressive)

Advanced Enemy Behaviors

Vulnerability System (Ghost-like Enemies)
{
    "x_range": [400, 415],
    "y": 0,
    "enemy_type": "ghost",
    "health": 60,
    "damage": 2,
    "attack_range": 1,
    "has_vulnerability": true,
    "is_vulnerable": false,
    "vulnerability_duration": 3000,
    "invulnerability_duration": 5000,
    "speed_multiplier": 0.8,
    "attack_cooldown": 1200,
    "attack_pattern": {
        "type": "hunter",
        "turn_threshold": 2
    }
}

Vulnerability system: Enemy alternates between vulnerable and invulnerable states. They can only be damaged when vulnerable (plays enemy_is_vulnerable.ogg sound).

Enemy Spawning System
{
    "x_range": [420, 470],
    "y": 0,
    "enemy_type": "revenant",
    "health": 80,
    "damage": 1,
    "attack_range": 1,
    "attack_pattern": {
        "type": "patrol"
    },
    "can_spawn": true,
    "spawn_type": "zombie",
    "spawn_cooldown": 2500,
    "spawn_chance": 75,
    "spawn_distance": 5
}

Spawning properties:

  • can_spawn: Enable spawning behavior
  • spawn_type: Type of enemy to spawn (e.g., "zombie", "black_cat")
  • spawn_cooldown: Milliseconds between spawn attempts
  • spawn_chance: 0-100 probability of spawning when cooldown expires
  • spawn_distance: How far from the spawner to place new enemies

Example combinations:

  • Witch spawning black cats: High health witch that periodically spawns fast black cats
  • Revenant spawning zombies: Tough enemy that creates zombie minions
  • Any enemy type can spawn any other enemy type

Hazards

Skull Storm

{
    "x_range": [40, 60],
    "y": 12,
    "type": "skull_storm",
    "damage": 4,
    "maximum_skulls": 2,
    "frequency": {
        "min": 1,
        "max": 4
    }
}

The maximum_skulls setting controls how many skulls can be falling simultaneously. frequency is the number of seconds that can elapse before the next skull falls.

Catapult

{
    "x": 55,
    "y": 0,
    "type": "catapult",
    "fire_interval": 4000,
    "range": 25
}

Properties:

  • fire_interval: Milliseconds between shots
  • range: How far the catapult can shoot

Spider Web

{
    "type": "spider_web",
    "x": 15,
    "y": 0
}

Slows player movement and attack speed temporarily when walked over. Automatically spawns a spider enemy at the web location.

Grasping Hands

{
    "x_range": [40, 60],
    "y": 0,
    "type": "grasping_hands",
    "delay": 1000,
    "crumble_speed": 0.065
}

The ground crumbles beneath the player as undead hands reach up. The delay is in milliseconds before crumbling starts, and crumble_speed controls how fast the crumbling catches up to the player.

Level System Flexibility

The level system supports complex interactions and behaviors:

Dynamic Item Drops

  • Monsters spawning items: Use enemy spawning system to create enemies that drop items when killed
  • Graves with random loot: Each grave can contain different items collected via specific mechanics

Creative Design Techniques

Coffins That Release Enemies

While not directly supported, you can create the illusion of coffins that release enemies when touched:

{
    "type": "spider_web",
    "x": 25,
    "y": 3
}

Setup in your level pack's sound directory:

  • Replace spiderweb.ogg with coffin.ogg (ambient coffin sound)
  • Replace hit_spiderweb.ogg with coffin_shatter.ogg (break sound)
  • Replace spider.ogg with mummy.ogg (spawned enemy sound)
  • Replace spider_dies.ogg with mummy_dies.ogg (death sound)

Result: Players hear a coffin, when they touch it they hear it shatter, get slowed down (representing the enemy emerging), and a "mummy" automatically spawns from the coffin. The enemy is internally still a spider but sounds and feels like a mummy emerging from a sarcophagus. The slow down for the player can be because the skeleton is tangled in the mummy wrappings, or perhapse he's tangled in the shattered wood. It also does not float, so players would quickly learn to be war, maybe this coffin is one to just be passed under (low hanging).

Complex Enemy Combinations

  • Spawner + Vulnerability: Create a tough ghost that spawns minions while alternating between vulnerable states
  • Multi-layered encounters: Combine patrol enemies with hunters and spawners for dynamic battles
  • Progressive difficulty: Later waves can spawn stronger enemy types

Environmental Storytelling

  • Themed hazard combinations: Skull storms over graveyards, catapults defending strategic chokepoints
  • Sound design integration: Custom footsteps and ambience per level

Creating New Enemy Types

Want to add a new enemy type? You'll need two sound files in the sounds/ directory:

  • enemy.ogg - The sound the enemy makes while alive
  • enemy_dies.ogg - The death sound

For example, to add a werewolf enemy:

  • Add werewolf.ogg and werewolf_dies.ogg to the sounds directory
  • Use "werewolf" as the enemy_type in your level file

Level Design Tips

Balance Guidelines

  • Add at least 33 bone dust per level (for extra life economy)
  • Space out hazards to give players a chance to react
  • Enemy health recommendations:
    • Regular enemies: 4-6 HP
    • Mini-bosses: 8-15 HP
    • Bosses: 40+ HP
  • Lock boss levels with "locked": true

Advanced Design Patterns

  • Layered challenges: Combine moving enemies with static hazards
  • Risk/reward decisions: Place valuable items near dangerous enemies
  • Tactical positioning: Use terrain and enemy placement to create strategic choices
  • Escalating difficulty: Gradually introduce new mechanics and combinations

Custom Level Pack Structure

You can keep your levels and sounds separate from the main game. Create a directory structure like this for a pack called "Samhain Showdown":

Directory Structure

levels/Samhain Showdown/
├── 1.json
├── 2.json
├── 3.json
└── end.ogg

sounds/Samhain Showdown/
├── custom_footstep.ogg
├── werewolf.ogg
└── werewolf_dies.ogg

sounds/Samhain Showdown/ambience
├── werewolf_hunting.ogg
├── howling_winds.ogg

Advanced Sound Override System

Beyond simple file replacement, Wicked Quest now supports granular sound overrides directly in your level JSON files. This allows thematic consistency where a catapult becomes a "snowball launcher" or grasping hands become an "avalanche" - same mechanics, different sounds and feel.

Weapon Sound Overrides

Override weapon sounds and names globally for an entire level:

{
    "level_id": 1,
    "name": "Winter Wonderland",
    "weapon_sound_overrides": {
        "rusty_shovel": {
            "name": "rusty snow shovel",
            "attack_sound": "player_snow_shovel_attack",
            "hit_sound": "player_snow_shovel_hit"
        },
        "nunchucks": {
            "name": "ice sickles",
            "attack_sound": "player_ice_sickles_attack",
            "hit_sound": "player_ice_sickles_hit"
        },
        "witch_broom": {
            "name": "snow broom",
            "attack_sound": "player_snow_broom_attack",
            "hit_sound": "player_snow_broom_hit"
        }
    }
}

Weapon Override Properties:

  • name: Display name for the weapon (e.g., "ice sickles" instead of "nunchucks")
  • attack_sound: Sound played when attacking
  • hit_sound: Sound played when hitting an enemy

Weapon Override Persistence: Weapon overrides set in level 1 automatically persist throughout the entire level pack! You only need to define weapon_sound_overrides in your first level (1.json) and all subsequent levels will inherit these themed weapon names.

// In levels/Wicked Christmas/1.json - Sets theme for entire pack
{
    "level_id": 1,
    "weapon_sound_overrides": {
        "rusty_shovel": { "name": "rusty snow shovel" },
        "witch_broom": { "name": "decorated witch's broom" },
        "nunchucks": { "name": "candy cane nunchucks" }
    }
}

// In levels/Wicked Christmas/2.json - No overrides needed!
{
    "level_id": 2,
    // Weapons automatically use themed names from level 1
}

How it works:

  • Level 1 overrides persist across all levels in the same play session
  • Switching to a different level pack resets weapons to their original names
  • Later levels can still override weapons if you want to change themes mid-pack
  • The system stores the original weapon names internally for proper lookups

Object Sound Overrides

Override sounds for individual objects in your level:

{
    "x": 25,
    "y": 0,
    "type": "catapult",
    "fire_interval": 3000,
    "range": 30,
    "sound_overrides": {
        "base": "snowball_launcher",
        "launch": "snowball_launcher_launch"
    }
}
{
    "x_range": [40, 60],
    "y": 0,
    "type": "grasping_hands",
    "delay": 1000,
    "sound_overrides": {
        "base": "avalanche"
    }
}
{
    "x": 35,
    "y": 0,
    "type": "grave",
    "item": "shin_bone",
    "sound_overrides": {
        "base": "snow_mound",
        "item": "candy_cane"
    }
}

Object Sound Override Properties:

  • base: Override the main ambient sound (e.g., "catapult" → "snowball_launcher")
  • launch: Override launch sound for catapults (e.g., "catapult_launch" → "snowball_launcher_launch")
  • item: Override pickup sound for grave items (e.g., "get_shin_bone.ogg" → "get_candy_cane.ogg")
  • warning_message: Override warning message for grasping hands (e.g., "The ground crumbles as snow begins to avalanche!")
  • death_message: Override death message for grasping hands (e.g., "You vanish under tons of snow!")

Themed Item Equivalents

The sound override system includes intelligent item mapping for crafting consistency. Certain themed items automatically behave like their original counterparts:

Christmas Theme:

  • "candy_cane" → Functions as "shin_bone" (increments shin bone count)
  • "reindeer_guts" → Functions as "guts" (enables nunchucks crafting)

Result: Collecting 2 candy canes + reindeer guts = nunchucks (can be renamed to "ice sickles")

This system allows complete thematic consistency where players collect "2 Candy Canes + Reindeer Guts = Ice Sickles" while preserving all original game mechanics. The mapping works automatically across any level pack - simply use themed item names and they'll function correctly.

Adding New Themed Equivalents: To add your own themed items, modify the themed_mappings in src/powerup.py:

themed_mappings = {
    "your_bone_item": "shin_bone",
    "your_guts_item": "guts",
}

Complete Thematic Example

Here's how to transform a Halloween level into a Christmas level using sound overrides:

{
    "level_id": 1,
    "name": "Winter Siege",
    "description": "Santa's workshop is under attack by snow witches!",
    "weapon_sound_overrides": {
        "rusty_shovel": {
            "name": "snow shovel",
            "attack_sound": "player_snow_shovel_attack",
            "hit_sound": "player_snow_shovel_hit"
        }
    },
    "objects": [
        {
            "x": 25,
            "y": 0,
            "type": "catapult",
            "sound_overrides": {
                "base": "snowball_launcher",
                "launch": "snowball_launcher_launch"
            }
        },
        {
            "x_range": [40, 60],
            "y": 12,
            "type": "skull_storm",
            "sound_overrides": {
                "base": "snowball_storm"
            }
        },
        {
            "x": 75,
            "y": 0,
            "type": "grave",
            "item": "shin_bone",
            "sound_overrides": {
                "base": "snow_pile",
                "item": "candy_cane"
            }
        },
        {
            "x_range": [90, 110],
            "y": 0,
            "type": "grasping_hands",
            "sound_overrides": {
                "base": "avalanche",
                "warning_message": "The ground crumbles as snow begins to avalanche!",
                "death_message": "You vanish under tons of snow!"
            }
        }
    ]
}

Result:

  • Weapons sound winter-themed when attacking
  • "Catapult" becomes "Snowball Launcher" with appropriate launch sounds
  • "Skull Storm" becomes "Snowball Storm"
  • "Graves" become "Snow Piles" containing "Candy Canes" instead of "Shin Bones"
  • "Grasping Hands" becomes "Avalanche" with snow-themed death messages
  • All mechanics remain identical - only audio and messaging changes

Legacy Sound Override System

  • Custom ambience: Place in sounds/[Pack Name]/ambience/
  • Custom enemy sounds: Place in sounds/[Pack Name]/
  • Custom footsteps: Reference in level JSON as "footstep_sound"
  • Ending scene: Add end.ogg in the level pack directory

This legacy system allows complete audio customization through file replacement. For example, skull storms could become firestorms just by replacing the skull storm sounds in your pack's sound directory.

Complete Example Level

Here's a comprehensive example showing multiple advanced features:

{
    "level_id": 5,
    "name": "The Witch's Domain",
    "dialog": {
        "allow_skip": true,
        "entries": [
            {
                "speaker": "Ancient Spirit",
                "text": "Mortal skeleton, you dare enter the witch's sacred domain?",
                "sound": "ancient_spirit"
            },
            {
                "speaker": "Billy Bones",
                "text": "I'm looking for trouble, and I think I found it."
            },
            {
                "text": "The air grows thick with dark magic as ancient runes begin to glow.",
                "narrative": true,
                "sound": "magic_ambience"
            },
            {
                "speaker": "Ancient Spirit",
                "text": "Then face the consequences of your boldness. The witch's minions will not show mercy.",
                "sound": "evil_laughter"
            },
            {
                "speaker": "Billy Bones",
                "text": "Good thing I don't need any."
            }
        ]
    },
    "locked": true,
    "player_start": {"x": 0, "y": 0},
    "boundaries": {"left": 0, "right": 300},
    "ambience": "witch_theme.ogg",
    "footstep_sound": "footstep_mud",
    "objects": [
        {
            "x_range": [5, 15],
            "y": 3,
            "sound": "bone_dust",
            "collectible": true,
            "static": true
        },
        {
            "x": 25,
            "y": 3,
            "type": "coffin",
            "item": "hand_of_glory"
        },
        {
            "x": 40,
            "y": 0,
            "type": "grave",
            "zombie_spawn_chance": 50,
            "item": "guts"
        },
        {
            "x_range": [60, 80],
            "y": 0,
            "type": "grasping_hands",
            "delay": 500,
            "crumble_speed": 0.08
        },
        {
            "x": 100,
            "y": 0,
            "type": "catapult",
            "fire_interval": 3000,
            "range": 30
        },
        {
            "x_range": [120, 140],
            "y": 12,
            "type": "skull_storm",
            "damage": 3,
            "maximum_skulls": 3,
            "frequency": {"min": 2, "max": 5}
        },
        {
            "x_range": [200, 250],
            "y": 0,
            "enemy_type": "ghost",
            "health": 40,
            "damage": 2,
            "attack_range": 1.5,
            "has_vulnerability": true,
            "vulnerability_duration": 2000,
            "invulnerability_duration": 4000,
            "can_spawn": true,
            "spawn_type": "zombie",
            "spawn_cooldown": 3000,
            "spawn_chance": 60,
            "spawn_distance": 4,
            "attack_pattern": {"type": "hunter", "turn_threshold": 3}
        },
        {
            "x_range": [260, 290],
            "y": 0,
            "enemy_type": "witch",
            "health": 60,
            "damage": 3,
            "attack_range": 2,
            "can_spawn": true,
            "spawn_type": "black_cat",
            "spawn_cooldown": 4000,
            "spawn_chance": 80,
            "spawn_distance": 6,
            "attack_pattern": {"type": "patrol"}
        }
    ]
}

This example demonstrates:

  • Environmental hazards (grasping hands, skull storm, catapult)
  • Interactive elements (coffin with invincibility, grave with health)
  • Advanced enemies (vulnerable zombie-spawning ghost, cat-spawning witch)
  • Custom audio (themed ambience and footsteps)
  • Strategic design (safe zones, risk/reward placement)

Check out the existing Wicked Quest levels for more examples and inspiration!