mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-24 14:43:24 -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>
250 lines
8.3 KiB
Python
250 lines
8.3 KiB
Python
import asyncio
|
|
import pkgutil
|
|
from asyncio import Task
|
|
from collections.abc import Buffer
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
from kivy import Config
|
|
from kivy.core.audio import Sound, SoundLoader
|
|
|
|
from CommonClient import logger
|
|
|
|
from .. import game
|
|
from .item_quality import ItemQuality
|
|
from .utils import make_data_directory
|
|
|
|
ITEM_JINGLES = {
|
|
ItemQuality.PROGUSEFUL: "8bit ProgUseful.ogg",
|
|
ItemQuality.PROGRESSION: "8bit Progression.ogg",
|
|
ItemQuality.USEFUL: "8bit Useful.ogg",
|
|
ItemQuality.TRAP: "8bit Trap.ogg",
|
|
ItemQuality.FILLER: "8bit Filler.ogg",
|
|
}
|
|
|
|
CONFETTI_CANNON = "APQuest Confetti Cannon.ogg"
|
|
MATH_PROBLEM_STARTED_JINGLE = "APQuest Math Problem Starter Jingle.ogg"
|
|
MATH_PROBLEM_SOLVED_JINGLE = "APQuest Math Problem Solved Jingle.ogg"
|
|
VICTORY_JINGLE = "8bit Victory.ogg"
|
|
|
|
ALL_JINGLES = [
|
|
MATH_PROBLEM_SOLVED_JINGLE,
|
|
MATH_PROBLEM_STARTED_JINGLE,
|
|
CONFETTI_CANNON,
|
|
VICTORY_JINGLE,
|
|
*ITEM_JINGLES.values(),
|
|
]
|
|
|
|
BACKGROUND_MUSIC_INTRO = "APQuest Intro.ogg"
|
|
BACKGROUND_MUSIC = "APQuest BGM.ogg"
|
|
MATH_TIME_BACKGROUND_MUSIC = "APQuest Math BGM.ogg"
|
|
|
|
ALL_BGM = [
|
|
BACKGROUND_MUSIC_INTRO,
|
|
BACKGROUND_MUSIC,
|
|
MATH_TIME_BACKGROUND_MUSIC,
|
|
]
|
|
|
|
ALL_SOUNDS = [
|
|
*ALL_JINGLES,
|
|
*ALL_BGM,
|
|
]
|
|
|
|
|
|
class SoundManager:
|
|
sound_paths: dict[str, Path]
|
|
|
|
jingles: dict[str, Sound]
|
|
bgm_songs: dict[str, Sound]
|
|
|
|
active_bgm_song: str = BACKGROUND_MUSIC_INTRO
|
|
|
|
current_background_music_volume: float = 1.0
|
|
background_music_target_volume: float = 0.0
|
|
|
|
background_music_task: Task[None] | None = None
|
|
background_music_last_position: int = 0
|
|
|
|
volume_percentage: int = 0
|
|
|
|
game_started: bool
|
|
math_trap_active: bool
|
|
allow_intro_to_play: bool
|
|
|
|
def __init__(self) -> None:
|
|
self.extract_sounds()
|
|
self.populate_sounds()
|
|
|
|
self.game_started = False
|
|
self.allow_intro_to_play = False
|
|
self.math_trap_active = False
|
|
|
|
self.ensure_config()
|
|
|
|
self.background_music_task = asyncio.create_task(self.sound_manager_loop())
|
|
|
|
def ensure_config(self) -> None:
|
|
Config.adddefaultsection("APQuest")
|
|
Config.setdefault("APQuest", "volume", 50)
|
|
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
|
|
|
async def sound_manager_loop(self) -> None:
|
|
while True:
|
|
self.update_background_music()
|
|
self.do_fade()
|
|
await asyncio.sleep(0.02)
|
|
|
|
def extract_sounds(self) -> None:
|
|
# Kivy appears to have no good way of loading audio from bytes.
|
|
# So, we have to extract it out of the .apworld first
|
|
|
|
sound_paths = {}
|
|
|
|
sound_directory = make_data_directory("sounds")
|
|
|
|
for sound in ALL_SOUNDS:
|
|
sound_file_location = sound_directory / sound
|
|
|
|
sound_paths[sound] = sound_file_location
|
|
|
|
if sound_file_location.exists():
|
|
continue
|
|
|
|
with open(sound_file_location, "wb") as sound_file:
|
|
data = pkgutil.get_data(game.__name__, f"audio/{sound}")
|
|
if data is None:
|
|
logger.exception(f"Unable to extract sound {sound} to Archipelago/data")
|
|
continue
|
|
sound_file.write(cast(Buffer, data))
|
|
|
|
self.sound_paths = sound_paths
|
|
|
|
def load_audio(self, sound_filename: str) -> Sound:
|
|
audio_path = self.sound_paths[sound_filename]
|
|
|
|
sound_object = SoundLoader.load(str(audio_path.absolute()))
|
|
sound_object.seek(0)
|
|
return sound_object
|
|
|
|
def populate_sounds(self) -> None:
|
|
try:
|
|
self.jingles = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_JINGLES}
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
|
|
try:
|
|
self.bgm_songs = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_BGM}
|
|
for bgm_song in self.bgm_songs.values():
|
|
bgm_song.loop = True
|
|
bgm_song.seek(0)
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
|
|
def play_jingle(self, audio_filename: str) -> None:
|
|
higher_priority_sound_is_playing = False
|
|
|
|
for sound_name, sound in self.jingles.items():
|
|
if higher_priority_sound_is_playing: # jingles are ordered by priority, lower priority gets eaten
|
|
sound.stop()
|
|
continue
|
|
|
|
if sound_name == audio_filename:
|
|
sound.play()
|
|
self.update_background_music()
|
|
higher_priority_sound_is_playing = True
|
|
|
|
elif sound.state == "play":
|
|
higher_priority_sound_is_playing = True
|
|
|
|
def update_background_music(self) -> None:
|
|
self.update_active_song()
|
|
if any(sound.state == "play" for sound in self.jingles.values()):
|
|
self.play_background_music(False)
|
|
else:
|
|
if self.math_trap_active:
|
|
# Don't fade math trap song, it ends up feeling better
|
|
self.play_background_music(True)
|
|
else:
|
|
self.fade_background_music(True)
|
|
|
|
def play_background_music(self, play: bool = True) -> None:
|
|
if play:
|
|
self.background_music_target_volume = 1
|
|
self.set_background_music_volume(1)
|
|
else:
|
|
self.background_music_target_volume = 0
|
|
self.set_background_music_volume(0)
|
|
|
|
def set_background_music_volume(self, volume: float) -> None:
|
|
self.current_background_music_volume = volume
|
|
|
|
for song_filename, song in self.bgm_songs.items():
|
|
if song_filename != self.active_bgm_song:
|
|
song.volume = 0
|
|
continue
|
|
song.volume = volume * self.volume_percentage / 100
|
|
|
|
def fade_background_music(self, fade_in: bool = True) -> None:
|
|
if fade_in:
|
|
self.background_music_target_volume = 1
|
|
else:
|
|
self.background_music_target_volume = 0
|
|
|
|
def set_volume_percentage(self, volume_percentage: float) -> None:
|
|
volume_percentage_int = int(volume_percentage)
|
|
if self.volume_percentage != volume_percentage:
|
|
self.volume_percentage = volume_percentage_int
|
|
Config.set("APQuest", "volume", volume_percentage_int)
|
|
Config.write()
|
|
self.set_background_music_volume(self.current_background_music_volume)
|
|
|
|
for jingle in self.jingles.values():
|
|
jingle.volume = self.volume_percentage / 100
|
|
|
|
def do_fade(self) -> None:
|
|
if self.current_background_music_volume > self.background_music_target_volume:
|
|
self.set_background_music_volume(max(0.0, self.current_background_music_volume - 0.02))
|
|
if self.current_background_music_volume < self.background_music_target_volume:
|
|
self.set_background_music_volume(min(1.0, self.current_background_music_volume + 0.02))
|
|
|
|
for song_filename, song in self.bgm_songs.items():
|
|
if song_filename != self.active_bgm_song:
|
|
if song_filename == BACKGROUND_MUSIC:
|
|
# It ends up feeling better if this just always continues playing quietly after being started.
|
|
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
|
|
if self.game_started and song.state == "stop":
|
|
song.play()
|
|
song.seek(0)
|
|
continue
|
|
|
|
song.stop()
|
|
song.seek(0)
|
|
continue
|
|
|
|
if self.active_bgm_song == BACKGROUND_MUSIC_INTRO and not self.allow_intro_to_play:
|
|
song.stop()
|
|
song.seek(0)
|
|
continue
|
|
|
|
if self.current_background_music_volume != 0:
|
|
if song.state == "stop":
|
|
song.play()
|
|
song.seek(0)
|
|
|
|
def update_active_song(self) -> None:
|
|
new_active_song = self.determine_correct_song()
|
|
if new_active_song == self.active_bgm_song:
|
|
return
|
|
self.active_bgm_song = new_active_song
|
|
# reevaluate song volumes
|
|
self.set_background_music_volume(self.current_background_music_volume)
|
|
|
|
def determine_correct_song(self) -> str:
|
|
if not self.game_started:
|
|
return BACKGROUND_MUSIC_INTRO
|
|
|
|
if self.math_trap_active:
|
|
return MATH_TIME_BACKGROUND_MUSIC
|
|
|
|
return BACKGROUND_MUSIC
|