puzzle racer WIP
parent
38da9f5b6a
commit
420617db64
|
@ -7,8 +7,9 @@ import play.api.libs.json._
|
|||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.user.User
|
||||
import lila.common.String.html.safeJsonValue
|
||||
import lila.racer.RacerRace
|
||||
import lila.user.User
|
||||
|
||||
object racer {
|
||||
|
||||
|
@ -30,12 +31,40 @@ object racer {
|
|||
def show(race: RacerRace, data: JsObject, pref: JsObject)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
moreCss = frag(cssTag("racer")),
|
||||
moreJs = frag(
|
||||
jsModule("racer"),
|
||||
embedJsUnsafeLoadThen(
|
||||
s"""LichessRacer.start(${safeJsonValue(
|
||||
Json.obj(
|
||||
"data" -> data,
|
||||
"pref" -> pref,
|
||||
"i18n" -> i18nJsObject(i18nKeys)
|
||||
)
|
||||
)})"""
|
||||
)
|
||||
),
|
||||
title = "Puzzle Racer",
|
||||
zoomable = true,
|
||||
chessground = false
|
||||
) {
|
||||
main(
|
||||
div(cls := "racer racer--app")(race.toString)
|
||||
div(cls := "racer racer-app racer--play")(
|
||||
div(cls := "racer__board main-board"),
|
||||
div(cls := "racer__side")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val i18nKeys = {
|
||||
import lila.i18n.I18nKeys.{ storm => s }
|
||||
List(
|
||||
s.moveToStart,
|
||||
s.puzzlesSolved,
|
||||
s.playAgain,
|
||||
s.score,
|
||||
s.moves,
|
||||
s.combo,
|
||||
s.newRun
|
||||
).map(_.key)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,26 +137,26 @@ object storm {
|
|||
)
|
||||
|
||||
private val i18nKeys = {
|
||||
import lila.i18n.I18nKeys.{ storm => t }
|
||||
import lila.i18n.I18nKeys.{ storm => s }
|
||||
List(
|
||||
t.moveToStart,
|
||||
t.puzzlesSolved,
|
||||
t.newDailyHighscore,
|
||||
t.newWeeklyHighscore,
|
||||
t.newMonthlyHighscore,
|
||||
t.newAllTimeHighscore,
|
||||
t.previousHighscoreWasX,
|
||||
t.playAgain,
|
||||
t.score,
|
||||
t.moves,
|
||||
t.accuracy,
|
||||
t.combo,
|
||||
t.time,
|
||||
t.timePerMove,
|
||||
t.highestSolved,
|
||||
t.puzzlesPlayed,
|
||||
t.newRun,
|
||||
t.endRun
|
||||
s.moveToStart,
|
||||
s.puzzlesSolved,
|
||||
s.newDailyHighscore,
|
||||
s.newWeeklyHighscore,
|
||||
s.newMonthlyHighscore,
|
||||
s.newAllTimeHighscore,
|
||||
s.previousHighscoreWasX,
|
||||
s.playAgain,
|
||||
s.score,
|
||||
s.moves,
|
||||
s.accuracy,
|
||||
s.combo,
|
||||
s.time,
|
||||
s.timePerMove,
|
||||
s.highestSolved,
|
||||
s.puzzlesPlayed,
|
||||
s.newRun,
|
||||
s.endRun
|
||||
).map(_.key)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
"ui/dgt",
|
||||
"ui/puz",
|
||||
"ui/storm",
|
||||
"ui/racer",
|
||||
"ui/@build/rollupProject",
|
||||
"ui/@types/lichess",
|
||||
"ui/@types/cash"
|
||||
|
|
2
ui/build
2
ui/build
|
@ -15,7 +15,7 @@ mkdir -p public/compiled
|
|||
|
||||
apps1="common"
|
||||
apps2="chess ceval game tree chat nvui puz"
|
||||
apps3="site swiss msg chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul dasher speech palantir serviceWorker dgt storm"
|
||||
apps3="site swiss msg chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul dasher speech palantir serviceWorker dgt storm racer"
|
||||
site_plugins="tvEmbed puzzleEmbed analyseEmbed user modUser clas coordinate captcha expandText team forum account coachShow coachForm challengePage checkout login passwordComplexity tourForm teamBattleForm gameSearch userComplete infiniteScroll flatpickr teamAdmin appeal modGames"
|
||||
round_plugins="nvui keyboardMove"
|
||||
analyse_plugins="nvui studyTopicForm"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
.puz-clock {
|
||||
@extend %flex-center-nowrap;
|
||||
|
||||
margin-bottom: -1em;
|
||||
font-family: 'storm';
|
||||
|
||||
&__time {
|
||||
font-size: 6em;
|
||||
transition: color 0.3s;
|
||||
margin: 2vh 0;
|
||||
|
||||
@include breakpoint($mq-col2) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
color: $c-good;
|
||||
}
|
||||
|
||||
.storm--mod-malus-slow & {
|
||||
color: $c-bad;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mod-fade-out {
|
||||
from {
|
||||
transform: translate(0, -10px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0, -40px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&__bonus,
|
||||
&__malus {
|
||||
font-size: 3.5em;
|
||||
color: $c-good;
|
||||
margin-left: 0.3ch;
|
||||
animation: mod-fade-out 1.1s ease-out;
|
||||
}
|
||||
|
||||
&__malus {
|
||||
color: $c-bad;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
.puz-combo {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
&__counter {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
&__value {
|
||||
@extend %flex-center-nowrap;
|
||||
|
||||
justify-content: center;
|
||||
font-family: 'storm';
|
||||
font-size: 2.4em;
|
||||
line-height: 0.9em;
|
||||
width: 2ch;
|
||||
}
|
||||
|
||||
&__combo {
|
||||
@extend %roboto;
|
||||
|
||||
font-size: 0.8em;
|
||||
letter-spacing: -1px;
|
||||
color: $c-font-dim;
|
||||
}
|
||||
|
||||
transition: color 0.1s;
|
||||
|
||||
.storm--mod-move & {
|
||||
color: $c-brag;
|
||||
}
|
||||
}
|
||||
|
||||
&__bars {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 100%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
@extend %box-radius;
|
||||
|
||||
$c-bar-base: $c-bg-zebra2;
|
||||
$c-in-base: $c-brag;
|
||||
|
||||
flex: 0 0 2.2em;
|
||||
background: $c-bar-base;
|
||||
border: $border;
|
||||
position: relative;
|
||||
|
||||
&__in,
|
||||
&__in-full {
|
||||
@extend %box-radius;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__in {
|
||||
background: $c-in-base;
|
||||
box-shadow: 0 0 15px $c-in-base;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.storm--mod-malus-slow & {
|
||||
transition-property: width;
|
||||
background: $c-bad;
|
||||
box-shadow: 0 0 10px $c-bad, 0 0 20px $c-bad;
|
||||
}
|
||||
}
|
||||
|
||||
&__in-full {
|
||||
background: $c-primary;
|
||||
box-shadow: 0 0 10px $c-primary, 0 0 20px $c-primary;
|
||||
width: 100%;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@keyframes bar-full {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
display: block;
|
||||
animation: bar-full 0.9s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__levels {
|
||||
@extend %flex-center;
|
||||
|
||||
margin: 0.3em 0 0 -0.6em;
|
||||
}
|
||||
|
||||
&__level {
|
||||
$c-level: $c-primary;
|
||||
|
||||
transform: skewX(-45deg);
|
||||
flex: 21% 0 0;
|
||||
margin-right: 4%;
|
||||
font-size: 0.9em;
|
||||
height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: $border;
|
||||
background: $c-bg-zebra;
|
||||
text-align: center;
|
||||
color: $c-font-dimmer;
|
||||
font-weight: bold;
|
||||
|
||||
span {
|
||||
transform: skewX(45deg);
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes level-fade-in {
|
||||
from {
|
||||
background: white;
|
||||
box-shadow: 0 0 15px white, 0 0 25px white;
|
||||
}
|
||||
|
||||
to {
|
||||
box-shadow: 0 0 10px $c-level;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
animation: level-fade-in 1s ease-out;
|
||||
background: mix($c-level, black, 80%);
|
||||
border: 1px solid $c-level;
|
||||
box-shadow: 0 0 10px $c-level;
|
||||
color: white;
|
||||
|
||||
&:nth-child(2) {
|
||||
background: $c-level;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: mix($c-level, white, 60%);
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: mix($c-level, white, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
.storm {
|
||||
&__side {
|
||||
@extend %box-neat;
|
||||
$mq-col2: $mq-col2-uniboard;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
background: $c-bg-box;
|
||||
margin: 0;
|
||||
padding: 2vh 2vw;
|
||||
.puz-side {
|
||||
@extend %box-neat;
|
||||
|
||||
@include breakpoint($mq-col2) {
|
||||
padding: 3vh 2vw;
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
background: $c-bg-box;
|
||||
margin: 0;
|
||||
padding: 2vh 2vw;
|
||||
|
||||
@include breakpoint($mq-col2) {
|
||||
padding: 3vh 2vw;
|
||||
}
|
||||
|
||||
&__top {
|
|
@ -0,0 +1,20 @@
|
|||
import { getNow } from './util';
|
||||
import config from './config';
|
||||
|
||||
export class Clock {
|
||||
startAt: number | undefined;
|
||||
initialMillis = config.clock.initial * 1000;
|
||||
|
||||
start = () => {
|
||||
this.startAt = getNow();
|
||||
};
|
||||
|
||||
millis = (): number =>
|
||||
this.startAt ? Math.max(0, this.startAt + this.initialMillis - getNow()) : this.initialMillis;
|
||||
|
||||
addSeconds = (seconds: number) => {
|
||||
this.initialMillis += seconds * 1000;
|
||||
};
|
||||
|
||||
flag = () => !this.millis();
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Role } from 'chessground/types';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import { Clock } from './clock';
|
||||
import { Combo } from './combo';
|
||||
import CurrentPuzzle from './current';
|
||||
|
||||
|
@ -13,7 +14,7 @@ export interface Promotion {
|
|||
view(): MaybeVNode;
|
||||
}
|
||||
|
||||
export interface Prefs {
|
||||
export interface PuzPrefs {
|
||||
coords: 0 | 1 | 2;
|
||||
is3d: boolean;
|
||||
destination: boolean;
|
||||
|
@ -37,7 +38,7 @@ export interface Run {
|
|||
moves: number;
|
||||
errors: number;
|
||||
current: CurrentPuzzle;
|
||||
clockMs: number;
|
||||
clock: Clock;
|
||||
history: Round[];
|
||||
combo: Combo;
|
||||
modifier: Modifier;
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Run } from './interfaces';
|
||||
import { Config as CgConfig } from 'chessground/config';
|
||||
import { getNow, uciToLastMove } from './util';
|
||||
import { uciToLastMove } from './util';
|
||||
import { opposite } from 'chessops';
|
||||
import { makeFen, parseFen } from 'chessops/fen';
|
||||
import { chessgroundDests } from 'chessops/compat';
|
||||
|
||||
export const boundedClockMillis = (run: Run) =>
|
||||
run.startAt ? Math.max(0, run.startAt + run.clockMs - getNow()) : run.clockMs;
|
||||
|
||||
export const makeCgOpts = (run: Run): CgConfig => {
|
||||
const cur = run.current;
|
||||
const pos = cur.position();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import changeColorHandle from 'common/coordsColor';
|
||||
import resizeHandle from 'common/resize';
|
||||
import { Config as CgConfig } from 'chessground/config';
|
||||
import { Prefs, UserMove } from '../interfaces';
|
||||
import { PuzPrefs, UserMove } from '../interfaces';
|
||||
|
||||
export function makeConfig(opts: CgConfig, pref: Prefs, userMove: UserMove): CgConfig {
|
||||
export function makeConfig(opts: CgConfig, pref: PuzPrefs, userMove: UserMove): CgConfig {
|
||||
return {
|
||||
fen: opts.fen,
|
||||
orientation: opts.orientation,
|
||||
|
|
|
@ -12,12 +12,12 @@ let lastText: string;
|
|||
export default function renderClock(run: Run, onFlag: OnFlag): VNode {
|
||||
const malus = run.modifier.malus;
|
||||
const bonus = run.modifier.bonus;
|
||||
return h('div.storm__clock', [
|
||||
h('div.storm__clock__time', {
|
||||
return h('div.puz-clock', [
|
||||
h('div.puz-clock__time', {
|
||||
hook: {
|
||||
insert(node) {
|
||||
const el = node.elm as HTMLDivElement;
|
||||
el.innerText = formatMs(run.clockMs);
|
||||
el.innerText = formatMs(run.clock.millis());
|
||||
refreshInterval = setInterval(() => renderIn(run, onFlag, el), 100);
|
||||
},
|
||||
destroy() {
|
||||
|
@ -25,17 +25,16 @@ export default function renderClock(run: Run, onFlag: OnFlag): VNode {
|
|||
},
|
||||
},
|
||||
}),
|
||||
!!malus && malus.at > getNow() - 900 ? h('div.storm__clock__malus', '-' + malus.seconds) : null,
|
||||
!!bonus && bonus.at > getNow() - 900 ? h('div.storm__clock__bonus', '+' + bonus.seconds) : null,
|
||||
!!malus && malus.at > getNow() - 900 ? h('div.puz-clock__malus', '-' + malus.seconds) : null,
|
||||
!!bonus && bonus.at > getNow() - 900 ? h('div.puz-clock__bonus', '+' + bonus.seconds) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
function renderIn(run: Run, onFlag: OnFlag, el: HTMLElement) {
|
||||
if (!run.startAt) return;
|
||||
const clock = run.clockMs;
|
||||
const mods = run.modifier;
|
||||
const now = getNow();
|
||||
const millis = run.startAt + clock - getNow();
|
||||
const millis = run.clock.millis();
|
||||
const diffs = computeModifierDiff(now, mods.bonus) - computeModifierDiff(now, mods.malus);
|
||||
const text = formatMs(millis - diffs);
|
||||
if (text != lastText) el.innerText = text;
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { h } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import config from '../config';
|
||||
import { Run } from '../interfaces';
|
||||
import { getNow } from '../util';
|
||||
|
||||
export const playModifiers = (run: Run) => {
|
||||
const now = getNow();
|
||||
const malus = run.modifier.malus;
|
||||
const bonus = run.modifier.bonus;
|
||||
return {
|
||||
'storm--mod-puzzle': run.current.startAt > now - 90,
|
||||
'storm--mod-move': run.modifier.moveAt > now - 90,
|
||||
'storm--mod-malus-slow': !!malus && malus.at > now - 950,
|
||||
'storm--mod-bonus-slow': !!bonus && bonus.at > now - 950,
|
||||
};
|
||||
};
|
||||
|
||||
export const renderCombo = (run: Run): VNode => {
|
||||
const level = run.combo.level();
|
||||
return h('div.puz-combo', [
|
||||
h('div.puz-combo__counter', [
|
||||
h('span.puz-combo__counter__value', run.combo.current),
|
||||
h('span.puz-combo__counter__combo', 'COMBO'),
|
||||
]),
|
||||
h('div.puz-combo__bars', [
|
||||
h('div.puz-combo__bar', [
|
||||
h('div.puz-combo__bar__in', {
|
||||
attrs: { style: `width:${run.combo.percent()}%` },
|
||||
}),
|
||||
h('div.puz-combo__bar__in-full'),
|
||||
]),
|
||||
h(
|
||||
'div.puz-combo__levels',
|
||||
[0, 1, 2, 3].map(l =>
|
||||
h(
|
||||
'div.puz-combo__level',
|
||||
{
|
||||
class: {
|
||||
active: l < level,
|
||||
},
|
||||
},
|
||||
h('span', `${config.combo.levels[l + 1][1]}s`)
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
]);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
.racer {
|
||||
&--play {
|
||||
display: grid;
|
||||
|
||||
&__side {
|
||||
grid-area: side;
|
||||
}
|
||||
|
||||
&__board {
|
||||
grid-area: board;
|
||||
}
|
||||
|
||||
grid-row-gap: $block-gap;
|
||||
grid-column-gap: $block-gap;
|
||||
grid-template-areas: 'board' 'side';
|
||||
|
||||
@include breakpoint($mq-col2) {
|
||||
grid-template-columns: $col2-uniboard-width $col2-uniboard-table;
|
||||
grid-template-rows: fit-content(0);
|
||||
grid-template-areas: 'board side';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/layout/uniboard';
|
||||
@import '../../../common/css/component/board-resize';
|
||||
@import '../../../common/css/component/bar-glider';
|
||||
@import '../../../chess/css/promotion';
|
||||
@import '../../../puz/css/font';
|
||||
@import '../../../puz/css/side';
|
||||
@import '../../../puz/css/combo';
|
||||
@import '../../../puz/css/clock';
|
||||
|
||||
@import '../racer';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/dark';
|
||||
@import 'racer';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/light';
|
||||
@import 'racer';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/transp';
|
||||
@import 'racer';
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "racer",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "lichess.org puzzle racer",
|
||||
"keywords": [
|
||||
"chess",
|
||||
"lichess",
|
||||
"puzzle",
|
||||
"racer"
|
||||
],
|
||||
"author": "Thibault Duplessis",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@build/rollupProject": "2.0.0",
|
||||
"@types/lichess": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessops": "^0.8.1",
|
||||
"chessground": "^7.11.0",
|
||||
"snabbdom": "^0.7.4",
|
||||
"common": "2.0.0",
|
||||
"puz": "2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "rollup --config",
|
||||
"prod": "rollup --config --config-prod"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { rollupProject } from '@build/rollupProject';
|
||||
|
||||
export default rollupProject({
|
||||
main: {
|
||||
name: 'LichessRacer',
|
||||
input: 'src/main.ts',
|
||||
output: 'racer',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
import config from 'puz/config';
|
||||
import makePromotion from 'puz/promotion';
|
||||
import sign from 'puz/sign';
|
||||
import { Api as CgApi } from 'chessground/api';
|
||||
import { getNow, loadSound } from 'puz/util';
|
||||
import { makeCgOpts } from 'puz/run';
|
||||
import { parseUci } from 'chessops/util';
|
||||
import { prop, Prop } from 'common';
|
||||
import { Role } from 'chessground/types';
|
||||
import { RacerOpts, RacerData, RacerVm, RacerPrefs } from './interfaces';
|
||||
import { Promotion, Run } from 'puz/interfaces';
|
||||
import { Combo } from 'puz/combo';
|
||||
import CurrentPuzzle from 'puz/current';
|
||||
import { Clock } from 'puz/clock';
|
||||
|
||||
export default class StormCtrl {
|
||||
private data: RacerData;
|
||||
private redraw: () => void;
|
||||
pref: RacerPrefs;
|
||||
run: Run;
|
||||
vm: RacerVm;
|
||||
trans: Trans;
|
||||
promotion: Promotion;
|
||||
ground = prop<CgApi | false>(false) as Prop<CgApi | false>;
|
||||
|
||||
constructor(opts: RacerOpts, redraw: (data: RacerData) => void) {
|
||||
this.data = opts.data;
|
||||
this.pref = opts.pref;
|
||||
this.redraw = () => redraw(this.data);
|
||||
this.trans = lichess.trans(opts.i18n);
|
||||
this.run = {
|
||||
moves: 0,
|
||||
errors: 0,
|
||||
current: new CurrentPuzzle(0, this.data.puzzles[0]),
|
||||
clock: new Clock(),
|
||||
history: [],
|
||||
combo: new Combo(),
|
||||
modifier: {
|
||||
moveAt: 0,
|
||||
},
|
||||
};
|
||||
this.vm = {
|
||||
signed: prop(undefined),
|
||||
};
|
||||
this.promotion = makePromotion(this.withGround, () => makeCgOpts(this.run), this.redraw);
|
||||
if (this.data.key) setTimeout(() => sign(this.data.key!).then(this.vm.signed), 1000 * 40);
|
||||
}
|
||||
|
||||
end = (): void => {
|
||||
this.run.history.reverse();
|
||||
this.run.endAt = getNow();
|
||||
this.ground(false);
|
||||
this.redraw();
|
||||
this.sound.end();
|
||||
this.redrawSlow();
|
||||
};
|
||||
|
||||
endNow = (): void => {
|
||||
this.pushToHistory(false);
|
||||
this.end();
|
||||
};
|
||||
|
||||
userMove = (orig: Key, dest: Key): void => {
|
||||
if (!this.promotion.start(orig, dest, this.playUserMove)) this.playUserMove(orig, dest);
|
||||
};
|
||||
|
||||
playUserMove = (orig: Key, dest: Key, promotion?: Role): void => {
|
||||
if (!this.run.moves) this.run.startAt = getNow();
|
||||
this.run.moves++;
|
||||
this.promotion.cancel();
|
||||
const cur = this.run.current;
|
||||
const uci = `${orig}${dest}${promotion ? (promotion == 'knight' ? 'n' : promotion[0]) : ''}`;
|
||||
const pos = cur.position();
|
||||
const move = parseUci(uci)!;
|
||||
let captureSound = pos.board.occupied.has(move.to);
|
||||
pos.play(move);
|
||||
if (pos.isCheckmate() || uci == cur.expectedMove()) {
|
||||
cur.moveIndex++;
|
||||
this.run.combo.inc();
|
||||
this.run.modifier.moveAt = getNow();
|
||||
const bonus = this.run.combo.bonus();
|
||||
if (bonus) {
|
||||
this.run.modifier.bonus = bonus;
|
||||
this.run.clock.addSeconds(bonus.seconds);
|
||||
this.sound.bonus();
|
||||
}
|
||||
if (cur.isOver()) {
|
||||
this.pushToHistory(true);
|
||||
if (!this.incPuzzle()) this.end();
|
||||
} else {
|
||||
cur.moveIndex++;
|
||||
captureSound = captureSound || pos.board.occupied.has(parseUci(cur.line[cur.moveIndex]!)!.to);
|
||||
}
|
||||
this.sound.move(captureSound);
|
||||
} else {
|
||||
lichess.sound.play('error');
|
||||
this.pushToHistory(false);
|
||||
this.run.errors++;
|
||||
this.run.combo.reset();
|
||||
this.run.clock.addSeconds(-config.clock.malus);
|
||||
this.run.modifier.malus = {
|
||||
seconds: config.clock.malus,
|
||||
at: getNow(),
|
||||
};
|
||||
if (this.run.clock.flag()) this.end();
|
||||
else if (!this.incPuzzle()) this.end();
|
||||
}
|
||||
this.redraw();
|
||||
this.redrawQuick();
|
||||
this.redrawSlow();
|
||||
this.withGround(g => g.set(makeCgOpts(this.run)));
|
||||
lichess.pubsub.emit('ply', this.run.moves);
|
||||
};
|
||||
|
||||
private redrawQuick = () => setTimeout(this.redraw, 100);
|
||||
private redrawSlow = () => setTimeout(this.redraw, 1000);
|
||||
|
||||
private pushToHistory = (win: boolean) =>
|
||||
this.run.history.push({
|
||||
puzzle: this.run.current.puzzle,
|
||||
win,
|
||||
millis: this.run.history.length ? getNow() - this.run.current.startAt : 0, // first one is free
|
||||
});
|
||||
|
||||
private incPuzzle = (): boolean => {
|
||||
const index = this.run.current.index;
|
||||
if (index < this.data.puzzles.length - 1) {
|
||||
this.run.current = new CurrentPuzzle(index + 1, this.data.puzzles[index + 1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
countWins = (): number => this.run.history.reduce((c, r) => c + (r.win ? 1 : 0), 0);
|
||||
|
||||
withGround = <A>(f: (cg: CgApi) => A): A | false => {
|
||||
const g = this.ground();
|
||||
return g && f(g);
|
||||
};
|
||||
|
||||
private sound = {
|
||||
move: (take: boolean) => lichess.sound.play(take ? 'capture' : 'move'),
|
||||
bonus: loadSound('other/ping', 0.8, 1000),
|
||||
end: loadSound('other/gewonnen', 0.6, 5000),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { Prop } from 'common';
|
||||
import { PuzPrefs, Puzzle } from 'puz/interfaces';
|
||||
|
||||
export interface RacerOpts {
|
||||
data: RacerData;
|
||||
pref: RacerPrefs;
|
||||
i18n: any;
|
||||
}
|
||||
|
||||
export interface RacerPrefs extends PuzPrefs {}
|
||||
|
||||
export interface RacerData {
|
||||
race: Race;
|
||||
puzzles: Puzzle[];
|
||||
owner: boolean;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface Race {
|
||||
id: string;
|
||||
players: Player[];
|
||||
startRel?: number;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
index: number;
|
||||
user?: LightUser;
|
||||
}
|
||||
|
||||
export interface RacerVm {
|
||||
signed: Prop<string | undefined>;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import attributes from 'snabbdom/modules/attributes';
|
||||
import klass from 'snabbdom/modules/class';
|
||||
import menuHover from 'common/menuHover';
|
||||
import RacerCtrl from './ctrl';
|
||||
import { Chessground } from 'chessground';
|
||||
import { init } from 'snabbdom';
|
||||
import { RacerOpts } from './interfaces';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
|
||||
const patch = init([klass, attributes]);
|
||||
|
||||
import view from './view/main';
|
||||
|
||||
export function start(opts: RacerOpts) {
|
||||
const element = document.querySelector('.racer-app') as HTMLElement;
|
||||
|
||||
let vnode: VNode;
|
||||
|
||||
function redraw() {
|
||||
vnode = patch(vnode, view(ctrl));
|
||||
}
|
||||
|
||||
const ctrl = new RacerCtrl(opts, redraw);
|
||||
|
||||
const blueprint = view(ctrl);
|
||||
element.innerHTML = '';
|
||||
vnode = patch(element, blueprint);
|
||||
|
||||
menuHover();
|
||||
$('script').remove();
|
||||
}
|
||||
|
||||
// that's for the rest of lichess to access chessground
|
||||
// without having to include it a second time
|
||||
window.Chessground = Chessground;
|
|
@ -0,0 +1,79 @@
|
|||
import { Chessground } from 'chessground';
|
||||
import { makeConfig as makeCgConfig } from 'puz/view/chessground';
|
||||
import renderClock from 'puz/view/clock';
|
||||
import RacerCtrl from '../ctrl';
|
||||
import { onInsert } from 'puz/util';
|
||||
import { playModifiers, renderCombo } from 'puz/view/util';
|
||||
import { h } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import { makeCgOpts } from 'puz/run';
|
||||
|
||||
export default function (ctrl: RacerCtrl): VNode {
|
||||
return h(
|
||||
'div.racer.racer-app.racer--play',
|
||||
{
|
||||
class: playModifiers(ctrl.run),
|
||||
},
|
||||
renderPlay(ctrl)
|
||||
);
|
||||
}
|
||||
|
||||
const chessground = (ctrl: RacerCtrl): VNode =>
|
||||
h('div.cg-wrap', {
|
||||
hook: {
|
||||
insert: vnode =>
|
||||
ctrl.ground(
|
||||
Chessground(vnode.elm as HTMLElement, makeCgConfig(makeCgOpts(ctrl.run), ctrl.pref, ctrl.userMove))
|
||||
),
|
||||
destroy: _ => ctrl.withGround(g => g.destroy()),
|
||||
},
|
||||
});
|
||||
|
||||
const renderPlay = (ctrl: RacerCtrl): VNode[] => [
|
||||
h('div.storm__board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
|
||||
h('div.storm__side', [
|
||||
ctrl.run.startAt ? renderSolved(ctrl) : renderStart(ctrl),
|
||||
renderClock(ctrl.run, ctrl.endNow),
|
||||
h('div.storm__table', [renderControls(ctrl), renderCombo(ctrl.run)]),
|
||||
]),
|
||||
];
|
||||
|
||||
const renderControls = (ctrl: RacerCtrl): VNode =>
|
||||
h('div.storm__control', [
|
||||
h('a.storm__control__reload.button.button-empty', {
|
||||
attrs: {
|
||||
href: '/storm',
|
||||
'data-icon': 'B',
|
||||
title: ctrl.trans('newRun'),
|
||||
},
|
||||
}),
|
||||
h('a.storm__control__end.button.button-empty', {
|
||||
attrs: {
|
||||
'data-icon': 'b',
|
||||
title: ctrl.trans('endRun'),
|
||||
},
|
||||
hook: onInsert(el => el.addEventListener('click', ctrl.endNow)),
|
||||
}),
|
||||
]);
|
||||
|
||||
const renderSolved = (ctrl: RacerCtrl): VNode =>
|
||||
h('div.storm__top.storm__solved', [h('div.storm__solved__text', ctrl.countWins())]);
|
||||
|
||||
const renderStart = (ctrl: RacerCtrl) =>
|
||||
h(
|
||||
'div.storm__top.storm__start',
|
||||
h('div.storm__start__text', [h('strong', 'Puzzle Storm'), h('span', ctrl.trans('moveToStart'))])
|
||||
);
|
||||
|
||||
const renderReload = (msg: string) =>
|
||||
h('div.storm.storm--reload.box.box-pad', [
|
||||
h('i', { attrs: { 'data-icon': '~' } }),
|
||||
h('p', msg),
|
||||
h(
|
||||
'a.storm--dup__reload.button',
|
||||
{
|
||||
attrs: { href: '/storm' },
|
||||
},
|
||||
'Click to reload'
|
||||
),
|
||||
]);
|
|
@ -1,50 +0,0 @@
|
|||
.storm {
|
||||
&__clock {
|
||||
@extend %flex-center-nowrap;
|
||||
|
||||
margin-bottom: -1em;
|
||||
font-family: 'storm';
|
||||
|
||||
&__time {
|
||||
font-size: 6em;
|
||||
transition: color 0.3s;
|
||||
margin: 2vh 0;
|
||||
|
||||
@include breakpoint($mq-col2) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
color: $c-good;
|
||||
}
|
||||
|
||||
.storm--mod-malus-slow & {
|
||||
color: $c-bad;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mod-fade-out {
|
||||
from {
|
||||
transform: translate(0, -10px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(0, -40px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&__bonus,
|
||||
&__malus {
|
||||
font-size: 3.5em;
|
||||
color: $c-good;
|
||||
margin-left: 0.3ch;
|
||||
animation: mod-fade-out 1.1s ease-out;
|
||||
}
|
||||
|
||||
&__malus {
|
||||
color: $c-bad;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
.storm {
|
||||
&__combo {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
&__counter {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
&__value {
|
||||
@extend %flex-center-nowrap;
|
||||
|
||||
justify-content: center;
|
||||
font-family: 'storm';
|
||||
font-size: 2.4em;
|
||||
line-height: 0.9em;
|
||||
width: 2ch;
|
||||
}
|
||||
|
||||
&__combo {
|
||||
@extend %roboto;
|
||||
|
||||
font-size: 0.8em;
|
||||
letter-spacing: -1px;
|
||||
color: $c-font-dim;
|
||||
}
|
||||
|
||||
transition: color 0.1s;
|
||||
|
||||
.storm--mod-move & {
|
||||
color: $c-brag;
|
||||
}
|
||||
}
|
||||
|
||||
&__bars {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 100%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
@extend %box-radius;
|
||||
|
||||
$c-bar-base: $c-bg-zebra2;
|
||||
$c-in-base: $c-brag;
|
||||
|
||||
flex: 0 0 2.2em;
|
||||
background: $c-bar-base;
|
||||
border: $border;
|
||||
position: relative;
|
||||
|
||||
&__in,
|
||||
&__in-full {
|
||||
@extend %box-radius;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__in {
|
||||
background: $c-in-base;
|
||||
box-shadow: 0 0 15px $c-in-base;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.storm--mod-malus-slow & {
|
||||
transition-property: width;
|
||||
background: $c-bad;
|
||||
box-shadow: 0 0 10px $c-bad, 0 0 20px $c-bad;
|
||||
}
|
||||
}
|
||||
|
||||
&__in-full {
|
||||
background: $c-primary;
|
||||
box-shadow: 0 0 10px $c-primary, 0 0 20px $c-primary;
|
||||
width: 100%;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@keyframes bar-full {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.storm--mod-bonus-slow & {
|
||||
display: block;
|
||||
animation: bar-full 0.9s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__levels {
|
||||
@extend %flex-center;
|
||||
|
||||
margin: 0.3em 0 0 -0.6em;
|
||||
}
|
||||
|
||||
&__level {
|
||||
$c-level: $c-primary;
|
||||
|
||||
transform: skewX(-45deg);
|
||||
flex: 21% 0 0;
|
||||
margin-right: 4%;
|
||||
font-size: 0.9em;
|
||||
height: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: $border;
|
||||
background: $c-bg-zebra;
|
||||
text-align: center;
|
||||
color: $c-font-dimmer;
|
||||
font-weight: bold;
|
||||
|
||||
span {
|
||||
transform: skewX(45deg);
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes level-fade-in {
|
||||
from {
|
||||
background: white;
|
||||
box-shadow: 0 0 15px white, 0 0 25px white;
|
||||
}
|
||||
|
||||
to {
|
||||
box-shadow: 0 0 10px $c-level;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
animation: level-fade-in 1s ease-out;
|
||||
background: mix($c-level, black, 80%);
|
||||
border: 1px solid $c-level;
|
||||
box-shadow: 0 0 10px $c-level;
|
||||
color: white;
|
||||
|
||||
&:nth-child(2) {
|
||||
background: $c-level;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: mix($c-level, white, 60%);
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: mix($c-level, white, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
@import 'side';
|
||||
@import 'combo';
|
||||
@import 'clock';
|
||||
|
||||
.storm {
|
||||
&--play {
|
||||
display: grid;
|
||||
|
|
|
@ -10,7 +10,6 @@ $mq-col2: $mq-col2-uniboard;
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
@import 'font';
|
||||
@import 'play';
|
||||
@import 'end';
|
||||
@import 'high';
|
||||
|
|
|
@ -3,5 +3,9 @@
|
|||
@import '../../../common/css/component/board-resize';
|
||||
@import '../../../common/css/component/bar-glider';
|
||||
@import '../../../chess/css/promotion';
|
||||
@import '../../../puz/css/font';
|
||||
@import '../../../puz/css/side';
|
||||
@import '../../../puz/css/combo';
|
||||
@import '../../../puz/css/clock';
|
||||
|
||||
@import '../storm';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as xhr from './xhr';
|
||||
import config from 'puz/config';
|
||||
import makePromotion from 'puz/promotion';
|
||||
import sign from './sign';
|
||||
import sign from 'puz/sign';
|
||||
import { Api as CgApi } from 'chessground/api';
|
||||
import { getNow, loadSound } from 'puz/util';
|
||||
import { boundedClockMillis, makeCgOpts } from 'puz/run';
|
||||
import { makeCgOpts } from 'puz/run';
|
||||
import { parseUci } from 'chessops/util';
|
||||
import { prop, Prop } from 'common';
|
||||
import { Role } from 'chessground/types';
|
||||
|
@ -12,6 +12,7 @@ import { StormOpts, StormData, StormVm, StormRecap, StormPrefs } from './interfa
|
|||
import { Promotion, Run } from 'puz/interfaces';
|
||||
import { Combo } from 'puz/combo';
|
||||
import CurrentPuzzle from 'puz/current';
|
||||
import { Clock } from 'puz/clock';
|
||||
|
||||
export default class StormCtrl {
|
||||
private data: StormData;
|
||||
|
@ -32,7 +33,7 @@ export default class StormCtrl {
|
|||
moves: 0,
|
||||
errors: 0,
|
||||
current: new CurrentPuzzle(0, this.data.puzzles[0]),
|
||||
clockMs: config.clock.initial * 1000,
|
||||
clock: new Clock(),
|
||||
history: [],
|
||||
combo: new Combo(),
|
||||
modifier: {
|
||||
|
@ -56,9 +57,6 @@ export default class StormCtrl {
|
|||
}, config.timeToStart + 1000);
|
||||
}
|
||||
|
||||
clockMillis = (): number | undefined =>
|
||||
this.run.startAt && Math.max(0, this.run.startAt + this.run.clockMs - getNow());
|
||||
|
||||
end = (): void => {
|
||||
this.run.history.reverse();
|
||||
this.run.endAt = getNow();
|
||||
|
@ -98,7 +96,7 @@ export default class StormCtrl {
|
|||
const bonus = this.run.combo.bonus();
|
||||
if (bonus) {
|
||||
this.run.modifier.bonus = bonus;
|
||||
this.run.clockMs += bonus.seconds * 1000;
|
||||
this.run.clock.addSeconds(bonus.seconds);
|
||||
this.sound.bonus();
|
||||
}
|
||||
if (cur.isOver()) {
|
||||
|
@ -114,12 +112,12 @@ export default class StormCtrl {
|
|||
this.pushToHistory(false);
|
||||
this.run.errors++;
|
||||
this.run.combo.reset();
|
||||
this.run.clockMs -= config.clock.malus * 1000;
|
||||
this.run.clock.addSeconds(-config.clock.malus);
|
||||
this.run.modifier.malus = {
|
||||
seconds: config.clock.malus,
|
||||
at: getNow(),
|
||||
};
|
||||
if (!boundedClockMillis(this.run)) this.end();
|
||||
if (this.run.clock.flag()) this.end();
|
||||
else if (!this.incPuzzle()) this.end();
|
||||
}
|
||||
this.redraw();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Prop } from 'common';
|
||||
import { Puzzle } from 'puz/interfaces';
|
||||
import { PuzPrefs, Puzzle } from 'puz/interfaces';
|
||||
|
||||
export interface StormOpts {
|
||||
data: StormData;
|
||||
|
@ -7,14 +7,7 @@ export interface StormOpts {
|
|||
i18n: any;
|
||||
}
|
||||
|
||||
export interface StormPrefs {
|
||||
coords: 0 | 1 | 2;
|
||||
is3d: boolean;
|
||||
destination: boolean;
|
||||
rookCastle: boolean;
|
||||
moveEvent: number;
|
||||
highlight: boolean;
|
||||
}
|
||||
export interface StormPrefs extends PuzPrefs {}
|
||||
|
||||
export interface StormData {
|
||||
puzzles: Puzzle[];
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Chessground } from 'chessground';
|
||||
import { makeConfig as makeCgConfig } from 'puz/view/chessground';
|
||||
import config from 'puz/config';
|
||||
import renderClock from 'puz/view/clock';
|
||||
import renderEnd from './end';
|
||||
import StormCtrl from '../ctrl';
|
||||
import { getNow, onInsert } from 'puz/util';
|
||||
import { onInsert } from 'puz/util';
|
||||
import { playModifiers, renderCombo } from 'puz/view/util';
|
||||
import { h } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import { makeCgOpts } from 'puz/run';
|
||||
|
@ -16,25 +16,13 @@ export default function (ctrl: StormCtrl): VNode {
|
|||
return h(
|
||||
'div.storm.storm-app.storm--play',
|
||||
{
|
||||
class: playModifiers(ctrl),
|
||||
class: playModifiers(ctrl.run),
|
||||
},
|
||||
renderPlay(ctrl)
|
||||
);
|
||||
return h('main.storm.storm--end', renderEnd(ctrl));
|
||||
}
|
||||
|
||||
const playModifiers = (ctrl: StormCtrl) => {
|
||||
const now = getNow();
|
||||
const malus = ctrl.run.modifier.malus;
|
||||
const bonus = ctrl.run.modifier.bonus;
|
||||
return {
|
||||
'storm--mod-puzzle': ctrl.run.current.startAt > now - 90,
|
||||
'storm--mod-move': ctrl.run.modifier.moveAt > now - 90,
|
||||
'storm--mod-malus-slow': !!malus && malus.at > now - 950,
|
||||
'storm--mod-bonus-slow': !!bonus && bonus.at > now - 950,
|
||||
};
|
||||
};
|
||||
|
||||
const chessground = (ctrl: StormCtrl): VNode =>
|
||||
h('div.cg-wrap', {
|
||||
hook: {
|
||||
|
@ -48,23 +36,23 @@ const chessground = (ctrl: StormCtrl): VNode =>
|
|||
|
||||
const renderPlay = (ctrl: StormCtrl): VNode[] => [
|
||||
h('div.storm__board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
|
||||
h('div.storm__side', [
|
||||
h('div.storm__side.puz-side', [
|
||||
ctrl.run.startAt ? renderSolved(ctrl) : renderStart(ctrl),
|
||||
renderClock(ctrl.run, ctrl.endNow),
|
||||
h('div.storm__table', [renderControls(ctrl), renderCombo(ctrl)]),
|
||||
h('div.puz-side__table', [renderControls(ctrl), renderCombo(ctrl.run)]),
|
||||
]),
|
||||
];
|
||||
|
||||
const renderControls = (ctrl: StormCtrl): VNode =>
|
||||
h('div.storm__control', [
|
||||
h('a.storm__control__reload.button.button-empty', {
|
||||
h('div.puz-side__control', [
|
||||
h('a.puz-side__control__reload.button.button-empty', {
|
||||
attrs: {
|
||||
href: '/storm',
|
||||
'data-icon': 'B',
|
||||
title: ctrl.trans('newRun'),
|
||||
},
|
||||
}),
|
||||
h('a.storm__control__end.button.button-empty', {
|
||||
h('a.puz-side__control__end.button.button-empty', {
|
||||
attrs: {
|
||||
'data-icon': 'b',
|
||||
title: ctrl.trans('endRun'),
|
||||
|
@ -73,45 +61,13 @@ const renderControls = (ctrl: StormCtrl): VNode =>
|
|||
}),
|
||||
]);
|
||||
|
||||
const renderCombo = (ctrl: StormCtrl): VNode => {
|
||||
const level = ctrl.run.combo.level();
|
||||
return h('div.storm__combo', [
|
||||
h('div.storm__combo__counter', [
|
||||
h('span.storm__combo__counter__value', ctrl.run.combo),
|
||||
h('span.storm__combo__counter__combo', 'COMBO'),
|
||||
]),
|
||||
h('div.storm__combo__bars', [
|
||||
h('div.storm__combo__bar', [
|
||||
h('div.storm__combo__bar__in', {
|
||||
attrs: { style: `width:${ctrl.run.combo.percent()}%` },
|
||||
}),
|
||||
h('div.storm__combo__bar__in-full'),
|
||||
]),
|
||||
h(
|
||||
'div.storm__combo__levels',
|
||||
[0, 1, 2, 3].map(l =>
|
||||
h(
|
||||
'div.storm__combo__level',
|
||||
{
|
||||
class: {
|
||||
active: l < level,
|
||||
},
|
||||
},
|
||||
h('span', `${config.combo.levels[l + 1][1]}s`)
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
]);
|
||||
};
|
||||
|
||||
const renderSolved = (ctrl: StormCtrl): VNode =>
|
||||
h('div.storm__top.storm__solved', [h('div.storm__solved__text', ctrl.countWins())]);
|
||||
h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', ctrl.countWins())]);
|
||||
|
||||
const renderStart = (ctrl: StormCtrl) =>
|
||||
h(
|
||||
'div.storm__top.storm__start',
|
||||
h('div.storm__start__text', [h('strong', 'Puzzle Storm'), h('span', ctrl.trans('moveToStart'))])
|
||||
'div.puz-side__top.puz-side__start',
|
||||
h('div.puz-side__start__text', [h('strong', 'Puzzle Storm'), h('span', ctrl.trans('moveToStart'))])
|
||||
);
|
||||
|
||||
const renderReload = (msg: string) =>
|
||||
|
|
Loading…
Reference in New Issue