Skip to content

Commit

Permalink
Fix game refresh rates (#11)
Browse files Browse the repository at this point in the history
* Implement a generic game tester component

* Hide manual login when NFC is enabled

* Add difficulty picker to GameTester

* Add FPS counters to Snake and FlappyDrone

* Add max FPS to FlappyDrone

* Add max FPS and touch controls to Snake

* Only use touch/keyboard controls in Snake

* Enable game debug mode for GMs

* Adjust difficulty to be consistent between devices

Old HANSCA phones have 60Hz refresh rate. By setting the max FPS to 58
we ensure that the game difficulty is consistent between older and newer
devices.
  • Loading branch information
nicou authored Jun 23, 2024
1 parent 6dbd33b commit a0d5d37
Showing 10 changed files with 556 additions and 69 deletions.
12 changes: 9 additions & 3 deletions src/components/Carousel.vue
Original file line number Diff line number Diff line change
@@ -27,7 +27,8 @@
import Scanner from './Scanner.vue'
import GameScanner from './GameScanner'
import RadiationDetector from './RadiationDetector.vue'
import GameTest from './GameTest.vue'
import GameTester from './GameTester.vue'
import PhaseSyncGameTest from './PhaseSyncGameTest.vue'
import MedicalRecords from './MedicalRecords.vue'
import MedicalDiagnosis from './MedicalDiagnosis.vue'
import MedicalSample from './MedicalSample.vue'
@@ -114,7 +115,7 @@ const PAGES = [
},
{
id: 'admin',
title: 'Off-game admin',
title: 'HANSCA / GM / ADMIN / DEV',
role: 'role:admin',
color: '#9c4e4e',
tools: [
@@ -126,7 +127,12 @@ const PAGES = [
{
icon: 'atom',
title: 'Phase sync game test',
page: GameTest,
page: PhaseSyncGameTest,
},
{
icon: 'atom',
title: 'Game tester',
page: GameTester,
},
{
icon: 'atom',
1 change: 0 additions & 1 deletion src/components/GameScanner.vue
Original file line number Diff line number Diff line change
@@ -157,7 +157,6 @@ export default {
return
}
console.log('getting game component');
if (config.game in GAMES) {
cancelWatch()
this.gameLoader = () => {
251 changes: 251 additions & 0 deletions src/components/GameTester.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<template>
<v-ons-page>
<toolbar-top></toolbar-top>
<div v-show="state === 'scanning'" class="game-selector">
<h1>Select game</h1>
<div class="game-selection-button-container">
<v-ons-select id="skill-level" v-model="skillLevel">
<option v-for="level in skillLevels" :value="level.value" :key="level.value">Difficulty: {{ level.label }}</option>
</v-ons-select>
<v-ons-button disabled @click="onGameSelected('phasesync')">Phase Sync</v-ons-button>
<v-ons-button disabled @click="onGameSelected('manual')">Manual</v-ons-button>
<v-ons-button disabled @click="onGameSelected('lightsout')">Lights Out</v-ons-button>
<v-ons-button @click="onGameSelected('flappy')">Flappy Drone</v-ons-button>
<v-ons-button disabled @click="onGameSelected('balance')">Value Balance</v-ons-button>
<v-ons-button disabled @click="onGameSelected('nonogram')">Nonogram</v-ons-button>
<v-ons-button @click="onGameSelected('snake')">Snake</v-ons-button>
</div>
</div>
<div v-show="state == 'init'">
<div class="desc">
<h1 v-if="config.title">{{config.title}}</h1>
<div v-html="config.initDescription"/>
</div>
<div class="center">
<v-ons-button @click="proceed">Proceed</v-ons-button>
</div>
</div>
<div></div>
<div v-show="state == 'game'">
<component :is="component" v-bind="{ config }" v-on:gameSuccess="success" v-on:gameFail="fail"></component>
</div>
<div></div>
<div v-show="state == 'end'">
<div class="desc">
<h1 v-if="config.title">{{config.title}}</h1>
<div v-html="config.endDescription"/>
</div>
<div class="center">
<v-ons-button @click="close">Close</v-ons-button>
</div>
</div>
<div v-show="state == 'fail'">
<div class="desc">
<h1 v-if="config.title">{{config.title}}</h1>
<div v-html="config.failDescription"/>
</div>
<div class="center">
<v-ons-button @click="close">Close</v-ons-button>
</div>
</div>
<div v-show="state == 'preFail'">
<div class="desc">
<h1 v-if="config.title">{{config.title}}</h1>
<div v-html="config.preFailDescription"/>
</div>
<div class="center">
<v-ons-button @click="close">Close</v-ons-button>
</div>
</div>
<div v-show="state == 'notbroken'">
<div class="desc">
<h1 v-if="config.title">{{config.title}}</h1>
<div v-html="config.notBrokenDescription || defaultNotBroken"/>
</div>
<div class="center">
<v-ons-button @click="close">Close</v-ons-button>
</div>
</div>
</v-ons-page>
</template>

<style lang="scss" scoped>
.desc {
margin: 1em;
}
.center {
text-align: center;
}
</style>

<script>
import PhaseSyncGame from './games/PhaseSyncGame'
import ManualGame from './games/ManualGame'
import LightsOut from './games/LightsOut'
import FlappyDrone from './games/FlappyDrone.vue'
import ValueBalance from './games/ValueBalance.vue'
import Nonogram from './games/Nonogram.vue'
import Snake from './games/Snake.vue'
import { flappyConfig } from './game-test-configs/flappy';
import { snakeConfig } from './game-test-configs/snake';
import cloneDeep from "lodash-es";
const GameComponents = {
phasesync: PhaseSyncGame,
manual: ManualGame,
lightsout: LightsOut,
flappy: FlappyDrone,
balance: ValueBalance,
nonogram: Nonogram,
snake: Snake,
}
const Games = {
PhaseSync: 'phasesync',
Manual: 'manual',
LightsOut: 'lightsout',
FlappyDrone: 'flappy',
ValueBalance: 'balance',
Nonogram: 'nonogram',
Snake: 'snake',
}
export default {
data() {
return {
tag: "",
component: undefined,
game: {},
gameConfig: {},
config: {},
state: 'scanning',
defaultNotBroken: 'The system is operating normally',
debug: false,
debugCount: 0,
gameLoader: () => undefined,
startTime: 0,
skillLevel: 'default',
skillLevels: [
{ label: 'Default', value: 'default' },
{ label: 'Novice (hard)', value: 'skill:novice' },
{ label: 'Master (medium)', value: 'skill:master' },
{ label: 'Expert (easy)', value: 'skill:expert' },]
}
},
methods: {
async onGameSelected(gameType) {
console.log("Launching game", gameType);
switch (gameType) {
case Games.PhaseSync:
this.tag = 'game:phasesync'
break
case Games.Manual:
this.tag = 'game:manual'
break
case Games.LightsOut:
this.tag = 'game:lightsout'
break
case Games.FlappyDrone:
this.tag = 'game:flappy'
this.gameConfig = cloneDeep(flappyConfig);
break
case Games.ValueBalance:
this.tag = 'game:balance'
break
case Games.Nonogram:
this.tag = 'game:nonogram'
break
case Games.Snake:
this.tag = 'game:snake'
this.gameConfig = cloneDeep(snakeConfig);
break
default:
console.error(`Unknown game type: ${gameType}`)
return
}
this.game = {};
this.startGame();
},
async startGame() {
const gameConfig = JSON.parse(JSON.stringify(this.gameConfig));
const difficulty = this.skillLevel;
let config;
if (difficulty in gameConfig) {
config = gameConfig[difficulty];
} else {
config = gameConfig.default;
console.log('Using default config', config)
}
if (this.game.config) {
config = { ...config, ...this.game.config }
}
this.config = config
if (config.game in GameComponents) {
this.gameLoader = () => {
this.component = GameComponents[config.game]
this.startTime = Date.now()
}
let condition = true
if (condition) {
if (config.initDescription) {
this.state = 'init'
} else {
this.state = 'game'
this.gameLoader();
this.gameLoader = () => undefined;
}
}else {
this.state = 'preFail'
}
} else {
console.log(`Game type '${config.game}' is unknown`)
}
},
proceed() {
if (typeof this.gameLoader === 'function') {
this.gameLoader();
this.gameLoader = () => undefined;
}
this.state = 'game';
},
success() {
if (this.config.endDescription) {
this.state = 'end';
} else {
this.$ons.notifications.alert('Game completed', { title: 'Game completed!', maskColor: 'rgba(0, 255, 0, 0.2)' })
this.close();
}
},
fail() {
if (this.config.failDescription) {
this.state = 'fail'
} else {
this.close()
}
},
close() {
this.$store.commit('navigator/pop')
},
},
}
</script>
<style lang="scss" scoped>
p.italic {
font-style: italic;
}
.game-selection-button-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
> * {
margin: 0.5em;
max-width: 300px;
text-align: center;
}
}
</style>
28 changes: 25 additions & 3 deletions src/components/Greeter.vue
Original file line number Diff line number Diff line change
@@ -3,9 +3,14 @@
<div class="greeter">
<h1>HANSCA</h1>
<h3>The Standard Universal Hand Scanner</h3>
<p class="bioid">Bio ID:</p>
<v-ons-input v-model="bioId"></v-ons-input>
<v-ons-button class="submit" @click="submit">Submit</v-ons-button>
<div class="login-form" v-if="showLoginInput">
<p class="bioid">Scan or enter your Bio ID:</p>
<v-ons-input v-model="bioId" @keyup="onKeyPress" autofocus></v-ons-input>
<v-ons-button class="submit" @click="submit">Proceed</v-ons-button>
</div>
<div class="login-form" v-else>
<p class="bioid">Scan your Bio ID to proceed</p>
</div>
<p class="version">Version {{version}}</p>
<v-ons-button @click="promptNfc" v-if="!isNfcPermissionGranted">Enable NFC reader</v-ons-button>
</div>
@@ -26,6 +31,11 @@ export default {
isNfcPermissionGranted: false,
}
},
computed: {
showLoginInput() {
return !this.isNfcPermissionGranted || process.env.NODE_ENV !== 'production';
}
},
created() {
getBlob('/data/misc', 'hansca').then(res => {
const analyseBaseTime = res.analyseBaseTime;
@@ -43,6 +53,11 @@ export default {
this.$ons.notification.toast('Scanned tag is not a Bio ID', { timeout: 2500, animation: 'fall' })
}
},
onKeyPress(event) {
if (event.key === 'Enter') {
this.submit()
}
},
submit() {
this.login(this.bioId)
},
@@ -93,6 +108,13 @@ export default {
justify-content: center;
align-items: center;
}
.login-form {
vertical-align: middle;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.greeter h1 {
color: #D38312;
}
File renamed without changes.
41 changes: 41 additions & 0 deletions src/components/game-test-configs/flappy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const flappyConfig = {
default: {
gap: 150,
game: "flappy",
score: 20,
gravity: 1.7,
interval: 250,
preCondition: "/data/misc/flappy_drone",
endDescription: "Drone successfully arrived at hull. Repairs started.",
failDescription: "Drone destroyed!",
initDescription:
"<p>Manual hull repairs required using a drone.</p>\n\t\t<p>You must fly the drone to the hull area. Tap HANSCA to fly upwards.</p>",
preFailDescription: "No drones available!",
},
"skill:expert": {
gap: 150,
game: "flappy",
score: 7,
gravity: 1.3,
interval: 250,
preCondition: "/data/misc/flappy_drone",
endDescription: "Drone successfully arrived at hull. Repairs started.",
failDescription: "Drone destroyed!",
initDescription:
"<p>Manual hull repairs required using a drone.</p>\n\t\t<p>You must fly the drone to the hull area. Tap HANSCA to fly upwards.</p>",
preFailDescription: "No drones available!",
},
"skill:master": {
gap: 150,
game: "flappy",
score: 10,
gravity: 1.6,
interval: 250,
preCondition: "/data/misc/flappy_drone",
endDescription: "Drone successfully arrived at hull. Repairs started.",
failDescription: "Drone destroyed!",
initDescription:
"<p>Manual hull repairs required using a drone.</p>\n\t\t<p>You must fly the drone to the hull area. Tap HANSCA to fly upwards.</p>",
preFailDescription: "No drones available!",
},
};
Loading

0 comments on commit a0d5d37

Please sign in to comment.