From e6b5dbbb256bdd047527e5939baf9d49649ea436 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Sat, 6 May 2023 23:21:13 +0530 Subject: [PATCH] rewind barebones --- src/MarbleWorld.hx | 28 ++++++ src/rewind/RewindFrame.hx | 83 ++++++++++++++++ src/rewind/RewindManager.hx | 193 ++++++++++++++++++++++++++++++++++++ src/shapes/AntiGravity.hx | 8 +- src/shapes/Gem.hx | 2 +- src/shapes/LandMine.hx | 6 +- src/shapes/Nuke.hx | 2 +- src/shapes/PowerUp.hx | 2 +- src/shapes/TimeTravel.hx | 3 +- 9 files changed, 317 insertions(+), 10 deletions(-) create mode 100644 src/rewind/RewindFrame.hx create mode 100644 src/rewind/RewindManager.hx diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 002076a5..105d4194 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,6 @@ package src; +import rewind.RewindManager; import Macros.MarbleWorldMacros; import shapes.PushButton; #if js @@ -179,6 +180,10 @@ class MarbleWorld extends Scheduler { public var isWatching:Bool = false; public var isRecording:Bool = false; + // Rewind + public var rewindManager:RewindManager; + public var rewinding:Bool = false; + // Loading var resourceLoadFuncs:Array<(() -> Void)->Void> = []; @@ -205,6 +210,7 @@ class MarbleWorld extends Scheduler { this.game = mission.game.toLowerCase(); this.replay = new Replay(mission.path, mission.isClaMission ? mission.id : 0); this.isRecording = record; + this.rewindManager = new RewindManager(this); } public function init() { @@ -448,6 +454,8 @@ class MarbleWorld extends Scheduler { } else this.replay.rewind(); + this.rewindManager.clear(); + this.timeState.currentAttemptTime = 0; this.timeState.gameplayClock = 0; this.bonusTime = 0; @@ -997,6 +1005,23 @@ class MarbleWorld extends Scheduler { } } + if (Key.isDown(Key.R)) { + this.rewinding = true; + } else { + this.rewinding = false; + } + + if (this.rewinding) { + var rframe = rewindManager.getNextRewindFrame(timeState.currentAttemptTime - dt); + if (rframe != null) { + var actualDt = timeState.currentAttemptTime - rframe.timeState.currentAttemptTime - dt; + dt = actualDt; + rewindManager.applyFrame(rframe); + } + } + if (dt < 0) + return; + ProfilerUI.measure("updateTimer"); this.updateTimer(dt); @@ -1080,6 +1105,9 @@ class MarbleWorld extends Scheduler { } } + if (!this.rewinding) + this.rewindManager.recordFrame(); + this.updateTexts(); } diff --git a/src/rewind/RewindFrame.hx b/src/rewind/RewindFrame.hx new file mode 100644 index 00000000..c5698f2e --- /dev/null +++ b/src/rewind/RewindFrame.hx @@ -0,0 +1,83 @@ +package rewind; + +import src.PathedInterior.PIState; +import shapes.PowerUp; +import h3d.Vector; +import h3d.Quat; +import src.TimeState; + +@:publicFields +class RewindFrame { + var timeState:TimeState; + var marblePosition:Vector; + var marbleOrientation:Quat; + var marbleVelocity:Vector; + var marbleAngularVelocity:Vector; + var marblePowerup:PowerUp; + var bonusTime:Float; + var mpStates:Array<{ + curState:PIState, + prevState:PIState, + stopped:Bool, + stopTime:Float + }>; + var gemCount:Int; + var gemStates:Array; + var powerupStates:Array; + var landMineStates:Array; + var activePowerupStates:Array; + var currentUp:Vector; + var trapdoorStates:Array<{lastContactTime:Float, lastDirection:Int, lastCompletion:Float}>; + var lastContactNormal:Vector; + + public function new() {} + + public function clone() { + var c = new RewindFrame(); + c.timeState = timeState.clone(); + c.marblePosition = marblePosition.clone(); + c.marbleOrientation = marbleOrientation.clone(); + c.marbleVelocity = marbleVelocity.clone(); + c.marbleAngularVelocity = marbleAngularVelocity.clone(); + c.marblePowerup = marblePowerup; + c.bonusTime = bonusTime; + c.gemCount = gemCount; + c.gemStates = gemStates.copy(); + c.powerupStates = powerupStates.copy(); + c.landMineStates = landMineStates.copy(); + c.activePowerupStates = activePowerupStates.copy(); + c.currentUp = currentUp.clone(); + c.lastContactNormal = lastContactNormal.clone(); + c.mpStates = []; + for (s in mpStates) { + c.mpStates.push({ + curState: { + currentTime: s.curState.currentTime, + targetTime: s.curState.targetTime, + velocity: s.curState.velocity.clone(), + currentPosition: s.curState.currentPosition.clone(), + prevPosition: s.curState.prevPosition.clone(), + changeTime: s.curState.changeTime, + }, + stopTime: s.stopTime, + stopped: s.stopped, + prevState: s.prevState != null ? { + currentTime: s.prevState.currentTime, + targetTime: s.prevState.targetTime, + velocity: s.prevState.velocity.clone(), + currentPosition: s.prevState.currentPosition.clone(), + prevPosition: s.prevState.prevPosition.clone(), + changeTime: s.prevState.changeTime, + } : null, + }); + } + c.trapdoorStates = []; + for (s in trapdoorStates) { + c.trapdoorStates.push({ + lastContactTime: s.lastContactTime, + lastDirection: s.lastDirection, + lastCompletion: s.lastCompletion, + }); + } + } +} diff --git a/src/rewind/RewindManager.hx b/src/rewind/RewindManager.hx new file mode 100644 index 00000000..d005566c --- /dev/null +++ b/src/rewind/RewindManager.hx @@ -0,0 +1,193 @@ +package rewind; + +import shapes.PowerUp; +import shapes.LandMine; +import src.MarbleWorld; +import shapes.Trapdoor; +import src.Util; + +class RewindManager { + var frames:Array = []; + var level:MarbleWorld; + + public function new(level:MarbleWorld) { + this.level = level; + } + + public function recordFrame() { + var rf = new RewindFrame(); + rf.timeState = level.timeState.clone(); + rf.marblePosition = level.marble.getAbsPos().getPosition().clone(); + rf.marbleOrientation = level.marble.getRotationQuat().clone(); + rf.marbleVelocity = level.marble.velocity.clone(); + rf.marbleAngularVelocity = level.marble.omega.clone(); + rf.marblePowerup = level.marble.heldPowerup; + rf.bonusTime = level.bonusTime; + rf.gemCount = level.gemCount; + rf.gemStates = level.gems.map(x -> x.pickedUp); + rf.activePowerupStates = [@:privateAccess + level.marble.superBounceEnableTime, @:privateAccess + level.marble.shockAbsorberEnableTime, @:privateAccess + level.marble.helicopterEnableTime, @:privateAccess + level.marble.megaMarbleEnableTime + ]; + rf.currentUp = level.currentUp.clone(); + rf.lastContactNormal = level.marble.lastContactNormal.clone(); + rf.mpStates = level.pathedInteriors.map(x -> { + return { + curState: { + currentTime: x.currentTime, + targetTime: x.targetTime, + velocity: x.velocity.clone(), + currentPosition: x.currentPosition.clone(), + prevPosition: x.prevPosition.clone(), + changeTime: x.changeTime, + }, + stopTime: @:privateAccess x.stopTime, + stopped: @:privateAccess x.stopped, + prevState: @:privateAccess x.previousState != null ? { + currentTime: @:privateAccess x.previousState.currentTime, + targetTime: @:privateAccess x.previousState.targetTime, + velocity: @:privateAccess x.previousState.velocity.clone(), + currentPosition: @:privateAccess x.previousState.currentPosition.clone(), + prevPosition: @:privateAccess x.previousState.prevPosition.clone(), + changeTime: @:privateAccess x.previousState.changeTime, + } : null, + } + }); + rf.powerupStates = []; + rf.landMineStates = []; + rf.trapdoorStates = []; + for (dts in level.dtsObjects) { + if (dts is PowerUp) { + var pow:PowerUp = cast dts; + rf.powerupStates.push(pow.lastPickUpTime); + } + if (dts is LandMine) { + var lm:LandMine = cast dts; + rf.landMineStates.push(lm.disappearTime); + } + if (dts is Trapdoor) { + var td:Trapdoor = cast dts; + rf.trapdoorStates.push({ + lastCompletion: td.lastCompletion, + lastDirection: td.lastDirection, + lastContactTime: td.lastContactTime + }); + } + } + frames.push(rf); + } + + public function applyFrame(rf:RewindFrame) { + level.timeState = rf.timeState.clone(); + level.marble.setPosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z); + level.marble.setRotationQuat(rf.marbleOrientation.clone()); + level.marble.velocity.set(rf.marbleVelocity.x, rf.marbleVelocity.y, rf.marbleVelocity.z); + level.marble.omega.set(rf.marbleAngularVelocity.x, rf.marbleAngularVelocity.y, rf.marbleAngularVelocity.z); + + if (level.marble.heldPowerup == null) { + if (rf.marblePowerup != null) { + level.pickUpPowerUp(rf.marblePowerup); + } + } else { + if (rf.marblePowerup == null) { + level.deselectPowerUp(); + } else { + level.pickUpPowerUp(rf.marblePowerup); + } + } + + level.bonusTime = rf.bonusTime; + level.gemCount = rf.gemCount; + @:privateAccess level.playGui.formatGemCounter(level.gemCount, level.totalGems); + for (i in 0...rf.gemStates.length) { + level.gems[i].setHide(rf.gemStates[i]); + } + @:privateAccess level.marble.superBounceEnableTime = rf.activePowerupStates[0]; + @:privateAccess level.marble.shockAbsorberEnableTime = rf.activePowerupStates[1]; + @:privateAccess level.marble.helicopterEnableTime = rf.activePowerupStates[2]; + @:privateAccess level.marble.megaMarbleEnableTime = rf.activePowerupStates[3]; + + if (level.currentUp.x != rf.currentUp.x || level.currentUp.y != rf.currentUp.y || level.currentUp.z != rf.currentUp.z) { + level.setUp(rf.currentUp, level.timeState); + // Hacky things + @:privateAccess level.orientationChangeTime = level.timeState.currentAttemptTime - 300; + var oldorient = level.newOrientationQuat; + level.newOrientationQuat = @:privateAccess level.oldOrientationQuat; + @:privateAccess level.oldOrientationQuat = oldorient; + } + + var gravitycompletion = Util.clamp((level.timeState.currentAttemptTime - @:privateAccess level.orientationChangeTime) / 300, 0, 1); + if (gravitycompletion == 0) { + level.newOrientationQuat = @:privateAccess level.oldOrientationQuat; + @:privateAccess level.orientationChangeTime = -1e8; + } + + level.currentUp.set(rf.currentUp.x, rf.currentUp.y, rf.currentUp.z); + level.marble.lastContactNormal.set(rf.lastContactNormal.x, rf.lastContactNormal.y, rf.lastContactNormal.z); + for (i in 0...rf.mpStates.length) { + level.pathedInteriors[i].currentTime = rf.mpStates[i].curState.currentTime; + level.pathedInteriors[i].targetTime = rf.mpStates[i].curState.targetTime; + level.pathedInteriors[i].velocity.set(rf.mpStates[i].curState.velocity.x, rf.mpStates[i].curState.velocity.y, rf.mpStates[i].curState.velocity.z); + level.pathedInteriors[i].currentPosition.set(rf.mpStates[i].curState.currentPosition.x, rf.mpStates[i].curState.currentPosition.y, + rf.mpStates[i].curState.currentPosition.z); + level.pathedInteriors[i].prevPosition.set(rf.mpStates[i].curState.prevPosition.x, rf.mpStates[i].curState.prevPosition.y, + rf.mpStates[i].curState.prevPosition.z); + level.pathedInteriors[i].changeTime = rf.mpStates[i].curState.changeTime; + @:privateAccess level.pathedInteriors[i].stopTime = rf.mpStates[i].stopTime; + @:privateAccess level.pathedInteriors[i].stopped = rf.mpStates[i].stopped; + if (rf.mpStates[i].prevState != null) { + @:privateAccess level.pathedInteriors[i].previousState.currentTime = rf.mpStates[i].prevState.currentTime; + @:privateAccess level.pathedInteriors[i].previousState.targetTime = rf.mpStates[i].prevState.targetTime; + @:privateAccess level.pathedInteriors[i].previousState.velocity.set(rf.mpStates[i].prevState.velocity.x, rf.mpStates[i].prevState.velocity.y, + rf.mpStates[i].prevState.velocity.z); + @:privateAccess level.pathedInteriors[i].previousState.currentPosition.set(rf.mpStates[i].prevState.currentPosition.x, + rf.mpStates[i].prevState.currentPosition.y, rf.mpStates[i].prevState.currentPosition.z); + @:privateAccess level.pathedInteriors[i].previousState.prevPosition.set(rf.mpStates[i].prevState.prevPosition.x, + rf.mpStates[i].prevState.prevPosition.y, rf.mpStates[i].prevState.prevPosition.z); + @:privateAccess level.pathedInteriors[i].previousState.changeTime = rf.mpStates[i].prevState.changeTime; + } else { + @:privateAccess level.pathedInteriors[i].previousState = null; + } + } + var pstates = rf.powerupStates.copy(); + var lmstates = rf.landMineStates.copy(); + var tstates = rf.trapdoorStates.copy(); + for (dts in level.dtsObjects) { + if (dts is PowerUp) { + var pow:PowerUp = cast dts; + pow.lastPickUpTime = pstates.shift(); + } + if (dts is LandMine) { + var lm:LandMine = cast dts; + lm.disappearTime = lmstates.shift(); + } + if (dts is Trapdoor) { + var td:Trapdoor = cast dts; + var tdState = tstates.shift(); + td.lastCompletion = tdState.lastCompletion; + td.lastDirection = tdState.lastDirection; + td.lastContactTime = tdState.lastContactTime; + } + } + } + + public function getNextRewindFrame(absTime:Float):RewindFrame { + if (frames.length == 0) + return null; + + var topFrame = frames[frames.length - 1]; + while (topFrame.timeState.currentAttemptTime > absTime) { + frames.pop(); + if (frames.length == 0) + return null; + topFrame = frames[frames.length - 1]; + } + return topFrame; + } + + public function clear() { + frames = []; + } +} diff --git a/src/shapes/AntiGravity.hx b/src/shapes/AntiGravity.hx index de4c5895..948dbbf1 100644 --- a/src/shapes/AntiGravity.hx +++ b/src/shapes/AntiGravity.hx @@ -27,9 +27,11 @@ class AntiGravity extends PowerUp { } public function use(timeState:TimeState) { - var direction = new Vector(0, 0, -1); - direction.transform(this.getRotationQuat().toMatrix()); - this.level.setUp(direction, timeState); + if (!this.level.rewinding) { + var direction = new Vector(0, 0, -1); + direction.transform(this.getRotationQuat().toMatrix()); + this.level.setUp(direction, timeState); + } // marble.body.addLinearVelocity(this.level.currentUp.scale(20)); // Simply add to vertical velocity // if (!this.level.rewinding) // AudioManager.play(this.sounds[1]); diff --git a/src/shapes/Gem.hx b/src/shapes/Gem.hx index 677d6de5..302e1027 100644 --- a/src/shapes/Gem.hx +++ b/src/shapes/Gem.hx @@ -53,7 +53,7 @@ class Gem extends DtsObject { override function onMarbleInside(timeState:TimeState) { super.onMarbleInside(timeState); - if (this.pickedUp) + if (this.pickedUp || this.level.rewinding) return; this.pickedUp = true; this.setOpacity(0); // Hide the gem diff --git a/src/shapes/LandMine.hx b/src/shapes/LandMine.hx index e747a474..92154ee6 100644 --- a/src/shapes/LandMine.hx +++ b/src/shapes/LandMine.hx @@ -122,13 +122,13 @@ class LandMine extends DtsObject { } override function onMarbleContact(timeState:TimeState, ?contact:CollisionInfo) { - if (this.isCollideable) { + if (this.isCollideable && !this.level.rewinding) { // marble.velocity = marble.velocity.add(vec); this.disappearTime = timeState.timeSinceLoad; this.setCollisionEnabled(false); - // if (!this.level.rewinding) - AudioManager.playSound(ResourceLoader.getResource("data/sound/explode1.wav", ResourceLoader.getAudio, this.soundResources)); + if (!this.level.rewinding) + AudioManager.playSound(ResourceLoader.getResource("data/sound/explode1.wav", ResourceLoader.getAudio, this.soundResources)); this.level.particleManager.createEmitter(landMineParticle, landMineParticleData, this.getAbsPos().getPosition()); this.level.particleManager.createEmitter(landMineSmokeParticle, landMineSmokeParticleData, this.getAbsPos().getPosition()); this.level.particleManager.createEmitter(landMineSparksParticle, landMineSparkParticleData, this.getAbsPos().getPosition()); diff --git a/src/shapes/Nuke.hx b/src/shapes/Nuke.hx index 14a963bd..080ca098 100644 --- a/src/shapes/Nuke.hx +++ b/src/shapes/Nuke.hx @@ -122,7 +122,7 @@ class Nuke extends DtsObject { } override function onMarbleContact(timeState:TimeState, ?contact:CollisionInfo) { - if (this.isCollideable) { + if (this.isCollideable && !this.level.rewinding) { // marble.velocity = marble.velocity.add(vec); this.disappearTime = timeState.timeSinceLoad; this.setCollisionEnabled(false); diff --git a/src/shapes/PowerUp.hx b/src/shapes/PowerUp.hx index 49fedb6e..f3ddf45b 100644 --- a/src/shapes/PowerUp.hx +++ b/src/shapes/PowerUp.hx @@ -45,7 +45,7 @@ abstract class PowerUp extends DtsObject { if (this.element.showhelponpickup == "1" && !this.autoUse) this.level.displayHelp('Press to use the ${this.pickUpName}!'); - if (pickupSound != null) { + if (pickupSound != null && !this.level.rewinding) { AudioManager.playSound(pickupSound); } } diff --git a/src/shapes/TimeTravel.hx b/src/shapes/TimeTravel.hx index 087a07e6..226539f0 100644 --- a/src/shapes/TimeTravel.hx +++ b/src/shapes/TimeTravel.hx @@ -43,6 +43,7 @@ class TimeTravel extends PowerUp { } public function use(time:TimeState) { - level.addBonusTime(this.timeBonus); + if (!this.level.rewinding) + level.addBonusTime(this.timeBonus); } }