diff --git a/data/ui/common/cancel_d.png b/data/ui/common/cancel_d.png new file mode 100644 index 00000000..4c6b3d0d Binary files /dev/null and b/data/ui/common/cancel_d.png differ diff --git a/data/ui/common/cancel_h.png b/data/ui/common/cancel_h.png new file mode 100644 index 00000000..0b379b22 Binary files /dev/null and b/data/ui/common/cancel_h.png differ diff --git a/data/ui/common/cancel_n.png b/data/ui/common/cancel_n.png new file mode 100644 index 00000000..b90b9275 Binary files /dev/null and b/data/ui/common/cancel_n.png differ diff --git a/data/ui/replay/cancel_d.png b/data/ui/replay/cancel_d.png new file mode 100644 index 00000000..5483acbf Binary files /dev/null and b/data/ui/replay/cancel_d.png differ diff --git a/data/ui/replay/cancel_h.png b/data/ui/replay/cancel_h.png new file mode 100644 index 00000000..69564903 Binary files /dev/null and b/data/ui/replay/cancel_h.png differ diff --git a/data/ui/replay/cancel_n.png b/data/ui/replay/cancel_n.png new file mode 100644 index 00000000..010bcb06 Binary files /dev/null and b/data/ui/replay/cancel_n.png differ diff --git a/data/ui/replay/home_d.png b/data/ui/replay/home_d.png new file mode 100644 index 00000000..6d3eaf7d Binary files /dev/null and b/data/ui/replay/home_d.png differ diff --git a/data/ui/replay/home_h.png b/data/ui/replay/home_h.png new file mode 100644 index 00000000..105f114b Binary files /dev/null and b/data/ui/replay/home_h.png differ diff --git a/data/ui/replay/home_n.png b/data/ui/replay/home_n.png new file mode 100644 index 00000000..e8ccdc5a Binary files /dev/null and b/data/ui/replay/home_n.png differ diff --git a/data/ui/replay/play_d.png b/data/ui/replay/play_d.png new file mode 100644 index 00000000..10282a26 Binary files /dev/null and b/data/ui/replay/play_d.png differ diff --git a/data/ui/replay/play_h.png b/data/ui/replay/play_h.png new file mode 100644 index 00000000..b1ab00d6 Binary files /dev/null and b/data/ui/replay/play_h.png differ diff --git a/data/ui/replay/play_i.png b/data/ui/replay/play_i.png new file mode 100644 index 00000000..7f71d30a Binary files /dev/null and b/data/ui/replay/play_i.png differ diff --git a/data/ui/replay/play_n.png b/data/ui/replay/play_n.png new file mode 100644 index 00000000..41165dc4 Binary files /dev/null and b/data/ui/replay/play_n.png differ diff --git a/data/ui/replay/replayframe.png b/data/ui/replay/replayframe.png new file mode 100644 index 00000000..d8d874f3 Binary files /dev/null and b/data/ui/replay/replayframe.png differ diff --git a/data/ui/replay/window.png b/data/ui/replay/window.png new file mode 100644 index 00000000..18454ea4 Binary files /dev/null and b/data/ui/replay/window.png differ diff --git a/src/Marble.hx b/src/Marble.hx index 226784a2..e74b5c53 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -489,13 +489,12 @@ class Marble extends GameObject { } function getExternalForces(currentTime:Float, m:Move, dt:Float) { + if (this.mode == Finish) + return this.velocity.multiply(-16); var gWorkGravityDir = this.level.currentUp.multiply(-1); var A = new Vector(); - if (this.mode != Finish) - A = gWorkGravityDir.multiply(this._gravity); - if (this.mode == Finish) - A = this.velocity.multiply(-16); - if (currentTime - this.helicopterEnableTime < 5 && this.mode != Finish) { + A = gWorkGravityDir.multiply(this._gravity); + if (currentTime - this.helicopterEnableTime < 5) { A = A.multiply(0.25); } for (obj in level.forceObjects) { @@ -543,7 +542,7 @@ class Marble extends GameObject { } } } - if (contacts.length == 0) { + if (contacts.length == 0 && this.mode != Start) { var axes = this.getMarbleAxis(); var sideDir = axes[0]; var motionDir = axes[1]; @@ -775,7 +774,7 @@ class Marble extends GameObject { A = A.add(contacts[j].normal.multiply(normalForce2)); } } - if (bestSurface != -1) { + if (bestSurface != -1 && this.mode != Finish) { var vAtC = this.velocity.add(this.omega.cross(bestContact.normal.multiply(-this._radius))).sub(bestContact.velocity); var vAtCMag = vAtC.length(); var slipping = false; @@ -832,6 +831,9 @@ class Marble extends GameObject { lastContactNormal = bestContact.normal; } a = a.add(aControl); + if (this.mode == Finish) { + a.set(); // Zero it out + } return [A, a]; } @@ -988,7 +990,7 @@ class Marble extends GameObject { // for (iter in 0...10) { // var iterationFound = false; - for (obj in foundObjs.filter(x -> x.go is InteriorObject && !(x.go is PathedInterior))) { + for (obj in foundObjs.filter(x -> x.go is InteriorObject || (x.go is PathedInterior))) { // Its an MP so bruh var invMatrix = @:privateAccess obj.invTransform; @@ -1520,6 +1522,11 @@ class Marble extends GameObject { var a = retf[1]; this.velocity = this.velocity.add(A.multiply(timeStep)); this.omega = this.omega.add(a.multiply(timeStep)); + if (this.mode == Start) { + // Bruh... + this.velocity.y = 0; + this.velocity.x = 0; + } stoppedPaths = this.velocityCancel(timeState.currentAttemptTime, timeStep, isCentered, true, stoppedPaths, pathedInteriors); this._totalTime += timeStep; if (contacts.length != 0) { @@ -1593,20 +1600,20 @@ class Marble extends GameObject { } } - if (mode == Start && startPad != null) { - var upVec = this.level.currentUp; - var startpadNormal = startPad.getAbsPos().up(); - this.velocity = upVec.multiply(this.velocity.dot(upVec)); - // Apply contact forces in startPad up direction if upVec is not startpad up, fixes the weird startpad shit in pinball wizard - if (upVec.dot(startpadNormal) < 0.95) { - for (contact in contacts) { - var normF = contact.normal.multiply(contact.normalForce); - var startpadF = startpadNormal.multiply(normF.dot(startpadNormal)); - var upF = upVec.multiply(normF.dot(upVec)); - this.velocity = this.velocity.add(startpadF.multiply(timeStep / 4)); - } - } - } + // if (mode == Start) { + // var upVec = this.level.currentUp; + // var startpadNormal = startPad.getAbsPos().up(); + // this.velocity = upVec.multiply(this.velocity.dot(upVec)); + // // Apply contact forces in startPad up direction if upVec is not startpad up, fixes the weird startpad shit in pinball wizard + // if (upVec.dot(startpadNormal) < 0.95) { + // for (contact in contacts) { + // var normF = contact.normal.multiply(contact.normalForce); + // var startpadF = startpadNormal.multiply(normF.dot(startpadNormal)); + // var upF = upVec.multiply(normF.dot(upVec)); + // this.velocity = this.velocity.add(startpadF.multiply(timeStep / 4)); + // } + // } + // } // if (mode == Finish) { // this.velocity = this.velocity.multiply(0.925); diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index 24c145f4..5fdd14b5 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -35,6 +35,7 @@ class MarbleGame { var paused:Bool; var toRecord:Bool = false; + var recordingName:String; var exitGameDlg:ExitGameDlg; diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 2c95aeb6..9549f881 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1955,18 +1955,27 @@ class MarbleWorld extends Scheduler { } public function saveReplay() { + this.replay.name = MarbleGame.instance.recordingName; var replayBytes = this.replay.write(); #if hl - hxd.File.saveAs(replayBytes, { - title: 'Save Replay', - fileTypes: [ - { - name: "Replay (*.mbr)", - extensions: ["mbr"] - } - ], - defaultPath: '${this.mission.title}${this.timeState.gameplayClock}.mbr' - }); + // hxd.File.saveAs(replayBytes, { + // title: 'Save Replay', + // fileTypes: [ + // { + // name: "Replay (*.mbr)", + // extensions: ["mbr"] + // } + // ], + // defaultPath: 'data/replay/${this.mission.title}${this.timeState.gameplayClock}.mbr' + // }); + sys.FileSystem.createDirectory(haxe.io.Path.join([Settings.settingsDir, "data", "replays"])); + var replayPath = haxe.io.Path.join([ + Settings.settingsDir, + "data", + "replays", + '${this.mission.title}${this.timeState.gameplayClock}.mbr' + ]); + sys.io.File.saveBytes(replayPath, replayBytes); #end #if js var blob = new js.html.Blob([replayBytes.getData()], { diff --git a/src/Replay.hx b/src/Replay.hx index a38c5c43..c5fc0664 100644 --- a/src/Replay.hx +++ b/src/Replay.hx @@ -1,5 +1,6 @@ package src; +import hxd.fs.FileEntry; import shapes.PowerUp; import haxe.io.BytesInput; import haxe.zip.Huffman; @@ -240,6 +241,7 @@ class ReplayInitialState { class Replay { public var mission:String; + public var name:String; var frames:Array; var initialState:ReplayInitialState; @@ -250,7 +252,8 @@ class Replay { var currentPlaybackFrameIdx:Int; var currentPlaybackTime:Float; - var version:Int = 4; + var version:Int = 5; + var readFullEntry:FileEntry; public function new(mission:String) { this.mission = mission; @@ -403,7 +406,6 @@ class Replay { public function write() { var bw = new BytesWriter(); - bw.writeStr(this.mission); this.initialState.write(bw); bw.writeInt32(this.frames.length); for (frame in this.frames) { @@ -416,13 +418,17 @@ class Replay { var compressed = haxe.zip.Compress.run(bw.getBuffer(), 9); #end #if js - var stream = zip.DeflateStream.create(zip.DeflateStream.CompressionLevel.GOOD, false); + var stream = zip.DeflateStream.create(zip.DeflateStream.CompressionLevel.GOOD, true); stream.write(new BytesInput(bw.getBuffer())); var compressed = stream.finalize(); #end var finalB = new BytesBuffer(); finalB.addByte(version); + finalB.addByte(this.name.length); + finalB.addString(this.name); + finalB.addByte(this.mission.length); + finalB.addString(this.mission); finalB.addInt32(bufsize); finalB.addBytes(compressed, 0, compressed.length); @@ -436,8 +442,12 @@ class Replay { Console.log("Replay loading failed: unknown version"); return false; } - var uncompressedLength = data.getInt32(1); - var compressedData = data.sub(5, data.length - 5); + var nameLength = data.get(1); + this.name = data.getString(2, nameLength); + var missionLength = data.get(2 + nameLength); + this.mission = data.getString(3 + nameLength, missionLength); + var uncompressedLength = data.getInt32(3 + nameLength + missionLength); + var compressedData = data.sub(7 + nameLength + missionLength, data.length - 7 - nameLength - missionLength); #if hl var uncompressed = haxe.zip.Uncompress.run(compressedData, uncompressedLength); @@ -446,7 +456,6 @@ class Replay { var uncompressed = haxe.zip.InflateImpl.run(new BytesInput(compressedData), uncompressedLength); #end var br = new BytesReader(uncompressed); - this.mission = br.readStr(); this.initialState.read(br); var frameCount = br.readInt32(); this.frames = []; @@ -457,4 +466,25 @@ class Replay { } return true; } + + public function readHeader(data:Bytes, fe:FileEntry) { + this.readFullEntry = fe; + Console.log("Loading replay"); + var replayVersion = data.get(0); + if (replayVersion > version) { + Console.log("Replay loading failed: unknown version"); + return false; + } + var nameLength = data.get(1); + this.name = data.getString(2, nameLength); + var missionLength = data.get(2 + nameLength); + this.mission = data.getString(3 + nameLength, missionLength); + return true; + } + + public function readFull() { + if (readFullEntry != null) + return read(readFullEntry.getBytes()); + return false; + } } diff --git a/src/gui/MainMenuGui.hx b/src/gui/MainMenuGui.hx index c9a42013..eb6cb89e 100644 --- a/src/gui/MainMenuGui.hx +++ b/src/gui/MainMenuGui.hx @@ -96,6 +96,10 @@ class MainMenuGui extends GuiImage { replButton.position = new Vector(552, 536); replButton.extent = new Vector(191, 141); replButton.pressedAction = (sender) -> { + #if hl + MarbleGame.canvas.setContent(new ReplayCenterGui()); + #end + #if js hxd.File.browse((replayToLoad) -> { replayToLoad.load((replayData) -> { var replay = new Replay(""); @@ -126,6 +130,7 @@ class MainMenuGui extends GuiImage { } ], }); + #end }; mainMenuContent.addChild(replButton); diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index 284d1e7e..b6deb686 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -667,8 +667,7 @@ class PlayMissionGui extends GuiImage { pmRecord.position = new Vector(247, 46); pmRecord.extent = new Vector(43, 43); pmRecord.pressedAction = (sender) -> { - cast(this.parent, Canvas).marbleGame.toRecord = true; - cast(this.parent, Canvas).pushDialog(new MessageBoxOkDlg("The next mission you play will be recorded.")); + cast(this.parent, Canvas).pushDialog(new ReplayNameDlg()); }; pmMorePopDlg.addChild(pmRecord); diff --git a/src/gui/ReplayCenterGui.hx b/src/gui/ReplayCenterGui.hx new file mode 100644 index 00000000..6ddae78f --- /dev/null +++ b/src/gui/ReplayCenterGui.hx @@ -0,0 +1,145 @@ +package gui; + +import src.Mission; +import hxd.BitmapData; +import hxd.res.BitmapFont; +import src.Replay; +import src.ResourceLoader; +import h3d.Vector; +import src.Util; +import src.MarbleGame; +import src.Settings; + +class ReplayCenterGui extends GuiImage { + public function new() { + function chooseBg() { + var rand = Math.random(); + if (rand >= 0 && rand <= 0.244) + return ResourceLoader.getImage('data/ui/backgrounds/gold/${cast (Math.floor(Util.lerp(1, 12, Math.random())), Int)}.jpg'); + if (rand > 0.244 && rand <= 0.816) + return ResourceLoader.getImage('data/ui/backgrounds/platinum/${cast (Math.floor(Util.lerp(1, 28, Math.random())), Int)}.jpg'); + return ResourceLoader.getImage('data/ui/backgrounds/ultra/${cast (Math.floor(Util.lerp(1, 9, Math.random())), Int)}.jpg'); + } + var img = chooseBg(); + super(img.resource.toTile()); + + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + var wnd = new GuiImage(ResourceLoader.getResource("data/ui/replay/window.png", ResourceLoader.getImage, this.imageResources).toTile()); + wnd.position = new Vector(0, 0); + wnd.extent = new Vector(640, 480); + wnd.horizSizing = Center; + wnd.vertSizing = Center; + this.addChild(wnd); + + function loadButtonImages(path:String, hasDisabled:Bool = false) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + var disabled = hasDisabled ? ResourceLoader.getResource('${path}_i.png', ResourceLoader.getImage, this.imageResources).toTile() : null; + return [normal, hover, pressed, disabled]; + } + + var selectedIdx = -1; + + var replayList = []; + var replayPath = haxe.io.Path.join([Settings.settingsDir, "data", "replays",]); + var replayFiles = ResourceLoader.fileSystem.dir(replayPath); + for (replayFile in replayFiles) { + if (replayFile.extension == "mbr") { + var replayF = new Replay(null); + if (replayF.readHeader(replayFile.getBytes(), replayFile)) + replayList.push(replayF); + } + } + + var playButton = new GuiButton(loadButtonImages('data/ui/replay/play', true)); + playButton.position = new Vector(323, 386); + playButton.extent = new Vector(94, 46); + playButton.disabled = true; + playButton.pressedAction = (e) -> { + var repl = replayList[selectedIdx]; + if (repl.readFull()) { + var repmis = repl.mission; + if (!StringTools.contains(repmis, "data/")) + repmis = "data/" + repmis; + var mi = MissionList.missions.get(repmis); + MarbleGame.instance.watchMissionReplay(mi, repl); + } + } + wnd.addChild(playButton); + + var homeButton = new GuiButton(loadButtonImages('data/ui/replay/home')); + homeButton.position = new Vector(224, 386); + homeButton.extent = new Vector(94, 46); + homeButton.pressedAction = (e) -> { + MarbleGame.canvas.setContent(new MainMenuGui()); + } + wnd.addChild(homeButton); + + var temprev = new BitmapData(1, 1); + temprev.setPixel(0, 0, 0); + var tmpprevtile = h2d.Tile.fromBitmap(temprev); + + var pmPreview = new GuiImage(tmpprevtile); + pmPreview.position = new Vector(360, 29); + pmPreview.extent = new Vector(216, 150); + wnd.addChild(pmPreview); + + var scrollCtrl = new GuiScrollCtrl(ResourceLoader.getResource("data/ui/common/philscroll.png", ResourceLoader.getImage, this.imageResources).toTile()); + scrollCtrl.position = new Vector(30, 25); + scrollCtrl.extent = new Vector(283, 346); + wnd.addChild(scrollCtrl); + + var arial14fontdata = ResourceLoader.getFileEntry("data/font/arial.fnt"); + var arial14b = new BitmapFont(arial14fontdata.entry); + @:privateAccess arial14b.loader = ResourceLoader.loader; + var arial14 = arial14b.toSdfFont(cast 12 * Settings.uiScale, MultiChannel); + var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt"); + var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry); + @:privateAccess markerFelt32b.loader = ResourceLoader.loader; + var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); + var markerFelt24 = markerFelt32b.toSdfFont(cast 18 * Settings.uiScale, MultiChannel); + var markerFelt18 = markerFelt32b.toSdfFont(cast 14 * Settings.uiScale, MultiChannel); + + var missionName = new GuiText(markerFelt24); + missionName.position = new Vector(327, 181); + missionName.extent = new Vector(278, 14); + missionName.text.textColor = 0; + missionName.justify = Center; + wnd.addChild(missionName); + + var replayListBox = new GuiTextListCtrl(markerFelt24, replayList.map(x -> x.name)); + replayListBox.position = new Vector(0, 0); + replayListBox.extent = new Vector(283, 346); + replayListBox.textYOffset = -6; + replayListBox.scrollable = true; + replayListBox.onSelectedFunc = (idx) -> { + if (idx < 0) + return; + selectedIdx = idx; + playButton.disabled = false; + var thisReplay = replayList[idx]; + var repmis = thisReplay.mission; + if (!StringTools.contains(repmis, "data/")) + repmis = "data/" + repmis; + if (MissionList.missions == null) + MissionList.buildMissionList(); + var m = MissionList.missions.get(repmis); + missionName.text.text = m.title; + m.getPreviewImage((t) -> { + pmPreview.bmp.tile = t; + }); + } + scrollCtrl.addChild(replayListBox); + scrollCtrl.setScrollMax(replayListBox.calculateFullHeight()); + + var replayFrame = new GuiImage(ResourceLoader.getResource("data/ui/replay/replayframe.png", ResourceLoader.getImage, this.imageResources).toTile()); + replayFrame.position = new Vector(351, 21); + replayFrame.extent = new Vector(234, 168); + wnd.addChild(replayFrame); + } +} diff --git a/src/gui/ReplayNameDlg.hx b/src/gui/ReplayNameDlg.hx new file mode 100644 index 00000000..49c421db --- /dev/null +++ b/src/gui/ReplayNameDlg.hx @@ -0,0 +1,93 @@ +package gui; + +import src.MarbleGame; +import hxd.res.BitmapFont; +import h3d.Vector; +import src.ResourceLoader; +import src.Settings; + +class ReplayNameDlg extends GuiControl { + public function new() { + super(); + var text = "Enter a name for the recording"; + this.horizSizing = Width; + this.vertSizing = Height; + this.position = new Vector(); + this.extent = new Vector(640, 480); + + var domcasual24fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt"); + var domcasual24b = new BitmapFont(domcasual24fontdata.entry); + @:privateAccess domcasual24b.loader = ResourceLoader.loader; + var domcasual24 = domcasual24b.toSdfFont(cast 20 * Settings.uiScale, MultiChannel); + + function loadButtonImages(path:String) { + var normal = ResourceLoader.getResource('${path}_n.png', ResourceLoader.getImage, this.imageResources).toTile(); + var hover = ResourceLoader.getResource('${path}_h.png', ResourceLoader.getImage, this.imageResources).toTile(); + var pressed = ResourceLoader.getResource('${path}_d.png', ResourceLoader.getImage, this.imageResources).toTile(); + return [normal, hover, pressed]; + } + + var yesNoFrame = new GuiImage(ResourceLoader.getResource("data/ui/common/dialog.png", ResourceLoader.getImage, this.imageResources).toTile()); + yesNoFrame.horizSizing = Center; + yesNoFrame.vertSizing = Center; + yesNoFrame.position = new Vector(187, 156); + yesNoFrame.extent = new Vector(300, 191); + this.addChild(yesNoFrame); + + var yesNoText = new GuiMLText(domcasual24, null); + yesNoText.position = new Vector(33, 26); + yesNoText.horizSizing = Center; + yesNoText.extent = new Vector(198, 23); + yesNoText.text.text = text; + yesNoText.text.textColor = 0; + yesNoText.text.maxWidth = 198; + yesNoFrame.addChild(yesNoText); + + var textFrame = new GuiImage(ResourceLoader.getResource("data/ui/endgame/window.png", ResourceLoader.getImage, this.imageResources).toTile()); + textFrame.position = new Vector(33, 67); + textFrame.extent = new Vector(232, 40); + textFrame.horizSizing = Center; + yesNoFrame.addChild(textFrame); + + var textInput = new GuiTextInput(domcasual24); + textInput.position = new Vector(6, 5); + textInput.extent = new Vector(216, 40); + textInput.horizSizing = Width; + textInput.vertSizing = Height; + textInput.text.textColor = 0; + textInput.text.selectionColor.setColor(0xFFFFFFFF); + textInput.text.selectionTile = h2d.Tile.fromColor(0x808080, 0, hxd.Math.ceil(textInput.text.font.lineHeight)); + textFrame.addChild(textInput); + + var yesButton = new GuiButton(loadButtonImages("data/ui/common/ok")); + yesButton.position = new Vector(171, 124); + yesButton.extent = new Vector(95, 45); + yesButton.vertSizing = Top; + yesButton.accelerator = hxd.Key.ENTER; + yesButton.pressedAction = (sender) -> { + if (StringTools.trim(textInput.text.text) != "") { + MarbleGame.instance.toRecord = true; + MarbleGame.instance.recordingName = textInput.text.text; + MarbleGame.canvas.popDialog(this); + } + } + yesNoFrame.addChild(yesButton); + + var noButton = new GuiButton(loadButtonImages("data/ui/common/cancel")); + noButton.position = new Vector(44, 124); + noButton.extent = new Vector(88, 41); + noButton.vertSizing = Top; + noButton.accelerator = hxd.Key.ESCAPE; + noButton.pressedAction = (sender) -> { + MarbleGame.canvas.popDialog(this); + } + yesNoFrame.addChild(noButton); + + if (yesNoText.text.getBounds().yMax > yesNoText.extent.y) { + var diff = yesNoText.text.getBounds().yMax - yesNoText.extent.y; + yesNoFrame.extent.y += diff; + yesButton.position.y += diff; + noButton.position.y += diff; + } + } +}