3173 lines
92 KiB
Plaintext
3173 lines
92 KiB
Plaintext
<script lang="ts">
|
|
import type { HtmlTag } from 'svelte/internal';
|
|
import type { GameConfig } from './shared';
|
|
|
|
import * as topicIcons from './topicicons.json'
|
|
import * as topicSpecialIcons f./topicspecialrom './specialtopiciconsIconss.json'
|
|
|
|
export let room: string
|
|
|
|
export let connection: 'waiting' | 'connecting' | 'connected'
|
|
|
|
export let game_config: GameConfig
|
|
// export let state // loading, waiting, playing, paused.
|
|
// export let start_time
|
|
// export let topic
|
|
// export let meditate
|
|
// export let players
|
|
// export let rounds
|
|
// export let seconds_per_bead
|
|
// export let paused_progress
|
|
|
|
export let _active_sessions: number
|
|
export let _magister: true | null
|
|
export let _clock_offset: number
|
|
|
|
// let game_completed = false // Derived from other properties
|
|
|
|
let round_audio: HTMLAudioElement
|
|
let complete_audio: Hlet topic_imgelem: HTMLElement
|
|
|
|
round_audio = new Audio()
|
|
round_audio.src = "/lo-metal-tone.mp3"
|
|
complete_audio <audio bin = neomplete_audiow Audio"/hi-metal-tone.mp3"()
|
|
cround_audio.src r ound_audiod:this={ro
|
|
// round_audio.preload = 'auto'pr// elround_audio
|
|
und_audio} src="/lo-metal-tone.mp3" preload="auto" autoplay><track kind="captions"></audio>
|
|
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
|
|
TMLAudtextioEleleement
|
|
let topic_img: HTMLElement
|
|
|
|
let state: GameConfig['state']
|
|
$: state = game_config.state
|
|
|
|
$: console.log('Game configuration changed', game_config)
|
|
|
|
// export let state
|
|
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty',
|
|
|
|
documentwindow.onclick = (
|
|
// ) if (!audio_works) )test_audio()=> {
|
|
console.log('onclick')"e. }
|
|
'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'At
|
|
// This ugly monstrosity brought to you by iOS Safari.
|
|
// TtThis seems to be the only way to bless the aud
|
|
// io objects to be able to play during the game. :/
|
|
tite// ntion', 'Art', 'Empat// hy', 'Eutopia'
|
|
const round_src = round_audio.src
|
|
const completeroundcroompleteundocld_src round_audio.src = complete_audio.src =complet '/silence.mp3'
|
|
= round_au
|
|
complete_audio.play()complete_audiodio.srcaround_audio], round_audioroun// 'Future', 'Game', 'Gift'round_src
|
|
complete_audio.src = complete_srccomplete_scomplete_audio// ,
|
|
'History',
|
|
round_audio round_audio round_audio
|
|
round_audio.src = old_srcold_rondound_src
|
|
.src = complete_srccomplete_scomplete_a
|
|
document.onclick = () => {
|
|
if (!audio_works) test_audio()
|
|
}udiocomplete_audio_audior.volume = 0.01muted = true
|
|
round_audio.play().then(
|
|
|
|
round_audio.src = old_src () => {
|
|
round_audio
|
|
round_audio.play().then(.muted = true
|
|
() => {false
|
|
audio_works = true
|
|
console.log('Audio works'
|
|
audio_works = true)
|
|
},
|
|
() => {
|
|
|
|
console.log('Audio works') ro
|
|
},und_audimuted
|
|
() => {false = trueo.volume = 0.01round_audio
|
|
round_audioa.play().then( 'Cosmos', 'Time', 'Lif
|
|
|
|
+let audio_works = true
|
|
+
|
|
+function test_audio() {
|
|
+ let a = new Audio()
|
|
+ a.volume = // 0.1
|
|
+ a.src = '/silence.mp3'
|
|
+ a.play().then(
|
|
+ () => {
|
|
+ audio_works = true
|
|
+ console.log('Audio works')
|
|
+ },
|
|
+ () => {
|
|
+ audio_works = false
|
|
+ console.log('Audio does not wo// rk')
|
|
+ }
|
|
+ )
|
|
+}
|
|
+functio// n fix_afixxxudio(e) {
|
|
+
|
|
+//// console.log(// 'xx')
|
|
+ testconst fixed_rand = Math.random()randomrand_audio()
|
|
+}
|
|
+setTimeout(test_audifixed_rando, 0)
|
|
+
|
|
|
|
e', '
|
|
const randInt
|
|
const n = randInt = (n: number) => Math.floor(floorMath.random())function; * nrand<T>omMat<T>h: {return arr
|
|
cTamonst randItem = < }randInt(arr.length)]
|
|
T>(arr: T[]) => t[t.: (Addiction', 'Paradox', 'Shadow', 'Soimg && topistarting...willc_text mciety'
|
|
]
|
|
|
|
ele$: {
|
|
|
|
const topic = game_config.topic.to const svgContent = topicIcons[topic as keyof typeof topicIcons]
|
|
LocaleLowerCase()if (topic_img) {
|
|
const svgContetopicnt = topicIcons
|
|
[game_config.topi{
|
|
topic_img.innerHTML = svgContent
|
|
topic_text.innerText = ''
|
|
} else if (textCrandItem(ontent) {
|
|
) topic_img.innerHTML = ''
|
|
topic_text.innerText = textContent
|
|
} else {
|
|
c.timgoLo
|
|
|
|
caleLowerCase''()ele as k
|
|
topic_text.innerText = '
|
|
} else {
|
|
topic_text.innerText = game_config.topic
|
|
} } '
|
|
} else if (textContent) {
|
|
topic_text.innerText = textContent } eyof ty pieof top
|
|
const svgContent = topicIcons[topic as
|
|
else topic_text.innerTextinner = {game_config.topic} keyof typeof topicIcons]icIcons]
|
|
if (svgContent) topic_img.innerH
|
|
else
|
|
if (svgContent) topic_img.innerHTML = svgContent{topic as ke
|
|
else {yof typeof topicSpecialt
|
|
text if (textContent) topic_eleimg.innerText = textContenttextConteinnertopic_imtextConopicSptopicIcons]
|
|
const textContent = topicSpecial[topicSp }
|
|
|
|
TML = svgContent
|
|
}
|
|
}
|
|
|
|
// Could make configurable. Eh.
|
|
const MEDITATION_SECONDS = 60
|
|
|
|
interface GameStage {
|
|
label: string,
|
|
type: 'waiting' | 'bead' | 'breath' | 'meditate' | 'contemplation' | 'complete',
|
|
duration: number,
|
|
no_sound?: true,
|
|
r?: number, p?: number,
|
|
id?: string
|
|
}
|
|
|
|
let game_stages: GameStage[] = []
|
|
$: {
|
|
game_stages = [{
|
|
label: `${game_config.meditate ? 'Meditation' : 'Game'} is about to start`,
|
|
type: 'waiting',
|
|
duration: 3,
|
|
no_sound: true
|
|
}]
|
|
if (game_config.meditate) game_stages.push({
|
|
label: 'Meditate',
|
|
type: 'meditate',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < game_config.rounds; r++) {
|
|
for (let p = 0; p < game_config.players; p++) {
|
|
if (game_config.seconds_between_bead && (r > 0 || p > 0)) game_stages.push({
|
|
label: 'Breathe',
|
|
duration: game_config.seconds_between_bead,
|
|
type: 'breath',
|
|
id: `b ${r} ${p}`
|
|
})
|
|
|
|
game_stages.push({
|
|
label: 'x ',
|
|
// label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
|
|
label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
|
|
duration: game_config.seconds_per_bead,
|
|
type: 'bead', r, p,
|
|
id: `s ${r} ${p}`
|
|
})
|
|
}
|
|
}
|
|
|
|
if (game_config.contemplation) game_stages.push({
|
|
label: "Contemplate the game's passing",
|
|
type: 'contemplation',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
|
|
|
|
console.log('game stages', game_stages, game_config.seconds_between_bead)
|
|
}
|
|
|
|
let total_game_length: number
|
|
$: total_game_length = game_stages.reduce((x, s) => x + s.duration, 0)
|
|
|
|
// Used for the overall game progress indicator.
|
|
let inner_game_stages: GameStage[]
|
|
$: inner_game_stages = game_stages.filter(s => s.type === 'breath' || s.type === 'bead')
|
|
let inner_game_length: number
|
|
$: inner_game_length = inner_game_stages.reduce((x, s) => x + s.duration, 0)
|
|
|
|
// TODO: The protocol for these update methods doesn't use game_state properly.
|
|
const update_state = async (patch: Record<string, string | number | boolean | null>) => {
|
|
await fetch(`${room}/configure`, {
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(patch)
|
|
})
|
|
}
|
|
|
|
const upd = (k: string, v: string | number | boolean | null) => () => update_state({[k]: v})
|
|
|
|
const config = (k: string): svelte.JSX.FormEventHandler<HTMLInputElement> => (e) => {
|
|
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
|
|
const target = e.target as HTMLInputElement
|
|
const raw_value = target.value
|
|
const value = target.type === 'number' ? ~~raw_value
|
|
: target.type === 'checkbox' ? target.checked
|
|
: raw_value
|
|
update_state({[k]: value})
|
|
}
|
|
|
|
const roundish = (x: number) => Math.round(x * 10) / 10
|
|
|
|
|
|
const waiting_stage: GameStage = { label: 'Waiting for game to start', type: 'waiting', duration: Infinity }
|
|
const complete_stage: GameStage = { label: 'Game complete', type: 'complete', duration: Infinity }
|
|
const get_current_stage = (offset_ms: number): {stage: GameStage, stage_idx: number, offset_sec: number} => {
|
|
if (state === 'waiting') return {stage: waiting_stage, stage_idx: -1, offset_sec: 0}
|
|
|
|
let offset_sec = Math.round(offset_ms / 1000)
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let stage = game_stages[s]
|
|
if (stage.duration > offset_sec) {
|
|
return {stage, stage_idx: s, offset_sec}
|
|
}
|
|
offset_sec -= stage.duration
|
|
}
|
|
return {
|
|
stage: complete_stage, stage_idx: game_stages.length, offset_sec
|
|
}
|
|
}
|
|
|
|
// Urgh kinda ugly storing state for both the index and stage itself. Better to
|
|
// have one derive the other.
|
|
let current_stage: GameStage | null = null, current_stage_idx: number = -1, offset_sec: number
|
|
$: console.log('current stage', current_stage)
|
|
// $: console.log('idx', current_stage_idx)
|
|
|
|
const tick = (play_audio: boolean) => {
|
|
console.log('tick')
|
|
// console.log('state', state, 'completed', state && state.complete)
|
|
|
|
const time = state === 'playing' ? Date.now() + _clock_offset - game_config.start_time
|
|
: state === 'paused' ? game_config.paused_progress!
|
|
: 0
|
|
const {stage: new_stage, stage_idx: new_stage_idx, offset_sec: new_offs} = get_current_stage(time)
|
|
// state_label = state.label
|
|
|
|
offset_sec = new_offs
|
|
if (new_stage !== current_stage) {
|
|
console.log('state changed', new_stage.label, new_stage.type === 'complete')
|
|
|
|
// This happens sometimes with other kinds of configuration changes -
|
|
// eg if a user enters or leaves the room, or the room is reconfigured.
|
|
// Only make a sound if the *stage* changes.
|
|
let changed = current_stage == null || (new_stage.id ?? new_stage.type) !== (current_stage.id ?? current_stage.type)
|
|
// console.log(new_stage, current_stage, changed)
|
|
|
|
current_stage = new_stage
|
|
current_stage_idx = new_stage_idx
|
|
// completed = new_game_state.complete
|
|
// if (!state.complete) round_audio.play()
|
|
|
|
if (play_audio && !new_stage.no_sound && changed) {
|
|
if (current_stage.type === 'complete' || current_stage.type === 'contemplation') complete_audio.play()
|
|
else round_audio.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
let timer: number | null | any // Timeout?
|
|
$: {
|
|
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
|
|
let completed = current_stage ? current_stage.type === 'complete' : false
|
|
// console.log('xx', state, timer, completed, current_stage)
|
|
|
|
// if (state !== 'loading') tick(false)
|
|
|
|
if (state === 'playing' && timer == null && !completed) {
|
|
// setTimeout needed to get around some weird race condition.
|
|
// There's probably better ways to structure this :/
|
|
setTimeout(() => tick(false))
|
|
timer = setInterval(() => {
|
|
tick(true)
|
|
}, 1000)
|
|
} else if ((completed || state !== 'playing') && timer != null) {
|
|
console.log('cancelled interval timer')
|
|
clearInterval(timer)
|
|
timer = null
|
|
} else if (state === 'waiting' || state === 'paused') {
|
|
setTimeout(() => tick(false))
|
|
}
|
|
}
|
|
|
|
let game_completed: boolean
|
|
$: {
|
|
// console.log('updating game_completed', current_stage)
|
|
game_completed = (state !== 'playing' || current_stage == null) ? false
|
|
: (current_stage.type === 'complete')
|
|
}
|
|
|
|
let internal_state: GameConfig['state'] | 'completed'
|
|
$: internal_state = game_completed ? 'completed' : state
|
|
|
|
let bar_width = 0
|
|
$: bar_width = current_stage == null ? 0
|
|
: state === 'waiting' ? 0
|
|
: current_stage.type === 'complete' ? 100
|
|
: 100 * offset_sec / current_stage.duration
|
|
|
|
let stage_label: string
|
|
$: stage_label = inte
|
|
const rount
|
|
rnal_state === 'waiting' ? 'Waiting for game to start'
|
|
: current_stage == null ? 'unknown' : current_stage.label
|
|
|
|
|
|
const progress_class = (stage_idx: number, baseline_idx: n<!-- umber): 's-done' | 's-active' | 's-waiting' => {
|
|
if (current_stage == null || base autoplaypplline_idx < 0) return 's-waiting'
|
|
|
|
return stage_idx < baseline_idx ? 's-done'
|
|
: stage_idx === baseline_idx ? 's-active'
|
|
--> : 's-waiting'
|
|
}
|
|
|
|
// const order = ['meditate', 'bead', 'complete']
|
|
// const cl
|
|
{#if !audio_works} abuttonutss_fo on:click={fix_audiofixau}onclickr = (x: number): string => x < 0 ? 'dobuttonne't
|
|
/u/ :
|
|
{/if} x > 0 ? 'waiting'
|
|
// : 'active'
|
|
|
|
// const progress_class = (stage: GameStage, type: string, r?: number, p?: number): string => {
|
|
// if (stage == null) return ''
|
|
|
|
// const current_o = order.indexOf(stage.type)
|
|
// const element_o = order.indexOf(type)
|
|
|
|
// // const o_diff = element_o - current_o
|
|
// return type === 'bead' && stage.type === 'bead'
|
|
// ? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
|
|
// : class_for(element_o - current_o)
|
|
// }
|
|
|
|
// This will get more complex in time. For now, pause the game to fiddle.
|
|
$: settings_d
|
|
<div id='fixaudio'>Audio muted. Click to unmuteun</div>
|
|
isabled = state === 'playing'
|
|
|
|
let config_open = false
|
|
|
|
$: if (_magister === true) config_open = true
|
|
|
|
// The first user has the config open by default.
|
|
// $: if (_active_sessions === 1) config_open = true
|
|
|
|
// The magister box is fully visible once there's a critical mass of players in the room
|
|
$: magister_opaque = _magister === true || _active_sessions >= 6
|
|
|
|
</script>
|
|
|
|
<svelte:head>
|
|
{#if _magister}
|
|
<style>
|
|
body {
|
|
background-color: var(--bg-highlight);
|
|
}
|
|
</style>
|
|
{/if}
|
|
</svelte:head>
|
|
|
|
<!-- <main class:magister={_magister}> -->
|
|
<main>
|
|
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" pr <div id='topicimg' bind:this={topic_img}></div>
|
|
eload="auto"><tratextck bind:this={topic_itextmge></div>lem}k
|
|
<div id='topic
|
|
<div id='topicimg' bind:this={topic_img}></div>img"topic'
|
|
ineled="captions"><
|
|
/audio>
|
|
|
|
{#if internal_state === 'loading'}
|
|
<h1>Loading game state</h1>
|
|
{:else}
|
|
<!-- <h1>Glass Bead Game Timer</h1> -->
|
|
<!-- <h1>{topic} id='stagelabel'</h1> -->
|
|
|
|
<div id='topicimg' bind:this={topic_img}>{game_config.topic}</div>
|
|
|
|
<h1>{stage_label}</h1>
|
|
<div id='progresscontainer'>
|
|
<div id='progress_time'>{((internal_state === 'playing' || internal_state === 'paused') && current_stage) ? current_stage.duration - offset_sec : ''}</div>
|
|
<div id='progress' style='width: {bar_width}%'></div>
|
|
</div>
|
|
|
|
<div id='gameprogress'>
|
|
{#each game_stages as s, i}
|
|
{#if s.type === 'bead' || s.type === 'breath'}
|
|
<span class={'prog-' + s.type + ' ' + progress_class(i, current_stage_idx)} style='width: {100 * s.duration / inner_game_length}%'></span>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
{#if (_magister == null || _magister == true)}
|
|
{#if internal_state == 'waiting'}
|
|
<button on:click={upd('state', 'playing')}>Start</button>
|
|
{:else if internal_state == 'playing'}
|
|
<button on:click={upd('state', 'paused')}>Pause</button>
|
|
{:else if internal_state == 'paused'}
|
|
<button on:click={upd('state', 'playing')}>Resume</button>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div style='height: 400px;'></div>
|
|
|
|
<details>
|
|
<!-- I'm not ready to delete these UI elements but we might not use them -->
|
|
<summary>Info</summary>
|
|
|
|
<h1>{game_config.topic}</h1>
|
|
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
|
|
|
|
<div>
|
|
{state === 'waiting' ? 'Waiting for the game to start'
|
|
: state === 'paused' ? 'GAME PAUSED'
|
|
: state === 'playing' ? 'Game in progress'
|
|
: ''}
|
|
</div>
|
|
{#if connection !== 'connected'}
|
|
<div>DISCONNECTED FROM GAME SERVER</div>
|
|
{:else}
|
|
{#if _active_sessions == 1}
|
|
<div>You are alone in the room</div>
|
|
{:else}
|
|
<div>{_active_sessions} players are in this room</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div id='rounds'>
|
|
<h2>Game</h2>
|
|
{#if game_config.meditate}
|
|
<div>
|
|
<!-- <span class={progress_class(current_stage, 'meditate')}>Meditati<!-- on (1 min)</span> -->
|
|
<span>Meditation (1 min)</span>
|
|
</div>
|
|
{/if}
|
|
{#each Array(Math.max(game_config.rounds, 0)) as _, r}
|
|
<div>Round {r+1}:
|
|
{#each Array(Math.max(game_config.players, 0)) as _, p}
|
|
<!-- <span class={'bead ' + progress_class(current_stage, 'bead', r, -->p)}>{p+1} </span> -->
|
|
<span>{p+1} </span>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
</details>
|
|
|
|
{#if _magister == null || _magister == true}
|
|
<details class='config' bind:open={config_open}>
|
|
<summary>Game controls</summary>
|
|
|
|
<p>
|
|
{#if _magister == null}
|
|
This will effect all players. Will you borrow power? Will you steal it?
|
|
{:else}
|
|
You are master of the games. These controls are yours alone.
|
|
{/if}
|
|
</p>
|
|
|
|
{#if internal_state == 'waiting'}
|
|
<button on:click={upd('state', 'playing')}>Start</button>
|
|
{:else if internal_state == 'playing'}
|
|
<button on:click={upd('state', 'paused')}>Pause</button>
|
|
{:else if i/* nternalabsoluteabsbottomolu_state == */ 'paused'}
|
|
<button on:click={upd('state', 'playing')}>Resume</button>
|
|
{/if}
|
|
|
|
|
|
font-size: 13252060%; {#if internal_state == 'paused' || internal_:not(:empty)state == 'com3p.5l 2em 0eted' }
|
|
<button on:click={upd('state', 'waiting')}>Restart game</button>
|
|
{/if}
|
|
|
|
<label>
|
|
/* <span/* >Topic</span>
|
|
*/ */ <input disab:e
|
|
nmptyled={2settings_disabled} type=min-/* 'text' value={ */game_config.topic} on:input={config('topic')} list='archetopics' >
|
|
<datalist id='archetopics'>
|
|
{#each ARCHETOPICS as topic}
|
|
<option value={topic}>
|
|
{/each}
|
|
</datalist>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Pre-game meditation</span>
|
|
<input disabled={settings_disabled} type='checkbox' checked={game_conf
|
|
z-index: 1;ig.meditate} on:input={config(
|
|
color: var(----fg-colo;r)/* /* fg'meditate')} >
|
|
</l */2abel>
|
|
|
|
*/ <label>
|
|
<s0.5pan1>Post game contemplation</span>
|
|
<input disabled={settings_disabled} type='check2bo 0x' checked={game_config.contemplation} on:input={config('contemplation')} >
|
|
</label>
|
|
|
|
<label>
|
|
<span>Number of players</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.players} on:input={config('players')} min=1 max=12 >
|
|
</label>
|
|
|
|
<label>
|
|
<span>Number of rounds</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.rounds} on:input={config('rounds')} min=
|
|
#fixaudio {
|
|
border: 1px solid white;
|
|
background-color: var(----bg-highligh;
|
|
position: fixed;
|
|
top: 1px;
|
|
width: 300px;
|
|
paddingma1rgin: 3
|
|
left: 50%;
|
|
/* transform: translateX(-50%); */tran1em 2em;t)bg-0-bg-coewhitedi }
|
|
|
|
1 max=20>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Seconds per bead</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={g45ame
|
|
font-size: 1340%;font-sizetext-size: font_config.seconds_per_bead} on:input={config('seconds_per_bead')}>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Seconds between beads</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_between_bead} on:input={config('seconds_between_bead')}>
|
|
</label>
|
|
|
|
<div style='margin-top: 1em;'>
|
|
(Total game length: {roundish(
|
|
game_stages.reduce((x#topicimg {
|
|
width: 300px;
|
|
display: inline-block;
|
|
}
|
|
, s) =textimg>padding: 2em;
|
|
font-style: italic;it x + s.duration, 0) / 60
|
|
)} minutes)
|
|
</div>
|
|
|
|
<div id='magister_box' class:magister_opaque>
|
|
{#if _magister == null}
|
|
<button on:click={upd('_magister', true)}>Assume the mantle of Magister Ludi</button>
|
|
<p><i>Advanced </style>- for large games</i></p>
|
|
<p>When present, the Magister Ludi (master of the games) has exclusive control of the game.</p>
|
|
{:else if _magister == true}
|
|
<button on:click={upd('_magister', null)}>Abdicate Magister Ludi status</button>
|
|
<p>You are the master of the games. You have exclusive control over playing, pausing and configuring this game.</p>
|
|
<p>Do not close this browser window or you will be dethroned.</p>
|
|
{/if}
|
|
</div>
|
|
</details>
|
|
{:else}
|
|
<p class='config'>Magister Ludi is managing this game.</p>
|
|
{/i > :global(svg) > > svgf}
|
|
{/if}
|
|
</main>
|
|
|
|
<style>
|
|
|
|
main {
|
|
/* margin-bottom: 3em; */
|
|
text-align: center;
|
|
}
|
|
|
|
#topicimg {
|
|
width: 300px margin-top: 1em;;
|
|
display: inline margin-t1.5.1op: 1em;
|
|
-
|
|
|
|
#stagelabel {
|
|
min-heig2ht: 1em;
|
|
} block;
|
|
}
|
|
|
|
/* .magister {
|
|
background-color: var(--bg-highlight);
|
|
} */
|
|
|
|
h1 {
|
|
margin-top: 1em;
|
|
}
|
|
|
|
#progresscontainer {
|
|
/* width: calc(100% - 50px); */
|
|
position: relative;
|
|
margin: 10px 25px;
|
|
height: 5em;
|
|
border: 2px solid var(--fg-color);
|
|
/* margin-bottom: 0; */
|
|
}
|
|
|
|
#progress_time {
|
|
position: absolute;
|
|
/* color: red; */
|
|
/* font-size: var(--bg-color); */
|
|
color: white;
|
|
/* color: white; */
|
|
font-size: 54px;
|
|
margin-left: 5px;
|
|
mix-blend-mode: difference;
|
|
}
|
|
|
|
#progress {
|
|
background-color: var(--fg-color);
|
|
/* width: 50%; */
|
|
height: 100%;
|
|
/* transition: width 1s linear; */
|
|
}
|
|
|
|
#gameprogress {
|
|
/* width: 300px; */
|
|
margin: 25px;
|
|
height: 15px;
|
|
/* background-color: blue; */
|
|
margin-top: 0;
|
|
}
|
|
|
|
#gameprogress > span {
|
|
display: inline-block;
|
|
/* height: 10px; */
|
|
background-color: var(--fg-color);
|
|
/* border-left: 1px solid var(--bg-color);
|
|
border-right: 1px solid var(--bg-color); */
|
|
}
|
|
|
|
/* .prog-waiting {
|
|
height: 100%;
|
|
} */
|
|
/* .prog-meditate, .prog-contemplation {
|
|
height: 50%;
|
|
} */
|
|
.prog-bead {
|
|
height: 100%;
|
|
}
|
|
/* .prog-breath {
|
|
} */
|
|
|
|
.s-done {
|
|
opacity: 20%;
|
|
}
|
|
/* .s-active {
|
|
|
|
} */
|
|
.s-waiting {
|
|
opacity: 50%;
|
|
type: 'waiting' | 'bead' | 'meditate' | 'contemplation' | 'complete',
|
|
duration: number,
|
|
no_sound?: true,
|
|
r?: number, p?: number,
|
|
id?: string
|
|
}
|
|
|
|
let game_stages: GameStage[] = []
|
|
$: {
|
|
game_stages = [{
|
|
label: `${game_config.meditate ? 'Meditation' : 'Game'} is about to start`,
|
|
type: 'waiting',
|
|
duration: 3,
|
|
no_sound: true
|
|
}]
|
|
if (game_config.meditate) game_stages.push({
|
|
label: 'Meditate',
|
|
type: 'meditate',
|
|
duration: ME
|
|
min-heightwi1.25em;dth: DITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < game_config.rounds; r++) {
|
|
for (let p = 0; p < game_config.players; p++) {
|
|
if (game_config.seconds_between_bead && (r > 0 || p > 0)) game_stages.push({
|
|
label: 'Breathe',
|
|
duration: game_config.seconds_between_bead,
|
|
type: 'bead'
|
|
})
|
|
|
|
game_stages.push({
|
|
label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
|
|
label: ''game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
|
|
// label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
|
|
duration: game_config.seconds_per_bead,
|
|
type: 'bead', r, p,
|
|
id: `s ${r} ${p}`
|
|
})
|
|
}
|
|
}
|
|
|
|
if (game_config.contemplation) game_stages.push({
|
|
label: "Contemplate the game's passing",
|
|
type: 'contemplation',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
|
|
console.log('game stages', game_stages, game_config.seconds_between_bead)
|
|
}
|
|
|
|
// TODO: The protocol for these update methods doesn't use game_state properly.
|
|
const update_state = async (patch: Record<string, string | number | boolean | null>) => {
|
|
await fetch(`${room}/configure`, {
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(patch)
|
|
})
|
|
}
|
|
|
|
const upd = (k: string, v: string | number | boolean | null) => () => update_state({[k]: v})
|
|
|
|
const config = (k: string): svelte.JSX.FormEventHandler<HTMLInputElement> => (e) => {
|
|
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
|
|
const target = e.target as HTMLInputElement
|
|
const raw_value = target.value
|
|
const value = target.type === 'number' ? ~~raw_value
|
|
: target.type === 'checkbox' ? target.checked
|
|
: raw_value
|
|
update_state({[k]: value})
|
|
}
|
|
|
|
const roundish = (x: number) => Math.round(x * 10) / 10
|
|
|
|
|
|
const waiting_stage: GameStage = { label: 'Waiting for game to start', type: 'waiting', duration: Infinity }
|
|
const complete_stage: GameStage = { label: 'Game complete', type: 'complete', duration: Infinity }
|
|
const get_current_stage = (offset_ms: number): {stage: GameStage, offset_sec: number} => {
|
|
if (state === 'waiting') return {stage: waiting_stage, offset_sec: 0}
|
|
|
|
let offset_sec = Math.round(offset_ms / 1000)
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let stage = game_stages[s]
|
|
if (stage.duration > offset_sec) {
|
|
return {stage, offset_sec}
|
|
}
|
|
offset_sec -= stage.duration
|
|
}
|
|
return {
|
|
stage: complete_stage, offset_sec
|
|
}
|
|
}
|
|
|
|
let current_stage: GameStage | null = null, offset_sec: number
|
|
$: console.log('current stage', current_stage)
|
|
|
|
|
|
const tick = (play_audio: boolean) => {
|
|
console.log('tick')
|
|
// console.log('state', state, 'completed', state && state.complete)
|
|
|
|
const time = state === 'playing' ? Date.now() + _clock_offset - game_config.start_time
|
|
: state === 'paused' ? game_config.paused_progress!
|
|
: 0
|
|
const {stage: new_stage, offset_sec: new_offs} = get_current_stage(time)
|
|
// state_label = state.label
|
|
|
|
offset_sec = new_offs
|
|
if (new_stage !== current_stage) {
|
|
console.log('state changed', new_stage.label, new_stage.type === 'complete')
|
|
|
|
// Hotfix
|
|
let changed = new_stage == null || current_stage == null || new_stage.id == null || current_stage.id == null || current_stage.id !== new_stage.id
|
|
console.log(new_stage, current_stage, changed)
|
|
|
|
current_stage = new_stage
|
|
// completed = new_game_state.complete
|
|
// if (!state.complete) round_audio.play()
|
|
|
|
// Hotfix
|
|
let changed = new_stage == null || current_stage == null || new_stage.id != null || = current_stage == null || current_stage.id == new_stage.id
|
|
console.log(new_stage, current_stage, changed) null !==\\ .idcurrcurrent_sta
|
|
if (play_audio && !new && changede_stage.no_sound && ) {
|
|
if (current_stage.type === 'complete' || current_stage.type === 'contemplation') complete_audio.play()
|
|
else round_audio.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
let timer: number | null | any // Timeout?
|
|
$: {
|
|
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
|
|
let completed = current_stage ? current_stage.type === 'complete' : false
|
|
// console.log('xx', state, timer, completed, current_stage)
|
|
|
|
// if (state !== 'loading') tick(false)
|
|
|
|
if (state === 'playing' && timer == null && !completed) {
|
|
// setTimeout needed to get around some weird race condition.
|
|
// There's probably better ways to structure this :/
|
|
setTimeout(() => tick(false))
|
|
timer = setInterval(() => {
|
|
tick(true)
|
|
}, 1000)
|
|
} else if ((completed || state !== 'playing') && timer != null) {
|
|
console.log('cancelled interval timer')
|
|
clearInterval(timer)
|
|
timer = null
|
|
} else if (state === 'waiting' || state === 'paused') {
|
|
setTimeout(() => tick(false))
|
|
}
|
|
}
|
|
|
|
let game_completed: boolean
|
|
$: {
|
|
// console.log('updating game_completed', current_stage)
|
|
game_completed = (state !== 'playing' || current_stage == null) ? false
|
|
: (current_stage.type === 'complete')
|
|
}
|
|
|
|
let internal_state: GameConfig['state'] | 'completed'
|
|
$: internal_state = game_completed ? 'completed' : state
|
|
|
|
let bar_width = 0
|
|
$: bar_width = current_stage == null ? 0
|
|
: state === 'waiting' ? 0
|
|
: current_stage.type === 'complete' ? 100
|
|
: 100 * offset_sec / current_stage.duration
|
|
|
|
let stage_label: string
|
|
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
|
|
: current_stage == null ? 'unknown' : current_stage.label
|
|
|
|
// const order = ['meditate', 'bead', 'complete']
|
|
// const class_for = (x: number): string => x < 0 ? 'done'
|
|
// : x > 0 ? 'waiting'
|
|
// : 'active'
|
|
|
|
// const progress_class = (stage: GameStage, type: string, r?: number, p?: number): string => {
|
|
// if (stage == null) return ''
|
|
|
|
// const current_o = order.indexOf(stage.type)
|
|
// const element_o = order.indexOf(type)
|
|
|
|
// // const o_diff = element_o - current_o
|
|
// return type === 'bead' && stage.type === 'bead'
|
|
// ? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
|
|
// : class_for(element_o - current_o)
|
|
// }
|
|
|
|
// This will get more complex in time. For now, pause the game to fiddle.
|
|
$: settings_disabled = state === 'playing'
|
|
|
|
let config_open = false
|
|
|
|
$: if (_magister === true) config_open = true
|
|
|
|
// The first user has the config open by default.
|
|
// $: if (_active_sessions === 1) config_open = true
|
|
|
|
// The magister box is fully visible once there's a critical mass of players in the room
|
|
$: magister_opaque = _magister === true || _active_sessions >= 6
|
|
|
|
</script>
|
|
|
|
<svelte:head>
|
|
{#if _magister}
|
|
<style>
|
|
body {
|
|
background-color: var(--bg-highlight);
|
|
}
|
|
</style>
|
|
{/if}
|
|
</svelte:head>
|
|
|
|
<!-- <main class:magister={_magister}> -->
|
|
<main>
|
|
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
|
|
{#if internal_state === 'loading'}
|
|
<h1>Loading game state</h1>
|
|
{:else}
|
|
<!-- <h1>Glass Bead Game Timer</h1> -->
|
|
<!-- <h1>{topic}</h1> -->
|
|
|
|
<div id='topicimg' bind:this={topic_img}>{game_config.topic}</div>
|
|
|
|
<h1>{stage_label}</h1>
|
|
<div id='progresscontainer'>
|
|
<div id='progress_time'>{((internal_state === 'playing' || internal_state === 'paused') && current_stage) ? current_stage.duration - offset_sec : ''}</div>
|
|
<div id='progress' style='width: {bar_width}%'></div>
|
|
</div>
|
|
|
|
{#if (_magister == null || _magister == true)}
|
|
{#if internal_state == 'waiting'}
|
|
<button on:click={upd('state', 'playing')}>Start</button>
|
|
{:else if internal_state == 'playing'}
|
|
<button on:click={upd('state', 'paused')}>Pause</button>
|
|
{:else if internal_state == 'paused'}
|
|
<button on:click={upd('state', 'playing')}>Resume</button>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div style='height: 400px;'></div>
|
|
|
|
<details>
|
|
<!-- I'm not ready to delete these UI elements but we might not use them -->
|
|
<summary>Info</summary>
|
|
|
|
<h1>{game_config.topic}</h1>
|
|
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
|
|
|
|
<div>
|
|
{state === 'waiting' ? 'Waiting for the game to start'
|
|
: state === 'paused' ? 'GAME PAUSED'
|
|
: state === 'playing' ? 'Game in progress'
|
|
: ''}
|
|
</div>
|
|
{#if connection !== 'connected'}
|
|
<div>DISCONNECTED FROM GAME SERVER</div>
|
|
{:else}
|
|
{#if _active_sessions == 1}
|
|
<div>You are alone in the room</div>
|
|
{:else}
|
|
<div>{_active_sessions} players are in this room</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div id='rounds'>
|
|
<h2>Game</h2>
|
|
{#if game_config.meditate}
|
|
<div>
|
|
<!-- <span class={progress_class(current_stage, 'meditate')}>Meditation (1 min)</span> -->
|
|
<span>Meditation (1 min)</span>
|
|
</div>
|
|
{/if}
|
|
{#each Array(Math.max(game_config.rounds, 0)) as _, r}
|
|
<div>Round {r+1}:
|
|
{#each Array(Math.max(game_config.players, 0)) as _, p}
|
|
<!-- <span class={'bead ' + progress_class(current_stage, 'bead', r, p)}>{p+1} </span> -->
|
|
<span>{p+1} </span>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
</details>
|
|
|
|
{#if _magister == null || _magister == true}
|
|
<details class='config' bind:open={config_open}>
|
|
<summary>Game controls</summary>
|
|
|
|
<p>
|
|
{#if _magister == null}
|
|
This will effect all players. Will you borrow power? Will you steal it?
|
|
{:else}
|
|
You are master of the games. These controls are yours alone.
|
|
{/if}
|
|
</p>
|
|
|
|
{#if internal_state == 'waiting'}
|
|
<button on:click={upd('state', 'playing')}>Start</button>
|
|
{:else if internal_state == 'playing'}
|
|
<button on:click={upd('state', 'paused')}>Pause</button>
|
|
{:else if internal_state == 'paused'}
|
|
<button on:click={upd('state', 'playing')}>Resume</button>
|
|
{/if}
|
|
|
|
{#if internal_state == 'paused' || internal_state == 'completed' }
|
|
<button on:click={upd('state', 'waiting')}>Restart game</button>
|
|
{/if}
|
|
|
|
<label>
|
|
<span>Topic</span>
|
|
<input disabled={settings_disabled} type='text' value={game_config.topic} on:input={config('topic')} list='archetopics' >
|
|
<datalist id='archetopics'>
|
|
{#each ARCHETOPICS as topic}
|
|
<option value={topic}>
|
|
{/each}
|
|
</datalist>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Pre-game meditation</span>
|
|
<input disabled={settings_disabled} type='checkbox' checked={game_config.meditate} on:input={config('meditate')} >
|
|
</label>
|
|
|
|
<label>
|
|
<span>Post game contemplation</span>
|
|
<input disabled={settings_disabled} type='checkbox' checked={game_config.contemplation} on:input={config('contemplation')} >
|
|
</label>
|
|
|
|
<label>
|
|
<span>Number of players</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.players} on:input={config('players')} min=1 max=12 >
|
|
</label>
|
|
|
|
<label>
|
|
<span>Number of rounds</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.rounds} on:input={config('rounds')} min=1 max=20>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Seconds per bead</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_per_bead} on:input={config('seconds_per_bead')}>
|
|
</label>
|
|
|
|
<label>
|
|
<span>Seconds between beads</span>
|
|
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_between_bead} on:input={config('seconds_between_bead')}>
|
|
</label>
|
|
|
|
<div style='margin-top: 1em;'>
|
|
(Total game length: {roundish(
|
|
game_stages.reduce((x, s) => x + s.duration, 0) / 60
|
|
)} minutes)
|
|
</div>
|
|
|
|
<div id='magister_box' class:magister_opaque>
|
|
{#if _magister == null}
|
|
<button on:click={upd('_magister', true)}>Assume the mantle of Magister Ludi</button>
|
|
<p><i>Advanced - for large games</i></p>
|
|
<p>When present, the Magister Ludi (master of the games) has exclusive control of the game.</p>
|
|
{:else if _magister == true}
|
|
<button on:click={upd('_magister', null)}>Abdicate Magister Ludi status</button>
|
|
<p>You are the master of the games. You have exclusive control over playing, pausing and configuring this game.</p>
|
|
<p>Do not close this browser window or you will be dethroned.</p>
|
|
{/if}
|
|
</div>
|
|
</details>
|
|
{:else}
|
|
<p class='config'>Magister Ludi is managing this game.</p>
|
|
{/if}
|
|
{/if}
|
|
</main>
|
|
|
|
<style>
|
|
|
|
main {
|
|
/* margin-bottom: 3em; */
|
|
text-align: center;
|
|
}
|
|
|
|
#topicimg {
|
|
width: 300px;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* .magister {
|
|
background-color: var(--bg-highlight);
|
|
} */
|
|
|
|
h1 {
|
|
margin-top: 1em;
|
|
}
|
|
|
|
#progresscontainer {
|
|
/* width: calc(100% - 50px); */
|
|
position: relative;
|
|
margin: 25px;
|
|
height: 5em;
|
|
border: 2px solid var(--fg-color);
|
|
}
|
|
|
|
#progress_time {
|
|
position: absolute;
|
|
/* color: red; */
|
|
/* font-size: var(--bg-color); */
|
|
color: white;
|
|
/* color: white; */
|
|
font-size: 54px;
|
|
margin-left: 5px;
|
|
mix-blend-mode: difference;
|
|
}
|
|
|
|
#progress {
|
|
background-color: var(--fg-color);
|
|
/* width: 50%; */
|
|
height: 100%;
|
|
/* transition: width 1s linear; */
|
|
}
|
|
|
|
|
|
/* .bead {
|
|
margin-right: 1em;
|
|
padding: 2px 4px;
|
|
}
|
|
.done {
|
|
text-decoration: line-through;
|
|
color: #888;
|
|
} */
|
|
/* .waiting {
|
|
color: #888;
|
|
} */
|
|
/* .active {
|
|
background-color: var(--fg-color);
|
|
color: var(--bg-color);
|
|
} */
|
|
|
|
/***** Game config *****/
|
|
.config {
|
|
margin-top: 2em;
|
|
}
|
|
|
|
summary {
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button {
|
|
font-size: 140%;
|
|
margin: 10px 0;
|
|
color: var(--bg-color);
|
|
/* color: var(--fg-color); */
|
|
}
|
|
|
|
details > :first-child {
|
|
margin-bottom: 1em;
|
|
}
|
|
|
|
label {
|
|
margin-bottom: 3px;
|
|
}
|
|
label > :first-child {
|
|
display: inline-block;
|
|
min-width: 14em;
|
|
}
|
|
|
|
input {
|
|
width: 7em;
|
|
font-size: 16px;
|
|
/* color: var(--bg-color); */
|
|
border: 2px solid #686868;
|
|
}
|
|
|
|
input[type=checkbox] {
|
|
height: 1em;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
}
|
|
|
|
#magister_box {
|
|
border: 1px dashed var(--fg-color);
|
|
/* margin: 1em 0; */
|
|
margin: 1em auto;
|
|
|
|
<script
|
|
import * as topiIconsSVGscs from './topics.json'
|
|
import Game from "./Game.svelte"
|
|
|
|
import { type } from "os";
|
|
|
|
>export let ready
|
|
|
|
export let room
|
|
export let connectio // loading, waiting, playing, paused.n
|
|
export let state
|
|
export let starexport let seconds_per_bead
|
|
t_time
|
|
expopaused_progressrexport let _active_sessions
|
|
t let topic_magister
|
|
export let _clock_offset: 0,
|
|
magiexport let meditate
|
|
export let players
|
|
export let round
|
|
console.log(topics)logs
|
|
export let// seconds_per_bead
|
|
let topic_img
|
|
exp
|
|
|
|
// export let state
|
|
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
|
|
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', if (topic_img) 'Shadow', 'Society
|
|
$: {
|
|
topic_img.innerHTtopicIconsML = topics[topic.s.toLocaleLowerCa()])setoLotopic/ }
|
|
|
|
'
|
|
]
|
|
|
|
// Could make configurable. Eh.
|
|
const MEDITATION_SECONDS = 60
|
|
|
|
let game_stages = []
|
|
$: {
|
|
game_stages = [{
|
|
label: `${meditate ? 'Meditation' : 'Game'} is about to start`,
|
|
duration: 3,
|
|
no_sound: true
|
|
}]
|
|
if (meditate) game_stages.push({
|
|
label: 'Meditate',
|
|
type: 'meditate',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < rounds; r++) {
|
|
for (let p = 0; p < players; p++) {
|
|
game_stages.push({
|
|
label: `Round ${r+1} player ${p+1}`,
|
|
duration: seconds_per_bead,
|
|
type: 'bead', r, p
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const update_state = async patch => {
|
|
await fetch(`${room}/configure`, {
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(patch)
|
|
})
|
|
}
|
|
|
|
const upd = (k, v) => () => update_state({[k]: v})
|
|
|
|
const config = k => e => {
|
|
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
|
|
const raw_value = e.target.value
|
|
const value = e.target.type === 'number' ? raw_value|0
|
|
: e.target.type === 'checkbox' ? e.target.checked
|
|
: raw_value
|
|
update_state({[k]: value})
|
|
}
|
|
|
|
const roundish = x => Math.round(x * 10) / 10
|
|
|
|
|
|
const waiting_stage = { label: 'Waiting for game to start', duration: Infinity }
|
|
const complete_stage = { label: 'Game complete', type: 'complete' }
|
|
const get_current_stage = (offset_ms) => {
|
|
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
|
|
|
|
let offset_sec = Math.round(offset_ms / 1000)
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let stage = game_stages[s]
|
|
if (stage.duration > offset_sec) {
|
|
return {stage, offset_sec}
|
|
}
|
|
offset_sec -= stage.duration
|
|
}
|
|
return {
|
|
stage: complete_stage, offset_sec
|
|
}
|
|
}
|
|
|
|
let current_stage = null, offset_sec
|
|
$: console.log('current stage', current_stage)
|
|
|
|
|
|
const tick = (play_audio) => {
|
|
console.log('tick')
|
|
// console.log('state', state, 'completed', state && state.complete)
|
|
|
|
const time = state === 'playing' ? Date.now() + _clock_offset - start_time
|
|
: state === 'paused' ? paused_progress
|
|
: 0
|
|
const {stage: new_stage, offset_sec: new_offs} = get_current_stage(time)
|
|
// state_label = state.label
|
|
|
|
offset_sec = new_offs
|
|
if (new_stage !== current_stage) {
|
|
console.log('state changed', new_stage.label, new_stage.type === 'complete')
|
|
current_stage = new_stage
|
|
// completed = new_game_state.complete
|
|
// if (!state.complete) round_audio.play()
|
|
|
|
if (play_audio && !new_stage.no_sound) {
|
|
if (current_stage.type === 'complete') complete_audio.play()
|
|
else round_audio.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
let timer
|
|
$: {
|
|
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
|
|
let completed = current_stage ? current_stage.type === 'complete' : false
|
|
// console.log('xx', state, timer, completed, current_stage)
|
|
|
|
// if (state !== 'loading') tick(false)
|
|
|
|
if (state === 'playing' && timer == null && !completed) {
|
|
// setTimeout needed to get around some weird race condition.
|
|
// There's probably better ways to structure this :/
|
|
setTimeout(() => tick(false))
|
|
timer = setInterval(() => {
|
|
tick(true)
|
|
}, 1000)
|
|
} else if ((completed || state !== 'playing') && timer != null) {
|
|
console.log('cancelled interval timer')
|
|
clearInterval(timer)
|
|
timer = null
|
|
} else if (state === 'waiting' || state === 'paused') {
|
|
setTimeout(() => tick(false))
|
|
}
|
|
}
|
|
|
|
let game_completed
|
|
$: {
|
|
// console.log('updating game_completed', current_stage)
|
|
game_completed = (state !== 'playing' || current_stage == null) ? false
|
|
: (current_stage.type === 'complete')
|
|
}
|
|
|
|
let internal_state
|
|
$: internal_state = game_completed ? 'completed' : state
|
|
|
|
let bar_width = 0
|
|
$: bar_width = current_stage == null ? 0
|
|
: state === 'waiting' ? 0
|
|
: current_stage.type === 'complete' ? 100
|
|
: 100 * offset_sec / current_stage.duration
|
|
|
|
let stage_label
|
|
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
|
|
: current_stage == null ? 'unknown' : current_stage.label
|
|
|
|
const order = ['meditate', 'bead', 'complete']
|
|
const class_for = x => x < 0 ? 'done'
|
|
: x > 0 ? 'waiting'
|
|
: 'active'
|
|
|
|
const progress_class = (stage, type, r, p) => {
|
|
if (stage == null) return ''
|
|
|
|
const current_o = order.indexOf(stage.type)
|
|
const element_o = order.indexOf(type)
|
|
|
|
// const o_diff = element_o - current_o
|
|
return type === 'bead' && stage.type === 'bead'
|
|
? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
|
|
: class_for(element_o - current_o)
|
|
}
|
|
|
|
// This will get more complex in time. For now, pause the game to fiddle.
|
|
$: settings_disabl// ed = state === 'playing'
|
|
|
|
let config_open = false
|
|
|
|
$: if (_magister === true) config_open = true
|
|
//
|
|
// The first user has the config open by default.
|
|
$: if (_active_sessions === 1) config_open = true
|
|
|
|
// The magister box is fully visible once there's a critical mass of players in the room
|
|
$: magister_opaque = _magister === true || _active_sessions >= 6
|
|
|
|
|
|
</script>
|
|
|
|
<svelte:head>
|
|
{#if _magister}
|
|
<style>
|
|
body {
|
|
background-color: var(--bg-highlight);
|
|
}
|
|
</style>
|
|
{/if}
|
|
</svelte:head>
|
|
|
|
<!-- <main class:magister={_magister}> -->
|
|
<main>
|
|
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
|
|
{#if internal_state === 'loading'}
|
|
<h1>Loading game state</h1>
|
|
{:else}
|
|
<!-- <h1>Glass Bead Game Timer</h1> -->
|
|
<!-- <h1>{topic}< id='topicimg'ip/h1> -->
|
|
<h1>Topic: <em>{topic}</em><
|
|
div
|
|
<imbind:this=topic_im</div>g{round_audio}g id='topic'asd
|
|
f /></img> /h1>
|
|
<!-- <h4>Topic: <em>{topic}</em></h4> -->
|
|
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
|
|
let audio_works = true
|
|
|
|
// function test_audio() {
|
|
// let a = new Audio()
|
|
// a.vo
|
|
(_magister == null || _magister == true) && {#if internal_state == 'waitin g'}
|
|
<button on:click={upd('state', 'play
|
|
<div style='height: 400px;ogh'></div>classvsping')}>Sta
|
|
{/if}<rt</button>
|
|
lu // a.src = '/lo-metal-tone.mp3'
|
|
m// e = 0./silence.mp3l1
|
|
//
|
|
|
|
<h1>{topic}</h1> a.src = '/lo-metal-tone.mp3'
|
|
// a.play().then(
|
|
// () => {
|
|
// audio_works = true
|
|
// console.log('Audio works')
|
|
// },
|
|
// () => {
|
|
// audio_works = false
|
|
// console
|
|
<!-- I'm not ready to delete these UI elements but <h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
|
|
we might not use them -->.log('Audio does not wor
|
|
k')
|
|
//
|
|
<div>
|
|
{state === 'waiting' ? 'Waiting for the game to start'
|
|
: state === 'paused' ? 'GAME PAUSED'
|
|
: state === 'playing' ? 'Game in progress'
|
|
: ''}
|
|
</div>
|
|
{#if connection !== 'connected'}
|
|
<div>DISCONNECTED FROM GAME SERVER</div>
|
|
{:else}
|
|
{#if _active_sessions == 1}
|
|
<div>You are alone in the room</div>
|
|
{:else}
|
|
<div>{_active_sessions} players are in this room</div>
|
|
{/if}
|
|
{/if}
|
|
}
|
|
// )
|
|
// }
|
|
function fix_audio(e) {
|
|
// console.log('xx')
|
|
// test_audio()
|
|
}
|
|
// setTimeout(test_audio, 0)
|
|
|
|
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'Attention', 'Art', 'Empathy', 'EutopiaAdvanced', 'Future', 'Game', 'Gift',
|
|
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
|
|
]
|
|
|
|
// Could make configurable. Eh.
|
|
const MEDITATI
|
|
<div id='rounds'>
|
|
<h2>Game</h2>
|
|
{#if meditate}
|
|
<div>
|
|
<span class={progress_class(current_stage, 'meditate')}>Meditation (1 min)</span>
|
|
</div>
|
|
{/if}
|
|
{#each Array(Math.max(rounds, 0)) as _, r}
|
|
<div>Round {r+1}:
|
|
{#each Array(Math.max(players, 0)) as _, p}
|
|
<span class={'bead ' + progress_class(current_stage, 'bead', r, p)}>{p+1} </span>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
ON_SECONDS = 60
|
|
|
|
let game_stages = []
|
|
$: {
|
|
game_stages = [{
|
|
label: `${meditate ? 'Meditation' : 'Game'} is about to start`,
|
|
duration: 3,
|
|
no_sound: true
|
|
}]
|
|
if (meditate) game_stages.push({
|
|
label: 'Meditate',
|
|
type: 'meditate',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < rounds; r++) {
|
|
for (let p = 0; p < players; p++) {
|
|
game_stages.push({
|
|
l
|
|
<details>
|
|
<summary>Other stuff</summary>
|
|
>/suUI
|
|
</details>details
|
|
abel: `Round ${r+1} player ${p+1}`,
|
|
duration: seconds_per_bead,
|
|
type: 'bead', r, p
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const update_state = async patch => {
|
|
await fetch(`${room}/configure`, {
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify(patch)
|
|
})
|
|
}
|
|
|
|
const upd = (k, v) => () => update_state({[k]: v})
|
|
|
|
const config = k => e => {
|
|
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
|
|
const raw_value = e.target.value
|
|
const value = e.target.type === 'number' ? raw_value|0
|
|
: e.target.type === 'checkbox' ? e.target.checked
|
|
: raw_value
|
|
update_state({[k]: value})
|
|
}
|
|
|
|
const roundish = x => Math.round(x * 10) / 10
|
|
|
|
|
|
const waiting_stage = { label: 'Waiting for game to start', duration: Infinity }
|
|
const complete_stage = { label: 'Game complete', type: 'complete' }
|
|
const get_current_stage = (offset_ms) => {
|
|
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
|
|
|
|
let offset_sec = Math.round(offset_ms / 1000)
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let stage = game_stages[s]
|
|
if (stage.duration > offset_sec) {
|
|
return {stage, offset_sec}
|
|
}
|
|
offset_sec -= stage.duration
|
|
}
|
|
return {
|
|
stage: complete_stage, offset_sec
|
|
}
|
|
}
|
|
|
|
let current_stage = null, offset_sec
|
|
$: console.log('current stage', current_stage)
|
|
|
|
|
|
const tick = (play_audio) => {
|
|
console.log('tick')
|
|
// console.log('state', state, 'completed', state && state.complete)
|
|
|
|
const time = state === 'playing' ? Date.now() + _clock_offset - start_time
|
|
: state === 'paused' ? paused_progress
|
|
: 0
|
|
const {stage: new_stage, offset_sec
|
|
text-align: center;cen: new_offs} = 3get_cu
|
|
display: inline-block;inline-blorrent_stage(time)
|
|
// state_label = state.label
|
|
|
|
offset_sec = new_offs
|
|
if (new_stage !== current_stage) {
|
|
console.log('state changed', new_stage.label, new_stage.type === 'complete')
|
|
current_stage = new_stage
|
|
// completed = new_game_state.complete
|
|
// if (!state.complete) round_audio.play()
|
|
|
|
if (play_audio && !new_stage.no_sound) {
|
|
if (current_stage.type === 'complete') complete_audio.play()
|
|
else round_audio.play()
|
|
}
|
|
}
|
|
}
|
|
|
|
let timer
|
|
$: {
|
|
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
|
|
let completed = current_stage ? current_stage.type === 'complete' : false
|
|
// console.log('xx', state, timer, completed, current_stage)
|
|
|
|
// if (state !== 'loading') tick(false)
|
|
|
|
if (state === 'playing' && timer == null && !completed) {
|
|
// setTimeout needed to get around some weird race condition.
|
|
// There's probably better ways to structure this :/
|
|
setTimeout(() => tick(false))
|
|
timer = setInterval(() => {
|
|
tick(true)
|
|
}, 1000)
|
|
} else if ((completed || state !== 'playing') && timer != null) {
|
|
console.log('cancelled interval timer')
|
|
clearInterval(timer)
|
|
timer = null
|
|
} else if (state === 'waiting' || state === 'paused') {
|
|
setTimeout(()
|
|
|
|
#topicimg {
|
|
max-width: 200px;width
|
|
} => tick(false))
|
|
}
|
|
}
|
|
|
|
let game_completed
|
|
$: {
|
|
// console.log('updating game_completed', current_stage)
|
|
game_completed = (state !== 'playing' || current_stage == null) ? false
|
|
: (current_stage.type === 'complete')
|
|
}
|
|
|
|
let internal_state
|
|
$: internal_state = game_completed ? 'completed' : state
|
|
|
|
let bar_width = 0
|
|
$: bar_width = current_stage == null ? 0
|
|
: state === 'waiting' ? 0
|
|
: current_stage.type === 'complete' ? 100
|
|
: 100 * offset_sec / current_stage.duration
|
|
|
|
let stage_label
|
|
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
|
|
: current_stage == null ? 'unknown' : current_stage.label
|
|
|
|
const order = ['meditate', 'bead', 'complete']
|
|
const class_for = x => x < 0 ? 'done'
|
|
: x > 0 ? 'waiting'
|
|
: 'active'
|
|
|
|
const progress_class = (stage, type, r, p) => {
|
|
if (stage == null) return ''
|
|
|
|
const current_o = order.indexOf(stage.type)
|
|
const element_o = order.indexOf(type)
|
|
|
|
// const o_diff = element_o - current_o
|
|
return type === 'bead' && stage.type === 'bead'
|
|
? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
|
|
: class_for(element_o - current_o)
|
|
}
|
|
|
|
// This will get more complex in time. For now, pause the game to fiddle.
|
|
$: settings_disabled = state === 'playing'
|
|
|
|
let config_open = false
|
|
|
|
$: if (_magister === true) config_open = true
|
|
|
|
// The first user has the config open by default.
|
|
$: if (_active_sessions === 1) config_open = true
|
|
|
|
// The magister box is fully visible once there's a critical mass of players in the room
|
|
$: magister_opaque = _magister === true || _active_sessions >= 6
|
|
|
|
|
|
</script>
|
|
|
|
<svelte:head>
|
|
{#if _magister}
|
|
<style>
|
|
body {
|
|
background-color: var(--bg-highlight);
|
|
}
|
|
</style>
|
|
{/if}
|
|
</svelte:head>
|
|
|
|
<!-- <main class:magister={_magister}> -->
|
|
<main>
|
|
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
|
|
|
|
{#if internal_state === 'loading'}
|
|
<h1>Loading game state</h1>
|
|
{:else}
|
|
<!-- <h1>Glass Bead Game Timer</h1> -->
|
|
<!-- <h1>{topic}</h1> -->
|
|
<h1>Topic: <em>{topic}</em></h1>
|
|
<!-- <h4>Topic: <em>{topic}</em></h4> -->
|
|
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
|
|
|
|
{#if !audio_works}
|
|
<div>
|
|
Audio does not work
|
|
<button on:click={fix_audio}>Fix</button>
|
|
</div>
|
|
{/if}
|
|
// let audio_workstrue =// fals// e
|
|
|
|
// function test_audi// o() {
|
|
// }
|
|
|
|
$::$ // let a =
|
|
// a.v// olume = 0.1v// o
|
|
a.src = '/xlo// -metal-tone.mp3/// / 'anew A// udio()
|
|
// a.play ()/// / .t
|
|
hen (
|
|
() => {a// ud
|
|
// co// nsole.log(// 'Audio works// does // // not work')//
|
|
io_work s =
|
|
audi//// o_w// ork// s // = falsetrue true} ,
|
|
// () =>// {//
|
|
// // c// onsole// .l// og('Aud
|
|
function// f
|
|
console.log('xx// ')
|
|
console.lo// g(// setTimeout('xx')logco, 100
|
|
0)nsoleix_audio(e) {
|
|
test_audio())test_ }]
|
|
{ io
|
|
test_audio()t
|
|
est_au doe s
|
|
} not work' )log.
|
|
|
|
$: { ) consol
|
|
audio_works = true},
|
|
audio_wothenplayAudio= {
|
|
}
|
|
$$"$falseort let _active_sessions
|
|
|
|
let game_completed = false // Derived from other properties
|
|
let internal_state
|
|
$: interna// l_sta// te = game_completed ? 'compl// eted' : state
|
|
$: {
|
|
if (s// tat// e !== 'playing') {
|
|
console.log('xxx')
|
|
game_compl
|
|
let round_audio1
|
|
letcompletend_ audio21
|
|
eted = false
|
|
}
|
|
}
|
|
|
|
// export let state
|
|
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
|
|
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
|
|
]
|
|
|
|
// Could make configurable. Eh.
|
|
co = []nst MEDITATION_SECONDS = 60
|
|
|
|
let game_stages
|
|
$:{
|
|
|
|
} `${meditate ? 'Meditation' : 'Game'} is about
|
|
type: ''
|
|
no_sound: true_to start`will start {
|
|
3game_stages = []
|
|
if (meditate)
|
|
type: 'meditate', game_sta
|
|
ges.pu
|
|
}]sh({
|
|
label: 'Meditate',
|
|
duration:
|
|
if (meditate) game_stages.push({
|
|
label: 'Meditate',
|
|
duration: MEDITATION_SECONDS, MEDITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < rounds; r++)
|
|
type: 'bead', r, p type: 'bead',
|
|
r, p {
|
|
for (let p = 0; p < players; p++) {
|
|
game_stages.push({
|
|
label: `Round ${r+1} player ${p+1}`,
|
|
duration: seconds_per_bead,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const update_state = async patch => {
|
|
await fetch(`${room}/configure`, {
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
body: // JSON.stringify(patch)
|
|
})
|
|
}
|
|
|
|
const upd = (k, v) => () => update_state({[k]: v})
|
|
|
|
const config = k => e => {
|
|
console.log('k', k, e.data, e.value, e.target.value, e.target.type)
|
|
const raw_value = e.target.value
|
|
const value = e.target.type === 'number' ? raw_value|0
|
|
: e.target.type =waiting== 'checkboxg'
|
|
co'Waiting for game to start', duration: Infinityinst complete type: 'complete',_state = { label: 'Game compl
|
|
const complete_stage = { const get_current_stage = (offset_ms) => {
|
|
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
|
|
|
|
let offset_secondMath.round(rounds = offse /) 1000t_msoffset
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let stage = game_stages[s]
|
|
if (stage.dursecation * 1000 > offset_ms) {
|
|
sec : offset_sec
|
|
const time = state + _clock_offset_cloooff=== 'playing' ? Date.now() - start_time
|
|
: state === 'paused' ? paused_progress
|
|
: 0pauslet * 1000return {stage, secoffset_ms}
|
|
}
|
|
offset_currentms -=time stage.duratiosecn * 1000
|
|
}
|
|
r -+ paused_progresspauseeturn {
|
|
stage: complete_secstage, offset_ms
|
|
}
|
|
}
|
|
// label: 'Game complete', complete: true }ete// '
|
|
if (state === 'waiting' || state{stage: === 'loading, offset_ms: 0}// '!== 'playing' && state
|
|
!== ') return waiting_// stagewaitinsecg, complete: trues// ac age}ivi? ye.target.checked
|
|
: raw_v// alue//
|
|
update_state({[k]: value})
|
|
/sec/ }
|
|
|
|
c// onst roundgisx_h = x => Math.roundnew_stage.type === 'complete'(xg// ame// _ * 1g00) // x_/ 100
|
|
|
|
// const startgame_ = () =// > {//
|
|
// consolge.log(x_x_'start!': xcurr_gamntex = null,
|
|
let _ggame_s// tatex_gagme)
|
|
// upstagedatege_current_stage.type === 'complete'game_statge({currstatnte: 'gpx_laying'})
|
|
// }
|
|
|
|
// legcurrame_ntt rogund_arr
|
|
// $: round_arrcurr = nntew Agrrg = null{}nullame_ay(Mathcomplete_stateconst {stage: new_stage, ne: new_offs: w_offs: current_
|
|
// setTimeout needed to get around some weird race condition.
|
|
// There's probably better ways to structure this :/other ways to stage.type === 'complete'offset_ // setTimeout needed to get around some weird race condition.ms}oenew_stage: stag.max(rounds, 0)).fill()$: console.log('game state xxx', game_state)logconsnew_offsole
|
|
|
|
|
|
new_stage_const || state === 'paused'ise gsetTsetTimeout(() => tick(false))imeout(() => setT et_cur)rebar_nt_scurrtatent = (gobar_ffset
|
|
|
|
letgw_stagee_ame_ {stw_stageae_cunrrte, ntoffsget_m
|
|
|
|
$n: gaw_stage
|
|
let game_completed
|
|
$: {
|
|
// console.log('updating game_completed', current_stage)
|
|
game_completed = (state !== 'playing' || current_stage == null) ? false
|
|
: (current_stage.type === 'complete')
|
|
}
|
|
|
|
|
|
let internal_state
|
|
$: internal_state = game_completed ? ' completed' : stat
|
|
// let b// ar_width = 0
|
|
|
|
: statecurrent_stage.type
|
|
: current_stage.type === 'complete' ? 100 === 'waiting' ? 0currenstar
|
|
e//
|
|
$: {//
|
|
if (state// !== 'playing') {//
|
|
game_completed = fals// e/// /
|
|
}//
|
|
}
|
|
mee// __c// om/// / ple// ted = game_state =// = nul// l ? fnalse
|
|
if (p// lay_// audi && !new_stage.no_sou// ndno// _new_stage!! o) {
|
|
if (current_stage.complete) com// $: game_completed = current_stage && (current_stage.type === 'complete')
|
|
plete_audio.play()
|
|
else
|
|
let game_completed round_let bar_width = 0
|
|
audio.play(
|
|
} ) : !!game_state.completegame_s tategame_
|
|
$: width = state.complete ? 100 : 100 * offset_ms / (state.duration * 1000)$: widtinternal_state === 'waiting'
|
|
let config_open = false
|
|
|
|
|
|
$: if (_magister === true) config_open = tru
|
|
|
|
// The first user has the config open by default. ysere
|
|
$: if (_active_ses=sio1ns_acti_magister == tr// TMaAke themagist iser box fully visible once there's a critical mass of players in the roomenouhg pl ue)
|
|
// let magister_opaque = false
|
|
$: magister_opa
|
|
<svelte:hea
|
|
<svelte:head>
|
|
{#if _magist e r_ m a g isM oda l }
|
|
<sty le>
|
|
body {
|
|
bac kgroun d-color: var(--bg- highlight); over flow: hidden;
|
|
}
|
|
</style>
|
|
{/if}
|
|
</svelte:head>
|
|
d<!-- <main class:magister={_magister}> -->
|
|
>
|
|
{#if isModal}
|
|
<style>
|
|
body {
|
|
overflow: hidden;
|
|
}
|
|
</s<!-- tyle>
|
|
{/if}
|
|
</svelte:head>
|
|
que = _magister = class:magi
|
|
|
|
{#if !audio_works}audio_
|
|
Audio does not wor -->k
|
|
<button on:{fix_audio}>Fix</button>/fix_audfclick=o
|
|
<div>
|
|
|
|
{/if}</div> </div ster={_magister} == true || _active_sess >= 616ions_activs truemagi
|
|
config_open = trueconfig_magstconsole.log('config', config_open)config_giflogconsoleconfig_op
|
|
? 'Waiting for game to start'
|
|
: h =
|
|
// $: console.log('current stage type', tlogcurrent_ && current_stagecurre, (current_stage && current_stage.type === 'complete')nt// {
|
|
console.log
|
|
$: console.log('current stage type', current_stage && current_stage.type)
|
|
|
|
('updating game_completed', current_stage)curreneinlogconsole
|
|
$: ga<!-- me_completed = current_stage = -->= n<!-- ul1l ? false : ( -->curre1nt_st <a 1hre
|
|
<!-- <h4>To1pic: <em>{to
|
|
<!-- <h4>Topic: <em>{topic}</em></h4> -->pic}</em></h4> -->f="../..">(leaveChange roprogressom)</a>age.ty <h4>Topic: <em>{t
|
|
<div>< span /div>o'bead ' + +'pic
|
|
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>pan}<s/e <!-- <d
|
|
</div>iv>{connection} / {state}</div> -->
|
|
m><!-->
|
|
</h4> === 'waiting' ? 'Waiting for the game to start'
|
|
: state === 'paused' ? 'GAME PAUSED'
|
|
: state === 'playingpauseGame in progress - dplayiGAME PAUSED'
|
|
: ''G
|
|
ame pausedng' ? 'Game in pPGame:
|
|
pe( === 'cdivh4omplet
|
|
}( e')
|
|
// --connection !div=4= h'connected'>
|
|
<div>DISCONNECTED FROM GAME SdivE4RVER<h/div>
|
|
{:els e}<div
|
|
4 {/if}_hst<!-- {#divi4f _activ h e_sessions == 1} -->)age).type ==div=4 'comp lete'h#:
|
|
{#if _active_sessions == 1} s
|
|
/&&/ $: ti
|
|
{#if _active_s essions == 1}c
|
|
{/if} <div>You are alone in the room{_active_sessions} player(s) in room</div>
|
|
{:elare in this roomthe inare with youjoin youse}
|
|
k(fals// e <a href<a href="../..">Change Join
|
|
{/if}another room</a>="../..">Join anotheclassr room</a>)ti(c
|
|
k
|
|
$: tick(f"al./..>Join another room</a>anohter r"// se)<!-- <!-- ticktate// .c)
|
|
|
|
<a href='.Game./../$: c <div id='progress -->conta -->iner'>
|
|
? Will youonso id='progress_time'le.log('game_completed', game_coclass:magister={_magister} class:magister={_magister} mpleted) class:magister={_magister}m
|
|
game_compllogconsole#:
|
|
|
|
<div >
|
|
Magister status
|
|
p<div>
|
|
{#if _magister == null}: {JSON .These cCstr ingify( aloneto wieldeialone_magister)
|
|
}
|
|
the </div>
|
|
|
|
|
|
These c C These cC{
|
|
aloneto weiield <p>You aloneto wieldeialone are the masterThese controls are yours.You are in charge of these controls.the only player with who of the games. You have exclusive control over playing, pausing and configuring this game.</p>
|
|
<p>Do not close this browser window or you will be dethroned.</p>
|
|
You are Magister Ludi - master of thisthe games. magister lu:pelse}
|
|
{/if}ese< {#if _magister == null | | _magist er == truefa l bind:open={config_open}_m <details id='conf ig'>
|
|
<summary>Game controls</summa ry>
|
|
|
|
< div> Will you borro pattern='[0-9]d*'w power, or steal it?
|
|
Magister status: {JSON.stringify(_magist er)}
|
|
</d iv>
|
|
|
|
< div>
|
|
This will effect all play ers.
|
|
</d iv>
|
|
|
|
{#if internal_stpattern='[0-9]*' ate == 'waiti ng'}
|
|
<button on:click={upd('state', 'playing')}>Start</but ton>
|
|
{:else if internal_state == 'playi ng'}
|
|
<button on:click={upd('state', 'ppattern='[0-9]*' aused')}>Pause</but ton>
|
|
{:else if internal_state == 'paus ed'}
|
|
<button on:click={upd('state', 'playing')}>ResumeRestart</but ton>
|
|
{/ if}
|
|
|
|
{#if internal_state == 'paused' || internal_state == 'complete d' }
|
|
<button on:click={upd('state', 'waiting')}>Reset game</but ton>
|
|
{/ if}
|
|
|
|
<la bel>
|
|
<span>Topic</s panAssume the mantle of Magister Ludi>Become
|
|
<input disabled={settings_disabled} type='text' value={topic} on:input={config('topic')} list='archetopic s' >
|
|
<datalist id='archetopi cs'>
|
|
{#each ARCHETOPICS as to pic}
|
|
<option value={top ic}>
|
|
{/e ach}
|
|
</datal ist>
|
|
</lab el>
|
|
|
|
<la bel>
|
|
<span>Pre-game meditation</s pan>
|
|
<input disabled={settings_disabled} type='checkbox' checked={meditate} on
|
|
<!-- class:magister_op class:magister_opaqueaque -->:inpu<!-- t={config('meditat e' )} >
|
|
</lab el>
|
|
-->
|
|
<la bel>
|
|
<span>Number of players</s pan>
|
|
<input disabled={setvar(--fg-color)--fgtings_disabled} type='number' value={players} on:input={config('players')} min=1 max= 12 >
|
|
</lawhitehb el class='config'>p
|
|
|
|
<la bel>is managing this gameis in control of this gamewill start
|
|
<span>Number of rounds</s p pan>var(--fg-color)
|
|
<input disabled={settings_disabled} type='number' value={rounds2px0} o2pxn:input={config('rounds')} min=1 max =20>
|
|
</lab el>
|
|
|
|
<la bel>
|
|
<span>Se class:magister_opaqueuconds per bead</s pan>
|
|
<input disabled={settings_disabled} type='nu
|
|
<div id='magiste
|
|
var(--fg-color)
|
|
padding: 32px;--fg-c.magister {
|
|
background-color: var(-b-bg-highli
|
|
ght);
|
|
}color
|
|
r_
|
|
{#if _m(--fg-coagister == );null_m p <i>Advanced - for large games</i> agier}
|
|
|
|
t} {/if Magister Ludi (mifawait }
|
|
g
|
|
exclusive i) of the game{:tphen
|
|
<pdiv>When fgpresent, the Magister Ludi (master of the games) has exclusive control of the game.</You have exclusive control overare responsible forspopd646ing, i/* v>}ing and
|
|
|
|
ing */
|
|
border: 2px solid #686868
|
|
|
|
input[type=checkbox] {
|
|
height: 1.25em20px;display: none;hiddihidd }
|
|
:2px solid #333eeevar(--fg-color);--ffg- {Do noto/awaicontrolcrownwtif#{bo or you will be dethronedx'b
|
|
Onc <div> e somebody ascends to Magister Ludi statuAssume the ma/* ntle ofs, of th</var(--fg-color)--fgbg-d */iv
|
|
|
|
main > {
|
|
margin-lef.t: 32em;lef: -= }
|
|
*>
|
|
{:e _magister == true}
|
|
Other players cannot play/pause or configure this gamecon r power will be lo
|
|
<svelte:body class="foo"lstyle="back
|
|
.foo { color: red; background-color: red;}
|
|
ground-color:/* red;"clasvar(- */-b/* g-color)s= //>st */<button on:click={upd(null'_mnullagisAbdicate Magister Ludi statuste <pdiv
|
|
position: relative;> You are the master of the games. No other players can a.ccess this room's controls</pdiv>. <pdiv>You will los/* e this statu */
|
|
padding: 0 20emauto;s if you 0auto/* m/* clos1008 */0e this b */rows
|
|
er window. <p/d
|
|
|
|
position: absoluterelativeabsolute;
|
|
left: 0; right: 0;t iv>r', ' <div>true')}>A<div>
|
|
</div>You are the master of the games. No other players can access this r.
|
|
oom's con You will lose this status if you close this browser window. exclusive right
|
|
<var(--bg-color)/div>trols.assume the mantle of Magister Ludi</bu
|
|
|
|
#config._magister {
|
|
background-color: gvar--bg-highlight(--bghigh-colo-hir;) }
|
|
in tton>
|
|
When present, only the Master of the Games has access to the game controls.
|
|
_maglse ife game, all ca
|
|
|
|
var(--bg-color) When pre
|
|
{/if}sent, only the Master of the Games has access to the game controlscan control the he t timerhis game.he Amesmn no l<!-- When theIf a master of the games is present, other pla cannot yers lo the games -->e
|
|
opacity: 40var(--bg-color)%;50cannot access the game's controls.F a ongera player start or stop the game/* . The
|
|
body {
|
|
background */
|
|
background-color:var(--bghighlight-color) white;
|
|
color: color: #33
|
|
0202;0c-colorbac
|
|
background-c, #magister_box:hover olor: red;ckcolor: red/* ; }
|
|
|
|
Magister Ludi will relinquish lo */scontrol thee oludotAscend to the throne ofher players are locked
|
|
Once somebody ascends to Magister Ludi status, other players can no longer start or stop the game.out of lousers mbecomesTheecome> mbe r' value={seconds_per_bead} on:input={config('seconds_per_
|
|
opacity: 40%;
|
|
transition: opacity 0.342 ease-outit
|
|
#magister_box.magister_opaque {
|
|
opacity: 1040%; }
|
|
in-out;1s; f
|
|
|
|
#magister_box:hover {
|
|
opacity: 100%; }
|
|
adeopaanimate:
|
|
|
|
|
|
|
|
opacity: 40%; opacity: 40%;bead ')}>
|
|
|
|
</div> </lab el>
|
|
|
|
<div style='margin-top:
|
|
|
|
#config {
|
|
margin-top: 2em;
|
|
} 1e m;'>
|
|
(Total game length: {round ish(
|
|
game_stages.reduce((x, s) => x + s.duration, 0) / 60
|
|
)} minu tes)
|
|
</d iv>
|
|
|
|
<button on:click={upd('_magister', 'true')}>Become Magister Ludi</but ton>
|
|
</details>
|
|
{:else}
|
|
<div>TOnly the Magisteris in control of this game</div>is in Ludi can Game controlled by WaiGamam,ag}
|
|
}
|
|
{/ifomplconst tick = (play_audio) => {/current_stage.type === 'complete'current/
|
|
c
|
|
|
|
<div>
|
|
JSON.stringify(stringify)_magistermagi Magister status: {}
|
|
</div> </div onso((internacontrolsorlsl_effectstat
|
|
<div>
|
|
This will drive the game for all players.
|
|
/***** Game config *****/
|
|
#config {
|
|
margin-top: 2em;
|
|
}
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
This will drive controlmodify thefor all players. game d
|
|
</div> e === 'playing' || internal_state === 'paused') && current_stagecur1
|
|
8#config {
|
|
margin-top: 2em;
|
|
}
|
|
|
|
/* **** Game config *****/ Conffi
|
|
#con
|
|
/* background-colo#1c0303r: white; */black;
|
|
background-bl
|
|
margin-top:
|
|
padding: 34px 0;3
|
|
padding: 200px auto;do;mar0;end-mode: da
|
|
mix-blend-mode: d
|
|
width: 100%;
|
|
margin: 0 2em;arken;blend-morken;blend-modefig {
|
|
margin-top: 2em;
|
|
}
|
|
rent_--interninterna font-size: 1080%;xcurrent_stage && !game_completed)game_complcomplinternal_internal_stateinternal_current_stage && !compl ? current_stag
|
|
|
|
#magister_box {
|
|
border: 1px dashed white;
|
|
margin: 1em 0;
|
|
padding:
|
|
max-width: 5300px;0.
|
|
|
|
#magister_box > button {
|
|
display: block;
|
|
}
|
|
> but52em;wdi2 }
|
|
.e.duration - le.lo// g( : '
|
|
<div>
|
|
This will drive
|
|
|
|
#config {
|
|
margin-top: 2em;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
} the game for all players.
|
|
<
|
|
/div>
|
|
|
|
''tick')
|
|
con
|
|
'bead ' + roundsround' <div>{offset_secoffsets
|
|
<div id='pr
|
|
<span>ogres</span>scontainer'>tage_secs_remaining}</div> id=sole.log('x{statcurre', ntstasgte,agea// // // c 'ivi
|
|
if (state !== =.type === 'completeelte'loading== 'playing' || state === 'paused') tick(false)coympleted', sta
|
|
const order = ['mediPre-game meditationm tate', 'bead', 'complete']
|
|
const cla
|
|
<span>Meditatess_for = x =:> x < 0 ? 'done'
|
|
:x > 0 </span>? 'waiting'
|
|
: 'active'
|
|
(, t
|
|
tick(false)e// && ststage, ate.complete)
|
|
|
|
l || sstagetate === 'paused'et x = get_curren const curr
|
|
<span>ent_o = ortageder</span>.indexOf(scurrent_stage.type)
|
|
tconst ele
|
|
// if (m
|
|
const o_diff = element_o -< current_
|
|
type === 'bead' && stage.type === 'bead'
|
|
(r
|
|
<span>=== stage.r ? cl</span>ass_for(p - stage.p) : class_for(r - stage.r))class_forclass_for?
|
|
: class_for(elemen
|
|
// This will get more complex in time. For now,
|
|
<span>pause the game t</span>o fiddle.
|
|
$:let settings_disabledstate === 'playing' =
|
|
t_o - current_o)ty
|
|
|
|
relement_o !==- current_oeturn o_d style='margin-top: 1em;ing'iffelement_o - current_o !== 0 ? class_for(o_d
|
|
|
|
<button on:click={upd('_magisterxxsttrueyate', 'playBecome Magister Ludiing')}>Resume</button> iff)
|
|
: (class_for=== 0 )
|
|
return r === stage.r ? oo return r === stage.r ?
|
|
? ent_ocurrent_o = o
|
|
|
|
main { margin-bottom: 32em;
|
|
}
|
|
rdert
|
|
: class_for(0)class_forype.
|
|
|
|
if (element_o < current_o) return 'done'
|
|
else if (element_o > current if (type === 'bead') return r === stage.r ? =
|
|
|
|
elseo_o) return 'waiting'
|
|
else return 'active'picurrentelemcurren indexOf(current_stage.type)currenindeorderiif _ if cu
|
|
button {
|
|
font-size: 140%2
|
|
margin: 10px 0;00%; }
|
|
|
|
|
|
rrent_stagecurrenstate(Datcurre.nontw() g- start_time)
|
|
// state_label = state.label
|
|
|
|
offset_ms = x.off
|
|
// if (state !== 'load//
|
|
const progress_class = (type, r, p) => {
|
|
if (current_stage == null) return ''
|
|
|
|
current_gstate }
|
|
type
|
|
ing') tick(false)
|
|
tickset_ms
|
|
if (// x.game_
|
|
h1 {
|
|
margin-top: 12em;ri
|
|
ng }
|
|
#state !== game_state)
|
|
tick(false){
|
|
cocurrnsolnte.logg('
|
|
button {
|
|
font-size: 140%;
|
|
margin: 10px 0;
|
|
color: #330202; /* TODO: Use CSS variable for this */AS
|
|
}
|
|
state changed', x.game_state.lab15el, x.game_state.complete)
|
|
game_state = x.game_state
|
|
// completed// = new_game _statesec.complet
|
|
|
|
summary >margin-bottomtt3px label {
|
|
line-height: 2;lmargin }
|
|
{ e
|
|
min- //7 if 6(!state.
|
|
|
|
de435t
|
|
color: #330202;ails > :first-child:firmarginst-childb1ottom
|
|
|
|
input {
|
|
width: 103em
|
|
font-size: 16pxheight: 101.px3;;2emheight: }
|
|
: nt:fir > :first-child:f
|
|
display: inline-block;inlineirst lab25ellabelinpumin-width: 10em;width: t{
|
|
padding-top: 2em;
|
|
}
|
|
de/* tails {
|
|
padding- */top: 2em; }
|
|
{: ila
|
|
details > :not(:first-child):firfir* {
|
|
margin-left: 2em;efl }
|
|
|
|
margin: 25px;com
|
|
setTimeo
|
|
height: 5em;ut(se
|
|
position: relative;
|
|
border: 2px/* solid whit */e /* f
|
|
|
|
summary {
|
|
text-decoration: underlineu
|
|
cursor: pointergrabpointer;mopointer: nd;text-decorat }
|
|
ont-size: #330202; */
|
|
/* color: rgb(204,254,254); */
|
|
/* color: whit54er
|
|
margin-left: 5px1em;ef*/gb(204,254,254);
|
|
;font-sizeco60px;lor/* p positirgb(204,254,254)oin#330202r */gb(#330202: tTi
|
|
mix-blend-mode: differencediffere;mix-blenmeoutse
|
|
.bead {
|
|
margin-righ
|
|
padding: 40 2px;t: 12em43p
|
|
x21em;leftdisplay: block; }
|
|
t
|
|
Im mediate(()
|
|
|
|
} => ) setIm/* m
|
|
.waiting { */
|
|
color: #888;
|
|
e s)etTimetick(fals
|
|
font-s12ize: 60px;e)tickple
|
|
padding: 0 2px;-te) round_audio.play()
|
|
|
|
|
|
#progress_-tim {
|
|
position: absolute;
|
|
color: red; }
|
|
e
|
|
if (game_statecurrent_stage, .co class={progress_class('meditate')}mplete) {
|
|
// And play gong
|
|
// completed = true
|
|
// if (
|
|
|
|
i else if (=state === 'waiting') {
|
|
tick(false)tick }
|
|
f (stcurrate nt=== g'playcurri
|
|
lentt current_stage, tig class={progress_class('beadm, r, peditate')}mer
|
|
ng') tick(falsef)tickplay_audio) complete_audio.play()
|
|
}
|
|
curr}
|
|
}entte
|
|
g$: s
|
|
let last_state = null
|
|
|
|
const tick = (play_audio) => {
|
|
console.log('ti ck' )
|
|
consodisabled={settings_disabled}sett le.log('state', statxestate
|
|
/* && state.comple */
|
|
testate, 'completed', co
|
|
mp: new_offsget_ms leted)
|
|
|
|
let {game_statx.e: new_game_stax.te, offset_ms} = get_current_stcurrent_stage == null ? 0
|
|
: ate(
|
|
offset_ms = current_ne
|
|
w_offset_x.m
|
|
//
|
|
$: game_completed = curcurr disabled={settings_disabled}ent_rent_stage == null ? false : !!current_stage.complete
|
|
let bar_width = 0
|
|
$: bar_width =ge state.complete ? 100 : 100 disabled={settings_disabled} * offset_ms ge/ (state.duration * 1000)
|
|
g
|
|
let state_labegl
|
|
$: state_label = current_stage == null ? 'unknown' : current_stag disabled={settings_disabled}e.label
|
|
|
|
snew_ofDate.nnew_g.amex_state !== gow() - s tart_time )
|
|
// // x.setTimeout(() => state_la, n, 0)ew_game_state disabled={settings_disabled}.la1bel, new_gam con1st tick = (play_audio) => {
|
|
console.log('t ick ')
|
|
console.log('state', state, 'completed', state && state .compl ete)
|
|
|
|
let x = get_current_state(Date.no w() - sta rt_tim e)
|
|
// st ate _label = stat e.la be l
|
|
|
|
obar_ffset _ms = x.offs et _ms
|
|
if (x.game_sta te !== game_st ate) {
|
|
console.log('state ch anged', x.game_state.labe l , x.game_gstate.c omplete)
|
|
game_ state = x. game_state
|
|
// completed = new_game_st ate.complete
|
|
// if (!state. complete) round _ audio.text-decoration: line-throughlinestri;text-decplay(#888eee)
|
|
|
|
if (game_s tate.co border: 1px solid white; mplet e ) {
|
|
// And play gong
|
|
/ / comp leted = true
|
|
// if (play_aud io) c ompl ete_audio.play()
|
|
}
|
|
}
|
|
}e_state. game_state = new_game_state
|
|
complet
|
|
<!-- <div>
|
|
(Total game length: {roundish(
|
|
(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
|
|
)} minutes)
|
|
</div> -->enew_ga
|
|
|
|
.done { color: blue; }
|
|
.waiting { color: white; }
|
|
.activewaitingdonmagentamagpurplee { colwhiteor: blue; }lgame_stateabenew_gambnew_el =// s tate.labe l
|
|
|
|
if (last_stat e !== game_state) {
|
|
conso le.log('state changed'bar_ )
|
|
las
|
|
t_// state = game_stat e
|
|
game_state = new_g ame_stat e
|
|
com pleted = new_game_state.complet e
|
|
/
|
|
/ i
|
|
}f
|
|
(! state.complete ) round_audio.p lay2em( )
|
|
}
|
|
|
|
i // f (game_state.com// plete) ? {
|
|
// And&& : false play gon g
|
|
ga // complet ed = tr, game_stateu e
|
|
// // if (play_audio) complete_audio.play( )
|
|
}
|
|
}const tick = (play_audio) => {
|
|
}
|
|
!completed!
|
|
tate_l // Sadly we can't use internal_state here because it generates a cyclic dependancy.
|
|
let completed = game_state == null ? false : !!game_state.complete
|
|
abel = game_s'xx', tate == null, completed ? 'unknown' : game_state.labelgame_stateostate_l#: ? 100
|
|
: 100 * offset_ms / (sta
|
|
let completedgame_completed = game_state == null ? false : !!game_
|
|
|
|
consolecon state.completete.duration * 1000)#: coms}
|
|
|
|
_ms)ompletedc => {
|
|
for (let s =// 0; s < game_stag// es.le// ngth; s++) {
|
|
let state = // game_stages[s]
|
|
if (state.duration * 1000 > offset_ms) {
|
|
return {state, offset_ms// }
|
|
}
|
|
offset_ms -= state.duration * 1000
|
|
}
|
|
return {
|
|
state: // {
|
|
// We might// have l oadeompletedd wh
|
|
consolce.log('state', state, 'completed', game_completed)game_comp'timstatelogconso
|
|
completed = new_game_state.completenew_game_stategame_leetick = (play_audiono_soun d) => {
|
|
console.loggame_state: new_game_(' tick')
|
|
|
|
let {state, offset_ms} = get_current_state(Date.now() - star t_time)
|
|
//
|
|
game_state = new_game_statenew_gam state_label = stat e.labgame_ el
|
|
|
|
if (last_state !
|
|
ompleted game_state
|
|
console.log('cancelled interval timer')logconsoleo = new_game_statec== s tate) {
|
|
consolegame_.log('state ch anged')
|
|
// last_state = state
|
|
// if (game_!state.comompletedplete) round_audio .play()
|
|
}
|
|
|
|
c if (state.comp lete) {
|
|
// And pl ay gong
|
|
// game_completed = trif (play_audio) ue
|
|
complete_audio .play( )
|
|
|
|
tick(false)t ick } }
|
|
n the game i s a
|
|
tick(tr ue)ticklready finished. This is kin// da
|
|
// dirty, but I
|
|
|
|
const 'm special casing it to avoid audio playi// ng....
|
|
if (let {state} }
|
|
ci dirty, but Ifht gthTh.state.compl// ete) {
|
|
game_compl// eted // =
|
|
state_label = 'Game already finishe// d'. finishe// dstate_true else {gam e_// }
|
|
statee game m i
|
|
g// et
|
|
|
|
_current_state(Da// te.now() - start_time) l abel:round_ 'Game complete', complete: t// // rue }, offset_ms
|
|
}
|
|
}
|
|
|
|
let widt // h = 0
|
|
|
|
le// t timer
|
|
let state // _lab el
|
|
|
|
$: {
|
|
console.log(state, tim er)
|
|
// // Sadly we can't use internal_s// tate here b ecause it generates// a cyclic de pendanc// y.
|
|
if (state === 'playing ' && timer == nul// l &&
|
|
// audio1.play()aud
|
|
l// et last_state = null
|
|
// at e!game _completed// ) {
|
|
timer =
|
|
// // if (!sta// te.com p// lete// ) round_audio.pl
|
|
} ay(// )r // oundsetInterval(() => {
|
|
// consol // e.log('tick')
|
|
|
|
let // {state, offset_// ms}//
|
|
|
|
$: {
|
|
console.l// og(state, timer)
|
|
// Sadly we can't use internal_state here beca// use it generates a cyclic dependancy.
|
|
if (state === 'playing' && timer == // null && !game_completed) {
|
|
// We might have loaded when the // game is already finished. This is kinda
|
|
// dirty, but I'm special// casing it to avoid audio// playing.
|
|
if (get_current_state(Date.no// w() - start// _time).state.complete) {
|
|
// game_completed = true
|
|
stat// e_label = 'Game already f// inished'
|
|
} else {
|
|
let last_state = null
|
|
|
|
timer = setInterval(() // => {
|
|
console.log('tick')
|
|
|
|
l// et {state, offset_ms} = get_curre// nt_state(Date.now() - start_time)
|
|
state_label// = state.label
|
|
|
|
width = // state.complete ? 100
|
|
: 100 * o// ffset_ms / (state.durati// on * 1000)
|
|
|
|
if (last_state !== state) {
|
|
// consol// e.log('state changed')
|
|
// last_state = state
|
|
// // if (!state.complete)// round_audio.play()
|
|
}
|
|
|
|
// if (// state.comple// te) // {
|
|
// And play gong
|
|
game_completed = true
|
|
complete_audio.p// lay()
|
|
}
|
|
|
|
|
|
}, 100// 0)
|
|
}
|
|
} else // if // ((game_completed || state !== 'playing') && timer != null) {
|
|
clearInterval(timer)
|
|
timer = null
|
|
}
|
|
}= get_curr
|
|
completend_audio.play()end_aucompleent_state(Date.now() - start_time)
|
|
state_label = state.label
|
|
|
|
width = state.complete ? 100
|
|
: 100 * offset_ms / (
|
|
if (last_state !== state) {
|
|
console.log('state changed')
|
|
last_state = state
|
|
}
|
|
|
|
state.duration * 1000)
|
|
|
|
{
|
|
// And play gong
|
|
if (state.complete
|
|
} )
|
|
|
|
if (last_state && last_state !== state) {
|
|
console.log('state changed')
|
|
last_round_state = statestatelast_logconsole }
|
|
!= last_stlascomplett_ game_completed = true
|
|
|
|
}, 10end_00)
|
|
} else if ((game_completed || state !== 'playing') && timer != null) {
|
|
clearInterval(timer)
|
|
timer = null
|
|
}
|
|
}
|
|
|
|
$: {
|
|
if (internal_state =
|
|
<track kinaudio1{audio1}d b.lo./ind:this=="captions">
|
|
|
|
<audio id="audiotag1" srchi-metal-<trac
|
|
<audio bind:this={a2udio1} src="../hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>k kind="captions">tone.mp3_="audio/flute_c_long_01.
|
|
</track> </trackwav" preload="auto"></audi
|
|
o>== 'waiting') {
|
|
state_label = 'Waiting for game to start'
|
|
width = 0
|
|
}
|
|
}
|
|
|
|
$: {
|
|
console.log('internal state', internal_state)
|
|
}
|
|
|
|
// setInterval(() => width++, 1000)
|
|
{#if internal_state === 'loading'}
|
|
<h1>Loading game state from server</h1>int
|
|
{:else} } intern
|
|
</script>
|
|
|
|
<main>
|
|
<h1> Glass Bead Game Timer<details/h1>
|
|
<h4> R
|
|
<summary>Game sSettingsConfiguration</summary>>/summ
|
|
|
|
oom: <em>{room}</em></h4>
|
|
<h4>To pic: <em>{topic}</em></h4>
|
|
<div>{c onnection} / {state}</div>
|
|
<div>{_active_sessions } player(s) in room</div >
|
|
|
|
<h4>{state_label}</h4>
|
|
<d iv id='progresscontainer'>
|
|
<div id='progress' styl e='width: {width}%'></div>
|
|
</div>
|
|
|
|
<div id='rounds'>
|
|
<h2>Game struc ture</h2>
|
|
{#if meditate}
|
|
<di v>Medita tion (1 min)</div>
|
|
{/if}
|
|
{#each Array(Mat h.max(rounds, 0)) as _, r}
|
|
<div>Round {r+1}:
|
|
{#each Array(Math .max(players, 0)) as _, p }
|
|
<span >{p+1} </s pan>
|
|
{ /each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div id=' config'>
|
|
<h2>Config</h2>
|
|
{#if in ternal_state == 'waiting'}
|
|
<button on:click={upd('state', 'playing')}>Start</button>
|
|
{:else if in ternal_state == 'playing'}
|
|
<button on:click={upd('state', 'paused')}>Pause</button>
|
|
{:els
|
|
e #if i internal_state == 'pausedcompleted' || nternal_state == 'paused'}
|
|
<button
|
|
{:else if internal_state == game'complete
|
|
{/if}d'} on:click={upd('state', ' playing')}>Resume</button>
|
|
{:els
|
|
<button on:click={upd('state', 'waiting')}>Reset</button>e if inte rnal_state == 'completed'}
|
|
<button on:click={upd('state', 'waiting' )}>Reset</butto n>
|
|
{/if}
|
|
|
|
<label>Topic
|
|
<input type='text' value={topic} on:input={config('top ic')} list='archetopics' >
|
|
<
|
|
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> --> datalist id='archetopics'<!-- >
|
|
{# each ARCHETOetailsPIdCS as topic}
|
|
<option value={topic}>
|
|
{/each}
|
|
<
|
|
<!-- <input type='range' value={players} on:sub -->mit={config('players')} min=1 max=12> -->/datalist>
|
|
</label>
|
|
|
|
<label>1 minute med itation before game starts
|
|
<input type='checkbox' checked={meditate} on
|
|
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->:i</style></style></style>n put={config('meditate')} >
|
|
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -
|
|
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->->
|
|
</label>
|
|
|
|
<label>Number of players
|
|
<input type='number' value={players} on:input={config( 'players')} min=1 max=12 >
|
|
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -->
|
|
</label>
|
|
|
|
<label>Number of rounds
|
|
<input type='number' value={rounds} on:input={confi g('rounds')} min=1 max=20>
|
|
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -->
|
|
</label>
|
|
|
|
<label>Seconds per bead
|
|
<input type='number' value={seconds_per_bead} on:input={co nfig('seconds_per_bead')}>
|
|
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=1 2> -->
|
|
</label>
|
|
|
|
<div>
|
|
(Tot al game length: {roundish(
|
|
(rounds * players * seconds_per_bead + (meditate ? MED ITATION_SECOND
|
|
{/if}S : 0)) / 60
|
|
)} minutes)
|
|
</div>
|
|
<div>
|
|
(Tot al game length: {roundish(
|
|
game_stages.reduce((x, s) => x + s.durati on, 0) / 60
|
|
)} minutes)
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<style>
|
|
|
|
#config {
|
|
margin-top: 2em;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
}
|
|
|
|
#progresscontainer {
|
|
/* width: calc(100% - 50px); */
|
|
margin: 25px;
|
|
height: 5em;
|
|
border: 2px solid white;
|
|
}
|
|
|
|
#progress {
|
|
background-color: white;
|
|
/* width: 50%; */
|
|
height: 100%;
|
|
/* transition: width 1s linear; */
|
|
}
|
|
/* main {
|
|
text-align: center;
|
|
padding: 1em;
|
|
max-width: 240px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: #ff3e00;
|
|
text-transform: uppercase;
|
|
font-size: 4em;
|
|
font-weight: 100;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
main {
|
|
max-width: none;
|
|
}
|
|
} */
|
|
</style><script>
|
|
import { start } from "repl";
|
|
|
|
|
|
import { arch } from "os";
|
|
|
|
|
|
import { kMa export let state
|
|
x// Length } frstart_timeom "buffer"
|
|
|
|
import { striroomn export let connarchetopicmeditate
|
|
export let meditateectionc
|
|
expor
|
|
|
|
let game_completedxxx = fa //Derived from other
|
|
// // let internal_sta// teintern $:properties
|
|
let internal_state = game_co$: {
|
|
|
|
if (state !== 'playing'{
|
|
console.log('xxx')logconsole
|
|
[
|
|
) game_completed =
|
|
} falsegame_ }
|
|
|
|
mpleted ? 'completed' : stategame_or lse
|
|
t let p export let rounds
|
|
layersonnectbead_rouned
|
|
beadgify
|
|
ex
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
|
|
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
|
|
]
|
|
|
|
// Could make configurable. Eh.?
|
|
const MEDITATION_SECONDS = 60
|
|
|
|
let game_stagessata
|
|
$ game_stages = []
|
|
if (meditate) game_stages.push{
|
|
label: 'Meditate',
|
|
duration: MEDITATION_SECONDS,
|
|
})
|
|
for (let r = 0; r < rounds; r++) {
|
|
for (let prri = 0p; playersri < rou
|
|
+1 game_stages+1.push({
|
|
label: `Round $#{r} player$ p}`,#{'
|
|
duration: seconds_per_bead,second })
|
|
|
|
const get_current_statstarttime_of_msfsete = let game_durations = 0
|
|
while start_offset_msstart_ofstart;()
|
|
let fo let state.duration * 1000 > offset_ms) {
|
|
return {state, offset_ms}offs
|
|
offset_ms -= state.duration * 1000off
|
|
return {
|
|
state: { label: 'Game complete' }, offset_ms
|
|
} off state: {
|
|
label: 'Game complete',et
|
|
} offsestatestate = game_stages[s]
|
|
if (game_str (let s = 0; s < game_stages.length; s++) {
|
|
}
|
|
igamei=> {
|
|
}
|
|
|
|
pushisgame_
|
|
} ndps; ri++) {
|
|
}
|
|
for (let i = 0} rou MEDIebl(pushgamemedigame_: {
|
|
}
|
|
const get_porst
|
|
const ARCHETOPICS = [
|
|
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
|
|
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopi
|
|
|
|
const get_current_state = (offset_ms) => {
|
|
for (let s = 0; s < game_stages.length; s++) {
|
|
let state = game_stages[s]
|
|
if (state.duration * 1000 > offset_ms) {
|
|
return {state, offset_ms}
|
|
}
|
|
offset_ms -= state.duration * 1000
|
|
}
|
|
return {
|
|
state: { lab0el: 'Game complete', complete// = 'Waiting for game to start'o host to start: true }, offset_ms
|
|
}
|
|
}
|
|
a', 'Future', 'Game', 'Gif
|
|
let state_label //gastate_progress
|
|
// Sadly we can't use internal_state here because it generates a cyclic dependancy.g// 0 to 1
|
|
let xxx = falsealme_state,intern && !game_completedgame_al_stateintern state_offset_msgame_, labelabelt',
|
|
'Histor!game_completedgame_comply', 'Cosmos', 'Tim && xxx === false& e', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
|
|
]
|
|
|
|
atetconnecte
|
|
export let colet ;nnectedd else else {
|
|
state_label = 'Waiting for game to start'
|
|
width }= 0
|
|
|
|
|
|
e: game_stategesgame_x
|
|
=}
|
|
}
|
|
waiting
|
|
$: {
|
|
if (internal_state !== 'playing') { 'pintern po: s state_label = 'Waiting for game to start'
|
|
width = 0
|
|
tate_offset_m// sstatestate.la1000bel
|
|
if (state.c state.(game_completed || completestate_label = 'Waiting for game to st
|
|
width = 0art'state_ ? 100
|
|
: || game_completed)game_&& sta
|
|
state_label= teomplete) state
|
|
width internal_stateinternal= 100 * ogame_completedfame_complf
|
|
g
|
|
if (state.complete) xxx = tr
|
|
|
|
$: {
|
|
console.log('internal state', internal_state)internlogconsole
|
|
} uestate set_ms / (game_completedsame_compltat(eg.duration * 1000) || xxx )offset25035_msoff_otate_labelffrt
|
|
sgame_state = state; state_offset_ms = offset_ms
|
|
offstate_oftlet {state, offset_ms} = get_current_state(Date.now( ) - start_time)
|
|
start_tnowDateget_curplayersvalue
|
|
exportrounds export let second = nulls_per_round
|
|
let value
|
|
|
|
const update_state = async patch => {
|
|
await await `${room}/configure`, '
|
|
|
|
le
|
|
<h4>{state_label}</h4>2state_gt timer
|
|
$: {
|
|
console.log(state, timer)statelogconsole
|
|
if (state === 'playing' && timer == null) {
|
|
timer = setInterconsointernal_le.log('tick')
|
|
|
|
let
|
|
val(() => {
|
|
console.log('tick')logconsole// else if (stateinternal_ !== 'playing' && timer != null) {
|
|
clearInterval(timer)
|
|
timer = nullclearInt }
|
|
}, 1internal_000)00
|
|
setInt// }
|
|
}
|
|
|
|
4:/{
|
|
method: 'POST',
|
|
mode: 'same-origin',
|
|
headers: {
|
|
internal_ 'contengame_completedt-type': 'application/json',
|
|
},
|
|
bowaitingd, v)y: JSOResetN.stringify(pat
|
|
|
|
{[k]: v}co
|
|
const config = k => e => {
|
|
console.log('k', k, {:else if state == 'paused'}
|
|
<button on:click={upd('state', 'playing')}>Resume</button>
|
|
{/if}
|
|
e, e.data, e.va, e.target.va lu e, e.target.typel
|
|
const raw_value = e.target.value+
|
|
co
|
|
{/if}nst value = e.target.type === 'number' ?raw
|
|
_ve.target.type ===
|
|
|
|
const roundish_tox => Math.round(roundx ) / 100
|
|
20(* 100 = ( 'checkbox' ? e.target.checked
|
|
: alue e.taraw_valueget.value|0 :r e.targvalueet.valueuedv
|
|
// update_state({target.value[k]: e.data})k, updaalue)logconsole }
|
|
(e)nst upd = (kk
|
|
// letconst _arr
|
|
// $: round_arrround_:$$:rounds = new Math.max(
|
|
let width = 50
|
|
// setInterval(() => width++, 1000)
|
|
letconst width
|
|
setInterval(() => width++
|
|
<h4>TopicRoom: <archetopicarchiem>{room}</em></h4>, 1000)se <div>{connection} / {state}</div>
|
|
tIn = 5_active_sessions_ player(s) in roomsesacti_0maxinr, 0)ounds).fill()fill.fill().mapfillArray(Arrayt
|
|
MaxLength, v)k(patc => () => update_state(patch)update> hpatc
|
|
|
|
<div id='progresscontainerpr
|
|
<div id='progress' style=':={{w: {width}i=dth: ' (1 min)50%'}}"></div>
|
|
ogress'></div>c> h = ch// )
|
|
) } stri
|
|
<h2>Game structure</h2>
|
|
{#if meditate}
|
|
<di
|
|
<div>
|
|
Total groundish(roundishame length: {((rounds * players +(meditate ? 1 : 0)) * )seconds_per_bead / 60} minutessecondmedit * ${ (includin
|
|
</div>v>Meditation</div>
|
|
{/if}[<medingifyhJSONd// ,sArray(Math.max(maxrou, 0)nds)Arraryademethd: each rround_ar {#each Array(Math.max(playersrounds, 0)pT) as
|
|
<spap+1} n>{</sp an>
|
|
{/each}>< _, i}r as _, i}
|
|
+1 :
|
|
|
|
<div>Round {i}</div>
|
|
{/each}ea fetch(
|
|
|
|
<div i
|
|
/* transition: width 10.21s lin */ear; d='rounds'>
|
|
{#f
|
|
</div> </div }//
|
|
(pat
|
|
update_state({state: 'play// ing'})updach)Archetopic =
|
|
|
|
const start = () => {
|
|
console.log list='archetopics'arch('
|
|
<datalist id='archetopics'>checked
|
|
{#each ARCH ETOPICS as topic}
|
|
<option value={topic}>
|
|
{/each}>/?ARCH<op
|
|
</datalist>cisstart!')logctextonsole }
|
|
archetopicarchetarch
|
|
e_activarchetopicpe_sessions
|
|
<h1>Glass Bead Game
|
|
Timers</h'1>seh',4cs_pe:r<em>_round</em
|
|
{#if state == 'waiting'
|
|
|
|
}
|
|
1 minute mMeditaionate before gam
|
|
|
|
<label>1 minute meditation before game starts
|
|
<input type='chec <div>
|
|
(Total game length: {roundish(
|
|
(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
|
|
)} minutes)
|
|
</div>
|
|
kbox' va (Total game length: {roundish(
|
|
game_stages.reduce((x, s) => x + s.duration, 0)game(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
|
|
)} minutes)
|
|
lue={meditate} on:input={config('meditate')} >
|
|
<!-- <input type='range' value={players} on:submit={config
|
|
('players')} min= + (meditate ? 1 : + + (meditateMEDITATION_SECONDSMEDI ? 1 :
|
|
0))0))1 max=12> -->
|
|
</l + MEDIabel>e stsart upd<butto'checkbox'nmeditate on:click={() => meditateupdate_'state,'({state: 'playing'})start}>Start</but {:else if state pd== 'playing'}
|
|
u<button on':cli,c'k={ <div>
|
|
<label>Number of players
|
|
<input type='number
|
|
'
|
|
|
|
<div>(
|
|
Total game length: {roundish((rounds * players + (meditate ? 1 : 0)) * seconds_per_bead / 60)} min)utes
|
|
</div>
|
|
v
|
|
<lab
|
|
el>Number of <label>Number of rounds
|
|
<input type='number' value={rounds} on:input={config('rounds')} min=1 max=2012>
|
|
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->
|
|
</label>
|
|
|
|
players
|
|
econds per beads S <input type='number' valuseconds_per_beadsecone={players} on:seconds_per_beadinput={config('players')} min=1 max=12 >
|
|
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->
|
|
</label>alue= {players} on:inputblur ={c onfig( min=1 max=12'players ')} min=1 max=12>
|
|
<!-- <input type='range' value={players} on:submit={config(' players')} min=1 max=12> -->
|
|
</label>
|
|
</div>
|
|
|
|
() => up
|
|
|
|
#progresscont/* ainer {
|
|
width: calc(100% */ - 520px);
|
|
height:25 3em;
|
|
border54em: 1px s
|
|
h2eight: 3
|
|
|
|
#progress {
|
|
background-color: white;
|
|
/* background- */color: white;
|
|
|
|
height: 100%;width: 50%;borderwidth: }
|
|
em;olid white;
|
|
marginpadding
|
|
border: 1px solid white;margin: 10px;: }
|
|
date_state({state:rounds 'rpause d'})}>P <!-- <input tyroundspenput='niumber' value={poundslablurryersubmits} on:inpu
|
|
label {
|
|
display: block; }
|
|
t={config('play min=1 max=12ers')}> -->
|
|
aus<!-- e<!-- </butto
|
|
range n>pd
|
|
{ value={submitplayers}:else if state == min=1 m -->ax=12oin'pausconfig( -->'players')in
|
|
configedpl on:input={()}ayinug'}
|
|
ton>
|
|
{:else if state == 'playing'}
|
|
playingresumResumee
|
|
{/ifii{upd(upd() =>
|
|
|
|
<div>
|
|
</div><label>Number of players<input type='n on:input=""on:inpbind:valuumber'></
|
|
</div>label>>/l update_state({state: 'pausedplayiPauseng'})fif#if>xport
|
|
<div> connected
|
|
:else if state == 'playing'#elif {else}elel# {c/onnecti}
|
|
{/ifon} sta
|
|
|
|
<div id='config'>
|
|
<h2>Config</h2>
|
|
<button on:click={start}>Start</button>/""on:cliu
|
|
</div><div id=bar te {st
|
|
#config {
|
|
margin-top: 2em;1m }
|
|
ate</div>>}spa let valueconnection
|
|
let state
|
|
<hh14>Ro state {state}om {
|
|
body {
|
|
background-color: black; }
|
|
|
|
ro/* om} connected {connected}</h1>;
|
|
evaluexpo// rt let state;
|
|
export let state;} from "querystring";
|
|
|
|
export let name;
|
|
export let statename;
|
|
|
|
</script>
|
|
|
|
<main>
|
|
<h1>Hello {name}!</<pre>{JSON.stringify(state)}</pre>stringifyJSONh1>
|
|
<p>Visit the */<a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
|
|
</main>
|
|
|
|
<style>
|
|
main {
|
|
text-align: center;
|
|
padding: 1em;
|
|
max-width: 240px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
color: #ff3e00;
|
|
text-transform: uppercase;
|
|
font-size: 4em;
|
|
font-weight: 100;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
main {
|
|
max-width: none;
|
|
}
|
|
}
|
|
</style><script lang="ts">
|
|
import {onMount} from 'svelte';
|
|
let count: number = 0;
|
|
onMount(() => {
|
|
const interval = setInterval(() => count++, 1000);
|
|
return () => {
|
|
clearInterval(interval);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
:global(body) {
|
|
margin: 0;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
.App {
|
|
text-align: center;
|
|
}
|
|
.App code {
|
|
background: #0002;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
.App p {
|
|
margin: 0.4rem;
|
|
}
|
|
|
|
.App-header {
|
|
background-color: #f9f6f6;
|
|
color: #333;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: calc(10px + 2vmin);
|
|
}
|
|
.App-link {
|
|
color: #ff3e00;
|
|
}
|
|
.App-logo {
|
|
height: 36vmin;
|
|
pointer-events: none;
|
|
margin-bottom: 3rem;
|
|
animation: App-logo-spin infinite 1.6s ease-in-out alternate;
|
|
}
|
|
@keyframes App-logo-spin {
|
|
from {
|
|
transform: scale(1);
|
|
}
|
|
to {
|
|
transform: scale(1.06);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="App">
|
|
<header class="App-header">
|
|
<img src="/logo.svg" class="App-logo" alt="logo" />
|
|
<p>Edit <code>src/App.svelte</code> and save to reload.</p>
|
|
<p>Page has been open for <code>{count}</code> seconds.</p>
|
|
<p>
|
|
<a class="App-link" href="https://svelte.dev" target="_blank" rel="noopener noreferrer">
|
|
Learn Svelte
|
|
</a>
|
|
</p>
|
|
</header>
|
|
</div>
|
|
<script lang="ts">
|
|
import {onMount} from 'svelte';
|
|
let count = 0;
|
|
onMount(() => {
|
|
const interval = setInterval(() => count++, 1000);
|
|
return () => {
|
|
clearInterval(interval);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
:global(body) {
|
|
margin: 0;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
.App {
|
|
text-align: center;
|
|
}
|
|
.App code {
|
|
background: #0002;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
.App p {
|
|
margin: 0.4rem;
|
|
}
|
|
|
|
.App-header {
|
|
background-color: #f9f6f6;
|
|
color: #333;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: calc(10px + 2vmin);
|
|
}
|
|
.App-link {
|
|
color: #ff3e00;
|
|
}
|
|
.App-logo {
|
|
height: 36vmin;
|
|
pointer-events: none;
|
|
margin-bottom: 3rem;
|
|
animation: App-logo-spin infinite 1.6s ease-in-out alternate;
|
|
}
|
|
@keyframes App-logo-spin {
|
|
from {
|
|
transform: scale(1);
|
|
}
|
|
to {
|
|
transform: scale(1.06);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="App">
|
|
<header class="App-header">
|
|
<img src="/logo.svg" class="App-logo" alt="logo" />
|
|
<p>Edit <code>src/App.svelte</code> and save to reload.</p>
|
|
<p>Page has been open for <code>{count}</code> seconds.</p>
|
|
<p>
|
|
<a class="App-link" href="https://svelte.dev" target="_blank" rel="noopener noreferrer">
|
|
Learn Svelte
|
|
</a>
|
|
</p>
|
|
</header>
|
|
</div>
|