ImportSongs: Add plugin support

- Files with filenames that end with .taikoweb.js can be imported and run to add custom functionality to the game
- The plugin file is a javascript module script that should have a class in the default export
- Currently supported methods in the class: name (string), load, start, stop, unload (functions)
- The class can be extended from the Patch class to add automatic patching of variables and functions
- Here are some of the plugins I made: https://github.com/KatieFrogs/taiko-web-plugins
This commit is contained in:
KatieFrogs 2022-02-11 17:28:22 +03:00
parent fd114d9f69
commit 1db4eb6710
43 changed files with 803 additions and 193 deletions

View File

@ -1,5 +1,8 @@
class About{
constructor(touchEnabled){
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("about", true)
cancelTouch = false

View File

@ -20,7 +20,10 @@ function filePermission(file){
})
}
class RemoteFile{
constructor(url){
constructor(...args){
this.init(...args)
}
init(url){
this.url = url
try{
this.path = new URL(url).pathname
@ -53,7 +56,10 @@ class RemoteFile{
}
}
class LocalFile{
constructor(file, path){
constructor(...args){
this.init(...args)
}
init(file, path){
this.file = file
this.path = path || file.webkitRelativePath
this.url = this.path
@ -70,7 +76,10 @@ class LocalFile{
}
}
class FilesystemFile{
constructor(file, path){
constructor(...args){
this.init(...args)
}
init(file, path){
this.file = file
this.path = path
this.url = this.path
@ -87,7 +96,10 @@ class FilesystemFile{
}
}
class GdriveFile{
constructor(fileObj){
constructor(...args){
this.init(...args)
}
init(fileObj){
this.path = fileObj.path
this.name = fileObj.name
this.id = fileObj.id
@ -108,7 +120,10 @@ class GdriveFile{
}
}
class CachedFile{
constructor(contents, oldFile){
constructor(...args){
this.init(...args)
}
init(contents, oldFile){
this.contents = contents
this.oldFile = oldFile
this.path = oldFile.path

View File

@ -1,5 +1,8 @@
class Account{
constructor(touchEnabled){
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
cancelTouch = false
this.locked = false

View File

@ -36,7 +36,8 @@ var assets = {
"lyrics.js",
"customsongs.js",
"abstractfile.js",
"idb.js"
"idb.js",
"plugins.js"
],
"css": [
"main.css",

View File

@ -1,5 +1,8 @@
class AutoScore {
constructor(difficulty, level, scoremode, circles) {
constructor(...args){
this.init(...args)
}
init(difficulty, level, scoremode, circles) {
this.scoremode = scoremode;
this.circles = circles;
this.basic_max_score_list = {

View File

@ -1,5 +1,8 @@
class CanvasAsset{
constructor(view, layer, position){
constructor(...args){
this.init(...args)
}
init(view, layer, position){
this.ctx = view.ctx
this.view = view
this.position = position

View File

@ -1,5 +1,8 @@
class CanvasCache{
constructor(noSmoothing, w, h, scale){
constructor(...args){
this.init(...args)
}
init(noSmoothing, w, h, scale){
this.noSmoothing = noSmoothing
if(w){
this.resize(w, h, scale)

View File

@ -1,5 +1,8 @@
class CanvasDraw{
constructor(noSmoothing){
constructor(...args){
this.init(...args)
}
init(noSmoothing){
this.diffStarPath = new Path2D(vectors.diffStar)
this.longVowelMark = new Path2D(vectors.longVowelMark)

View File

@ -1,5 +1,8 @@
class CanvasTest{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.canvas = document.createElement("canvas")
var pixelRatio = window.devicePixelRatio || 1
var width = innerWidth * pixelRatio

View File

@ -1,5 +1,8 @@
class Circle{
constructor(config){
constructor(...args){
this.init(...args)
}
init(config){
this.id = config.id
this.ms = config.start
this.originalMS = this.ms

View File

@ -1,5 +1,8 @@
class Controller{
constructor(selectedSong, songData, autoPlayEnabled, multiplayer, touchEnabled){
constructor(...args){
this.init(...args)
}
init(selectedSong, songData, autoPlayEnabled, multiplayer, touchEnabled){
this.selectedSong = selectedSong
this.songData = songData
this.autoPlayEnabled = autoPlayEnabled

View File

@ -1,5 +1,8 @@
class CustomSongs{
constructor(touchEnabled, noPage){
constructor(...args){
this.init(...args)
}
init(touchEnabled, noPage){
this.loaderDiv = document.createElement("div")
this.loaderDiv.innerHTML = assets.pages["loadsong"]
var loadingText = this.loaderDiv.querySelector("#loading-text")
@ -151,8 +154,10 @@ class CustomSongs{
this.changeSelected(this.linkLocalFolder)
if(typeof showDirectoryPicker === "function"){
return showDirectoryPicker().then(file => {
this.walkFilesystem(file).then(files => this.importLocal(files)).then(e => {
this.walkFilesystem(file).then(files => this.importLocal(files)).then(input => {
if(input){
db.setItem("customFolder", [file])
}
}).catch(e => {
if(e !== "cancel"){
return Promise.reject(e)
@ -217,8 +222,8 @@ class CustomSongs{
}))
}
}
Promise.all(dropPromises).then(() => this.importLocal(allFiles)).then(() => {
if(dbItems.length){
Promise.all(dropPromises).then(() => this.importLocal(allFiles)).then(input => {
if(input && dbItems.length){
db.setItem("customFolder", dbItems)
}
})
@ -265,6 +270,7 @@ class CustomSongs{
}else if(e !== "cancel"){
return Promise.reject(e)
}
return false
})
}
gdriveFolder(event){
@ -387,6 +393,7 @@ class CustomSongs{
new SongSelect("customSongs", false, this.touchEnabled)
pageEvents.send("import-songs", length)
}, 500)
return songs && songs.length
}
keyPressed(pressed, name){
if(!pressed || this.locked){

View File

@ -1,5 +1,8 @@
class Debug{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
if(!assets.pages["debug"]){
return
}
@ -329,7 +332,10 @@ class Debug{
}
}
class InputSlider{
constructor(sliderDiv, min, max, fixedPoint){
constructor(...args){
this.init(...args)
}
init(sliderDiv, min, max, fixedPoint){
this.fixedPoint = fixedPoint
this.mul = Math.pow(10, fixedPoint)
this.min = min * this.mul

View File

@ -1,5 +1,8 @@
class Game{
constructor(controller, selectedSong, songData){
constructor(...args){
this.init(...args)
}
init(controller, selectedSong, songData){
this.controller = controller
this.selectedSong = selectedSong
this.songData = songData

View File

@ -1,5 +1,8 @@
class GameInput{
constructor(controller){
constructor(...args){
this.init(...args)
}
init(controller){
this.controller = controller
this.game = this.controller.game

View File

@ -1,5 +1,8 @@
class Gamepad{
constructor(bindings, callback){
constructor(...args){
this.init(...args)
}
init(bindings, callback){
this.bindings = bindings
this.callback = !!callback
this.b = {

View File

@ -1,5 +1,8 @@
class GameRules{
constructor(game){
constructor(...args){
this.init(...args)
}
init(game){
this.difficulty = game.controller.selectedSong.difficulty
var frame = 1000 / 60

View File

@ -1,5 +1,8 @@
class Gpicker{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.apiKey = gameConfig.google_credentials.api_key
this.oauthClientId = gameConfig.google_credentials.oauth_client_id
this.projectNumber = gameConfig.google_credentials.project_number

View File

@ -1,9 +1,12 @@
class IDB{
constructor(name, store){
constructor(...args){
this.init(...args)
}
init(name, store){
this.name = name
this.store = store
}
init(){
start(){
if(this.db){
return Promise.resolve(this.db)
}
@ -31,7 +34,7 @@ class IDB{
})
}
transaction(method, ...args){
return this.init().then(db =>
return this.start().then(db =>
db.transaction(this.store, "readwrite").objectStore(this.store)[method](...args)
).then(this.promise.bind(this))
}

View File

@ -1,12 +1,19 @@
class ImportSongs{
constructor(limited, otherFiles){
constructor(...args){
this.init(...args)
}
init(limited, otherFiles, noPlugins, pluginAmount){
this.limited = limited
this.tjaFiles = []
this.osuFiles = []
this.assetFiles = {}
this.pluginFiles = []
this.otherFiles = otherFiles || {}
this.noPlugins = noPlugins
this.pluginAmount = pluginAmount
this.songs = []
this.stylesheet = []
this.plugins = []
this.songTitle = this.otherFiles.songTitle || {}
this.uraRegex = /\s*[\(]裏[\)]$/
this.courseTypes = {
@ -77,11 +84,48 @@
if(!(name in this.assetFiles)){
this.assetFiles[name] = file
}
}else if(name.endsWith(".taikoweb.js")){
this.pluginFiles.push({
file: file,
index: i
})
}else{
this.otherFiles[path] = file
}
}
if(!this.noPlugins && this.pluginFiles.length){
var pluginPromises = []
this.pluginFiles.forEach(fileObj => {
pluginPromises.push(this.addPlugin(fileObj).catch(e => console.warn(e)))
})
return Promise.all(pluginPromises).then(() => {
var startPromises = []
var pluginAmount = 0
if(this.plugins.length && confirm(strings.plugins.warning.replace("%s",
strings.plugins.plugin[strings.plural.select(this.plugins.length)].replace("%s",
this.plugins.length.toString()
)
))){
this.plugins.forEach(obj => {
var plugin = plugins.add(obj.data, obj.name)
if(plugin){
pluginAmount++
plugins.imported.push({
name: plugin.name,
plugin: plugin
})
startPromises.push(plugin.start())
}
})
}
return Promise.all(startPromises).then(() => {
var importSongs = new ImportSongs(this.limited, this.otherFiles, true, pluginAmount)
return importSongs.load(files)
})
})
}
var metaPromises = []
metaFiles.forEach(fileObj => {
metaPromises.push(this.addMeta(fileObj))
@ -468,6 +512,18 @@
return name.slice(0, name.lastIndexOf("."))
}
addPlugin(fileObj){
var file = fileObj.file
var filePromise = file.read()
return filePromise.then(dataRaw => {
var name = file.name.slice(0, file.name.lastIndexOf(".taikoweb.js"))
this.plugins.push({
name: name,
data: dataRaw
})
})
}
getCategory(file, exclude){
var path = file.path.toLowerCase().split("/")
for(var i = path.length - 2; i >= 0; i--){
@ -543,11 +599,13 @@
assets.otherFiles.songTitle = this.songTitle
}
return Promise.resolve(this.songs)
}else if(Object.keys(this.assetFiles).length){
}else{
if(this.noPlugins && this.pluginAmount || Object.keys(this.assetFiles).length){
return Promise.resolve()
}else{
return Promise.reject("nosongs")
}
}
this.clean()
}

View File

@ -1,5 +1,8 @@
class Keyboard{
constructor(bindings, callback){
constructor(...args){
this.init(...args)
}
init(bindings, callback){
this.bindings = bindings
this.callback = callback
this.wildcard = false

View File

@ -1,5 +1,8 @@
class Loader{
constructor(callback){
constructor(...args){
this.init(...args)
}
init(callback){
this.callback = callback
this.loadedAssets = 0
this.assetsDiv = document.getElementById("assets")
@ -253,6 +256,7 @@ class Loader{
pageEvents.setKbd()
scoreStorage = new ScoreStorage()
db = new IDB("taiko", "store")
plugins = new Plugins()
Promise.all(this.promises).then(() => {
if(this.error){

View File

@ -1,5 +1,8 @@
class LoadSong{
constructor(selectedSong, autoPlayEnabled, multiplayer, touchEnabled){
constructor(...args){
this.init(...args)
}
init(selectedSong, autoPlayEnabled, multiplayer, touchEnabled){
this.selectedSong = selectedSong
this.autoPlayEnabled = autoPlayEnabled
this.multiplayer = multiplayer

View File

@ -1,5 +1,8 @@
class Logo{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.canvas = document.getElementById("logo")
this.ctx = this.canvas.getContext("2d")
this.pathSvg = failedTests.indexOf("Path2D SVG") === -1 && vectors.logo1

View File

@ -1,5 +1,8 @@
class Lyrics{
constructor(file, songOffset, div, parsed){
constructor(...args){
this.init(...args)
}
init(file, songOffset, div, parsed){
this.div = div
this.stroke = document.createElement("div")
this.stroke.classList.add("stroke")

View File

@ -91,6 +91,7 @@ var scoreStorage
var account = {}
var gpicker
var db
var plugins
pageEvents.add(root, ["touchstart", "touchmove", "touchend"], event => {
if(event.cancelable && cancelTouch && event.target.tagName !== "SELECT"){

View File

@ -1,5 +1,8 @@
class Mekadon{
constructor(controller, game){
constructor(...args){
this.init(...args)
}
init(controller, game){
this.controller = controller
this.game = game
this.lr = false

View File

@ -1,5 +1,8 @@
class P2Connection{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.closed = true
this.lastMessages = {}
this.otherConnected = false

View File

@ -1,5 +1,8 @@
class PageEvents{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.allEvents = new Map()
this.keyListeners = new Map()
this.mouseListeners = new Map()

View File

@ -1,5 +1,8 @@
class ParseOsu{
constructor(fileContent, difficulty, stars, offset, metaOnly){
constructor(...args){
this.init(...args)
}
init(fileContent, difficulty, stars, offset, metaOnly){
this.osu = {
OFFSET: 0,
MSPERBEAT: 1,

View File

@ -1,5 +1,8 @@
class ParseTja{
constructor(file, difficulty, stars, offset, metaOnly){
constructor(...args){
this.init(...args)
}
init(file, difficulty, stars, offset, metaOnly){
this.data = []
for(let line of file){
var indexComment = line.indexOf("//")

342
public/src/js/plugins.js Normal file
View File

@ -0,0 +1,342 @@
class Plugins{
constructor(...args){
this.init(...args)
}
init(){
this.imported = []
this.allPlugins = []
this.pluginMap = {}
this.hashes = []
}
add(script, name){
var hash = md5.base64(script.toString())
if(this.hashes.indexOf(hash) !== -1){
console.warn("Skip adding an already addded plugin: " + name)
return
}
name = name || "plugin"
var baseName = name
for(var i = 2; name in this.allPlugins; i++){
name = baseName + i.toString()
}
var plugin = new PluginLoader(script, name, hash)
this.allPlugins.push({
name: name,
plugin: plugin
})
this.pluginMap[name] = plugin
this.hashes.push(hash)
return plugin
}
remove(name){
var hash = this.pluginMap[name].hash
if(hash){
var index = this.hashes.indexOf(hash)
if(index !== -1){
this.hashes.splice(index, 1)
}
}
this.unload(name)
var index = this.imported.findIndex(obj => obj.name === name)
if(index !== -1){
this.imported.splice(index, 1)
}
var index = this.allPlugins.findIndex(obj => obj.name === name)
if(index !== -1){
this.allPlugins.splice(index, 1)
}
delete this.pluginMap[name]
}
load(name){
this.pluginMap[name].load()
}
loadAll(){
for(var i = 0; i < this.allPlugins.length; i++){
this.allPlugins[i].plugin.load()
}
}
start(name){
this.pluginMap[name].start()
}
startAll(){
for(var i = 0; i < this.allPlugins.length; i++){
this.allPlugins[i].plugin.start()
}
}
stop(name){
this.pluginMap[name].stop()
}
stopAll(){
for(var i = this.allPlugins.length; i--;){
this.allPlugins[i].plugin.stop()
}
}
unload(name){
this.pluginMap[name].unload()
}
unloadAll(){
for(var i = this.allPlugins.length; i--;){
this.allPlugins[i].plugin.unload()
}
}
unloadImported(){
for(var i = this.imported.length; i--;){
this.imported[i].plugin.unload()
}
}
strFromFunc(func){
var output = func.toString()
return output.slice(output.indexOf("{") + 1, output.lastIndexOf("}"))
}
argsFromFunc(func){
var output = func.toString()
output = output.slice(0, output.indexOf("{"))
output = output.slice(output.indexOf("(") + 1, output.lastIndexOf(")"))
return output.split(",").map(str => str.trim()).filter(Boolean)
}
insertBefore(input, insertedText, searchString){
var index = input.indexOf(searchString)
if(index === -1){
throw new Error("searchString not found: " + searchString)
}
return input.slice(0, index) + insertedText + input.slice(index)
}
insertAfter(input, searchString, insertedText){
var index = input.indexOf(searchString)
if(index === -1){
throw new Error("searchString not found: " + searchString)
}
var length = searchString.length
return input.slice(0, index + length) + insertedText + input.slice(index + length)
}
strReplace(input, searchString, insertedText){
var index = input.indexOf(searchString)
if(index === -1){
throw new Error("searchString not found: " + searchString)
}
return input.slice(0, index) + insertedText + input.slice(index + searchString.length)
}
getSettings(){
var items = {}
for(var i = 0; i < this.allPlugins.length; i++){
var obj = this.allPlugins[i]
var plugin = obj.plugin
items[obj.name] = {
name: plugin.module.name || obj.name,
type: "toggle",
default: true,
getItem: () => plugin.started,
setItem: value => {
if(plugin.started && !value){
plugin.stop()
}else if(!plugin.started && value){
plugin.start()
}
}
}
}
return items
}
}
class PluginLoader{
constructor(...args){
this.init(...args)
}
init(script, name, hash){
this.name = name
this.hash = hash
if(typeof script === "string"){
this.url = URL.createObjectURL(new Blob([script], {
type: "application/javascript"
}))
}else{
this.class = script
}
}
load(){
if(this.loaded || !this.url && !this.class){
return Promise.resolve()
}else{
return (this.url ? import(this.url) : Promise.resolve({
default: this.class
})).then(module => {
if(this.url){
URL.revokeObjectURL(this.url)
delete this.url
}else{
delete this.class
}
this.loaded = true
try{
this.module = new module.default()
}catch(e){
console.error(e)
this.error()
return
}
try{
if(this.module.beforeLoad){
this.module.beforeLoad(this)
}
if(this.module.load){
this.module.load(this)
}
}catch(e){
console.error(e)
this.error()
}
})
}
}
start(){
return this.load().then(() => {
if(!this.started && this.module){
this.started = true
try{
if(this.module.beforeStart){
this.module.beforeStart()
}
if(this.module.start){
this.module.start()
}
}catch(e){
console.error(e)
this.error()
}
}
})
}
stop(error){
if(this.loaded && this.started){
this.started = false
try{
if(this.module.beforeStop){
this.module.beforeStop()
}
if(this.module.stop){
this.module.stop()
}
}catch(e){
console.error(e)
if(!error){
this.error()
}
}
}
}
unload(error){
if(this.loaded){
if(this.started){
this.stop(error)
}
this.loaded = false
plugins.remove(this.name)
if(this.module){
try{
if(this.module.beforeUnload){
this.module.beforeUnload()
}
if(this.module.unload){
this.module.unload()
}
}catch(e){
console.error(e)
}
delete this.module
}
}
}
error(){
if(this.module && this.module.error){
try{
this.module.error()
}catch(e){
console.error(e)
}
}
this.unload(true)
}
}
class EditValue{
constructor(...args){
this.init(...args)
}
init(parent, name){
if(name){
this.original = parent[name]
this.name = [parent, name]
this.delete = !(name in parent)
}else{
this.original = parent
}
}
load(callback){
var output = callback(this.original)
if(typeof output === "undefined"){
throw new Error("A value is expected to be returned")
}
this.edited = output
return this
}
start(){
if(this.name){
this.name[0][this.name[1]] = this.edited
}
return this.edited
}
stop(){
if(this.name){
if(this.delete){
delete this.name[0][this.name[1]]
}else{
this.name[0][this.name[1]] = this.original
}
}
return this.original
}
unload(){
delete this.name
delete this.edited
delete this.original
}
}
class EditFunction extends EditValue{
load(callback){
var output = callback(plugins.strFromFunc(this.original))
if(typeof output === "undefined"){
throw new Error("A value is expected to be returned")
}
var args = plugins.argsFromFunc(this.original)
this.edited = Function(...args, output)
return this
}
}
class Patch{
edits = []
addEdits(...args){
args.forEach(arg => this.edits.push(arg))
}
beforeStart(){
this.edits.forEach(edit => edit.start())
}
beforeStop(){
this.edits.forEach(edit => edit.stop())
}
beforeUnload(){
this.edits.forEach(edit => edit.unload())
}
log(message){
var name = this.name || "Plugin"
console.log(
"%c[" + name + "]%c " + message,
"font-weight: bold;",
""
)
}
}

View File

@ -1,5 +1,8 @@
class Scoresheet{
constructor(controller, results, multiplayer, touchEnabled){
constructor(...args){
this.init(...args)
}
init(controller, results, multiplayer, touchEnabled){
this.controller = controller
this.resultsObj = results
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]

View File

@ -1,5 +1,8 @@
class ScoreStorage{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
this.scores = {}
this.scoresP2 = {}
this.requestP2 = new Set()

View File

@ -1,5 +1,8 @@
class Session{
constructor(touchEnabled){
constructor(...args){
this.init(...args)
}
init(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("session", true)
this.endButton = this.getElement("view-end-button")

View File

@ -1,5 +1,8 @@
class Settings{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
var ios = /iPhone|iPad/.test(navigator.userAgent)
var phone = /Android|iPhone|iPad/.test(navigator.userAgent)
this.allLanguages = []
@ -151,6 +154,7 @@ class Settings{
loader.screen.style.fontFamily = strings.font
loader.screen.style.fontWeight = boldFonts ? "bold" : ""
loader.screen.classList[boldFonts ? "add" : "remove"]("bold-fonts")
strings.plural = new Intl.PluralRules(lang.intl)
if(!noEvent){
pageEvents.send("language-change", lang.id)
}
@ -158,10 +162,15 @@ class Settings{
}
class SettingsView{
constructor(touchEnabled, tutorial, songId, toSetting){
constructor(...args){
this.init(...args)
}
init(touchEnabled, tutorial, songId, toSetting, settingsItems){
this.touchEnabled = touchEnabled
this.tutorial = tutorial
this.songId = songId
this.customSettings = !!settingsItems
this.settingsItems = settingsItems || settings.items
loader.changePage("settings", tutorial)
assets.sounds["bgm_settings"].playLoop(0.1, false, 0, 1.392, 26.992)
@ -233,8 +242,8 @@ class SettingsView{
var content = this.getElement("view-content")
this.items = []
this.selected = 0
for(let i in settings.items){
var current = settings.items[i]
for(let i in this.settingsItems){
var current = this.settingsItems[i]
if(
!touchEnabled && current.touch === true ||
touchEnabled && current.touch === false ||
@ -246,7 +255,7 @@ class SettingsView{
settingBox.classList.add("setting-box")
var nameDiv = document.createElement("div")
nameDiv.classList.add("setting-name", "stroke-sub")
var name = strings.settings[i].name
var name = current.name || strings.settings[i].name
this.setAltText(nameDiv, name)
settingBox.appendChild(nameDiv)
var valueDiv = document.createElement("div")
@ -277,6 +286,7 @@ class SettingsView{
})
this.addTouch(this.endButton, this.onEnd.bind(this))
if(!this.customSettings){
this.gamepadSettings = document.getElementById("settings-gamepad")
this.addTouch(this.gamepadSettings, event => {
if(event.target === event.currentTarget){
@ -372,6 +382,7 @@ class SettingsView{
settingBox: this.latencyEndButton
})
this.addTouch(this.latencyEndButton, event => this.latencyBack(true))
}
this.setStrings()
@ -432,8 +443,12 @@ class SettingsView{
pageEvents.remove(element, ["mousedown", "touchend"])
}
getValue(name, valueDiv){
var current = settings.items[name]
var current = this.settingsItems[name]
if(current.getItem){
var value = current.getItem()
}else{
var value = settings.getItem(name)
}
if(current.type === "language"){
value = allStrings[value].name + " (" + value + ")"
}else if(current.type === "select" || current.type === "gamepad"){
@ -471,8 +486,13 @@ class SettingsView{
valueDiv.innerText = value
}
setValue(name){
var current = settings.items[name]
var promise
var current = this.settingsItems[name]
if(current.getItem){
var value = current.getItem()
}else{
var value = settings.getItem(name)
}
var selectedIndex = this.items.findIndex(item => item.id === name)
var selected = this.items[selectedIndex]
if(this.mode !== "settings"){
@ -511,12 +531,18 @@ class SettingsView{
this.playSound("se_don")
return
}
if(current.setItem){
promise = current.setItem(value)
}else{
settings.setItem(name, value)
}
(promise || Promise.resolve()).then(() => {
this.getValue(name, this.items[this.selected].valueDiv)
this.playSound("se_ka")
if(current.type === "language"){
this.setLang(allStrings[value])
}
})
}
keyPressed(pressed, name, event, repeat){
if(pressed){
@ -627,7 +653,7 @@ class SettingsView{
}
keyboardSet(){
var selected = this.items[this.selected]
var current = settings.items[selected.id]
var current = this.settingsItems[selected.id]
selected.valueDiv.innerHTML = ""
for(var i in current.default){
var keyDiv = document.createElement("div")
@ -665,7 +691,7 @@ class SettingsView{
return
}
var selected = this.items[this.selected]
var current = settings.items[selected.id]
var current = this.settingsItems[selected.id]
if(diff){
this.gamepadSelected = this.mod(current.options.length, this.gamepadSelected + diff)
this.playSound("se_ka")
@ -681,7 +707,7 @@ class SettingsView{
return
}
var selected = this.items[this.selected]
var current = settings.items[selected.id]
var current = this.settingsItems[selected.id]
settings.setItem(selected.id, current.options[this.gamepadSelected])
this.getValue(selected.id, selected.valueDiv)
this.playSound(confirm ? "se_don" : "se_cancel")
@ -693,7 +719,7 @@ class SettingsView{
return
}
var selected = this.items[this.selected]
var current = settings.items[selected.id]
var current = this.settingsItems[selected.id]
this.latencySettings.style.display = "flex"
}
latencyGetValue(name, valueText){
@ -801,7 +827,7 @@ class SettingsView{
return
}
var selected = this.items[this.selected]
var current = settings.items[selected.id]
var current = this.settingsItems[selected.id]
this.getValue(selected.id, selected.valueDiv)
this.playSound(confirm ? "se_don" : "se_cancel")
this.latencySettings.style.display = ""
@ -821,10 +847,14 @@ class SettingsView{
return output
}
defaultSettings(){
if(this.customSettings){
plugins.unloadImported()
return this.onEnd()
}
if(this.mode === "keyboard"){
this.keyboardBack(this.items[this.selected])
}
for(var i in settings.items){
for(var i in this.settingsItems){
settings.setItem(i, null)
}
this.setLang(allStrings[settings.getItem("language")])
@ -848,7 +878,7 @@ class SettingsView{
try{
localStorage.setItem("tutorial", "true")
}catch(e){}
new SongSelect(this.tutorial ? false : "settings", false, this.touched, this.songId)
new SongSelect(this.tutorial ? false : this.customSettings ? "plugins" : "settings", false, this.touched, this.songId)
}
}, 500)
}
@ -877,14 +907,16 @@ class SettingsView{
this.setStrings()
}
setStrings(){
this.setAltText(this.viewTitle, strings.gameSettings)
this.setAltText(this.viewTitle, this.customSettings ? strings.plugins.title : strings.gameSettings)
this.setAltText(this.endButton, strings.settings.ok)
if(!this.customSettings){
this.setAltText(this.gamepadTitle, strings.settings.gamepadLayout.name)
this.setAltText(this.gamepadEndButton, strings.settings.ok)
this.setAltText(this.latencyTitle, strings.settings.latency.name)
this.setAltText(this.latencyDefaultButton, strings.settings.default)
this.setAltText(this.latencyEndButton, strings.settings.ok)
this.setAltText(this.defaultButton, strings.settings.default)
}
this.setAltText(this.defaultButton, this.customSettings ? strings.plugins.unloadAll : strings.settings.default)
}
setAltText(element, text){
element.innerText = text
@ -941,12 +973,14 @@ class SettingsView{
if(this.defaultButton){
delete this.defaultButton
}
if(!this.customSettings){
this.removeTouch(this.gamepadSettings)
this.removeTouch(this.gamepadEndButton)
this.removeTouch(this.gamepadBox)
this.removeTouch(this.latencySettings)
this.removeTouch(this.latencyDefaultButton)
this.removeTouch(this.latencyEndButton)
}
delete this.windowSymbol
delete this.touchMove
delete this.viewOuter

View File

@ -1,5 +1,8 @@
class SongSelect{
constructor(fromTutorial, fadeIn, touchEnabled, songId, showWarning){
constructor(...args){
this.init(...args)
}
init(fromTutorial, fadeIn, touchEnabled, songId, showWarning){
this.touchEnabled = touchEnabled
loader.changePage("songselect", false)
@ -55,6 +58,12 @@ class SongSelect{
border: ["#ffe7ef", "#d36aa2"],
outline: "#d36aa2"
},
"plugins": {
sort: 0,
background: "#f6bba1",
border: ["#fde9df", "#ce7553"],
outline: "#ce7553"
},
"default": {
sort: null,
background: "#ececec",
@ -150,6 +159,14 @@ class SongSelect{
category: strings.random
})
}
if(plugins.allPlugins.length){
this.songs.push({
title: strings.plugins.title,
skin: this.songSkin.plugins,
action: "plugins",
category: strings.random
})
}
this.songs.push({
title: strings.back,
@ -218,8 +235,12 @@ class SongSelect{
this.playedSounds = {}
var songIdIndex = -1
var newSelected = -1
if(fromTutorial){
this.selectedSong = this.songs.findIndex(song => song.action === fromTutorial)
newSelected = this.songs.findIndex(song => song.action === fromTutorial)
}
if(newSelected !== -1){
this.selectedSong = newSelected
this.playBgm(true)
}else{
if(songId){
@ -481,18 +502,6 @@ class SongSelect{
}
touchEnd(event){
event.preventDefault()
if(this.state.screen === "song" && this.redrawRunning){
var currentSong = this.songs[this.selectedSong]
if(currentSong.action === "customSongs"){
var x = event.changedTouches[0].pageX - this.canvas.offsetLeft
var y = event.changedTouches[0].pageY - this.canvas.offsetTop
var mouse = this.mouseOffset(x, y)
var moveBy = this.songSelMouse(mouse.x, mouse.y)
if(moveBy === 0){
this.toCustomSongs()
}
}
}
}
mouseMove(event){
var mouse = this.mouseOffset(event.offsetX, event.offsetY)
@ -565,8 +574,8 @@ class SongSelect{
}
diffSelMouse(x, y){
if(this.state.locked === 0){
if(223 < x && x < 367 && 132 < y && y < 436){
return Math.floor((x - 223) / ((367 - 223) / 2))
if(223 < x && x < 223 + 72 * this.diffOptions.length && 132 < y && y < 436){
return Math.floor((x - 223) / 72)
}else if(this.songs[this.selectedSong].maker && this.songs[this.selectedSong].maker.id > 0 && this.songs[this.selectedSong].maker.url && x > 230 && x < 485 && y > 446 && y < 533) {
return "maker"
}else if(550 < x && x < 1050 && 109 < y && y < 538){
@ -706,6 +715,8 @@ class SongSelect{
this.toSettings()
}else if(currentSong.action === "customSongs"){
this.toCustomSongs()
}else if(currentSong.action === "plugins"){
this.toPlugins()
}
}
this.pointer(false)
@ -864,6 +875,13 @@ class SongSelect{
}, 500)
}
}
toPlugins(){
this.playSound("se_don")
this.clean()
setTimeout(() => {
new SettingsView(this.touchEnabled, false, undefined, undefined, plugins.getSettings())
}, 500)
}
redraw(){
if(!this.redrawRunning){

View File

@ -1,5 +1,8 @@
class SoundBuffer{
constructor(){
constructor(...args){
this.init(...args)
}
init(){
var AudioContext = window.AudioContext || window.webkitAudioContext
this.context = new AudioContext()
this.audioDecoder = this.context.decodeAudioData.bind(this.context)

View File

@ -21,6 +21,13 @@ var translations = {
tw: "Microsoft YaHei, sans-serif",
ko: "Microsoft YaHei, sans-serif"
},
intl: {
ja: "ja",
en: "en-GB",
cn: "zh-Hans",
tw: "zh-Hant",
ko: "ko"
},
taikoWeb: {
ja: "たいこウェブ",
@ -1278,6 +1285,27 @@ var translations = {
en: "This function requires third party cookies.",
tw: "此功能需要第三方 cookies。"
}
},
plugins: {
title: {
ja: null,
en: "Plugins"
},
unloadAll: {
ja: null,
en: "Unload All"
},
warning: {
ja: null,
en: "You are about to load %s. Plugins should only be loaded if you trust them. Continue?"
},
plugin: {
ja: null,
en: {
one: "%s plugin",
other: "%s plugins"
}
}
}
}
var allStrings = {}

View File

@ -1,5 +1,8 @@
class Titlescreen{
constructor(songId){
constructor(...args){
this.init(...args)
}
init(songId){
this.songId = songId
db.getItem("customFolder").then(folder => this.customFolder = folder)

View File

@ -1,5 +1,8 @@
class Tutorial{
constructor(fromSongSel, songId){
constructor(...args){
this.init(...args)
}
init(fromSongSel, songId){
this.fromSongSel = fromSongSel
this.songId = songId
loader.changePage("tutorial", true)

View File

@ -1,5 +1,8 @@
class View{
constructor(controller){
constructor(...args){
this.init(...args)
}
init(controller){
this.controller = controller
this.canvas = document.getElementById("canvas")

View File

@ -1,5 +1,8 @@
class ViewAssets{
constructor(view){
constructor(...args){
this.init(...args)
}
init(view){
this.view = view
this.controller = this.view.controller
this.allAssets = []