forked from mirror/Archipelago
* 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>
132 lines
7.6 KiB
Python
132 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from BaseClasses import CollectionState
|
|
from worlds.generic.Rules import add_rule, set_rule
|
|
|
|
if TYPE_CHECKING:
|
|
from .world import APQuestWorld
|
|
|
|
|
|
def set_all_rules(world: APQuestWorld) -> None:
|
|
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
|
# we need to define rules for our Entrances and Locations.
|
|
# Note: Regions do not have rules, the Entrances connecting them do!
|
|
# We'll do entrances first, then locations, and then finally we set our victory condition.
|
|
|
|
set_all_entrance_rules(world)
|
|
set_all_location_rules(world)
|
|
set_completion_condition(world)
|
|
|
|
|
|
def set_all_entrance_rules(world: APQuestWorld) -> None:
|
|
# First, we need to actually grab our entrances. Luckily, there is a helper method for this.
|
|
overworld_to_bottom_right_room = world.get_entrance("Overworld to Bottom Right Room")
|
|
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
|
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
|
|
|
# An access rule is a function. We can define this function like any other function.
|
|
# This function must accept exactly one parameter: A "CollectionState".
|
|
# A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
|
|
# which regions they've reached, etc.
|
|
# In an access rule, we can ask whether the player has a collected a certain item.
|
|
# We can do this via the state.has(...) function.
|
|
# This function takes an item name, a player number, and an optional count parameter (more on that below)
|
|
# Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
|
|
# our function needs to be locally defined so that it has access to the player number from the outer scope.
|
|
# In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
|
|
def can_destroy_bush(state: CollectionState) -> bool:
|
|
return state.has("Sword", world.player)
|
|
|
|
# Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
|
|
# One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
|
|
set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
|
|
|
# Because the function has to be defined locally, most worlds prefer the lambda syntax.
|
|
set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
|
|
|
# Conditions can depend on event items.
|
|
set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
|
|
|
|
# Some entrance rules may only apply if the player enabled certain options.
|
|
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
|
# Overworld to the Top Middle Room.
|
|
if world.options.hammer:
|
|
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
|
set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
|
|
|
|
|
|
def set_all_location_rules(world: APQuestWorld) -> None:
|
|
# Location rules work no differently from Entrance rules.
|
|
# Most of our locations are chests that can simply be opened by walking up to them.
|
|
# Thus, their logical requirements are covered by the Entrance rules of the Entrances that were required to
|
|
# reach the region that the chest sits in.
|
|
# However, our two enemies work differently.
|
|
# Entering the room with the enemy is not enough, you also need to have enough combat items to be able to defeat it.
|
|
# So, we need to set requirements on the Locations themselves.
|
|
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
|
|
|
# Sometimes, you may want to have different rules depending on the player's chosen options.
|
|
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
|
|
right_room_enemy = world.get_location("Right Room Enemy Drop")
|
|
|
|
# DON'T DO THIS!!!!
|
|
set_rule(
|
|
right_room_enemy,
|
|
lambda state: (
|
|
state.has("Sword", world.player)
|
|
and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
|
|
),
|
|
)
|
|
# DON'T DO THIS!!!!
|
|
|
|
# Now, what's actually wrong with this? It works perfectly fine, right?
|
|
# If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
|
|
# The access rule we just wrote does this correctly, so what's the problem?
|
|
# The problem is performance.
|
|
# Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
|
|
# However, access rules in particular are by far the hottest code path in Archipelago.
|
|
# An access rule will potentially be called thousands or even millions of times over the course of one generation.
|
|
# As a result, access rules are the one place where it's really worth putting in some effort to optimize.
|
|
# What's the performance problem here?
|
|
# Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
|
|
# Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
|
|
# Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
|
|
# Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
|
|
# and instead having two *different* set_rule calls depending on which case we're in.
|
|
|
|
if world.options.hard_mode:
|
|
# If you have multiple conditions, you can obviously chain them via "or" or "and".
|
|
# However, there are also the nice helper functions "state.has_any" and "state.has_all".
|
|
set_rule(
|
|
right_room_enemy,
|
|
lambda state: (
|
|
state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
|
|
),
|
|
)
|
|
else:
|
|
set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
|
|
|
|
# Another way to chain multiple conditions is via the add_rule function.
|
|
# This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
|
|
# In our case, it's pretty useful because hard mode and easy mode have different requirements.
|
|
final_boss = world.get_location("Final Boss Defeated")
|
|
|
|
# For the "known" requirements, it's still better to chain them using a normal "and" condition.
|
|
add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
|
|
|
|
if world.options.hard_mode:
|
|
# You can check for multiple copies of an item by using the optional count parameter of state.has().
|
|
add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
|
|
|
|
|
|
def set_completion_condition(world: APQuestWorld) -> None:
|
|
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
|
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
|
world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
|
|
|
|
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
|
# So lets undo what we just did, and instead set the completion condition to:
|
|
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|