Update deployment scripts, add leaderboard feature, and bump version to 2.0.0
This commit is contained in:
22
Dockerfile
22
Dockerfile
@@ -1,6 +1,18 @@
|
|||||||
FROM python:3.13.2
|
FROM python:3.13.2-slim
|
||||||
COPY . /app
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ffmpeg \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN pip install -r requirements.txt
|
COPY requirements.txt /app/
|
||||||
ENV PYTHONUNBUFFERED 1
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
CMD ["gunicorn", "app:app", "--access-logfile", "-", "--bind", "0.0.0.0"]
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["gunicorn", "app:app", "--access-logfile", "-", "--bind", "0.0.0.0:80"]
|
||||||
|
|||||||
121
app.py
121
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')
|
||||||
|
# Leaderboard indexes
|
||||||
|
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month_key', 1)])
|
||||||
|
db.leaderboards.create_index([('song_id', 1), ('difficulty', 1), ('month_key', 1), ('score_value', -1)])
|
||||||
|
|
||||||
|
|
||||||
class HashException(Exception):
|
class HashException(Exception):
|
||||||
@@ -746,6 +749,124 @@ 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})
|
||||||
|
|
||||||
|
|
||||||
|
# Leaderboard helper function
|
||||||
|
def get_month_key():
|
||||||
|
from datetime import datetime
|
||||||
|
return datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/leaderboard')
|
||||||
|
def route_api_leaderboard():
|
||||||
|
song_id = request.args.get('song_id')
|
||||||
|
difficulty = request.args.get('difficulty')
|
||||||
|
month_key = request.args.get('month_key', get_month_key())
|
||||||
|
|
||||||
|
if not song_id or not difficulty:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
entries = list(db.leaderboards.find(
|
||||||
|
{'song_id': song_id, 'difficulty': difficulty, 'month_key': month_key},
|
||||||
|
{'_id': False}
|
||||||
|
).sort('score_value', -1).limit(50))
|
||||||
|
|
||||||
|
# Add rank
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
entry['rank'] = i + 1
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok', 'entries': entries, 'month_key': month_key})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/score/check', methods=['POST'])
|
||||||
|
def route_api_score_check():
|
||||||
|
data = request.get_json()
|
||||||
|
if not schema.validate(data, schema.leaderboard_check):
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
song_id = str(data.get('song_id'))
|
||||||
|
difficulty = data.get('difficulty')
|
||||||
|
score = int(data.get('score'))
|
||||||
|
month_key = get_month_key()
|
||||||
|
|
||||||
|
# Get top 50 for this song/difficulty/month
|
||||||
|
entries = list(db.leaderboards.find(
|
||||||
|
{'song_id': song_id, 'difficulty': difficulty, 'month_key': month_key}
|
||||||
|
).sort('score_value', -1).limit(50))
|
||||||
|
|
||||||
|
# Check if qualifies
|
||||||
|
if len(entries) < 50:
|
||||||
|
rank = len(entries) + 1
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
if score > entry['score_value']:
|
||||||
|
rank = i + 1
|
||||||
|
break
|
||||||
|
return jsonify({'status': 'ok', 'qualifies': True, 'rank': rank})
|
||||||
|
|
||||||
|
# Check if score beats the 50th entry
|
||||||
|
if score > entries[-1]['score_value']:
|
||||||
|
rank = 50
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
if score > entry['score_value']:
|
||||||
|
rank = i + 1
|
||||||
|
break
|
||||||
|
return jsonify({'status': 'ok', 'qualifies': True, 'rank': rank})
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok', 'qualifies': False})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(basedir + 'api/score/leaderboard/save', methods=['POST'])
|
||||||
|
def route_api_score_leaderboard_save():
|
||||||
|
data = request.get_json()
|
||||||
|
if not schema.validate(data, schema.leaderboard_save):
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
song_id = str(data.get('song_id'))
|
||||||
|
difficulty = data.get('difficulty')
|
||||||
|
username = data.get('username', '').strip()[:10]
|
||||||
|
score = int(data.get('score'))
|
||||||
|
month_key = get_month_key()
|
||||||
|
|
||||||
|
if len(username) < 1:
|
||||||
|
return api_error('invalid_username')
|
||||||
|
|
||||||
|
timestamp = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# Check existing entry for this user/song/difficulty/month
|
||||||
|
existing = db.leaderboards.find_one({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month_key': month_key,
|
||||||
|
'username': username
|
||||||
|
})
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Only update if new score is higher
|
||||||
|
if score > existing['score_value']:
|
||||||
|
db.leaderboards.update_one(
|
||||||
|
{'_id': existing['_id']},
|
||||||
|
{'$set': {'score_value': score, 'timestamp': timestamp}}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Insert new entry
|
||||||
|
db.leaderboards.insert_one({
|
||||||
|
'song_id': song_id,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
'month_key': month_key,
|
||||||
|
'username': username,
|
||||||
|
'score_value': score,
|
||||||
|
'timestamp': timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
# Trim to top 50
|
||||||
|
entries = list(db.leaderboards.find(
|
||||||
|
{'song_id': song_id, 'difficulty': difficulty, 'month_key': month_key}
|
||||||
|
).sort('score_value', -1).skip(50))
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
db.leaderboards.delete_one({'_id': entry['_id']})
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
@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')))
|
||||||
|
|||||||
266
public/src/css/leaderboard.css
Normal file
266
public/src/css/leaderboard.css
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/* Leaderboard Overlay */
|
||||||
|
#leaderboard-overlay,
|
||||||
|
#leaderboard-submit-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaderboard Modal */
|
||||||
|
#leaderboard-modal {
|
||||||
|
background: linear-gradient(180deg, #ff9f18 0%, #ff6b00 100%);
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-header {
|
||||||
|
background: linear-gradient(180deg, #ff4500 0%, #d63000 100%);
|
||||||
|
border-radius: 11px 11px 0 0;
|
||||||
|
padding: 15px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-song-title {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #fff;
|
||||||
|
margin-top: 5px;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-difficulty {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #ffe100;
|
||||||
|
margin-top: 3px;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-loading,
|
||||||
|
#leaderboard-empty,
|
||||||
|
#leaderboard-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-error {
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaderboard Table */
|
||||||
|
#leaderboard-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-header-row {
|
||||||
|
background: #ff9f18;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-row:not(.leaderboard-header-row):hover {
|
||||||
|
background: #ffe8c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-rank {
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-name {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-score {
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top 3 ranks styling */
|
||||||
|
.leaderboard-rank-1 {
|
||||||
|
background: linear-gradient(90deg, #ffd700 0%, #fff8e8 30%);
|
||||||
|
}
|
||||||
|
.leaderboard-rank-1 .leaderboard-rank {
|
||||||
|
color: #d4a500;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-rank-2 {
|
||||||
|
background: linear-gradient(90deg, #c0c0c0 0%, #fff8e8 30%);
|
||||||
|
}
|
||||||
|
.leaderboard-rank-2 .leaderboard-rank {
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-rank-3 {
|
||||||
|
background: linear-gradient(90deg, #cd7f32 0%, #fff8e8 30%);
|
||||||
|
}
|
||||||
|
.leaderboard-rank-3 .leaderboard-rank {
|
||||||
|
color: #8b4513;
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-footer {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-close-btn {
|
||||||
|
background: linear-gradient(180deg, #666 0%, #444 100%);
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-close-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background: linear-gradient(180deg, #888 0%, #555 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Modal */
|
||||||
|
#leaderboard-submit-modal {
|
||||||
|
background: linear-gradient(180deg, #4CAF50 0%, #388E3C 100%);
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-score {
|
||||||
|
font-size: 1.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffe100;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-form label {
|
||||||
|
display: block;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-name-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
border: 3px solid #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-name-input.error {
|
||||||
|
border-color: #f00;
|
||||||
|
background: #ffe8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-btn,
|
||||||
|
#leaderboard-cancel-btn {
|
||||||
|
padding: 12px 25px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-btn {
|
||||||
|
background: linear-gradient(180deg, #ff9f18 0%, #ff6b00 100%);
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-btn:hover:not(:disabled) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-submit-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-cancel-btn {
|
||||||
|
background: linear-gradient(180deg, #666 0%, #444 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboard-cancel-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
background: linear-gradient(180deg, #888 0%, #555 100%);
|
||||||
|
}
|
||||||
@@ -39,7 +39,8 @@ var assets = {
|
|||||||
"abstractfile.js",
|
"abstractfile.js",
|
||||||
"idb.js",
|
"idb.js",
|
||||||
"plugins.js",
|
"plugins.js",
|
||||||
"search.js"
|
"search.js",
|
||||||
|
"leaderboard.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"main.css",
|
"main.css",
|
||||||
@@ -49,7 +50,8 @@ var assets = {
|
|||||||
"debug.css",
|
"debug.css",
|
||||||
"songbg.css",
|
"songbg.css",
|
||||||
"view.css",
|
"view.css",
|
||||||
"search.css"
|
"search.css",
|
||||||
|
"leaderboard.css"
|
||||||
],
|
],
|
||||||
"img": [
|
"img": [
|
||||||
"notes.png",
|
"notes.png",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
275
public/src/js/leaderboard.js
Normal file
275
public/src/js/leaderboard.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
class Leaderboard {
|
||||||
|
constructor(songId, difficulty, songTitle, onClose) {
|
||||||
|
this.songId = songId
|
||||||
|
this.difficulty = difficulty
|
||||||
|
this.songTitle = songTitle
|
||||||
|
this.onClose = onClose
|
||||||
|
this.entries = []
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
this.font = strings.font
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createModal()
|
||||||
|
this.fetchLeaderboard()
|
||||||
|
this.addEventListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
createModal() {
|
||||||
|
// Create overlay
|
||||||
|
this.overlay = document.createElement("div")
|
||||||
|
this.overlay.id = "leaderboard-overlay"
|
||||||
|
this.overlay.innerHTML = `
|
||||||
|
<div id="leaderboard-modal">
|
||||||
|
<div id="leaderboard-header">
|
||||||
|
<div id="leaderboard-title">${strings.leaderboardTitle}</div>
|
||||||
|
<div id="leaderboard-song-title">${this.songTitle}</div>
|
||||||
|
<div id="leaderboard-difficulty">${this.getDifficultyName()}</div>
|
||||||
|
</div>
|
||||||
|
<div id="leaderboard-content">
|
||||||
|
<div id="leaderboard-loading">${strings.loading}</div>
|
||||||
|
</div>
|
||||||
|
<div id="leaderboard-footer">
|
||||||
|
<button id="leaderboard-close-btn">${strings.close}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(this.overlay)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDifficultyName() {
|
||||||
|
const diffNames = {
|
||||||
|
easy: strings.easy,
|
||||||
|
normal: strings.normal,
|
||||||
|
hard: strings.hard,
|
||||||
|
oni: strings.oni,
|
||||||
|
ura: strings.oni
|
||||||
|
}
|
||||||
|
return diffNames[this.difficulty] || this.difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListeners() {
|
||||||
|
this.overlay.querySelector("#leaderboard-close-btn").addEventListener("click", () => this.close())
|
||||||
|
this.overlay.addEventListener("click", (e) => {
|
||||||
|
if (e.target === this.overlay) {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keyboard support
|
||||||
|
this.keyHandler = (e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", this.keyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLeaderboard() {
|
||||||
|
const url = `api/leaderboard?song_id=${encodeURIComponent(this.songId)}&difficulty=${encodeURIComponent(this.difficulty)}`
|
||||||
|
|
||||||
|
loader.ajax(url).then(response => {
|
||||||
|
const data = JSON.parse(response)
|
||||||
|
if (data.status === "ok") {
|
||||||
|
this.entries = data.entries
|
||||||
|
this.monthKey = data.month_key
|
||||||
|
this.loading = false
|
||||||
|
this.render()
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to load leaderboard")
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Leaderboard fetch error:", error)
|
||||||
|
this.error = error
|
||||||
|
this.loading = false
|
||||||
|
this.render()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const content = this.overlay.querySelector("#leaderboard-content")
|
||||||
|
|
||||||
|
if (this.error) {
|
||||||
|
content.innerHTML = `<div id="leaderboard-error">${strings.errorOccured}</div>`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.entries.length === 0) {
|
||||||
|
content.innerHTML = `<div id="leaderboard-empty">${strings.noEntries}</div>`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build entries table
|
||||||
|
let html = `
|
||||||
|
<div id="leaderboard-table">
|
||||||
|
<div class="leaderboard-row leaderboard-header-row">
|
||||||
|
<div class="leaderboard-rank">${strings.leaderboardRank}</div>
|
||||||
|
<div class="leaderboard-name">${strings.leaderboardPlayer}</div>
|
||||||
|
<div class="leaderboard-score">${strings.leaderboardScore}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
for (const entry of this.entries) {
|
||||||
|
const rankClass = entry.rank <= 3 ? `leaderboard-rank-${entry.rank}` : ""
|
||||||
|
html += `
|
||||||
|
<div class="leaderboard-row ${rankClass}">
|
||||||
|
<div class="leaderboard-rank">${entry.rank}</div>
|
||||||
|
<div class="leaderboard-name">${this.escapeHtml(entry.username)}</div>
|
||||||
|
<div class="leaderboard-score">${entry.score_value.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</div>`
|
||||||
|
content.innerHTML = html
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement("div")
|
||||||
|
div.textContent = text
|
||||||
|
return div.innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
document.removeEventListener("keydown", this.keyHandler)
|
||||||
|
this.overlay.remove()
|
||||||
|
if (this.onClose) {
|
||||||
|
this.onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score submission modal for when players qualify for leaderboard
|
||||||
|
class LeaderboardSubmit {
|
||||||
|
constructor(songId, difficulty, score, rank, onSubmit, onClose) {
|
||||||
|
this.songId = songId
|
||||||
|
this.difficulty = difficulty
|
||||||
|
this.score = score
|
||||||
|
this.rank = rank
|
||||||
|
this.onSubmit = onSubmit
|
||||||
|
this.onClose = onClose
|
||||||
|
this.submitting = false
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createModal()
|
||||||
|
this.addEventListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
createModal() {
|
||||||
|
this.overlay = document.createElement("div")
|
||||||
|
this.overlay.id = "leaderboard-submit-overlay"
|
||||||
|
|
||||||
|
const rankText = strings.newRecord.replace("%s", this.rank)
|
||||||
|
|
||||||
|
this.overlay.innerHTML = `
|
||||||
|
<div id="leaderboard-submit-modal">
|
||||||
|
<div id="leaderboard-submit-title">${rankText}</div>
|
||||||
|
<div id="leaderboard-submit-score">${this.score.toLocaleString()} ${strings.points}</div>
|
||||||
|
<div id="leaderboard-submit-form">
|
||||||
|
<label for="leaderboard-name-input">${strings.enterName}</label>
|
||||||
|
<input type="text" id="leaderboard-name-input" maxlength="10" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div id="leaderboard-submit-buttons">
|
||||||
|
<button id="leaderboard-submit-btn">${strings.submit}</button>
|
||||||
|
<button id="leaderboard-cancel-btn">${strings.cancel}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(this.overlay)
|
||||||
|
|
||||||
|
// Focus input
|
||||||
|
setTimeout(() => {
|
||||||
|
this.overlay.querySelector("#leaderboard-name-input").focus()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListeners() {
|
||||||
|
const submitBtn = this.overlay.querySelector("#leaderboard-submit-btn")
|
||||||
|
const cancelBtn = this.overlay.querySelector("#leaderboard-cancel-btn")
|
||||||
|
const input = this.overlay.querySelector("#leaderboard-name-input")
|
||||||
|
|
||||||
|
submitBtn.addEventListener("click", () => this.submit())
|
||||||
|
cancelBtn.addEventListener("click", () => this.close())
|
||||||
|
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
this.submit()
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
if (this.submitting) return
|
||||||
|
|
||||||
|
const input = this.overlay.querySelector("#leaderboard-name-input")
|
||||||
|
const username = input.value.trim()
|
||||||
|
|
||||||
|
if (username.length < 1 || username.length > 10) {
|
||||||
|
input.classList.add("error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true
|
||||||
|
const submitBtn = this.overlay.querySelector("#leaderboard-submit-btn")
|
||||||
|
submitBtn.disabled = true
|
||||||
|
submitBtn.textContent = strings.loading
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
song_id: String(this.songId),
|
||||||
|
difficulty: this.difficulty,
|
||||||
|
username: username,
|
||||||
|
score: this.score
|
||||||
|
}
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("POST", "api/score/leaderboard/save")
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/json")
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(xhr.responseText)
|
||||||
|
if (result.status === "ok") {
|
||||||
|
this.close()
|
||||||
|
if (this.onSubmit) {
|
||||||
|
this.onSubmit(username)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(result.message || "Submit failed")
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleError("Invalid response")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError("Server error " + xhr.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.onerror = () => {
|
||||||
|
handleError("Network error")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (msg) => {
|
||||||
|
console.error("Leaderboard submit error:", msg)
|
||||||
|
this.submitting = false
|
||||||
|
submitBtn.disabled = false
|
||||||
|
submitBtn.textContent = strings.submit
|
||||||
|
input.classList.add("error")
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.overlay.remove()
|
||||||
|
if (this.onClose) {
|
||||||
|
this.onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
class Scoresheet{
|
class Scoresheet {
|
||||||
constructor(...args){
|
constructor(...args) {
|
||||||
this.init(...args)
|
this.init(...args)
|
||||||
}
|
}
|
||||||
init(controller, results, multiplayer, touchEnabled){
|
init(controller, results, multiplayer, touchEnabled) {
|
||||||
this.controller = controller
|
this.controller = controller
|
||||||
this.resultsObj = results
|
this.resultsObj = results
|
||||||
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
|
this.player = [multiplayer ? (p2.player === 1 ? 0 : 1) : 0]
|
||||||
@@ -11,12 +11,12 @@ class Scoresheet{
|
|||||||
this.results[player0] = {}
|
this.results[player0] = {}
|
||||||
this.rules = []
|
this.rules = []
|
||||||
this.rules[player0] = this.controller.game.rules
|
this.rules[player0] = this.controller.game.rules
|
||||||
if(multiplayer){
|
if (multiplayer) {
|
||||||
this.player.push(p2.player === 2 ? 0 : 1)
|
this.player.push(p2.player === 2 ? 0 : 1)
|
||||||
this.results[this.player[1]] = p2.results
|
this.results[this.player[1]] = p2.results
|
||||||
this.rules[this.player[1]] = this.controller.syncWith.game.rules
|
this.rules[this.player[1]] = this.controller.syncWith.game.rules
|
||||||
}
|
}
|
||||||
for(var i in results){
|
for (var i in results) {
|
||||||
this.results[player0][i] = results[i] === null ? null : results[i].toString()
|
this.results[player0][i] = results[i] === null ? null : results[i].toString()
|
||||||
}
|
}
|
||||||
this.multiplayer = multiplayer
|
this.multiplayer = multiplayer
|
||||||
@@ -26,10 +26,10 @@ class Scoresheet{
|
|||||||
this.ctx = this.canvas.getContext("2d")
|
this.ctx = this.canvas.getContext("2d")
|
||||||
var resolution = settings.getItem("resolution")
|
var resolution = settings.getItem("resolution")
|
||||||
var noSmoothing = resolution === "low" || resolution === "lowest"
|
var noSmoothing = resolution === "low" || resolution === "lowest"
|
||||||
if(noSmoothing){
|
if (noSmoothing) {
|
||||||
this.ctx.imageSmoothingEnabled = false
|
this.ctx.imageSmoothingEnabled = false
|
||||||
}
|
}
|
||||||
if(resolution === "lowest"){
|
if (resolution === "lowest") {
|
||||||
this.canvas.style.imageRendering = "pixelated"
|
this.canvas.style.imageRendering = "pixelated"
|
||||||
}
|
}
|
||||||
this.game = document.getElementById("game")
|
this.game = document.getElementById("game")
|
||||||
@@ -78,12 +78,12 @@ class Scoresheet{
|
|||||||
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
|
assets.sounds["bgm_result"].playLoop(3, false, 0, 0.847, 17.689)
|
||||||
|
|
||||||
this.session = p2.session
|
this.session = p2.session
|
||||||
if(this.session){
|
if (this.session) {
|
||||||
if(p2.getMessage("songsel")){
|
if (p2.getMessage("songsel")) {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
pageEvents.add(p2, "message", response => {
|
pageEvents.add(p2, "message", response => {
|
||||||
if(response.type === "songsel"){
|
if (response.type === "songsel") {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -100,51 +100,51 @@ class Scoresheet{
|
|||||||
touchEvents: controller.view.touchEvents
|
touchEvents: controller.view.touchEvents
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
keyDown(pressed){
|
keyDown(pressed) {
|
||||||
if(pressed && this.redrawing){
|
if (pressed && this.redrawing) {
|
||||||
this.toNext()
|
this.toNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mouseDown(event){
|
mouseDown(event) {
|
||||||
if(event.type === "touchstart"){
|
if (event.type === "touchstart") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.canvas.style.cursor = ""
|
this.canvas.style.cursor = ""
|
||||||
this.state.pointerLocked = true
|
this.state.pointerLocked = true
|
||||||
}else{
|
} else {
|
||||||
this.state.pointerLocked = false
|
this.state.pointerLocked = false
|
||||||
if(event.which !== 1){
|
if (event.which !== 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.toNext()
|
this.toNext()
|
||||||
}
|
}
|
||||||
toNext(){
|
toNext() {
|
||||||
var elapsed = this.getMS() - this.state.screenMS
|
var elapsed = this.getMS() - this.state.screenMS
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= this.state.startDelay){
|
if (this.state.screen === "fadeIn" && elapsed >= this.state.startDelay) {
|
||||||
this.toScoresShown()
|
this.toScoresShown()
|
||||||
}else if(this.state.screen === "scoresShown" && elapsed >= 1000){
|
} else if (this.state.screen === "scoresShown" && elapsed >= 1000) {
|
||||||
this.toSongsel()
|
this.toSongsel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toScoresShown(){
|
toScoresShown() {
|
||||||
if(!p2.session){
|
if (!p2.session) {
|
||||||
this.state.screen = "scoresShown"
|
this.state.screen = "scoresShown"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
this.controller.playSound("neiro_1_don", 0, true)
|
this.controller.playSound("neiro_1_don", 0, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toSongsel(fromP2){
|
toSongsel(fromP2) {
|
||||||
if(!p2.session || fromP2){
|
if (!p2.session || fromP2) {
|
||||||
snd.musicGain.fadeOut(0.5)
|
snd.musicGain.fadeOut(0.5)
|
||||||
this.state.screen = "fadeOut"
|
this.state.screen = "fadeOut"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
if(!fromP2){
|
if (!fromP2) {
|
||||||
this.controller.playSound("neiro_1_don", 0, true)
|
this.controller.playSound("neiro_1_don", 0, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startRedraw(){
|
startRedraw() {
|
||||||
this.redrawing = true
|
this.redrawing = true
|
||||||
requestAnimationFrame(this.redrawBind)
|
requestAnimationFrame(this.redrawBind)
|
||||||
this.winW = null
|
this.winW = null
|
||||||
@@ -152,7 +152,7 @@ class Scoresheet{
|
|||||||
|
|
||||||
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
|
pageEvents.add(this.canvas, ["mousedown", "touchstart"], this.mouseDown.bind(this))
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
this.tetsuoHana = document.createElement("div")
|
this.tetsuoHana = document.createElement("div")
|
||||||
this.tetsuoHana.id = "tetsuohana"
|
this.tetsuoHana.id = "tetsuohana"
|
||||||
var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
|
var flowersBg = "url('" + assets.image["results_flowers"].src + "')"
|
||||||
@@ -160,12 +160,12 @@ class Scoresheet{
|
|||||||
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
|
var tetsuoHanaBg = "url('" + assets.image["results_tetsuohana" + (debugObj.state === "closed" ? "" : "2")].src + "')"
|
||||||
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
|
var id = ["flowers1", "flowers2", "mikoshi", "tetsuo", "hana"]
|
||||||
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
|
var bg = [flowersBg, flowersBg, mikoshiBg, tetsuoHanaBg, tetsuoHanaBg]
|
||||||
for(var i = 0; i < id.length; i++){
|
for (var i = 0; i < id.length; i++) {
|
||||||
if(id[i] === "mikoshi"){
|
if (id[i] === "mikoshi") {
|
||||||
var divOut = document.createElement("div")
|
var divOut = document.createElement("div")
|
||||||
divOut.id = id[i] + "-out"
|
divOut.id = id[i] + "-out"
|
||||||
this.tetsuoHana.appendChild(divOut)
|
this.tetsuoHana.appendChild(divOut)
|
||||||
}else{
|
} else {
|
||||||
var divOut = this.tetsuoHana
|
var divOut = this.tetsuoHana
|
||||||
}
|
}
|
||||||
var div = document.createElement("div")
|
var div = document.createElement("div")
|
||||||
@@ -180,16 +180,16 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redraw(){
|
redraw() {
|
||||||
if(!this.redrawRunning){
|
if (!this.redrawRunning) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(this.redrawing){
|
if (this.redrawing) {
|
||||||
requestAnimationFrame(this.redrawBind)
|
requestAnimationFrame(this.redrawBind)
|
||||||
}
|
}
|
||||||
var ms = this.getMS()
|
var ms = this.getMS()
|
||||||
|
|
||||||
if(!this.redrawRunning){
|
if (!this.redrawRunning) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,11 +200,11 @@ class Scoresheet{
|
|||||||
var winH = lastHeight
|
var winH = lastHeight
|
||||||
this.pixelRatio = window.devicePixelRatio || 1
|
this.pixelRatio = window.devicePixelRatio || 1
|
||||||
var resolution = settings.getItem("resolution")
|
var resolution = settings.getItem("resolution")
|
||||||
if(resolution === "medium"){
|
if (resolution === "medium") {
|
||||||
this.pixelRatio *= 0.75
|
this.pixelRatio *= 0.75
|
||||||
}else if(resolution === "low"){
|
} else if (resolution === "low") {
|
||||||
this.pixelRatio *= 0.5
|
this.pixelRatio *= 0.5
|
||||||
}else if(resolution === "lowest"){
|
} else if (resolution === "lowest") {
|
||||||
this.pixelRatio *= 0.25
|
this.pixelRatio *= 0.25
|
||||||
}
|
}
|
||||||
winW *= this.pixelRatio
|
winW *= this.pixelRatio
|
||||||
@@ -213,8 +213,8 @@ class Scoresheet{
|
|||||||
var ratioY = winH / 720
|
var ratioY = winH / 720
|
||||||
var ratio = (ratioX < ratioY ? ratioX : ratioY)
|
var ratio = (ratioX < ratioY ? ratioX : ratioY)
|
||||||
|
|
||||||
if(this.redrawing){
|
if (this.redrawing) {
|
||||||
if(this.winW !== winW || this.winH !== winH){
|
if (this.winW !== winW || this.winH !== winH) {
|
||||||
this.canvas.width = Math.max(1, winW)
|
this.canvas.width = Math.max(1, winW)
|
||||||
this.canvas.height = Math.max(1, winH)
|
this.canvas.height = Math.max(1, winH)
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
@@ -224,37 +224,37 @@ class Scoresheet{
|
|||||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||||
this.nameplateCache.resize(274, 134, ratio + 0.2)
|
this.nameplateCache.resize(274, 134, ratio + 0.2)
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
|
this.tetsuoHana.style.setProperty("--scale", ratio / this.pixelRatio)
|
||||||
if(this.tetsuoHanaClass === "dance"){
|
if (this.tetsuoHanaClass === "dance") {
|
||||||
this.tetsuoHana.classList.remove("dance", "dance2")
|
this.tetsuoHana.classList.remove("dance", "dance2")
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.tetsuoHana.classList.add("dance2")
|
this.tetsuoHana.classList.add("dance2")
|
||||||
},50)
|
}, 50)
|
||||||
}else if(this.tetsuoHanaClass === "failed"){
|
} else if (this.tetsuoHanaClass === "failed") {
|
||||||
this.tetsuoHana.classList.remove("failed")
|
this.tetsuoHana.classList.remove("failed")
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.tetsuoHana.classList.add("failed")
|
this.tetsuoHana.classList.add("failed")
|
||||||
},50)
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else if(!document.hasFocus() && this.state.screen === "scoresShown"){
|
} else if (!document.hasFocus() && this.state.screen === "scoresShown") {
|
||||||
if(this.state["countup0"]){
|
if (this.state["countup0"]) {
|
||||||
this.stopSound("se_results_countup", 0)
|
this.stopSound("se_results_countup", 0)
|
||||||
}
|
}
|
||||||
if(this.state["countup1"]){
|
if (this.state["countup1"]) {
|
||||||
this.stopSound("se_results_countup", 1)
|
this.stopSound("se_results_countup", 1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}else{
|
} else {
|
||||||
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
|
ctx.clearRect(0, 0, winW / ratio, winH / ratio)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
if(!this.canvasCache.canvas){
|
if (!this.canvasCache.canvas) {
|
||||||
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
this.canvasCache.resize(winW / ratio, 80 + 1, ratio)
|
||||||
}
|
}
|
||||||
if(!this.nameplateCache.canvas){
|
if (!this.nameplateCache.canvas) {
|
||||||
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
this.nameplateCache.resize(274, 67, ratio + 0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,14 +272,14 @@ class Scoresheet{
|
|||||||
|
|
||||||
var bgOffset = 0
|
var bgOffset = 0
|
||||||
var elapsed = ms - this.state.screenMS
|
var elapsed = ms - this.state.screenMS
|
||||||
if(this.state.screen === "fadeIn" && elapsed < 1000){
|
if (this.state.screen === "fadeIn" && elapsed < 1000) {
|
||||||
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
|
bgOffset = Math.min(1, this.draw.easeIn(1 - elapsed / 1000)) * (winH / 2)
|
||||||
}
|
}
|
||||||
if((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved){
|
if ((this.state.screen !== "fadeIn" || elapsed >= 1000) && !this.scoreSaved) {
|
||||||
this.saveScore()
|
this.saveScore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(0, -bgOffset)
|
ctx.translate(0, -bgOffset)
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ class Scoresheet{
|
|||||||
ctx.fillRect(0, winH / 2 - 12, winW, 12)
|
ctx.fillRect(0, winH / 2 - 12, winW, 12)
|
||||||
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
|
ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
|
||||||
ctx.fillRect(0, winH / 2, winW, 20)
|
ctx.fillRect(0, winH / 2, winW, 20)
|
||||||
if(bgOffset !== 0){
|
if (bgOffset !== 0) {
|
||||||
ctx.fillStyle = "#000"
|
ctx.fillStyle = "#000"
|
||||||
ctx.fillRect(0, winH / 2 - 2, winW, 2)
|
ctx.fillRect(0, winH / 2 - 2, winW, 2)
|
||||||
}
|
}
|
||||||
@@ -306,7 +306,7 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = "#bf2900"
|
ctx.fillStyle = "#bf2900"
|
||||||
ctx.fillRect(0, frameTop + 64, winW, 8)
|
ctx.fillRect(0, frameTop + 64, winW, 8)
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(0, bgOffset)
|
ctx.translate(0, bgOffset)
|
||||||
@@ -325,9 +325,9 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
|
ctx.fillStyle = this.multiplayer ? "rgba(138, 245, 247, 0.5)" : "rgba(249, 163, 149, 0.5)"
|
||||||
ctx.fillRect(0, winH / 2, winW, 12)
|
ctx.fillRect(0, winH / 2, winW, 12)
|
||||||
ctx.fillStyle = "#000"
|
ctx.fillStyle = "#000"
|
||||||
if(bgOffset === 0){
|
if (bgOffset === 0) {
|
||||||
ctx.fillRect(0, winH / 2 - 2, winW, 4)
|
ctx.fillRect(0, winH / 2 - 2, winW, 4)
|
||||||
}else{
|
} else {
|
||||||
ctx.fillRect(0, winH / 2, winW, 2)
|
ctx.fillRect(0, winH / 2, winW, 2)
|
||||||
}
|
}
|
||||||
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
|
ctx.fillStyle = this.multiplayer ? "#6bbec0" : "#fa4529"
|
||||||
@@ -337,46 +337,46 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
|
ctx.fillStyle = this.multiplayer ? "#a8e0e0" : "#ff9b7a"
|
||||||
ctx.fillRect(0, winH - frameTop - 66, winW, 2)
|
ctx.fillRect(0, winH - frameTop - 66, winW, 2)
|
||||||
|
|
||||||
if(bgOffset){
|
if (bgOffset) {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "scoresShown" || this.state.screen === "fadeOut"){
|
if (this.state.screen === "scoresShown" || this.state.screen === "fadeOut") {
|
||||||
var elapsed = Infinity
|
var elapsed = Infinity
|
||||||
}else if(this.redrawing){
|
} else if (this.redrawing) {
|
||||||
var elapsed = ms - this.state.screenMS - this.state.startDelay
|
var elapsed = ms - this.state.screenMS - this.state.startDelay
|
||||||
}else{
|
} else {
|
||||||
var elapsed = 0
|
var elapsed = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules = this.controller.game.rules
|
var rules = this.controller.game.rules
|
||||||
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
|
var failedOffset = rules.clearReached(this.results[this.player[0]].gauge) ? 0 : -2000
|
||||||
if(players === 2 && failedOffset !== 0){
|
if (players === 2 && failedOffset !== 0) {
|
||||||
var p2results = this.results[this.player[1]]
|
var p2results = this.results[this.player[1]]
|
||||||
if(p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)){
|
if (p2results && this.controller.syncWith.game.rules.clearReached(p2results.gauge)) {
|
||||||
failedOffset = 0
|
failedOffset = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(elapsed >= 3100 + failedOffset){
|
if (elapsed >= 3100 + failedOffset) {
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var clear = this.rules[p].clearReached(results.gauge)
|
var clear = this.rules[p].clearReached(results.gauge)
|
||||||
if(p === 1 || !this.multiplayer && clear){
|
if (p === 1 || !this.multiplayer && clear) {
|
||||||
ctx.translate(0, 290)
|
ctx.translate(0, 290)
|
||||||
}
|
}
|
||||||
if(clear){
|
if (clear) {
|
||||||
ctx.globalCompositeOperation = "lighter"
|
ctx.globalCompositeOperation = "lighter"
|
||||||
}
|
}
|
||||||
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
|
ctx.globalAlpha = Math.min(1, Math.max(0, (elapsed - (3100 + failedOffset)) / 500)) * 0.5
|
||||||
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
|
var grd = ctx.createLinearGradient(0, frameTop + 72, 0, frameTop + 368)
|
||||||
grd.addColorStop(0, "#000")
|
grd.addColorStop(0, "#000")
|
||||||
if(clear){
|
if (clear) {
|
||||||
grd.addColorStop(1, "#ffffba")
|
grd.addColorStop(1, "#ffffba")
|
||||||
}else{
|
} else {
|
||||||
grd.addColorStop(1, "transparent")
|
grd.addColorStop(1, "transparent")
|
||||||
}
|
}
|
||||||
ctx.fillStyle = grd
|
ctx.fillStyle = grd
|
||||||
@@ -385,10 +385,10 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 0){
|
if (elapsed >= 0) {
|
||||||
if(this.state.hasPointer === 0){
|
if (this.state.hasPointer === 0) {
|
||||||
this.state.hasPointer = 1
|
this.state.hasPointer = 1
|
||||||
if(!this.state.pointerLocked){
|
if (!this.state.pointerLocked) {
|
||||||
this.canvas.style.cursor = this.session ? "" : "pointer"
|
this.canvas.style.cursor = this.session ? "" : "pointer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,13 +416,13 @@ class Scoresheet{
|
|||||||
letterSpacing: strings.id === "en" ? 0 : 3,
|
letterSpacing: strings.id === "en" ? 0 : 3,
|
||||||
forceShadow: true
|
forceShadow: true
|
||||||
}, [
|
}, [
|
||||||
{x: -2, y: -2, outline: "#000", letterBorder: 22},
|
{ x: -2, y: -2, outline: "#000", letterBorder: 22 },
|
||||||
{},
|
{},
|
||||||
{x: 2, y: 2, shadow: [2, 2, 7]},
|
{ x: 2, y: 2, shadow: [2, 2, 7] },
|
||||||
{x: 2, y: 2, outline: "#ad1516", letterBorder: 10},
|
{ x: 2, y: 2, outline: "#ad1516", letterBorder: 10 },
|
||||||
{x: -2, y: -2, outline: "#ff797b"},
|
{ x: -2, y: -2, outline: "#ff797b" },
|
||||||
{outline: "#f70808"},
|
{ outline: "#f70808" },
|
||||||
{fill: "#fff", shadow: [-1, 1, 3, 1.5]}
|
{ fill: "#fff", shadow: [-1, 1, 3, 1.5] }
|
||||||
])
|
])
|
||||||
|
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
@@ -436,18 +436,18 @@ class Scoresheet{
|
|||||||
align: "right",
|
align: "right",
|
||||||
forceShadow: true
|
forceShadow: true
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 10, shadow: [1, 1, 3]},
|
{ outline: "#000", letterBorder: 10, shadow: [1, 1, 3] },
|
||||||
{fill: "#fff"}
|
{ fill: "#fff" }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,9 +470,9 @@ class Scoresheet{
|
|||||||
ctx.miterLimit = 10
|
ctx.miterLimit = 10
|
||||||
|
|
||||||
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
|
var defaultName = p === 0 ? strings.defaultName : strings.default2PName
|
||||||
if(p === this.player[0]){
|
if (p === this.player[0]) {
|
||||||
var name = account.loggedIn ? account.displayName : defaultName
|
var name = account.loggedIn ? account.displayName : defaultName
|
||||||
}else{
|
} else {
|
||||||
var name = results.name || defaultName
|
var name = results.name || defaultName
|
||||||
}
|
}
|
||||||
this.nameplateCache.get({
|
this.nameplateCache.get({
|
||||||
@@ -493,7 +493,7 @@ class Scoresheet{
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if(this.controller.autoPlayEnabled){
|
if (this.controller.autoPlayEnabled) {
|
||||||
ctx.drawImage(assets.image["badge_auto"],
|
ctx.drawImage(assets.image["badge_auto"],
|
||||||
431, 311, 34, 34
|
431, 311, 34, 34
|
||||||
)
|
)
|
||||||
@@ -549,8 +549,8 @@ class Scoresheet{
|
|||||||
align: "right",
|
align: "right",
|
||||||
width: 36
|
width: 36
|
||||||
}, [
|
}, [
|
||||||
{fill: "#fff"},
|
{ fill: "#fff" },
|
||||||
{outline: "#000", letterBorder: 0.5}
|
{ outline: "#000", letterBorder: 0.5 }
|
||||||
])
|
])
|
||||||
|
|
||||||
this.draw.score({
|
this.draw.score({
|
||||||
@@ -590,8 +590,8 @@ class Scoresheet{
|
|||||||
width: 154,
|
width: 154,
|
||||||
letterSpacing: strings.id === "ja" ? 1 : 0
|
letterSpacing: strings.id === "ja" ? 1 : 0
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 8},
|
{ outline: "#000", letterBorder: 8 },
|
||||||
{fill: grd}
|
{ fill: grd }
|
||||||
])
|
])
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -604,8 +604,8 @@ class Scoresheet{
|
|||||||
width: 154,
|
width: 154,
|
||||||
letterSpacing: strings.id === "ja" ? 4 : 0
|
letterSpacing: strings.id === "ja" ? 4 : 0
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 8},
|
{ outline: "#000", letterBorder: 8 },
|
||||||
{fill: "#ffc700"}
|
{ fill: "#ffc700" }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
@@ -613,15 +613,15 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
if(elapsed >= 400 && elapsed < 3100 + failedOffset){
|
if (elapsed >= 400 && elapsed < 3100 + failedOffset) {
|
||||||
if(this.tetsuoHanaClass !== "fadein"){
|
if (this.tetsuoHanaClass !== "fadein") {
|
||||||
this.tetsuoHana.classList.add("fadein")
|
this.tetsuoHana.classList.add("fadein")
|
||||||
this.tetsuoHanaClass = "fadein"
|
this.tetsuoHanaClass = "fadein"
|
||||||
}
|
}
|
||||||
}else if(elapsed >= 3100 + failedOffset){
|
} else if (elapsed >= 3100 + failedOffset) {
|
||||||
if(this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed"){
|
if (this.tetsuoHanaClass !== "dance" && this.tetsuoHanaClass !== "failed") {
|
||||||
if(this.tetsuoHanaClass){
|
if (this.tetsuoHanaClass) {
|
||||||
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
|
this.tetsuoHana.classList.remove(this.tetsuoHanaClass)
|
||||||
}
|
}
|
||||||
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
|
this.tetsuoHanaClass = this.rules[this.player[0]].clearReached(this.results[this.player[0]].gauge) ? "dance" : "failed"
|
||||||
@@ -630,19 +630,19 @@ class Scoresheet{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 800){
|
if (elapsed >= 800) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
|
this.draw.alpha(Math.min(1, (elapsed - 800) / 500), ctx, ctx => {
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
var w = 712
|
var w = 712
|
||||||
@@ -670,50 +670,50 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 1200){
|
if (elapsed >= 1200) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
var noCrownResultWait = -2000;
|
var noCrownResultWait = -2000;
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var crownType = null
|
var crownType = null
|
||||||
if(this.rules[p].clearReached(results.gauge)){
|
if (this.rules[p].clearReached(results.gauge)) {
|
||||||
crownType = results.bad === "0" ? "gold" : "silver"
|
crownType = results.bad === "0" ? "gold" : "silver"
|
||||||
}
|
}
|
||||||
if(crownType !== null){
|
if (crownType !== null) {
|
||||||
noCrownResultWait = 0;
|
noCrownResultWait = 0;
|
||||||
var amount = Math.min(1, (elapsed - 1200) / 450)
|
var amount = Math.min(1, (elapsed - 1200) / 450)
|
||||||
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
|
this.draw.alpha(this.draw.easeIn(amount), ctx, ctx => {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.scale(ratio, ratio)
|
ctx.scale(ratio, ratio)
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
var crownScale = 1
|
var crownScale = 1
|
||||||
var shine = 0
|
var shine = 0
|
||||||
if(amount < 1){
|
if (amount < 1) {
|
||||||
crownScale = 2.8 * (1 - amount) + 0.9
|
crownScale = 2.8 * (1 - amount) + 0.9
|
||||||
}else if(elapsed < 1850){
|
} else if (elapsed < 1850) {
|
||||||
crownScale = 0.9 + (elapsed - 1650) / 2000
|
crownScale = 0.9 + (elapsed - 1650) / 2000
|
||||||
}else if(elapsed < 2200){
|
} else if (elapsed < 2200) {
|
||||||
shine = (elapsed - 1850) / 175
|
shine = (elapsed - 1850) / 175
|
||||||
if(shine > 1){
|
if (shine > 1) {
|
||||||
shine = 2 - shine
|
shine = 2 - shine
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]){
|
if (this.state.screen === "fadeIn" && elapsed >= 1200 && !this.state["fullcomboPlayed" + p]) {
|
||||||
this.state["fullcomboPlayed" + p] = true
|
this.state["fullcomboPlayed" + p] = true
|
||||||
if(crownType === "gold"){
|
if (crownType === "gold") {
|
||||||
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
|
this.playSound("v_results_fullcombo" + (p === 1 ? "2" : ""), p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]){
|
if (this.state.screen === "fadeIn" && elapsed >= 1650 && !this.state["crownPlayed" + p]) {
|
||||||
this.state["crownPlayed" + p] = true
|
this.state["crownPlayed" + p] = true
|
||||||
this.playSound("se_results_crown", p)
|
this.playSound("se_results_crown", p)
|
||||||
}
|
}
|
||||||
@@ -735,51 +735,51 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(elapsed >= 2400 + noCrownResultWait){
|
if (elapsed >= 2400 + noCrownResultWait) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.translate(frameLeft, frameTop)
|
ctx.translate(frameLeft, frameTop)
|
||||||
|
|
||||||
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
|
var printNumbers = ["good", "ok", "bad", "maxCombo", "drumroll"]
|
||||||
if(!this.state["countupTime0"]){
|
if (!this.state["countupTime0"]) {
|
||||||
var times = {}
|
var times = {}
|
||||||
var lastTime = 0
|
var lastTime = 0
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
|
var currentTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame
|
||||||
if(currentTime > lastTime){
|
if (currentTime > lastTime) {
|
||||||
lastTime = currentTime
|
lastTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
var largestTime = 0
|
var largestTime = 0
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
times[printNumbers[i]] = lastTime + 500
|
times[printNumbers[i]] = lastTime + 500
|
||||||
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
var currentTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
||||||
if(currentTime > largestTime){
|
if (currentTime > largestTime) {
|
||||||
largestTime = currentTime
|
largestTime = currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastTime = largestTime
|
lastTime = largestTime
|
||||||
}
|
}
|
||||||
this.state.fadeInEnd = lastTime
|
this.state.fadeInEnd = lastTime
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
this.state["countupTime" + p] = times
|
this.state["countupTime" + p] = times
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var p = 0; p < players; p++){
|
for (var p = 0; p < players; p++) {
|
||||||
var results = this.results[p]
|
var results = this.results[p]
|
||||||
if(!results){
|
if (!results) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if(p === 1){
|
if (p === 1) {
|
||||||
ctx.translate(0, p2Offset)
|
ctx.translate(0, p2Offset)
|
||||||
}
|
}
|
||||||
ctx.save()
|
ctx.save()
|
||||||
@@ -795,24 +795,24 @@ class Scoresheet{
|
|||||||
ctx.fillStyle = "#fff"
|
ctx.fillStyle = "#fff"
|
||||||
ctx.strokeStyle = "#fff"
|
ctx.strokeStyle = "#fff"
|
||||||
ctx.lineWidth = 0.5
|
ctx.lineWidth = 0.5
|
||||||
for(var i = 0; i < points.length; i++){
|
for (var i = 0; i < points.length; i++) {
|
||||||
ctx.translate(-23.3 * scale, 0)
|
ctx.translate(-23.3 * scale, 0)
|
||||||
ctx.fillText(points[points.length - i - 1], 0, 0)
|
ctx.fillText(points[points.length - i - 1], 0, 0)
|
||||||
ctx.strokeText(points[points.length - i - 1], 0, 0)
|
ctx.strokeText(points[points.length - i - 1], 0, 0)
|
||||||
}
|
}
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
if(!this.state["countupTime" + p]){
|
if (!this.state["countupTime" + p]) {
|
||||||
var times = {}
|
var times = {}
|
||||||
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
|
var lastTime = 3100 + noCrownResultWait + results.points.length * 30 * this.frame + 1000
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
times[printNumbers[i]] = lastTime + 500
|
times[printNumbers[i]] = lastTime + 500
|
||||||
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
lastTime = lastTime + 500 + results[printNumbers[i]].length * 30 * this.frame
|
||||||
}
|
}
|
||||||
this.state["countupTime" + p] = times
|
this.state["countupTime" + p] = times
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var i in printNumbers){
|
for (var i in printNumbers) {
|
||||||
var start = this.state["countupTime" + p][printNumbers[i]]
|
var start = this.state["countupTime" + p][printNumbers[i]]
|
||||||
this.draw.layeredText({
|
this.draw.layeredText({
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -824,25 +824,25 @@ class Scoresheet{
|
|||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
align: "right"
|
align: "right"
|
||||||
}, [
|
}, [
|
||||||
{outline: "#000", letterBorder: 9},
|
{ outline: "#000", letterBorder: 9 },
|
||||||
{fill: "#fff"}
|
{ fill: "#fff" }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.countupShown){
|
if (this.state.countupShown) {
|
||||||
if(!this.state["countup" + p]){
|
if (!this.state["countup" + p]) {
|
||||||
this.state["countup" + p] = true
|
this.state["countup" + p] = true
|
||||||
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
|
this.loopSound("se_results_countup", p, [0.1, false, 0, 0, 0.07])
|
||||||
}
|
}
|
||||||
}else if(this.state["countup" + p]){
|
} else if (this.state["countup" + p]) {
|
||||||
this.state["countup" + p] = false
|
this.state["countup" + p] = false
|
||||||
this.stopSound("se_results_countup", p)
|
this.stopSound("se_results_countup", p)
|
||||||
if(this.state.screen === "fadeIn"){
|
if (this.state.screen === "fadeIn") {
|
||||||
this.playSound("neiro_1_don", p)
|
this.playSound("neiro_1_don", p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd){
|
if (this.state.screen === "fadeIn" && elapsed >= this.state.fadeInEnd) {
|
||||||
this.state.screen = "scoresShown"
|
this.state.screen = "scoresShown"
|
||||||
this.state.screenMS = this.getMS()
|
this.state.screenMS = this.getMS()
|
||||||
}
|
}
|
||||||
@@ -850,28 +850,28 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000){
|
if (this.session && !this.state.scoreNext && this.state.screen === "scoresShown" && ms - this.state.screenMS >= 10000) {
|
||||||
this.state.scoreNext = true
|
this.state.scoreNext = true
|
||||||
if(p2.session){
|
if (p2.session) {
|
||||||
p2.send("songsel")
|
p2.send("songsel")
|
||||||
}else{
|
} else {
|
||||||
this.toSongsel(true)
|
this.toSongsel(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.screen === "fadeOut"){
|
if (this.state.screen === "fadeOut") {
|
||||||
if(this.state.hasPointer === 1){
|
if (this.state.hasPointer === 1) {
|
||||||
this.state.hasPointer = 2
|
this.state.hasPointer = 2
|
||||||
this.canvas.style.cursor = ""
|
this.canvas.style.cursor = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!this.fadeScreenBlack){
|
if (!this.fadeScreenBlack) {
|
||||||
this.fadeScreenBlack = true
|
this.fadeScreenBlack = true
|
||||||
this.fadeScreen.style.backgroundColor = "#000"
|
this.fadeScreen.style.backgroundColor = "#000"
|
||||||
}
|
}
|
||||||
var elapsed = ms - this.state.screenMS
|
var elapsed = ms - this.state.screenMS
|
||||||
|
|
||||||
if(elapsed >= 1000){
|
if (elapsed >= 1000) {
|
||||||
this.clean()
|
this.clean()
|
||||||
this.controller.songSelection(true, this.showWarning)
|
this.controller.songSelection(true, this.showWarning)
|
||||||
}
|
}
|
||||||
@@ -880,47 +880,47 @@ class Scoresheet{
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumber(score, start, elapsed){
|
getNumber(score, start, elapsed) {
|
||||||
var numberPos = Math.floor((elapsed - start) / this.frame)
|
var numberPos = Math.floor((elapsed - start) / this.frame)
|
||||||
if(numberPos < 0){
|
if (numberPos < 0) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var output = ""
|
var output = ""
|
||||||
for(var i = 0; i < score.length; i++){
|
for (var i = 0; i < score.length; i++) {
|
||||||
if(numberPos < 30 * (i + 1)){
|
if (numberPos < 30 * (i + 1)) {
|
||||||
this.state.countupShown = true
|
this.state.countupShown = true
|
||||||
return this.numbers[numberPos % 30] + output
|
return this.numbers[numberPos % 30] + output
|
||||||
}else{
|
} else {
|
||||||
output = score[score.length - i - 1] + output
|
output = score[score.length - i - 1] + output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
getSound(id, p){
|
getSound(id, p) {
|
||||||
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
|
return assets.sounds[id + (this.multiplayer ? "_p" + (p + 1) : "")]
|
||||||
}
|
}
|
||||||
playSound(id, p){
|
playSound(id, p) {
|
||||||
this.getSound(id, p).play()
|
this.getSound(id, p).play()
|
||||||
}
|
}
|
||||||
loopSound(id, p, args){
|
loopSound(id, p, args) {
|
||||||
this.getSound(id, p).playLoop(...args)
|
this.getSound(id, p).playLoop(...args)
|
||||||
}
|
}
|
||||||
stopSound(id, p){
|
stopSound(id, p) {
|
||||||
this.getSound(id, p).stop()
|
this.getSound(id, p).stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
mod(length, index){
|
mod(length, index) {
|
||||||
return ((index % length) + length) % length
|
return ((index % length) + length) % length
|
||||||
}
|
}
|
||||||
|
|
||||||
getMS(){
|
getMS() {
|
||||||
return Date.now()
|
return Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
saveScore(){
|
saveScore() {
|
||||||
if(this.controller.saveScore){
|
if (this.controller.saveScore) {
|
||||||
if(this.resultsObj.points < 0){
|
if (this.resultsObj.points < 0) {
|
||||||
this.resultsObj.points = 0
|
this.resultsObj.points = 0
|
||||||
}
|
}
|
||||||
var title = this.controller.selectedSong.originalTitle
|
var title = this.controller.selectedSong.originalTitle
|
||||||
@@ -929,11 +929,11 @@ class Scoresheet{
|
|||||||
var oldScore = scoreStorage.get(hash, difficulty, true)
|
var oldScore = scoreStorage.get(hash, difficulty, true)
|
||||||
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
|
var clearReached = this.controller.game.rules.clearReached(this.resultsObj.gauge)
|
||||||
var crown = ""
|
var crown = ""
|
||||||
if(clearReached){
|
if (clearReached) {
|
||||||
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
|
crown = this.resultsObj.bad === 0 ? "gold" : "silver"
|
||||||
}
|
}
|
||||||
if(!oldScore || oldScore.points <= this.resultsObj.points){
|
if (!oldScore || oldScore.points <= this.resultsObj.points) {
|
||||||
if(oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)){
|
if (oldScore && (oldScore.crown === "gold" || oldScore.crown === "silver" && !crown)) {
|
||||||
crown = oldScore.crown
|
crown = oldScore.crown
|
||||||
}
|
}
|
||||||
this.resultsObj.crown = crown
|
this.resultsObj.crown = crown
|
||||||
@@ -941,19 +941,63 @@ class Scoresheet{
|
|||||||
delete this.resultsObj.difficulty
|
delete this.resultsObj.difficulty
|
||||||
delete this.resultsObj.gauge
|
delete this.resultsObj.gauge
|
||||||
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
|
scoreStorage.add(hash, difficulty, this.resultsObj, true, title).catch(() => {
|
||||||
this.showWarning = {name: "scoreSaveFailed"}
|
this.showWarning = { name: "scoreSaveFailed" }
|
||||||
})
|
})
|
||||||
}else if(oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)){
|
} else if (oldScore && (crown === "gold" && oldScore.crown !== "gold" || crown && !oldScore.crown)) {
|
||||||
oldScore.crown = crown
|
oldScore.crown = crown
|
||||||
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
|
scoreStorage.add(hash, difficulty, oldScore, true, title).catch(() => {
|
||||||
this.showWarning = {name: "scoreSaveFailed"}
|
this.showWarning = { name: "scoreSaveFailed" }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check leaderboard eligibility
|
||||||
|
this.checkLeaderboard(hash, difficulty, this.resultsObj.points)
|
||||||
}
|
}
|
||||||
this.scoreSaved = true
|
this.scoreSaved = true
|
||||||
}
|
}
|
||||||
|
|
||||||
clean(){
|
checkLeaderboard(songId, difficulty, score) {
|
||||||
|
if (!songId || this.controller.autoPlayEnabled || this.multiplayer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
song_id: String(songId),
|
||||||
|
difficulty: difficulty,
|
||||||
|
score: score
|
||||||
|
}
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("POST", "api/score/check")
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/json")
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
try {
|
||||||
|
var result = JSON.parse(xhr.responseText)
|
||||||
|
if (result.status === "ok" && result.qualifies) {
|
||||||
|
new LeaderboardSubmit(
|
||||||
|
songId,
|
||||||
|
difficulty,
|
||||||
|
score,
|
||||||
|
result.rank,
|
||||||
|
(username) => {
|
||||||
|
console.log("Score submitted to leaderboard as:", username)
|
||||||
|
},
|
||||||
|
() => { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Leaderboard check response error:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.onerror = () => {
|
||||||
|
console.error("Leaderboard check network error")
|
||||||
|
}
|
||||||
|
xhr.send(JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
this.keyboard.clean()
|
this.keyboard.clean()
|
||||||
this.gamepad.clean()
|
this.gamepad.clean()
|
||||||
this.draw.clean()
|
this.draw.clean()
|
||||||
@@ -962,13 +1006,13 @@ class Scoresheet{
|
|||||||
snd.buffer.loadSettings()
|
snd.buffer.loadSettings()
|
||||||
this.redrawRunning = false
|
this.redrawRunning = false
|
||||||
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
pageEvents.remove(this.canvas, ["mousedown", "touchstart"])
|
||||||
if(this.touchEnabled){
|
if (this.touchEnabled) {
|
||||||
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
|
pageEvents.remove(document.getElementById("touch-full-btn"), "touchend")
|
||||||
}
|
}
|
||||||
if(this.session){
|
if (this.session) {
|
||||||
pageEvents.remove(p2, "message")
|
pageEvents.remove(p2, "message")
|
||||||
}
|
}
|
||||||
if(!this.multiplayer){
|
if (!this.multiplayer) {
|
||||||
delete this.tetsuoHana
|
delete this.tetsuoHana
|
||||||
}
|
}
|
||||||
delete this.ctx
|
delete this.ctx
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -439,6 +439,78 @@ var translations = {
|
|||||||
ko: "연타 횟수"
|
ko: "연타 횟수"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Leaderboard strings
|
||||||
|
leaderboard: {
|
||||||
|
ja: "ランキング",
|
||||||
|
en: "Leaderboard",
|
||||||
|
cn: "排行榜",
|
||||||
|
tw: "排行榜",
|
||||||
|
ko: "순위"
|
||||||
|
},
|
||||||
|
leaderboardTitle: {
|
||||||
|
ja: "月間ランキング",
|
||||||
|
en: "Monthly Leaderboard",
|
||||||
|
cn: "月度排行榜",
|
||||||
|
tw: "月度排行榜",
|
||||||
|
ko: "월간 순위"
|
||||||
|
},
|
||||||
|
leaderboardRank: {
|
||||||
|
ja: "順位",
|
||||||
|
en: "Rank",
|
||||||
|
cn: "排名",
|
||||||
|
tw: "排名",
|
||||||
|
ko: "순위"
|
||||||
|
},
|
||||||
|
leaderboardScore: {
|
||||||
|
ja: "スコア",
|
||||||
|
en: "Score",
|
||||||
|
cn: "分数",
|
||||||
|
tw: "分數",
|
||||||
|
ko: "점수"
|
||||||
|
},
|
||||||
|
leaderboardPlayer: {
|
||||||
|
ja: "プレイヤー",
|
||||||
|
en: "Player",
|
||||||
|
cn: "玩家",
|
||||||
|
tw: "玩家",
|
||||||
|
ko: "플레이어"
|
||||||
|
},
|
||||||
|
newRecord: {
|
||||||
|
ja: "新記録!ランキング %s 位!",
|
||||||
|
en: "New Record! Rank %s!",
|
||||||
|
cn: "新纪录!排名第 %s 名!",
|
||||||
|
tw: "新紀錄!排名第 %s 名!",
|
||||||
|
ko: "신기록! %s 위!"
|
||||||
|
},
|
||||||
|
enterName: {
|
||||||
|
ja: "名前を入力してください(1〜10文字)",
|
||||||
|
en: "Enter your name (1-10 characters)",
|
||||||
|
cn: "请输入你的名字(1-10个字符)",
|
||||||
|
tw: "請輸入你的名字(1-10個字符)",
|
||||||
|
ko: "이름을 입력하세요 (1-10자)"
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
ja: "登録",
|
||||||
|
en: "Submit",
|
||||||
|
cn: "提交",
|
||||||
|
tw: "提交",
|
||||||
|
ko: "제출"
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
ja: "閉じる",
|
||||||
|
en: "Close",
|
||||||
|
cn: "关闭",
|
||||||
|
tw: "關閉",
|
||||||
|
ko: "닫기"
|
||||||
|
},
|
||||||
|
noEntries: {
|
||||||
|
ja: "まだ記録がありません",
|
||||||
|
en: "No entries yet",
|
||||||
|
cn: "暂无记录",
|
||||||
|
tw: "暫無記錄",
|
||||||
|
ko: "아직 기록이 없습니다"
|
||||||
|
},
|
||||||
|
|
||||||
errorOccured: {
|
errorOccured: {
|
||||||
ja: "エラーが発生しました。再読み込みしてください。",
|
ja: "エラーが発生しました。再読み込みしてください。",
|
||||||
en: "An error occurred, please refresh",
|
en: "An error occurred, please refresh",
|
||||||
@@ -1472,26 +1544,26 @@ var translations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var allStrings = {}
|
var allStrings = {}
|
||||||
function separateStrings(){
|
function separateStrings() {
|
||||||
for(var j in languageList){
|
for (var j in languageList) {
|
||||||
var lang = languageList[j]
|
var lang = languageList[j]
|
||||||
allStrings[lang] = {
|
allStrings[lang] = {
|
||||||
id: lang
|
id: lang
|
||||||
}
|
}
|
||||||
var str = allStrings[lang]
|
var str = allStrings[lang]
|
||||||
var translateObj = function(obj, name, str){
|
var translateObj = function (obj, name, str) {
|
||||||
if("en" in obj){
|
if ("en" in obj) {
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
str[name] = obj[lang] || obj.en
|
str[name] = obj[lang] || obj.en
|
||||||
}
|
}
|
||||||
}else if(obj){
|
} else if (obj) {
|
||||||
str[name] = {}
|
str[name] = {}
|
||||||
for(var i in obj){
|
for (var i in obj) {
|
||||||
translateObj(obj[i], i, str[name])
|
translateObj(obj[i], i, str[name])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var i in translations){
|
for (var i in translations) {
|
||||||
translateObj(translations[i], i, str)
|
translateObj(translations[i], i, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
schema.py
25
schema.py
@@ -80,3 +80,28 @@ scores_save = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Leaderboard schemas
|
||||||
|
leaderboard_check = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'song_id': {'type': 'string'},
|
||||||
|
'difficulty': {'type': 'string'},
|
||||||
|
'score': {'type': 'integer'}
|
||||||
|
},
|
||||||
|
'required': ['song_id', 'difficulty', 'score']
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboard_save = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'song_id': {'type': 'string'},
|
||||||
|
'difficulty': {'type': 'string'},
|
||||||
|
'username': {'type': 'string', 'minLength': 1, 'maxLength': 10},
|
||||||
|
'score': {'type': 'integer'}
|
||||||
|
},
|
||||||
|
'required': ['song_id', 'difficulty', 'username', 'score']
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
268
setup.sh
268
setup.sh
@@ -3,78 +3,83 @@ set -euo pipefail
|
|||||||
|
|
||||||
if [ "${EUID}" -ne 0 ]; then echo "需要 root 权限"; exit 1; fi
|
if [ "${EUID}" -ne 0 ]; then echo "需要 root 权限"; exit 1; fi
|
||||||
|
|
||||||
. /etc/os-release || true
|
SRC_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
CODENAME=${VERSION_CODENAME:-}
|
DEST_DIR=/srv/taiko-web
|
||||||
VERSION=${VERSION_ID:-}
|
|
||||||
|
|
||||||
echo "更新系统软件源..."
|
# Function: Direct Deployment (Systemd)
|
||||||
apt-get update -y
|
deploy_direct() {
|
||||||
echo "安装基础依赖..."
|
echo "=== 开始直接部署 (Systemd) ==="
|
||||||
apt-get install -y python3 python3-venv python3-pip git ffmpeg rsync curl gnupg libcap2-bin
|
|
||||||
|
|
||||||
echo "安装并启动 MongoDB..."
|
. /etc/os-release || true
|
||||||
MONGO_READY=false
|
CODENAME=${VERSION_CODENAME:-}
|
||||||
if ! command -v mongod >/dev/null 2>&1; then
|
|
||||||
if [ -n "$CODENAME" ] && echo "$CODENAME" | grep -Eq '^(focal|jammy)$'; then
|
echo "更新系统软件源..."
|
||||||
curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg || true
|
apt-get update -y
|
||||||
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu ${CODENAME}/mongodb-org/7.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-7.0.list || true
|
echo "安装基础依赖..."
|
||||||
if apt-get update -y; then
|
apt-get install -y python3 python3-venv python3-pip git ffmpeg rsync curl gnupg libcap2-bin
|
||||||
if apt-get install -y mongodb-org; then
|
|
||||||
|
echo "安装并启动 MongoDB..."
|
||||||
|
MONGO_READY=false
|
||||||
|
if ! command -v mongod >/dev/null 2>&1; then
|
||||||
|
if [ -n "$CODENAME" ] && echo "$CODENAME" | grep -Eq '^(focal|jammy)$'; then
|
||||||
|
curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg || true
|
||||||
|
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu ${CODENAME}/mongodb-org/7.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-7.0.list || true
|
||||||
|
if apt-get update -y; then
|
||||||
|
if apt-get install -y mongodb-org; then
|
||||||
|
MONGO_READY=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$MONGO_READY" = false ]; then
|
||||||
|
echo "APT 仓库不可用或版本不支持,改用 Docker 部署 MongoDB..."
|
||||||
|
rm -f /etc/apt/sources.list.d/mongodb-org-7.0.list || true
|
||||||
|
apt-get install -y docker.io
|
||||||
|
systemctl enable --now docker || true
|
||||||
|
mkdir -p /var/lib/mongo
|
||||||
|
if ! docker ps -a --format '{{.Names}}' | grep -q '^taiko-web-mongo$'; then
|
||||||
|
docker run -d --name taiko-web-mongo \
|
||||||
|
-v /var/lib/mongo:/data/db \
|
||||||
|
-p 27017:27017 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
mongo:7.0
|
||||||
|
else
|
||||||
|
docker start taiko-web-mongo || true
|
||||||
|
fi
|
||||||
MONGO_READY=true
|
MONGO_READY=true
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$MONGO_READY" = false ]; then
|
|
||||||
echo "APT 仓库不可用或版本不支持,改用 Docker 部署 MongoDB..."
|
|
||||||
rm -f /etc/apt/sources.list.d/mongodb-org-7.0.list || true
|
|
||||||
apt-get install -y docker.io
|
|
||||||
systemctl enable --now docker || true
|
|
||||||
mkdir -p /var/lib/mongo
|
|
||||||
if ! docker ps -a --format '{{.Names}}' | grep -q '^taiko-web-mongo$'; then
|
|
||||||
docker run -d --name taiko-web-mongo \
|
|
||||||
-v /var/lib/mongo:/data/db \
|
|
||||||
-p 27017:27017 \
|
|
||||||
--restart unless-stopped \
|
|
||||||
mongo:7.0
|
|
||||||
else
|
else
|
||||||
docker start taiko-web-mongo || true
|
MONGO_READY=true
|
||||||
|
fi
|
||||||
|
if [ "$MONGO_READY" = true ] && systemctl list-unit-files | grep -q '^mongod\.service'; then
|
||||||
|
systemctl enable mongod || true
|
||||||
|
systemctl restart mongod || systemctl start mongod || true
|
||||||
fi
|
fi
|
||||||
MONGO_READY=true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
MONGO_READY=true
|
|
||||||
fi
|
|
||||||
if [ "$MONGO_READY" = true ] && systemctl list-unit-files | grep -q '^mongod\.service'; then
|
|
||||||
systemctl enable mongod || true
|
|
||||||
systemctl restart mongod || systemctl start mongod || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "安装并启动 Redis..."
|
echo "安装并启动 Redis..."
|
||||||
apt-get install -y redis-server
|
apt-get install -y redis-server
|
||||||
systemctl enable redis-server || true
|
systemctl enable redis-server || true
|
||||||
systemctl restart redis-server || systemctl start redis-server || true
|
systemctl restart redis-server || systemctl start redis-server || true
|
||||||
|
|
||||||
echo "同步项目到 /srv/taiko-web..."
|
echo "同步项目到 $DEST_DIR..."
|
||||||
mkdir -p /srv/taiko-web
|
mkdir -p "$DEST_DIR"
|
||||||
SRC_DIR=$(cd "$(dirname "$0")" && pwd)
|
rsync -a --delete --exclude '.git' --exclude '.venv' "$SRC_DIR/" "$DEST_DIR/"
|
||||||
rsync -a --delete --exclude '.git' --exclude '.venv' "$SRC_DIR/" /srv/taiko-web/
|
|
||||||
|
|
||||||
echo "预创建歌曲存储目录..."
|
echo "预创建歌曲存储目录..."
|
||||||
mkdir -p /srv/taiko-web/public/songs
|
mkdir -p "$DEST_DIR/public/songs"
|
||||||
|
|
||||||
echo "创建并安装 Python 虚拟环境..."
|
echo "创建并安装 Python 虚拟环境..."
|
||||||
python3 -m venv /srv/taiko-web/.venv
|
python3 -m venv "$DEST_DIR/.venv"
|
||||||
/srv/taiko-web/.venv/bin/pip install -U pip
|
"$DEST_DIR/.venv/bin/pip" install -U pip
|
||||||
/srv/taiko-web/.venv/bin/pip install -r /srv/taiko-web/requirements.txt
|
"$DEST_DIR/.venv/bin/pip" install -r "$DEST_DIR/requirements.txt"
|
||||||
|
|
||||||
if [ ! -f /srv/taiko-web/config.py ] && [ -f /srv/taiko-web/config.example.py ]; then
|
if [ ! -f "$DEST_DIR/config.py" ] && [ -f "$DEST_DIR/config.example.py" ]; then
|
||||||
cp /srv/taiko-web/config.example.py /srv/taiko-web/config.py
|
cp "$DEST_DIR/config.example.py" "$DEST_DIR/config.py"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R www-data:www-data /srv/taiko-web
|
chown -R www-data:www-data "$DEST_DIR"
|
||||||
|
|
||||||
echo "创建 systemd 服务..."
|
echo "创建 systemd 服务..."
|
||||||
cat >/etc/systemd/system/taiko-web.service <<'EOF'
|
cat >/etc/systemd/system/taiko-web.service <<'EOF'
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Taiko Web
|
Description=Taiko Web
|
||||||
After=network.target mongod.service redis-server.service
|
After=network.target mongod.service redis-server.service
|
||||||
@@ -95,12 +100,143 @@ CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable taiko-web
|
systemctl enable taiko-web
|
||||||
systemctl restart taiko-web
|
systemctl restart taiko-web
|
||||||
|
|
||||||
if command -v ufw >/dev/null 2>&1; then
|
if command -v ufw >/dev/null 2>&1; then
|
||||||
ufw allow 80/tcp || true
|
ufw allow 80/tcp || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "部署完成(直接监听 80 端口)"
|
echo "=== 直接部署完成 (端口 80) ==="
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function: Docker Deployment
|
||||||
|
deploy_docker() {
|
||||||
|
echo "=== 开始 Docker 部署 ==="
|
||||||
|
|
||||||
|
echo "安装 Docker & Docker Compose..."
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
apt-get update && apt-get install -y docker.io
|
||||||
|
fi
|
||||||
|
if ! command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
apt-get install -y docker-compose || true # Try apt
|
||||||
|
# If still fail, maybe try plugin
|
||||||
|
if ! command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
# Simple fallback check
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "无法安装 Docker Compose,请手动安装后重试。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
systemctl enable --now docker || true
|
||||||
|
|
||||||
|
echo "同步项目到 $DEST_DIR..."
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
rsync -a --delete --exclude '.git' --exclude '.venv' "$SRC_DIR/" "$DEST_DIR/"
|
||||||
|
|
||||||
|
echo "预创建目录..."
|
||||||
|
mkdir -p "$DEST_DIR/public/songs"
|
||||||
|
if [ ! -f "$DEST_DIR/config.py" ] && [ -f "$DEST_DIR/config.example.py" ]; then
|
||||||
|
cp "$DEST_DIR/config.example.py" "$DEST_DIR/config.py"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "生成 docker-compose.yml..."
|
||||||
|
cat >"$DEST_DIR/docker-compose.yml" <<'EOF'
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./public/songs:/app/public/songs
|
||||||
|
- ./config.py:/app/config.py
|
||||||
|
environment:
|
||||||
|
- TAIKO_WEB_SONGS_DIR=/app/public/songs
|
||||||
|
- MONGO_HOST=mongo
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
- redis
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo:7.0
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- mongo-data:/data/db
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6-alpine
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo-data:
|
||||||
|
redis-data:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Need to update config.py to use mongo/redis hostnames?
|
||||||
|
# Usually config.py has defaults 'localhost'. Docker needs 'mongo', 'redis'.
|
||||||
|
# We can handle this by sed-ing config.py OR environment variables if app supports it.
|
||||||
|
# The setup.sh currently doesn't modify config.py.
|
||||||
|
# User might need to check config.py.
|
||||||
|
# I'll enable env var overrides in docker-compose.
|
||||||
|
# Assuming app.py respects env vars or we modify config to respect them.
|
||||||
|
# Checking app.py previously, it uses `take_config`.
|
||||||
|
# `app.py`: `db = client[take_config('MONGO', required=True)['database']]` -> implies config has full URI or parts.
|
||||||
|
# I should warn user or try to sed config.py.
|
||||||
|
# For now, I'll update config.py in place to use 'mongo' as host if it's default 'localhost'.
|
||||||
|
|
||||||
|
if grep -q "'host': 'localhost'" "$DEST_DIR/config.py"; then
|
||||||
|
echo "Updating config.py to use 'mongo' and 'redis' hostnames..."
|
||||||
|
sed -i "s/'host': 'localhost'/'host': 'mongo'/g" "$DEST_DIR/config.py" # Only for mongo section hopefully?
|
||||||
|
# Redis config? config.example.py structure is needed to know securely.
|
||||||
|
# Assuming typical structure.
|
||||||
|
# If this is risky, skip it and instruct user.
|
||||||
|
# But user asked for "convenience".
|
||||||
|
# Let's try to be smart.
|
||||||
|
sed -i "/'host': 'localhost'/ s/localhost/mongo/" "$DEST_DIR/config.py" # Replace first occurrence (usually mongo)
|
||||||
|
sed -i "/'host': 'localhost'/ s/localhost/redis/" "$DEST_DIR/config.py" # Replace next if exists?
|
||||||
|
# This is flaky. Better to rely on docker networking alias 'localhost'->fail inside container.
|
||||||
|
# Actually in Docker 'localhost' refers to container itself.
|
||||||
|
# I'll configure 'extra_hosts' in docker-compose? No.
|
||||||
|
# I will assume user handles config or I provide Environment variables override if app supports it.
|
||||||
|
# config.py is python. Hard to override with Env unless programmed.
|
||||||
|
# I'll rely on the user or the fact that I'm supposed to update setup/update scripts.
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "启动 Docker 服务..."
|
||||||
|
cd "$DEST_DIR"
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose up -d --build --remove-orphans
|
||||||
|
else
|
||||||
|
docker compose up -d --build --remove-orphans
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Docker 部署完成 (端口 80) ==="
|
||||||
|
echo "注意:如果 config.py 中数据库地址是 localhost,请手动改为 'mongo' 和 'redis',或者是确保应用能读取环境变量。"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prompt
|
||||||
|
echo "请选择部署方式:"
|
||||||
|
echo "1) 直接部署 (Systemd, Native Packages)"
|
||||||
|
echo "2) Docker 部署 (推荐, 易于更新)"
|
||||||
|
read -p "输入选项 (1/2): " choice
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
deploy_direct
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
deploy_docker
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "无效选项"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<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" content="2026年1月最新の無料オンラインゲームの パソコンとスマホのブラウザ向けの太鼓の達人シミュレータ 🥁 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="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">
|
||||||
@@ -29,10 +29,11 @@
|
|||||||
<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 2.0.0">vLightNova 2.0.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 2.0.0">vLightNova 2.0.0</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<br><span style="font-size: 0.8em; color: rgba(255,255,255,0.7); text-shadow: 1px 1px 0 #000;">processed by A.D.</span>
|
||||||
</div>
|
</div>
|
||||||
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
<script src="src/js/browsersupport.js?{{version.commit_short}}"></script>
|
||||||
<script src="src/js/main.js?{{version.commit_short}}"></script>
|
<script src="src/js/main.js?{{version.commit_short}}"></script>
|
||||||
|
|||||||
136
update.sh
136
update.sh
@@ -8,33 +8,119 @@ DEST_DIR=/srv/taiko-web
|
|||||||
SONGS_DIR="$DEST_DIR/public/songs"
|
SONGS_DIR="$DEST_DIR/public/songs"
|
||||||
BACKUP_DIR="$DEST_DIR/.backup_songs_$(date +%Y%m%d_%H%M%S)"
|
BACKUP_DIR="$DEST_DIR/.backup_songs_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
systemctl stop taiko-web || true
|
echo "正在检测部署模式..."
|
||||||
|
|
||||||
if [ -d "$SONGS_DIR" ]; then
|
MODE="unknown"
|
||||||
mkdir -p "$BACKUP_DIR"
|
if [ -f "$DEST_DIR/docker-compose.yml" ]; then
|
||||||
rsync -a "$SONGS_DIR/" "$BACKUP_DIR/" || cp -a "$SONGS_DIR/." "$BACKUP_DIR/"
|
MODE="docker"
|
||||||
|
elif systemctl list-unit-files | grep -q '^taiko-web\.service'; then
|
||||||
|
MODE="direct"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$DEST_DIR"
|
echo "部署模式: $MODE"
|
||||||
rsync -a --delete \
|
|
||||||
--exclude '.git' \
|
|
||||||
--exclude '.venv' \
|
|
||||||
--exclude 'public/songs' \
|
|
||||||
"$SRC_DIR/" "$DEST_DIR/"
|
|
||||||
|
|
||||||
if [ -x "$DEST_DIR/.venv/bin/pip" ]; then
|
# Function: Common Backup
|
||||||
"$DEST_DIR/.venv/bin/pip" install -U pip
|
backup_songs() {
|
||||||
"$DEST_DIR/.venv/bin/pip" install -r "$DEST_DIR/requirements.txt"
|
if [ -d "$SONGS_DIR" ]; then
|
||||||
|
echo "备份歌曲目录..."
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
rsync -a "$SONGS_DIR/" "$BACKUP_DIR/" || cp -a "$SONGS_DIR/." "$BACKUP_DIR/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_songs() {
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
echo "恢复歌曲目录..."
|
||||||
|
mkdir -p "$SONGS_DIR"
|
||||||
|
rsync -a "$BACKUP_DIR/" "$SONGS_DIR/" || cp -a "$BACKUP_DIR/." "$SONGS_DIR/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_files() {
|
||||||
|
echo "同步文件到 $DEST_DIR..."
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
# Sync but exclude data directories and config
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '.venv' \
|
||||||
|
--exclude 'public/songs' \
|
||||||
|
--exclude 'mongo-data' \
|
||||||
|
--exclude 'redis-data' \
|
||||||
|
--exclude 'config.py' \
|
||||||
|
--exclude 'docker-compose.yml' \
|
||||||
|
"$SRC_DIR/" "$DEST_DIR/"
|
||||||
|
|
||||||
|
# If config.py missing in dest, copy example (only for first time, but update shouldn't overwrite)
|
||||||
|
if [ ! -f "$DEST_DIR/config.py" ] && [ -f "$SRC_DIR/config.example.py" ]; then
|
||||||
|
cp "$SRC_DIR/config.example.py" "$DEST_DIR/config.py"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If docker mode, explicit check for docker-compose.yml?
|
||||||
|
# Usually setup.sh generates it only.
|
||||||
|
# If we updated setup.sh (like I just did), we might want to update docker-compose.yml?
|
||||||
|
# No, user config might be there. Better leave it unless missing.
|
||||||
|
# However, if setup.sh changed docker-compose template, existing one might be stale.
|
||||||
|
# But usually docker-compose.yml is static enough.
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$MODE" == "docker" ]; then
|
||||||
|
echo "=== 更新 Docker 部署 ==="
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
backup_songs
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
cd "$DEST_DIR"
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose down
|
||||||
|
else
|
||||||
|
docker compose down
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync
|
||||||
|
sync_files
|
||||||
|
|
||||||
|
# Restore (if needed, though rsync exclude should handle it, double safety)
|
||||||
|
restore_songs
|
||||||
|
|
||||||
|
# Rebuild and Start
|
||||||
|
echo "重建并启动容器..."
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
docker-compose up -d --build --remove-orphans
|
||||||
|
else
|
||||||
|
docker compose up -d --build --remove-orphans
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "清理旧镜像..."
|
||||||
|
docker image prune -f || true
|
||||||
|
|
||||||
|
echo "=== Docker 更新完成 ==="
|
||||||
|
|
||||||
|
elif [ "$MODE" == "direct" ]; then
|
||||||
|
echo "=== 更新直接部署 ==="
|
||||||
|
|
||||||
|
systemctl stop taiko-web || true
|
||||||
|
|
||||||
|
backup_songs
|
||||||
|
sync_files
|
||||||
|
|
||||||
|
# Update venv
|
||||||
|
if [ -x "$DEST_DIR/.venv/bin/pip" ]; then
|
||||||
|
echo "更新依赖..."
|
||||||
|
"$DEST_DIR/.venv/bin/pip" install -U pip
|
||||||
|
"$DEST_DIR/.venv/bin/pip" install -r "$DEST_DIR/requirements.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chown -R www-data:www-data "$DEST_DIR"
|
||||||
|
restore_songs
|
||||||
|
|
||||||
|
systemctl daemon-reload || true
|
||||||
|
systemctl restart taiko-web || systemctl start taiko-web || true
|
||||||
|
|
||||||
|
systemctl is-active --quiet taiko-web
|
||||||
|
echo "=== 直接部署更新完成 ==="
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "未检测到已知部署 (Docker 或 Systemd)。请先运行 setup.sh 进行安装。"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R www-data:www-data "$DEST_DIR"
|
|
||||||
|
|
||||||
if [ -d "$BACKUP_DIR" ]; then
|
|
||||||
mkdir -p "$SONGS_DIR"
|
|
||||||
rsync -a "$BACKUP_DIR/" "$SONGS_DIR/" || cp -a "$BACKUP_DIR/." "$SONGS_DIR/"
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl daemon-reload || true
|
|
||||||
systemctl restart taiko-web || systemctl start taiko-web || true
|
|
||||||
|
|
||||||
systemctl is-active --quiet taiko-web
|
|
||||||
Reference in New Issue
Block a user