From db30a0116e66abdef37d0c83fadba8a194c4f526 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:55:56 -0500 Subject: [PATCH] Celeste 64: Implement New Game (#2798) Co-authored-by: chandler05 <66492208+chandler05@users.noreply.github.com> Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Zach Parks --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/celeste64/Items.py | 58 ++++++++++ worlds/celeste64/Locations.py | 142 +++++++++++++++++++++++++ worlds/celeste64/Names/ItemName.py | 11 ++ worlds/celeste64/Names/LocationName.py | 31 ++++++ worlds/celeste64/Names/__init__.py | 0 worlds/celeste64/Options.py | 25 +++++ worlds/celeste64/Regions.py | 11 ++ worlds/celeste64/Rules.py | 104 ++++++++++++++++++ worlds/celeste64/__init__.py | 92 ++++++++++++++++ worlds/celeste64/docs/en_Celeste 64.md | 24 +++++ worlds/celeste64/docs/guide_en.md | 32 ++++++ 13 files changed, 534 insertions(+) create mode 100644 worlds/celeste64/Items.py create mode 100644 worlds/celeste64/Locations.py create mode 100644 worlds/celeste64/Names/ItemName.py create mode 100644 worlds/celeste64/Names/LocationName.py create mode 100644 worlds/celeste64/Names/__init__.py create mode 100644 worlds/celeste64/Options.py create mode 100644 worlds/celeste64/Regions.py create mode 100644 worlds/celeste64/Rules.py create mode 100644 worlds/celeste64/__init__.py create mode 100644 worlds/celeste64/docs/en_Celeste 64.md create mode 100644 worlds/celeste64/docs/guide_en.md diff --git a/README.md b/README.md index 4a3c53548c38..3c3c41475bab 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Currently, the following games are supported: * Final Fantasy Mystic Quest * TUNIC * Kirby's Dream Land 3 +* Celeste 64 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 diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 6ec3802edea6..d6730b7308ae 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -28,6 +28,9 @@ # Bumper Stickers /worlds/bumpstik/ @FelicitusNeko +# Celeste 64 +/worlds/celeste64/ @PoryGone + # ChecksFinder /worlds/checksfinder/ @jonloveslegos diff --git a/worlds/celeste64/Items.py b/worlds/celeste64/Items.py new file mode 100644 index 000000000000..94db0e8ef4d2 --- /dev/null +++ b/worlds/celeste64/Items.py @@ -0,0 +1,58 @@ +from typing import Dict, NamedTuple, Optional + +from BaseClasses import Item, ItemClassification +from .Names import ItemName + + +celeste_64_base_id: int = 0xCA0000 + + +class Celeste64Item(Item): + game = "Celeste 64" + + +class Celeste64ItemData(NamedTuple): + code: Optional[int] = None + type: ItemClassification = ItemClassification.filler + + +item_data_table: Dict[str, Celeste64ItemData] = { + ItemName.strawberry: Celeste64ItemData( + code = celeste_64_base_id + 0, + type=ItemClassification.progression_skip_balancing, + ), + ItemName.dash_refill: Celeste64ItemData( + code = celeste_64_base_id + 1, + type=ItemClassification.progression, + ), + ItemName.double_dash_refill: Celeste64ItemData( + code = celeste_64_base_id + 2, + type=ItemClassification.progression, + ), + ItemName.feather: Celeste64ItemData( + code = celeste_64_base_id + 3, + type=ItemClassification.progression, + ), + ItemName.coin: Celeste64ItemData( + code = celeste_64_base_id + 4, + type=ItemClassification.progression, + ), + ItemName.cassette: Celeste64ItemData( + code = celeste_64_base_id + 5, + type=ItemClassification.progression, + ), + ItemName.traffic_block: Celeste64ItemData( + code = celeste_64_base_id + 6, + type=ItemClassification.progression, + ), + ItemName.spring: Celeste64ItemData( + code = celeste_64_base_id + 7, + type=ItemClassification.progression, + ), + ItemName.breakables: Celeste64ItemData( + code = celeste_64_base_id + 8, + type=ItemClassification.progression, + ) +} + +item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None} diff --git a/worlds/celeste64/Locations.py b/worlds/celeste64/Locations.py new file mode 100644 index 000000000000..92ca425f8383 --- /dev/null +++ b/worlds/celeste64/Locations.py @@ -0,0 +1,142 @@ +from typing import Dict, NamedTuple, Optional + +from BaseClasses import Location +from .Names import LocationName + + +celeste_64_base_id: int = 0xCA0000 + + +class Celeste64Location(Location): + game = "Celeste 64" + + +class Celeste64LocationData(NamedTuple): + region: str + address: Optional[int] = None + + +location_data_table: Dict[str, Celeste64LocationData] = { + LocationName.strawberry_1 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 0, + ), + LocationName.strawberry_2 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 1, + ), + LocationName.strawberry_3 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 2, + ), + LocationName.strawberry_4 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 3, + ), + LocationName.strawberry_5 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 4, + ), + LocationName.strawberry_6 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 5, + ), + LocationName.strawberry_7 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 6, + ), + LocationName.strawberry_8 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 7, + ), + LocationName.strawberry_9 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 8, + ), + LocationName.strawberry_10 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 9, + ), + LocationName.strawberry_11 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 10, + ), + LocationName.strawberry_12 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 11, + ), + LocationName.strawberry_13 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 12, + ), + LocationName.strawberry_14 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 13, + ), + LocationName.strawberry_15 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 14, + ), + LocationName.strawberry_16 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 15, + ), + LocationName.strawberry_17 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 16, + ), + LocationName.strawberry_18 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 17, + ), + LocationName.strawberry_19 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 18, + ), + LocationName.strawberry_20 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 19, + ), + LocationName.strawberry_21 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 20, + ), + LocationName.strawberry_22 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 21, + ), + LocationName.strawberry_23 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 22, + ), + LocationName.strawberry_24 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 23, + ), + LocationName.strawberry_25 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 24, + ), + LocationName.strawberry_26 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 25, + ), + LocationName.strawberry_27 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 26, + ), + LocationName.strawberry_28 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 27, + ), + LocationName.strawberry_29 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 28, + ), + LocationName.strawberry_30 : Celeste64LocationData( + region = "Forsaken City", + address = celeste_64_base_id + 29, + ) +} + +location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None} diff --git a/worlds/celeste64/Names/ItemName.py b/worlds/celeste64/Names/ItemName.py new file mode 100644 index 000000000000..5e4daf8e4f2a --- /dev/null +++ b/worlds/celeste64/Names/ItemName.py @@ -0,0 +1,11 @@ +strawberry = "Strawberry" + +dash_refill = "Dash Refills" +double_dash_refill = "Double Dash Refills" +feather = "Feathers" +coin = "Coins" +cassette = "Cassettes" + +traffic_block = "Traffic Blocks" +spring = "Springs" +breakables = "Breakable Blocks" diff --git a/worlds/celeste64/Names/LocationName.py b/worlds/celeste64/Names/LocationName.py new file mode 100644 index 000000000000..a9902f70f7ab --- /dev/null +++ b/worlds/celeste64/Names/LocationName.py @@ -0,0 +1,31 @@ +# Strawberry Locations +strawberry_1 = "First Strawberry" +strawberry_2 = "Floating Blocks Strawberry" +strawberry_3 = "South-East Tower Top Strawberry" +strawberry_4 = "Theo Strawberry" +strawberry_5 = "Fall Through Spike Floor Strawberry" +strawberry_6 = "Troll Strawberry" +strawberry_7 = "Falling Blocks Strawberry" +strawberry_8 = "Traffic Block Strawberry" +strawberry_9 = "South-West Dash Refills Strawberry" +strawberry_10 = "South-East Tower Side Strawberry" +strawberry_11 = "Girders Strawberry" +strawberry_12 = "North-East Tower Bottom Strawberry" +strawberry_13 = "Breakable Blocks Strawberry" +strawberry_14 = "Feather Maze Strawberry" +strawberry_15 = "Feather Chain Strawberry" +strawberry_16 = "Feather Hidden Strawberry" +strawberry_17 = "Double Dash Puzzle Strawberry" +strawberry_18 = "Double Dash Spike Climb Strawberry" +strawberry_19 = "Double Dash Spring Strawberry" +strawberry_20 = "North-East Tower Breakable Bottom Strawberry" +strawberry_21 = "Theo Tower Lower Cassette Strawberry" +strawberry_22 = "Theo Tower Upper Cassette Strawberry" +strawberry_23 = "South End of Bridge Cassette Strawberry" +strawberry_24 = "You Are Ready Cassette Strawberry" +strawberry_25 = "Cassette Hidden in the House Strawberry" +strawberry_26 = "North End of Bridge Cassette Strawberry" +strawberry_27 = "Distant Feather Cassette Strawberry" +strawberry_28 = "Feather Arches Cassette Strawberry" +strawberry_29 = "North-East Tower Cassette Strawberry" +strawberry_30 = "Badeline Cassette Strawberry" diff --git a/worlds/celeste64/Names/__init__.py b/worlds/celeste64/Names/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/celeste64/Options.py b/worlds/celeste64/Options.py new file mode 100644 index 000000000000..f94fbb02931f --- /dev/null +++ b/worlds/celeste64/Options.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass + +from Options import Range, DeathLink, PerGameCommonOptions + + +class StrawberriesRequired(Range): + """How many Strawberries you must receive to finish""" + display_name = "Strawberries Required" + range_start = 0 + range_end = 20 + default = 15 + +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 + + +@dataclass +class Celeste64Options(PerGameCommonOptions): + death_link: DeathLink + death_link_amnesty: DeathLinkAmnesty + strawberries_required: StrawberriesRequired diff --git a/worlds/celeste64/Regions.py b/worlds/celeste64/Regions.py new file mode 100644 index 000000000000..6f01c873a4f9 --- /dev/null +++ b/worlds/celeste64/Regions.py @@ -0,0 +1,11 @@ +from typing import Dict, List, NamedTuple + + +class Celeste64RegionData(NamedTuple): + connecting_regions: List[str] = [] + + +region_data_table: Dict[str, Celeste64RegionData] = { + "Menu": Celeste64RegionData(["Forsaken City"]), + "Forsaken City": Celeste64RegionData(), +} diff --git a/worlds/celeste64/Rules.py b/worlds/celeste64/Rules.py new file mode 100644 index 000000000000..3baa231892ad --- /dev/null +++ b/worlds/celeste64/Rules.py @@ -0,0 +1,104 @@ +from worlds.generic.Rules import set_rule + +from . import Celeste64World +from .Names import ItemName, LocationName + + +def set_rules(world: Celeste64World): + set_rule(world.multiworld.get_location(LocationName.strawberry_4, world.player), + lambda state: state.has_all({ItemName.traffic_block, + ItemName.breakables}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_5, world.player), + lambda state: state.has(ItemName.breakables, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_6, world.player), + lambda state: state.has(ItemName.dash_refill, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_8, world.player), + lambda state: state.has(ItemName.traffic_block, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_9, world.player), + lambda state: state.has(ItemName.dash_refill, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_11, world.player), + lambda state: state.has(ItemName.dash_refill, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_12, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.double_dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_13, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.breakables}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_14, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.feather}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_15, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.feather}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_16, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.feather}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_17, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.double_dash_refill, + ItemName.feather, + ItemName.traffic_block}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_18, world.player), + lambda state: state.has(ItemName.double_dash_refill, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_19, world.player), + lambda state: state.has_all({ItemName.double_dash_refill, + ItemName.spring}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_20, world.player), + lambda state: state.has_all({ItemName.dash_refill, + ItemName.feather, + ItemName.breakables}, world.player)) + + set_rule(world.multiworld.get_location(LocationName.strawberry_21, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.traffic_block, + ItemName.breakables}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_22, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.dash_refill, + ItemName.breakables}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_23, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.dash_refill, + ItemName.coin}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_24, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.traffic_block, + ItemName.dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_25, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.dash_refill, + ItemName.double_dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_26, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_27, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.feather, + ItemName.coin, + ItemName.dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_28, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.feather, + ItemName.coin, + ItemName.dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_29, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.feather, + ItemName.coin, + ItemName.dash_refill}, world.player)) + set_rule(world.multiworld.get_location(LocationName.strawberry_30, world.player), + lambda state: state.has_all({ItemName.cassette, + ItemName.feather, + ItemName.traffic_block, + ItemName.spring, + ItemName.breakables, + ItemName.dash_refill, + ItemName.double_dash_refill}, world.player)) + + # Completion condition. + world.multiworld.completion_condition[world.player] = lambda state: (state.has(ItemName.strawberry,world.player,world.options.strawberries_required.value) and + state.has_all({ItemName.feather, + ItemName.traffic_block, + ItemName.breakables, + ItemName.dash_refill, + ItemName.double_dash_refill}, world.player)) diff --git a/worlds/celeste64/__init__.py b/worlds/celeste64/__init__.py new file mode 100644 index 000000000000..0d3b5d015829 --- /dev/null +++ b/worlds/celeste64/__init__.py @@ -0,0 +1,92 @@ +from typing import List + +from BaseClasses import ItemClassification, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from .Items import Celeste64Item, item_data_table, item_table +from .Locations import Celeste64Location, location_data_table, location_table +from .Names import ItemName +from .Options import Celeste64Options + + +class Celeste64WebWorld(WebWorld): + theme = "ice" + + setup_en = Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Celeste 64 in Archipelago.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["PoryGone"] + ) + + tutorials = [setup_en] + + +class Celeste64World(World): + """Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer. + Created in a week(ish) by the Celeste team to celebrate the game’s sixth anniversary 🍓✨""" + + game = "Celeste 64" + web = Celeste64WebWorld() + options_dataclass = Celeste64Options + options: Celeste64Options + location_name_to_id = location_table + item_name_to_id = item_table + + + def create_item(self, name: str) -> Celeste64Item: + # Only make required amount of strawberries be Progression + if getattr(self, "options", None) and name == ItemName.strawberry: + classification: ItemClassification = ItemClassification.filler + self.prog_strawberries = getattr(self, "prog_strawberries", 0) + if self.prog_strawberries < self.options.strawberries_required.value: + classification = ItemClassification.progression_skip_balancing + self.prog_strawberries += 1 + + return Celeste64Item(name, classification, item_data_table[name].code, self.player) + else: + return Celeste64Item(name, item_data_table[name].type, item_data_table[name].code, self.player) + + def create_items(self) -> None: + item_pool: List[Celeste64Item] = [] + + item_pool += [self.create_item(name) for name in item_data_table.keys()] + + item_pool += [self.create_item(ItemName.strawberry) for _ in range(21)] + + self.multiworld.itempool += item_pool + + + def create_regions(self) -> None: + from .Regions import region_data_table + # Create regions. + for region_name in region_data_table.keys(): + region = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(region) + + # Create locations. + for region_name, region_data in region_data_table.items(): + region = self.multiworld.get_region(region_name, self.player) + region.add_locations({ + location_name: location_data.address for location_name, location_data in location_data_table.items() + if location_data.region == region_name + }, Celeste64Location) + region.add_exits(region_data_table[region_name].connecting_regions) + + + def get_filler_item_name(self) -> str: + return ItemName.strawberry + + + def set_rules(self) -> None: + from .Rules import set_rules + set_rules(self) + + + def fill_slot_data(self): + return { + "death_link": self.options.death_link.value, + "death_link_amnesty": self.options.death_link_amnesty.value, + "strawberries_required": self.options.strawberries_required.value + } diff --git a/worlds/celeste64/docs/en_Celeste 64.md b/worlds/celeste64/docs/en_Celeste 64.md new file mode 100644 index 000000000000..efc42bfe56eb --- /dev/null +++ b/worlds/celeste64/docs/en_Celeste 64.md @@ -0,0 +1,24 @@ +# Celeste 64 + +## What is this game? + +Relive the magic of Celeste Mountain alongside Madeline in this small, heartfelt 3D platformer. +Created in a week(ish) by the Celeste team to celebrate the game's sixth anniversary. + +Ported to Archipelago in a week(ish) by PoryGone, this World provides the following as unlockable items: +- Strawberries +- Dash Refills +- Double Dash Refills +- Feathers +- Coins +- Cassettes +- Traffic Blocks +- Springs +- Breakable Blocks + +The goal is to collect a certain number of Strawberries, then visit Badeline on her floating island. + +## 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. diff --git a/worlds/celeste64/docs/guide_en.md b/worlds/celeste64/docs/guide_en.md new file mode 100644 index 000000000000..116a3b13e91b --- /dev/null +++ b/worlds/celeste64/docs/guide_en.md @@ -0,0 +1,32 @@ +# Celeste 64 Setup Guide + +## Required Software +- Archipelago Build of Celeste 64 from: [Celeste 64 Archipelago Releases Page](https://github.com/PoryGoneDev/Celeste64/releases/) + +## Installation Procedures (Windows) + +1. Download the above release and extract it. + +## Joining a MultiWorld Game + +1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install. + +2. For the `Url` field, enter the address of the server, such as `archipelago.gg:38281`. Your server host should be able to tell you this. + +3. For the `SlotName` field, enter your "name" field from the yaml or website config. + +4. For the `Password` field, enter the server password if one exists; otherwise leave this field blank. + +5. Save the file, and run `Celeste64.exe`. If you can continue past the title screen, then you are successfully connected. + +An Example `AP.json` file: + +``` +{ + "Url": "archipelago:12345", + "SlotName": "Maddy", + "Password": "" +} +``` + +