exp-runtime-player 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exp_runtime/app_icon.py +12 -0
- exp_runtime/app_info.py +16 -0
- exp_runtime/application.py +825 -0
- exp_runtime/assets/kiwi.svg +49 -0
- exp_runtime/desktop.py +726 -0
- exp_runtime/desktop_choice.py +202 -0
- exp_runtime/desktop_dialogue.py +271 -0
- exp_runtime/desktop_input.py +176 -0
- exp_runtime/desktop_loading.py +45 -0
- exp_runtime/desktop_menu.py +448 -0
- exp_runtime/desktop_message.py +123 -0
- exp_runtime/desktop_pause.py +105 -0
- exp_runtime/desktop_picker.py +68 -0
- exp_runtime/desktop_text.py +88 -0
- exp_runtime/desktop_title.py +82 -0
- exp_runtime/graphics.py +79 -0
- exp_runtime/platforms/__init__.py +1 -0
- exp_runtime/platforms/drawing.py +612 -0
- exp_runtime/platforms/ios.py +225 -0
- exp_runtime/pointer.py +25 -0
- exp_runtime/projection.py +68 -0
- exp_runtime/settings.py +46 -0
- exp_runtime/updates.py +144 -0
- exp_runtime_player-0.5.0.dist-info/METADATA +49 -0
- exp_runtime_player-0.5.0.dist-info/RECORD +29 -0
- exp_runtime_player-0.5.0.dist-info/WHEEL +5 -0
- exp_runtime_player-0.5.0.dist-info/entry_points.txt +3 -0
- exp_runtime_player-0.5.0.dist-info/licenses/LICENSE +21 -0
- exp_runtime_player-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Shared native system-menu skin for an episode's pause menu."""
|
|
2
|
+
from . import graphics as pygame
|
|
3
|
+
from .fonts import TextStyle
|
|
4
|
+
from .menu import MenuStrings
|
|
5
|
+
from .ui_assets import LayoutBank, Rect, read_ui
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PAUSE_OPEN_MS = 400
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pause_scale(elapsed_ms):
|
|
12
|
+
"""Native system-menu zoom: 0.001 -> 1, UIKit ease-in/out, 400ms."""
|
|
13
|
+
progress = max(0, min(1, elapsed_ms / PAUSE_OPEN_MS))
|
|
14
|
+
if progress in (0, 1):
|
|
15
|
+
return .001 if progress == 0 else 1.
|
|
16
|
+
# Invert the time axis of cubic-bezier(.42, 0, .58, 1).
|
|
17
|
+
low, high = 0., 1.
|
|
18
|
+
for _ in range(20):
|
|
19
|
+
t = (low + high) / 2
|
|
20
|
+
x = 3 * (1 - t)**2 * t * .42 + 3 * (1 - t) * t*t * .58 + t**3
|
|
21
|
+
if x < progress:
|
|
22
|
+
low = t
|
|
23
|
+
else:
|
|
24
|
+
high = t
|
|
25
|
+
eased = 3 * (1 - t) * t*t + t**3
|
|
26
|
+
return .001 + .999 * eased
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def draw_pause_gear(target, art, *, scale=1):
|
|
30
|
+
image = art.frame(126, 50 if getattr(art, 'gear_pressed', False) else 49)
|
|
31
|
+
if scale != 1:
|
|
32
|
+
image = pygame.transform.smoothscale(image, (round(image.get_width() * scale),
|
|
33
|
+
round(image.get_height() * scale)))
|
|
34
|
+
target.blit(image, (0, round(408 * scale)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PauseRenderer:
|
|
38
|
+
def __init__(self, library, text, art):
|
|
39
|
+
self.bank = LayoutBank.parse(read_ui(library, 14))
|
|
40
|
+
self.strings = MenuStrings.load(library)
|
|
41
|
+
self.text, self.art = text, art
|
|
42
|
+
self.canvas = pygame.Surface((320, 480), pygame.SRCALPHA).convert_alpha()
|
|
43
|
+
self.dimmer = self.canvas.copy()
|
|
44
|
+
self.dimmer.fill((0, 0, 0, 140))
|
|
45
|
+
|
|
46
|
+
def label(self, text, rect, name, size, *, center=False):
|
|
47
|
+
layout = self.text.layout(name, text, rect.width, TextStyle(size, 0, (41, 104, 221)))
|
|
48
|
+
left, top, right, bottom = layout.ink_bounds
|
|
49
|
+
x = rect.x + (rect.width - (right - left)) / 2 - left if center else rect.x
|
|
50
|
+
self.text.draw_layout(self.canvas, name, layout, x,
|
|
51
|
+
rect.y + (rect.height - (bottom - top)) / 2 - top)
|
|
52
|
+
|
|
53
|
+
def layout(self, index, bounds, *, frames=None):
|
|
54
|
+
for node, rect in self.bank.walk(index, bounds):
|
|
55
|
+
if node.kind != 1 or rect.width <= 0 or rect.height <= 0:
|
|
56
|
+
continue
|
|
57
|
+
slot, frame = node.payload
|
|
58
|
+
if frames is not None and frame not in frames:
|
|
59
|
+
continue
|
|
60
|
+
image = self.art.frame({0: 126, 3: 16}[slot], frame)
|
|
61
|
+
self.canvas.blit(pygame.transform.scale(image, (rect.width, rect.height)), (rect.x, rect.y))
|
|
62
|
+
|
|
63
|
+
def draw(self, target, *, application=True, main_menu=True, pressed=None,
|
|
64
|
+
elapsed_ms=PAUSE_OPEN_MS):
|
|
65
|
+
entries = [(self.strings[21], ('resume',))]
|
|
66
|
+
if application:
|
|
67
|
+
entries += [(self.strings[11], ('pause_page', 'options')),
|
|
68
|
+
(self.strings[128], ('pause_page', 'help'))]
|
|
69
|
+
else:
|
|
70
|
+
# The standalone story viewer has no application settings pages.
|
|
71
|
+
entries += [('Save', ('save',)), ('Load', ('load',))]
|
|
72
|
+
if main_menu:
|
|
73
|
+
entries.append((self.strings[10], ('main_menu',)))
|
|
74
|
+
# SHSWidgetMenu::solveMetrics centers the system menu in 480 pixels;
|
|
75
|
+
# UIKit heightForRowAtIndexPath returns 44 for these ordinary rows.
|
|
76
|
+
header, footer = self.bank.layouts[33].height, self.bank.layouts[36].height
|
|
77
|
+
top = (480 - header - 44 * len(entries) - footer) // 2
|
|
78
|
+
self.canvas.fill((0, 0, 0, 0))
|
|
79
|
+
self.layout(33, Rect(9, top, 302, header))
|
|
80
|
+
title = self.bank.rectangle(33, 2, Rect(9, top, 302, header))
|
|
81
|
+
self.label(self.strings[9], title, 'ArialRoundedMTBold28', 28, center=True)
|
|
82
|
+
buttons = []
|
|
83
|
+
for index, (label, command) in enumerate(entries):
|
|
84
|
+
y = top + header + index * 44
|
|
85
|
+
row = Rect(9, y, 302, 44)
|
|
86
|
+
# Native table cells occupy the 296px interior. Stretching their
|
|
87
|
+
# art to the 302px outer box painted over its three-pixel borders.
|
|
88
|
+
color = ((170, 225, 249, 243) if pressed == command else
|
|
89
|
+
(255, 242, 255, 239) if index % 2 else (239, 239, 249, 239))
|
|
90
|
+
pygame.draw.rect(self.canvas, color, (12, y, 296, 44))
|
|
91
|
+
edge = self.art.frame(16, 74)
|
|
92
|
+
for x, source in ((9, 0), (308, 299)):
|
|
93
|
+
border = edge.subsurface((source, 0, 3, 1))
|
|
94
|
+
self.canvas.blit(pygame.transform.scale(border, (3, 44)), (x, y))
|
|
95
|
+
image = self.art.frame(16, 75)
|
|
96
|
+
self.canvas.blit(pygame.transform.scale(image, (296, 1)), (12, y))
|
|
97
|
+
self.label(label, Rect(27, y + 3, 266, 38), 'ArialRoundedMTBold14', 14)
|
|
98
|
+
buttons.append((pygame.Rect(row.x, row.y, row.width, row.height), command))
|
|
99
|
+
self.layout(36, Rect(9, top + header + 44 * len(entries), 302, footer), frames={74, 79})
|
|
100
|
+
target.blit(pygame.transform.smoothscale(self.dimmer, target.get_size()), (0, 0))
|
|
101
|
+
scale = pause_scale(elapsed_ms)
|
|
102
|
+
size = tuple(max(1, round(length * scale)) for length in target.get_size())
|
|
103
|
+
image = pygame.transform.smoothscale(self.canvas, size)
|
|
104
|
+
target.blit(image, image.get_rect(center=target.get_rect().center))
|
|
105
|
+
return buttons if elapsed_ms >= PAUSE_OPEN_MS else []
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Original service-78 portrait layout, artwork, and confirmation control."""
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
from . import graphics as pygame
|
|
5
|
+
from .desktop_pause import draw_pause_gear
|
|
6
|
+
|
|
7
|
+
from .fonts import TextStyle
|
|
8
|
+
from .menu import MenuStrings
|
|
9
|
+
from .ui_assets import Rect, read_ui
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CharacterPickerRenderer:
|
|
13
|
+
def __init__(self, resources, text_renderer, artwork):
|
|
14
|
+
self.resources, self.text, self.art = resources, text_renderer, artwork
|
|
15
|
+
self.canvas = pygame.Surface((320, 480)).convert(32)
|
|
16
|
+
self.hint = None
|
|
17
|
+
|
|
18
|
+
@lru_cache(maxsize=32)
|
|
19
|
+
def portrait(self, asset, theme):
|
|
20
|
+
# The selector passes the fifth portrait flag as 1: FUN_0009bd40
|
|
21
|
+
# uses the combined selection ring (43/44), rather than dialogue rings.
|
|
22
|
+
ring = self.art.frame(126, {1: 43, 2: 44}.get(theme, 0))
|
|
23
|
+
pieces = [(ring, ring.get_rect(center=(0, 0)))]
|
|
24
|
+
image = self.art.portrait(asset, False) # Expression zero, portrait mode 1.
|
|
25
|
+
if image is not None:
|
|
26
|
+
pieces.append((image, self.art.portrait_rect(image)))
|
|
27
|
+
bounds = pieces[0][1].unionall([rect for _, rect in pieces[1:]])
|
|
28
|
+
layer = pygame.Surface(bounds.size, pygame.SRCALPHA).convert_alpha()
|
|
29
|
+
for piece, rect in pieces:
|
|
30
|
+
layer.blit(piece, rect.move(-bounds.x, -bounds.y))
|
|
31
|
+
return layer, bounds.topleft
|
|
32
|
+
|
|
33
|
+
def draw(self, target, session):
|
|
34
|
+
picker, engine = session.engine.character_picker, session.engine
|
|
35
|
+
self.canvas.fill((0, 0, 0))
|
|
36
|
+
self.art.draw_background(self.canvas, engine)
|
|
37
|
+
theme = engine.numbers.get(engine.number_key(picker.characters[0], 651), 0)
|
|
38
|
+
# FUN_000d2238/FUN_000d2558 override layout 47's root to this rectangle.
|
|
39
|
+
self.art.box(self.canvas, Rect(0, 120, 320, 240), theme)
|
|
40
|
+
for index, character in enumerate(picker.characters):
|
|
41
|
+
variants = engine.character_art_variants.get(character, [-1])
|
|
42
|
+
color = engine.numbers.get(engine.number_key(character, 651), 0)
|
|
43
|
+
layer, (ox, oy) = self.portrait(variants[0], color)
|
|
44
|
+
motion = picker.portraits[index]
|
|
45
|
+
x, y, scale = motion.pose
|
|
46
|
+
layer = pygame.transform.smoothscale(layer, (max(1, round(layer.get_width() * scale)),
|
|
47
|
+
max(1, round(layer.get_height() * scale))))
|
|
48
|
+
layer.set_alpha(motion.opacity)
|
|
49
|
+
self.canvas.blit(layer, (round(x + ox * scale), round(y + oy * scale)))
|
|
50
|
+
title = self.text.layout('PajamaHip26', session.pending.details['text'], 300,
|
|
51
|
+
TextStyle(16, -16, (255, 255, 255)))
|
|
52
|
+
self.text.draw_layout(self.canvas, 'PajamaHip26', title, 30, 67.5)
|
|
53
|
+
self.canvas.blit(self.art.frame(126, 41), (205, 347))
|
|
54
|
+
self.canvas.blit(self.art.frame(126, 47), (0, 431))
|
|
55
|
+
draw_pause_gear(self.canvas, self.art)
|
|
56
|
+
if self.hint is None:
|
|
57
|
+
self.hint = MenuStrings.load(self.resources.library)[37]
|
|
58
|
+
hint = self.text.layout('ArialRoundedMTBold11', self.hint,
|
|
59
|
+
247, TextStyle(11, 0, (185, 185, 185)))
|
|
60
|
+
self.text.draw_layout(self.canvas, 'ArialRoundedMTBold11', hint, 63 + (252 - hint.width) / 2, 460)
|
|
61
|
+
target.blit(pygame.transform.smoothscale(self.canvas, target.get_size()), (0, 0))
|
|
62
|
+
return [(pygame.Rect(0, 422, 60, 58), ('menu',))]
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def confirm_hit(point):
|
|
66
|
+
x, y = point
|
|
67
|
+
# FUN_000d2008: the explicit 50x50 area plus the flipped sprite rectangle.
|
|
68
|
+
return (210 <= x <= 260 and 330 <= y <= 380) or (205 <= x <= 275 and 344 <= y <= 389)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shared atlas drawing for the presentation-independent text layout."""
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from math import floor
|
|
6
|
+
|
|
7
|
+
from . import graphics as pygame
|
|
8
|
+
|
|
9
|
+
from .fonts import FontError, layout_text, load_font
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BitmapTextRenderer:
|
|
13
|
+
def __init__(self, library):
|
|
14
|
+
self.library = library
|
|
15
|
+
self.fonts = {}
|
|
16
|
+
self.glyph_images = OrderedDict()
|
|
17
|
+
|
|
18
|
+
def font(self, name):
|
|
19
|
+
if name not in self.fonts:
|
|
20
|
+
font = load_font(self.library, name)
|
|
21
|
+
try:
|
|
22
|
+
if getattr(self.library, 'kind', 'apk') == 'ipa':
|
|
23
|
+
from .ios_fonts import render_font
|
|
24
|
+
image = render_font(self.library, name)[1]
|
|
25
|
+
atlas = pygame.image.frombytes(image.tobytes(), image.size, 'RGBA')
|
|
26
|
+
else:
|
|
27
|
+
atlas = pygame.image.load(BytesIO(self.library.read_ui_asset('fonts/' + font.page)))
|
|
28
|
+
except pygame.error as error:
|
|
29
|
+
raise FontError(f'Cannot decode font atlas {font.page}: {error}') from error
|
|
30
|
+
width, height = atlas.get_size()
|
|
31
|
+
for glyph in font.glyphs.values():
|
|
32
|
+
rect = (glyph.x, glyph.y, glyph.width, glyph.height)
|
|
33
|
+
if any(v != int(v) for v in rect):
|
|
34
|
+
raise FontError('Fractional font atlas rectangles are not supported yet')
|
|
35
|
+
if glyph.x + glyph.width > width or glyph.y + glyph.height > height:
|
|
36
|
+
raise FontError(f'Glyph {glyph.code} exceeds actual atlas {font.page}')
|
|
37
|
+
self.fonts[name] = font, atlas.convert_alpha()
|
|
38
|
+
return self.fonts[name][0]
|
|
39
|
+
|
|
40
|
+
@lru_cache(maxsize=128)
|
|
41
|
+
def layout(self, name, text, width, style):
|
|
42
|
+
return layout_text(self.font(name), text, width, style)
|
|
43
|
+
|
|
44
|
+
def _glyph_image(self, name, placed, scale):
|
|
45
|
+
glyph = placed.glyph
|
|
46
|
+
key = name, glyph.code, placed.color, scale
|
|
47
|
+
if key not in self.glyph_images:
|
|
48
|
+
atlas = self.fonts[name][1]
|
|
49
|
+
image = atlas.subsurface(pygame.Rect(glyph.x, glyph.y, glyph.width, glyph.height)).copy()
|
|
50
|
+
# Modulate original RGB, retaining colored atlas variants and alpha.
|
|
51
|
+
image.fill((*placed.color, 255), special_flags=pygame.BLEND_RGBA_MULT)
|
|
52
|
+
size = max(1, round(glyph.width * scale)), max(1, round(glyph.height * scale))
|
|
53
|
+
if image.get_size() != size:
|
|
54
|
+
image = pygame.transform.smoothscale(image, size)
|
|
55
|
+
self.glyph_images[key] = image
|
|
56
|
+
if len(self.glyph_images) > 512:
|
|
57
|
+
self.glyph_images.popitem(last=False)
|
|
58
|
+
self.glyph_images.move_to_end(key)
|
|
59
|
+
return self.glyph_images[key]
|
|
60
|
+
|
|
61
|
+
def draw(self, surface, name, text, x, y, width, style, *, scale=1):
|
|
62
|
+
"""Draw using the current canvas placement; return the next free Y.
|
|
63
|
+
|
|
64
|
+
Normalize only the block's top ink overhang for the provisional desktop
|
|
65
|
+
panel. Native line positions, offsets, advances and colors stay in the
|
|
66
|
+
pure layout. Complete native panel anchoring is still being recovered.
|
|
67
|
+
"""
|
|
68
|
+
if scale <= 0:
|
|
69
|
+
raise FontError('Font display scale must be positive')
|
|
70
|
+
layout = self.layout(name, text, width / scale, style)
|
|
71
|
+
_, top, _, bottom = layout.ink_bounds
|
|
72
|
+
top = min(0, top)
|
|
73
|
+
for placed in layout.glyphs:
|
|
74
|
+
surface.blit(self._glyph_image(name, placed, scale),
|
|
75
|
+
(floor(x + placed.x * scale + .5), floor(y + (placed.y - top) * scale + .5)))
|
|
76
|
+
return y + (max(layout.height, bottom) - top) * scale
|
|
77
|
+
|
|
78
|
+
def draw_layout(self, surface, name, layout, x, y, *, scale=1, source_end=None):
|
|
79
|
+
"""Draw a native layout origin without moving its ink overhang."""
|
|
80
|
+
self.font(name)
|
|
81
|
+
for placed in layout.glyphs:
|
|
82
|
+
if source_end is not None and placed.index >= source_end:
|
|
83
|
+
continue
|
|
84
|
+
# Half-to-even rounding snaps adjacent glyph bearings in opposite
|
|
85
|
+
# directions at a half-pixel line origin (notably Continue's i).
|
|
86
|
+
# Use one translation-invariant pixel rule for the whole line.
|
|
87
|
+
surface.blit(self._glyph_image(name, placed, scale),
|
|
88
|
+
(floor(x + placed.x * scale + .5), floor(y + placed.y * scale + .5)))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Original episode intros and week cards, drawn from the player's APK."""
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
from . import graphics as pygame
|
|
5
|
+
from .desktop_pause import draw_pause_gear
|
|
6
|
+
|
|
7
|
+
from .atlas import AtlasFont
|
|
8
|
+
from .content import ContentError
|
|
9
|
+
from .fonts import TextStyle, layout_label
|
|
10
|
+
from .menu import MenuStrings
|
|
11
|
+
from .title_screen import title_labels
|
|
12
|
+
from .ui_assets import Rect, read_ui
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TitleRenderer:
|
|
16
|
+
def __init__(self, resources, text_renderer, artwork):
|
|
17
|
+
self.resources, self.text, self.art = resources, text_renderer, artwork
|
|
18
|
+
self.canvas = pygame.Surface((320, 480)).convert(32)
|
|
19
|
+
|
|
20
|
+
@lru_cache(maxsize=2)
|
|
21
|
+
def font(self, asset):
|
|
22
|
+
image = getattr(self.art, 'ui_image', self.art.image)(asset)
|
|
23
|
+
if image is None:
|
|
24
|
+
raise ContentError(f'Title font atlas {asset} is missing')
|
|
25
|
+
return AtlasFont.parse(read_ui(self.resources, asset + 1), Rect(0, 0, *image.get_size()))
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=64)
|
|
28
|
+
def labels(self, title, subtitle):
|
|
29
|
+
return title_labels(self.resources.dialogue_layout().bank, self.font(528), self.font(530),
|
|
30
|
+
title, subtitle)
|
|
31
|
+
|
|
32
|
+
@lru_cache(maxsize=128)
|
|
33
|
+
def label_image(self, label):
|
|
34
|
+
font = self.font(label.asset_id)
|
|
35
|
+
atlas = getattr(self.art, 'ui_image', self.art.image)(label.asset_id)
|
|
36
|
+
if not label.glyphs:
|
|
37
|
+
return None, (0, 0)
|
|
38
|
+
left, top = min(g.x for g in label.glyphs), min(g.y for g in label.glyphs)
|
|
39
|
+
right = max(g.x + font.glyph(g.character)[2] for g in label.glyphs)
|
|
40
|
+
bottom = max(g.y + font.height for g in label.glyphs)
|
|
41
|
+
image = pygame.Surface((max(1, right - left), max(1, bottom - top)), pygame.SRCALPHA).convert_alpha()
|
|
42
|
+
for g in label.glyphs:
|
|
43
|
+
x, y, width = font.glyph(g.character)
|
|
44
|
+
image.blit(atlas, (g.x - left, g.y - top), (x, y, width, font.height))
|
|
45
|
+
return image, (left, top)
|
|
46
|
+
|
|
47
|
+
def draw_label(self, label, scale_y=1.):
|
|
48
|
+
image, (left, top) = self.label_image(label)
|
|
49
|
+
if image is None:
|
|
50
|
+
return
|
|
51
|
+
sx, sy = label.scale, label.scale * scale_y
|
|
52
|
+
size = max(1, round(image.get_width() * sx)), max(1, round(image.get_height() * sy))
|
|
53
|
+
self.canvas.blit(pygame.transform.smoothscale(image, size),
|
|
54
|
+
(round(label.origin[0] + left * sx), round(label.origin[1] + top * sy)))
|
|
55
|
+
|
|
56
|
+
@lru_cache(maxsize=1)
|
|
57
|
+
def hint(self):
|
|
58
|
+
strings = MenuStrings.load(self.resources.library)
|
|
59
|
+
# 0007cbbc / 0007d4c8: font registry 11, zero content size,
|
|
60
|
+
# center/center alignment, GL position (180,25), white text.
|
|
61
|
+
font = self.text.font('ArialMT14')
|
|
62
|
+
return layout_label(font, strings[36], 0, 0, TextStyle(14, 8), 0x0a)
|
|
63
|
+
|
|
64
|
+
def draw(self, target, session):
|
|
65
|
+
details, motion = session.pending.details, session.engine.title_screen
|
|
66
|
+
self.canvas.fill((0, 0, 0))
|
|
67
|
+
background = self.art.image(details['asset_id'])
|
|
68
|
+
if background is not None and motion.background_alpha > 0:
|
|
69
|
+
background = pygame.transform.smoothscale(background, (320, 480))
|
|
70
|
+
background.set_alpha(round(255 * motion.background_alpha))
|
|
71
|
+
self.canvas.blit(background, (0, 0))
|
|
72
|
+
title, subtitle = self.labels(details['title'], details['subtitle'])
|
|
73
|
+
self.canvas.set_clip((0, 0, motion.reveal_width, 480))
|
|
74
|
+
self.draw_label(title)
|
|
75
|
+
self.canvas.set_clip(None)
|
|
76
|
+
self.draw_label(subtitle, motion.subtitle_scale_y)
|
|
77
|
+
self.canvas.blit(self.art.frame(126, 47), (0, 431))
|
|
78
|
+
draw_pause_gear(self.canvas, self.art)
|
|
79
|
+
self.text.draw_layout(self.canvas, 'ArialMT14', self.hint(), 180, 455)
|
|
80
|
+
target.blit(pygame.transform.smoothscale(self.canvas, target.get_size()), (0, 0))
|
|
81
|
+
return [(pygame.Rect(0, 422, 60, 58), ('menu',)),
|
|
82
|
+
(pygame.Rect(0, 0, 320, 480), ('continue',))]
|
exp_runtime/graphics.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Drawing/platform primitives for the shared presentation code.
|
|
2
|
+
|
|
3
|
+
Desktop uses pygame directly. A mobile host installs its backend before it
|
|
4
|
+
imports the application; menus, panel geometry and event handling stay shared.
|
|
5
|
+
"""
|
|
6
|
+
_backend = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _MenuMusic:
|
|
10
|
+
"""An SDL channel independent of the paused story's streaming music."""
|
|
11
|
+
def __init__(self, mixer):
|
|
12
|
+
self.mixer = mixer
|
|
13
|
+
self.sound = self.channel = None
|
|
14
|
+
|
|
15
|
+
def load(self, stream):
|
|
16
|
+
self.sound = self.mixer.Sound(file=stream)
|
|
17
|
+
|
|
18
|
+
def play(self):
|
|
19
|
+
self.channel = self.sound.play()
|
|
20
|
+
if self.channel is None:
|
|
21
|
+
raise ValueError('No audio channel is available for menu music')
|
|
22
|
+
|
|
23
|
+
def stop(self):
|
|
24
|
+
if self.channel is not None:
|
|
25
|
+
self.channel.stop()
|
|
26
|
+
self.sound = self.channel = None
|
|
27
|
+
|
|
28
|
+
def pause(self):
|
|
29
|
+
if self.channel is not None:
|
|
30
|
+
self.channel.pause()
|
|
31
|
+
|
|
32
|
+
def unpause(self):
|
|
33
|
+
if self.channel is not None:
|
|
34
|
+
self.channel.unpause()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_menu_music():
|
|
38
|
+
mixer = __getattr__('mixer')
|
|
39
|
+
return mixer.menu_music if hasattr(mixer, 'menu_music') else _MenuMusic(mixer)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def project_quad(target, image, points):
|
|
43
|
+
"""Project a sprite with the shared scanline rule, batching on native hosts.
|
|
44
|
+
|
|
45
|
+
Board-plane quads have horizontal top/bottom edges. Geometry and rounding
|
|
46
|
+
stay here; the platform only draws the resulting textured rows.
|
|
47
|
+
"""
|
|
48
|
+
tl, tr, br, bl = points
|
|
49
|
+
top, bottom = round(tl[1]), round(bl[1])
|
|
50
|
+
height = max(1, bottom - top)
|
|
51
|
+
rows = []
|
|
52
|
+
for y in range(height):
|
|
53
|
+
t = (y + .5) / height
|
|
54
|
+
left = tl[0] + (bl[0] - tl[0]) * t
|
|
55
|
+
right = tr[0] + (br[0] - tr[0]) * t
|
|
56
|
+
rows.append((round(left), max(1, round(right - left))))
|
|
57
|
+
batch = getattr(__getattr__('draw'), 'textured_rows', None)
|
|
58
|
+
if batch is not None:
|
|
59
|
+
batch(target, image, top, rows)
|
|
60
|
+
return
|
|
61
|
+
transform = __getattr__('transform')
|
|
62
|
+
source = transform.smoothscale(image, (image.get_width(), height))
|
|
63
|
+
for y, (left, width) in enumerate(rows):
|
|
64
|
+
row = source.subsurface((0, y, source.get_width(), 1))
|
|
65
|
+
target.blit(transform.smoothscale(row, (width, 1)), (left, top + y))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def install(backend):
|
|
69
|
+
global _backend
|
|
70
|
+
previous, _backend = _backend, backend
|
|
71
|
+
return previous
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __getattr__(name):
|
|
75
|
+
global _backend
|
|
76
|
+
if _backend is None:
|
|
77
|
+
import pygame
|
|
78
|
+
_backend = pygame
|
|
79
|
+
return getattr(_backend, name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Host adapters. Game behavior and layout live in the shared runtime."""
|