syndicate-js/packages/html/examples/flappy-bird/src/index.ts

156 lines
5.8 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { $QuitDataspace, Double, Facet, Inbound, Outbound, floatValue } from '@syndicate-lang/core';
activate import { WindowEvent, template, Anchor } from '@syndicate-lang/html';
activate import { PeriodicTick } from '@syndicate-lang/timer';
assertion type Position(x, y);
assertion type GameOver();
assertion type Score(count);
message type IncreaseScore();
message type Reset();
const BOARD_HEIGHT = 567;
const FLAPPY_WIDTH = 57;
const FLAPPY_HEIGHT = 41;
const FLAPPY_XPOS = 212;
const PILLAR_WIDTH = 86;
const PILLAR_GAP = 158;
const FIELD_HEIGHT = 561;
const PILLAR_HEAD_HEIGHT = 40;
boot {
spawn named 'game-factory' {
on start spawnGame(thisFacet);
during GameOver() => {
on stop spawnGame(thisFacet);
on message WindowEvent('+keypress', $_e) => send message Reset();
on message WindowEvent('+click', $_e) => send message Reset();
}
}
}
function spawnGame<T>(thisFacet: Facet<T>) {
spawn dataspace named 'GameInstance' {
spawn named 'game-instance-control' {
during GameOver() => assert Outbound(GameOver());
on message Inbound(Reset()) => send message $QuitDataspace;
}
spawn named 'score' {
let ui = new Anchor();
field score: number = 0;
assert Score(this.score);
on start react {
assert Outbound(ui.html('#board-area',
template`<h1 class="score">${this.score}</h1>`));
stop on asserted GameOver() => react {
assert Outbound(ui.html('#board-area',
template`<h1 class="score">${this.score}<br/>GAME OVER</h1>`));
}
}
on message IncreaseScore() => this.score++;
}
spawn named 'flappy' {
let ui = new Anchor();
field xpos: number = 0;
field ypos: number = 312;
field yvel: number = 0;
assert Position(Double(this.xpos), Double(this.ypos));
assert Outbound(
ui.html('#board-area',
template`<div class="flappy"
style="${`transform: rotate(${2 * this.yvel}deg);
top: ${this.ypos}px`}"></div>`));
on (this.ypos > BOARD_HEIGHT - FLAPPY_HEIGHT) {
this.ypos = BOARD_HEIGHT - FLAPPY_HEIGHT;
react {
assert GameOver();
}
}
on start react {
stop on asserted GameOver();
on message Inbound(WindowEvent('+keypress', $_e)) => this.yvel = -10;
on message Inbound(WindowEvent('+click', $_e)) => this.yvel = -10;
const ms_per_tick = 1000.0 / 60;
on message Inbound(PeriodicTick(Double(ms_per_tick))) => {
this.xpos += 0.15 * ms_per_tick;
this.ypos = (this.ypos + this.yvel);
this.yvel += ms_per_tick * 0.05;
}
}
}
spawn named 'border-scroll' {
let ui = new Anchor();
field pos: number = 0;
on asserted Position($xpos, _) => this.pos = floatValue(xpos) % 23;
assert Outbound(ui.html(
'#board-area',
template`<div class="scrolling-border" style="${`background-position-x: ${-this.pos}px`}"></div>`,
0));
}
spawn named 'pipe-factory' {
field nextPipe: number = 0;
on asserted Score(this.nextPipe) => spawnPipe(thisFacet, this.nextPipe++);
}
function spawnPipe<T>(thisFacet: Facet<T>, i: number) {
spawn named ['pipe', i] {
let ui = new Anchor();
const xlocation = (i + 1) * 324;
const upperHeight =
Math.random() * (FIELD_HEIGHT - PILLAR_GAP - PILLAR_HEAD_HEIGHT * 6)
+ PILLAR_HEAD_HEIGHT * 3;
const lowerHeight = FIELD_HEIGHT - upperHeight - PILLAR_GAP;
stop on (this.xpos < -(PILLAR_WIDTH + FLAPPY_XPOS));
on start react { stop on (this.xpos <= 0) send message IncreaseScore(); };
field xpos: number = xlocation;
on asserted Position($xpos, _) => this.xpos = xlocation - floatValue(xpos);
on asserted Position($xpos, $ypos) => {
if (touchingPillar(floatValue(xpos), floatValue(ypos))) {
react {
assert GameOver();
}
}
}
assert Outbound(ui.html(
'#board-area',
template`<div class="pillars">
<div class="pillar pillar-upper"
style="${`left: ${this.xpos + FLAPPY_XPOS}px; height: ${upperHeight}px;`}"></div>
<div class="pillar pillar-lower"
style="${`left: ${this.xpos + FLAPPY_XPOS}px; height: ${lowerHeight}px;`}"></div>
</div>`));
function touchingPillar(xpos: number, ypos: number): boolean {
const inHorizontalRange =
(xpos + FLAPPY_WIDTH >= xlocation) && (xpos <= xlocation + PILLAR_WIDTH);
const aboveGapTop = (ypos <= upperHeight);
const belowGapBottom = (ypos + FLAPPY_HEIGHT >= upperHeight + PILLAR_GAP);
return inHorizontalRange && (aboveGapTop || belowGapBottom);
}
}
}
}
}