Celeste (Open World): Implement New Game (#4937)
* APWorld Skeleton * Hair Color Rando and first items * All interactable items * Checkpoint Items and Locations * First pass sample intermediate data * Bulk of Region/location code * JSON Data Parser * New items and Level Item mapping * Data Parsing fixes and most of 1a data * 1a complete data and region/location/item creation fixes * Add Key Location type and ID output * Add options to slot data * 1B Level Data * Added Location logging * Add Goal Area Options * 1c Level Data * Old Site A B C level data * Key/Binosanity and Hair Length options * Key Item/Location and Clutter Event handling * Remove generic 'keys' item * 3a level data * 3b and 3c level data * Chapter 4 level data * Chapter 5 Logic Data * Chapter 5 level data * Trap Support * Add TrapLink Support * Chapter 6 A/B/C Level Data * Add active_levels to slot_data * Item and Location Name Groups + style cleanups * Chapter 7 Level Data and Items, Gemsanity option * Goal Area and victory handling * Fix slot_data * Add Core Level Data * Carsanity * Farewell Level Data and ID Range Update * Farewell level data and handling * Music Shuffle * Require Cassettes * Change default trap expiration action to Deaths * Handle Poetry * Mod versioning * Rename folder, general cleanup * Additional Cleanup * Handle Farewell Golden Goal when Include Goldens is off * Better handling of Farewell Golden * Update Docs * Beta test bug fixes * Bump to v1.0.0 * Update Changelog * Several Logic tweaks * Update APWorld Version * Add Celeste (Open World) to README * Peer review changes * Logic Fixes: * Adjust Mirror Temple B Key logic * Increment APWorld version * Fix several logic bugs * Add missing link * Add Item Name Groups for common alternative item names * Account for Madeline's post-Celeste hair-dying activities * Account for ignored member variable and hardcoded color in Celeste codebase * Add Blue Clouds to the logic of reaching Farewell - intro-02-launch * Type checking workaround * Bump version number * Adjust Setup Guide * Minor typing fixes * Logic and PR fixes * Increment APWorld Version * Use more world helpers * Core review * CODEOWNERS
This commit is contained in:
47
worlds/celeste_open_world/CHANGELOG.md
Normal file
47
worlds/celeste_open_world/CHANGELOG.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Celeste - Changelog
|
||||
|
||||
|
||||
## v1.0 - First Stable Release
|
||||
|
||||
### Features:
|
||||
|
||||
- Goal is to collect a certain number of Strawberries, finish your chosen Goal Area, and reach the credits in the Epilogue
|
||||
- Locations included:
|
||||
- Level Clears
|
||||
- Strawberries
|
||||
- Crystal Hearts
|
||||
- Cassettes
|
||||
- Golden Strawberries
|
||||
- Keys
|
||||
- Checkpoints
|
||||
- Summit Gems
|
||||
- Cars
|
||||
- Binoculars
|
||||
- Rooms
|
||||
- Items included:
|
||||
- 34 different interactable objects
|
||||
- Keys
|
||||
- Checkpoints
|
||||
- Summit Gems
|
||||
- Crystal Hearts
|
||||
- Cassettes
|
||||
- Traps
|
||||
- Bald Trap
|
||||
- Literature Trap
|
||||
- Stun Trap
|
||||
- Invisible Trap
|
||||
- Fast Trap
|
||||
- Slow Trap
|
||||
- Ice Trap
|
||||
- Reverse Trap
|
||||
- Screen Flip Trap
|
||||
- Laughter Trap
|
||||
- Hiccup Trap
|
||||
- Zoom Trap
|
||||
- Aesthetic Options:
|
||||
- Music Shuffle
|
||||
- Require Cassette items to hear music
|
||||
- Hair Length/Color options
|
||||
- Death Link
|
||||
- Amnesty option to select how many deaths must occur to send a DeathLink
|
||||
- Trap Link
|
||||
264
worlds/celeste_open_world/Items.py
Normal file
264
worlds/celeste_open_world/Items.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Names import ItemName
|
||||
|
||||
|
||||
level_item_lists: dict[str, set[str]] = {
|
||||
"0a": set(),
|
||||
|
||||
"1a": {ItemName.springs, ItemName.traffic_blocks, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"1b": {ItemName.springs, ItemName.traffic_blocks, ItemName.dash_refills, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"1c": {ItemName.traffic_blocks, ItemName.dash_refills, ItemName.coins},
|
||||
|
||||
"2a": {ItemName.springs, ItemName.dream_blocks, ItemName.traffic_blocks, ItemName.strawberry_seeds, ItemName.dash_refills, ItemName.coins},
|
||||
"2b": {ItemName.springs, ItemName.dream_blocks, ItemName.dash_refills, ItemName.coins, ItemName.blue_cassette_blocks},
|
||||
"2c": {ItemName.springs, ItemName.dream_blocks, ItemName.dash_refills, ItemName.coins},
|
||||
|
||||
"3a": {ItemName.springs, ItemName.moving_platforms, ItemName.sinking_platforms, ItemName.dash_refills, ItemName.coins, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"3b": {ItemName.springs, ItemName.dash_refills, ItemName.sinking_platforms, ItemName.coins, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"3c": {ItemName.dash_refills, ItemName.sinking_platforms, ItemName.coins},
|
||||
|
||||
"4a": {ItemName.blue_clouds, ItemName.blue_boosters, ItemName.moving_platforms, ItemName.coins, ItemName.strawberry_seeds, ItemName.springs, ItemName.move_blocks, ItemName.pink_clouds, ItemName.white_block, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"4b": {ItemName.blue_boosters, ItemName.moving_platforms, ItemName.move_blocks, ItemName.springs, ItemName.coins, ItemName.blue_clouds, ItemName.pink_clouds, ItemName.dash_refills, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"4c": {ItemName.blue_boosters, ItemName.move_blocks, ItemName.dash_refills, ItemName.pink_clouds},
|
||||
|
||||
"5a": {ItemName.swap_blocks, ItemName.red_boosters, ItemName.dash_switches, ItemName.dash_refills, ItemName.coins, ItemName.springs, ItemName.torches, ItemName.seekers, ItemName.theo_crystal, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"5b": {ItemName.swap_blocks, ItemName.red_boosters, ItemName.dash_switches, ItemName.dash_refills, ItemName.coins, ItemName.springs, ItemName.torches, ItemName.seekers, ItemName.theo_crystal, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"5c": {ItemName.swap_blocks, ItemName.red_boosters, ItemName.dash_switches, ItemName.dash_refills},
|
||||
|
||||
"6a": {ItemName.feathers, ItemName.kevin_blocks, ItemName.dash_refills, ItemName.bumpers, ItemName.springs, ItemName.coins, ItemName.badeline_boosters, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"6b": {ItemName.feathers, ItemName.kevin_blocks, ItemName.dash_refills, ItemName.bumpers, ItemName.coins, ItemName.springs, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"6c": {ItemName.feathers, ItemName.kevin_blocks, ItemName.dash_refills, ItemName.bumpers},
|
||||
|
||||
"7a": {ItemName.springs, ItemName.dash_refills, ItemName.badeline_boosters, ItemName.traffic_blocks, ItemName.coins, ItemName.dream_blocks, ItemName.sinking_platforms, ItemName.blue_boosters, ItemName.blue_clouds, ItemName.pink_clouds, ItemName.move_blocks, ItemName.moving_platforms, ItemName.swap_blocks, ItemName.red_boosters, ItemName.dash_switches, ItemName.feathers, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"7b": {ItemName.springs, ItemName.dash_refills, ItemName.badeline_boosters, ItemName.traffic_blocks, ItemName.coins, ItemName.dream_blocks, ItemName.moving_platforms, ItemName.blue_boosters, ItemName.blue_clouds, ItemName.pink_clouds, ItemName.move_blocks, ItemName.swap_blocks, ItemName.red_boosters, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"7c": {ItemName.springs, ItemName.dash_refills, ItemName.badeline_boosters, ItemName.coins, ItemName.pink_clouds},
|
||||
|
||||
# Epilogue
|
||||
"8a": set(),
|
||||
|
||||
# Core
|
||||
"9a": {ItemName.springs, ItemName.dash_refills, ItemName.fire_ice_balls, ItemName.bumpers, ItemName.core_toggles, ItemName.core_blocks, ItemName.coins, ItemName.badeline_boosters, ItemName.feathers, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"9b": {ItemName.springs, ItemName.dash_refills, ItemName.fire_ice_balls, ItemName.bumpers, ItemName.core_toggles, ItemName.core_blocks, ItemName.coins, ItemName.badeline_boosters, ItemName.dream_blocks, ItemName.moving_platforms, ItemName.blue_clouds, ItemName.swap_blocks, ItemName.kevin_blocks, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks},
|
||||
"9c": {ItemName.dash_refills, ItemName.bumpers, ItemName.core_toggles, ItemName.core_blocks, ItemName.traffic_blocks, ItemName.dream_blocks, ItemName.pink_clouds, ItemName.swap_blocks, ItemName.kevin_blocks},
|
||||
|
||||
# Farewell Pre/Post Empty Space
|
||||
"10a": {ItemName.blue_clouds, ItemName.badeline_boosters, ItemName.dash_refills, ItemName.double_dash_refills, ItemName.swap_blocks, ItemName.springs, ItemName.pufferfish, ItemName.coins, ItemName.dream_blocks, ItemName.jellyfish, ItemName.red_boosters, ItemName.dash_switches, ItemName.move_blocks, ItemName.breaker_boxes, ItemName.traffic_blocks},
|
||||
"10b": {ItemName.dream_blocks, ItemName.badeline_boosters, ItemName.bird, ItemName.dash_refills, ItemName.double_dash_refills, ItemName.kevin_blocks, ItemName.coins, ItemName.traffic_blocks, ItemName.move_blocks, ItemName.blue_boosters, ItemName.springs, ItemName.feathers, ItemName.swap_blocks, ItemName.red_boosters, ItemName.core_blocks, ItemName.fire_ice_balls, ItemName.kevin_blocks, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks, ItemName.yellow_cassette_blocks, ItemName.green_cassette_blocks, ItemName.breaker_boxes, ItemName.pufferfish, ItemName.jellyfish},
|
||||
"10c": {ItemName.badeline_boosters, ItemName.double_dash_refills, ItemName.springs, ItemName.pufferfish, ItemName.jellyfish},
|
||||
}
|
||||
|
||||
level_cassette_items: dict[str, str] = {
|
||||
"0a": ItemName.prologue_cassette,
|
||||
"1a": ItemName.fc_a_cassette,
|
||||
"1b": ItemName.fc_b_cassette,
|
||||
"1c": ItemName.fc_c_cassette,
|
||||
"2a": ItemName.os_a_cassette,
|
||||
"2b": ItemName.os_b_cassette,
|
||||
"2c": ItemName.os_c_cassette,
|
||||
"3a": ItemName.cr_a_cassette,
|
||||
"3b": ItemName.cr_b_cassette,
|
||||
"3c": ItemName.cr_c_cassette,
|
||||
"4a": ItemName.gr_a_cassette,
|
||||
"4b": ItemName.gr_b_cassette,
|
||||
"4c": ItemName.gr_c_cassette,
|
||||
"5a": ItemName.mt_a_cassette,
|
||||
"5b": ItemName.mt_b_cassette,
|
||||
"5c": ItemName.mt_c_cassette,
|
||||
"6a": ItemName.ref_a_cassette,
|
||||
"6b": ItemName.ref_b_cassette,
|
||||
"6c": ItemName.ref_c_cassette,
|
||||
"7a": ItemName.sum_a_cassette,
|
||||
"7b": ItemName.sum_b_cassette,
|
||||
"7c": ItemName.sum_c_cassette,
|
||||
"8a": ItemName.epilogue_cassette,
|
||||
"9a": ItemName.core_a_cassette,
|
||||
"9b": ItemName.core_b_cassette,
|
||||
"9c": ItemName.core_c_cassette,
|
||||
"10a":ItemName.farewell_cassette,
|
||||
}
|
||||
|
||||
|
||||
celeste_base_id: int = 0xCA10000
|
||||
|
||||
|
||||
class CelesteItem(Item):
|
||||
game = "Celeste"
|
||||
|
||||
|
||||
class CelesteItemData(NamedTuple):
|
||||
code: Optional[int] = None
|
||||
type: ItemClassification = ItemClassification.filler
|
||||
|
||||
|
||||
collectable_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.strawberry: CelesteItemData(celeste_base_id + 0x0, ItemClassification.progression_skip_balancing),
|
||||
ItemName.raspberry: CelesteItemData(celeste_base_id + 0x1, ItemClassification.filler),
|
||||
}
|
||||
|
||||
goal_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.house_keys: CelesteItemData(celeste_base_id + 0x10, ItemClassification.progression_skip_balancing),
|
||||
}
|
||||
|
||||
trap_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.bald_trap: CelesteItemData(celeste_base_id + 0x20, ItemClassification.trap),
|
||||
ItemName.literature_trap: CelesteItemData(celeste_base_id + 0x21, ItemClassification.trap),
|
||||
ItemName.stun_trap: CelesteItemData(celeste_base_id + 0x22, ItemClassification.trap),
|
||||
ItemName.invisible_trap: CelesteItemData(celeste_base_id + 0x23, ItemClassification.trap),
|
||||
ItemName.fast_trap: CelesteItemData(celeste_base_id + 0x24, ItemClassification.trap),
|
||||
ItemName.slow_trap: CelesteItemData(celeste_base_id + 0x25, ItemClassification.trap),
|
||||
ItemName.ice_trap: CelesteItemData(celeste_base_id + 0x26, ItemClassification.trap),
|
||||
ItemName.reverse_trap: CelesteItemData(celeste_base_id + 0x28, ItemClassification.trap),
|
||||
ItemName.screen_flip_trap: CelesteItemData(celeste_base_id + 0x29, ItemClassification.trap),
|
||||
ItemName.laughter_trap: CelesteItemData(celeste_base_id + 0x2A, ItemClassification.trap),
|
||||
ItemName.hiccup_trap: CelesteItemData(celeste_base_id + 0x2B, ItemClassification.trap),
|
||||
ItemName.zoom_trap: CelesteItemData(celeste_base_id + 0x2C, ItemClassification.trap),
|
||||
}
|
||||
|
||||
checkpoint_item_data_table: dict[str, CelesteItemData] = {}
|
||||
|
||||
key_item_data_table: dict[str, CelesteItemData] = {}
|
||||
gem_item_data_table: dict[str, CelesteItemData] = {}
|
||||
|
||||
interactable_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.springs: CelesteItemData(celeste_base_id + 0x2000 + 0x00, ItemClassification.progression),
|
||||
ItemName.traffic_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x01, ItemClassification.progression),
|
||||
ItemName.pink_cassette_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x02, ItemClassification.progression),
|
||||
ItemName.blue_cassette_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x03, ItemClassification.progression),
|
||||
|
||||
ItemName.dream_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x04, ItemClassification.progression),
|
||||
ItemName.coins: CelesteItemData(celeste_base_id + 0x2000 + 0x05, ItemClassification.progression),
|
||||
ItemName.strawberry_seeds: CelesteItemData(celeste_base_id + 0x2000 + 0x1F, ItemClassification.progression),
|
||||
|
||||
ItemName.sinking_platforms: CelesteItemData(celeste_base_id + 0x2000 + 0x20, ItemClassification.progression),
|
||||
|
||||
ItemName.moving_platforms: CelesteItemData(celeste_base_id + 0x2000 + 0x06, ItemClassification.progression),
|
||||
ItemName.blue_boosters: CelesteItemData(celeste_base_id + 0x2000 + 0x07, ItemClassification.progression),
|
||||
ItemName.blue_clouds: CelesteItemData(celeste_base_id + 0x2000 + 0x08, ItemClassification.progression),
|
||||
ItemName.move_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x09, ItemClassification.progression),
|
||||
ItemName.white_block: CelesteItemData(celeste_base_id + 0x2000 + 0x21, ItemClassification.progression),
|
||||
|
||||
ItemName.swap_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x0A, ItemClassification.progression),
|
||||
ItemName.red_boosters: CelesteItemData(celeste_base_id + 0x2000 + 0x0B, ItemClassification.progression),
|
||||
ItemName.torches: CelesteItemData(celeste_base_id + 0x2000 + 0x22, ItemClassification.useful),
|
||||
ItemName.theo_crystal: CelesteItemData(celeste_base_id + 0x2000 + 0x0C, ItemClassification.progression),
|
||||
|
||||
ItemName.feathers: CelesteItemData(celeste_base_id + 0x2000 + 0x0D, ItemClassification.progression),
|
||||
ItemName.bumpers: CelesteItemData(celeste_base_id + 0x2000 + 0x0E, ItemClassification.progression),
|
||||
ItemName.kevin_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x0F, ItemClassification.progression),
|
||||
|
||||
ItemName.pink_clouds: CelesteItemData(celeste_base_id + 0x2000 + 0x10, ItemClassification.progression),
|
||||
ItemName.badeline_boosters: CelesteItemData(celeste_base_id + 0x2000 + 0x11, ItemClassification.progression),
|
||||
|
||||
ItemName.fire_ice_balls: CelesteItemData(celeste_base_id + 0x2000 + 0x12, ItemClassification.progression),
|
||||
ItemName.core_toggles: CelesteItemData(celeste_base_id + 0x2000 + 0x13, ItemClassification.progression),
|
||||
ItemName.core_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x14, ItemClassification.progression),
|
||||
|
||||
ItemName.pufferfish: CelesteItemData(celeste_base_id + 0x2000 + 0x15, ItemClassification.progression),
|
||||
ItemName.jellyfish: CelesteItemData(celeste_base_id + 0x2000 + 0x16, ItemClassification.progression),
|
||||
ItemName.breaker_boxes: CelesteItemData(celeste_base_id + 0x2000 + 0x17, ItemClassification.progression),
|
||||
ItemName.dash_refills: CelesteItemData(celeste_base_id + 0x2000 + 0x18, ItemClassification.progression),
|
||||
ItemName.double_dash_refills: CelesteItemData(celeste_base_id + 0x2000 + 0x19, ItemClassification.progression),
|
||||
ItemName.yellow_cassette_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x1A, ItemClassification.progression),
|
||||
ItemName.green_cassette_blocks: CelesteItemData(celeste_base_id + 0x2000 + 0x1B, ItemClassification.progression),
|
||||
ItemName.bird: CelesteItemData(celeste_base_id + 0x2000 + 0x23, ItemClassification.progression),
|
||||
|
||||
ItemName.dash_switches: CelesteItemData(celeste_base_id + 0x2000 + 0x1C, ItemClassification.progression),
|
||||
ItemName.seekers: CelesteItemData(celeste_base_id + 0x2000 + 0x1D, ItemClassification.progression),
|
||||
}
|
||||
|
||||
cassette_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.prologue_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x00, ItemClassification.filler),
|
||||
ItemName.fc_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x01, ItemClassification.filler),
|
||||
ItemName.fc_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x02, ItemClassification.filler),
|
||||
ItemName.fc_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x03, ItemClassification.filler),
|
||||
ItemName.os_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x04, ItemClassification.filler),
|
||||
ItemName.os_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x05, ItemClassification.filler),
|
||||
ItemName.os_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x06, ItemClassification.filler),
|
||||
ItemName.cr_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x07, ItemClassification.filler),
|
||||
ItemName.cr_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x08, ItemClassification.filler),
|
||||
ItemName.cr_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x09, ItemClassification.filler),
|
||||
ItemName.gr_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0A, ItemClassification.filler),
|
||||
ItemName.gr_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0B, ItemClassification.filler),
|
||||
ItemName.gr_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0C, ItemClassification.filler),
|
||||
ItemName.mt_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0D, ItemClassification.filler),
|
||||
ItemName.mt_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0E, ItemClassification.filler),
|
||||
ItemName.mt_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x0F, ItemClassification.filler),
|
||||
ItemName.ref_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x10, ItemClassification.filler),
|
||||
ItemName.ref_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x11, ItemClassification.filler),
|
||||
ItemName.ref_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x12, ItemClassification.filler),
|
||||
ItemName.sum_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x13, ItemClassification.filler),
|
||||
ItemName.sum_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x14, ItemClassification.filler),
|
||||
ItemName.sum_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x15, ItemClassification.filler),
|
||||
ItemName.epilogue_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x16, ItemClassification.filler),
|
||||
ItemName.core_a_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x17, ItemClassification.filler),
|
||||
ItemName.core_b_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x18, ItemClassification.filler),
|
||||
ItemName.core_c_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x19, ItemClassification.filler),
|
||||
ItemName.farewell_cassette: CelesteItemData(celeste_base_id + 0x1000 + 0x1A, ItemClassification.filler),
|
||||
}
|
||||
|
||||
crystal_heart_item_data_table: dict[str, CelesteItemData] = {
|
||||
ItemName.crystal_heart_1: CelesteItemData(celeste_base_id + 0x3000 + 0x00, ItemClassification.filler),
|
||||
ItemName.crystal_heart_2: CelesteItemData(celeste_base_id + 0x3000 + 0x01, ItemClassification.filler),
|
||||
ItemName.crystal_heart_3: CelesteItemData(celeste_base_id + 0x3000 + 0x02, ItemClassification.filler),
|
||||
ItemName.crystal_heart_4: CelesteItemData(celeste_base_id + 0x3000 + 0x03, ItemClassification.filler),
|
||||
ItemName.crystal_heart_5: CelesteItemData(celeste_base_id + 0x3000 + 0x04, ItemClassification.filler),
|
||||
ItemName.crystal_heart_6: CelesteItemData(celeste_base_id + 0x3000 + 0x05, ItemClassification.filler),
|
||||
ItemName.crystal_heart_7: CelesteItemData(celeste_base_id + 0x3000 + 0x06, ItemClassification.filler),
|
||||
ItemName.crystal_heart_8: CelesteItemData(celeste_base_id + 0x3000 + 0x07, ItemClassification.filler),
|
||||
ItemName.crystal_heart_9: CelesteItemData(celeste_base_id + 0x3000 + 0x08, ItemClassification.filler),
|
||||
ItemName.crystal_heart_10: CelesteItemData(celeste_base_id + 0x3000 + 0x09, ItemClassification.filler),
|
||||
ItemName.crystal_heart_11: CelesteItemData(celeste_base_id + 0x3000 + 0x0A, ItemClassification.filler),
|
||||
ItemName.crystal_heart_12: CelesteItemData(celeste_base_id + 0x3000 + 0x0B, ItemClassification.filler),
|
||||
ItemName.crystal_heart_13: CelesteItemData(celeste_base_id + 0x3000 + 0x0C, ItemClassification.filler),
|
||||
ItemName.crystal_heart_14: CelesteItemData(celeste_base_id + 0x3000 + 0x0D, ItemClassification.filler),
|
||||
ItemName.crystal_heart_15: CelesteItemData(celeste_base_id + 0x3000 + 0x0E, ItemClassification.filler),
|
||||
ItemName.crystal_heart_16: CelesteItemData(celeste_base_id + 0x3000 + 0x0F, ItemClassification.filler),
|
||||
}
|
||||
|
||||
def add_checkpoint_to_table(id: int, name: str):
|
||||
checkpoint_item_data_table[name] = CelesteItemData(id, ItemClassification.progression)
|
||||
|
||||
def add_key_to_table(id: int, name: str):
|
||||
key_item_data_table[name] = CelesteItemData(id, ItemClassification.progression)
|
||||
|
||||
def add_gem_to_table(id: int, name: str):
|
||||
gem_item_data_table[name] = CelesteItemData(id, ItemClassification.progression)
|
||||
|
||||
def generate_item_data_table() -> dict[str, CelesteItemData]:
|
||||
return {**collectable_item_data_table,
|
||||
**goal_item_data_table,
|
||||
**trap_item_data_table,
|
||||
**checkpoint_item_data_table,
|
||||
**key_item_data_table,
|
||||
**gem_item_data_table,
|
||||
**cassette_item_data_table,
|
||||
**crystal_heart_item_data_table,
|
||||
**interactable_item_data_table}
|
||||
|
||||
|
||||
def generate_item_table() -> dict[str, int]:
|
||||
return {name: data.code for name, data in generate_item_data_table().items() if data.code is not None}
|
||||
|
||||
|
||||
def generate_item_groups() -> dict[str, list[str]]:
|
||||
item_groups: dict[str, list[str]] = {
|
||||
"Collectables": list(collectable_item_data_table.keys()),
|
||||
"Traps": list(trap_item_data_table.keys()),
|
||||
"Checkpoints": list(checkpoint_item_data_table.keys()),
|
||||
"Keys": list(key_item_data_table.keys()),
|
||||
"Gems": list(gem_item_data_table.keys()),
|
||||
"Cassettes": list(cassette_item_data_table.keys()),
|
||||
"Crystal Hearts": list(crystal_heart_item_data_table.keys()),
|
||||
"Interactables": list(interactable_item_data_table.keys()),
|
||||
|
||||
# Commonly mistaken names
|
||||
"Green Boosters": [ItemName.blue_boosters],
|
||||
"Green Bubbles": [ItemName.blue_boosters],
|
||||
"Blue Bubbles": [ItemName.blue_boosters],
|
||||
"Red Bubbles": [ItemName.red_boosters],
|
||||
"Touch Switches": [ItemName.coins],
|
||||
}
|
||||
|
||||
return item_groups
|
||||
208
worlds/celeste_open_world/Levels.py
Normal file
208
worlds/celeste_open_world/Levels.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
goal_area_option_to_name: dict[int, str] = {
|
||||
0: "7a",
|
||||
1: "7b",
|
||||
2: "7c",
|
||||
3: "9a",
|
||||
4: "9b",
|
||||
5: "9c",
|
||||
6: "10a",
|
||||
7: "10b",
|
||||
8: "10c",
|
||||
}
|
||||
|
||||
|
||||
goal_area_option_to_display_name: dict[int, str] = {
|
||||
0: "The Summit A",
|
||||
1: "The Summit B",
|
||||
2: "The Summit C",
|
||||
3: "Core A",
|
||||
4: "Core B",
|
||||
5: "Core C",
|
||||
6: "Farewell",
|
||||
7: "Farewell",
|
||||
8: "Farewell",
|
||||
}
|
||||
|
||||
goal_area_to_location_name: dict[str, str] = {
|
||||
"7a": "The Summit A - Level Clear",
|
||||
"7b": "The Summit B - Level Clear",
|
||||
"7c": "The Summit C - Level Clear",
|
||||
"9a": "Core A - Level Clear",
|
||||
"9b": "Core B - Level Clear",
|
||||
"9c": "Core C - Level Clear",
|
||||
"10a": "Farewell - Crystal Heart?",
|
||||
"10b": "Farewell - Level Clear",
|
||||
"10c": "Farewell - Golden Strawberry",
|
||||
}
|
||||
|
||||
|
||||
class LocationType(IntEnum):
|
||||
strawberry = 0
|
||||
golden_strawberry = 1
|
||||
cassette = 2
|
||||
crystal_heart = 3
|
||||
checkpoint = 4
|
||||
level_clear = 5
|
||||
key = 6
|
||||
binoculars = 7
|
||||
room_enter = 8
|
||||
clutter = 9
|
||||
gem = 10
|
||||
car = 11
|
||||
|
||||
class DoorDirection(IntEnum):
|
||||
up = 0
|
||||
right = 1
|
||||
down = 2
|
||||
left = 3
|
||||
special = 4
|
||||
|
||||
|
||||
class Door:
|
||||
name: str
|
||||
room_name: str
|
||||
room: Room
|
||||
dir: DoorDirection
|
||||
blocked: bool
|
||||
closes_behind: bool
|
||||
region: PreRegion
|
||||
|
||||
def __init__(self, name: str, room_name: str, dir: DoorDirection, blocked: bool, closes_behind: bool):
|
||||
self.name = name
|
||||
self.room_name = room_name
|
||||
self.dir = dir
|
||||
self.blocked = blocked
|
||||
self.closes_behind = closes_behind
|
||||
# Find PreRegion later using our name once we know it exists
|
||||
|
||||
|
||||
class PreRegion:
|
||||
name: str
|
||||
room_name: str
|
||||
room: Room
|
||||
connections: list[RegionConnection]
|
||||
locations: list[LevelLocation]
|
||||
|
||||
def __init__(self, name: str, room_name: str, connections: list[RegionConnection], locations: list[LevelLocation]):
|
||||
self.name = name
|
||||
self.room_name = room_name
|
||||
self.connections = connections.copy()
|
||||
self.locations = locations.copy()
|
||||
|
||||
for loc in self.locations:
|
||||
loc.region = self
|
||||
|
||||
|
||||
class RegionConnection:
|
||||
source_name: str
|
||||
source: PreRegion
|
||||
destination_name: str
|
||||
destination: PreRegion
|
||||
possible_access: list[list[str]]
|
||||
|
||||
def __init__(self, source_name: str, destination_name: str, possible_access: list[list[str]] = []):
|
||||
self.source_name = source_name
|
||||
self.destination_name = destination_name
|
||||
self.possible_access = possible_access.copy()
|
||||
|
||||
|
||||
class LevelLocation:
|
||||
name: str
|
||||
display_name: str
|
||||
region_name: str
|
||||
region: PreRegion
|
||||
loc_type: LocationType
|
||||
possible_access: list[list[str]]
|
||||
|
||||
def __init__(self, name: str, display_name: str, region_name: str, loc_type: LocationType, possible_access: list[list[str]] = []):
|
||||
self.name = name
|
||||
self.display_name = display_name
|
||||
self.region_name = region_name
|
||||
self.loc_type = loc_type
|
||||
self.possible_access = possible_access.copy()
|
||||
|
||||
class Room:
|
||||
level_name: str
|
||||
name: str
|
||||
display_name: str
|
||||
regions: list[PreRegion]
|
||||
doors: list[Door]
|
||||
checkpoint: str
|
||||
checkpoint_region: str
|
||||
|
||||
def __init__(self, level_name: str, name: str, display_name: str, regions: list[PreRegion], doors: list[Door], checkpoint: str = None, checkpoint_region: str = None):
|
||||
self.level_name = level_name
|
||||
self.name = name
|
||||
self.display_name = display_name
|
||||
self.regions = regions.copy()
|
||||
self.doors = doors.copy()
|
||||
self.checkpoint = checkpoint
|
||||
self.checkpoint_region = checkpoint_region
|
||||
|
||||
from .data.CelesteLevelData import all_regions
|
||||
|
||||
for reg in self.regions:
|
||||
reg.room = self
|
||||
|
||||
for reg_con in reg.connections:
|
||||
reg_con.source = reg
|
||||
reg_con.destination = all_regions[reg_con.destination_name]
|
||||
|
||||
for door in self.doors:
|
||||
door.room = self
|
||||
|
||||
|
||||
class RoomConnection:
|
||||
level_name: str
|
||||
source: Door
|
||||
dest: Door
|
||||
two_way: bool
|
||||
|
||||
def __init__(self, level_name: str, source: Door, dest: Door):
|
||||
self.level_name = level_name
|
||||
self.source = source
|
||||
self.dest = dest
|
||||
self.two_way = not self.dest.closes_behind
|
||||
|
||||
if (self.source.dir == DoorDirection.left and self.dest.dir != DoorDirection.right or
|
||||
self.source.dir == DoorDirection.right and self.dest.dir != DoorDirection.left or
|
||||
self.source.dir == DoorDirection.up and self.dest.dir != DoorDirection.down or
|
||||
self.source.dir == DoorDirection.down and self.dest.dir != DoorDirection.up):
|
||||
raise Exception(f"Door {source.name} ({self.source.dir}) and Door {dest.name} ({self.dest.dir}) have mismatched directions.")
|
||||
|
||||
|
||||
class Level:
|
||||
name: str
|
||||
display_name: str
|
||||
rooms: list[Room]
|
||||
room_connections: list[RoomConnection]
|
||||
|
||||
def __init__(self, name: str, display_name: str, rooms: list[Room], room_connections: list[RoomConnection]):
|
||||
self.name = name
|
||||
self.display_name = display_name
|
||||
self.rooms = rooms.copy()
|
||||
self.room_connections = room_connections.copy()
|
||||
|
||||
|
||||
def load_logic_data() -> dict[str, Level]:
|
||||
from .data.CelesteLevelData import all_levels
|
||||
|
||||
#for _, level in all_levels.items():
|
||||
# print(level.display_name)
|
||||
#
|
||||
# for room in level.rooms:
|
||||
# print(" " + room.display_name)
|
||||
#
|
||||
# for region in room.regions:
|
||||
# print(" " + region.name)
|
||||
#
|
||||
# for location in region.locations:
|
||||
# print(" " + location.display_name)
|
||||
|
||||
return all_levels
|
||||
281
worlds/celeste_open_world/Locations.py
Normal file
281
worlds/celeste_open_world/Locations.py
Normal file
@@ -0,0 +1,281 @@
|
||||
from typing import NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Location, Region
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
from .Levels import Level, LocationType
|
||||
from .Names import ItemName
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CelesteOpenWorld
|
||||
else:
|
||||
CelesteOpenWorld = object
|
||||
|
||||
|
||||
celeste_base_id: int = 0xCA10000
|
||||
|
||||
|
||||
class CelesteLocation(Location):
|
||||
game = "Celeste"
|
||||
|
||||
|
||||
class CelesteLocationData(NamedTuple):
|
||||
region: str
|
||||
address: Optional[int] = None
|
||||
|
||||
|
||||
checkpoint_location_data_table: dict[str, CelesteLocationData] = {}
|
||||
key_location_data_table: dict[str, CelesteLocationData] = {}
|
||||
|
||||
location_id_offsets: dict[LocationType, int | None] = {
|
||||
LocationType.strawberry: celeste_base_id,
|
||||
LocationType.golden_strawberry: celeste_base_id + 0x1000,
|
||||
LocationType.cassette: celeste_base_id + 0x2000,
|
||||
LocationType.car: celeste_base_id + 0x2A00,
|
||||
LocationType.crystal_heart: celeste_base_id + 0x3000,
|
||||
LocationType.checkpoint: celeste_base_id + 0x4000,
|
||||
LocationType.level_clear: celeste_base_id + 0x5000,
|
||||
LocationType.key: celeste_base_id + 0x6000,
|
||||
LocationType.gem: celeste_base_id + 0x6A00,
|
||||
LocationType.binoculars: celeste_base_id + 0x7000,
|
||||
LocationType.room_enter: celeste_base_id + 0x8000,
|
||||
LocationType.clutter: None,
|
||||
}
|
||||
|
||||
|
||||
def generate_location_table() -> dict[str, int]:
|
||||
from .Levels import Level, LocationType, load_logic_data
|
||||
level_data: dict[str, Level] = load_logic_data()
|
||||
location_table = {}
|
||||
|
||||
location_counts: dict[LocationType, int] = {
|
||||
LocationType.strawberry: 0,
|
||||
LocationType.golden_strawberry: 0,
|
||||
LocationType.cassette: 0,
|
||||
LocationType.car: 0,
|
||||
LocationType.crystal_heart: 0,
|
||||
LocationType.checkpoint: 0,
|
||||
LocationType.level_clear: 0,
|
||||
LocationType.key: 0,
|
||||
LocationType.gem: 0,
|
||||
LocationType.binoculars: 0,
|
||||
LocationType.room_enter: 0,
|
||||
}
|
||||
|
||||
for _, level in level_data.items():
|
||||
for room in level.rooms:
|
||||
if room.name != "10b_GOAL":
|
||||
location_table[room.display_name] = location_id_offsets[LocationType.room_enter] + location_counts[LocationType.room_enter]
|
||||
location_counts[LocationType.room_enter] += 1
|
||||
|
||||
if room.checkpoint is not None and room.checkpoint != "Start":
|
||||
checkpoint_id: int = location_id_offsets[LocationType.checkpoint] + location_counts[LocationType.checkpoint]
|
||||
checkpoint_name: str = level.display_name + " - " + room.checkpoint
|
||||
location_table[checkpoint_name] = checkpoint_id
|
||||
location_counts[LocationType.checkpoint] += 1
|
||||
checkpoint_location_data_table[checkpoint_name] = CelesteLocationData(level.display_name, checkpoint_id)
|
||||
|
||||
from .Items import add_checkpoint_to_table
|
||||
add_checkpoint_to_table(checkpoint_id, checkpoint_name)
|
||||
|
||||
for region in room.regions:
|
||||
for location in region.locations:
|
||||
if location_id_offsets[location.loc_type] is not None:
|
||||
location_id = location_id_offsets[location.loc_type] + location_counts[location.loc_type]
|
||||
location_table[location.display_name] = location_id
|
||||
location_counts[location.loc_type] += 1
|
||||
|
||||
if location.loc_type == LocationType.key:
|
||||
from .Items import add_key_to_table
|
||||
add_key_to_table(location_id, location.display_name)
|
||||
|
||||
if location.loc_type == LocationType.gem:
|
||||
from .Items import add_gem_to_table
|
||||
add_gem_to_table(location_id, location.display_name)
|
||||
|
||||
return location_table
|
||||
|
||||
|
||||
def create_regions_and_locations(world: CelesteOpenWorld):
|
||||
menu_region = Region("Menu", world.player, world.multiworld)
|
||||
world.multiworld.regions.append(menu_region)
|
||||
|
||||
world.active_checkpoint_names: list[str] = []
|
||||
world.goal_checkpoint_names: dict[str, str] = dict()
|
||||
world.active_key_names: list[str] = []
|
||||
world.active_gem_names: list[str] = []
|
||||
world.active_clutter_names: list[str] = []
|
||||
|
||||
for _, level in world.level_data.items():
|
||||
if level.name not in world.active_levels:
|
||||
continue
|
||||
|
||||
for room in level.rooms:
|
||||
room_region = Region(room.name + "_room", world.player, world.multiworld)
|
||||
world.multiworld.regions.append(room_region)
|
||||
|
||||
for pre_region in room.regions:
|
||||
region = Region(pre_region.name, world.player, world.multiworld)
|
||||
world.multiworld.regions.append(region)
|
||||
|
||||
for level_location in pre_region.locations:
|
||||
if level_location.loc_type == LocationType.golden_strawberry:
|
||||
if level_location.display_name == "Farewell - Golden Strawberry":
|
||||
if not world.options.goal_area == "farewell_golden":
|
||||
continue
|
||||
elif not world.options.include_goldens:
|
||||
continue
|
||||
|
||||
if level_location.loc_type == LocationType.car and not world.options.carsanity:
|
||||
continue
|
||||
|
||||
if level_location.loc_type == LocationType.binoculars and not world.options.binosanity:
|
||||
continue
|
||||
|
||||
if level_location.loc_type == LocationType.key:
|
||||
world.active_key_names.append(level_location.display_name)
|
||||
|
||||
if level_location.loc_type == LocationType.gem:
|
||||
world.active_gem_names.append(level_location.display_name)
|
||||
|
||||
location_rule = None
|
||||
if len(level_location.possible_access) == 1:
|
||||
only_access = level_location.possible_access[0]
|
||||
if len(only_access) == 1:
|
||||
only_item = level_location.possible_access[0][0]
|
||||
def location_rule_func(state, only_item=only_item):
|
||||
return state.has(only_item, world.player)
|
||||
location_rule = location_rule_func
|
||||
else:
|
||||
def location_rule_func(state, only_access=only_access):
|
||||
return state.has_all(only_access, world.player)
|
||||
location_rule = location_rule_func
|
||||
elif len(level_location.possible_access) > 0:
|
||||
def location_rule_func(state, level_location=level_location):
|
||||
for sublist in level_location.possible_access:
|
||||
if state.has_all(sublist, world.player):
|
||||
return True
|
||||
return False
|
||||
location_rule = location_rule_func
|
||||
|
||||
if level_location.loc_type == LocationType.clutter:
|
||||
world.active_clutter_names.append(level_location.display_name)
|
||||
location = CelesteLocation(world.player, level_location.display_name, None, region)
|
||||
if location_rule is not None:
|
||||
set_rule(location, location_rule)
|
||||
region.locations.append(location)
|
||||
continue
|
||||
|
||||
location = CelesteLocation(world.player, level_location.display_name, world.location_name_to_id[level_location.display_name], region)
|
||||
if location_rule is not None:
|
||||
set_rule(location, location_rule)
|
||||
region.locations.append(location)
|
||||
|
||||
for pre_region in room.regions:
|
||||
region = world.get_region(pre_region.name)
|
||||
for connection in pre_region.connections:
|
||||
connection_rule = None
|
||||
if len(connection.possible_access) == 1:
|
||||
only_access = connection.possible_access[0]
|
||||
if len(only_access) == 1:
|
||||
only_item = connection.possible_access[0][0]
|
||||
def connection_rule_func(state, only_item=only_item):
|
||||
return state.has(only_item, world.player)
|
||||
connection_rule = connection_rule_func
|
||||
else:
|
||||
def connection_rule_func(state, only_access=only_access):
|
||||
return state.has_all(only_access, world.player)
|
||||
connection_rule = connection_rule_func
|
||||
elif len(connection.possible_access) > 0:
|
||||
def connection_rule_func(state, connection=connection):
|
||||
for sublist in connection.possible_access:
|
||||
if state.has_all(sublist, world.player):
|
||||
return True
|
||||
return False
|
||||
|
||||
connection_rule = connection_rule_func
|
||||
|
||||
if connection_rule is None:
|
||||
region.add_exits([connection.destination_name])
|
||||
else:
|
||||
region.add_exits([connection.destination_name], {connection.destination_name: connection_rule})
|
||||
region.add_exits([room_region.name])
|
||||
|
||||
if room.checkpoint != None:
|
||||
if room.checkpoint == "Start":
|
||||
if world.options.lock_goal_area and (level.name == world.goal_area or (level.name[:2] == world.goal_area[:2] == "10")):
|
||||
world.goal_start_region: str = room.checkpoint_region
|
||||
elif level.name == "8a":
|
||||
world.epilogue_start_region: str = room.checkpoint_region
|
||||
else:
|
||||
menu_region.add_exits([room.checkpoint_region])
|
||||
else:
|
||||
checkpoint_location_name = level.display_name + " - " + room.checkpoint
|
||||
world.active_checkpoint_names.append(checkpoint_location_name)
|
||||
checkpoint_rule = lambda state, checkpoint_location_name=checkpoint_location_name: state.has(checkpoint_location_name, world.player)
|
||||
room_region.add_locations({
|
||||
checkpoint_location_name: world.location_name_to_id[checkpoint_location_name]
|
||||
}, CelesteLocation)
|
||||
|
||||
if world.options.lock_goal_area and (level.name == world.goal_area or (level.name[:2] == world.goal_area[:2] == "10")):
|
||||
world.goal_checkpoint_names[room.checkpoint_region] = checkpoint_location_name
|
||||
else:
|
||||
menu_region.add_exits([room.checkpoint_region], {room.checkpoint_region: checkpoint_rule})
|
||||
|
||||
if world.options.roomsanity:
|
||||
if room.name != "10b_GOAL":
|
||||
room_location_name = room.display_name
|
||||
room_region.add_locations({
|
||||
room_location_name: world.location_name_to_id[room_location_name]
|
||||
}, CelesteLocation)
|
||||
|
||||
for room_connection in level.room_connections:
|
||||
source_region = world.get_region(room_connection.source.name)
|
||||
source_region.add_exits([room_connection.dest.name])
|
||||
if room_connection.two_way:
|
||||
dest_region = world.get_region(room_connection.dest.name)
|
||||
dest_region.add_exits([room_connection.source.name])
|
||||
|
||||
if level.name == "10b":
|
||||
# Manually connect the two parts of Farewell
|
||||
source_region = world.get_region("10a_e-08_east")
|
||||
source_region.add_exits(["10b_f-door_west"])
|
||||
|
||||
if level.name == "10c":
|
||||
# Manually connect the Golden room of Farewell
|
||||
golden_items: list[str] = [ItemName.traffic_blocks, ItemName.dash_refills, ItemName.double_dash_refills, ItemName.dream_blocks, ItemName.swap_blocks, ItemName.move_blocks, ItemName.blue_boosters, ItemName.springs, ItemName.feathers, ItemName.coins, ItemName.red_boosters, ItemName.kevin_blocks, ItemName.core_blocks, ItemName.fire_ice_balls, ItemName.badeline_boosters, ItemName.bird, ItemName.breaker_boxes, ItemName.pufferfish, ItemName.jellyfish, ItemName.pink_cassette_blocks, ItemName.blue_cassette_blocks, ItemName.yellow_cassette_blocks, ItemName.green_cassette_blocks]
|
||||
golden_rule = lambda state: state.has_all(golden_items, world.player)
|
||||
|
||||
source_region_end = world.get_region("10b_j-19_top")
|
||||
source_region_end.add_exits(["10c_end-golden_bottom"], {"10c_end-golden_bottom": golden_rule})
|
||||
source_region_moon = world.get_region("10b_j-16_east")
|
||||
source_region_moon.add_exits(["10c_end-golden_bottom"], {"10c_end-golden_bottom": golden_rule})
|
||||
source_region_golden = world.get_region("10c_end-golden_top")
|
||||
source_region_golden.add_exits(["10b_GOAL_main"])
|
||||
|
||||
|
||||
location_data_table: dict[str, int] = generate_location_table()
|
||||
|
||||
|
||||
def generate_location_groups() -> dict[str, list[str]]:
|
||||
from .Levels import Level, LocationType, load_logic_data
|
||||
level_data: dict[str, Level] = load_logic_data()
|
||||
|
||||
location_groups: dict[str, list[str]] = {
|
||||
"Strawberries": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.strawberry] and id < location_id_offsets[LocationType.golden_strawberry]],
|
||||
"Golden Strawberries": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.golden_strawberry] and id < location_id_offsets[LocationType.cassette]],
|
||||
"Cassettes": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.cassette] and id < location_id_offsets[LocationType.car]],
|
||||
"Cars": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.car] and id < location_id_offsets[LocationType.crystal_heart]],
|
||||
"Crystal Hearts": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.crystal_heart] and id < location_id_offsets[LocationType.checkpoint]],
|
||||
"Checkpoints": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.checkpoint] and id < location_id_offsets[LocationType.level_clear]],
|
||||
"Level Clears": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.level_clear] and id < location_id_offsets[LocationType.key]],
|
||||
"Keys": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.key] and id < location_id_offsets[LocationType.gem]],
|
||||
"Gems": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.gem] and id < location_id_offsets[LocationType.binoculars]],
|
||||
"Binoculars": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.binoculars] and id < location_id_offsets[LocationType.room_enter]],
|
||||
"Rooms": [name for name, id in location_data_table.items() if id >= location_id_offsets[LocationType.room_enter]],
|
||||
}
|
||||
|
||||
for level in level_data.values():
|
||||
location_groups.update({level.display_name: [loc_name for loc_name, id in location_data_table.items() if level.display_name in loc_name]})
|
||||
|
||||
return location_groups
|
||||
210
worlds/celeste_open_world/Names/ItemName.py
Normal file
210
worlds/celeste_open_world/Names/ItemName.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# Collectables
|
||||
strawberry = "Strawberry"
|
||||
raspberry = "Raspberry"
|
||||
|
||||
# Goal Items
|
||||
house_keys = "Granny's House Keys"
|
||||
victory = "Victory"
|
||||
|
||||
# Traps
|
||||
bald_trap = "Bald Trap"
|
||||
literature_trap = "Literature Trap"
|
||||
stun_trap = "Stun Trap"
|
||||
invisible_trap = "Invisible Trap"
|
||||
fast_trap = "Fast Trap"
|
||||
slow_trap = "Slow Trap"
|
||||
ice_trap = "Ice Trap"
|
||||
reverse_trap = "Reverse Trap"
|
||||
screen_flip_trap = "Screen Flip Trap"
|
||||
laughter_trap = "Laughter Trap"
|
||||
hiccup_trap = "Hiccup Trap"
|
||||
zoom_trap = "Zoom Trap"
|
||||
|
||||
# Movement
|
||||
dash = "Dash"
|
||||
u_dash = "Up Dash"
|
||||
r_dash = "Right Dash"
|
||||
d_dash = "Down Dash"
|
||||
l_dash = "Left Dash"
|
||||
ur_dash = "Up-Right Dash"
|
||||
dr_dash = "Down-Right Dash"
|
||||
dl_dash = "Down-Left Dash"
|
||||
ul_dash = "Up-Left Dash"
|
||||
|
||||
# Interactables
|
||||
springs = "Springs"
|
||||
traffic_blocks = "Traffic Blocks"
|
||||
pink_cassette_blocks = "Pink Cassette Blocks"
|
||||
blue_cassette_blocks = "Blue Cassette Blocks"
|
||||
|
||||
dream_blocks = "Dream Blocks"
|
||||
coins = "Coins"
|
||||
strawberry_seeds = "Strawberry Seeds"
|
||||
|
||||
sinking_platforms = "Sinking Platforms"
|
||||
|
||||
moving_platforms = "Moving Platforms"
|
||||
blue_boosters = "Blue Boosters"
|
||||
blue_clouds = "Blue Clouds"
|
||||
move_blocks = "Move Blocks"
|
||||
white_block = "White Block"
|
||||
|
||||
swap_blocks = "Swap Blocks"
|
||||
red_boosters = "Red Boosters"
|
||||
torches = "Torches"
|
||||
theo_crystal = "Theo Crystal"
|
||||
|
||||
feathers = "Feathers"
|
||||
bumpers = "Bumpers"
|
||||
kevin_blocks = "Kevins"
|
||||
|
||||
pink_clouds = "Pink Clouds"
|
||||
badeline_boosters = "Badeline Boosters"
|
||||
|
||||
fire_ice_balls = "Fire and Ice Balls"
|
||||
core_toggles = "Core Toggles"
|
||||
core_blocks = "Core Blocks"
|
||||
|
||||
pufferfish = "Pufferfish"
|
||||
jellyfish = "Jellyfish"
|
||||
breaker_boxes = "Breaker Boxes"
|
||||
dash_refills = "Dash Refills"
|
||||
double_dash_refills = "Double Dash Refills"
|
||||
yellow_cassette_blocks = "Yellow Cassette Blocks"
|
||||
green_cassette_blocks = "Green Cassette Blocks"
|
||||
|
||||
dash_switches = "Dash Switches"
|
||||
seekers = "Seekers"
|
||||
bird = "Bird"
|
||||
|
||||
brown_clutter = "Celestial Resort A - Brown Clutter"
|
||||
green_clutter = "Celestial Resort A - Green Clutter"
|
||||
pink_clutter = "Celestial Resort A - Pink Clutter"
|
||||
|
||||
cannot_access = "Cannot Access"
|
||||
|
||||
# Checkpoints
|
||||
fc_a_checkpoint_1 = "Forsaken City A - Crossing"
|
||||
fc_a_checkpoint_2 = "Forsaken City A - Chasm"
|
||||
|
||||
fc_b_checkpoint_1 = "Forsaken City B - Contraption"
|
||||
fc_b_checkpoint_2 = "Forsaken City B - Scrap Pit"
|
||||
|
||||
os_a_checkpoint_1 = "Old Site A - Intervention"
|
||||
os_a_checkpoint_2 = "Old Site A - Awake"
|
||||
|
||||
os_b_checkpoint_1 = "Old Site B - Combination Lock"
|
||||
os_b_checkpoint_2 = "Old Site B - Dream Altar"
|
||||
|
||||
cr_a_checkpoint_1 = "Celestial Resort A - Huge Mess"
|
||||
cr_a_checkpoint_2 = "Celestial Resort A - Elevator Shaft"
|
||||
cr_a_checkpoint_3 = "Celestial Resort A - Presidential Suite"
|
||||
|
||||
cr_b_checkpoint_1 = "Celestial Resort B - Staff Quarters"
|
||||
cr_b_checkpoint_2 = "Celestial Resort B - Library"
|
||||
cr_b_checkpoint_3 = "Celestial Resort B - Rooftop"
|
||||
|
||||
gr_a_checkpoint_1 = "Golden Ridge A - Shrine"
|
||||
gr_a_checkpoint_2 = "Golden Ridge A - Old Trail"
|
||||
gr_a_checkpoint_3 = "Golden Ridge A - Cliff Face"
|
||||
|
||||
gr_b_checkpoint_1 = "Golden Ridge B - Stepping Stones"
|
||||
gr_b_checkpoint_2 = "Golden Ridge B - Gusty Canyon"
|
||||
gr_b_checkpoint_3 = "Golden Ridge B - Eye of the Storm"
|
||||
|
||||
mt_a_checkpoint_1 = "Mirror Temple A - Depths"
|
||||
mt_a_checkpoint_2 = "Mirror Temple A - Unravelling"
|
||||
mt_a_checkpoint_3 = "Mirror Temple A - Search"
|
||||
mt_a_checkpoint_4 = "Mirror Temple A - Rescue"
|
||||
|
||||
mt_b_checkpoint_1 = "Mirror Temple B - Central Chamber"
|
||||
mt_b_checkpoint_2 = "Mirror Temple B - Through the Mirror"
|
||||
mt_b_checkpoint_3 = "Mirror Temple B - Mix Master"
|
||||
|
||||
ref_a_checkpoint_1 = "Reflection A - Lake"
|
||||
ref_a_checkpoint_2 = "Reflection A - Hollows"
|
||||
ref_a_checkpoint_3 = "Reflection A - Reflection"
|
||||
ref_a_checkpoint_4 = "Reflection A - Rock Bottom"
|
||||
ref_a_checkpoint_5 = "Reflection A - Resolution"
|
||||
|
||||
ref_b_checkpoint_1 = "Reflection B - Reflection"
|
||||
ref_b_checkpoint_2 = "Reflection B - Rock Bottom"
|
||||
ref_b_checkpoint_3 = "Reflection B - Reprieve"
|
||||
|
||||
sum_a_checkpoint_1 = "The Summit A - 500 M"
|
||||
sum_a_checkpoint_2 = "The Summit A - 1000 M"
|
||||
sum_a_checkpoint_3 = "The Summit A - 1500 M"
|
||||
sum_a_checkpoint_4 = "The Summit A - 2000 M"
|
||||
sum_a_checkpoint_5 = "The Summit A - 2500 M"
|
||||
sum_a_checkpoint_6 = "The Summit A - 3000 M"
|
||||
|
||||
sum_b_checkpoint_1 = "The Summit B - 500 M"
|
||||
sum_b_checkpoint_2 = "The Summit B - 1000 M"
|
||||
sum_b_checkpoint_3 = "The Summit B - 1500 M"
|
||||
sum_b_checkpoint_4 = "The Summit B - 2000 M"
|
||||
sum_b_checkpoint_5 = "The Summit B - 2500 M"
|
||||
sum_b_checkpoint_6 = "The Summit B - 3000 M"
|
||||
|
||||
core_a_checkpoint_1 = "Core A - Into the Core"
|
||||
core_a_checkpoint_2 = "Core A - Hot and Cold"
|
||||
core_a_checkpoint_3 = "Core A - Heart of the Mountain"
|
||||
|
||||
core_b_checkpoint_1 = "Core B - Into the Core"
|
||||
core_b_checkpoint_2 = "Core B - Burning or Freezing"
|
||||
core_b_checkpoint_3 = "Core B - Heartbeat"
|
||||
|
||||
farewell_checkpoint_1 = "Farewell - Singular"
|
||||
farewell_checkpoint_2 = "Farewell - Power Source"
|
||||
farewell_checkpoint_3 = "Farewell - Remembered"
|
||||
farewell_checkpoint_4 = "Farewell - Event Horizon"
|
||||
farewell_checkpoint_5 = "Farewell - Determination"
|
||||
farewell_checkpoint_6 = "Farewell - Stubbornness"
|
||||
farewell_checkpoint_7 = "Farewell - Reconcilliation"
|
||||
farewell_checkpoint_8 = "Farewell - Farewell"
|
||||
|
||||
# Cassettes
|
||||
prologue_cassette = "Prologue Cassette"
|
||||
fc_a_cassette = "Forsaken City Cassette - A Side"
|
||||
fc_b_cassette = "Forsaken City Cassette - B Side"
|
||||
fc_c_cassette = "Forsaken City Cassette - C Side"
|
||||
os_a_cassette = "Old Site Cassette - A Side"
|
||||
os_b_cassette = "Old Site Cassette - B Side"
|
||||
os_c_cassette = "Old Site Cassette - C Side"
|
||||
cr_a_cassette = "Celestial Resort Cassette - A Side"
|
||||
cr_b_cassette = "Celestial Resort Cassette - B Side"
|
||||
cr_c_cassette = "Celestial Resort Cassette - C Side"
|
||||
gr_a_cassette = "Golden Ridge Cassette - A Side"
|
||||
gr_b_cassette = "Golden Ridge Cassette - B Side"
|
||||
gr_c_cassette = "Golden Ridge Cassette - C Side"
|
||||
mt_a_cassette = "Mirror Temple Cassette - A Side"
|
||||
mt_b_cassette = "Mirror Temple Cassette - B Side"
|
||||
mt_c_cassette = "Mirror Temple Cassette - C Side"
|
||||
ref_a_cassette = "Reflection Cassette - A Side"
|
||||
ref_b_cassette = "Reflection Cassette - B Side"
|
||||
ref_c_cassette = "Reflection Cassette - C Side"
|
||||
sum_a_cassette = "The Summit Cassette - A Side"
|
||||
sum_b_cassette = "The Summit Cassette - B Side"
|
||||
sum_c_cassette = "The Summit Cassette - C Side"
|
||||
epilogue_cassette = "Epilogue Cassette"
|
||||
core_a_cassette = "Core Cassette - A Side"
|
||||
core_b_cassette = "Core Cassette - B Side"
|
||||
core_c_cassette = "Core Cassette - C Side"
|
||||
farewell_cassette = "Farewell Cassette"
|
||||
|
||||
# Crystal Hearts
|
||||
crystal_heart_1 = "Crystal Heart 1"
|
||||
crystal_heart_2 = "Crystal Heart 2"
|
||||
crystal_heart_3 = "Crystal Heart 3"
|
||||
crystal_heart_4 = "Crystal Heart 4"
|
||||
crystal_heart_5 = "Crystal Heart 5"
|
||||
crystal_heart_6 = "Crystal Heart 6"
|
||||
crystal_heart_7 = "Crystal Heart 7"
|
||||
crystal_heart_8 = "Crystal Heart 8"
|
||||
crystal_heart_9 = "Crystal Heart 9"
|
||||
crystal_heart_10 = "Crystal Heart 10"
|
||||
crystal_heart_11 = "Crystal Heart 11"
|
||||
crystal_heart_12 = "Crystal Heart 12"
|
||||
crystal_heart_13 = "Crystal Heart 13"
|
||||
crystal_heart_14 = "Crystal Heart 14"
|
||||
crystal_heart_15 = "Crystal Heart 15"
|
||||
crystal_heart_16 = "Crystal Heart 16"
|
||||
0
worlds/celeste_open_world/Names/__init__.py
Normal file
0
worlds/celeste_open_world/Names/__init__.py
Normal file
528
worlds/celeste_open_world/Options.py
Normal file
528
worlds/celeste_open_world/Options.py
Normal file
@@ -0,0 +1,528 @@
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
from Options import Choice, Range, DefaultOnToggle, Toggle, TextChoice, DeathLink, OptionGroup, PerGameCommonOptions, OptionError
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class DeathLinkAmnesty(Range):
|
||||
"""
|
||||
How many deaths it takes to send a DeathLink
|
||||
"""
|
||||
display_name = "Death Link Amnesty"
|
||||
range_start = 1
|
||||
range_end = 30
|
||||
default = 10
|
||||
|
||||
class TrapLink(Toggle):
|
||||
"""
|
||||
Whether your received traps are linked to other players
|
||||
|
||||
You will also receive any linked traps from other players with Trap Link enabled,
|
||||
if you have a weight above "none" set for that trap
|
||||
"""
|
||||
display_name = "Trap Link"
|
||||
|
||||
|
||||
class GoalArea(Choice):
|
||||
"""
|
||||
What Area must be cleared to gain access to the Epilogue and complete the game
|
||||
"""
|
||||
display_name = "Goal Area"
|
||||
option_the_summit_a = 0
|
||||
option_the_summit_b = 1
|
||||
option_the_summit_c = 2
|
||||
option_core_a = 3
|
||||
option_core_b = 4
|
||||
option_core_c = 5
|
||||
option_empty_space = 6
|
||||
option_farewell = 7
|
||||
option_farewell_golden = 8
|
||||
default = 0
|
||||
|
||||
class LockGoalArea(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether your Goal Area will be locked until you receive your required Strawberries, or only the Epilogue
|
||||
"""
|
||||
display_name = "Lock Goal Area"
|
||||
|
||||
class GoalAreaCheckpointsanity(Toggle):
|
||||
"""
|
||||
Determines whether the Checkpoints in your Goal Area will be shuffled into the item pool (if Checkpointsanity is active)
|
||||
"""
|
||||
display_name = "Goal Area Checkpointsanity"
|
||||
|
||||
class TotalStrawberries(Range):
|
||||
"""
|
||||
Maximum number of how many Strawberries can exist
|
||||
"""
|
||||
display_name = "Total Strawberries"
|
||||
range_start = 0
|
||||
range_end = 202
|
||||
default = 50
|
||||
|
||||
class StrawberriesRequiredPercentage(Range):
|
||||
"""
|
||||
Percentage of existing Strawberries you must receive to access your Goal Area (if Lock Goal Area is active) and the Epilogue
|
||||
"""
|
||||
display_name = "Strawberries Required Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 80
|
||||
|
||||
|
||||
class Checkpointsanity(Toggle):
|
||||
"""
|
||||
Determines whether Checkpoints will be shuffled into the item pool
|
||||
"""
|
||||
display_name = "Checkpointsanity"
|
||||
|
||||
class Binosanity(Toggle):
|
||||
"""
|
||||
Determines whether using Binoculars sends location checks
|
||||
"""
|
||||
display_name = "Binosanity"
|
||||
|
||||
class Keysanity(Toggle):
|
||||
"""
|
||||
Determines whether individual Keys are shuffled into the item pool
|
||||
"""
|
||||
display_name = "Keysanity"
|
||||
|
||||
class Gemsanity(Toggle):
|
||||
"""
|
||||
Determines whether Summit Gems are shuffled into the item pool
|
||||
"""
|
||||
display_name = "Gemsanity"
|
||||
|
||||
class Carsanity(Toggle):
|
||||
"""
|
||||
Determines whether riding on cars grants location checks
|
||||
"""
|
||||
display_name = "Carsanity"
|
||||
|
||||
class Roomsanity(Toggle):
|
||||
"""
|
||||
Determines whether entering individual rooms sends location checks
|
||||
"""
|
||||
display_name = "Roomsanity"
|
||||
|
||||
class IncludeGoldens(Toggle):
|
||||
"""
|
||||
Determines whether collecting Golden Strawberries sends location checks
|
||||
"""
|
||||
display_name = "Include Goldens"
|
||||
|
||||
|
||||
class IncludeCore(Toggle):
|
||||
"""
|
||||
Determines whether Chapter 8 - Core Levels will be included
|
||||
"""
|
||||
display_name = "Include Core"
|
||||
|
||||
class IncludeFarewell(Choice):
|
||||
"""
|
||||
Determines how much of Chapter 9 - Farewell Level will be included
|
||||
"""
|
||||
display_name = "Include Farewell"
|
||||
option_none = 0
|
||||
option_empty_space = 1
|
||||
option_farewell = 2
|
||||
default = 0
|
||||
|
||||
class IncludeBSides(Toggle):
|
||||
"""
|
||||
Determines whether the B-Side Levels will be included
|
||||
"""
|
||||
display_name = "Include B-Sides"
|
||||
|
||||
class IncludeCSides(Toggle):
|
||||
"""
|
||||
Determines whether the C-Side Levels will be included
|
||||
"""
|
||||
display_name = "Include C-Sides"
|
||||
|
||||
|
||||
class JunkFillPercentage(Range):
|
||||
"""
|
||||
Replace a percentage of non-required Strawberries in the item pool with junk items
|
||||
"""
|
||||
display_name = "Junk Fill Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 50
|
||||
|
||||
class TrapFillPercentage(Range):
|
||||
"""
|
||||
Replace a percentage of junk items in the item pool with random traps
|
||||
"""
|
||||
display_name = "Trap Fill Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
class TrapExpirationAction(Choice):
|
||||
"""
|
||||
The type of action which causes traps to wear off
|
||||
"""
|
||||
display_name = "Trap Expiration Action"
|
||||
option_return_to_menu = 0
|
||||
option_deaths = 1
|
||||
option_new_screens = 2
|
||||
default = 1
|
||||
|
||||
class TrapExpirationAmount(Range):
|
||||
"""
|
||||
The amount of the selected Trap Expiration Action that must occur for the trap to wear off
|
||||
"""
|
||||
display_name = "Trap Expiration Amount"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 5
|
||||
|
||||
class BaseTrapWeight(Choice):
|
||||
"""
|
||||
Base Class for Trap Weights
|
||||
"""
|
||||
option_none = 0
|
||||
option_low = 1
|
||||
option_medium = 2
|
||||
option_high = 4
|
||||
default = 2
|
||||
|
||||
class BaldTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of receiving a trap which makes Maddy bald
|
||||
"""
|
||||
display_name = "Bald Trap Weight"
|
||||
|
||||
class LiteratureTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes the player to read literature
|
||||
"""
|
||||
display_name = "Literature Trap Weight"
|
||||
|
||||
class StunTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which briefly stuns Maddy
|
||||
"""
|
||||
display_name = "Stun Trap Weight"
|
||||
|
||||
class InvisibleTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which turns Maddy invisible
|
||||
"""
|
||||
display_name = "Invisible Trap Weight"
|
||||
|
||||
class FastTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which increases the game speed
|
||||
"""
|
||||
display_name = "Fast Trap Weight"
|
||||
|
||||
class SlowTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which decreases the game speed
|
||||
"""
|
||||
display_name = "Slow Trap Weight"
|
||||
|
||||
class IceTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes the level to become slippery
|
||||
"""
|
||||
display_name = "Ice Trap Weight"
|
||||
|
||||
class ReverseTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes the controls to be reversed
|
||||
"""
|
||||
display_name = "Reverse Trap Weight"
|
||||
|
||||
class ScreenFlipTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes the screen to be flipped
|
||||
"""
|
||||
display_name = "Screen Flip Trap Weight"
|
||||
|
||||
class LaughterTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes Maddy to laugh uncontrollably
|
||||
"""
|
||||
display_name = "Laughter Trap Weight"
|
||||
|
||||
class HiccupTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes Maddy to hiccup uncontrollably
|
||||
"""
|
||||
display_name = "Hiccup Trap Weight"
|
||||
|
||||
class ZoomTrapWeight(BaseTrapWeight):
|
||||
"""
|
||||
Likelihood of a receiving a trap which causes the camera to focus on Maddy
|
||||
"""
|
||||
display_name = "Zoom Trap Weight"
|
||||
|
||||
|
||||
class MusicShuffle(Choice):
|
||||
"""
|
||||
Music shuffle type
|
||||
|
||||
None: No Music is shuffled
|
||||
|
||||
Consistent: Each music track is consistently shuffled throughout the game
|
||||
|
||||
Singularity: The entire game uses one song for levels
|
||||
"""
|
||||
display_name = "Music Shuffle"
|
||||
option_none = 0
|
||||
option_consistent = 1
|
||||
option_singularity = 2
|
||||
default = 0
|
||||
|
||||
class RequireCassettes(Toggle):
|
||||
"""
|
||||
Determines whether you must receive a level's Cassette Item to hear that level's music
|
||||
"""
|
||||
display_name = "Require Cassettes"
|
||||
|
||||
|
||||
class MadelineHairLength(Choice):
|
||||
"""
|
||||
How long Madeline's hair is
|
||||
"""
|
||||
display_name = "Madeline Hair Length"
|
||||
option_very_short = 1
|
||||
option_short = 2
|
||||
option_default = 4
|
||||
option_long = 7
|
||||
option_very_long = 10
|
||||
option_absurd = 20
|
||||
default = 4
|
||||
|
||||
|
||||
class ColorChoice(TextChoice):
|
||||
option_strawberry = 0xAC3232
|
||||
option_empty = 0x44B7FF
|
||||
option_double = 0xFF6DEF
|
||||
option_golden = 0xFFD65C
|
||||
option_baddy = 0x9B3FB5
|
||||
option_fire_red = 0xFF0000
|
||||
option_maroon = 0x800000
|
||||
option_salmon = 0xFF3A65
|
||||
option_orange = 0xD86E0A
|
||||
option_lime_green = 0x8DF920
|
||||
option_bright_green = 0x0DAF05
|
||||
option_forest_green = 0x132818
|
||||
option_royal_blue = 0x0036BF
|
||||
option_brown = 0xB78726
|
||||
option_black = 0x000000
|
||||
option_white = 0xFFFFFF
|
||||
option_grey = 0x808080
|
||||
option_any_color = -1
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(cls.option_any_color)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
class MadelineOneDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has one dash
|
||||
The `any_color` option will choose a fully random color
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline One Dash Hair Color"
|
||||
default = ColorChoice.option_strawberry
|
||||
|
||||
class MadelineTwoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has two dashes
|
||||
The `any_color` option will choose a fully random color
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Two Dash Hair Color"
|
||||
default = ColorChoice.option_double
|
||||
|
||||
class MadelineNoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has no dashes
|
||||
The `any_color` option will choose a fully random color
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline No Dash Hair Color"
|
||||
default = ColorChoice.option_empty
|
||||
|
||||
class MadelineFeatherHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has a feather
|
||||
The `any_color` option will choose a fully random color
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Feather Hair Color"
|
||||
default = ColorChoice.option_golden
|
||||
|
||||
|
||||
|
||||
celeste_option_groups = [
|
||||
OptionGroup("Goal Options", [
|
||||
GoalArea,
|
||||
LockGoalArea,
|
||||
GoalAreaCheckpointsanity,
|
||||
TotalStrawberries,
|
||||
StrawberriesRequiredPercentage,
|
||||
]),
|
||||
OptionGroup("Location Options", [
|
||||
Checkpointsanity,
|
||||
Binosanity,
|
||||
Keysanity,
|
||||
Gemsanity,
|
||||
Carsanity,
|
||||
Roomsanity,
|
||||
IncludeGoldens,
|
||||
IncludeCore,
|
||||
IncludeFarewell,
|
||||
IncludeBSides,
|
||||
IncludeCSides,
|
||||
]),
|
||||
OptionGroup("Junk and Traps", [
|
||||
JunkFillPercentage,
|
||||
TrapFillPercentage,
|
||||
TrapExpirationAction,
|
||||
TrapExpirationAmount,
|
||||
BaldTrapWeight,
|
||||
LiteratureTrapWeight,
|
||||
StunTrapWeight,
|
||||
InvisibleTrapWeight,
|
||||
FastTrapWeight,
|
||||
SlowTrapWeight,
|
||||
IceTrapWeight,
|
||||
ReverseTrapWeight,
|
||||
ScreenFlipTrapWeight,
|
||||
LaughterTrapWeight,
|
||||
HiccupTrapWeight,
|
||||
ZoomTrapWeight,
|
||||
]),
|
||||
OptionGroup("Aesthetic Options", [
|
||||
MusicShuffle,
|
||||
RequireCassettes,
|
||||
MadelineHairLength,
|
||||
MadelineOneDashHairColor,
|
||||
MadelineTwoDashHairColor,
|
||||
MadelineNoDashHairColor,
|
||||
MadelineFeatherHairColor,
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
def resolve_options(world: World):
|
||||
# One Dash Hair
|
||||
if isinstance(world.options.madeline_one_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_one_dash_hair_color = int(world.options.madeline_one_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_one_dash_hair_color`:"
|
||||
f"{world.options.madeline_one_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_one_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_one_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_one_dash_hair_color = world.options.madeline_one_dash_hair_color.value
|
||||
|
||||
# Two Dash Hair
|
||||
if isinstance(world.options.madeline_two_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_two_dash_hair_color = int(world.options.madeline_two_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_two_dash_hair_color`:"
|
||||
f"{world.options.madeline_two_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_two_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_two_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_two_dash_hair_color = world.options.madeline_two_dash_hair_color.value
|
||||
|
||||
# No Dash Hair
|
||||
if isinstance(world.options.madeline_no_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_no_dash_hair_color = int(world.options.madeline_no_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_no_dash_hair_color`:"
|
||||
f"{world.options.madeline_no_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_no_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_no_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_no_dash_hair_color = world.options.madeline_no_dash_hair_color.value
|
||||
|
||||
# Feather Hair
|
||||
if isinstance(world.options.madeline_feather_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_feather_hair_color = int(world.options.madeline_feather_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_feather_hair_color`:"
|
||||
f"{world.options.madeline_feather_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_feather_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_feather_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_feather_hair_color = world.options.madeline_feather_hair_color.value
|
||||
|
||||
|
||||
@dataclass
|
||||
class CelesteOptions(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
death_link_amnesty: DeathLinkAmnesty
|
||||
trap_link: TrapLink
|
||||
|
||||
goal_area: GoalArea
|
||||
lock_goal_area: LockGoalArea
|
||||
goal_area_checkpointsanity: GoalAreaCheckpointsanity
|
||||
total_strawberries: TotalStrawberries
|
||||
strawberries_required_percentage: StrawberriesRequiredPercentage
|
||||
|
||||
junk_fill_percentage: JunkFillPercentage
|
||||
trap_fill_percentage: TrapFillPercentage
|
||||
trap_expiration_action: TrapExpirationAction
|
||||
trap_expiration_amount: TrapExpirationAmount
|
||||
bald_trap_weight: BaldTrapWeight
|
||||
literature_trap_weight: LiteratureTrapWeight
|
||||
stun_trap_weight: StunTrapWeight
|
||||
invisible_trap_weight: InvisibleTrapWeight
|
||||
fast_trap_weight: FastTrapWeight
|
||||
slow_trap_weight: SlowTrapWeight
|
||||
ice_trap_weight: IceTrapWeight
|
||||
reverse_trap_weight: ReverseTrapWeight
|
||||
screen_flip_trap_weight: ScreenFlipTrapWeight
|
||||
laughter_trap_weight: LaughterTrapWeight
|
||||
hiccup_trap_weight: HiccupTrapWeight
|
||||
zoom_trap_weight: ZoomTrapWeight
|
||||
|
||||
checkpointsanity: Checkpointsanity
|
||||
binosanity: Binosanity
|
||||
keysanity: Keysanity
|
||||
gemsanity: Gemsanity
|
||||
carsanity: Carsanity
|
||||
roomsanity: Roomsanity
|
||||
include_goldens: IncludeGoldens
|
||||
include_core: IncludeCore
|
||||
include_farewell: IncludeFarewell
|
||||
include_b_sides: IncludeBSides
|
||||
include_c_sides: IncludeCSides
|
||||
|
||||
music_shuffle: MusicShuffle
|
||||
require_cassettes: RequireCassettes
|
||||
|
||||
madeline_hair_length: MadelineHairLength
|
||||
madeline_one_dash_hair_color: MadelineOneDashHairColor
|
||||
madeline_two_dash_hair_color: MadelineTwoDashHairColor
|
||||
madeline_no_dash_hair_color: MadelineNoDashHairColor
|
||||
madeline_feather_hair_color: MadelineFeatherHairColor
|
||||
351
worlds/celeste_open_world/__init__.py
Normal file
351
worlds/celeste_open_world/__init__.py
Normal file
@@ -0,0 +1,351 @@
|
||||
from copy import deepcopy
|
||||
import math
|
||||
|
||||
from BaseClasses import ItemClassification, Location, MultiWorld, Region, Tutorial
|
||||
from Utils import visualize_regions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .Items import CelesteItem, generate_item_table, generate_item_data_table, generate_item_groups, level_item_lists, level_cassette_items,\
|
||||
cassette_item_data_table, crystal_heart_item_data_table, trap_item_data_table
|
||||
from .Locations import CelesteLocation, location_data_table, generate_location_groups, checkpoint_location_data_table, location_id_offsets
|
||||
from .Names import ItemName
|
||||
from .Options import CelesteOptions, celeste_option_groups, resolve_options
|
||||
from .Levels import Level, LocationType, load_logic_data, goal_area_option_to_name, goal_area_option_to_display_name, goal_area_to_location_name
|
||||
|
||||
|
||||
class CelesteOpenWebWorld(WebWorld):
|
||||
theme = "ice"
|
||||
|
||||
setup_en = Tutorial(
|
||||
tutorial_name="Start Guide",
|
||||
description="A guide to playing Celeste (Open World) in Archipelago.",
|
||||
language="English",
|
||||
file_name="guide_en.md",
|
||||
link="guide/en",
|
||||
authors=["PoryGone"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
option_groups = celeste_option_groups
|
||||
|
||||
|
||||
class CelesteOpenWorld(World):
|
||||
"""
|
||||
Celeste (Open World) is a randomizer for the original Celeste. In this acclaimed platformer created by ExOK Games, you control Madeline as she attempts to climb the titular mountain, meeting friends and obstacles along the way. Progression is found in unlocking the ability to interact with various objects in the areas, such as springs, traffic blocks, feathers, and many more. Please be safe on the climb.
|
||||
"""
|
||||
|
||||
# Class Data
|
||||
game = "Celeste (Open World)"
|
||||
web = CelesteOpenWebWorld()
|
||||
options_dataclass = CelesteOptions
|
||||
options: CelesteOptions
|
||||
|
||||
level_data: dict[str, Level] = load_logic_data()
|
||||
|
||||
location_name_to_id: dict[str, int] = location_data_table
|
||||
location_name_groups: dict[str, list[str]] = generate_location_groups()
|
||||
item_name_to_id: dict[str, int] = generate_item_table()
|
||||
item_name_groups: dict[str, list[str]] = generate_item_groups()
|
||||
|
||||
|
||||
# Instance Data
|
||||
madeline_one_dash_hair_color: int
|
||||
madeline_two_dash_hair_color: int
|
||||
madeline_no_dash_hair_color: int
|
||||
madeline_feather_hair_color: int
|
||||
|
||||
active_levels: set[str]
|
||||
active_items: set[str]
|
||||
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if not self.player_name.isascii():
|
||||
raise RuntimeError(f"Invalid player_name {self.player_name} for game {self.game}. Name must be ascii.")
|
||||
|
||||
resolve_options(self)
|
||||
|
||||
self.goal_area: str = goal_area_option_to_name[self.options.goal_area.value]
|
||||
|
||||
self.active_levels = {"0a", "1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a"}
|
||||
if self.options.include_core:
|
||||
self.active_levels.add("9a")
|
||||
if self.options.include_farewell >= 1:
|
||||
self.active_levels.add("10a")
|
||||
if self.options.include_farewell == 2:
|
||||
self.active_levels.add("10b")
|
||||
if self.options.include_b_sides:
|
||||
self.active_levels.update({"1b", "2b", "3b", "4b", "5b", "6b", "7b"})
|
||||
if self.options.include_core:
|
||||
self.active_levels.add("9b")
|
||||
if self.options.include_c_sides:
|
||||
self.active_levels.update({"1c", "2c", "3c", "4c", "5c", "6c", "7c"})
|
||||
if self.options.include_core:
|
||||
self.active_levels.add("9c")
|
||||
|
||||
self.active_levels.add(self.goal_area)
|
||||
if self.goal_area == "10c":
|
||||
self.active_levels.add("10a")
|
||||
self.active_levels.add("10b")
|
||||
elif self.goal_area == "10b":
|
||||
self.active_levels.add("10a")
|
||||
|
||||
self.active_items = set()
|
||||
for level in self.active_levels:
|
||||
self.active_items.update(level_item_lists[level])
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
from .Locations import create_regions_and_locations
|
||||
|
||||
create_regions_and_locations(self)
|
||||
|
||||
|
||||
def create_item(self, name: str, force_useful: bool = False) -> CelesteItem:
|
||||
item_data_table = generate_item_data_table()
|
||||
|
||||
if name == ItemName.strawberry and force_useful:
|
||||
return CelesteItem(name, ItemClassification.useful, item_data_table[name].code, self.player)
|
||||
elif name in item_data_table:
|
||||
return CelesteItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
|
||||
else:
|
||||
return CelesteItem(name, ItemClassification.progression, None, self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
item_pool: list[CelesteItem] = []
|
||||
|
||||
location_count: int = len(self.get_locations())
|
||||
goal_area_location_count: int = sum(goal_area_option_to_display_name[self.options.goal_area] in loc.name for loc in self.get_locations())
|
||||
|
||||
# Goal Items
|
||||
goal_item_loc: Location = self.get_location(goal_area_to_location_name[self.goal_area])
|
||||
goal_item_loc.place_locked_item(self.create_item(ItemName.house_keys))
|
||||
location_count -= 1
|
||||
|
||||
epilogue_region: Region = self.get_region(self.epilogue_start_region)
|
||||
epilogue_region.add_locations({ItemName.victory: None }, CelesteLocation)
|
||||
victory_loc: Location = self.get_location(ItemName.victory)
|
||||
victory_loc.place_locked_item(self.create_item(ItemName.victory))
|
||||
|
||||
# Checkpoints
|
||||
for item_name in self.active_checkpoint_names:
|
||||
if self.options.checkpointsanity:
|
||||
if not self.options.goal_area_checkpointsanity and goal_area_option_to_display_name[self.options.goal_area] in item_name:
|
||||
checkpoint_loc: Location = self.get_location(item_name)
|
||||
checkpoint_loc.place_locked_item(self.create_item(item_name))
|
||||
location_count -= 1
|
||||
else:
|
||||
item_pool.append(self.create_item(item_name))
|
||||
else:
|
||||
checkpoint_loc: Location = self.get_location(item_name)
|
||||
checkpoint_loc.place_locked_item(self.create_item(item_name))
|
||||
location_count -= 1
|
||||
|
||||
# Keys
|
||||
if self.options.keysanity:
|
||||
item_pool += [self.create_item(item_name) for item_name in self.active_key_names]
|
||||
else:
|
||||
for item_name in self.active_key_names:
|
||||
key_loc: Location = self.get_location(item_name)
|
||||
key_loc.place_locked_item(self.create_item(item_name))
|
||||
location_count -= 1
|
||||
|
||||
# Summit Gems
|
||||
if self.options.gemsanity:
|
||||
item_pool += [self.create_item(item_name) for item_name in self.active_gem_names]
|
||||
else:
|
||||
for item_name in self.active_gem_names:
|
||||
gem_loc: Location = self.get_location(item_name)
|
||||
gem_loc.place_locked_item(self.create_item(item_name))
|
||||
location_count -= 1
|
||||
|
||||
# Clutter Events
|
||||
for item_name in self.active_clutter_names:
|
||||
clutter_loc: Location = self.get_location(item_name)
|
||||
clutter_loc.place_locked_item(self.create_item(item_name))
|
||||
location_count -= 1
|
||||
|
||||
# Interactables
|
||||
item_pool += [self.create_item(item_name) for item_name in sorted(self.active_items)]
|
||||
|
||||
# Strawberries
|
||||
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - goal_area_location_count - len(item_pool))
|
||||
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
|
||||
|
||||
menu_region = self.get_region("Menu")
|
||||
if getattr(self, "goal_start_region", None):
|
||||
menu_region.add_exits([self.goal_start_region], {self.goal_start_region: lambda state: state.has(ItemName.strawberry, self.player, self.strawberries_required)})
|
||||
if getattr(self, "goal_checkpoint_names", None):
|
||||
for region_name, location_name in self.goal_checkpoint_names.items():
|
||||
checkpoint_rule = lambda state, location_name=location_name: state.has(location_name, self.player) and state.has(ItemName.strawberry, self.player, self.strawberries_required)
|
||||
menu_region.add_exits([region_name], {region_name: checkpoint_rule})
|
||||
|
||||
menu_region.add_exits([self.epilogue_start_region], {self.epilogue_start_region: lambda state: (state.has(ItemName.strawberry, self.player, self.strawberries_required) and state.has(ItemName.house_keys, self.player))})
|
||||
|
||||
item_pool += [self.create_item(ItemName.strawberry) for _ in range(self.strawberries_required)]
|
||||
|
||||
# Filler and Traps
|
||||
non_required_strawberries = (real_total_strawberries - self.strawberries_required)
|
||||
replacement_filler_count = math.floor(non_required_strawberries * (self.options.junk_fill_percentage.value / 100.0))
|
||||
remaining_extra_strawberries = non_required_strawberries - replacement_filler_count
|
||||
item_pool += [self.create_item(ItemName.strawberry, True) for _ in range(remaining_extra_strawberries)]
|
||||
|
||||
trap_weights = []
|
||||
trap_weights += ([ItemName.bald_trap] * self.options.bald_trap_weight.value)
|
||||
trap_weights += ([ItemName.literature_trap] * self.options.literature_trap_weight.value)
|
||||
trap_weights += ([ItemName.stun_trap] * self.options.stun_trap_weight.value)
|
||||
trap_weights += ([ItemName.invisible_trap] * self.options.invisible_trap_weight.value)
|
||||
trap_weights += ([ItemName.fast_trap] * self.options.fast_trap_weight.value)
|
||||
trap_weights += ([ItemName.slow_trap] * self.options.slow_trap_weight.value)
|
||||
trap_weights += ([ItemName.ice_trap] * self.options.ice_trap_weight.value)
|
||||
trap_weights += ([ItemName.reverse_trap] * self.options.reverse_trap_weight.value)
|
||||
trap_weights += ([ItemName.screen_flip_trap] * self.options.screen_flip_trap_weight.value)
|
||||
trap_weights += ([ItemName.laughter_trap] * self.options.laughter_trap_weight.value)
|
||||
trap_weights += ([ItemName.hiccup_trap] * self.options.hiccup_trap_weight.value)
|
||||
trap_weights += ([ItemName.zoom_trap] * self.options.zoom_trap_weight.value)
|
||||
|
||||
total_filler_count: int = (location_count - len(item_pool))
|
||||
|
||||
# Cassettes
|
||||
if self.options.require_cassettes:
|
||||
shuffled_active_levels = sorted(self.active_levels)
|
||||
self.random.shuffle(shuffled_active_levels)
|
||||
for level_name in shuffled_active_levels:
|
||||
if level_name == "10b" or level_name == "10c":
|
||||
continue
|
||||
if level_name not in self.multiworld.precollected_items[self.player]:
|
||||
if total_filler_count > 0:
|
||||
item_pool.append(self.create_item(level_cassette_items[level_name]))
|
||||
total_filler_count -= 1
|
||||
else:
|
||||
self.multiworld.push_precollected(self.create_item(level_cassette_items[level_name]))
|
||||
|
||||
# Crystal Hearts
|
||||
for name in crystal_heart_item_data_table.keys():
|
||||
if total_filler_count > 0:
|
||||
if name not in self.multiworld.precollected_items[self.player]:
|
||||
item_pool.append(self.create_item(name))
|
||||
total_filler_count -= 1
|
||||
|
||||
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(total_filler_count * (self.options.trap_fill_percentage.value / 100.0))
|
||||
total_filler_count -= trap_count
|
||||
|
||||
item_pool += [self.create_item(ItemName.raspberry) for _ in range(total_filler_count)]
|
||||
|
||||
trap_pool = []
|
||||
for i in range(trap_count):
|
||||
trap_item = self.random.choice(trap_weights)
|
||||
trap_pool.append(self.create_item(trap_item))
|
||||
|
||||
item_pool += trap_pool
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return ItemName.raspberry
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.victory, self.player)
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"apworld_version": 10004,
|
||||
"min_mod_version": 10000,
|
||||
|
||||
"death_link": self.options.death_link.value,
|
||||
"death_link_amnesty": self.options.death_link_amnesty.value,
|
||||
"trap_link": self.options.trap_link.value,
|
||||
|
||||
"active_levels": self.active_levels,
|
||||
"goal_area": self.goal_area,
|
||||
"lock_goal_area": self.options.lock_goal_area.value,
|
||||
"strawberries_required": self.strawberries_required,
|
||||
|
||||
"checkpointsanity": self.options.checkpointsanity.value,
|
||||
"binosanity": self.options.binosanity.value,
|
||||
"keysanity": self.options.keysanity.value,
|
||||
"gemsanity": self.options.gemsanity.value,
|
||||
"carsanity": self.options.carsanity.value,
|
||||
"roomsanity": self.options.roomsanity.value,
|
||||
"include_goldens": self.options.include_goldens.value,
|
||||
|
||||
"include_core": self.options.include_core.value,
|
||||
"include_farewell": self.options.include_farewell.value,
|
||||
"include_b_sides": self.options.include_b_sides.value,
|
||||
"include_c_sides": self.options.include_c_sides.value,
|
||||
|
||||
"trap_expiration_action": self.options.trap_expiration_action.value,
|
||||
"trap_expiration_amount": self.options.trap_expiration_amount.value,
|
||||
"active_traps": self.output_active_traps(),
|
||||
|
||||
"madeline_hair_length": self.options.madeline_hair_length.value,
|
||||
"madeline_one_dash_hair_color": self.madeline_one_dash_hair_color,
|
||||
"madeline_two_dash_hair_color": self.madeline_two_dash_hair_color,
|
||||
"madeline_no_dash_hair_color": self.madeline_no_dash_hair_color,
|
||||
"madeline_feather_hair_color": self.madeline_feather_hair_color,
|
||||
|
||||
"music_shuffle": self.options.music_shuffle.value,
|
||||
"music_map": self.generate_music_data(),
|
||||
"require_cassettes": self.options.require_cassettes.value,
|
||||
"chosen_poem": self.random.randint(0, 119),
|
||||
}
|
||||
|
||||
def output_active_traps(self) -> dict[int, int]:
|
||||
trap_data = {}
|
||||
|
||||
trap_data[0x20] = self.options.bald_trap_weight.value
|
||||
trap_data[0x21] = self.options.literature_trap_weight.value
|
||||
trap_data[0x22] = self.options.stun_trap_weight.value
|
||||
trap_data[0x23] = self.options.invisible_trap_weight.value
|
||||
trap_data[0x24] = self.options.fast_trap_weight.value
|
||||
trap_data[0x25] = self.options.slow_trap_weight.value
|
||||
trap_data[0x26] = self.options.ice_trap_weight.value
|
||||
trap_data[0x28] = self.options.reverse_trap_weight.value
|
||||
trap_data[0x29] = self.options.screen_flip_trap_weight.value
|
||||
trap_data[0x2A] = self.options.laughter_trap_weight.value
|
||||
trap_data[0x2B] = self.options.hiccup_trap_weight.value
|
||||
trap_data[0x2C] = self.options.zoom_trap_weight.value
|
||||
|
||||
return trap_data
|
||||
|
||||
def generate_music_data(self) -> dict[int, int]:
|
||||
if self.options.music_shuffle == "consistent":
|
||||
musiclist_o = list(range(0, 48))
|
||||
musiclist_s = musiclist_o.copy()
|
||||
self.random.shuffle(musiclist_s)
|
||||
|
||||
return dict(zip(musiclist_o, musiclist_s))
|
||||
elif self.options.music_shuffle == "singularity":
|
||||
musiclist_o = list(range(0, 48))
|
||||
musiclist_s = [self.random.choice(musiclist_o)] * len(musiclist_o)
|
||||
|
||||
return dict(zip(musiclist_o, musiclist_s))
|
||||
else:
|
||||
musiclist_o = list(range(0, 48))
|
||||
musiclist_s = musiclist_o.copy()
|
||||
|
||||
return dict(zip(musiclist_o, musiclist_s))
|
||||
|
||||
|
||||
# Useful Debugging tools, kept around for later.
|
||||
#@classmethod
|
||||
#def stage_assert_generate(cls, _multiworld: MultiWorld) -> None:
|
||||
# with open("./worlds/celeste_open_world/data/IDs.txt", "w") as f:
|
||||
# print("Items:", file=f)
|
||||
# for name in sorted(CelesteOpenWorld.item_name_to_id, key=CelesteOpenWorld.item_name_to_id.get):
|
||||
# id = CelesteOpenWorld.item_name_to_id[name]
|
||||
# print(f"{{ 0x{id:X}, \"{name}\" }},", file=f)
|
||||
# print("\nLocations:", file=f)
|
||||
# for name in sorted(CelesteOpenWorld.location_name_to_id, key=CelesteOpenWorld.location_name_to_id.get):
|
||||
# id = CelesteOpenWorld.location_name_to_id[name]
|
||||
# print(f"{{ 0x{id:X}, \"{name}\" }},", file=f)
|
||||
# print("\nLocations 2:", file=f)
|
||||
# for name in sorted(CelesteOpenWorld.location_name_to_id, key=CelesteOpenWorld.location_name_to_id.get):
|
||||
# id = CelesteOpenWorld.location_name_to_id[name]
|
||||
# print(f"{{ \"{name}\", 0x{id:X} }},", file=f)
|
||||
#
|
||||
#def generate_output(self, output_directory: str):
|
||||
# visualize_regions(self.get_region("Menu"), f"Player{self.player}.puml", show_entrance_names=False,
|
||||
# regions_to_highlight=self.multiworld.get_all_state(self.player).reachable_regions[self.player])
|
||||
41232
worlds/celeste_open_world/data/CelesteLevelData.json
Normal file
41232
worlds/celeste_open_world/data/CelesteLevelData.json
Normal file
File diff suppressed because it is too large
Load Diff
9792
worlds/celeste_open_world/data/CelesteLevelData.py
Normal file
9792
worlds/celeste_open_world/data/CelesteLevelData.py
Normal file
File diff suppressed because it is too large
Load Diff
190
worlds/celeste_open_world/data/ParseData.py
Normal file
190
worlds/celeste_open_world/data/ParseData.py
Normal file
@@ -0,0 +1,190 @@
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
all_doors: list[str] = []
|
||||
all_region_connections: list[str] = []
|
||||
all_locations: list[str] = []
|
||||
all_regions: list[str] = []
|
||||
all_room_connections: list[str] = []
|
||||
all_rooms: list[str] = []
|
||||
all_levels: list[str] = []
|
||||
|
||||
|
||||
data_file = open('CelesteLevelData.json')
|
||||
level_data = json.load(data_file)
|
||||
data_file.close()
|
||||
|
||||
# Levels
|
||||
for level in level_data["levels"]:
|
||||
level_str = (f" \"{level['name']}\": Level(\"{level['name']}\", "
|
||||
f"\"{level['display_name']}\", "
|
||||
f"[room for _, room in all_rooms.items() if room.level_name == \"{level['name']}\"], "
|
||||
f"[room_con for _, room_con in all_room_connections.items() if room_con.level_name == \"{level['name']}\"]),"
|
||||
)
|
||||
|
||||
all_levels.append(level_str)
|
||||
|
||||
# Rooms
|
||||
for room in level["rooms"]:
|
||||
room_full_name = f"{level['name']}_{room['name']}"
|
||||
room_full_display_name = f"{level['display_name']} - Room {room['name']}"
|
||||
|
||||
room_str = (f" \"{room_full_name}\": Room(\"{level['name']}\", "
|
||||
f"\"{room_full_name}\", \"{room_full_display_name}\", "
|
||||
f"[reg for _, reg in all_regions.items() if reg.room_name == \"{room_full_name}\"], "
|
||||
f"[door for _, door in all_doors.items() if door.room_name == \"{room_full_name}\"]"
|
||||
)
|
||||
|
||||
if "checkpoint" in room and room["checkpoint"] != "":
|
||||
room_str += f", \"{room['checkpoint']}\", \"{room_full_name}_{room['checkpoint_region']}\""
|
||||
room_str += "),"
|
||||
|
||||
all_rooms.append(room_str)
|
||||
|
||||
# Regions
|
||||
for region in room["regions"]:
|
||||
region_full_name = f"{room_full_name}_{region['name']}"
|
||||
|
||||
region_str = (f" \"{region_full_name}\": PreRegion(\"{region_full_name}\", "
|
||||
f"\"{room_full_name}\", "
|
||||
f"[reg_con for _, reg_con in all_region_connections.items() if reg_con.source_name == \"{region_full_name}\"], "
|
||||
f"[loc for _, loc in all_locations.items() if loc.region_name == \"{region_full_name}\"]),"
|
||||
)
|
||||
|
||||
all_regions.append(region_str)
|
||||
|
||||
# Locations
|
||||
if "locations" in region:
|
||||
for location in region["locations"]:
|
||||
location_full_name = f"{room_full_name}_{location['name']}"
|
||||
|
||||
location_display_name = location['display_name']
|
||||
if (location['type'] == "strawberry" and location_display_name != "Moon Berry") or location['type'] == "binoculars" :
|
||||
location_display_name = f"Room {room['name']} {location_display_name}"
|
||||
location_full_display_name = f"{level['display_name']} - {location_display_name}"
|
||||
|
||||
location_str = (f" \"{location_full_name}\": LevelLocation(\"{location_full_name}\", "
|
||||
f"\"{location_full_display_name}\", \"{region_full_name}\", "
|
||||
f"LocationType.{location['type']}, ["
|
||||
)
|
||||
|
||||
if "rule" in location:
|
||||
for possible_access in location['rule']:
|
||||
location_str += f"["
|
||||
for item in possible_access:
|
||||
if "Key" in item or "Gem" in item:
|
||||
location_str += f"\"{level['display_name']} - {item}\", "
|
||||
else:
|
||||
location_str += f"ItemName.{item}, "
|
||||
location_str += f"], "
|
||||
elif "rules" in location:
|
||||
raise Exception(f"Location {location_full_name} uses 'rules' instead of 'rule")
|
||||
|
||||
location_str += "]),"
|
||||
|
||||
all_locations.append(location_str)
|
||||
|
||||
# Region Connections
|
||||
for reg_con in region["connections"]:
|
||||
dest_region_full_name = f"{room_full_name}_{reg_con['dest']}"
|
||||
reg_con_full_name = f"{region_full_name}---{dest_region_full_name}"
|
||||
|
||||
reg_con_str = f" \"{reg_con_full_name}\": RegionConnection(\"{region_full_name}\", \"{dest_region_full_name}\", ["
|
||||
|
||||
for possible_access in reg_con['rule']:
|
||||
reg_con_str += f"["
|
||||
for item in possible_access:
|
||||
if "Key" in item or "Gem" in item:
|
||||
reg_con_str += f"\"{level['display_name']} - {item}\", "
|
||||
else:
|
||||
reg_con_str += f"ItemName.{item}, "
|
||||
reg_con_str += f"], "
|
||||
|
||||
reg_con_str += "]),"
|
||||
|
||||
all_region_connections.append(reg_con_str)
|
||||
|
||||
for door in room["doors"]:
|
||||
door_full_name = f"{room_full_name}_{door['name']}"
|
||||
|
||||
door_str = (f" \"{door_full_name}\": Door(\"{door_full_name}\", "
|
||||
f"\"{room_full_name}\", "
|
||||
f"DoorDirection.{door['direction']}, "
|
||||
)
|
||||
|
||||
door_str += "True, " if door["blocked"] else "False, "
|
||||
door_str += "True)," if door["closes_behind"] else "False),"
|
||||
|
||||
all_doors.append(door_str)
|
||||
|
||||
all_regions.append("")
|
||||
all_region_connections.append("")
|
||||
all_doors.append("")
|
||||
|
||||
all_locations.append("")
|
||||
all_rooms.append("")
|
||||
|
||||
# Room Connections
|
||||
for room_con in level["room_connections"]:
|
||||
source_door_full_name = f"{level['name']}_{room_con['source_room']}_{room_con['source_door']}"
|
||||
dest_door_full_name = f"{level['name']}_{room_con['dest_room']}_{room_con['dest_door']}"
|
||||
|
||||
room_con_str = (f" \"{source_door_full_name}---{dest_door_full_name}\": RoomConnection(\"{level['name']}\", "
|
||||
f"all_doors[\"{source_door_full_name}\"], "
|
||||
f"all_doors[\"{dest_door_full_name}\"]),"
|
||||
)
|
||||
|
||||
all_room_connections.append(room_con_str)
|
||||
|
||||
all_room_connections.append("")
|
||||
|
||||
|
||||
all_levels.append("")
|
||||
|
||||
|
||||
import sys
|
||||
out_file = open("CelesteLevelData.py", "w")
|
||||
sys.stdout = out_file
|
||||
|
||||
print("# THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MANUALLY EDIT.")
|
||||
print("")
|
||||
print("from ..Levels import Level, Room, PreRegion, LevelLocation, RegionConnection, RoomConnection, Door, DoorDirection, LocationType")
|
||||
print("from ..Names import ItemName")
|
||||
print("")
|
||||
print("all_doors: dict[str, Door] = {")
|
||||
for line in all_doors:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_region_connections: dict[str, RegionConnection] = {")
|
||||
for line in all_region_connections:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_locations: dict[str, LevelLocation] = {")
|
||||
for line in all_locations:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_regions: dict[str, PreRegion] = {")
|
||||
for line in all_regions:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_room_connections: dict[str, RoomConnection] = {")
|
||||
for line in all_room_connections:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_rooms: dict[str, Room] = {")
|
||||
for line in all_rooms:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("all_levels: dict[str, Level] = {")
|
||||
for line in all_levels:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
|
||||
out_file.close()
|
||||
0
worlds/celeste_open_world/data/__init__.py
Normal file
0
worlds/celeste_open_world/data/__init__.py
Normal file
98
worlds/celeste_open_world/docs/en_Celeste (Open World).md
Normal file
98
worlds/celeste_open_world/docs/en_Celeste (Open World).md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Celeste Open World
|
||||
|
||||
## What is this game?
|
||||
|
||||
**Celeste (Open World)** is a Randomizer for the original Celeste. In this acclaimed platformer created by ExOK Games, you control Madeline as she attempts to climb the titular mountain, meeting friends and obstacles along the way.
|
||||
This randomizer takes an "Open World" approach. All of your active areas are open to you from the start. Progression is found in unlocking the ability to interact with various objects in the areas, such as springs, traffic blocks, feathers, and many more. One area can be selected as your "Goal Area", requiring you to clear that area before you can access the Epilogue and finish the game. Additionally, you can be required to receive a customizable amount of `Strawberry` items to access the Epilogue and optionally to access your Goal Area as well.
|
||||
There are a variety of progression, location, and aesthetic options available. Please be safe on the climb.
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
By default, the Prologue, the A-Side levels for Chapters 1-7, and the Epilogue are included in the randomizer. Using options, B- and C-Sides can also be included, as can the Core and Farewell chapters. One level is chosen via an option to be the "Goal Area". Obtaining the required amount of Strawberry items from the multiworld and clearing this Goal Area will grant access to the Epilogue and the Credits, which is the goal of the randomizer.
|
||||
|
||||
## What items get shuffled?
|
||||
|
||||
The main collectable in this game is Strawberries, which you must collect to complete the game.
|
||||
|
||||
16 Crystal Heart items are included as filler items (Heart Gates are disabled in this mod). Any additional space in the item pool is filled by Raspberries, which do nothing, and Traps.
|
||||
|
||||
The following interactable items are included in the item pool, so long as any active level includes them:
|
||||
- Springs
|
||||
- Dash Refills
|
||||
- Traffic Blocks
|
||||
- Pink Cassette Blocks
|
||||
- Blue Cassette Blocks
|
||||
- Dream Blocks
|
||||
- Coins
|
||||
- Strawberry Seeds
|
||||
- Sinking Platforms
|
||||
- Moving Platforms
|
||||
- Blue Clouds
|
||||
- Pink Clouds
|
||||
- Blue Boosters
|
||||
- Red Boosters
|
||||
- Move Blocks
|
||||
- White Block
|
||||
- Swap Blocks
|
||||
- Dash Switches
|
||||
- Torches
|
||||
- Theo Crystal
|
||||
- Feathers
|
||||
- Bumpers
|
||||
- Kevins
|
||||
- Badeline Boosters
|
||||
- Fire and Ice Balls
|
||||
- Core Toggles
|
||||
- Core Blocks
|
||||
- Pufferfish
|
||||
- Jellyfish
|
||||
- Double Dash Refills
|
||||
- Breaker Boxes
|
||||
- Yellow Cassette Blocks
|
||||
- Green Cassette Blocks
|
||||
- Bird
|
||||
|
||||
Additionally, the following items can optionally be included in the Item Pool:
|
||||
- Keys
|
||||
- Checkpoints
|
||||
- Summit Gems
|
||||
- One Cassette per active level
|
||||
|
||||
Finally, the following Traps can be optionally included in the Item Pool:
|
||||
- Bald Trap
|
||||
- Literature Trap
|
||||
- Stun Trap
|
||||
- Invisible Trap
|
||||
- Fast Trap
|
||||
- Slow Trap
|
||||
- Ice Trap
|
||||
- Reverse Trap
|
||||
- Screen Flip Trap
|
||||
- Laughter Trap
|
||||
- Hiccup Trap
|
||||
- Zoom Trap
|
||||
|
||||
## What locations get shuffled?
|
||||
|
||||
By default, the locations in Celeste (Open World) which can contain items are:
|
||||
- Level Clears
|
||||
- Strawberries
|
||||
- Crystal Hearts
|
||||
- Cassettes
|
||||
|
||||
Additionally, the following locations can optionally be included in the Location Pool:
|
||||
- Golden Strawberries
|
||||
- Keys
|
||||
- Checkpoints
|
||||
- Summit Gems
|
||||
- Cars
|
||||
- Binoculars
|
||||
- Rooms
|
||||
|
||||
## How can I get started?
|
||||
|
||||
To get started playing Celeste (Open World) in Archipelago, [go to the setup guide for this game](../../../tutorial/Celeste%20(Open%20World)/guide/en)
|
||||
20
worlds/celeste_open_world/docs/guide_en.md
Normal file
20
worlds/celeste_open_world/docs/guide_en.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Celeste (Open World) Setup Guide
|
||||
|
||||
## Required Software
|
||||
- The latest version of Celeste (1.4) from any official PC game distributor
|
||||
- Olympus (Celeste Mod Manager) from: [Olympus Download Page](https://everestapi.github.io/)
|
||||
- The latest version of the Archipelago Open World mod for Celeste from: [GitHub Release](https://github.com/PoryGoneDev/Celeste-Archipelago-Open-World/releases)
|
||||
|
||||
## Installation Procedures (Windows/Linux)
|
||||
|
||||
1. Install the latest version of Celeste (v1.4) on PC
|
||||
2. Install `Olympus` (mod manager/launcher) and `Everest` (mod loader) per its instructions: [Olympus Setup Instructions](https://everestapi.github.io/)
|
||||
3. Place the `Archipelago_Open_World.zip` from the GitHub release into the `mods` folder in your Celeste install
|
||||
4. (Recommended) From the main menu, enter `Mod Options` and set `Debug Mode` to `Everest` or `Always`. This will give you access to a rudimentary Text Client which can be toggled with the `~` key.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Load Everest from the Olympus Launcher with the Archipelago Open World mod enabled
|
||||
2. Enter the Connection Menu via the `Connect` button on the main menu
|
||||
3. Use the keyboard to enter your connection information, then press the Connect button
|
||||
4. Once connected, you can use the Debug Menu (opened with `~`) as a Text Client, by typing "`!ap `" followed by what you would normally enter into a Text Client
|
||||
Reference in New Issue
Block a user