pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
554 lines (495 loc) • 18.7 kB
text/typescript
/**
* Game transitions and dialog
**/
namespace game {
/**
* Determines if diagnostics are shown
*/
export let debug = false;
export let stats = false;
export enum ScoringType {
//% block="high score"
HighScore,
//% block="low score"
LowScore,
//% block="none"
None
}
// To stay synchronized with https://github.com/microsoft/pxt/blob/stable8.5/webapp/src/components/ImageEditor/sprite/Palette.tsx#L98.
/**
* The available colors for Arcade.
* NOTE: If the color palette is changed, these values will change along with it.
**/
export enum Color {
Transparent = 0,
White = 1,
Red = 2,
Pink = 3,
Orange = 4,
Yellow = 5,
Teal = 6,
Green = 7,
Blue = 8,
LightBlue = 9,
Purple = 0xa,
LightPurple = 0xb,
DarkPurple = 0xc,
Tan = 0xd,
Brown = 0xe,
Black = 0xf
}
export class GameOverConfig {
scoringType: ScoringType;
winEffect: effects.BackgroundEffect;
loseEffect: effects.BackgroundEffect;
loseSound: music.Playable;
winSound: music.Playable;
loseSoundLooping: boolean;
winSoundLooping: boolean;
winMessage: string;
winMessageMultiplayer: string;
loseMessage: string;
effectSetByUser: boolean;
soundSetByUser: boolean;
messageSetByUser: boolean;
scoringTypeSetByUser: boolean;
constructor() {
this.init();
}
init() {
this.scoringType = ScoringType.HighScore;
this.winEffect = effects.confetti;
this.loseEffect = effects.melt;
this.winSound = music.melodyPlayable(music.powerUp);
this.loseSound = music.melodyPlayable(music.wawawawaa);
this.winSoundLooping = false;
this.loseSoundLooping = false;
this.winMessage = "YOU WIN!";
this.winMessageMultiplayer = "${WINNER} WINS!";
this.loseMessage = "GAME OVER";
this.effectSetByUser = false;
this.soundSetByUser = false;
this.messageSetByUser = false;
this.scoringTypeSetByUser = false;
}
setScoringType(type: ScoringType, explicit: boolean) {
if (!explicit && this.scoringTypeSetByUser) return;
this.scoringType = type;
if (explicit) this.scoringTypeSetByUser = true;
}
setEffect(win: boolean, effect: effects.BackgroundEffect, explicit: boolean) {
if (!explicit && this.effectSetByUser) return;
if (win) this.winEffect = effect;
else this.loseEffect = effect;
if (explicit) this.effectSetByUser = true;
}
getEffect(win: boolean) {
return win ? this.winEffect : this.loseEffect;
}
setSound(win: boolean, sound: music.Playable, looping: boolean, explicit: boolean) {
if (!explicit && this.soundSetByUser) return;
if (win) {
this.winSound = sound;
this.winSoundLooping = looping;
} else {
this.loseSound = sound;
this.loseSoundLooping = looping;
}
if (explicit) this.soundSetByUser = true;
}
getSound(win: boolean) {
return win ? this.winSound : this.loseSound;
}
getSoundLooping(win: boolean) {
return win ? this.winSoundLooping : this.loseSoundLooping;
}
setMessage(win: boolean, message: string, explicit: boolean) {
if (!explicit && this.messageSetByUser) return;
if (win) this.winMessage = message;
else this.loseMessage = message;
if (explicit) this.messageSetByUser = true;
}
getMessage(win: boolean, preferMultiplayer?: boolean) {
if (this.messageSetByUser)
return win ? this.winMessage : this.loseMessage;
else if (preferMultiplayer)
return win ? this.winMessageMultiplayer : this.loseMessage;
else
return win ? this.winMessage : this.loseMessage;
}
}
let _gameOverConfig: GameOverConfig;
export const gameOverConfig = () => {
if (!_gameOverConfig) _gameOverConfig = new GameOverConfig();
return _gameOverConfig;
}
let _scene: scene.Scene;
let _sceneStack: scene.Scene[];
let _scenePushHandlers: ((scene: scene.Scene) => void)[];
let _scenePopHandlers: ((scene: scene.Scene) => void)[];
export function currentScene(): scene.Scene {
init();
return _scene;
}
let __waitAnyButton: () => void;
let __gameOverHandler: (win: boolean) => void;
let __isOver = false;
export function setWaitAnyButton(f: () => void) {
__waitAnyButton = f
}
export function waitAnyButton() {
if (__waitAnyButton) __waitAnyButton()
else pause(3000)
}
export function eventContext(): control.EventContext {
init();
return _scene.eventContext;
}
function init(forceNewScene ?: boolean) {
if (!_scene || forceNewScene) {
_scene = new scene.Scene(control.pushEventContext(), _scene);
}
_scene.init();
}
export function pushScene() {
const oldScene = game.currentScene()
particles.clearAll();
particles.disableAll();
if (!_sceneStack) _sceneStack = [];
_sceneStack.push(_scene);
init(/** forceNewScene **/ true);
if (_scenePushHandlers) {
_scenePushHandlers.forEach(cb => cb(oldScene));
}
}
export function popScene() {
const oldScene = game.currentScene()
if (_sceneStack && _sceneStack.length) {
// pop scenes from the stack
_scene = _sceneStack.pop();
control.popEventContext();
} else if (_scene) {
// post last scene
control.popEventContext();
_scene = undefined;
}
if (_scene)
particles.enableAll();
if (_scenePopHandlers) {
_scenePopHandlers.forEach(cb => cb(oldScene));
}
}
function showDialogBackground(h: number, c: number) {
const top = (screen.height - h) >> 1;
screen.fillRect(0, top, screen.width, h, 0)
screen.drawLine(0, top, screen.width, top, 1)
screen.drawLine(0, top + h - 1, screen.width, top + h - 1, 1)
return top;
}
export function showDialog(title: string, subtitle: string, footer?: string) {
init();
const titleFont = image.getFontForText(title || "");
const subFont = image.getFontForText(subtitle || "")
const footerFont = image.getFontForText(footer || "");
let h = 8;
if (title)
h += titleFont.charHeight;
if (subtitle)
h += 2 + subFont.charHeight
h += 8;
const top = showDialogBackground(h, 9)
let y = top + 8;
if (title) {
screen.print(title, 8, y, screen.isMono ? 1 : 7, titleFont);
y += titleFont.charHeight + 2;
}
if (subtitle) {
screen.print(subtitle, 8, y, screen.isMono ? 1 : 6, subFont);
y += subFont.charHeight + 2;
}
if (footer) {
const footerTop = screen.height - footerFont.charHeight - 4;
screen.fillRect(0, footerTop, screen.width, footerFont.charHeight + 4, 0);
screen.drawLine(0, footerTop, screen.width, footerTop, 1);
screen.print(
footer,
screen.width - footer.length * footerFont.charWidth - 8,
screen.height - footerFont.charHeight - 2,
1,
footerFont
)
}
}
/**
* Set the effect that occurs when the game is over
* @param win whether the effect should run on a win (true) or lose (false)
* @param effect
*/
//% blockId=game_setgameovereffect
//% block="use effect $effect for $win"
//% effect.defl=effects.confetti
//% win.shadow=toggleWinLose
//% win.defl=true
//% group="Game Over"
//% weight=90
//% blockGap=8
//% help=game/set-game-over-effect
export function setGameOverEffect(win: boolean, effect: effects.BackgroundEffect) {
init();
const goc = game.gameOverConfig();
goc.setEffect(win, effect, true);
}
/**
* Set the music that occurs when the game is over
* @param win whether the sound should play on a win (true) or lose (false)
* @param effect
*/
//% blockId=game_setgameoverplayable
//% block="use $sound looping $looping for $win"
//% sound.shadow=music_melody_playable
//% sound.defl=music.powerUp
//% looping.shadow=toggleOnOff
//% looping.defl=false
//% win.shadow=toggleWinLose
//% win.defl=true
//% group="Game Over"
//% weight=80
//% blockGap=8
//% help=game/set-game-over-playable
export function setGameOverPlayable(win: boolean, sound: music.Playable, looping: boolean) {
init();
const goc = game.gameOverConfig();
goc.setSound(win, sound, looping, true);
}
// Legacy api. Older extensions may still use this.
export function setGameOverSound(win: boolean, sound: music.Melody) {
init();
const goc = game.gameOverConfig();
goc.setSound(win, music.melodyPlayable(sound), false, true);
}
/**
* Set the message that displays when the game is over
* @param win whether the message should show on a win (true) or lose (false)
* @param message
*/
//% blockId=game_setgameovermessage
//% block="use message $message for $win"
//% message.defl="GAME OVER!"
//% win.shadow=toggleWinLose
//% win.defl=true
//% group="Game Over"
//% weight=70
//% blockGap=8
//% help=game/set-game-over-message
export function setGameOverMessage(win: boolean, message: string) {
init();
const goc = game.gameOverConfig();
goc.setMessage(win, message, true);
}
/**
* Set the method of judging the best score for the game
* @param type the scoring type
*/
//% blockId=game_setgameoverscoringtype
//% block="use $type as best score"
//% type.defl=ScoringType.HighScore
//% group="Game Over"
//% weight=60
//% blockGap=8
//% help=game/set-game-over-scoring-type
export function setGameOverScoringType(type: ScoringType) {
init();
const goc = game.gameOverConfig();
goc.setScoringType(type, true);
}
/**
* Set the function to call on game over. The 'win' boolean is
* passed to the handler.
* @param handler
*/
export function onGameOver(handler: (win: boolean) => void) {
__gameOverHandler = handler;
}
/**
* Finish the game and display the score
*/
//% group="Gameplay"
//% blockId=gameOver block="game over %win=toggleWinLose || with %effect effect"
//% weight=80 help=game/over
//% deprecated=true
export function over(win: boolean = false, effect?: effects.BackgroundEffect) {
// Match legacy behavior unless effect was set by user
const goc = game.gameOverConfig();
goc.setEffect(win, effect, false);
_gameOverImpl(win);
}
//% blockId=gameOver2 block="game over $win"
//% win.shadow=toggleWinLose
//% win.defl=true
//% weight=100
//% blockGap=8
//% help=game/over
//% group="Game Over"
export function gameOver(win: boolean) {
_gameOverImpl(win);
}
export function gameOverPlayerWin(player: number) {
_gameOverImpl(true, player);
}
function _mapScoreTypeToString(scoreType: ScoringType): string {
switch (scoreType) {
case ScoringType.HighScore: return "highscore";
case ScoringType.LowScore: return "lowscore";
case ScoringType.None: return "none";
default: return "none";
}
}
function _gameOverImpl(win: boolean, winnerOverride?: number) {
init();
if (__isOver) return;
__isOver = true;
if (__gameOverHandler) {
__gameOverHandler(win);
} else {
const goc = game.gameOverConfig();
const judged = !winnerOverride && goc.scoringType !== ScoringType.None;
const playersWithScores = info.playersWithScores();
const prevBestScore = judged && info.highScore();
const winner = judged && win && info.winningPlayer();
const scores = playersWithScores.map(player => new GameOverPlayerScore(player.number, player.impl.score(), player === winner));
// Save all scores. Dependency Note: this action triggers Kiosk to exit the simulator and show the high score screen.
const scoreTypeString = _mapScoreTypeToString(goc.scoringType);
info.saveAllScores(scoreTypeString);
// Save high score if this was a judged game and there was a winner (don't save in the LOSE case).
if (judged && winner) {
info.saveHighScore();
}
const preferMultiplayer = !!winnerOverride || (judged && info.multiplayerScoring());
const message = goc.getMessage(win, preferMultiplayer);
const effect = goc.getEffect(win);
const sound = goc.getSound(win);
const looping = goc.getSoundLooping(win);
const playbackMode = looping ? music.PlaybackMode.LoopingInBackground : music.PlaybackMode.InBackground;
// releasing memory and clear fibers. Do not add anything that releases the fiber until background is set below,
// or screen will be cleared on the new frame and will not appear as background in the game over screen.
while (_sceneStack && _sceneStack.length) {
_scene.destroy();
popScene();
}
pushScene();
scene.setBackgroundImage(screen.clone());
if (sound) music.play(sound, playbackMode);
if (effect) effect.startScreenEffect();
pause(400);
const overDialog = new GameOverDialog(win, message, judged, scores, prevBestScore, winnerOverride);
scene.createRenderable(scene.HUD_Z, target => {
overDialog.update();
target.drawTransparentImage(
overDialog.image,
0,
(screen.height - overDialog.image.height) >> 1
);
});
pause(500); // wait for users to stop pressing keys
overDialog.displayCursor();
waitAnyButton();
control.reset();
}
}
// Indicates whether the fiber needs to be created
let foreverRunning = false;
/**
* Repeats the code forever in the background for this scene.
* On each iteration, allows other codes to run.
* @param body code to execute
*/
export function forever(action: () => void): void {
if (!foreverRunning) {
foreverRunning = true;
control.runInParallel(() => {
while (1) {
const handlers = game.currentScene().gameForeverHandlers;
handlers.forEach(h => {
if (!h.lock) {
h.lock = true;
control.runInParallel(() => {
h.handler();
h.lock = false;
});
}
});
pause(20);
}
});
}
game.currentScene().gameForeverHandlers.push(
new scene.GameForeverHandler(action)
);
}
/**
* Draw on screen before sprites, after background
* @param body code to execute
*/
//% group="Gameplay"
//% help=game/paint weight=10 afterOnStart=true
export function onPaint(a: () => void): void {
init();
if (!a) return;
scene.createRenderable(scene.ON_PAINT_Z, a);
}
/**
* Draw on screen after sprites
* @param body code to execute
*/
//% group="Gameplay"
//% help=game/shade weight=10 afterOnStart=true
export function onShade(a: () => void): void {
init();
if (!a) return;
scene.createRenderable(scene.ON_SHADE_Z, a);
}
/**
* Register a handler that runs whenever a scene is pushed onto the scene
* stack. Useful for extensions that need to store/restore state as the
* event context changes. The handler is run AFTER the push operation (i.e.
* after game.currentScene() has changed)
*
* @param handler Code to run when a scene is pushed onto the stack
*/
export function addScenePushHandler(handler: (oldScene: scene.Scene) => void) {
if (!_scenePushHandlers) _scenePushHandlers = [];
if (_scenePushHandlers.indexOf(handler) < 0)
_scenePushHandlers.push(handler);
}
/**
* Remove a scene push handler. Useful for extensions that need to store/restore state as the
* event context changes.
*
* @param handler The handler to remove
*/
export function removeScenePushHandler(handler: (oldScene: scene.Scene) => void) {
if (_scenePushHandlers) _scenePushHandlers.removeElement(handler);
}
/**
* Register a handler that runs whenever a scene is popped off of the scene
* stack. Useful for extensions that need to store/restore state as the
* event context changes. The handler is run AFTER the pop operation. (i.e.
* after game.currentScene() has changed)
*
* @param handler Code to run when a scene is removed from the top of the stack
*/
export function addScenePopHandler(handler: (oldScene: scene.Scene) => void) {
if (!_scenePopHandlers) _scenePopHandlers = [];
if (_scenePopHandlers.indexOf(handler) < 0)
_scenePopHandlers.push(handler);
}
/**
* Remove a scene pop handler. Useful for extensions that need to store/restore state as the
* event context changes.
*
* @param handler The handler to remove
*/
export function removeScenePopHandler(handler: (oldScene: scene.Scene) => void) {
if (_scenePopHandlers) _scenePopHandlers.removeElement(handler);
}
}