Controller: Separate game and view loops

This commit is contained in:
LoveEevee 2019-01-16 15:33:42 +03:00
parent 926b163460
commit 3398791afe
20 changed files with 275 additions and 275 deletions

View File

@ -1,3 +1,31 @@
html,
body{
margin: 0;
width: 100%;
height: 100%;
background: #fe7839;
position: absolute;
user-select: none;
touch-action: none;
overflow: hidden;
}
#screen{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: #000;
background-position: top center;
background-size: 30vh;
font-family: TnT, Meiryo, sans-serif;
}
#screen.pattern-bg{
background-color: #fe7839;
}
#assets,
#browse{
display: none;
}
#loader{
width:90%;
height:10%;

View File

@ -1,28 +1,3 @@
html,
body{
margin: 0;
width: 100%;
height: 100%;
background: #fe7839;
position: absolute;
user-select: none;
touch-action: none;
overflow: hidden;
}
#screen{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: #fe7839;
background-position: top center;
background-size: 30vh;
font-family: TnT, Meiryo, sans-serif;
}
#assets,
#browse{
display: none;
}
.window{
width: 60vmin;
height: 23vmin;
@ -40,40 +15,6 @@ body{
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stroke-main{
font-weight: 300;
}
.result-title{
margin-top: 9px !important;
margin-left: 5px !important;
z-index: 1;
}
.result-song,
.game-song{
position: absolute;
right: 0;
font-size: 5vmin;
margin: 3vmin 3vmin 0px 0px;
color: white;
float: right;
z-index: 1;
font-weight: 300;
}
.stroke-main::before{
content: attr(alt);
left: 0;
z-index: -1;
position: absolute;
-webkit-text-stroke: 0.3em #fb3c0c;
}
.stroke-main::after{
content: attr(alt);
left: 0;
z-index: -2;
position: absolute;
-webkit-text-stroke: 0.5em #000;
}
.stroke-sub::before{
content: attr(alt);
@ -82,44 +23,6 @@ body{
left: 0;
z-index: -1;
}
.don{
background-position-y: 0;
position: absolute;
top: 0px;
}
.alpha-title .song-title-char{
transform: scale(1.3, 1);
font-size: 80%;
line-height: 22px;
}
.song-title-apos{
padding-left: 4px;
}
.song-title-char[alt="ぁ"],
.song-title-char[alt="ぃ"],
.song-title-char[alt="ぅ"],
.song-title-char[alt="ぇ"],
.song-title-char[alt="ぉ"],
.song-title-char[alt="ゃ"],
.song-title-char[alt="ゅ"],
.song-title-char[alt="ょ"],
.song-title-char[alt="っ"],
.song-title-char[alt="ァ"],
.song-title-char[alt="ィ"],
.song-title-char[alt="ゥ"],
.song-title-char[alt="ェ"],
.song-title-char[alt="ォ"],
.song-title-char[alt="ャ"],
.song-title-char[alt="ュ"],
.song-title-char[alt="ョ"],
.song-title-char[alt="ッ"]{
margin-top: -6px;
}
.song-title-char[alt="ー"],
.song-title-char[alt="-"]{
transform: rotate(95deg);
font-size: 90%;
}
#tutorial-outer{
display: flex;
justify-content: center;

View File

@ -6,7 +6,7 @@
]
this.touchEnabled = touchEnabled
loader.changePage("about")
loader.changePage("about", true)
cancelTouch = false
this.endButton = document.getElementById("tutorial-end-button")

View File

@ -1,4 +1,42 @@
var assets = {
"js": [
"lib/fontdetect.min.js",
"loadsong.js",
"parseosu.js",
"titlescreen.js",
"scoresheet.js",
"songselect.js",
"keyboard.js",
"game.js",
"controller.js",
"circle.js",
"view.js",
"mekadon.js",
"gamepad.js",
"tutorial.js",
"soundbuffer.js",
"p2.js",
"canvasasset.js",
"viewassets.js",
"gamerules.js",
"canvasdraw.js",
"canvastest.js",
"canvascache.js",
"parsetja.js",
"about.js",
"debug.js",
"session.js",
"strings.js",
"importsongs.js"
],
"css": [
"main.css",
"titlescreen.css",
"loadsong.css",
"game.css",
"debug.css",
"songbg.css"
],
"img": [
"title-screen.png",
"logo-big.png",

View File

@ -1,7 +1,7 @@
class CanvasAsset{
constructor(view, layer, position){
this.ctx = view.ctx
this.controller = view.controller
this.view = view
this.position = position
this.animationFrames = {}
this.speed = 1000 / 60
@ -13,7 +13,7 @@ class CanvasAsset{
if(this.animation){
var u = (a, b) => typeof a === "undefined" ? b : a
var frame = 0
var ms = this.controller.getElapsedTime()
var ms = this.view.getMS()
var beatInterval = this.frameSpeed ? 1000 / 60 : this.beatInterval
if(this.animationEnd){
@ -95,7 +95,7 @@ class CanvasAsset{
}
changeBeatInterval(beatMS, initial){
if(!initial && !this.frameSpeed){
var ms = this.controller.getElapsedTime()
var ms = this.view.getMS()
this.animationStart = ms - (ms - this.animationStart) / this.beatInterval * beatMS
}
this.beatInterval = beatMS

View File

@ -7,6 +7,10 @@ class Controller{
this.touchEnabled = touchEnabled
this.snd = this.multiplayer ? "_p" + this.multiplayer : ""
if(this.multiplayer !== 2){
loader.changePage("game", false)
}
if(selectedSong.type === "tja"){
this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.offset)
}else{
@ -47,27 +51,48 @@ class Controller{
})
}
startMainLoop(){
this.mainLoopStarted = false
this.mainLoopRunning = true
this.mainLoop()
this.gameLoop()
this.viewLoop()
this.gameInterval = setInterval(this.gameLoop.bind(this), 1000 / 60)
}
stopMainLoop(){
this.mainLoopRunning = false
this.mainAsset.stop()
clearInterval(this.gameInterval)
}
mainLoop(){
gameLoop(){
if(this.mainLoopRunning){
if(this.multiplayer !== 2){
requestAnimationFrame(() => {
if(this.syncWith){
this.syncWith.game.elapsedTime = this.game.elapsedTime
this.syncWith.game.startDate = this.game.startDate
}
this.mainLoop()
if(this.syncWith){
this.syncWith.mainLoop()
}
var ms = this.game.elapsedTime
this.keyboard.checkMenuKeys()
if(!this.game.isPaused()){
this.keyboard.checkGameKeys()
if(ms < 0){
this.game.updateTime()
}else{
this.game.update()
if(!this.mainLoopRunning){
return
}
this.game.playMainMusic()
}
}
}
}
viewLoop(){
if(this.mainLoopRunning){
if(this.multiplayer !== 2){
requestAnimationFrame(() => {
this.viewLoop()
if(this.syncWith){
this.syncWith.viewLoop()
}
if(this.scoresheet){
if(this.view.ctx){
this.view.ctx.save()
@ -80,27 +105,7 @@ class Controller{
}
})
}
var ms = this.game.elapsedTime
if(!this.game.isPaused()){
this.keyboard.checkGameKeys()
if(ms >= 0 && !this.mainLoopStarted){
this.mainLoopStarted = true
}
if(ms < 0){
this.game.updateTime()
}
if(this.mainLoopStarted){
this.game.update()
if(!this.mainLoopRunning){
return
}
this.game.playMainMusic()
}
}
this.view.refresh()
this.keyboard.checkMenuKeys()
}
}
gameEnded(){
@ -130,7 +135,6 @@ class Controller{
if(!fadeIn){
this.clean()
}
loader.screen.classList.remove("view")
new SongSelect(false, fadeIn, this.touchEnabled)
}
restartSong(){
@ -138,7 +142,6 @@ class Controller{
if(this.multiplayer){
new LoadSong(this.selectedSong, false, true, this.touchEnabled)
}else{
loader.changePage("game")
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
}

View File

@ -64,7 +64,8 @@ class Game{
updateCirclesStatus(){
var nextSet = false
var circles = this.songData.circles
for(var i in circles){
var startIndex = this.currentCircle === 0 ? 0 : this.currentCircle - 1
for(var i = startIndex; i < circles.length && i < this.currentCircle + 2; i++){
var circle = circles[i]
if(!circle.getPlayed()){
var ms = this.elapsedTime

View File

@ -28,21 +28,23 @@ class Gamepad{
if(callback){
this.interval = setInterval(() => {
this.play(callback)
}, 100)
}, 1000 / 60)
}
}
play(callback){
if(pageEvents.lastKeyEvent + 5000 > Date.now()){
return
}
if("getGamepads" in navigator){
var gamepads = navigator.getGamepads()
if(gamepads.length === 0){
return
}
}else{
return
}
if(pageEvents.lastKeyEvent + 5000 > Date.now()){
return
}
var bindings = this.bindings
var force = {
lsu: false,
lsr: false,

View File

@ -35,6 +35,7 @@ class Keyboard{
gameBtn[this.kbd["ka_l"]] = ["lb", "lt"]
gameBtn[this.kbd["ka_r"]] = ["rb", "rt"]
this.gamepad = new Gamepad(gameBtn)
this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2)
var menuBtn = {
"cancel": ["a"],
@ -84,23 +85,25 @@ class Keyboard{
return true
}
checkGameKeys(){
if(!this.controller.autoPlayEnabled){
var ms = this.game.getAccurateTime()
if(this.controller.autoPlayEnabled){
this.checkKeySound(this.kbd["don_l"], "don")
this.checkKeySound(this.kbd["don_r"], "don")
this.checkKeySound(this.kbd["ka_l"], "ka")
this.checkKeySound(this.kbd["ka_r"], "ka")
}
}
gamepadKeys(){
if(!this.game.isPaused() && !this.controller.autoPlayEnabled){
this.gamepad.play((pressed, keyCode) => {
if(pressed){
if(this.keys[keyCode]){
this.setKey(keyCode, false)
}
this.setKey(keyCode, true, ms)
this.setKey(keyCode, true, this.game.getAccurateTime())
}else{
this.setKey(keyCode, false)
}
})
}else{
this.checkKeySound(this.kbd["don_l"], "don")
this.checkKeySound(this.kbd["don_r"], "don")
this.checkKeySound(this.kbd["ka_l"], "ka")
this.checkKeySound(this.kbd["ka_r"], "ka")
}
}
checkMenuKeys(){
@ -239,5 +242,6 @@ class Keyboard{
}
clean(){
pageEvents.keyRemove(this, "all")
clearInterval(this.gamepadInterval)
}
}

View File

@ -3,18 +3,92 @@ class Loader{
this.callback = callback
this.loadedAssets = 0
this.assetsDiv = document.getElementById("assets")
this.canvasTest = new CanvasTest()
this.screen = document.getElementById("screen")
this.startTime = Date.now()
this.ajax("/src/views/loader.html").then(this.run.bind(this))
}
run(page){
this.promises = []
this.screen = document.getElementById("screen")
var promises = []
promises.push(this.ajax("/src/views/loader.html").then(page => {
this.screen.innerHTML = page
}))
promises.push(this.ajax("/api/config").then(conf => {
gameConfig = JSON.parse(conf)
}))
Promise.all(promises).then(this.run.bind(this))
}
run(){
this.promises = []
this.loaderPercentage = document.querySelector("#loader .percentage")
this.loaderProgress = document.querySelector("#loader .progress")
var queryString = gameConfig._version ? "?" + gameConfig._version.commit_short : ""
assets.js.forEach(name => {
var script = document.createElement("script")
this.addPromise(pageEvents.load(script))
script.src = "/src/js/" + name + queryString
document.head.appendChild(script)
})
this.addPromise(new Promise(resolve => {
var cssCount = document.styleSheets.length + assets.css.length
assets.css.forEach(name => {
var stylesheet = document.createElement("link")
stylesheet.rel = "stylesheet"
stylesheet.href = "/src/css/" + name + queryString
document.head.appendChild(stylesheet)
})
var checkStyles = () => {
if(document.styleSheets.length >= cssCount){
resolve()
clearInterval(interval)
}
}
var interval = setInterval(checkStyles, 100)
checkStyles()
}))
assets.fonts.forEach(name => {
var font = document.createElement("h1")
font.style.fontFamily = name
font.appendChild(document.createTextNode("I am a font"))
this.assetsDiv.appendChild(font)
})
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
this.addPromise(pageEvents.load(image))
image.id = name
image.src = gameConfig.assets_baseurl + "img/" + name
this.assetsDiv.appendChild(image)
assets.image[id] = image
})
assets.views.forEach(name => {
var id = this.getFilename(name)
this.addPromise(this.ajax("/src/views/" + name + queryString).then(page => {
assets.pages[id] = page
}))
})
this.addPromise(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault
}))
this.afterJSCount =
[assets.audioOgg, "blurPerformance", "P2Connection"].length +
assets.fonts.length +
assets.audioSfx.length +
assets.audioMusic.length +
assets.audioSfxLR.length +
assets.audioSfxLoud.length
Promise.all(this.promises).then(() => {
snd.buffer = new SoundBuffer()
snd.musicGain = snd.buffer.createGain()
snd.sfxGain = snd.buffer.createGain()
@ -29,66 +103,41 @@ class Loader{
)
snd.sfxLoudGain.setVolume(1.2)
this.promises.push(this.ajax("/api/config").then(conf => {
gameConfig = JSON.parse(conf)
this.afterJSCount--
snd.buffer.load(gameConfig.assets_baseurl + "audio/" + assets.audioOgg).then(() => {
this.addPromise(snd.buffer.load(gameConfig.assets_baseurl + "audio/" + assets.audioOgg).then(() => {
this.oggNotSupported = false
}, () => {
this.oggNotSupported = true
}).then(() => {
this.afterJSCount = 0
assets.fonts.forEach(name => {
var font = document.createElement("h1")
font.style.fontFamily = name
font.appendChild(document.createTextNode("I am a font"))
this.assetsDiv.appendChild(font)
this.promises.push(new Promise((resolve, reject) => {
FontDetect.onFontLoaded(name, resolve, reject, {msTimeout: 90000})
this.addPromise(new Promise(resolve => {
FontDetect.onFontLoaded(name, resolve, resolve, {msTimeout: Infinity})
}))
})
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
this.promises.push(pageEvents.load(image))
image.id = name
image.src = gameConfig.assets_baseurl + "img/" + name
this.assetsDiv.appendChild(image)
assets.image[id] = image
})
assets.audioSfx.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxGain))
this.addPromise(this.loadSound(name, snd.sfxGain))
})
assets.audioMusic.forEach(name => {
this.promises.push(this.loadSound(name, snd.musicGain))
this.addPromise(this.loadSound(name, snd.musicGain))
})
assets.audioSfxLR.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxGain).then(sound => {
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
var id = this.getFilename(name)
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
}))
})
assets.audioSfxLoud.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxLoudGain))
this.addPromise(this.loadSound(name, snd.sfxLoudGain))
})
this.promises.push(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault
}))
assets.views.forEach(name => {
var id = this.getFilename(name)
var qs = gameConfig._version ? '?' + gameConfig._version.commit_short : '?'
this.promises.push(this.ajax("/src/views/" + name + qs).then(page => {
assets.pages[id] = page
}))
})
this.promises.push(this.canvasTest.blurPerformance().then(result => {
this.canvasTest = new CanvasTest()
this.addPromise(this.canvasTest.blurPerformance().then(result => {
perf.blur = result
if(result > 1000 / 50){
// Less than 50 fps with blur enabled
@ -96,8 +145,10 @@ class Loader{
}
}))
p2 = new P2Connection()
if(location.hash.length === 6){
this.promises.push(new Promise(resolve => {
p2.hashLock = true
this.addPromise(new Promise(resolve => {
p2.open()
pageEvents.add(p2, "message", response => {
if(response.type === "session"){
@ -119,12 +170,10 @@ class Loader{
}).then(() => {
pageEvents.remove(p2, "message")
}))
}else{
p2.hash("")
}
this.promises.forEach(promise => {
promise.then(this.assetLoaded.bind(this))
})
Promise.all(this.promises).then(() => {
this.canvasTest.drawAllImages().then(result => {
perf.allImg = result
@ -135,10 +184,14 @@ class Loader{
})
}, this.errorMsg.bind(this))
})
}))
})
}
addPromise(promise){
this.promises.push(promise)
promise.then(this.assetLoaded.bind(this))
}
loadSound(name, gain){
if(this.oggNotSupported && name.endsWith(".ogg")){
name = name.slice(0, -4) + ".wav"
@ -161,13 +214,14 @@ class Loader{
assetLoaded(){
if(!this.error){
this.loadedAssets++
var percentage = Math.floor(this.loadedAssets * 100 / this.promises.length)
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.loaderProgress.style.width = percentage + "%"
this.loaderPercentage.firstChild.data = percentage + "%"
}
}
changePage(name){
changePage(name, patternBg){
this.screen.innerHTML = assets.pages[name]
this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg")
}
ajax(url, customRequest){
return new Promise((resolve, reject) => {

View File

@ -5,7 +5,7 @@ class LoadSong{
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
loader.changePage("loadsong")
loader.changePage("loadsong", true)
var loadingText = document.getElementById("loading-text")
loadingText.appendChild(document.createTextNode(strings.loading))
loadingText.setAttribute("alt", strings.loading)
@ -233,7 +233,6 @@ class LoadSong{
}else if(event.type === "gamestart"){
this.clean()
p2.clearMessage("songsel")
loader.changePage("game")
var taikoGame1 = new Controller(song, this.songData, false, 1, this.touchEnabled)
var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled)
taikoGame1.run(taikoGame2)
@ -248,7 +247,6 @@ class LoadSong{
})
}else{
this.clean()
loader.changePage("game")
var taikoGame = new Controller(song, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
}

View File

@ -58,11 +58,16 @@ function debug(){
}
var root = document.documentElement
var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root
if(/iPhone|iPad/.test(navigator.userAgent)){
var fullScreenSupported = false
}else{
var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root
}
var pageEvents = new PageEvents()
var snd = {}
var p2 = new P2Connection()
var p2
var disableBlur = false
var cancelTouch = true
var lastHeight
@ -104,11 +109,6 @@ pageEvents.keyAdd(debugObj, "all", "down", event => {
debugObj.controller.restartSong()
}
})
if(location.hash.length === 6){
p2.hashLock = true
}else{
p2.hash("")
}
var loader = new Loader(() => {
new Titlescreen()

View File

@ -290,7 +290,7 @@ class ParseOsu{
var extras = values.slice(this.osu.EXTRAS)
var distance = parseFloat(extras[this.osu.PIXELLENGTH])
var distance = parseFloat(extras[this.osu.PIXELLENGTH]) * parseFloat(extras[this.osu.REPEAT])
var velocity = this.difficulty.sliderMultiplier * speed / 10
var endTime = start + distance / velocity

View File

@ -493,7 +493,7 @@ class Scoresheet{
fontSize: 29,
fontFamily: this.font,
align: "right",
width: 215,
width: 154,
letterSpacing: 1
}, [
{outline: "#000", letterBorder: 8},

View File

@ -1,7 +1,7 @@
class Session{
constructor(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("session")
loader.changePage("session", true)
this.endButton = document.getElementById("tutorial-end-button")
if(touchEnabled){
document.getElementById("tutorial-outer").classList.add("touch-enabled")

View File

@ -2,7 +2,7 @@ class SongSelect{
constructor(fromTutorial, fadeIn, touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("songselect")
loader.changePage("songselect", false)
this.canvas = document.getElementById("song-sel-canvas")
this.ctx = this.canvas.getContext("2d")

View File

@ -1,6 +1,6 @@
class Titlescreen{
constructor(){
loader.changePage("titlescreen")
loader.changePage("titlescreen", false)
this.titleScreen = document.getElementById("title-screen")
var proceed = document.getElementById("title-proceed")
proceed.appendChild(document.createTextNode(strings.titleProceed))

View File

@ -1,7 +1,7 @@
class Tutorial{
constructor(fromSongSel){
this.fromSongSel = fromSongSel
loader.changePage("tutorial")
loader.changePage("tutorial", true)
assets.sounds["bgm_setsume"].playLoop(0.1, false, 0, 1.054, 16.054)
this.endButton = document.getElementById("tutorial-end-button")

View File

@ -124,7 +124,7 @@
}
this.setDonBg()
this.lastMousemove = this.controller.getElapsedTime()
this.lastMousemove = this.controller.game.getAccurateTime()
pageEvents.mouseAdd(this, this.onmousemove.bind(this))
this.refresh()
@ -180,7 +180,10 @@
}
winW /= ratio
winH /= ratio
var ms = this.getMS()
if(!this.controller.game.paused){
this.ms = this.controller.game.getAccurateTime()
}
var ms = this.ms
if(this.portrait){
var frameTop = winH / 2 - 1280 / 2
@ -978,7 +981,6 @@
}else{
var catId = this.categories.default.sort
}
loader.screen.classList.add("view")
if(!selectedSong.songSkin.song){
var id = selectedSong.songBg
@ -1100,7 +1102,7 @@
}
drawCircles(circles){
var distanceForCircle = this.winW / this.ratio - this.slotPos.x
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
for(var i = circles.length; i--;){
var circle = circles[i]
@ -1127,7 +1129,7 @@
}
}
drawAnimatedCircles(circles){
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
for(var i = 0; i < circles.length; i++){
var circle = circles[i]
@ -1174,7 +1176,7 @@
var fill, size, faceID
var type = circle.getType()
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
var circleMs = circle.getMS()
var endTime = circle.getEndTime()
var animated = circle.isAnimated()
@ -1458,7 +1460,7 @@
&& animation !== "gogo"
){
don.setAnimation("10combo")
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
don.setAnimationStart(ms)
var length = don.getAnimationLength("normal")
don.setUpdateSpeed(4 / length)
@ -1677,7 +1679,7 @@
this.assets.changeBeatInterval(beatMS)
}
getMS(){
return this.controller.getElapsedTime()
return this.ms
}
clean(){
this.draw.clean()

View File

@ -7,53 +7,20 @@
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="description" content="パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers">
<link rel="stylesheet" href="/src/css/main.css?{{version.commit_short}}"/>
<link rel="stylesheet" href="/src/css/loader.css?{{version.commit_short}}">
<link rel="stylesheet" href="/src/css/titlescreen.css?{{version.commit_short}}">
<link rel="stylesheet" href="/src/css/loadsong.css?{{version.commit_short}}">
<link rel="stylesheet" href="/src/css/game.css?{{version.commit_short}}">
<link rel="stylesheet" href="/src/css/debug.css?{{version.commit_short}}">
<link rel="stylesheet" href="/src/css/songbg.css?{{version.commit_short}}">
<link rel="stylesheet" href="{{config.assets_baseurl}}fonts/fonts.css?{{version.commit_short}}">
<link rel="stylesheet" href="{{config.assets_baseurl}}img/img.css?{{version.commit_short}}">
<script src="/src/js/lib/fontdetect.min.js?{{version.commit_short}}"></script>
<script src="/src/js/assets.js?{{version.commit_short}}"></script>
<script src="/src/js/loadsong.js?{{version.commit_short}}"></script>
<script src="/src/js/parseosu.js?{{version.commit_short}}"></script>
<script src="/src/js/titlescreen.js?{{version.commit_short}}"></script>
<script src="/src/js/scoresheet.js?{{version.commit_short}}"></script>
<script src="/src/js/songselect.js?{{version.commit_short}}"></script>
<script src="/src/js/keyboard.js?{{version.commit_short}}"></script>
<script src="/src/js/game.js?{{version.commit_short}}"></script>
<script src="/src/js/controller.js?{{version.commit_short}}"></script>
<script src="/src/js/circle.js?{{version.commit_short}}"></script>
<script src="/src/js/view.js?{{version.commit_short}}"></script>
<script src="/src/js/mekadon.js?{{version.commit_short}}"></script>
<script src="/src/js/gamepad.js?{{version.commit_short}}"></script>
<script src="/src/js/tutorial.js?{{version.commit_short}}"></script>
<script src="/src/js/soundbuffer.js?{{version.commit_short}}"></script>
<script src="/src/js/p2.js?{{version.commit_short}}"></script>
<script src="/src/js/canvasasset.js?{{version.commit_short}}"></script>
<script src="/src/js/pageevents.js?{{version.commit_short}}"></script>
<script src="/src/js/viewassets.js?{{version.commit_short}}"></script>
<script src="/src/js/gamerules.js?{{version.commit_short}}"></script>
<script src="/src/js/canvasdraw.js?{{version.commit_short}}"></script>
<script src="/src/js/loader.js?{{version.commit_short}}"></script>
<script src="/src/js/canvastest.js?{{version.commit_short}}"></script>
<script src="/src/js/canvascache.js?{{version.commit_short}}"></script>
<script src="/src/js/parsetja.js?{{version.commit_short}}"></script>
<script src="/src/js/about.js?{{version.commit_short}}"></script>
<script src="/src/js/debug.js?{{version.commit_short}}"></script>
<script src="/src/js/session.js?{{version.commit_short}}"></script>
<script src="/src/js/strings.js?{{version.commit_short}}"></script>
<script src="/src/js/importsongs.js?{{version.commit_short}}"></script>
</head>
<body>
<div id="assets"></div>
<div id="screen"></div>
<div id="screen" class="orange-bg"></div>
<div id="version">
{% if version %}
<a href="https://github.com/bui/taiko-web/commit/{{version.commit}}" target="_blank" id="version-link" class="stroke-sub" alt="taiko-web ver.{{version.version}} ({{version.commit_short}})">taiko-web ver.{{version.version}} ({{version.commit_short}})</a>