Files
Archipelago/worlds/apquest/client/graphics.py
NewSoupVi 88dc135960 APQuest: Various fixes (#6079)
* Import Buffer from typing_extensions instead of collections.abc for 3.11 compat

* always re-set sound volumes before playing

* fix game window scaling if parent is vertical

* make default volume lower
2026-03-30 00:32:06 +02:00

181 lines
7.2 KiB
Python

import pkgutil
from enum import Enum
from io import BytesIO
from typing import Literal, NamedTuple, Protocol, cast
from kivy.uix.image import CoreImage
from typing_extensions import Buffer
from CommonClient import logger
from .. import game
from ..game.graphics import Graphic
# The import "from kivy.graphics.texture import Texture" does not work correctly.
# We never need the class directly, so we need to use a protocol.
class Texture(Protocol):
mag_filter: Literal["nearest"]
def get_region(self, x: int, y: int, w: int, h: int) -> "Texture": ...
class RelatedTexture(NamedTuple):
base_texture_file: str
x: int
y: int
width: int
height: int
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
Graphic.BUSH: RelatedTexture("inanimates.png", 16, 16, 16, 16),
Graphic.KEY_DOOR: RelatedTexture("inanimates.png", 32, 16, 16, 16),
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
Graphic.ITEMS_TEXT: "items_text.png",
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
Graphic.THREE: RelatedTexture("numbers.png", 48, 16, 16, 16),
Graphic.FOUR: RelatedTexture("numbers.png", 64, 16, 16, 16),
Graphic.FIVE: RelatedTexture("numbers.png", 0, 0, 16, 16),
Graphic.SIX: RelatedTexture("numbers.png", 16, 0, 16, 16),
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
}
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
class PlayerSprite(Enum):
HUMAN = 0
DUCK = 1
HORSE = 2
CAT = 3
UNKNOWN = -1
PLAYER_GRAPHICS = {
Graphic.PLAYER_DOWN: {
PlayerSprite.HUMAN: RelatedTexture("human.png", 0, 16, 16, 16),
PlayerSprite.DUCK: RelatedTexture("duck.png", 0, 16, 16, 16),
PlayerSprite.HORSE: RelatedTexture("horse.png", 0, 16, 16, 16),
PlayerSprite.CAT: RelatedTexture("cat.png", 0, 16, 16, 16),
},
Graphic.PLAYER_UP: {
PlayerSprite.HUMAN: RelatedTexture("human.png", 16, 0, 16, 16),
PlayerSprite.DUCK: RelatedTexture("duck.png", 16, 0, 16, 16),
PlayerSprite.HORSE: RelatedTexture("horse.png", 16, 0, 16, 16),
PlayerSprite.CAT: RelatedTexture("cat.png", 16, 0, 16, 16),
},
Graphic.PLAYER_LEFT: {
PlayerSprite.HUMAN: RelatedTexture("human.png", 16, 16, 16, 16),
PlayerSprite.DUCK: RelatedTexture("duck.png", 16, 16, 16, 16),
PlayerSprite.HORSE: RelatedTexture("horse.png", 16, 16, 16, 16),
PlayerSprite.CAT: RelatedTexture("cat.png", 16, 16, 16, 16),
},
Graphic.PLAYER_RIGHT: {
PlayerSprite.HUMAN: RelatedTexture("human.png", 0, 0, 16, 16),
PlayerSprite.DUCK: RelatedTexture("duck.png", 0, 0, 16, 16),
PlayerSprite.HORSE: RelatedTexture("horse.png", 0, 0, 16, 16),
PlayerSprite.CAT: RelatedTexture("cat.png", 0, 0, 16, 16),
},
}
ALL_GRAPHICS = [
BACKGROUND_TILE,
*IMAGE_GRAPHICS.values(),
*[graphic for sub_dict in PLAYER_GRAPHICS.values() for graphic in sub_dict.values()],
]
_textures: dict[str | RelatedTexture, Texture] = {}
def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Texture:
if texture_identifier in _textures:
return _textures[texture_identifier]
if isinstance(texture_identifier, str):
image_data = pkgutil.get_data(game.__name__, f"graphics/{texture_identifier}")
if image_data is None:
raise RuntimeError(f'Could not find file "graphics/{texture_identifier}" for texture {texture_identifier}')
image_bytes = BytesIO(cast(Buffer, image_data))
texture = cast(Texture, CoreImage(image_bytes, ext="png").texture)
texture.mag_filter = "nearest"
_textures[texture_identifier] = texture
return texture
base_texture_filename, x, y, w, h = texture_identifier
base_texture = get_texture_by_identifier(base_texture_filename)
sub_texture = base_texture.get_region(x, y, w, h)
sub_texture.mag_filter = "nearest"
_textures[texture_identifier] = sub_texture
return sub_texture
def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
if graphic == Graphic.EMPTY:
return None
if graphic == "Grass":
return get_texture_by_identifier(BACKGROUND_TILE)
if graphic in IMAGE_GRAPHICS:
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
if graphic in PLAYER_GRAPHICS:
if player_sprite is None:
raise ValueError("Tried to load a player graphic without specifying a player_sprite")
return get_texture_by_identifier(PLAYER_GRAPHICS[graphic][player_sprite])
logger.exception(f"Tried to load unknown graphic {graphic}.")
return get_texture(Graphic.UNKNOWN)