pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
1,281 lines (1,127 loc) • 39.3 kB
text/typescript
/**
* Head-up display
*
*/
//% color=#cf6a87 weight=80 icon="\uf2bb" blockGap=8
//% groups='["Score", "Life", "Countdown", "Multiplayer"]'
//% blockGap=8
namespace info {
export enum Visibility {
None = 0,
Countdown = 1 << 0,
Score = 1 << 1,
Life = 1 << 2,
Hud = 1 << 3,
Multi = 1 << 4,
UserHeartImage = 1 << 5,
_ExplicitlySetScore = 1 << 6,
_ExplicitlySetLife = 1 << 7,
}
class ScoreReachedHandler {
public isTriggered: boolean;
constructor(public score: number, public handler: () => void) {
this.isTriggered = false;
}
}
export class PlayerState {
public score: number;
// undefined: not used
// null: reached 0 and callback was invoked
public life: number;
public lifeZeroHandler: () => void;
public scoreReachedHandlers: ScoreReachedHandler[];
public showScore?: boolean;
public showLife?: boolean;
public visibility: Visibility;
public showPlayer?: boolean;
constructor() {
this.visibility = Visibility.None;
this.showScore = undefined;
this.showLife = undefined;
this.showPlayer = undefined;
this.scoreReachedHandlers = [];
}
}
class InfoState {
public playerStates: PlayerState[];
public visibilityFlag: number;
public gameEnd: number;
public heartImage: Image;
public multiplierImage: Image;
public bgColor: number;
public borderColor: number;
public fontColor: number;
public countdownExpired: boolean;
public countdownEndHandler: () => void;
constructor() {
this.visibilityFlag = Visibility.Hud;
this.playerStates = [];
this.heartImage = defaultHeartImage();
this.multiplierImage = img`
1 . . . 1
. 1 . 1 .
. . 1 . .
. 1 . 1 .
1 . . . 1
`;
this.bgColor = screen.isMono ? 0 : 1;
this.borderColor = screen.isMono ? 1 : 3;
this.fontColor = screen.isMono ? 1 : 3;
this.countdownExpired = undefined;
this.countdownEndHandler = undefined;
this.gameEnd = undefined;
this.playerStates = [];
}
}
let infoState: InfoState = undefined;
let players: PlayerInfo[];
let infoStateStack: {
state: InfoState,
scene: scene.Scene
}[];
game.addScenePushHandler(oldScene => {
if (infoState) {
if (!infoStateStack) infoStateStack = [];
infoStateStack.push({
state: infoState,
scene: oldScene
});
infoState = undefined;
}
});
game.addScenePopHandler(() => {
const scene = game.currentScene();
infoState = undefined;
if (infoStateStack && infoStateStack.length) {
const nextState = infoStateStack.pop();
if (nextState.scene == scene) {
infoState = nextState.state;
} else {
infoStateStack.push(nextState);
}
}
});
function initHUD() {
if (infoState) return;
infoState = new InfoState();
scene.createRenderable(
scene.HUD_Z,
() => {
if (!infoState) return;
control.enablePerfCounter("info")
// show score, lifes
if (infoState.visibilityFlag & Visibility.Multi) {
const ps = players.filter(p => !!p);
// First draw players
ps.forEach(p => p.drawPlayer());
// Then run life over events
ps.forEach(p => p.impl.raiseLifeZero(false));
} else { // single player
// show score
const p = player1;
if (p.impl.hasScore() && (infoState.visibilityFlag & Visibility.Score)) {
p.drawScore();
}
// show life
if (p.impl.hasLife() && (infoState.visibilityFlag & Visibility.Life)) {
p.drawLives();
}
p.impl.raiseLifeZero(true);
}
// show countdown in both modes
if (infoState.gameEnd !== undefined && infoState.visibilityFlag & Visibility.Countdown) {
const scene = game.currentScene();
const elapsed = infoState.gameEnd - scene.millis();
drawTimer(elapsed);
let t = elapsed / 1000;
if (t <= 0) {
t = 0;
if (!infoState.countdownExpired) {
infoState.countdownExpired = true;
infoState.gameEnd = undefined;
if (infoState.countdownEndHandler) {
infoState.countdownEndHandler();
} else {
// Clear effect and sound, unless set by user
const goc = game.gameOverConfig();
goc.setEffect(false, null, false);
goc.setSound(false, null, false, false);
game.gameOver(false);
}
}
}
}
}
);
}
function initMultiHUD() {
if (infoState.visibilityFlag & Visibility.Multi) return;
infoState.visibilityFlag |= Visibility.Multi;
if (!(infoState.visibilityFlag & Visibility.UserHeartImage))
infoState.heartImage = defaultMultiplayerHeartImage();
infoState.multiplierImage = img`
1 . 1
. 1 .
1 . 1
`;
}
function defaultHeartImage() {
return screen.isMono ?
img`
. 1 1 . 1 1 . .
1 . . 1 . . 1 .
1 . . . . . 1 .
1 . . . . . 1 .
. 1 . . . 1 . .
. . 1 . 1 . . .
. . . 1 . . . .
`
:
img`
. c 2 2 . 2 2 .
c 2 2 2 2 2 4 2
c 2 2 2 2 4 2 2
c 2 2 2 2 2 2 2
. c 2 2 2 2 2 .
. . c 2 2 2 . .
. . . c 2 . . .
`;
}
function defaultMultiplayerHeartImage() {
return screen.isMono ?
img`
. . 1 . 1 . .
. 1 . 1 . 1 .
. 1 . . . 1 .
. . 1 . 1 . .
. . . 1 . . .
`
:
img`
. . 1 . 1 . .
. 1 2 1 4 1 .
. 1 2 4 2 1 .
. . 1 2 1 . .
. . . 1 . . .
`;
}
export function multiplayerScoring() {
const pws = playersWithScores();
for (const p of pws) {
if (p.number > 1) {
return true;
}
}
return false;
}
export function playersWithScores(): PlayerInfo[] {
return players ? players.filter(item => item.impl.hasScore()) : [];
}
export function saveAllScores(scoringType: string) {
const allScoresKey = "all-scores";
let allScores: number[];
const pws = playersWithScores();
if (pws) {
allScores = pws.map(item => item.impl.score());
}
else {
allScores = [];
}
const scoresObj = {
"allScores": allScores,
"scoringType": allScores.length ? scoringType : "None"
}
settings.writeJSON(allScoresKey, scoresObj);
}
export function winningPlayer(): PlayerInfo {
let winner: PlayerInfo = null;
const pws = playersWithScores();
if (pws) {
const goc = game.gameOverConfig();
let hs: number = null;
pws.forEach(p => {
const s = p.impl.score();
if (isBetterScore(s, hs)) {
hs = s;
winner = p;
}
});
}
return winner;
}
export function isBetterScore(newScore: number, prevScore: number): boolean {
const goc = game.gameOverConfig();
switch (goc.scoringType) {
case game.ScoringType.HighScore: {
return prevScore == null || newScore > prevScore;
}
case game.ScoringType.LowScore: {
return prevScore == null || newScore < prevScore;
}
}
return false;
}
export function saveHighScore() {
const winner = winningPlayer();
if (winner) {
let hs = winner.impl.score();
let curr = settings.readNumber("high-score");
if (isBetterScore(hs, curr)) {
settings.writeNumber("high-score", hs);
}
}
}
/**
* Get the current score if any
*/
//% weight=95 blockGap=8
//% blockId=hudScore block="score"
//% help=info/score
//% group="Score"
export function score() {
return player1.impl.score();
}
//%
//% group="Score"
export function hasScore() {
return player1.impl.hasScore();
}
/**
* Get the last recorded high score
*/
//% weight=94
//% blockId=highScore block="high score"
//% help=info/high-score
//% group="Score"
export function highScore(): number {
return settings.readNumber("high-score") || 0;
}
/**
* Set the score
*/
//% weight=93 blockGap=8
//% blockId=hudsetScore block="set score to %value"
//% help=info/set-score
//% group="Score"
export function setScore(value: number) {
player1.impl.setScore(value);
}
/**
* Change the score by the given amount
* @param value the amount of change, eg: 1
*/
//% weight=92
//% blockId=hudChangeScoreBy block="change score by %value"
//% help=info/change-score-by
//% group="Score"
export function changeScoreBy(value: number) {
player1.impl.changeScoreBy(value);
}
/**
* Get the number of lives
*/
//% weight=85 blockGap=8
//% blockId=hudLife block="life"
//% help=info/life
//% group="Life"
export function life() {
return player1.impl.life();
}
//% group="Life"
export function hasLife() {
return player1.impl.hasLife();
}
/**
* Set the number of lives
* @param value the number of lives, eg: 3
*/
//% weight=84 blockGap=8
//% blockId=hudSetLife block="set life to %value"
//% help=info/set-life
//% group="Life"
export function setLife(value: number) {
player1.impl.setLife(value);
}
/**
* Change the lives by the given amount
* @param value the change of lives, eg: -1
*/
//% weight=83
//% blockId=hudChangeLifeBy block="change life by %value"
//% help=info/change-life-by
//% group="Life"
export function changeLifeBy(value: number) {
player1.impl.changeLifeBy(value);
}
/**
* Run code when the player's life reaches 0. If this function
* is not called then game.over() is called instead
*/
//% weight=82
//% blockId=gamelifeevent block="on life zero"
//% help=info/on-life-zero
//% group="Life"
export function onLifeZero(handler: () => void) {
player1.impl.onLifeZero(handler);
}
/**
* Runs code once each time the score reaches a given value. This will also
* run if the score "passes" the given value in either direction without ever
* having the exact value (e.g. if score is changed by more than 1)
*
* @param score the score to fire the event on
* @param handler code to run when the score reaches the given value
*/
//% weight=10
//% blockId=gameonscore
//% block="on score $score"
//% score.defl=100
//% help=info/on-score
//% group="Score"
export function onScore(score: number, handler: () => void) {
player1.impl.onScore(score, handler);
}
/**
* Get the value of the current count down
*/
//% block="countdown"
//% blockId=gamegetcountdown
//% weight=79 help=info/countdown
//% group="Countdown"
export function countdown(): number {
initHUD();
return infoState.gameEnd ? ((infoState.gameEnd - game.currentScene().millis()) / 1000) : 0;
}
/**
* Start a countdown of the given duration in seconds
* @param duration the duration of the countdown, eg: 10
*/
//% blockId=gamecountdown block="start countdown %duration (s)"
//% help=info/start-countdown weight=78 blockGap=8
//% group="Countdown"
export function startCountdown(duration: number) {
updateFlag(Visibility.Countdown, true);
infoState.gameEnd = game.currentScene().millis() + duration * 1000;
infoState.countdownExpired = false;
}
/**
* Change the running countdown by the given number of seconds
* @param seconds the number of seconds the countdown should be changed by
*/
//% block="change countdown by $seconds (s)"
//% blockId=gamechangecountdown
//% weight=77 help=info/change-countdown-by
//% group="Countdown"
export function changeCountdownBy(seconds: number) {
startCountdown((countdown() + seconds));
}
/**
* Stop the current countdown and hides the timer display
*/
//% blockId=gamestopcountdown block="stop countdown" weight=76
//% help=info/stop-countdown
//% group="Countdown"
export function stopCountdown() {
updateFlag(Visibility.Countdown, false);
infoState.gameEnd = undefined;
infoState.countdownExpired = true;
}
/**
* Run code when the countdown reaches 0. If this function
* is not called then game.over() is called instead
*/
//% blockId=gamecountdownevent block="on countdown end" weight=75
//% help=info/on-countdown-end
//% group="Countdown"
export function onCountdownEnd(handler: () => void) {
initHUD();
infoState.countdownEndHandler = handler;
}
/**
* Replaces the image used to represent the player's lives. Images
* should be no larger than 8x8
*/
//% group="Life"
export function setLifeImage(image: Image) {
updateFlag(Visibility.UserHeartImage, true);
infoState.heartImage = image;
}
/**
* Set whether life should be displayed
* @param on if true, lives are shown; otherwise, lives are hidden
*/
//% group="Life"
export function showLife(on: boolean) {
updateFlag(Visibility.Life, on);
updateFlag(Visibility._ExplicitlySetLife, true);
}
/**
* Set whether score should be displayed
* @param on if true, score is shown; otherwise, score is hidden
*/
//% group="Score"
export function showScore(on: boolean) {
updateFlag(Visibility.Score, on);
updateFlag(Visibility._ExplicitlySetScore, true);
}
/**
* Set whether countdown should be displayed
* @param on if true, countdown is shown; otherwise, countdown is hidden
*/
//% group="Countdown"
export function showCountdown(on: boolean) {
updateFlag(Visibility.Countdown, on);
}
function updateFlag(flag: Visibility, on: boolean) {
initHUD();
if (on) infoState.visibilityFlag |= flag;
else infoState.visibilityFlag = ~(~infoState.visibilityFlag | flag);
}
/**
* Sets the color of the borders around the score, countdown, and life
* elements. Defaults to 3
* @param color The index of the color (0-15)
*/
//% group="Theme"
export function setBorderColor(color: number) {
initHUD();
infoState.borderColor = Math.min(Math.max(color, 0), 15) | 0;
}
/**
* Sets the color of the background of the score, countdown, and life
* elements. Defaults to 1
* @param color The index of the color (0-15)
*/
//% group="Theme"
export function setBackgroundColor(color: number) {
initHUD();
infoState.bgColor = Math.min(Math.max(color, 0), 15) | 0;
}
/**
* Sets the color of the text used in the score, countdown, and life
* elements. Defaults to 3
* @param color The index of the color (0-15)
*/
//% group="Theme"
export function setFontColor(color: number) {
initHUD();
infoState.fontColor = Math.min(Math.max(color, 0), 15) | 0;
}
/**
* Get the current color of the borders around the score, countdown, and life
* elements
*/
//% group="Theme"
export function borderColor(): number {
initHUD();
return infoState.borderColor ? infoState.borderColor : 3;
}
/**
* Get the current color of the background of the score, countdown, and life
* elements
*/
//% group="Theme"
export function backgroundColor(): number {
initHUD();
return infoState.bgColor ? infoState.bgColor : 1;
}
/**
* Get the current color of the text usded in the score, countdown, and life
* elements
*/
//% group="Theme"
export function fontColor(): number {
initHUD();
return infoState.fontColor ? infoState.fontColor : 3;
}
function drawTimer(millis: number) {
if (millis < 0) millis = 0;
millis |= 0;
const font = image.font8;
const smallFont = image.font5;
const seconds = Math.idiv(millis, 1000);
const width = font.charWidth * 5 - 2;
let left = (screen.width >> 1) - (width >> 1) + 1;
let color1 = infoState.fontColor;
let color2 = infoState.bgColor;
if (seconds < 10 && (seconds & 1) && !screen.isMono) {
const temp = color1;
color1 = color2;
color2 = temp;
}
screen.fillRect(left - 3, 0, width + 6, font.charHeight + 3, infoState.borderColor)
screen.fillRect(left - 2, 0, width + 4, font.charHeight + 2, color2)
if (seconds < 60) {
const top = 1;
const remainder = Math.idiv(millis % 1000, 10);
screen.print(formatDecimal(seconds) + ".", left, top, color1, font)
const decimalLeft = left + 3 * font.charWidth;
screen.print(formatDecimal(remainder), decimalLeft, top + 2, color1, smallFont)
}
else {
const minutes = Math.idiv(seconds, 60);
const remainder = seconds % 60;
screen.print(formatDecimal(minutes) + ":" + formatDecimal(remainder), left, 1, color1, font);
}
}
/**
* Splits the implementation of the player info from the user-facing APIs so that
* we can reference this internally without causing the "multiplayer" part to show
* up in the usedParts array of the user program's compile result. Make sure to
* use the APIs on this class and not the PlayerInfo to avoid false-positives when
* we detect if a game is multiplayer or not
*/
export class PlayerInfoImpl {
protected _player: number;
public bg: number; // background color
public border: number; // border color
public fc: number; // font color
public x?: number;
public y?: number;
public left?: boolean; // if true banner goes from x to the left, else goes rightward
public up?: boolean; // if true banner goes from y up, else goes downward
constructor(player: number) {
this._player = player;
this.border = 1;
this.fc = 1;
this.left = undefined;
this.up = undefined;
if (this._player === 1) {
// Top left, and banner is white on red
this.bg = screen.isMono ? 0 : 2;
this.x = 0;
this.y = 0;
} else if (player === 2) {
// Top right, and banner is white on blue
this.bg = screen.isMono ? 0 : 8;
this.x = screen.width;
this.y = 0;
this.left = true;
} else if (player === 3) {
this.bg = screen.isMono ? 0 : 4;
this.x = 0;
this.y = screen.height;
this.up = true;
} else {
// bottom left, banner is white on green
this.bg = screen.isMono ? 0 : 7;
this.x = screen.width;
this.y = screen.height;
this.left = true;
this.up = true;
}
}
private init() {
initHUD();
if (this._player > 1) initMultiHUD();
if (!infoState.playerStates[this._player - 1]) {
infoState.playerStates[this._player - 1] = new PlayerState();
}
}
getState(): PlayerState {
this.init();
return infoState.playerStates[this._player - 1];
}
// the id numbera of the player
id(): number {
return this._player;
}
score(): number {
const state = this.getState();
if (state.showScore === undefined) state.showScore = true;
if (state.showPlayer === undefined) state.showPlayer = true;
if (state.score == null)
state.score = 0;
return state.score;
}
setScore(value: number) {
const state = this.getState();
if (!(infoState.visibilityFlag & Visibility._ExplicitlySetScore)) {
updateFlag(Visibility.Score, true);
}
this.score(); // invoked for side effects
const oldScore = state.score || 0;
state.score = (value | 0);
state.scoreReachedHandlers.forEach(srh => {
if ((oldScore < srh.score && state.score >= srh.score) ||
(oldScore > srh.score && state.score <= srh.score)) {
srh.handler();
}
});
}
changeScoreBy(value: number): void {
this.setScore(this.score() + value);
}
hasScore() {
const state = this.getState();
return state.score !== undefined;
}
life(): number {
const state = this.getState();
if (state.showLife === undefined) state.showLife = true;
if (state.showPlayer === undefined) state.showPlayer = true;
if (state.life === undefined) {
state.life = 3;
}
return state.life || 0;
}
setLife(value: number): void {
const state = this.getState();
if (!(infoState.visibilityFlag & Visibility._ExplicitlySetLife)) {
updateFlag(Visibility.Life, true);
}
this.life(); // invoked for side effects
state.life = (value | 0);
}
changeLifeBy(value: number): void {
this.setLife(this.life() + value);
}
hasLife(): boolean {
const state = this.getState();
return state.life !== undefined && state.life !== null;
}
onLifeZero(handler: () => void) {
const state = this.getState();
state.lifeZeroHandler = handler;
}
onScore(score: number, handler: () => void) {
const state = this.getState();
for (const element of state.scoreReachedHandlers) {
if (element.score === score) {
// Score handlers are implemented as "last one wins."
element.handler = handler;
return;
}
}
state.scoreReachedHandlers.push(new ScoreReachedHandler(score, handler));
}
raiseLifeZero(gameOver: boolean) {
const state = this.getState();
if (state.life !== null && state.life <= 0) {
state.life = null;
if (state.lifeZeroHandler) {
state.lifeZeroHandler();
} else if (gameOver) {
// Clear effect and sound, unless set by user
const goc = game.gameOverConfig();
goc.setEffect(false, null, false);
goc.setSound(false, null, false, false);
game.gameOver(false);
}
}
}
}
//% fixedInstances
//% blockGap=8
export class PlayerInfo {
protected _player: number;
public impl: PlayerInfoImpl;
constructor(player: number) {
this._player = player;
this.impl = new PlayerInfoImpl(player);
if (!players) players = [];
players[this._player - 1] = this;
}
private init() {
initHUD();
if (this._player > 1) initMultiHUD();
if (!infoState.playerStates[this._player - 1]) {
infoState.playerStates[this._player - 1] = new PlayerState();
}
}
/**
* Returns the one-based number of the player
*/
get number() {
return this._player;
}
get bg(): number {
return this.impl.bg;
}
set bg(value: number) {
this.impl.bg = value;
}
get border(): number {
return this.impl.border;
}
set border(value: number) {
this.impl.border = value;
}
get fc(): number {
return this.impl.fc;
}
set fc(value: number) {
this.impl.fc = value;
}
get showScore(): boolean {
return this.impl.getState().showScore;
}
set showScore(value: boolean) {
this.impl.getState().showScore = value;
}
get showLife(): boolean {
return this.impl.getState().showLife;
}
set showLife(value: boolean) {
this.impl.getState().showLife = value;
}
get visibility(): Visibility {
return this.impl.getState().visibility;
}
set visibility(value: Visibility) {
this.impl.getState().visibility = value;
}
get showPlayer(): boolean {
return this.impl.getState().showPlayer;
}
set showPlayer(value: boolean) {
this.impl.getState().showPlayer = value;
}
get x(): number {
return this.impl.x;
}
set x(value: number) {
this.impl.x = value;
}
get y(): number {
return this.impl.y;
}
set y(value: number) {
this.impl.y = value;
}
get left(): boolean {
return this.impl.left;
}
set left(value: boolean) {
this.impl.left = value;
}
get up(): boolean {
return this.impl.up;
}
set up(value: boolean) {
this.impl.up = value;
}
getState(): PlayerState {
this.init();
return infoState.playerStates[this._player - 1];
}
// the id numbera of the player
id(): number {
return this.impl.id();
}
/**
* Get the player score
*/
//% group="Multiplayer"
//% blockId=piscore block="%player score"
//% help=info/score
//% parts="multiplayer"
score(): number {
return this.impl.score();
}
/**
* Set the player score
*/
//% group="Multiplayer"
//% blockId=pisetscore block="set %player score to %value"
//% value.defl=0
//% help=info/set-score
//% parts="multiplayer"
setScore(value: number) {
this.impl.setScore(value);
}
/**
* Change the score of a player
* @param value
*/
//% group="Multiplayer"
//% blockId=pichangescore block="change %player score by %value"
//% value.defl=1
//% help=info/change-score-by
//% parts="multiplayer"
changeScoreBy(value: number): void {
this.impl.changeScoreBy(value);
}
hasScore() {
return this.impl.hasScore();
}
/**
* Get the player life
*/
//% group="Multiplayer"
//% blockid=piflife block="%player life"
//% help=info/life
//% parts="multiplayer"
life(): number {
return this.impl.life();
}
/**
* Set the player life
*/
//% group="Multiplayer"
//% blockId=pisetlife block="set %player life to %value"
//% value.defl=3
//% help=info/set-life
//% parts="multiplayer"
setLife(value: number): void {
this.impl.setLife(value);
}
/**
* Change the life of a player
* @param value
*/
//% group="Multiplayer"
//% blockId=pichangelife block="change %player life by %value"
//% value.defl=-1
//% help=info/change-life-by
//% parts="multiplayer"
changeLifeBy(value: number): void {
this.impl.changeLifeBy(value);
}
/**
* Return true if the given player currently has a value set for health,
* and false otherwise.
* @param player player to check life of
*/
//% group="Multiplayer"
//% blockId=pihaslife block="%player has life"
//% help=info/has-life
//% parts="multiplayer"
hasLife(): boolean {
return this.impl.hasLife();
}
/**
* Runs code when life reaches zero
* @param handler
*/
//% group="Multiplayer"
//% blockId=playerinfoonlifezero block="on %player life zero"
//% help=info/on-life-zero
//% parts="multiplayer"
onLifeZero(handler: () => void) {
this.impl.onLifeZero(handler);
}
/**
* Runs code once each time the score reaches a given value. This will also
* run if the score "passes" the given value in either direction without ever
* having the exact value (e.g. if score is changed by more than 1)
*
* @param score the score to fire the event on
* @param handler code to run when the score reaches the given value
*/
//% blockId=playerinfoonscore
//% block="on $this score $score"
//% score.defl=100
//% help=info/on-score
//% group="Multiplayer"
//% parts="multiplayer"
onScore(score: number, handler: () => void) {
this.impl.onScore(score, handler);
}
drawPlayer() {
const state = this.getState();
const font = image.font5;
let score: string;
let life: string;
let height = 4;
let scoreWidth = 0;
let lifeWidth = 0;
const offsetX = 1;
let offsetY = 2;
let showScore = state.showScore && state.score !== undefined;
let showLife = state.showLife && state.life !== undefined;
if (showScore) {
score = "" + state.score;
scoreWidth = score.length * font.charWidth + 3;
height += font.charHeight;
offsetY += font.charHeight + 1;
}
if (showLife) {
life = "" + (state.life || 0);
lifeWidth = infoState.heartImage.width + infoState.multiplierImage.width + life.length * font.charWidth + 3;
height += infoState.heartImage.height;
}
const width = Math.max(scoreWidth, lifeWidth);
// bump size for space between lines
if (showScore && showLife) height++;
const x = this.impl.x - (this.impl.left ? width : 0);
const y = this.impl.y - (this.impl.up ? height : 0);
// Bordered Box
if (showScore || showLife) {
screen.fillRect(x, y, width, height, this.impl.border);
screen.fillRect(x + 1, y + 1, width - 2, height - 2, this.impl.bg);
}
// print score
if (showScore) {
const bump = this.impl.left ? width - scoreWidth : 0;
screen.print(score, x + offsetX + bump + 1, y + 2, this.impl.fc, font);
}
// print life
if (showLife) {
const xLoc = x + offsetX + (this.impl.left ? width - lifeWidth : 0);
let mult = infoState.multiplierImage.clone();
mult.replace(1, this.impl.fc);
screen.drawTransparentImage(
infoState.heartImage,
xLoc,
y + offsetY
);
screen.drawTransparentImage(
mult,
xLoc + infoState.heartImage.width,
y + offsetY + font.charHeight - infoState.multiplierImage.height - 1
);
screen.print(
life,
xLoc + infoState.heartImage.width + infoState.multiplierImage.width + 1,
y + offsetY,
this.impl.fc,
font
);
}
// print player icon
if (state.showPlayer) {
const pNum = "" + this._player;
let iconWidth = pNum.length * font.charWidth + 1;
const iconHeight = Math.max(height, font.charHeight + 2);
let iconX = this.impl.left ? (x - iconWidth + 1) : (x + width - 1);
let iconY = y;
// adjustments when only player icon shown
if (!showScore && !showLife) {
iconX += this.impl.left ? -1 : 1;
if (this.impl.up) iconY -= 3;
}
screen.fillRect(
iconX,
iconY,
iconWidth,
iconHeight,
this.impl.border
);
screen.print(
pNum,
iconX + 1,
iconY + (iconHeight >> 1) - (font.charHeight >> 1),
this.impl.bg,
font
);
}
}
drawScore() {
const s = this.impl.score() | 0;
let font: image.Font;
let offsetY: number;
if (s >= 1000000) {
offsetY = 2;
font = image.font5;
}
else {
offsetY = 1;
font = image.font8;
}
const num = s.toString();
const width = num.length * font.charWidth;
screen.fillRect(
screen.width - width - 2,
0,
screen.width,
image.font8.charHeight + 3,
infoState.borderColor
);
screen.fillRect(
screen.width - width - 1,
0,
screen.width,
image.font8.charHeight + 2,
infoState.bgColor
);
screen.print(
num,
screen.width - width,
offsetY,
infoState.fontColor,
font
);
}
drawLives() {
const state = this.getState();
if (state.life < 0) return;
const font = image.font8;
if (state.life <= 4) {
screen.fillRect(
0,
0,
state.life * (infoState.heartImage.width + 1) + 3,
infoState.heartImage.height + 4,
infoState.borderColor
);
screen.fillRect(
0,
0,
state.life * (infoState.heartImage.width + 1) + 2,
infoState.heartImage.height + 3,
infoState.bgColor
);
for (let i = 0; i < state.life; i++) {
screen.drawTransparentImage(
infoState.heartImage,
1 + i * (infoState.heartImage.width + 1),
1
);
}
}
else {
const num = state.life + "";
const textWidth = num.length * font.charWidth - 1;
screen.fillRect(
0,
0,
infoState.heartImage.width + infoState.multiplierImage.width + textWidth + 5,
infoState.heartImage.height + 4,
infoState.borderColor
);
screen.fillRect(
0,
0,
infoState.heartImage.width + infoState.multiplierImage.width + textWidth + 4,
infoState.heartImage.height + 3,
infoState.bgColor
);
screen.drawTransparentImage(
infoState.heartImage,
1,
1
);
let mult = infoState.multiplierImage.clone();
mult.replace(1, infoState.fontColor);
screen.drawTransparentImage(
mult,
infoState.heartImage.width + 2,
font.charHeight - infoState.multiplierImage.height - 1
);
screen.print(
num,
infoState.heartImage.width + 3 + infoState.multiplierImage.width,
1,
infoState.fontColor,
font
);
}
}
}
function formatDecimal(val: number) {
val |= 0;
if (val < 10) {
return "0" + val;
}
return val.toString();
}
//% fixedInstance whenUsed block="player 2"
export const player2 = new PlayerInfo(2);
//% fixedInstance whenUsed block="player 3"
export const player3 = new PlayerInfo(3);
//% fixedInstance whenUsed block="player 4"
export const player4 = new PlayerInfo(4);
//% fixedInstance whenUsed block="player 1"
export const player1 = new PlayerInfo(1);
}