Loading files
e14740673This file is pinned to the selected release. Download the original to open it in your design tool.
// Generated by compose.mjs from mini_app_sample and stackchan_catch.
// Source SHA-256: b49c39a53f3c49d72b87f69760d20e8a5e67d15cfa016c187495929889037f3b
// Run: node mods/examples/stackchan_minigames/compose.mjs
import type { MiniAppDefinition } from 'capabilities'
import 'piu/MC'
import type { Port as PiuPort, Texture as PiuTexture } from 'piu/MC'
const jumpDefinitions = (() => {
const FRAME_INTERVAL_MS = 40
const SPRITE_SIZE = 32
const PLAYER_X = 48
const GROUND_BOTTOM = 18
const INITIAL_SPEED = 4
const MAX_SPEED = 8
const JUMP_VELOCITY = -11
const GRAVITY = 1
const COLOR_BACKGROUND = '#f7f3df'
const COLOR_GROUND = '#29323c'
const COLOR_OBSTACLE = '#397b44'
const COLOR_CLOUD = '#ffffff'
const COLOR_SCORE = '#29323c'
const COLOR_ACCENT = '#dc5b45'
const COLOR_OVERLAY = '#f7f3dfe6'
const DIGIT_MASKS = Object.freeze([
0b111101101101111, 0b010110010010111, 0b111001111100111, 0b111001111001111, 0b101101111001001, 0b111100111001111,
0b111100111101111, 0b111001001001001, 0b111101111101111, 0b111101111001111,
])
const OBSTACLE_HEIGHTS = Object.freeze([28, 36, 44, 32, 40])
const OBSTACLE_WIDTHS = Object.freeze([14, 18, 16, 20, 14])
const OBSTACLE_GAPS = Object.freeze([88, 120, 104, 144, 112])
const background = new Skin({ fill: COLOR_BACKGROUND })
let stackchanTexture: PiuTexture | undefined
function getStackchanTexture(): PiuTexture {
stackchanTexture ??= new Texture('stack-chan.png')
return stackchanTexture
}
function intersects(
x: number,
y: number,
width: number,
height: number,
targetX: number,
targetY: number,
targetWidth: number,
targetHeight: number,
): boolean {
return x < targetX + targetWidth && x + width > targetX && y < targetY + targetHeight && y + height > targetY
}
class StackchanJumpBehavior extends Behavior {
#groundY = 0
#playerY = 0
#velocityY = 0
#onGround = true
#obstacleX = 0
#obstacleWidth = OBSTACLE_WIDTHS[0]
#obstacleHeight = OBSTACLE_HEIGHTS[0]
#spawnIndex = 0
#score = 0
#speed = INITIAL_SPEED
#animationTick = 0
#animationFrame = 0
#hasJumped = false
#gameOver = false
get jumping(): boolean {
return !this.#onGround && !this.#gameOver
}
get score(): number {
return this.#score
}
onDisplaying(port: PiuPort): void {
port.interval = FRAME_INTERVAL_MS
this.#reset(port)
port.start()
}
onUndisplaying(port: PiuPort): void {
port.stop()
}
onTouchBegan(port: PiuPort): void {
if (this.#gameOver) {
this.#reset(port)
port.start()
return
}
if (!this.#onGround) return
this.#hasJumped = true
this.#onGround = false
this.#velocityY = JUMP_VELOCITY
this.#animationFrame = 0
port.invalidate()
}
onTouchEnded(): void {}
onTimeChanged(port: PiuPort): void {
if (this.#gameOver) return
const oldPlayerY = this.#playerY
const oldObstacleX = this.#obstacleX
if (!this.#onGround) {
this.#velocityY += GRAVITY
this.#playerY += this.#velocityY
const floorY = this.#groundY - SPRITE_SIZE
if (this.#playerY >= floorY) {
this.#playerY = floorY
this.#velocityY = 0
this.#onGround = true
}
}
this.#obstacleX -= this.#speed
this.#animationTick += 1
if (this.#animationTick >= 3) {
this.#animationTick = 0
this.#animationFrame = (this.#animationFrame + 1) & 3
}
if (this.#obstacleX + this.#obstacleWidth < 0) {
this.#score += 1
this.#speed = Math.min(MAX_SPEED, INITIAL_SPEED + (this.#score >> 2))
this.#spawnObstacle(port)
port.invalidate()
return
}
if (this.#collides()) {
this.#gameOver = true
port.stop()
port.invalidate()
return
}
this.#invalidateSprite(port, oldPlayerY)
this.#invalidateSprite(port, this.#playerY)
this.#invalidateObstacle(port, oldObstacleX)
this.#invalidateObstacle(port, this.#obstacleX)
}
onDraw(port: PiuPort, x = 0, y = 0, width = port.width, height = port.height): void {
port.fillColor(COLOR_BACKGROUND, x, y, width, height)
this.#drawClouds(port, x, y, width, height)
this.#drawGround(port, x, y, width, height)
this.#drawObstacle(port, x, y, width, height)
this.#drawPlayer(port, x, y, width, height)
this.#drawScore(port, x, y, width, height)
if (!this.#hasJumped && !this.#gameOver) this.#drawJumpHint(port, x, y, width, height)
if (this.#gameOver) this.#drawGameOver(port)
}
#reset(port: PiuPort): void {
this.#groundY = port.height - GROUND_BOTTOM
this.#playerY = this.#groundY - SPRITE_SIZE
this.#velocityY = 0
this.#onGround = true
this.#spawnIndex = 0
this.#score = 0
this.#speed = INITIAL_SPEED
this.#animationTick = 0
this.#animationFrame = 0
this.#hasJumped = false
this.#gameOver = false
this.#spawnObstacle(port, true)
port.invalidate()
}
#spawnObstacle(port: PiuPort, initial = false): void {
const index = this.#spawnIndex
this.#obstacleWidth = OBSTACLE_WIDTHS[index]
this.#obstacleHeight = OBSTACLE_HEIGHTS[index]
this.#obstacleX = port.width + (initial ? 72 : OBSTACLE_GAPS[index])
this.#spawnIndex = (index + 1) % OBSTACLE_HEIGHTS.length
}
#collides(): boolean {
const playerLeft = PLAYER_X + 6
const playerRight = PLAYER_X + SPRITE_SIZE - 5
const playerTop = this.#playerY + 4
const playerBottom = this.#playerY + SPRITE_SIZE - 2
const obstacleTop = this.#groundY - this.#obstacleHeight
return (
playerRight > this.#obstacleX &&
playerLeft < this.#obstacleX + this.#obstacleWidth &&
playerBottom > obstacleTop &&
playerTop < this.#groundY
)
}
#invalidateSprite(port: PiuPort, playerY: number): void {
this.#invalidateArea(port, PLAYER_X - 1, playerY - 1, SPRITE_SIZE + 2, SPRITE_SIZE + 2)
}
#invalidateObstacle(port: PiuPort, obstacleX: number): void {
this.#invalidateArea(
port,
obstacleX - 2,
this.#groundY - this.#obstacleHeight - 1,
this.#obstacleWidth + 4,
this.#obstacleHeight + 2,
)
}
#invalidateArea(port: PiuPort, x: number, y: number, width: number, height: number): void {
const left = Math.max(0, x)
const top = Math.max(0, y)
const right = Math.min(port.width, x + width)
const bottom = Math.min(port.height, y + height)
if (right > left && bottom > top) port.invalidate(left, top, right - left, bottom - top)
}
#drawClouds(port: PiuPort, x: number, y: number, width: number, height: number): void {
if (!intersects(x, y, width, height, 208, 20, 96, 46)) return
port.fillColor(COLOR_CLOUD, 208, 28, 48, 8)
port.fillColor(COLOR_CLOUD, 218, 20, 28, 8)
port.fillColor(COLOR_CLOUD, 274, 60, 30, 6)
port.fillColor(COLOR_CLOUD, 280, 54, 18, 6)
}
#drawGround(port: PiuPort, x: number, y: number, width: number, height: number): void {
if (!intersects(x, y, width, height, 0, this.#groundY, port.width, 12)) return
port.fillColor(COLOR_GROUND, 0, this.#groundY, port.width, 3)
port.fillColor(COLOR_GROUND, 24, this.#groundY + 8, 18, 2)
port.fillColor(COLOR_GROUND, 116, this.#groundY + 10, 26, 2)
port.fillColor(COLOR_GROUND, 244, this.#groundY + 7, 20, 2)
}
#drawPlayer(port: PiuPort, x: number, y: number, width: number, height: number): void {
if (!intersects(x, y, width, height, PLAYER_X, this.#playerY, SPRITE_SIZE, SPRITE_SIZE)) return
const sourceY = this.#onGround && !this.#gameOver ? 0 : SPRITE_SIZE
port.drawTexture(
getStackchanTexture(),
'#ffffff',
PLAYER_X,
this.#playerY,
this.#animationFrame * SPRITE_SIZE,
sourceY,
SPRITE_SIZE,
SPRITE_SIZE,
)
}
#drawObstacle(port: PiuPort, x: number, y: number, width: number, height: number): void {
const obstacleX = this.#obstacleX
const top = this.#groundY - this.#obstacleHeight
if (!intersects(x, y, width, height, obstacleX, top, this.#obstacleWidth, this.#obstacleHeight)) return
const stemX = obstacleX + ((this.#obstacleWidth - 8) >> 1)
port.fillColor(COLOR_OBSTACLE, stemX, top, 8, this.#obstacleHeight)
port.fillColor(COLOR_OBSTACLE, obstacleX, top + 10, 6, 8)
port.fillColor(COLOR_OBSTACLE, obstacleX, top + 10, 3, 15)
port.fillColor(COLOR_OBSTACLE, obstacleX + this.#obstacleWidth - 6, top + 17, 6, 7)
port.fillColor(COLOR_OBSTACLE, obstacleX + this.#obstacleWidth - 3, top + 10, 3, 14)
}
#drawScore(port: PiuPort, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void {
if (!intersects(dirtyX, dirtyY, dirtyWidth, dirtyHeight, port.width - 34, 12, 30, 10)) return
let divisor = 100
let x = port.width - 34
const value = this.#score % 1000
while (divisor >= 1) {
const digit = Math.floor(value / divisor) % 10
this.#drawDigit(port, digit, x, 12)
x += 10
divisor = Math.floor(divisor / 10)
}
}
#drawDigit(port: PiuPort, digit: number, x: number, y: number): void {
const mask = DIGIT_MASKS[digit]
let bit = 1 << 14
for (let row = 0; row < 5; row += 1) {
for (let column = 0; column < 3; column += 1) {
if (mask & bit) port.fillColor(COLOR_SCORE, x + column * 2, y + row * 2, 2, 2)
bit >>= 1
}
}
}
#drawJumpHint(port: PiuPort, x: number, y: number, width: number, height: number): void {
if (!intersects(x, y, width, height, 9, 23, 20, 33)) return
port.fillColor(COLOR_ACCENT, 17, 31, 4, 18)
port.fillColor(COLOR_ACCENT, 11, 31, 16, 4)
port.fillColor(COLOR_ACCENT, 14, 27, 10, 4)
port.fillColor(COLOR_ACCENT, 17, 23, 4, 4)
port.fillColor(COLOR_ACCENT, 9, 53, 20, 3)
}
#drawGameOver(port: PiuPort): void {
const left = Math.floor((port.width - 112) / 2)
const top = Math.floor((port.height - 56) / 2)
port.fillColor(COLOR_OVERLAY, 0, 0, port.width, port.height)
port.fillColor(COLOR_GROUND, left, top, 112, 56)
port.fillColor(COLOR_ACCENT, left + 3, top + 3, 106, 3)
port.fillColor(COLOR_ACCENT, left + 3, top + 50, 106, 3)
port.fillColor(COLOR_CLOUD, left + 53, top + 13, 6, 19)
port.fillColor(COLOR_CLOUD, left + 53, top + 36, 6, 6)
port.fillColor(COLOR_ACCENT, left + 18, top + 25, 18, 4)
port.fillColor(COLOR_ACCENT, left + 18, top + 25, 4, 14)
port.fillColor(COLOR_ACCENT, left + 78, top + 25, 18, 4)
port.fillColor(COLOR_ACCENT, left + 92, top + 15, 4, 14)
}
}
const sample: MiniAppDefinition = Object.freeze({
id: 'sample.stackchan-jump',
title: 'スタックチャン JUMP',
icon: 'play',
create() {
return new Container(null, {
left: 0,
right: 0,
top: 0,
bottom: 0,
skin: background,
contents: [
new Port(null, {
left: 0,
right: 0,
top: 0,
bottom: 0,
active: true,
Behavior: StackchanJumpBehavior,
}),
],
})
},
})
return Object.freeze([sample])
})()
const catchDefinitions = (() => {
type PlayerPosition = 0 | 1 | 2
type DropLane = 0 | 1 | 2
type DropStep = 0 | 1 | 2 | 3 | 4
type DropKind = 'screw' | 'm5stack' | 'bubble' | 'bomb'
type DropSpeed = 'fast' | 'steady' | 'slow'
type GamePhase = 'title' | 'playing' | 'gameover'
interface Drop {
lane: DropLane
step: DropStep
kind: DropKind
active: boolean
speed: DropSpeed
ticksUntilAdvance: number
landingTick: number
}
interface GameState {
phase: GamePhase
player: PlayerPosition
drops: Drop[]
score: number
misses: number
tickInterval: number
ticks: number
spawnIndex: number
lastLandingTick: number
}
const INITIAL_TICK_INTERVAL_MS = 600
const MIN_TICK_INTERVAL_MS = 250
const SPEED_UP_EVERY_SCORE = 5
const SPEED_UP_STEP_MS = 50
const MIN_LANDING_GAP_TICKS = 3
const PLAYER_SIZE = 48
const ITEM_SIZE = 24
const MISS_SIZE = 12
const PLAYER_Y = 125
const CONTROL_Y = 178
const LANE_CENTERS = Object.freeze([52, 160, 268] as const)
const DROP_Y = Object.freeze([31, 50, 69, 88, 107] as const)
interface DropSpec {
lane: DropLane
kind: DropKind
speed: DropSpeed
}
const STEP_DELAYS: Readonly<Record<DropSpeed, readonly number[]>> = Object.freeze({
fast: Object.freeze([1, 1, 1, 1, 1]),
steady: Object.freeze([1, 1, 2, 1, 1]),
slow: Object.freeze([2, 2, 2, 2, 1]),
})
const DROP_SEQUENCE: readonly DropSpec[] = Object.freeze([
{ lane: 0, kind: 'screw', speed: 'slow' },
{ lane: 1, kind: 'bubble', speed: 'fast' },
{ lane: 2, kind: 'bomb', speed: 'steady' },
{ lane: 0, kind: 'm5stack', speed: 'fast' },
{ lane: 1, kind: 'screw', speed: 'steady' },
{ lane: 2, kind: 'bomb', speed: 'fast' },
{ lane: 0, kind: 'bubble', speed: 'steady' },
{ lane: 2, kind: 'm5stack', speed: 'fast' },
{ lane: 1, kind: 'bomb', speed: 'steady' },
{ lane: 0, kind: 'screw', speed: 'fast' },
{ lane: 2, kind: 'bubble', speed: 'steady' },
{ lane: 1, kind: 'm5stack', speed: 'fast' },
])
const COLOR_BACKGROUND = '#d8deb7'
const COLOR_GHOST = '#b7be99'
const COLOR_INK = '#343a31'
const PLAYER_TEXTURE_NAMES = Object.freeze(['player-left.png', 'player-center.png', 'player-right.png'] as const)
const ITEM_TEXTURE_NAMES = Object.freeze({
bomb: 'bomb.png',
bubble: 'bubble.png',
m5stack: 'm5stack.png',
screw: 'screw.png',
})
type MoveDirection = -1 | 1
const background = new Skin({ fill: COLOR_BACKGROUND })
const hudStyle = new Style({ font: 'k8x12-12', horizontal: 'left', vertical: 'middle' })
const panelStyle = new Style({ font: 'k8x12-24', horizontal: 'center', vertical: 'middle' })
const panelHintStyle = new Style({ font: 'k8x12-12', horizontal: 'center', vertical: 'middle' })
const controlStyle = new Style({ font: 'k8x12-12', horizontal: 'center', vertical: 'middle' })
const playerTextures: Array<PiuTexture | undefined> = [undefined, undefined, undefined]
const itemTextures: Partial<Record<DropKind, PiuTexture>> = {}
let missTexture: PiuTexture | undefined
function inactiveDrop(): Drop {
return {
lane: 1,
step: 0,
kind: 'screw',
active: false,
speed: 'fast',
ticksUntilAdvance: 1,
landingTick: 0,
}
}
function fallDuration(speed: DropSpeed): number {
return STEP_DELAYS[speed].reduce((total, delay) => total + delay, 0)
}
function canSpawnDrop(state: GameState, spec: DropSpec): boolean {
const landingTick = state.ticks + fallDuration(spec.speed)
if (landingTick - state.lastLandingTick < MIN_LANDING_GAP_TICKS) return false
for (const drop of state.drops) {
if (!drop.active) continue
if (Math.abs(drop.landingTick - landingTick) < MIN_LANDING_GAP_TICKS) return false
}
return true
}
function spawnNextDrop(state: GameState, force = false): boolean {
if (state.drops.every((drop) => drop.active)) return false
const spec = DROP_SEQUENCE[state.spawnIndex % DROP_SEQUENCE.length]
if (!force && !canSpawnDrop(state, spec)) return false
const index = state.drops[0].active ? 1 : 0
const delays = STEP_DELAYS[spec.speed]
state.drops[index] = {
lane: spec.lane,
step: 0,
kind: spec.kind,
active: true,
speed: spec.speed,
ticksUntilAdvance: delays[0],
landingTick: state.ticks + fallDuration(spec.speed),
}
state.spawnIndex += 1
return true
}
function createGameState(): GameState {
return {
phase: 'title',
player: 1,
drops: [inactiveDrop(), inactiveDrop()],
score: 0,
misses: 0,
tickInterval: INITIAL_TICK_INTERVAL_MS,
ticks: 0,
spawnIndex: 0,
lastLandingTick: -MIN_LANDING_GAP_TICKS,
}
}
function startGame(state: GameState): void {
state.phase = 'playing'
state.player = 1
state.drops = [inactiveDrop(), inactiveDrop()]
state.score = 0
state.misses = 0
state.tickInterval = INITIAL_TICK_INTERVAL_MS
state.ticks = 0
state.spawnIndex = 0
state.lastLandingTick = -MIN_LANDING_GAP_TICKS
spawnNextDrop(state, true)
}
function movePlayer(state: GameState, direction: MoveDirection): void {
const next = state.player + direction
state.player = Math.max(0, Math.min(2, next)) as PlayerPosition
}
function advanceDrops(state: GameState): readonly boolean[] {
const landed = [false, false]
for (let index = 0; index < state.drops.length; index += 1) {
const drop = state.drops[index]
if (!drop.active) continue
drop.ticksUntilAdvance -= 1
if (drop.ticksUntilAdvance > 0) continue
if (drop.step === 4) landed[index] = true
else {
drop.step = (drop.step + 1) as DropStep
drop.ticksUntilAdvance = STEP_DELAYS[drop.speed][drop.step]
}
}
return landed
}
function resolveCatchOrMiss(state: GameState, landed: readonly boolean[]): void {
for (let index = 0; index < state.drops.length; index += 1) {
const drop = state.drops[index]
if (!landed[index] || !drop.active || drop.step !== 4) continue
drop.active = false
state.lastLandingTick = state.ticks
if (drop.kind === 'bomb') {
if (drop.lane === state.player) state.misses += 1
} else if (drop.lane === state.player) state.score += 1
else state.misses += 1
if (state.misses >= 3) {
state.phase = 'gameover'
return
}
}
}
function spawnDropsIfNeeded(state: GameState): void {
const activeCount = state.drops.filter((drop) => drop.active).length
if (activeCount >= 2) return
spawnNextDrop(state, activeCount === 0)
}
function increaseDifficulty(state: GameState): void {
const speedUps = Math.floor(state.score / SPEED_UP_EVERY_SCORE)
state.tickInterval = Math.max(MIN_TICK_INTERVAL_MS, INITIAL_TICK_INTERVAL_MS - speedUps * SPEED_UP_STEP_MS)
}
function updateGame(state: GameState): void {
if (state.phase !== 'playing') return
state.ticks += 1
const landed = advanceDrops(state)
resolveCatchOrMiss(state, landed)
if (state.phase !== 'playing') return
spawnDropsIfNeeded(state)
increaseDifficulty(state)
}
function getPlayerTexture(position: PlayerPosition): PiuTexture {
playerTextures[position] ??= new Texture(PLAYER_TEXTURE_NAMES[position])
return playerTextures[position]
}
function getItemTexture(kind: DropKind): PiuTexture {
itemTextures[kind] ??= new Texture(ITEM_TEXTURE_NAMES[kind])
return itemTextures[kind]
}
function getMissTexture(): PiuTexture {
missTexture ??= new Texture('miss.png')
return missTexture
}
class StackchanCatchBehavior extends Behavior {
#state = createGameState()
get state(): GameState {
return this.#state
}
onCreate(_port: PiuPort): void {
this.#state = createGameState()
}
onDisplaying(port: PiuPort): void {
port.interval = this.#state.tickInterval
if (this.#state.phase === 'playing') port.start()
port.invalidate()
}
onUndisplaying(port: PiuPort): void {
port.stop()
}
onTouchBegan(port: PiuPort, _id: number, x: number): void {
const third = port.width / 3
if (this.#state.phase === 'playing') {
if (x < third) movePlayer(this.#state, -1)
else if (x >= third * 2) movePlayer(this.#state, 1)
else return
port.invalidate()
return
}
if (x < third || x >= third * 2) return
startGame(this.#state)
port.interval = this.#state.tickInterval
port.start()
port.invalidate()
}
onTouchEnded(): void {}
onTimeChanged(port: PiuPort): void {
updateGame(this.#state)
port.interval = this.#state.tickInterval
if (this.#state.phase === 'gameover') port.stop()
port.invalidate()
}
onDraw(port: PiuPort): void {
port.fillColor(COLOR_BACKGROUND, 0, 0, port.width, port.height)
this.#drawFrame(port)
this.#drawHud(port)
this.#drawDrops(port)
this.#drawPlayer(port)
this.#drawControls(port)
if (this.#state.phase === 'title') this.#drawTitle(port)
else if (this.#state.phase === 'gameover') this.#drawGameOver(port)
}
#drawFrame(port: PiuPort): void {
port.fillColor(COLOR_INK, 4, 3, port.width - 8, 2)
port.fillColor(COLOR_INK, 4, port.height - 5, port.width - 8, 2)
port.fillColor(COLOR_GHOST, 4, 5, 2, port.height - 10)
port.fillColor(COLOR_GHOST, port.width - 6, 5, 2, port.height - 10)
for (const center of LANE_CENTERS) {
port.fillColor(COLOR_GHOST, center - 1, 31, 2, 94)
}
}
#drawHud(port: PiuPort): void {
const score = (this.#state.score % 1000).toString().padStart(3, '0')
port.drawString(`SCORE ${score}`, hudStyle, COLOR_INK, 10, 6, 94, 22)
port.drawString('CATCH', hudStyle, COLOR_INK, 125, 6, 64, 22)
port.drawString('MISS', hudStyle, COLOR_INK, 211, 6, 42, 22)
for (let index = 0; index < 3; index += 1) {
const color = index < this.#state.misses ? COLOR_INK : COLOR_GHOST
port.drawTexture(getMissTexture(), color, 260 + index * 16, 8, 0, 0, MISS_SIZE, MISS_SIZE)
}
}
#drawDrops(port: PiuPort): void {
for (const drop of this.#state.drops) {
if (!drop.active) continue
const texture = getItemTexture(drop.kind)
const left = LANE_CENTERS[drop.lane] - ITEM_SIZE / 2
for (const top of DROP_Y) {
port.fillColor(COLOR_GHOST, LANE_CENTERS[drop.lane] - 6, top + 11, 12, 2)
}
port.drawTexture(texture, COLOR_INK, left, DROP_Y[drop.step], 0, 0, ITEM_SIZE, ITEM_SIZE)
if (drop.kind === 'bomb') {
port.drawString('!', controlStyle, COLOR_INK, left - 9, DROP_Y[drop.step], 8, ITEM_SIZE)
}
}
}
#drawPlayer(port: PiuPort): void {
for (let position = 0; position < 3; position += 1) {
const typedPosition = position as PlayerPosition
port.fillColor(COLOR_GHOST, LANE_CENTERS[typedPosition] - 18, PLAYER_Y + PLAYER_SIZE + 1, 36, 2)
}
const position = this.#state.player
port.drawTexture(
getPlayerTexture(position),
COLOR_INK,
LANE_CENTERS[position] - PLAYER_SIZE / 2,
PLAYER_Y,
0,
0,
PLAYER_SIZE,
PLAYER_SIZE,
)
port.fillColor(COLOR_INK, LANE_CENTERS[position] - 18, PLAYER_Y + PLAYER_SIZE + 1, 36, 2)
}
#drawControls(port: PiuPort): void {
const playing = this.#state.phase === 'playing'
drawControl(port, 8, 96, 'LEFT', playing)
drawControl(port, 112, 96, 'START', !playing)
drawControl(port, 216, 96, 'RIGHT', playing)
}
#drawTitle(port: PiuPort): void {
drawPanel(port, 82, 61, 156, 60)
port.drawString('CATCH', panelStyle, COLOR_INK, 85, 66, 150, 30)
port.drawString('TAP CENTER', panelHintStyle, COLOR_INK, 85, 95, 150, 18)
}
#drawGameOver(port: PiuPort): void {
drawPanel(port, 66, 54, 188, 78)
port.drawString('GAME OVER', panelStyle, COLOR_INK, 69, 61, 182, 34)
port.drawString('TAP CENTER', panelHintStyle, COLOR_INK, 69, 104, 182, 18)
}
}
function drawPanel(port: PiuPort, x: number, y: number, width: number, height: number): void {
port.fillColor(COLOR_BACKGROUND, x, y, width, height)
port.fillColor(COLOR_INK, x, y, width, 3)
port.fillColor(COLOR_INK, x, y + height - 3, width, 3)
port.fillColor(COLOR_INK, x, y, 3, height)
port.fillColor(COLOR_INK, x + width - 3, y, 3, height)
}
function drawControl(port: PiuPort, x: number, width: number, label: string, enabled: boolean): void {
const color = enabled ? COLOR_INK : COLOR_GHOST
port.fillColor(color, x, CONTROL_Y, width, 1)
port.fillColor(color, x, CONTROL_Y + 14, width, 1)
port.fillColor(color, x, CONTROL_Y, 1, 15)
port.fillColor(color, x + width - 1, CONTROL_Y, 1, 15)
port.drawString(label, controlStyle, color, x + 1, CONTROL_Y, width - 2, 15)
}
const definition: MiniAppDefinition = Object.freeze({
id: 'sample.stackchan-catch',
title: 'スタックチャン CATCH',
icon: 'play',
create() {
return new Container(null, {
left: 0,
right: 0,
top: 0,
bottom: 0,
skin: background,
contents: [
new Port(null, {
left: 0,
right: 0,
top: 0,
bottom: 0,
active: true,
Behavior: StackchanCatchBehavior,
}),
],
})
},
})
return Object.freeze([definition])
})()
const definitions: readonly MiniAppDefinition[] = Object.freeze([...jumpDefinitions, ...catchDefinitions])
export default definitions