Compare commits

...

9 commits

Author SHA1 Message Date
RandomityGuy
2ef28aae5f - try to make it case insensitive
- escape all text derived from user input
- fix various race condition issues
- make scrolling smooth for touch controls
- fix leaderboards count
- fix potential crash when joining MP
- clamp input
- fix replay clock when stopped time
- also update ci to only run when tagged
2026-01-27 22:15:10 +00:00
RandomityGuy
51c456e907 update readme 2025-11-19 17:43:36 +00:00
RandomityGuy
afa42fe498 update readme 2025-08-17 21:53:06 +05:30
RandomityGuy
a79f7c8fcc readme update 2025-06-30 20:24:47 +05:30
RandomityGuy
ef9b79f120 Merge branch 'master' of https://github.com/RandomityGuy/MBHaxe 2025-03-21 21:30:35 +05:30
RandomityGuy
553ed365e9 update links 2025-03-21 21:30:23 +05:30
RandomityGuy
d69cb92028
Update README.md 2025-02-15 01:20:16 +05:30
RandomityGuy
e06d871aaf
add discord link 2025-02-15 01:20:06 +05:30
RandomityGuy
8a9866db54 update changelog 2025-02-14 22:50:07 +05:30
16 changed files with 172 additions and 93 deletions

View file

@ -462,10 +462,14 @@ workflows:
filters: filters:
tags: tags:
only: /^\d+.\d+.\d+$/ only: /^\d+.\d+.\d+$/
branches:
ignore: /.*/
build-windows: build-windows:
jobs: jobs:
- build-win: - build-win:
filters: filters:
tags: tags:
only: /^\d+.\d+.\d+$/ only: /^\d+.\d+.\d+$/
branches:
ignore: /.*/

View file

@ -1,3 +1,9 @@
# 1.7.1
This update brings the following bugfixes:
- Fixed a crash when the marble goes out of bounds.
- Fixed the FPS limiter not limiting rendered frames per second.
- Fixed scores not being sent in certain cases.
# 1.7.0 # 1.7.0
It's the fabled Leaderboards update! It's the fabled Leaderboards update!
Leaderboards have been implemented for all the levels with automatic replay uploading for official levels as well as watching top replays. Additionally, segregation has been made to allow switching between rewind and non-rewind scores on the leaderboards. Leaderboards have been implemented for all the levels with automatic replay uploading for official levels as well as watching top replays. Additionally, segregation has been made to allow switching between rewind and non-rewind scores on the leaderboards.

View file

