Wir haben das vom Entwickler prüfen lassen. Es gibt hier scheinbar ein Rundungsproblem in php das wir nicht so einfach behoben bekommen.
Hust.
a) Ich betreute etliche tausend Zeilen PHP-Code, die wissenschaftliche Daten verarbeiteten (und das wahrscheinlich auch noch immer tun), und wenn man mal von Eingabefehlern der Leute absieht, die die Zahlenlisten abgelesen und eingetippt haben, hatten alle Fehler dieselbe Ursache: Blödheit das Programmierers.
Hätte PHP einen so absurden Rundungsfehler, wie er hier beschrieben wird, hätte ich gewaltige Sprünge in den erzeugten Graphen bemerkt - und wenn nicht ich, dann die Labore, für die wir gearbeitet haben, denn die benutzten die Vorschau, die der PHP-Code lieferte, etwa so häufig wie die finalen Graphen, die das Batch-Prozessing mit R dann ausspuckte.
Rundungsfehler in PHP haben (heute, in der Steinzeit war das noch anders) eine von zwei Ursachen: (1) man schiebt eine Fließkommazahl in MySQL/MariaDB, hat dort den Datentyp falsch deklariert, und wundert sich dann später. (2) man wandelt fröhlich zwischen Fließkommazahl und String hin und her, und passt dabei nicht höllisch auf (don't do that). (3) man kommt an die Grenzen der Genauigkeit, aber bei der lächerlichen Genauigkeit, die hier benötigt wird, kann man das ausschließen.
b) royalfakers Screenshots zeigen Bilder des Wikis. Das Wiki benutzt zwar im Hintergrund PHP, aber die Berechnung der Boni auf der Seite selbst erfolgt in Javascript (westui.set_calc.calc und westui.set_calc.lvlUp). Da ist PHP zwangsweise völlig unschuldig.
Und weil das alles so schön ist, wollen wir uns das alles mal näher ansehen... am Beispiel des Werkzeugs und der Beweglichkeit.
Das Werkzeug hat einen Beweglichkeitsbonus von 0.08 pro Level.
Und nun gucken wir mal, was im Code passiert (Javascript vom Wiki):
JavaScript:
lvlUp: function(upg, val) {
var d = val < 1 ? 3 : -1; // val ist in unserem Fall immer >=1, daher ist d=-1
// Math.pow = Potenzfunktion
// Math.round runded.
// !negiert einen ja/nein-Wert und ist dabei ziemlich freizügig.
var e = !upg ? 0 : Math.round(Math.max(1, val * Math.pow(10, d) * upg)) / Math.pow(10, d + 1);
// in unserem Fall kann man das vereinfachen, weil val >= 1 und damit d=-1 ist, und Math.pow(10,-1)=0.1, Math.pow(10,d)=1 ist
var e = !upg ? 0 : Math.round(Math.max(1, val * 0.1 * upg))
return val + e;
},
....
// upg = Veredelungslevel
// lvl = Char level
// key = 1 für Levelabhängige Boni, 0 für feste
// bi eingabe = der Bonus, um den es geht (0.08, 0.12)
// bi ausgabe = der tatsächliche Bonus, also das, was uns interessiert.
// Math.ceil() liefert die nächsthöhere Ganzzahl bei Fließkommazahlen, rundet also nach oben im Zahlenstrahl
bi = wsc.lvlUp(upg, (lvl && key ? Math.ceil(bi * lvl) : bi));
// in unserem Fall gilt, weil levelabhängig:
// bi = wsc.lvlUp(upg, Math.ceil(bi * lvl));
Bei 0.08, Level 200, unveredelt: aufgerufen wird wsc.lvlUp(0, ceil(0.08*200)) => lvlUp(0, 16)
lvlUp(0,16): d= -1, e = 0 (unveredelt), return 0+16
Bei 0.08, Level 201 unveredelt: aufgerufen wird wsc.lvlUp(0, ceil(0.08*201)) => lvlUp(0, 17)
lvlUp(0,17): d= -1, e = 0 (unveredelt), return 0+17
Das ist also offensichtlich wie erwartet.
Bei 0.08, Level 200, veredelt auf 4, passiert das: lvlup(4, 16)
e= round(max(1, 16*0.1*4))
= round(6.4) = 6
return = 16+6 = 22
Bei 0.08, Level 201, veredelt auf 4, passiert das: lvlup(4, 17)
e= round(max(1, 17*0.1*4))
= round(6.8) = 7
return = 17+7 = 24
Da passiert kein Rundungsfehler irgendeiner Art, sondern es schlagen zwei Rundungen gleichzeitig zu: ceil(0.08*level), und round vom "Ergebnis des ceil multipliziert mit Veredelung durch 10".
Die einzige Möglichkeit, so etwas zu verhindern, wäre überhaupt nur einmal, ganz am Ende, zu runden. Das wäre unzweifelhaft mathematisch korrekter, und hätte als kleine Nebenwirkung, dass die Veredelung auf Stufe 1 unter Umständen wirkungslos wäre, wenn die Rundung nämlich schon dasselbe Bewirken würde.
Mit anderen Worten: kein Fehler, sondern tatsächlich ein Feature. Also ist alles in bester Ordnung.