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:
PoryGone
2025-08-31 17:31:09 -04:00
committed by GitHub
parent cdf7165ab4
commit c753fbff2d
16 changed files with 53225 additions and 0 deletions

View File

@@ -81,6 +81,7 @@ Currently, the following games are supported:
* Super Mario Land 2: 6 Golden Coins * Super Mario Land 2: 6 Golden Coins
* shapez * shapez
* Paint * Paint
* Celeste (Open World)
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -42,6 +42,9 @@
# Celeste 64 # Celeste 64
/worlds/celeste64/ @PoryGone /worlds/celeste64/ @PoryGone
# Celeste (Open World)
/worlds/celeste_open_world/ @PoryGone
# ChecksFinder # ChecksFinder
/worlds/checksfinder/ @SunCatMC /worlds/checksfinder/ @SunCatMC

View 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

View 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

View 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

View 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

View 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"

View 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

View 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])

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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()

View 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)

View 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