mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 13:23:21 -07:00
* 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>
291 lines
10 KiB
Python
291 lines
10 KiB
Python
import asyncio
|
|
import sys
|
|
from argparse import Namespace
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from CommonClient import CommonContext, gui_enabled, logger, server_loop
|
|
from NetUtils import ClientStatus
|
|
|
|
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
|
|
from ..game.game import Game
|
|
from ..game.inputs import Input
|
|
from ..game.items import Item
|
|
from ..game.locations import Location
|
|
from .game_manager import APQuestManager
|
|
from .graphics import PlayerSprite
|
|
from .item_quality import get_quality_for_network_item
|
|
from .sounds import (
|
|
CONFETTI_CANNON,
|
|
ITEM_JINGLES,
|
|
MATH_PROBLEM_SOLVED_JINGLE,
|
|
MATH_PROBLEM_STARTED_JINGLE,
|
|
VICTORY_JINGLE,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
import kvui
|
|
|
|
|
|
# !!! IMPORTANT !!!
|
|
# The client implementation is *not* meant for teaching.
|
|
# Obviously, it is written to the best of its author's abilities,
|
|
# but it is not to the same standard as the rest of the apworld.
|
|
# Copy things from here at your own risk.
|
|
|
|
|
|
class ConnectionStatus(Enum):
|
|
NOT_CONNECTED = 0
|
|
SCOUTS_NOT_SENT = 1
|
|
SCOUTS_SENT = 2
|
|
GAME_RUNNING = 3
|
|
|
|
|
|
class APQuestContext(CommonContext):
|
|
game = "APQuest"
|
|
items_handling = 0b111 # full remote
|
|
|
|
client_loop: asyncio.Task[None]
|
|
|
|
last_connected_slot: int | None = None
|
|
|
|
slot_data: dict[str, Any]
|
|
|
|
ap_quest_game: Game | None = None
|
|
hard_mode: bool = False
|
|
hammer: bool = False
|
|
extra_starting_chest: bool = False
|
|
player_sprite: PlayerSprite = PlayerSprite.HUMAN
|
|
|
|
connection_status: ConnectionStatus = ConnectionStatus.NOT_CONNECTED
|
|
|
|
highest_processed_item_index: int = 0
|
|
queued_locations: list[int]
|
|
|
|
delay_intro_song: bool
|
|
|
|
ui: APQuestManager
|
|
|
|
def __init__(
|
|
self, server_address: str | None = None, password: str | None = None, delay_intro_song: bool = False
|
|
) -> None:
|
|
super().__init__(server_address, password)
|
|
|
|
self.queued_locations = []
|
|
self.slot_data = {}
|
|
self.delay_intro_song = delay_intro_song
|
|
|
|
async def server_auth(self, password_requested: bool = False) -> None:
|
|
if password_requested and not self.password:
|
|
self.ui.allow_intro_song()
|
|
await super().server_auth(password_requested)
|
|
await self.get_username()
|
|
await self.send_connect(game=self.game)
|
|
|
|
def handle_connection_loss(self, msg: str) -> None:
|
|
self.ui.allow_intro_song()
|
|
super().handle_connection_loss(msg)
|
|
|
|
async def connect(self, address: str | None = None) -> None:
|
|
self.ui.switch_to_regular_tab()
|
|
await super().connect(address)
|
|
|
|
async def apquest_loop(self) -> None:
|
|
while not self.exit_event.is_set():
|
|
if self.connection_status != ConnectionStatus.GAME_RUNNING:
|
|
if self.connection_status == ConnectionStatus.SCOUTS_NOT_SENT:
|
|
await self.send_msgs([{"cmd": "LocationScouts", "locations": self.server_locations}])
|
|
self.connection_status = ConnectionStatus.SCOUTS_SENT
|
|
|
|
await asyncio.sleep(0.1)
|
|
continue
|
|
|
|
if not self.ap_quest_game or not self.ap_quest_game.gameboard or not self.ap_quest_game.gameboard.ready:
|
|
await asyncio.sleep(0.1)
|
|
continue
|
|
|
|
try:
|
|
while self.queued_locations:
|
|
location = self.queued_locations.pop(0)
|
|
self.location_checked_side_effects(location)
|
|
self.locations_checked.add(location)
|
|
await self.check_locations({location})
|
|
|
|
rerender = False
|
|
|
|
new_items = self.items_received[self.highest_processed_item_index :]
|
|
for item in new_items:
|
|
self.highest_processed_item_index += 1
|
|
self.ap_quest_game.receive_item(item.item, item.location, item.player)
|
|
rerender = True
|
|
|
|
for new_remotely_cleared_location in self.checked_locations - self.locations_checked:
|
|
self.ap_quest_game.force_clear_location(new_remotely_cleared_location)
|
|
rerender = True
|
|
|
|
if rerender:
|
|
self.render()
|
|
|
|
if self.ap_quest_game.player.has_won and not self.finished_game:
|
|
await self.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
self.finished_game = True
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
def on_package(self, cmd: str, args: dict[str, Any]) -> None:
|
|
if cmd == "ConnectionRefused":
|
|
self.ui.allow_intro_song()
|
|
|
|
if cmd == "Connected":
|
|
if self.connection_status == ConnectionStatus.GAME_RUNNING:
|
|
# In a connection loss -> auto reconnect scenario, we can seamlessly keep going
|
|
return
|
|
|
|
self.last_connected_slot = self.slot
|
|
|
|
self.connection_status = ConnectionStatus.NOT_CONNECTED # for safety, it will get set again later
|
|
|
|
self.slot_data = args["slot_data"]
|
|
self.hard_mode = self.slot_data["hard_mode"]
|
|
self.hammer = self.slot_data["hammer"]
|
|
self.extra_starting_chest = self.slot_data["extra_starting_chest"]
|
|
try:
|
|
self.player_sprite = PlayerSprite(self.slot_data["player_sprite"])
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
self.player_sprite = PlayerSprite.UNKNOWN
|
|
|
|
self.ap_quest_game = Game(self.hard_mode, self.hammer, self.extra_starting_chest)
|
|
self.highest_processed_item_index = 0
|
|
self.render()
|
|
|
|
self.connection_status = ConnectionStatus.SCOUTS_NOT_SENT
|
|
if cmd == "LocationInfo":
|
|
remote_item_graphic_overrides = {
|
|
Location(location): Item(network_item.item)
|
|
for location, network_item in self.locations_info.items()
|
|
if self.slot_info[network_item.player].game == self.game
|
|
}
|
|
|
|
assert self.ap_quest_game is not None
|
|
self.ap_quest_game.gameboard.fill_remote_location_content(remote_item_graphic_overrides)
|
|
self.render()
|
|
self.ui.game_view.bind_keyboard()
|
|
|
|
self.connection_status = ConnectionStatus.GAME_RUNNING
|
|
self.ui.game_started()
|
|
|
|
async def disconnect(self, *args: Any, **kwargs: Any) -> None:
|
|
self.finished_game = False
|
|
self.locations_checked = set()
|
|
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
|
await super().disconnect(*args, **kwargs)
|
|
|
|
def render(self) -> None:
|
|
if self.ap_quest_game is None:
|
|
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
|
|
|
|
self.ui.render(self.ap_quest_game, self.player_sprite)
|
|
self.handle_game_events()
|
|
|
|
def location_checked_side_effects(self, location: int) -> None:
|
|
network_item = self.locations_info[location]
|
|
|
|
if network_item.player == self.slot and network_item.item == Item.MATH_TRAP.value:
|
|
# In case of a local math trap, we only play the math trap trigger jingle
|
|
return
|
|
|
|
item_quality = get_quality_for_network_item(network_item)
|
|
self.play_jingle(ITEM_JINGLES[item_quality])
|
|
|
|
def play_jingle(self, audio_filename: str) -> None:
|
|
self.ui.play_jingle(audio_filename)
|
|
|
|
def handle_game_events(self) -> None:
|
|
if self.ap_quest_game is None:
|
|
return
|
|
|
|
while self.ap_quest_game.queued_events:
|
|
event = self.ap_quest_game.queued_events.pop(0)
|
|
|
|
if isinstance(event, LocationClearedEvent):
|
|
self.queued_locations.append(event.location_id)
|
|
continue
|
|
|
|
if isinstance(event, VictoryEvent):
|
|
self.play_jingle(VICTORY_JINGLE)
|
|
continue
|
|
|
|
if isinstance(event, ConfettiFired):
|
|
gameboard_x, gameboard_y = self.ap_quest_game.gameboard.size
|
|
gameboard_x += 1 # vertical item column
|
|
x = (event.x + 0.5) / gameboard_x
|
|
y = 1 - (event.y + 0.5) / gameboard_y # Kivy's y is bottom to top (ew)
|
|
|
|
self.ui.play_jingle(CONFETTI_CANNON)
|
|
self.ui.add_confetti((x, y), (self.slot_data["confetti_explosiveness"] + 1) * 5)
|
|
continue
|
|
|
|
if isinstance(event, MathProblemStarted):
|
|
self.play_jingle(MATH_PROBLEM_STARTED_JINGLE)
|
|
continue
|
|
|
|
if isinstance(event, MathProblemSolved):
|
|
self.play_jingle(MATH_PROBLEM_SOLVED_JINGLE)
|
|
continue
|
|
|
|
def input_and_rerender(self, input_key: Input) -> None:
|
|
if self.ap_quest_game is None:
|
|
return
|
|
if not self.ap_quest_game.gameboard.ready:
|
|
return
|
|
self.ap_quest_game.input(input_key)
|
|
self.render()
|
|
|
|
def make_gui(self) -> "type[kvui.GameManager]":
|
|
self.load_kv()
|
|
return APQuestManager
|
|
|
|
def load_kv(self) -> None:
|
|
import pkgutil
|
|
|
|
from kivy.lang import Builder
|
|
|
|
data = pkgutil.get_data(__name__, "ap_quest_client.kv")
|
|
if data is None:
|
|
raise RuntimeError("ap_quest_client.kv could not be loaded.")
|
|
|
|
Builder.load_string(data.decode())
|
|
|
|
|
|
async def main(args: Namespace) -> None:
|
|
if not gui_enabled:
|
|
raise RuntimeError("APQuest cannot be played without gui.")
|
|
|
|
# Assume we shouldn't play the intro song in the auto-connect scenario, because the game will instantly start.
|
|
delay_intro_song = args.connect and args.name
|
|
|
|
ctx = APQuestContext(args.connect, args.password, delay_intro_song=delay_intro_song)
|
|
ctx.auth = args.name
|
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
|
|
|
ctx.run_gui()
|
|
ctx.run_cli()
|
|
|
|
ctx.client_loop = asyncio.create_task(ctx.apquest_loop(), name="Client Loop")
|
|
|
|
await ctx.exit_event.wait()
|
|
await ctx.shutdown()
|
|
|
|
|
|
def launch(*args: str) -> None:
|
|
from .launch import launch_ap_quest_client
|
|
|
|
launch_ap_quest_client(*args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
launch(*sys.argv[1:])
|