@ -2,7 +2,8 @@
A Haxe port of Marble Blast Gold, Ultra and Platinum, name subject to change. A Haxe port of Marble Blast Gold, Ultra and Platinum, name subject to change.
The marble physics code was taken from [OpenMBU](https://github.com/MBU-Team/OpenMBU) along with my own collision detection code, game logic was partially from scratch and taken with permission from [Marble Blast Web Port](https://github.com/Vanilagy/MarbleBlast). The marble physics code was taken from [OpenMBU](https://github.com/MBU-Team/OpenMBU) along with my own collision detection code, game logic was partially from scratch and taken with permission from [Marble Blast Web Port](https://github.com/Vanilagy/MarbleBlast).
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/H2H5FRTTL) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/H2H5FRTTL)
Support Discord: https://discord.gg/GsmTVQQAhG
# Play # Play
## Web Browser ## Web Browser
The browser port supports touch controls, meaning it can be played on mobile devices. The browser port supports touch controls, meaning it can be played on mobile devices.
@ -11,15 +12,20 @@ The browser port supports touch controls, meaning it can be played on mobile dev
### Marble Blast Ultra: [Play](https://marbleblastultra.randomityguy.me/) ### Marble Blast Ultra: [Play](https://marbleblastultra.randomityguy.me/)
## Windows and Mac ## Windows and Mac
### Marble Blast Gold: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.1.12) ### Marble Blast Gold: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.1.12)
### Marble Blast Platinum: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.7.0) ### Marble Blast Platinum: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.7.1)
### Marble Blast Ultra: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.2.0-mbu) ### Marble Blast Ultra: [Download](https://github.com/RandomityGuy/MBHaxe/releases/tag/1.2.5-mbu)
## Mac Instructions - Important ## Mac Instructions - Important
Put the .app file in either /Applications or ~/Applications in order to run it properly. Put the .app file in either /Applications or ~/Applications in order to run it properly.
You will also have to bypass Gatekeeper since the .app is not signed. You will also have to bypass Gatekeeper since the .app is not signed.
## Android ## Android
### Marble Blast Gold: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.1.12/MBHaxe-Gold.apk) ### Marble Blast Gold: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.1.12/MBHaxe-Gold.apk)
### Marble Blast Platinum: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.7.0/MBHaxe-Platinum.apk) ### Marble Blast Platinum: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.7.1/MBHaxe-Platinum.apk)
### Marble Blast Ultra: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.2.0-mbu/MBHaxe-Ultra.apk) ### Marble Blast Ultra: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.2.5-mbu/MBHaxe-Ultra.apk)
## Xbox (NEW!)
### Marble Blast Ultra: [Download](https://github.com/RandomityGuy/MBHaxe/releases/download/1.2.5-mbu/MBHaxe-Ultra-UWP-Xbox.msix)
Ported to Xbox via UWP by [Daniel Worley](https://github.com/worleydl).
You will need to enable Developer Mode on your Xbox in order to sideload the app. The walkthrough can be found at https://www.youtube.com/watch?v=2Ly9TIdu9uw.
## Additional Features ## Additional Features
- Cross Platform Multiplayer: Available in Ultra and Platinum. You can host and join multiplayer matches in any of these platforms: Windows, Mac, Web, Android. - Cross Platform Multiplayer: Available in Ultra and Platinum. You can host and join multiplayer matches in any of these platforms: Windows, Mac, Web, Android.
@ -60,6 +66,7 @@ Requires Haxe 4.3.0 or above
You require the following Haxe libraries: You require the following Haxe libraries:
- heaps: The specific version located [here](https://github.com/RandomityGuy/heaps) - heaps: The specific version located [here](https://github.com/RandomityGuy/heaps)
- hlsdl (Obtain the haxelib version of hlsdl, then patch it with these files [here](https://github.com/RandomityGuy/hashlink/tree/master/libs/sdl)) (Hashlink/C native target) - hlsdl (Obtain the haxelib version of hlsdl, then patch it with these files [here](https://github.com/RandomityGuy/hashlink/tree/master/libs/sdl)) (Hashlink/C native target)
- datachannel: obtained from [here](https://github.com/RandomityGuy/hxDatachannel)
- stb_ogg_sound (JS/Browser target) - stb_ogg_sound (JS/Browser target)
- zip 1.1.0 (JS/Browser target) - zip 1.1.0 (JS/Browser target)
@ -88,9 +95,6 @@ This will build the apk file at Export/android/app/build/outputs/apk/release/app
If you are on browser, please send the browser console log to me If you are on browser, please send the browser console log to me
If you are on native, please run marbleblast-debug.bat and reproduce the crash, send the resulting stacktrace that occurs during the crash to me. If you are on native, please run marbleblast-debug.bat and reproduce the crash, send the resulting stacktrace that occurs during the crash to me.
## Help it shows a black screen when playing a level!
Your PC does not support the game, please upgrade it, there is nothing I can do about it to fix it.
## How accurate are the marble physics? ## How accurate are the marble physics?
Very accurate with up to 1% deviation from the original physics. The deviations are due to traplaunches being slightly different and occassional internal edge collisions, and the lower delta t values for physics simulations. Very accurate with up to 1% deviation from the original physics. The deviations are due to traplaunches being slightly different and occassional internal edge collisions, and the lower delta t values for physics simulations.
@ -100,11 +104,11 @@ In native version, you can just resize the window if windowed or use the resolut
## How do I change my FOV? ## How do I change my FOV?
Edit settings.json for native version, edit the MBHaxeSettings key in LocalStorage in browser. Edit settings.json for native version, edit the MBHaxeSettings key in LocalStorage in browser.
In the platinum version, there is an FOV slider. In the Platinum and Ultra versions, there is an FOV slider.
## How do I unlock/lock FPS? ## How do I unlock/lock FPS?
You cannot unlock fps in the browser, it is forever set to vsync. You cannot unlock fps in the browser, it is forever set to vsync.
In the native version, edit settings.json or the options menu in the platinum. In the native version, use the options menu to unlock/lock fps, or edit settings.json and set "vsync" to false to unlock fps.
## Hey can you please add this new feature? ## Hey can you please add this new feature?
If this new feature of yours already exists in MBG but not in this port, then I will try to add it, if I get time to do so, otherwise chances are, I won't add it since I have other things to do and would rather not waste my time on this any further. You are free to do pull requests if you have already implemented said feature. If this new feature of yours already exists in MBG but not in this port, then I will try to add it, if I get time to do so, otherwise chances are, I won't add it since I have other things to do and would rather not waste my time on this any further. You are free to do pull requests if you have already implemented said feature.

View file

@ -49,7 +49,7 @@ class Leaderboards {
public static function getScores(mission:String, kind:LeaderboardsKind, cb:Array<LBScore>->Void) { public static function getScores(mission:String, kind:LeaderboardsKind, cb:Array<LBScore>->Void) {
if (!StringTools.startsWith(mission, "data/")) if (!StringTools.startsWith(mission, "data/"))
mission = "data/" + mission; mission = "data/" + mission;
return Http.get('${host}/api/scores?mission=${StringTools.urlEncode(mission)}&game=${game}&view=${kind}&count=10', (b) -> { return Http.get('${host}/api/scores?mission=${StringTools.urlEncode(mission)}&game=${game}&view=${kind}&count=5', (b) -> {
var s = b.toString(); var s = b.toString();
var scores:Array<LBScore> = Json.parse(s).scores; var scores:Array<LBScore> = Json.parse(s).scores;
cb(scores); cb(scores);

View file

@ -394,6 +394,7 @@ class Marble extends GameObject {
this.netSmoothOffset = new Vector(); this.netSmoothOffset = new Vector();
this.netCorrected = false; this.netCorrected = false;
this.currentUp = new Vector(0, 0, 1); this.currentUp = new Vector(0, 0, 1);
this.lastContactNormal = new Vector(0, 0, 1);
var marbleDts = new DtsObject(); var marbleDts = new DtsObject();
var marbleShader = ""; var marbleShader = "";
@ -817,7 +818,7 @@ class Marble extends GameObject {
function computeMoveForces(m:Move, aControl:Vector, desiredOmega:Vector) { function computeMoveForces(m:Move, aControl:Vector, desiredOmega:Vector) {
var currentGravityDir = this.currentUp.multiply(-1); var currentGravityDir = this.currentUp.multiply(-1);
var R = currentGravityDir.multiply(-this._radius); var R = this.currentUp.multiply(this._radius);
var rollVelocity = this.omega.cross(R); var rollVelocity = this.omega.cross(R);
var axes = this.getMarbleAxis(); var axes = this.getMarbleAxis();
// if (!level.isReplayingMovement) // if (!level.isReplayingMovement)
@ -850,15 +851,15 @@ class Marble extends GameObject {
} }
var rsq = R.lengthSq(); var rsq = R.lengthSq();
var crossP = R.cross(motionDir.multiply(desiredYVelocity).add(sideDir.multiply(desiredXVelocity))).multiply(1 / rsq); var crossP = R.cross(motionDir.multiply(desiredYVelocity).add(sideDir.multiply(desiredXVelocity))).multiply(1 / rsq);
desiredOmega.set(crossP.x, crossP.y, crossP.z); desiredOmega.load(crossP);
aControl.set(desiredOmega.x - this.omega.x, desiredOmega.y - this.omega.y, desiredOmega.z - this.omega.z); aControl.load(desiredOmega.sub(this.omega));
var aScalar = aControl.length(); var aScalar = aControl.length();
if (aScalar > this._angularAcceleration) { if (aScalar > this._angularAcceleration) {
aControl.scale(this._angularAcceleration / aScalar); aControl.scale(this._angularAcceleration / aScalar);
} }
return false; return false;
} }
return return true; return true;
} }
function velocityCancel(timeState:TimeState, surfaceSlide:Bool, noBounce:Bool, stoppedPaths:Bool, pi:Array<PathedInterior>) { function velocityCancel(timeState:TimeState, surfaceSlide:Bool, noBounce:Bool, stoppedPaths:Bool, pi:Array<PathedInterior>) {
@ -873,7 +874,7 @@ class Marble extends GameObject {
var sVel = this.velocity.sub(contacts[i].velocity); var sVel = this.velocity.sub(contacts[i].velocity);
var surfaceDot = contacts[i].normal.dot(sVel); var surfaceDot = contacts[i].normal.dot(sVel);
if ((!looped && surfaceDot < 0) || surfaceDot < -SurfaceDotThreshold) { if ((!looped && surfaceDot < 0.0) || surfaceDot < -SurfaceDotThreshold) {
var velLen = this.velocity.length(); var velLen = this.velocity.length();
var surfaceVel = this.contacts[i].normal.multiply(surfaceDot); var surfaceVel = this.contacts[i].normal.multiply(surfaceDot);
@ -908,7 +909,7 @@ class Marble extends GameObject {
} }
contacts[i].velocity.load(otherMarble.velocity); contacts[i].velocity.load(otherMarble.velocity);
} else { } else {
if (contacts[i].velocity.length() == 0 && !surfaceSlide && surfaceDot > -this._maxDotSlide * velLen) { if (contacts[i].velocity.length() == 0.0 && !surfaceSlide && surfaceDot > -this._maxDotSlide * velLen) {
this.velocity.load(this.velocity.sub(surfaceVel)); this.velocity.load(this.velocity.sub(surfaceVel));
this.velocity.normalize(); this.velocity.normalize();
this.velocity.load(this.velocity.multiply(velLen)); this.velocity.load(this.velocity.multiply(velLen));
@ -934,7 +935,7 @@ class Marble extends GameObject {
vAtC.load(vAtC.sub(contacts[i].normal.multiply(contacts[i].normal.dot(sVel)))); vAtC.load(vAtC.sub(contacts[i].normal.multiply(contacts[i].normal.dot(sVel))));
var vAtCMag = vAtC.length(); var vAtCMag = vAtC.length();
if (vAtCMag != 0) { if (vAtCMag != 0.0) {
var friction = this._bounceKineticFriction * contacts[i].friction; var friction = this._bounceKineticFriction * contacts[i].friction;
var angVMagnitude = friction * 5 * normalVel / (2 * this._radius); var angVMagnitude = friction * 5 * normalVel / (2 * this._radius);
@ -970,7 +971,7 @@ class Marble extends GameObject {
} }
} }
} while (!done && itersIn < 1e4); // Maximum limit pls } while (!done && itersIn < 1e4); // Maximum limit pls
if (this.velocity.lengthSq() < 625) { if (this.velocity.lengthSq() < 625.0) {
var gotOne = false; var gotOne = false;
var dir = new Vector(0, 0, 0); var dir = new Vector(0, 0, 0);
for (j in 0...contacts.length) { for (j in 0...contacts.length) {
@ -994,10 +995,10 @@ class Marble extends GameObject {
soFar += (dist - outVel * timeToSeparate) / timeToSeparate / contacts[k].normal.dot(dir); soFar += (dist - outVel * timeToSeparate) / timeToSeparate / contacts[k].normal.dot(dir);
} }
} }
if (soFar < -25) if (soFar < -25.0)
soFar = -25; soFar = -25.0;
if (soFar > 25) if (soFar > 25.0)
soFar = 25; soFar = 25.0;
this.velocity.load(this.velocity.add(dir.multiply(soFar))); this.velocity.load(this.velocity.add(dir.multiply(soFar)));
} }
} }
@ -1066,7 +1067,7 @@ class Marble extends GameObject {
slipping = false; slipping = false;
} }
var vAtCDir = vAtC.multiply(1 / vAtCMag); var vAtCDir = vAtC.multiply(1 / vAtCMag);
aFriction.load(bestContact.normal.multiply(-1).cross(vAtCDir.multiply(-1)).multiply(angAMagnitude)); aFriction.load(bestContact.normal.cross(vAtCDir).multiply(angAMagnitude));
AFriction.load(vAtCDir.multiply(-AMagnitude)); AFriction.load(vAtCDir.multiply(-AMagnitude));
this._slipAmount = vAtCMag - totalDeltaV; this._slipAmount = vAtCMag - totalDeltaV;
} }
@ -1093,16 +1094,16 @@ class Marble extends GameObject {
friction2 = this._kineticFriction * bestContact.friction; friction2 = this._kineticFriction * bestContact.friction;
Aadd.load(Aadd.multiply(friction2 * bestNormalForce / aAtCMag)); Aadd.load(Aadd.multiply(friction2 * bestNormalForce / aAtCMag));
} }
A.set(A.x + Aadd.x, A.y + Aadd.y, A.z + Aadd.z); A.load(A.add(Aadd));
a.set(a.x + aadd.x, a.y + aadd.y, a.z + aadd.z); a.load(a.add(aadd));
} }
A.set(A.x + AFriction.x, A.y + AFriction.y, A.z + AFriction.z); A.load(A.add(AFriction));
a.set(a.x + aFriction.x, a.y + aFriction.y, a.z + aFriction.z); a.load(a.add(aFriction));
lastContactNormal = bestContact.normal; lastContactNormal = bestContact.normal;
lastContactPosition = this.getAbsPos().getPosition(); lastContactPosition = this.getAbsPos().getPosition();
} }
a.set(a.x + aControl.x, a.y + aControl.y, a.z + aControl.z); a.load(a.add(aControl));
if (this.mode == Finish) { if (this.mode == Finish) {
a.set(); // Zero it out a.set(); // Zero it out
} }
@ -1371,7 +1372,7 @@ class Marble extends GameObject {
var surfaceNormal = new Vector(verts.nx, verts.ny, var surfaceNormal = new Vector(verts.nx, verts.ny,
verts.nz); // surface.normals[surface.indices[i]].transformed3x3(obj.transform).normalized(); verts.nz); // surface.normals[surface.indices[i]].transformed3x3(obj.transform).normalized();
if (obj is DtsObject) if (obj is DtsObject)
surfaceNormal.multiply(-1); surfaceNormal.load(v.sub(v0).cross(v2.sub(v0)).normalized().multiply(-1));
var surfaceD = -surfaceNormal.dot(v0); var surfaceD = -surfaceNormal.dot(v0);
// If we're going the wrong direction or not going to touch the plane, ignore... // If we're going the wrong direction or not going to touch the plane, ignore...
@ -2390,6 +2391,8 @@ class Marble extends GameObject {
if (Key.isDown(Settings.controlsSettings.right)) { if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1; move.d.y -= 1;
} }
move.d.x = Util.clamp(move.d.x, -1, 1);
move.d.y = Util.clamp(move.d.y, -1, 1);
if (Key.isDown(Settings.controlsSettings.jump) if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed || MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) { || Gamepad.isDown(Settings.gamepadSettings.jump)) {

View file

@ -2524,7 +2524,7 @@ class MarbleWorld extends Scheduler {
} else { } else {
nextLevelCode(); nextLevelCode();
} }
}, mission, finishTime); }, mission, finishTime, this.replay.write());
MarbleGame.canvas.pushDialog(egg); MarbleGame.canvas.pushDialog(egg);
this.setCursorLock(false); this.setCursorLock(false);
return 0; return 0;

View file

@ -52,6 +52,7 @@ class ReplayFrame {
var t = (time - this.time) / (next.time - this.time); var t = (time - this.time) / (next.time - this.time);
var dt = time - this.time; var dt = time - this.time;
var clockDt = next.clockTime - this.clockTime;
var interpFrame = new ReplayFrame(); var interpFrame = new ReplayFrame();
@ -59,18 +60,20 @@ class ReplayFrame {
interpFrame.time = time; interpFrame.time = time;
interpFrame.bonusTime = this.bonusTime; interpFrame.bonusTime = this.bonusTime;
interpFrame.clockTime = this.clockTime; interpFrame.clockTime = this.clockTime;
if (interpFrame.bonusTime != 0 && time >= 3.5) { if (clockDt > 0) {
if (dt <= this.bonusTime) { if (interpFrame.bonusTime != 0 && time >= 3.5) {
interpFrame.bonusTime -= dt; if (dt <= this.bonusTime) {
interpFrame.bonusTime -= dt;
} else {
interpFrame.clockTime += dt - this.bonusTime;
interpFrame.bonusTime = 0;
}
} else { } else {
interpFrame.clockTime += dt - this.bonusTime; if (this.time >= 3.5)
interpFrame.bonusTime = 0; interpFrame.clockTime += dt;
} else if (this.time + dt >= 3.5) {
} else { interpFrame.clockTime += (this.time + dt) - 3.5;
if (this.time >= 3.5) }
interpFrame.clockTime += dt;
else if (this.time + dt >= 3.5) {
interpFrame.clockTime += (this.time + dt) - 3.5;
} }
} }

View file

@ -130,14 +130,27 @@ class ManifestEntry extends FileEntry {
if (onReady != null) if (onReady != null)
onReady(); onReady();
} else { } else {
js.Browser.window.fetch(file).then((res:js.html.Response) -> { js.Browser.window.fetch(file)
return res.arrayBuffer(); .then((res:js.html.Response) -> {
}).then((buf:js.lib.ArrayBuffer) -> { return res.arrayBuffer();
loaded = true; })
bytes = Bytes.ofData(buf); .then((buf:js.lib.ArrayBuffer) -> {
if (onReady != null) loaded = true;
onReady(); bytes = Bytes.ofData(buf);
}); if (onReady != null)
onReady();
})
.catchError((e) -> {
// Try the original file path
js.Browser.window.fetch('data/' + originalFile).then((res:js.html.Response) -> {
return res.arrayBuffer();
}).then((buf:js.lib.ArrayBuffer) -> {
loaded = true;
bytes = Bytes.ofData(buf);
if (onReady != null)
onReady();
});
});
} }
#else #else
if (onReady != null) if (onReady != null)

View file

@ -118,7 +118,7 @@ class ChatCtrl extends GuiControl {
} }
public function addChatMessage(text:String) { public function addChatMessage(text:String) {
var realText = StringTools.htmlUnescape(text); var realText = StringTools.htmlEscape(text);
this.chats.push({ this.chats.push({
text: realText, text: realText,
age: 10.0 age: 10.0

View file

@ -19,7 +19,8 @@ class EndGameGui extends GuiControl {
var scoreSubmitted:Bool = false; var scoreSubmitted:Bool = false;
public function new(continueFunc:GuiControl->Void, restartFunc:GuiControl->Void, nextLevelFunc:GuiControl->Void, mission:Mission, timeState:TimeState) { public function new(continueFunc:GuiControl->Void, restartFunc:GuiControl->Void, nextLevelFunc:GuiControl->Void, mission:Mission, timeState:TimeState,
replayData:haxe.io.Bytes) {
super(); super();
this.horizSizing = Width; this.horizSizing = Width;
this.vertSizing = Height; this.vertSizing = Height;
@ -312,12 +313,11 @@ class EndGameGui extends GuiControl {
scoreData.push({name: "Matan W.", time: 5999.999}); scoreData.push({name: "Matan W.", time: 5999.999});
} }
egFirstLine.text.text = '<p align="left"><font color="#EEC884">1. </font>${scoreData[0].name}</p>'; egFirstLine.text.text = '<p align="left"><font color="#EEC884">1. </font>${StringTools.htmlEscape(scoreData[0].name)}</p>';
egSecondLine.text.text = '<p align="left"><font color="#CDCDCD">2. </font>${scoreData[1].name}</p>'; egSecondLine.text.text = '<p align="left"><font color="#CDCDCD">2. </font>${StringTools.htmlEscape(scoreData[1].name)}</p>';
egThirdLine.text.text = '<p align="left"><font color="#C9AFA0">3. </font>${scoreData[2].name}</p>'; egThirdLine.text.text = '<p align="left"><font color="#C9AFA0">3. </font>${StringTools.htmlEscape(scoreData[2].name)}</p>';
egFourthLine.text.text = '<p align="left"><font color="#A4A4A4">4. </font>${scoreData[3].name}</p>'; egFourthLine.text.text = '<p align="left"><font color="#A4A4A4">4. </font>${StringTools.htmlEscape(scoreData[3].name)}</p>';
egFifthLine.text.text = '<p align="left"><font color="#949494">5. </font>${scoreData[4].name}</p>'; egFifthLine.text.text = '<p align="left"><font color="#949494">5. </font>${StringTools.htmlEscape(scoreData[4].name)}</p>';
var lineelems = [ var lineelems = [
egFirstLineScore, egFirstLineScore,
egSecondLineScore, egSecondLineScore,
@ -393,6 +393,8 @@ class EndGameGui extends GuiControl {
// } // }
Settings.save(); Settings.save();
var rewindUsed = MarbleGame.instance.world.rewindUsed;
if (idx <= 4) { if (idx <= 4) {
setButtonStates(false); setButtonStates(false);
var end = new EnterNameDlg(idx, (name) -> { var end = new EnterNameDlg(idx, (name) -> {
@ -426,8 +428,7 @@ class EndGameGui extends GuiControl {
var lbPath = mission.path; var lbPath = mission.path;
if (mission.isClaMission) if (mission.isClaMission)
lbPath = 'custom/${mission.id}'; lbPath = 'custom/${mission.id}';
var replayData = MarbleGame.instance.world.replay.write(); Leaderboards.submitScore(lbPath, myScore.time, rewindUsed, (sendReplay, rowId) -> {
Leaderboards.submitScore(lbPath, myScore.time, MarbleGame.instance.world.rewindUsed, (sendReplay, rowId) -> {
if (sendReplay && !mission.isClaMission) { if (sendReplay && !mission.isClaMission) {
Leaderboards.submitReplay(rowId, replayData); Leaderboards.submitReplay(rowId, replayData);
} }
@ -438,7 +439,6 @@ class EndGameGui extends GuiControl {
this.addChild(end); this.addChild(end);
} else { } else {
// Check if we can submit LB scores // Check if we can submit LB scores
var replayData = MarbleGame.instance.world.replay.write();
var lbPath = mission.path; var lbPath = mission.path;
if (mission.isClaMission) if (mission.isClaMission)
lbPath = 'custom/${mission.id}'; lbPath = 'custom/${mission.id}';
@ -453,7 +453,7 @@ class EndGameGui extends GuiControl {
} }
} }
if (!hasMyScore || (hasMyScore && myTopScoreLB > timeState.gameplayClock)) { if (!hasMyScore || (hasMyScore && myTopScoreLB > timeState.gameplayClock)) {
Leaderboards.submitScore(lbPath, timeState.gameplayClock, MarbleGame.instance.world.rewindUsed, (sendReplay, rowId) -> { Leaderboards.submitScore(lbPath, timeState.gameplayClock, rewindUsed, (sendReplay, rowId) -> {
if (sendReplay && !mission.isClaMission) { if (sendReplay && !mission.isClaMission) {
Leaderboards.submitReplay(rowId, replayData); Leaderboards.submitReplay(rowId, replayData);
} }

View file

@ -10,6 +10,7 @@ import h2d.Tile;
import h2d.Graphics; import h2d.Graphics;
import src.MarbleGame; import src.MarbleGame;
import src.Util; import src.Util;
import haxe.Timer;
class GuiScrollCtrl extends GuiControl { class GuiScrollCtrl extends GuiControl {
public var scrollY:Float = 0; public var scrollY:Float = 0;
@ -40,6 +41,12 @@ class GuiScrollCtrl extends GuiControl {
var dirty:Bool = true; var dirty:Bool = true;
var prevMousePos:Vector; var prevMousePos:Vector;
var scrollVelocity:Float = 0;
var lastMoveStamp:Float = 0;
var momentumActive:Bool = false;
static inline var MOMENTUM_DAMPING:Float = 8;
var _contentYPositions:Map<h2d.Object, Float> = []; var _contentYPositions:Map<h2d.Object, Float> = [];
var deltaY:Float = 0; var deltaY:Float = 0;
@ -239,34 +246,59 @@ class GuiScrollCtrl extends GuiControl {
} }
public override function onMousePress(mouseState:MouseState) { public override function onMousePress(mouseState:MouseState) {
if (Util.isTouchDevice()) { // if (Util.isTouchDevice()) {
this.pressed = true; this.pressed = true;
this.dirty = true; this.dirty = true;
this.updateScrollVisual(); this.updateScrollVisual();
this.prevMousePos = mouseState.position; this.prevMousePos = mouseState.position;
} this.scrollVelocity = 0;
this.momentumActive = false;
this.lastMoveStamp = Timer.stamp();
// }
} }
public override function onMouseRelease(mouseState:MouseState) { public override function onMouseRelease(mouseState:MouseState) {
if (Util.isTouchDevice()) { // if (Util.isTouchDevice()) {
this.pressed = false; this.pressed = false;
this.dirty = true; this.dirty = true;
deltaY = 0; deltaY = 0;
this.updateScrollVisual(); this.updateScrollVisual();
} this.momentumActive = Math.abs(scrollVelocity) > 0.01;
this.lastMoveStamp = 0;
// }
} }
public override function onMouseMove(mouseState:MouseState) { public override function onMouseMove(mouseState:MouseState) {
if (Util.isTouchDevice()) { // if (Util.isTouchDevice()) {
super.onMouseMove(mouseState); super.onMouseMove(mouseState);
if (this.pressed) { if (this.pressed) {
var dy = (mouseState.position.y - this.prevMousePos.y) * scrollSpeed / this.maxScrollY; var renderRect = this.getRenderRectangle();
deltaY = -dy; var scrollExtentY = renderRect.extent.y;
this.scrollY -= dy; var dy = (mouseState.position.y - this.prevMousePos.y) / ((maxScrollY * Settings.uiScale) / scrollExtentY);
this.prevMousePos = mouseState.position; deltaY = -dy;
this.updateScrollVisual(); this.scrollY -= dy;
this.prevMousePos = mouseState.position;
var now = Timer.stamp();
if (lastMoveStamp > 0) {
var dt = now - lastMoveStamp;
if (dt > 0)
scrollVelocity = -dy / dt;
} }
lastMoveStamp = now;
momentumActive = false;
this.updateScrollVisual();
} }
// }
}
public override function onMouseLeave(mouseState:MouseState) {
// if (Util.isTouchDevice()) {
this.pressed = false;
this.dirty = true;
this.updateScrollVisual();
this.momentumActive = Math.abs(scrollVelocity) > 0.01;
this.lastMoveStamp = 0;
// }
} }
public override function update(dt:Float, mouseState:MouseState) { public override function update(dt:Float, mouseState:MouseState) {
@ -285,6 +317,21 @@ class GuiScrollCtrl extends GuiControl {
this.updateScrollVisual(); this.updateScrollVisual();
} }
super.update(dt, mouseState); super.update(dt, mouseState);
if (!pressed && momentumActive) {
var damping = Math.exp(-MOMENTUM_DAMPING * dt);
scrollVelocity *= damping;
if (Math.abs(scrollVelocity) < 0.01) {
scrollVelocity = 0;
momentumActive = false;
return;
}
var before = scrollY;
scrollY += scrollVelocity * dt;
updateScrollVisual();
if (scrollY == 0 || scrollY == before)
momentumActive = false;
}
} }
// public override function onMouseDown(mouseState:MouseState) { // public override function onMouseDown(mouseState:MouseState) {

View file

@ -186,7 +186,7 @@ class JoinServerGui extends GuiImage {
serverInfo.text.text = '<p align="center">Select a Server</p><p align="center">or Host your own</p>'; serverInfo.text.text = '<p align="center">Select a Server</p><p align="center">or Host your own</p>';
} else { } else {
var server = ourServerList[curSelection]; var server = ourServerList[curSelection];
serverInfo.text.text = '<p align="center">${server.name}</p><p align="center"><font face="MarkerFelt18" color="#DDDDEE">Hosted by ${server.host}</font></p><p align="left">${server.description}</p>'; serverInfo.text.text = '<p align="center">${StringTools.htmlEscape(server.name)}</p><p align="center"><font face="MarkerFelt18" color="#DDDDEE">Hosted by ${StringTools.htmlEscape(server.host)}</font></p><p align="left">${StringTools.htmlEscape(server.description)}</p>';
} }
} }
serverListContainer.addChild(serverList); serverListContainer.addChild(serverList);
@ -197,7 +197,7 @@ class JoinServerGui extends GuiImage {
function updateServerListDisplay() { function updateServerListDisplay() {
serverDisplays = ourServerList.map(x -> serverDisplays = ourServerList.map(x ->
'<img src="${platformToString[x.platform]}"></img><font color="#FFFFFF">${x.name} <offset value="${400 * Settings.uiScale}">${x.players}/${x.maxPlayers}</offset></font>'); '<img src="${platformToString[x.platform]}"></img><font color="#FFFFFF">${StringTools.htmlEscape(x.name)} <offset value="${400 * Settings.uiScale}">${x.players}/${x.maxPlayers}</offset></font>');
serverList.setTexts(serverDisplays); serverList.setTexts(serverDisplays);
} }

View file

@ -585,11 +585,11 @@ class MPPlayMissionGui extends GuiImage {
currentSelection = -1; currentSelection = -1;
} }
pmDesc.text.text = '<font face="MarkerFelt32" color="#E3F3FF"><p align="center">#${currentSelection + 1}: ${currentMission.title}</p></font>' pmDesc.text.text = '<font face="MarkerFelt32" color="#E3F3FF"><p align="center">#${currentSelection + 1}: ${StringTools.htmlEscape(currentMission.title)}</p></font>'
+ '<font face="MarkerFelt18" color="#CEE0F4">${currentMission.description}</font>'; + '<font face="MarkerFelt18" color="#CEE0F4">${StringTools.htmlEscape(currentMission.description)}</font>';
parTime.text.text = '<font face="MarkerFelt24" color="#E3F3FF">Duration: <font color="#FFFFFF">${Util.formatTime(currentMission.qualifyTime)}</font></font><br/>' parTime.text.text = '<font face="MarkerFelt24" color="#E3F3FF">Duration: <font color="#FFFFFF">${Util.formatTime(currentMission.qualifyTime)}</font></font><br/>'
+ '<font face="MarkerFelt24" color="#E3F3FF">Author: <font color="#FFFFFF">${currentMission.artist}</font></font>'; + '<font face="MarkerFelt24" color="#E3F3FF">Author: <font color="#FFFFFF">${StringTools.htmlEscape(currentMission.artist)}</font></font>';
// pmPreview.bmp.tile = tmpprevtile; // pmPreview.bmp.tile = tmpprevtile;
#if js #if js
@ -718,7 +718,7 @@ class MPPlayMissionGui extends GuiImage {
} }
var playerListCompiled = playerListArr.map(player -> var playerListCompiled = playerListArr.map(player ->
'<img src="${platformToString(player.platform)}"></img><font color="#FFFFFF">${player.name}<offset value="${220 * Settings.uiScale}">${player.ready ? "Ready" : ""}</offset></font>'); '<img src="${platformToString(player.platform)}"></img><font color="#FFFFFF">${StringTools.htmlEscape(player.name)}<offset value="${220 * Settings.uiScale}">${player.ready ? "Ready" : ""}</offset></font>');
playerListCtrl.setTexts(playerListCompiled); playerListCtrl.setTexts(playerListCompiled);
// if (!showingCustoms) // if (!showingCustoms)
@ -728,7 +728,7 @@ class MPPlayMissionGui extends GuiImage {
} }
public static function addChatMessage(s:String) { public static function addChatMessage(s:String) {
var realText = StringTools.htmlUnescape(s); var realText = StringTools.htmlEscape(s);
allChats.push(realText); allChats.push(realText);
if (allChats.length > 100) { if (allChats.length > 100) {
allChats = allChats.slice(allChats.length - 100); allChats = allChats.slice(allChats.length - 100);

View file

@ -738,7 +738,7 @@ class PlayGui {
} else { } else {
isSpectating = Net.clientIdMap[item.id].spectator; isSpectating = Net.clientIdMap[item.id].spectator;
} }
pl.push('<font color="${color}">${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(item.name, 25, 3)}</font>'); pl.push('<font color="${color}">${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(StringTools.htmlEscape(item.name), 25, 3)}</font>');
var connPing = item.us ? (Net.isHost ? 0 : Net.clientConnection.pingTicks) : (item.id == 0 ? 0 : Net.clientIdMap[item.id].pingTicks); var connPing = item.us ? (Net.isHost ? 0 : Net.clientConnection.pingTicks) : (item.id == 0 ? 0 : Net.clientIdMap[item.id].pingTicks);
var pingStatus = "unknown"; var pingStatus = "unknown";
if (connPing <= 5) if (connPing <= 5)

View file

@ -1234,7 +1234,7 @@ class PlayMissionGui extends GuiImage {
var i = 1; var i = 1;
for (score in scoreList) { for (score in scoreList) {
sFmt.push('${i}. sFmt.push('${i}.
<offset value="15">${score.name.substr(0, 30)}</offset> <offset value="15">${StringTools.htmlEscape(score.name.substr(0, 30))}</offset>
<offset value="215">${Util.formatTime(score.score)}</offset> <offset value="215">${Util.formatTime(score.score)}</offset>
<offset value="279"><img src="${platformToString(score.platform)}"/></offset> <offset value="279"><img src="${platformToString(score.platform)}"/></offset>
${score.rewind == 1 ? '<offset value="299"><img src="rewind"/></offset> ' : ""}'); ${score.rewind == 1 ? '<offset value="299"><img src="rewind"/></offset> ' : ""}');

View file

@ -562,7 +562,6 @@ class Net {
serverInfo.players++; serverInfo.players++;
} }
serverInfo.players++;
MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the new player MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the new player
if (MarbleGame.canvas.content is MPPlayMissionGui) { if (MarbleGame.canvas.content is MPPlayMissionGui) {