Add Bumper Stickers (#811)

* bumpstik: initial commit

* bumpstik: fix game name in location obj

* bumpstik: specified offset

* bumpstik: forgot to call create_regions

* bumpstik: fix entrance generation

* bumpstik: fix completion definition

* bumpstik: treasure bumper, LttP text

* bumpstik: add more score-based locations

* bumpstik: adjust regions

* bumpstik: fill with Treasure Bumpers

* bumpstik: force Treasure Bumper on last location

* bumpstik: don't require Hazard Bumpers for level 4

* bumpstik: treasure bumper locations

* bumpstik: formatting

* bumpstik: refactor to 0.3.5

* bumpstik: Treasure bumpers are now progression

* bumpstik: complete reimplementation of locations

* bumpstik: implement Nothing as item

* bumpstik: level 3 and 4 locations

* bumpstik: correct a goal value

* bumpstik: region defs need one extra treasure

* bumpstik: add more starting paint cans

* bumpstik: toned down final score goal

* bumpstik: changing items, Hazards no longer traps

* bumpstik: remove item groups

* bumpstik: update self.world to self.multiworld

* bumpstik: clean up item types and classes

* bumpstik: add options
also add traps to item pool

* bumpstik: update docs

* bumpstik: oops

* bumpstik: add to master game list on readme

* bumpstik: renaming Task Skip to Task Advance
because "Task Skip" is surprisingly hard to say

* bumpstik: fill with score on item gen
instead of nothing (nothing is still the default filler)

* bumpstik: add 18 checks

* bumpstik: bump ap ver

* bumpstik: add item groups

* bumpstik: make helper items and traps configurable

* bumpstik: make Hazard Bumper progression

* bumpstik: tone final score goal down to 50K

* bumpstik: 0.4.0 region update

* bumpstik: clean up docs
also final goal is now 50K or your score + 5000, whichever is higher

* bumpstik: take datapackage out of testing mode

* bumpstik: Apply suggestions from code review

code changes for .apworld support

Co-authored-by: Zach Parks <zach@alliware.com>

---------

Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
Felix R 2023-06-27 17:37:17 -03:00 committed by GitHub
parent a4e485e297
commit 36b5b1207c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 530 additions and 0 deletions

View File

@ -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

129
worlds/bumpstik/Items.py Normal file
View File

@ -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()})

View File

@ -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)
}

View File

@ -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
}

View File

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

128
worlds/bumpstik/__init__.py Normal file
View File

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

View File

@ -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.

View File

@ -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.