Compare commits
3 Commits
6d7be5c45c
...
Bang
| Author | SHA1 | Date | |
|---|---|---|---|
| 1038fc85b9 | |||
| 76a3d52098 | |||
| d6a1b6bd41 |
155
app.py
155
app.py
@@ -105,6 +105,9 @@ db.users.create_index('username', unique=True)
|
|||||||
db.songs.create_index('id', unique=True)
|
db.songs.create_index('id', unique=True)
|
||||||
db.songs.create_index('song_type')
|
db.songs.create_index('song_type')
|
||||||
db.scores.create_index('username')
|
db.scores.create_index('username')
|
||||||
|
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month', 1), ('score_value', -1)])
|
||||||
|
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('username', 1), ('month', 1)], unique=True)
|
||||||
|
db.leaderboards.create_index('month')
|
||||||
|
|
||||||
|
|
||||||
class HashException(Exception):
|
class HashException(Exception):
|
||||||
@@ -746,6 +749,158 @@ def route_api_scores_get():
|
|||||||
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/submit', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def route_api_leaderboard_submit():
|
||||||
|
data = request.get_json()
|
||||||
|
if not schema.validate(data, schema.leaderboard_submit):
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
username = session.get('username')
|
||||||
|
user = db.users.find_one({'username': username})
|
||||||
|
if not user:
|
||||||
|
return api_error('user_not_found')
|
||||||
|
|
||||||
|
song_id = data.get('song_id')
|
||||||
|
difficulty = data.get('difficulty')
|
||||||
|
score_data = data.get('score')
|
||||||
|
|
||||||
|
# Validate difficulty
|
||||||
|
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||||
|
if difficulty not in valid_difficulties:
|
||||||
|
return api_error('invalid_difficulty')
|
||||||
|
|
||||||
|
# Get current month (YYYY-MM format)
|
||||||
|
current_month = time.strftime('%Y-%m', time.gmtime())
|
||||||
|
|
||||||
|
# Check if user already has a record for this song/difficulty/month
|
||||||
|
existing = db.leaderboards.find_one({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'username': username,
|
||||||
|
'month': current_month
|
||||||
|
})
|
||||||
|
|
||||||
|
# Parse score (assuming it's in the same format as the scores collection)
|
||||||
|
try:
|
||||||
|
if isinstance(score_data, str):
|
||||||
|
import json as json_module
|
||||||
|
score_obj = json_module.loads(score_data)
|
||||||
|
else:
|
||||||
|
score_obj = score_data
|
||||||
|
|
||||||
|
score_value = int(score_obj.get('score', 0))
|
||||||
|
except:
|
||||||
|
return api_error('invalid_score_format')
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Update only if new score is higher
|
||||||
|
existing_score = existing.get('score_value', 0)
|
||||||
|
if score_value > existing_score:
|
||||||
|
db.leaderboards.update_one(
|
||||||
|
{'_id': existing['_id']},
|
||||||
|
{'$set': {
|
||||||
|
'score': score_data,
|
||||||
|
'score_value': score_value,
|
||||||
|
'display_name': user['display_name'],
|
||||||
|
'submitted_at': time.time()
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
return jsonify({'status': 'ok', 'message': 'score_updated'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'ok', 'message': 'score_not_higher'})
|
||||||
|
else:
|
||||||
|
# Check if this score would be in top 50
|
||||||
|
count = db.leaderboards.count_documents({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
})
|
||||||
|
|
||||||
|
if count >= 50:
|
||||||
|
# Find the 50th score
|
||||||
|
leaderboard = list(db.leaderboards.find({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
}).sort('score_value', -1).limit(50))
|
||||||
|
|
||||||
|
if len(leaderboard) >= 50:
|
||||||
|
last_score = leaderboard[49].get('score_value', 0)
|
||||||
|
if score_value <= last_score:
|
||||||
|
return jsonify({'status': 'ok', 'message': 'score_too_low'})
|
||||||
|
|
||||||
|
# Insert new record
|
||||||
|
db.leaderboards.insert_one({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'username': username,
|
||||||
|
'display_name': user['display_name'],
|
||||||
|
'score': score_data,
|
||||||
|
'score_value': score_value,
|
||||||
|
'submitted_at': time.time(),
|
||||||
|
'month': current_month
|
||||||
|
})
|
||||||
|
|
||||||
|
# Remove entries beyond 50th place
|
||||||
|
if count >= 50:
|
||||||
|
# Get all entries sorted by score
|
||||||
|
all_entries = list(db.leaderboards.find({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
}).sort('score_value', -1))
|
||||||
|
|
||||||
|
# Delete entries beyond 50
|
||||||
|
if len(all_entries) > 50:
|
||||||
|
for entry in all_entries[50:]:
|
||||||
|
db.leaderboards.delete_one({'_id': entry['_id']})
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok', 'message': 'score_submitted'})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard/get')
|
||||||
|
def route_api_leaderboard_get():
|
||||||
|
song_id = request.args.get('song_id', None)
|
||||||
|
difficulty = request.args.get('difficulty', None)
|
||||||
|
|
||||||
|
if not song_id or not difficulty:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
song_id = int(song_id)
|
||||||
|
except:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
# Validate difficulty
|
||||||
|
valid_difficulties = ['easy', 'normal', 'hard', 'oni', 'ura']
|
||||||
|
if difficulty not in valid_difficulties:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
# Get current month
|
||||||
|
current_month = time.strftime('%Y-%m', time.gmtime())
|
||||||
|
|
||||||
|
# Get top 50 scores
|
||||||
|
leaderboard = list(db.leaderboards.find({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month': current_month
|
||||||
|
}, {
|
||||||
|
'_id': False,
|
||||||
|
'username': True,
|
||||||
|
'display_name': True,
|
||||||
|
'score': True,
|
||||||
|
'score_value': True,
|
||||||
|
'submitted_at': True
|
||||||
|
}).sort('score_value', -1).limit(50))
|
||||||
|
|
||||||
|
# Add rank to each entry
|
||||||
|
for i, entry in enumerate(leaderboard):
|
||||||
|
entry['rank'] = i + 1
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok', 'leaderboard': leaderboard, 'month': current_month})
|
||||||
|
|
||||||
|
|
||||||
@app.route(basedir + 'privacy')
|
@app.route(basedir + 'privacy')
|
||||||
def route_api_privacy():
|
def route_api_privacy():
|
||||||
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt')))
|
||||||
|
|||||||
13
public/src/css/leaderboard.css
Normal file
13
public/src/css/leaderboard.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#leaderboard {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
287
public/src/js/leaderboard.js
Normal file
287
public/src/js/leaderboard.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
class Leaderboard {
|
||||||
|
constructor() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.canvas = document.getElementById("leaderboard-canvas")
|
||||||
|
if (!this.canvas) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.ctx = this.canvas.getContext("2d")
|
||||||
|
|
||||||
|
var resolution = settings.getItem("resolution")
|
||||||
|
var noSmoothing = resolution === "low" || resolution === "lowest"
|
||||||
|
if (noSmoothing) {
|
||||||
|
this.ctx.imageSmoothingEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.songId = null
|
||||||
|
this.difficulty = null
|
||||||
|
this.leaderboardData = []
|
||||||
|
this.currentMonth = ""
|
||||||
|
this.visible = false
|
||||||
|
|
||||||
|
this.draw = new CanvasDraw(noSmoothing)
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
this.keyboard = new Keyboard({
|
||||||
|
confirm: ["enter", "escape", "don_l", "don_r"],
|
||||||
|
back: ["escape"],
|
||||||
|
left: ["left", "ka_l"],
|
||||||
|
right: ["right", "ka_r"]
|
||||||
|
}, this.keyPress.bind(this))
|
||||||
|
|
||||||
|
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.onClose.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPress(pressed, name) {
|
||||||
|
if (!pressed || !this.visible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (name === "confirm" || name === "back") {
|
||||||
|
this.close()
|
||||||
|
} else if (name === "left") {
|
||||||
|
this.changeDifficulty(-1)
|
||||||
|
} else if (name === "right") {
|
||||||
|
this.changeDifficulty(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async display(songId, difficulty) {
|
||||||
|
this.songId = songId
|
||||||
|
this.difficulty = difficulty
|
||||||
|
this.visible = true
|
||||||
|
|
||||||
|
loader.changePage("leaderboard", false)
|
||||||
|
|
||||||
|
// Fetch leaderboard data
|
||||||
|
await this.fetchLeaderboard()
|
||||||
|
|
||||||
|
// Start rendering
|
||||||
|
this.redrawRunning = true
|
||||||
|
this.redrawBind = this.redraw.bind(this)
|
||||||
|
this.redraw()
|
||||||
|
|
||||||
|
assets.sounds["se_don"].play()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchLeaderboard() {
|
||||||
|
try {
|
||||||
|
var response = await loader.ajax(
|
||||||
|
`${gameConfig.basedir || "/"}api/leaderboard/get?song_id=${this.songId}&difficulty=${this.difficulty}`
|
||||||
|
)
|
||||||
|
var data = JSON.parse(response)
|
||||||
|
if (data.status === "ok") {
|
||||||
|
this.leaderboardData = data.leaderboard || []
|
||||||
|
this.currentMonth = data.month || ""
|
||||||
|
} else {
|
||||||
|
this.leaderboardData = []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch leaderboard:", e)
|
||||||
|
this.leaderboardData = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDifficulty(direction) {
|
||||||
|
var difficulties = ["easy", "normal", "hard", "oni", "ura"]
|
||||||
|
var currentIndex = difficulties.indexOf(this.difficulty)
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = (currentIndex + direction + difficulties.length) % difficulties.length
|
||||||
|
this.difficulty = difficulties[newIndex]
|
||||||
|
|
||||||
|
this.fetchLeaderboard().then(() => {
|
||||||
|
assets.sounds["se_ka"].play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(event) {
|
||||||
|
if (!this.visible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = this.canvas.getBoundingClientRect()
|
||||||
|
var x = (event.offsetX || event.touches[0].pageX - rect.left)
|
||||||
|
var y = (event.offsetY || event.touches[0].pageY - rect.top)
|
||||||
|
|
||||||
|
// Check if clicked outside modal (approximately)
|
||||||
|
var centerX = this.canvas.width / 2
|
||||||
|
var centerY = this.canvas.height / 2
|
||||||
|
var modalWidth = 800
|
||||||
|
var modalHeight = 600
|
||||||
|
|
||||||
|
if (x < centerX - modalWidth / 2 || x > centerX + modalWidth / 2 ||
|
||||||
|
y < centerY - modalHeight / 2 || y > centerY + modalHeight / 2) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.visible = false
|
||||||
|
this.redrawRunning = false
|
||||||
|
|
||||||
|
if (this.keyboard) {
|
||||||
|
this.keyboard.clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
assets.sounds["se_cancel"].play()
|
||||||
|
|
||||||
|
// Return to song select - get touchEnabled from global or default to false
|
||||||
|
setTimeout(() => {
|
||||||
|
var touchEnabled = typeof window.touchEnabled !== 'undefined' ? window.touchEnabled : false
|
||||||
|
new SongSelect(false, false, touchEnabled)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
if (!this.redrawRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(this.redrawBind)
|
||||||
|
|
||||||
|
var winW = innerWidth
|
||||||
|
var winH = innerHeight
|
||||||
|
var ratio = winH / 720
|
||||||
|
|
||||||
|
this.canvas.width = winW
|
||||||
|
this.canvas.height = winH
|
||||||
|
this.ctx.save()
|
||||||
|
|
||||||
|
// Draw semi-transparent background
|
||||||
|
this.ctx.fillStyle = "rgba(0, 0, 0, 0.8)"
|
||||||
|
this.ctx.fillRect(0, 0, winW, winH)
|
||||||
|
|
||||||
|
this.ctx.scale(ratio, ratio)
|
||||||
|
|
||||||
|
// Draw modal background
|
||||||
|
var modalX = 240
|
||||||
|
var modalY = 60
|
||||||
|
var modalW = 800
|
||||||
|
var modalH = 600
|
||||||
|
|
||||||
|
this.ctx.fillStyle = "#ffffff"
|
||||||
|
this.ctx.fillRect(modalX, modalY, modalW, modalH)
|
||||||
|
|
||||||
|
this.ctx.strokeStyle = "#333333"
|
||||||
|
this.ctx.lineWidth = 4
|
||||||
|
this.ctx.strokeRect(modalX, modalY, modalW, modalH)
|
||||||
|
|
||||||
|
// Draw title
|
||||||
|
this.ctx.fillStyle = "#000000"
|
||||||
|
this.ctx.font = "bold 40px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText("🏆 排行榜 Leaderboard", 640, 110)
|
||||||
|
|
||||||
|
// Draw difficulty selector
|
||||||
|
var diffX = 640
|
||||||
|
var diffY = 160
|
||||||
|
var difficulties = [
|
||||||
|
{ id: "easy", name: "簡単", color: "#00a0e9" },
|
||||||
|
{ id: "normal", name: "普通", color: "#00a040" },
|
||||||
|
{ id: "hard", name: "難しい", color: "#ff8c00" },
|
||||||
|
{ id: "oni", name: "鬼", color: "#dc143c" },
|
||||||
|
{ id: "ura", name: "裏", color: "#9400d3" }
|
||||||
|
]
|
||||||
|
|
||||||
|
this.ctx.font = "24px " + (strings.font || "sans-serif")
|
||||||
|
for (var i = 0; i < difficulties.length; i++) {
|
||||||
|
var diff = difficulties[i]
|
||||||
|
var x = diffX - 200 + i * 100
|
||||||
|
|
||||||
|
if (diff.id === this.difficulty) {
|
||||||
|
this.ctx.fillStyle = diff.color
|
||||||
|
this.ctx.fillRect(x - 45, diffY - 25, 90, 40)
|
||||||
|
this.ctx.fillStyle = "#ffffff"
|
||||||
|
} else {
|
||||||
|
this.ctx.strokeStyle = diff.color
|
||||||
|
this.ctx.lineWidth = 2
|
||||||
|
this.ctx.strokeRect(x - 45, diffY - 25, 90, 40)
|
||||||
|
this.ctx.fillStyle = diff.color
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText(diff.name, x, diffY + 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw month info
|
||||||
|
this.ctx.fillStyle = "#666666"
|
||||||
|
this.ctx.font = "18px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText("当月排行 " + this.currentMonth, 640, 210)
|
||||||
|
|
||||||
|
// Draw leaderboard entries
|
||||||
|
var startY = 240
|
||||||
|
var rowHeight = 35
|
||||||
|
|
||||||
|
this.ctx.font = "20px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "left"
|
||||||
|
|
||||||
|
if (this.leaderboardData.length === 0) {
|
||||||
|
this.ctx.fillStyle = "#999999"
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText("暂无排行数据", 640, startY + 100)
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < Math.min(this.leaderboardData.length, 15); i++) {
|
||||||
|
var entry = this.leaderboardData[i]
|
||||||
|
var y = startY + i * rowHeight
|
||||||
|
var rank = entry.rank
|
||||||
|
|
||||||
|
// Rank background color
|
||||||
|
if (rank === 1) {
|
||||||
|
this.ctx.fillStyle = "#ffd700" // Gold
|
||||||
|
} else if (rank === 2) {
|
||||||
|
this.ctx.fillStyle = "#c0c0c0" // Silver
|
||||||
|
} else if (rank === 3) {
|
||||||
|
this.ctx.fillStyle = "#cd7f32" // Bronze
|
||||||
|
} else {
|
||||||
|
this.ctx.fillStyle = "#f5f5f5"
|
||||||
|
}
|
||||||
|
this.ctx.fillRect(modalX + 20, y - 20, modalW - 40, 30)
|
||||||
|
|
||||||
|
// Rank
|
||||||
|
this.ctx.fillStyle = rank <= 3 ? "#ffffff" : "#333333"
|
||||||
|
this.ctx.font = "bold 20px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText(rank, modalX + 60, y)
|
||||||
|
|
||||||
|
// Display name
|
||||||
|
this.ctx.fillStyle = "#000000"
|
||||||
|
this.ctx.font = "20px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "left"
|
||||||
|
var displayName = entry.display_name || entry.username
|
||||||
|
if (displayName.length > 15) {
|
||||||
|
displayName = displayName.substring(0, 15) + "..."
|
||||||
|
}
|
||||||
|
this.ctx.fillText(displayName, modalX + 100, y)
|
||||||
|
|
||||||
|
// Score
|
||||||
|
var score = entry.score && entry.score.score ? entry.score.score : 0
|
||||||
|
this.ctx.textAlign = "right"
|
||||||
|
this.ctx.fillText(score.toLocaleString(), modalX + modalW - 40, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw close hint
|
||||||
|
this.ctx.fillStyle = "#666666"
|
||||||
|
this.ctx.font = "18px " + (strings.font || "sans-serif")
|
||||||
|
this.ctx.textAlign = "center"
|
||||||
|
this.ctx.fillText("按ESC或点击外部关闭 Press ESC or click outside to close", 640, 680)
|
||||||
|
|
||||||
|
this.ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
if (this.keyboard) {
|
||||||
|
this.keyboard.clean()
|
||||||
|
}
|
||||||
|
if (this.redrawRunning) {
|
||||||
|
this.redrawRunning = false
|
||||||
|
}
|
||||||
|
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3
public/src/views/leaderboard.html
Normal file
3
public/src/views/leaderboard.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div id="leaderboard">
|
||||||
|
<canvas id="leaderboard-canvas"></canvas>
|
||||||
|
</div>
|
||||||
11
schema.py
11
schema.py
@@ -80,3 +80,14 @@ scores_save = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaderboard_submit = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'song_id': {'type': 'number'},
|
||||||
|
'difficulty': {'type': 'string'},
|
||||||
|
'score': {'type': 'object'}
|
||||||
|
},
|
||||||
|
'required': ['song_id', 'difficulty', 'score']
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>太鼓ウェブ - Taiko Web | (゚∀゚)</title>
|
<title>太鼓ウェブ - Taiko Web | (゚∀゚)</title>
|
||||||
<link rel="icon" href="{{config.assets_baseurl}}img/favicon.png" type="image/png">
|
<link rel="icon" href="{{config.assets_baseurl}}img/favicon.png" type="image/png">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<meta name="description" content="2025年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers">
|
<meta name="description"
|
||||||
<meta name="keywords" content="taiko no tatsujin, taiko, don chan, online, rhythm, browser, html5, game, for browsers, pc, arcade, emulator, free, download, website, 太鼓の達人, 太鼓ウェブ, 太鼓之達人, 太鼓達人, 太鼓网页, 网页版, 太鼓網頁, 網頁版, 태고의 달인, 태고 웹">
|
content="2026年最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 Taiko no Tatsujin rhythm game simulator for desktop and mobile browsers">
|
||||||
|
<meta name="keywords"
|
||||||
|
content="taiko no tatsujin, taiko, don chan, online, rhythm, browser, html5, game, for browsers, pc, arcade, emulator, free, download, website, 太鼓の達人, 太鼓ウェブ, 太鼓之達人, 太鼓達人, 太鼓网页, 网页版, 太鼓網頁, 網頁版, 태고의 달인, 태고 웹">
|
||||||
<meta name="robots" content="notranslate">
|
<meta name="robots" content="notranslate">
|
||||||
<meta name="robots" content="noimageindex">
|
<meta name="robots" content="noimageindex">
|
||||||
<meta name="color-scheme" content="only light">
|
<meta name="color-scheme" content="only light">
|
||||||
|
|
||||||
<link rel="canonical" href="https://taikoapp.uk/" />
|
<link rel="canonical" href="https://taikoapp.uk/" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}">
|
<link rel="stylesheet" href="src/css/loader.css?{{version.commit_short}}">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
@@ -24,14 +27,17 @@
|
|||||||
<script src="src/js/lib/jszip.js"></script>
|
<script src="src/js/lib/jszip.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="assets"></div>
|
<div id="assets"></div>
|
||||||
<div id="screen" class="pattern-bg"></div>
|
<div id="screen" class="pattern-bg"></div>
|
||||||
<div data-nosnippet id="version">
|
<div data-nosnippet id="version">
|
||||||
{% if version.version and version.commit_short and version.commit %}
|
{% if version.version and version.commit_short and version.commit %}
|
||||||
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
<a href="{{version.url}}commit/{{version.commit}}" target="_blank" rel="noopener" id="version-link"
|
||||||
|
class="stroke-sub" alt="vLightNova 1.1.0">vLightNova 1.1.0</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.0.0">vLightNova 1.0.0</a>
|
<a target="_blank" rel="noopener" id="version-link" class="stroke-sub" alt="vLightNova 1.1.0">vLightNova
|
||||||
|
1.1.0</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
||||||
@@ -43,4 +49,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user