Files
dockipelago/worlds/apquest/items.py
NewSoupVi e0cbf77dae APQuest: Implement New Game (#5393)
* APQuest

* Add confetti cannon

* ID change on enemy drop

* nevermind

* Write the apworld

* Actually implement hard mode

* split everything into multiple files

* Push out webworld into a file

* Comment

* Enemy health graphics

* more ruff rules

* graphics :)

* heal player when receiving health upgrade

* the dumbest client of all time

* Fix typo

* You can kinda play it now! Now we just need to render the game... :)))

* fix kvui imports again

* It's playable. Kind of

* oops

* Sounds and stuff

* exceptions for audio

* player sprite stuff

* Not attack without sword

* Make sure it plays correctly

* Collect behavior

* ruff

* don't need to clear checked_locations, but do need to still clear finished_game

* Connect calls disconnect, so this is not necessary

* more seemless reconnection

* Ok now I think it's correct

* Bgm

* Bgm

* minor adjustment

* More refactoring of graphics and sound

* add graphics

* Item column

* Fix enemies not regaining their health

* oops

* oops

* oops

* 6 health final boss on hard mode

* boss_6.png

* Display APQuest items correctly

* auto switch tabs

* some mypy stuff

* Intro song

* Confetti Cannon

* a bit more confetti work

* launcher component

* Graphics change

* graphics and cleanup

* fix apworld

* comment out horse and cat for now

* add docs

* copypasta

* ruff made my comment look unhinged

* Move that comment

* Fix typing and don't import kvui in nogui

* lmao that already exists I don't need to do it myself

* Must've just copied this from somewhere

* order change

* Add unit tests

* Notes about the client

* oops

* another intro song case

* Write WebWorld and setup guides

* Yes description provided

* thing

* how to play

* Music and Volume

* Add cat and horse player sprites

* updates

* Add hammer and breakable wall

* TODO

* replace wav with ogg

* Codeowners and readme

* finish unit tests

* lint

* Todid

* Update worlds/apquest/client/ap_quest_client.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/apquest/client/custom_views.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Filler pattern

* __future__ annotations

* twebhost

* Allow wasd and arrow keys

* correct wording

* oops

* just say the website

* append instead of +=

* qwint is onto my favoritism

* kitty alias

* Add a comment about preplaced items for assertAccessDependency

* Use classvar_matrix instead of MultiworldTestBase

* actually remove multiworld stuff from those tests

* missed one more

* Refactor a bit more

* Fix getting of the user path

* Actually explain components

* Meh

* Be a bit clearer about what's what

* oops

* More comments in the regions.py file

* Nevermind

* clarify regions further

* I use too many brackets

* Ok I'm done fr

* simplify wording

* missing .

* Add precollected example

* add note about precollected advancements

* missing s

* APQuest sound rework

* Volume slider

* I forgot I made this

* a

* fix volume of jingles

* Add math trap to game (only works in play_in_console mode so far)

* Math trap in apworld and client side

* Fix background during math trap

* fix leading 0

* Sound and further ui improvements for Math Trap

* fix music bug

* rename apquest subfolder to game

* Move comment to where it belongs

* Clear up language around components (hopefully)

* Clear up what CommonClient is

* Reword some more

* Mention Archipelago (the program) explicitly

* Update worlds/apquest/docs/en_APQuest.md

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Explain a bit more why you would use classvar matrix

* reword the assert raises stuff

* the volume slider thing is no longer true

* german game page

* Be more clear about why we're overriding Item and Location

* default item classification

* logically considered -> relevant to logic ()

* Update worlds/apquest/items.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* a word on the ambiguity of the word 'filler'

* more rewording

* amount -> number

* stress the necessity of appending to the multiworld itempool

* Update worlds/apquest/locations.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* get_location_names_with_ids

* slight rewording of the new helper method

* add some words about creating known location+item pairs

* Add some more words to worlds/apqeust/options.py

* more words in options.py

