diff --git a/README.md b/README.md index 135059b726b2..9f4c0858f3c5 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Currently, the following games are supported: * DLC Quest * Noita * Undertale +* Bumper Stickers 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/worlds/bumpstik/Items.py b/worlds/bumpstik/Items.py new file mode 100644 index 000000000000..c714b7432027 --- /dev/null +++ b/worlds/bumpstik/Items.py @@ -0,0 +1,129 @@ +# Copyright (c) 2022 FelicitusNeko +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +import typing + +from BaseClasses import Item, ItemClassification +from worlds.alttp import ALTTPWorld + + +class BumpStikLttPText(typing.NamedTuple): + pedestal: typing.Optional[str] + sickkid: typing.Optional[str] + magicshop: typing.Optional[str] + zora: typing.Optional[str] + fluteboy: typing.Optional[str] + + +LttPCreditsText = { + "Nothing": BumpStikLttPText("blank space", + "Forgot it at home again", + "Hallucinating again", + "Bucket o' Nothing for 9999.99", + "King Nothing"), + "Score Bonus": BumpStikLttPText("helpful hand", + "Busy kid gets the point...s", + "Variable conversion rate", + "Stonks", + "Catchy ad jingle"), + "Task Advance": BumpStikLttPText("hall pass", + "Faker kid skips again", + "I know a way around it", + "Money can fix it", + "Quick! A distraction"), + "Starting Turner": BumpStikLttPText("fidget spinner", + "Spinning kid turns heads", + "This turns things around", + "Your turn to turn", + "Turn turn turn"), + "Reserved": BumpStikLttPText("... wuh?", + "Why's this here?", + "Why's this here?", + "Why's this here?", + "Why's this here?"), + "Starting Paint Can": BumpStikLttPText("paint bucket", + "Artsy kid paints again", + "Your rainbow destiny", + "Rainbow for sale", + "Let me paint a picture"), + "Booster Bumper": BumpStikLttPText("multiplier", + "Math kid multiplies again", + "Growing shrooms", + "Investment opportunity", + "In harmony with themself"), + "Hazard Bumper": BumpStikLttPText("dull stone", + "...I got better", + "Mischief Maker", + "Whoops for sale", + "Stuck in a moment"), + "Treasure Bumper": BumpStikLttPText("odd treasure box", + "Interdimensional treasure", + "Shrooms for ???", + "Who knows what this is", + "You get what you give"), + "Rainbow Trap": BumpStikLttPText("chaos prism", + "Roy G Biv in disguise", + "The colors Duke! The colors", + "Paint overstock", + "Raise a little hell"), + "Spinner Trap": BumpStikLttPText("whirlwind", + "Vertigo kid gets dizzy", + "The room is spinning Dave", + "International sabotage", + "You spin me right round"), + "Killer Trap": BumpStikLttPText("broken board", + "Thank you Mr Coffey", + "Lethal dosage", + "Assassin for hire", + "Killer Queen"), +} + + +item_groups = { + "Helpers": ["Task Advance", "Starting Turner", "Starting Paint Can"], + "Targets": ["Treasure Bumper", "Booster Bumper", "Hazard Bumper"], + "Traps": ["Rainbow Trap", "Spinner Trap", "Killer Trap"] +} + + +class BumpStikItem(Item): + game = "Bumper Stickers" + type: str + + def __init__(self, name, classification, code, player): + super(BumpStikItem, self).__init__( + name, classification, code, player) + + if code is None: + self.type = "Event" + elif name in item_groups["Traps"]: + self.type = "Trap" + self.classification = ItemClassification.trap + elif name in item_groups["Targets"]: + self.type = "Target" + self.classification = ItemClassification.progression + elif name in item_groups["Helpers"]: + self.type = "Helper" + self.classification = ItemClassification.useful + else: + self.type = "Other" + + +offset = 595_000 + +item_table = { + item: offset + x for x, item in enumerate(LttPCreditsText.keys()) +} + +ALTTPWorld.pedestal_credit_texts.update({item_table[name]: f"and the {texts.pedestal}" + for name, texts in LttPCreditsText.items()}) +ALTTPWorld.sickkid_credit_texts.update( + {item_table[name]: texts.sickkid for name, texts in LttPCreditsText.items()}) +ALTTPWorld.magicshop_credit_texts.update( + {item_table[name]: texts.magicshop for name, texts in LttPCreditsText.items()}) +ALTTPWorld.zora_credit_texts.update( + {item_table[name]: texts.zora for name, texts in LttPCreditsText.items()}) +ALTTPWorld.fluteboy_credit_texts.update( + {item_table[name]: texts.fluteboy for name, texts in LttPCreditsText.items()}) diff --git a/worlds/bumpstik/Locations.py b/worlds/bumpstik/Locations.py new file mode 100644 index 000000000000..919e167f5086 --- /dev/null +++ b/worlds/bumpstik/Locations.py @@ -0,0 +1,49 @@ +# Copyright (c) 2022 FelicitusNeko +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +from BaseClasses import Location + + +class BumpStikLocation(Location): + game = "Bumper Stickers" + + +offset = 595_000 + +level1_locs = [f"{(i + 1) * 250} Points" for i in range(4)] + \ + [f"{(i + 1) * 500} Level Points" for i in range(4)] + \ + [f"{(i + 1) * 25} Level Bumpers" for i in range(3)] + \ + ["Combo 5"] + +level2_locs = [f"{(i + 1) * 500} Points" for i in range(4)] + \ + [f"{(i + 1) * 1000} Level Points" for i in range(4)] + \ + [f"{(i + 1) * 25} Level Bumpers" for i in range(4)] + \ + ["Combo 5"] + ["Chain x2"] + +level3_locs = [f"{(i + 1) * 800} Points" for i in range(4)] + \ + [f"{(i + 1) * 2000} Level Points" for i in range(4)] + \ + [f"{(i + 1) * 25} Level Bumpers" for i in range(5)] + \ + ["Combo 5", "Combo 7"] + ["Chain x2"] + \ + ["All Clear, 3 colors"] + +level4_locs = [f"{(i + 1) * 1500} Points" for i in range(4)] + \ + [f"{(i + 1) * 3000} Level Points" for i in range(4)] + \ + [f"{(i + 1) * 25} Level Bumpers" for i in range(6)] + \ + ["Combo 5", "Combo 7"] + ["Chain x2", "Chain x3"] + +level5_locs = ["50,000+ Total Points", "Cleared all Hazards"] + +for x, loc_list in enumerate([level1_locs, level2_locs, level3_locs, level4_locs, level5_locs]): + for y, loc in enumerate(loc_list): + loc_list[y] = f"Level {x + 1} - {loc}" + +extra_locs = [f"Bonus Booster {i+1}" for i in range(5)] + \ + [f"Treasure Bumper {i+1}" for i in range(32)] + +all_locs = level1_locs + level2_locs + level3_locs + level4_locs + level5_locs + extra_locs + +location_table = { + loc: offset + i for i, loc in enumerate(all_locs) +} diff --git a/worlds/bumpstik/Options.py b/worlds/bumpstik/Options.py new file mode 100644 index 000000000000..021f10af2016 --- /dev/null +++ b/worlds/bumpstik/Options.py @@ -0,0 +1,80 @@ +# Copyright (c) 2022 FelicitusNeko +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +import typing +from Options import Option, Range + + +class TaskAdvances(Range): + """Task Advances allow you to skip one step of a level task. They do not restock, so use them sparingly.""" + display_name = "Task Advances" + range_start = 0 + range_end = 5 + default = 4 + + +class Turners(Range): + """Turners allow you to change the direction of a Bumper. These restock when the board resets.""" + display_name = "Turners" + range_start = 0 + range_end = 5 + default = 3 + + +class PaintCans(Range): + """ + Paint Cans allow you to change the color of a Bumper. + The ones you get from the multiworld restock when the board resets; you also get one-time ones from score. + """ + display_name = "Paint Cans" + range_start = 0 + range_end = 5 + default = 3 + + +class Traps(Range): + """ + Traps affect the board in various ways. + This number indicates how many total traps will be added to the item pool. + """ + display_name = "Trap Count" + range_start = 0 + range_end = 15 + default = 5 + + +class RainbowTrapWeight(Range): + """Rainbow Traps change the color of every bumper on the field.""" + display_name = "Rainbow Trap weight" + range_start = 0 + range_end = 100 + default = 50 + + +class SpinnerTrapWeight(Range): + """Spinner Traps change the direction of every bumper on the field.""" + display_name = "Spinner Trap weight" + range_start = 0 + range_end = 100 + default = 50 + + +class KillerTrapWeight(Range): + """Killer Traps end the current board immediately.""" + display_name = "Killer Trap weight" + range_start = 0 + range_end = 100 + default = 0 + + +bumpstik_options: typing.Dict[str, type(Option)] = { + "task_advances": TaskAdvances, + "turners": Turners, + "paint_cans": PaintCans, + "trap_count": Traps, + "rainbow_trap_weight": RainbowTrapWeight, + "spinner_trap_weight": SpinnerTrapWeight, + "killer_trap_weight": KillerTrapWeight +} diff --git a/worlds/bumpstik/Regions.py b/worlds/bumpstik/Regions.py new file mode 100644 index 000000000000..247d6d61a34b --- /dev/null +++ b/worlds/bumpstik/Regions.py @@ -0,0 +1,50 @@ +# Copyright (c) 2022 FelicitusNeko +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +from BaseClasses import MultiWorld, Region, Entrance +from .Locations import BumpStikLocation, level1_locs, level2_locs, level3_locs, level4_locs, level5_locs, location_table + + +def _generate_entrances(player: int, entrance_list: [str], parent: Region): + return [Entrance(player, entrance, parent) for entrance in entrance_list] + + +def create_regions(world: MultiWorld, player: int): + region_map = { + "Menu": level1_locs + ["Bonus Booster 1"] + [f"Treasure Bumper {i + 1}" for i in range(8)], + "Level 1": level2_locs + ["Bonus Booster 2"] + [f"Treasure Bumper {i + 9}" for i in range(8)], + "Level 2": level3_locs + ["Bonus Booster 3"] + [f"Treasure Bumper {i + 17}" for i in range(8)], + "Level 3": level4_locs + [f"Bonus Booster {i + 4}" for i in range(2)] + + [f"Treasure Bumper {i + 25}" for i in range(8)], + "Level 4": level5_locs + } + + entrance_map = { + "Level 1": lambda state: + state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9), + "Level 2": lambda state: + state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17), + "Level 3": lambda state: + state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25), + "Level 4": lambda state: + state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33) + } + + for x, region_name in enumerate(region_map): + region_list = region_map[region_name] + region = Region(region_name, player, world) + for location_name in region_list: + region.locations += [BumpStikLocation( + player, location_name, location_table[location_name], region)] + if x < 4: + region.exits += _generate_entrances(player, + [f"To Level {x + 1}"], region) + + world.regions += [region] + + for entrance in entrance_map: + connection = world.get_entrance(f"To {entrance}", player) + connection.access_rule = entrance_map[entrance] + connection.connect(world.get_region(entrance, player)) diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py new file mode 100644 index 000000000000..9eeb3325e38f --- /dev/null +++ b/worlds/bumpstik/__init__.py @@ -0,0 +1,128 @@ +# Copyright (c) 2022 FelicitusNeko +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from .Items import BumpStikItem, item_table, item_groups +from .Locations import location_table +from .Options import * +from .Regions import create_regions +from worlds.AutoWorld import World, WebWorld +from worlds.generic.Rules import forbid_item + + +class BumpStikWeb(WebWorld): + tutorials = [Tutorial( + "Bumper Stickers Setup Tutorial", + "A guide to setting up the Archipelago Bumper Stickers software on your computer.", + "English", + "setup_en.md", + "setup/en", + ["KewlioMZX"] + )] + theme = "stone" + bug_report_page = "https://github.com/FelicitusNeko/FlixelBumpStik/issues" + + +class BumpStikWorld(World): + """ + Bumper Stickers is a match-three puzzle game unlike any you've seen. + Launch Bumpers onto the field, and match them in sets of three of the same color. + How long can you go without getting Jammed? + """ + + game = "Bumper Stickers" + web = BumpStikWeb() + + item_name_to_id = item_table + location_name_to_id = location_table + item_name_groups = item_groups + + data_version = 1 + + required_client_version = (0, 3, 8) + + option_definitions = bumpstik_options + + def __init__(self, world: MultiWorld, player: int): + super(BumpStikWorld, self).__init__(world, player) + self.task_advances = TaskAdvances.default + self.turners = Turners.default + self.paint_cans = PaintCans.default + self.traps = Traps.default + self.rainbow_trap_weight = RainbowTrapWeight.default + self.spinner_trap_weight = SpinnerTrapWeight.default + self.killer_trap_weight = KillerTrapWeight.default + + def create_item(self, name: str) -> Item: + return BumpStikItem(name, ItemClassification.filler, item_table[name], self.player) + + def create_event(self, event: str) -> Item: + return BumpStikItem(event, ItemClassification.filler, None, self.player) + + def _create_item_in_quantities(self, name: str, qty: int) -> [Item]: + return [self.create_item(name) for _ in range(0, qty)] + + def _create_traps(self): + max_weight = self.rainbow_trap_weight + \ + self.spinner_trap_weight + self.killer_trap_weight + rainbow_threshold = self.rainbow_trap_weight + spinner_threshold = self.rainbow_trap_weight + self.spinner_trap_weight + trap_return = [0, 0, 0] + + for i in range(self.traps): + draw = self.multiworld.random.randrange(0, max_weight) + if draw < rainbow_threshold: + trap_return[0] += 1 + elif draw < spinner_threshold: + trap_return[1] += 1 + else: + trap_return[2] += 1 + + return trap_return + + def get_filler_item_name(self) -> str: + return "Nothing" + + def generate_early(self): + self.task_advances = self.multiworld.task_advances[self.player].value + self.turners = self.multiworld.turners[self.player].value + self.paint_cans = self.multiworld.paint_cans[self.player].value + self.traps = self.multiworld.trap_count[self.player].value + self.rainbow_trap_weight = self.multiworld.rainbow_trap_weight[self.player].value + self.spinner_trap_weight = self.multiworld.spinner_trap_weight[self.player].value + self.killer_trap_weight = self.multiworld.killer_trap_weight[self.player].value + + def create_regions(self): + create_regions(self.multiworld, self.player) + + def create_items(self): + frequencies = [ + 0, 0, self.task_advances, self.turners, 0, self.paint_cans, 5, 25, 33 + ] + self._create_traps() + item_pool = [] + + for i, name in enumerate(item_table): + if i < len(frequencies): + item_pool += self._create_item_in_quantities( + name, frequencies[i]) + + item_delta = len(location_table) - len(item_pool) - 1 + if item_delta > 0: + item_pool += self._create_item_in_quantities( + "Score Bonus", item_delta) + + self.multiworld.itempool += item_pool + + def set_rules(self): + forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player), + "Booster Bumper", self.player) + + def generate_basic(self): + self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item( + self.create_item(self.get_filler_item_name())) + + self.multiworld.completion_condition[self.player] = \ + lambda state: state.has("Booster Bumper", self.player, 5) and \ + state.has("Treasure Bumper", self.player, 32) diff --git a/worlds/bumpstik/docs/en_Bumper Stickers.md b/worlds/bumpstik/docs/en_Bumper Stickers.md new file mode 100644 index 000000000000..17a66d76122a --- /dev/null +++ b/worlds/bumpstik/docs/en_Bumper Stickers.md @@ -0,0 +1,34 @@ +# Bumper Stickers + +## Where is the settings page? +The [player settings page for Bumper Stickers](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? +Playing this in Archipelago is a very different experience from Classic mode. You start with a very small board and a set of tasks. Completing those tasks will give you a larger board and more, harder tasks. In addition, special types of bumpers exist that must be cleared in order to progress. + +## What is the goal of Bumper Stickers when randomized? +The goal is to complete all of the tasks for all five levels. + +## Which items can be in another player's world? +The main objects are: + - Treasure Bumpers, which are worth double points and send a check. + - Bonus Boosters, which permanently increase your score multiplier and send a check. + +Some utilities are also available: + - Paint Cans allow you to change the color of any bumper. Receiving a Starting Paint Can will give you one to use immediately, plus start you with one more when a new board starts. + - Turners allow you to change the direction of any bumper. Receiving a Starting Turner will give you one to use immediately, plus start you with one more when a new board starts. + - Task Skips allow you to skip one step of any level task. Careful; these do not replenish! + +There are also traps, if you want to enable them: + - Hazard Bumpers start popping up on Level 2. They cannot be cleared for five turns; after that, they remain immobile, but are colored and can be cleared, as well as painted. + - Rainbow Traps change the color of all bumpers on the field. + - Spinner Traps change the direction of all bumpers on the field. + - Killer Traps end your board immediately. + +The rest of checks are either score bonuses, or simply nothing. + +## What is considered a location check in Bumper Stickers? +Every step of a task completed sends a check. Every Treasure Bumper and Bonus Booster will also send a check, whether or not it completes a task. + +## When the player receives an item, what happens? +A notification will briefly appear at the bottom of the screen informing you of what you have received. diff --git a/worlds/bumpstik/docs/setup_en.md b/worlds/bumpstik/docs/setup_en.md new file mode 100644 index 000000000000..51334aa27701 --- /dev/null +++ b/worlds/bumpstik/docs/setup_en.md @@ -0,0 +1,59 @@ +## Required Software + +Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases). + +*A web version will be made available on itch.io at a later time.* + +## Installation Procedures + +Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like. + +- ⚠️ Do not extract Bumper Stickers to Program Files, as this will cause file access issues. + +## Joining a Multiworld Game + +1. Run `BumpStik-AP.exe`. +2. Select "Archipelago Mode". +3. Enter your server details in the fields provided, and click "Start". + - ※ If you are connecting to a WSS server (such as archipelago.gg), specify `wss://` in the host name. Otherwise, the game will assume `ws://`. + +## How to play Bumper Stickers (Classic) + +Here's a rundown of how to play a classic round of Bumper Stickers. +- You are presented with a 5×5 field, surrounded by Launchers. Your next Bumper to be played is seen at the bottom-right. +- Launch the Bumper onto the field by clicking on a Launcher. It will first move in the direction launched, then in the direction printed on the Bumper once it hits something. +- Line up three Bumpers of the same color, regardless of direction, to form a Bumper Sticker and clear them from the field. + - Sticking more than three in one move is a Combo, worth bonus points. + - After sticking Bumpers, any that are then able to move forward will do so. This can result in another Bumper Sticker being formed. This is a Chain, and is worth even more bonus points. +- You start with three colors. Sticking enough Bumpers will result in more colors being added to play, up to six. Each additional color adds a score multiplier. +- Clearing out the entire board results in an All Clear, which is worth big points! +- Getting 1000 points will award a Paint Can, which can be used to change the color of any Bumper in play, including the next Bumper. + - Each subsequent Paint Can will require 500 more points than the last one (1000, +1500 (2500), +2000 (4500), etc.) +- The game is over when all Launchers are jammed and no further move can be made. + +## Archipelago Mode + +Archipelago Mode differs from Classic mode in the following ways: +- The board starts as a 3×3 field, with only two colors in play. +- You'll be presented with a set of tasks to complete on the HUD. +- Tasks may have multiple steps. If they do, you will see **[+#]** next to it, indicating how many more steps are left in this task. +- Completing each step of a task will send a check. Clearing Bonus Boosters and Treasure Bumpers will send one check, whether or not they complete a task. +- Completing all tasks will end the board in play, and start a larger board with more colors and a new set of tasks. +- If the board becomes jammed, it is wiped out and the board is reset. Note that this will reset your progress for Score and Bumpers tasks, but not Level or Total Score/Bumpers tasks. +- Your goal is to complete all five levels. + +There are some additional types of Bumpers in this game mode: +- Treasure Bumpers, which have the Archipelago logo on them, award a score bonus and send a check when sticked. +- Bonus Boosters, which have yellow and blue dots on them, award a permanent multiplier and send a check when sticked. +- Hazard Bumpers are an obstacle that start showing up in level 2. First, a red space will show up to warn that a Hazard is about to appear. After making a move, it will show up as a grey Bumper with a red octagon (like a Stop sign) on it. It is not stickable for five moves, after which time it will stay immobile, but take on a random color, and can be sticked like a normal Bumper, and even recolored with a Paint Can. + - Playing a Bumper which stops on the red warning space will cause that space to move to another random location. + +In addition to Paint Cans from Classic mode, two new tools are also available: +- Turners allow you to change the direction of any bumper. You won't get them from scoring, but you can get them as Archipelago items, and they'll refresh every time you start a new board. +- Task Advances allow you to skip one step in any task. They can only be obtained as Archipelago items. Make sure you keep them for when you need them most; if you use one, it won't come back! +- You can also get Starting Paint Cans from the AP server. These refresh when you start a new board. + +## Commands + +While playing the multiworld, you can interact with the server using various commands listed in the [commands guide](/tutorial/Archipelago/commands/en). As this game does not have an in-game text client at the moment, you can optionally connect to the multiworld using the text client, which can be found in the [main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases) as Archipelago Text Client to enter these commands. +