Files
dockipelago/worlds/apquest/locations.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

137 lines
7.6 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import ItemClassification, Location
from . import items
if TYPE_CHECKING:
from .world import APQuestWorld
# Every location must have a unique integer ID associated with it.
# We will have a lookup from location name to ID here that, in world.py, we will import and bind to the world class.
# Even if a location doesn't exist on specific options, it must be present in this lookup.
LOCATION_NAME_TO_ID = {
"Top Left Room Chest": 1,
"Top Middle Chest": 2,
"Bottom Left Chest": 3,
"Bottom Left Extra Chest": 4,
"Bottom Right Room Left Chest": 5,
"Bottom Right Room Right Chest": 6,
# Location IDs don't need to be sequential, as long as they're unique and greater than 0.
"Right Room Enemy Drop": 10,
}
# Each Location instance must correctly report the "game" it belongs to.
# To make this simple, it is common practice to subclass the basic Location class and override the "game" field.
class APQuestLocation(Location):
game = "APQuest"
# Let's make one more helper method before we begin actually creating locations.
# Later on in the code, we'll want specific subsections of LOCATION_NAME_TO_ID.
# To reduce the chance of copy-paste errors writing something like {"Chest": LOCATION_NAME_TO_ID["Chest"]},
# let's make a helper method that takes a list of location names and returns them as a dict with their IDs.
# Note: There is a minor typing quirk here. Some functions want location addresses to be an "int | None",
# so while our function here only ever returns dict[str, int], we annotate it as dict[str, int | None].
def get_location_names_with_ids(location_names: list[str]) -> dict[str, int | None]:
return {location_name: LOCATION_NAME_TO_ID[location_name] for location_name in location_names}
def create_all_locations(world: APQuestWorld) -> None:
create_regular_locations(world)
create_events(world)
def create_regular_locations(world: APQuestWorld) -> None:
# Finally, we need to put the Locations ("checks") into their regions.
# Once again, before we do anything, we can grab our regions we created by using world.get_region()
overworld = world.get_region("Overworld")
top_left_room = world.get_region("Top Left Room")
bottom_right_room = world.get_region("Bottom Right Room")
right_room = world.get_region("Right Room")
# One way to create locations is by just creating them directly via their constructor.
bottom_left_chest = APQuestLocation(
world.player, "Bottom Left Chest", world.location_name_to_id["Bottom Left Chest"], overworld
)
# You can then add them to the region.
overworld.locations.append(bottom_left_chest)
# A simpler way to do this is by using the region.add_locations helper.
# For this, you need to have a dict of location names to their IDs (i.e. a subset of location_name_to_id)
# Aha! So that's why we made that "get_location_names_with_ids" helper method earlier.
# You also need to pass your overridden Location class.
bottom_right_room_locations = get_location_names_with_ids(
["Bottom Right Room Left Chest", "Bottom Right Room Right Chest"]
)
bottom_right_room.add_locations(bottom_right_room_locations, APQuestLocation)
top_left_room_locations = get_location_names_with_ids(["Top Left Room Chest"])
top_left_room.add_locations(top_left_room_locations, APQuestLocation)
right_room_locations = get_location_names_with_ids(["Right Room Enemy Drop"])
right_room.add_locations(right_room_locations, APQuestLocation)
# Locations may be in different regions depending on the player's options.
# In our case, the hammer option puts the Top Middle Chest into its own room called Top Middle Room.
top_middle_room_locations = get_location_names_with_ids(["Top Middle Chest"])
if world.options.hammer:
top_middle_room = world.get_region("Top Middle Room")
top_middle_room.add_locations(top_middle_room_locations, APQuestLocation)
else:
overworld.add_locations(top_middle_room_locations, APQuestLocation)
# Locations may exist only if the player enables certain options.
# In our case, the extra_starting_chest option adds the Bottom Left Extra Chest location.
if world.options.extra_starting_chest:
# Once again, it is important to stress that even though the Bottom Left Extra Chest location doesn't always
# exist, it must still always be present in the world's location_name_to_id.
# Whether the location actually exists in the seed is purely determined by whether we create and add it here.
bottom_left_extra_chest = get_location_names_with_ids(["Bottom Left Extra Chest"])
overworld.add_locations(bottom_left_extra_chest, APQuestLocation)
def create_events(world: APQuestWorld) -> None:
# Sometimes, the player may perform in-game actions that allow them to progress which are not related to Items.
# In our case, the player must press a button in the top left room to open the final boss door.
# AP has something for this purpose: "Event locations" and "Event items".
# An event location is no different than a regular location, except it has the address "None".
# It is treated during generation like any other location, but then it is discarded.
# This location cannot be "sent" and its item cannot be "received", but the item can be used in logic rules.
# Since we are creating more locations and adding them to regions, we need to grab those regions again first.
top_left_room = world.get_region("Top Left Room")
final_boss_room = world.get_region("Final Boss Room")
# One way to create an event is simply to use one of the normal methods of creating a location.
button_in_top_left_room = APQuestLocation(world.player, "Top Left Room Button", None, top_left_room)
top_left_room.locations.append(button_in_top_left_room)
# We then need to put an event item onto the location.
# An event item is an item whose code is "None" (same as the event location's address),
# and whose classification is "progression". Item creation will be discussed more in items.py.
# Note: Usually, items are created in world.create_items(), which for us happens in items.py.
# However, when the location of an item is known ahead of time (as is the case with an event location/item pair),
# it is common practice to create the item when creating the location.
# Since locations also have to be finalized after world.create_regions(), which runs before world.create_items(),
# we'll create both the event location and the event item in our locations.py code.
button_item = items.APQuestItem("Top Left Room Button Pressed", ItemClassification.progression, None, world.player)
button_in_top_left_room.place_locked_item(button_item)
# A way simpler way to do create an event location/item pair is by using the region.create_event helper.
# Luckily, we have another event we want to create: The Victory event.
# We will use this event to track whether the player can win the game.
# The Victory event is a completely optional abstraction - This will be discussed more in set_rules().
final_boss_room.add_event(
"Final Boss Defeated", "Victory", location_type=APQuestLocation, item_type=items.APQuestItem
)
# If you create all your regions and locations line-by-line like this,
# the length of your create_regions might get out of hand.
# Many worlds use more data-driven approaches using dataclasses or NamedTuples.
# However, it is worth understanding how the actual creation of regions and locations works,
# That way, we're not just mindlessly copy-pasting! :)