* 120 chars (thanks Ixrec >:((( LOL)

* Less confusing wording about rules, hopefully?

* victory -> completion

* remove the immediate creation of the hammer rule on the option region entrance

* access rule performance

* Make all imports module-level in world.py

* formatting

* get rid of noqa RUF012 (and also disable the rule in my local ruff.toml

* move comment for docstring closer to docstring in another place

* advancement????

* Missing function type annotations

* pass mypy again (I don't love this one but all the alternatives are equally bad)

* subclass instead of override

* I forgor to remove these

* Get rid of classvar_matrix and instead talk about some other stuff

* protect people a bit from the assertAccessDependency nonsense

* reword a bit more

* word

* More accessdependency text

* More accessdependency text

* More accessdependency text

* More accessdependency text

* oops

* this is supposed to be absolute

* Add some links to docs

* that's called game now

* Add an archipelago.json and explain what it means

* new line who dis

* reorganize a bit

* ignore instead of skip

* Update archipelago.json

* She new on my line till I

* Update archipelago.json

* add controls tab

* new ruff rule? idk

* WHOOPS

* Pack graphics into fewer files

* annoying ruff format thing

* Cleanup + mypy

* relative import

* Update worlds/apquest/client/custom_views.py

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Update generate_math_problem.py

* Update worlds/apquest/game/player.py

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

---------

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2025-11-25 00:38:06 +01:00

167 lines
9.8 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING:
from .world import APQuestWorld
# Every item must have a unique integer ID associated with it.
# We will have a lookup from item name to ID here that, in world.py, we will import and bind to the world class.
# Even if an item doesn't exist on specific options, it must be present in this lookup.
ITEM_NAME_TO_ID = {
"Key": 1,
"Sword": 2,
"Shield": 3,
"Hammer": 4,
"Health Upgrade": 5,
"Confetti Cannon": 6,
"Math Trap": 7,
}
# Items should have a defined default classification.
# In our case, we will make a dictionary from item name to classification.
DEFAULT_ITEM_CLASSIFICATIONS = {
"Key": ItemClassification.progression,
"Sword": ItemClassification.progression | ItemClassification.useful, # Items can have multiple classifications.
"Shield": ItemClassification.progression,
"Hammer": ItemClassification.progression,
"Health Upgrade": ItemClassification.useful,
"Confetti Cannon": ItemClassification.filler,
"Math Trap": ItemClassification.trap,
}
# Each Item instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Item class and override the "game" field.
class APQuestItem(Item):
game = "APQuest"
# Ontop of our regular itempool, our world must be able to create arbitrary amounts of filler as requested by core.
# To do this, it must define a function called world.get_filler_item_name(), which we will define in world.py later.
# For now, let's make a function that returns the name of a random filler item here in items.py.
def get_random_filler_item_name(world: APQuestWorld) -> str:
# APQuest has an option called "trap_chance".
# This is the percentage chance that each filler item is a Math Trap instead of a Confetti Cannon.
# For this purpose, we need to use a random generator.
# IMPORTANT: Whenever you need to use a random generator, you must use world.random.
# This ensures that generating with the same generator seed twice yields the same output.
# DO NOT use a bare random object from Python's built-in random module.
if world.random.randint(0, 99) < world.options.trap_chance:
return "Math Trap"
return "Confetti Cannon"
def create_item_with_correct_classification(world: APQuestWorld, name: str) -> APQuestItem:
# Our world class must have a create_item() function that can create any of our items by name at any time.
# So, we make this helper function that creates the item by name with the correct classification.
# Note: This function's content could just be the contents of world.create_item in world.py directly,
# but it seemed nicer to have it in its own function over here in items.py.
classification = DEFAULT_ITEM_CLASSIFICATIONS[name]
# It is perfectly normal and valid for an item's classification to differ based on the player's options.
# In our case, Health Upgrades are only relevant to logic (and thus labeled as "progression") in hard mode.
if name == "Health Upgrade" and world.options.hard_mode:
classification = ItemClassification.progression
return APQuestItem(name, classification, ITEM_NAME_TO_ID[name], world.player)
# With those two helper functions defined, let's now get to actually creating and submitting our itempool.
def create_all_items(world: APQuestWorld) -> None:
# This is the function in which we will create all the items that this world submits to the multiworld item pool.
# There must be exactly as many items as there are locations.
# In our case, there are either six or seven locations.
# We must make sure that when there are six locations, there are six items,
# and when there are seven locations, there are seven items.
# Creating items should generally be done via the world's create_item method.
# First, we create a list containing all the items that always exist.
itempool: list[Item] = [
world.create_item("Key"),
world.create_item("Sword"),
world.create_item("Shield"),
world.create_item("Health Upgrade"),
world.create_item("Health Upgrade"),
]
# Some items may only exist if the player enables certain options.
# In our case, If the hammer option is enabled, the sixth item is the Hammer.
# Otherwise, we add a filler Confetti Cannon.
if world.options.hammer:
# Once again, it is important to stress that even though the Hammer doesn't always exist,
# it must be present in the worlds item_name_to_id.
# Whether it is actually in the itempool is determined purely by whether we create and add the item here.
itempool.append(world.create_item("Hammer"))
# Archipelago requires that each world submits as many locations as it submits items.
# This is where we can use our filler and trap items.
# APQuest has two of these: The Confetti Cannon and the Math Trap.
# (Unfortunately, Archipelago is a bit ambiguous about its terminology here:
# "filler" is an ItemClassification separate from "trap", but in a lot of its functions,
# Archipelago will use "filler" to just mean "an additional item created to fill out the itempool".
# "Filler" in this sense can technically have any ItemClassification,
# but most commonly ItemClassification.filler or ItemClassification.trap.
# Starting here, the word "filler" will be used to collectively refer to APQuest's Confetti Cannon and Math Trap,
# which are ItemClassification.filler and ItemClassification.trap respectively.)
# Creating filler items works the same as any other item. But there is a question:
# How many filler items do we actually need to create?
# In regions.py, we created either six or seven locations depending on the "extra_starting_chest" option.
# In this function, we have created five or six items depending on whether the "hammer" option is enabled.
# We *could* have a really complicated if-else tree checking the options again, but there is a better way.
# We can compare the size of our itempool so far to the number of locations in our world.
# The length of our itempool is easy to determine, since we have it as a list.
number_of_items = len(itempool)
# The number of locations is also easy to determine, but we have to be careful.
# Just calling len(world.get_locations()) would report an incorrect number, because of our *event locations*.
# What we actually want is the number of *unfilled* locations. Luckily, there is a helper method for this:
number_of_unfilled_locations = len(world.multiworld.get_unfilled_locations(world.player))
# Now, we just subtract the number of items from the number of locations to get the number of empty item slots.
needed_number_of_filler_items = number_of_unfilled_locations - number_of_items
# Finally, we create that many filler items and add them to the itempool.
# To create our filler, we could just use world.create_item("Confetti Cannon").
# But there is an alternative that works even better for most worlds, including APQuest.
# As discussed above, our world must have a get_filler_item_name() function defined,
# which must return the name of an infinitely repeatable filler item.
# Defining this function enables the use of a helper function called world.create_filler().
# You can just use this function directly to create as many filler items as you need to complete your itempool.
itempool += [world.create_filler() for _ in range(needed_number_of_filler_items)]
# But... is that the right option for your game? Let's explore that.
# For some games, the concepts of "regular itempool filler" and "additionally created filler" are different.
# These games might want / require specific amounts of specific filler items in their regular pool.
# To achieve this, they will have to intentionally create the correct quantities using world.create_item().
# They may still use world.create_filler() to fill up the rest of their itempool with "repeatable filler",
# after creating their "specific quantity" filler and still having room left over.
# But there are many other games which *only* have infinitely repeatable filler items.
# They don't care about specific amounts of specific filler items, instead only caring about the proportions.
# In this case, world.create_filler() can just be used for the entire filler itempool.
# APQuest is one of these games:
# Regardless of whether it's filler for the regular itempool or additional filler for item links / etc.,
# we always just want a Confetti Cannon or a Math Trap depending on the "trap_chance" option.
# We defined this behavior in our get_random_filler_item_name() function, which in world.py,
# we'll bind to world.get_filler_item_name(). So, we can just use world.create_filler() for all of our filler.
# Anyway. With our world's itempool finalized, we now need to submit it to the multiworld itempool.
# This is how the generator actually knows about the existence of our items.
world.multiworld.itempool += itempool
# Sometimes, you might want the player to start with certain items already in their inventory.
# These items are called "precollected items".
# They will be sent as soon as they connect for the first time (depending on your client's item handling flag).
# Players can add precollected items themselves via the generic "start_inventory" option.
# If you want to add your own precollected items, you can do so via world.push_precollected().
if world.options.start_with_one_confetti_cannon:
# We're adding a filler item, but you can also add progression items to the player's precollected inventory.
starting_confetti_cannon = world.create_item("Confetti Cannon")
world.push_precollected(starting_confetti_cannon)