Files
monkeygg2.github.io/games/web-osu/scripts/overlay/score.js
T
2023-08-25 13:31:04 +05:30

406 lines
16 KiB
JavaScript

/*
* class: ScoreOverlay (extends PIXI.Container)
* responsible for calculating & displaying combo, score, HP, accuracy...
*
* Construct params
* gamefield: {width, height} in real pixels
*
* properties
* tint: 24-bit integer color of display
*
*/
define([], function()
{
function addPlayHistory(summary) {
if (!window.playHistory1000) {
window.playHistory1000 = [];
}
window.playHistory1000.push(summary);
if (window.playHistory1000.length > 1000)
window.playHistory1000.shift();
// save history
if (window.localforage) {
localforage.setItem("playhistory1000", window.playHistory1000, function(err, val){
if (err) {
console.error("Error saving play history");
}
});
}
}
function grade(acc) {
if (acc >= 1) return 'SS';
if (acc >= 0.95) return 'S';
if (acc >= 0.9) return 'A';
if (acc >= 0.8) return 'B';
if (acc >= 0.7) return 'C';
return 'D';
}
function LazyNumber(value = 0) {
this.value = value;
this.target = value;
this.lasttime = -1000000; // playback can start before time=0
}
LazyNumber.prototype.lag = 200;
// param time must be non-decreasing
LazyNumber.prototype.update = function(time) {
this.value += (this.target - this.value) * (1 - Math.exp((this.lasttime - time) / this.lag));
this.lasttime = time;
}
// param time must be non-decreasing
LazyNumber.prototype.set = function(time, value) {
this.update(time);
this.target = value;
}
// param time must be non-decreasing
LazyNumber.prototype.valueAt = function(time) {
this.update(time);
return this.value;
}
function ScoreOverlay(windowfield, HPdrain, scoreMultiplier) // constructor.
{
PIXI.Container.call(this);
this.field = windowfield;
this.HPdrain = HPdrain;
this.scaleMul = windowfield.height / 800;
this.scoreMultiplier = scoreMultiplier;
this.score = 0; // this have been multiplied by scoreMultiplier
this.combo = 0;
this.maxcombo = 0;
this.judgeTotal = 0;
this.maxJudgeTotal = 0;
this.HP = 1;
this.fullcombo = true;
// accuracy = judgeTotal / maxJudgeTotal
this.onfail = null;
this.judgecnt = {
great: 0,
good: 0,
meh: 0,
miss: 0,
}
this.score4display = new LazyNumber(this.score);
this.combo4display = new LazyNumber(this.combo);
this.accuracy4display = new LazyNumber(1);
this.HP4display = new LazyNumber(this.HP);
this.newSpriteArray = function(len, scaleMul = 1, tint = 0xffffff) {
let a = new Array(len);
for (let i=0; i<len; ++i) {
a[i] = new PIXI.Sprite();
a[i].scale.x = a[i].scale.y = this.scaleMul * scaleMul;
a[i].anchor.x = 0;
a[i].anchor.y = 0;
a[i].alpha = 1;
a[i].tint = tint;
this.addChild(a[i]);
}
return a;
}
this.scoreDigits = this.newSpriteArray(10, 0.4, 0xddffff); // 9999999999
this.comboDigits = this.newSpriteArray(6, 0.2, 0xddffff); // 99999x
this.accuracyDigits = this.newSpriteArray(7, 0.2, 0xddffff); // 100.00%
this.HPbar = this.newSpriteArray(3, 0.5);
this.HPbar[0].texture = Skin["hpbarleft.png"];
this.HPbar[1].texture = Skin["hpbarright.png"];
this.HPbar[2].texture = Skin["hpbarmid.png"];
this.HPbar[0].anchor.x = 1;
this.HPbar[0].scale.x = this.field.width / 500;
this.HPbar[1].scale.x = this.field.width / 500;
this.HPbar[0].y = -7 * this.scaleMul;
this.HPbar[1].y = -7 * this.scaleMul;
this.HPbar[2].y = -7 * this.scaleMul;
// value initialization ends
this.resize = function(windowfield) {
this.field = windowfield;
this.scaleMul = windowfield.height / 800;
let f = function(a, mul) {
for (let i=0; i<a.length; ++i) {
a[i].scale.x = a[i].scale.y = mul;
}
};
f(this.scoreDigits, this.scaleMul * 0.4);
f(this.comboDigits, this.scaleMul * 0.2);
f(this.accuracyDigits, this.scaleMul * 0.2);
f(this.HPbar, this.scaleMul * 0.5);
this.HPbar[0].scale.x = this.field.width / 500;
this.HPbar[1].scale.x = this.field.width / 500;
this.HPbar[0].y = -7 * this.scaleMul;
this.HPbar[1].y = -7 * this.scaleMul;
this.HPbar[2].y = -7 * this.scaleMul;
}
this.HPincreasefor = function(result) {
switch (result)
{
case 0:
return -0.02 * this.HPdrain;
case 50:
return 0.01 * (4 - this.HPdrain);
case 100:
return 0.01 * (8 - this.HPdrain);
case 300:
return 0.01 * (10.2 - this.HPdrain);
default:
return 0;
}
}
// should be called when note is hit or missed
// maxresult: 300 for a hitcircle / slider start & end of every repeat
// maxresult: 10 for a tick
this.hit = function(result, maxresult, time) {
if (maxresult == 300) {
if (result == 300) this.judgecnt.great++;
if (result == 100) this.judgecnt.good++;
if (result == 50) this.judgecnt.meh++;
if (result == 0) this.judgecnt.miss++;
}
this.judgeTotal += result;
this.maxJudgeTotal += maxresult;
this.score += this.scoreMultiplier * result * (1 + this.combo / 25);
// any zero-score result is a miss
let oldCombo = this.combo;
this.combo = (result > 0)? this.combo+1 : 0;
if (result == 0) {
this.fullcombo = false;
// combo creak
if (oldCombo > 20) {
// play combo break sound
window.game.sampleComboBreak.volume = window.game.masterVolume * window.game.effectVolume;
window.game.sampleComboBreak.play();
}
}
this.maxcombo = Math.max(this.maxcombo, this.combo);
if (this.HP >= 0)
this.HP += this.HPincreasefor(result);
this.HP = Math.min(1, this.HP);
this.score4display.set(time, this.score);
this.combo4display.set(time, this.combo);
this.accuracy4display.set(time, this.judgeTotal / this.maxJudgeTotal);
this.HP4display.set(time, Math.max(0, this.HP));
}
this.charspacing = 10; // in texture pixel
this.setSpriteArrayText = function(arr, str) {
let width = 0;
if (str.length > arr.length)
console.error("displaying string failed");
for (let i=0; i<str.length; ++i) {
let ch = str[i];
if (ch == "%") ch = "percent";
let textname = "score-" + ch + ".png";
arr[i].texture = Skin[textname];
arr[i].knownwidth = arr[i].scale.x * (Skin[textname].width + this.charspacing);
arr[i].visible = true;
width += arr[i].knownwidth;
}
for (let i=str.length; i<arr.length; ++i) {
arr[i].visible = false;
}
arr.width = width;
arr.useLength = str.length;
}
this.setSpriteArrayPos = function(arr, x, y) {
let curx = x;
if (arr.useLength > 0) {} // TODO
else throw "wtf!";
for (let i=0; i<arr.useLength; ++i) {
arr[i].x = curx + arr[i].scale.x * this.charspacing / 2;
arr[i].y = y;
curx += arr[i].knownwidth;
}
}
this.update = function(time) {
if (Number.isNaN(time)) {
console.error("score overlay update with time = NaN");
return;
}
let HPpos = this.HP4display.valueAt(time) * this.field.width;
this.HPbar[0].x = HPpos;
this.HPbar[1].x = HPpos;
this.HPbar[2].x = HPpos;
this.setSpriteArrayText(this.scoreDigits, Math.round(this.score4display.valueAt(time)).toString().padStart(6,'0'));
this.setSpriteArrayText(this.comboDigits, Math.round(this.combo4display.valueAt(time)).toString() + "x");
this.setSpriteArrayText(this.accuracyDigits, (this.accuracy4display.valueAt(time) * 100).toFixed(2) + "%");
let basex = this.field.width * 0.5;
let basey = this.field.height * 0.017;
let unit = Math.min(this.field.width / 640, this.field.height / 480);
this.setSpriteArrayPos(this.scoreDigits, basex - this.scoreDigits.width / 2, basey);
this.setSpriteArrayPos(this.accuracyDigits, basex - this.scoreDigits.width / 2 - this.accuracyDigits.width - 16*unit, basey + 3*unit);
this.setSpriteArrayPos(this.comboDigits, basex + this.scoreDigits.width / 2 + 16*unit, basey + 3*unit);
}
function uploadScore(summary) {
let xhr = new XMLHttpRequest();
let url = "http://api.osugame.online/send/";
url += "?sid=" + encodeURIComponent(summary.sid);
url += "&bid=" + encodeURIComponent(summary.bid);
url += "&title=" + encodeURIComponent(summary.title);
url += "&version=" + encodeURIComponent(summary.version);
url += "&mods=" + encodeURIComponent(summary.mods);
url += "&grade=" + encodeURIComponent(summary.grade);
url += "&score=" + encodeURIComponent(summary.score);
url += "&combo=" + encodeURIComponent(summary.combo);
url += "&acc=" + encodeURIComponent(summary.acc);
url += "&time=" + encodeURIComponent(summary.time);
xhr.open("GET", url);
console.log(url);
xhr.onload = function() {
console.log("play record uploaded");
}
xhr.onerror = function() {
console.error("play record upload failed");
}
xhr.send();
}
this.showSummary = function(metadata, hiterrors, retryCallback, quitCallback) {
function errortext(a) {
let sum = 0;
for (let i=0; i<a.length; ++i)
sum += a[i];
let avg = sum / a.length;
let sumsqerr = 0;
for (let i=0; i<a.length; ++i)
sumsqerr += (a[i]-avg) * (a[i]-avg);
let variance = sumsqerr / a.length;
let stdev = Math.sqrt(variance);
let sgnavg = avg.toFixed(0);
if (sgnavg[0] != '-')
sgnavg = '+' + sgnavg;
return sgnavg + "±" + stdev.toFixed(0) + "ms";
}
function modstext(game) {
let l = [];
if (game.easy) l.push("EZ");
if (game.daycore) l.push("DC");
if (game.hidden) l.push("HD");
if (game.hardrock) l.push("HR");
if (game.nightcore) l.push("NC");
if (game.autoplay) l.push("AT");
if (l.length == 0) return "";
let s = l[0];
for (let i=1; i<l.length; ++i)
s = s + '+' + l[i];
return s;
}
function newdiv(parent, classname, text) {
let div = document.createElement("div");
if (parent)
parent.appendChild(div);
if (classname)
div.className = classname;
if (text)
div.innerText = text;
return div;
}
let acc = this.judgeTotal / this.maxJudgeTotal;
let rank = this.HP<0? "F": grade(acc);
let grading = newdiv(null, "grading");
grading.classList.add("transparent");
document.body.appendChild(grading);
let top = newdiv(grading, "top");
let info = newdiv(top, "beatmap-info");
newdiv(info, "title", metadata.Title);
newdiv(info, "artist", metadata.Artist);
newdiv(info, "version", metadata.Version);
newdiv(info, "mapper", "mapped by " + metadata.Creator);
newdiv(info, "version", modstext(window.game));
newdiv(top, "ranking", "Ranking");
newdiv(top, "grade " + rank, rank);
let left = newdiv(grading, "left");
newdiv(left, "block score", Math.round(this.score).toString());
newdiv(left, "block acc", (acc*100).toFixed(2)+"%");
newdiv(left, "block err", errortext(hiterrors));
newdiv(left, "block great", this.judgecnt.great.toString());
newdiv(left, "block good", this.judgecnt.good.toString());
newdiv(left, "block meh", this.judgecnt.meh.toString());
newdiv(left, "block miss", this.judgecnt.miss.toString());
newdiv(left, "block placeholder");
newdiv(left, "block combo", this.maxcombo.toString() + "x");
if (this.fullcombo)
newdiv(left, "fullcombo");
let b1 = newdiv(grading, "btn retry");
newdiv(b1, "inner", "Retry");
b1.onclick = function() {
grading.remove();
retryCallback();
}
let b2 = newdiv(grading, "btn quit");
newdiv(b2, "inner", "Quit");
b2.onclick = function() {
grading.remove();
quitCallback();
}
window.setTimeout(function(){grading.classList.remove("transparent")},100);
// generate summary data
let summary = {
sid: metadata.BeatmapSetID,
bid: metadata.BeatmapID,
title: metadata.Title,
version: metadata.Version,
mods: modstext(window.game),
grade: rank,
score: Math.round(this.score).toString(),
combo: this.maxcombo.toString(),
acc: (acc*100).toFixed(2)+"%",
time: new Date().getTime()
}
addPlayHistory(summary);
uploadScore(summary);
// show history best
if (window.localforage && summary.bid) {
window.localforage.getItem("historybest", function(err, val) {
if (err) return;
let historybest = 0;
if (val && val.size) {
historybest = val.get(summary.bid) || 0;
}
newdiv(left, "history-best", historybest.toString());
if (parseInt(summary.score) > historybest) {
if (!val || !val.size)
val = new Map();
val.set(summary.bid, parseInt(summary.score));
window.localforage.setItem("historybest", val, function(err, val){
if (err) console.error("failed saving best score");
});
}
})
}
}
}
if ( PIXI.Container ) { ScoreOverlay.__proto__ = PIXI.Container; }
ScoreOverlay.prototype = Object.create( PIXI.Container && PIXI.Container.prototype );
ScoreOverlay.prototype.constructor = ScoreOverlay;
ScoreOverlay.prototype.destroy = function destroy (options)
{
PIXI.Container.prototype.destroy.call(this, options);
};
return ScoreOverlay;
});