diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 5b1be0e1..74527df9 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,7 @@ package src; +import shapes.Explodable; +import net.ExplodablePredictionStore; import gui.MPPreGameDlg; import src.Radar; import rewind.InputRecorder; @@ -145,6 +147,7 @@ class MarbleWorld extends Scheduler { public var dtsObjects:Array = []; public var powerUps:Array = []; public var forceObjects:Array = []; + public var explodables:Array = []; public var triggers:Array = []; public var gems:Array = []; public var namedObjects:Map = []; @@ -230,6 +233,7 @@ class MarbleWorld extends Scheduler { var predictions:MarblePredictionStore; var powerupPredictions:PowerupPredictionStore; var gemPredictions:GemPredictionStore; + var explodablePredictions:ExplodablePredictionStore; public var lastMoves:MarbleUpdateQueue; @@ -274,6 +278,7 @@ class MarbleWorld extends Scheduler { predictions = new MarblePredictionStore(); powerupPredictions = new PowerupPredictionStore(); gemPredictions = new GemPredictionStore(); + explodablePredictions = new ExplodablePredictionStore(); } } @@ -613,6 +618,7 @@ class MarbleWorld extends Scheduler { predictions = new MarblePredictionStore(); powerupPredictions = new PowerupPredictionStore(); gemPredictions = new GemPredictionStore(); + explodablePredictions = new ExplodablePredictionStore(); } } @@ -1171,6 +1177,13 @@ class MarbleWorld extends Scheduler { if (obj is ForceObject) { this.forceObjects.push(cast obj); } + if (obj is Explodable) { + var exp:Explodable = cast obj; + exp.netId = this.explodables.length; + this.explodables.push(exp); + if (Net.isClient) + explodablePredictions.alloc(); + } obj.isTSStatic = isTsStatic; obj.init(cast this, () -> { obj.update(this.timeState); @@ -1485,7 +1498,7 @@ class MarbleWorld extends Scheduler { // if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { // Only for our clients pls // if (qm != null) { // var mvs = qm.powerupStates.copy(); - for (pw in marble.level.powerUps) { + for (pw in powerUps) { // var val = mvs.shift(); // if (pw.lastPickUpTime != val) // Console.log('Revert powerup pickup: ${pw.lastPickUpTime} -> ${val}'); @@ -1493,6 +1506,9 @@ class MarbleWorld extends Scheduler { if (pw.pickupClient != -1 && marbleNeedsPrediction & (1 << pw.pickupClient) > 0) pw.lastPickUpTime = powerupPredictions.getState(pw.netIndex); } + for (exp in explodables) { + exp.revertContactTicks(explodablePredictions.getState(exp.netId)); + } var huntMode:HuntMode = cast this.gameMode; if (@:privateAccess huntMode.activeGemSpawnGroup != null) { for (activeGem in @:privateAccess huntMode.activeGemSpawnGroup) { @@ -2807,7 +2823,8 @@ class MarbleWorld extends Scheduler { dtsObject.dispose(); } dtsObjects = null; - powerUps = []; + powerUps = null; + explodables = null; for (trigger in this.triggers) { trigger.dispose(); } diff --git a/src/ParticleSystem.hx b/src/ParticleSystem.hx index 9ef36a1d..ba53a797 100644 --- a/src/ParticleSystem.hx +++ b/src/ParticleSystem.hx @@ -65,9 +65,9 @@ class Particle { public function update(time:Float, dt:Float) { var t = dt; var a = this.acc; - a.load(a.sub(this.vel.multiply(this.o.dragCoefficient))); - this.vel.load(this.vel.add(a.multiply(dt))); - this.position.load(this.position.add(this.vel.multiply(dt))); + a = a.sub(this.vel.multiply(this.o.dragCoefficient)); + this.vel = this.vel.add(a.multiply(dt)); + this.position = this.position.add(this.vel.multiply(dt)); this.currentAge += dt; diff --git a/src/Util.hx b/src/Util.hx index b96eb676..cd70bb16 100644 --- a/src/Util.hx +++ b/src/Util.hx @@ -429,7 +429,7 @@ class Util { #end } - public static inline inline function isIOS() { + public static inline function isIOS() { #if js var reg = ~/iPad|iPhone|iPod/; return reg.match(js.Browser.navigator.userAgent); diff --git a/src/net/ExplodablePredictionStore.hx b/src/net/ExplodablePredictionStore.hx new file mode 100644 index 00000000..bd22f5b0 --- /dev/null +++ b/src/net/ExplodablePredictionStore.hx @@ -0,0 +1,25 @@ +package net; + +import net.NetPacket.ExplodableUpdatePacket; +import src.TimeState; +import net.NetPacket.PowerupPickupPacket; + +class ExplodablePredictionStore { + var predictions:Array; + + public inline function new() { + predictions = []; + } + + public inline function alloc() { + predictions.push(-100000); + } + + public inline function getState(netIndex:Int) { + return predictions[netIndex]; + } + + public inline function acknowledgeExplodableUpdate(packet:ExplodableUpdatePacket) { + predictions[packet.explodableId] = packet.serverTicks; + } +} diff --git a/src/net/Net.hx b/src/net/Net.hx index b7850804..28e21355 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -1,5 +1,6 @@ package net; +import net.NetPacket.ExplodableUpdatePacket; import gui.MPMessageGui; import gui.MessageBoxOkDlg; import gui.JoinServerGui; @@ -37,6 +38,7 @@ enum abstract NetPacketType(Int) from Int to Int { var PowerupPickup; var GemSpawn; var GemPickup; + var ExplodableUpdate; var PlayerInfo; var ScoreBoardInfo; } @@ -784,6 +786,13 @@ class Net { @:privateAccess MarbleGame.instance.world.playGui.updatePlayerScores(scoreboardPacket); } + case ExplodableUpdate: + var explodableUpdatePacket = new ExplodableUpdatePacket(); + explodableUpdatePacket.deserialize(input); + if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) { + @:privateAccess MarbleGame.instance.world.explodablePredictions.acknowledgeExplodableUpdate(explodableUpdatePacket); + } + case _: Console.log("unknown command: " + packetType); } diff --git a/src/net/NetPacket.hx b/src/net/NetPacket.hx index b2279de1..bae8441d 100644 --- a/src/net/NetPacket.hx +++ b/src/net/NetPacket.hx @@ -208,6 +208,24 @@ class PowerupPickupPacket implements NetPacket { } } +@:publicFields +class ExplodableUpdatePacket implements NetPacket { + var serverTicks:Int; + var explodableId:Int; + + public function new() {} + + public inline function deserialize(b:InputBitStream) { + serverTicks = b.readUInt16(); + explodableId = b.readInt(11); + } + + public inline function serialize(b:OutputBitStream) { + b.writeUInt16(serverTicks); + b.writeInt(explodableId, 11); + } +} + @:publicFields class GemSpawnPacket implements NetPacket { var gemIds:Array; diff --git a/src/shapes/Explodable.hx b/src/shapes/Explodable.hx new file mode 100644 index 00000000..a077bc29 --- /dev/null +++ b/src/shapes/Explodable.hx @@ -0,0 +1,154 @@ +package shapes; + +import src.ParticleSystem.ParticleEmitter; +import src.Marble; +import h3d.Vector; +import src.ParticleSystem.ParticleEmitterOptions; +import net.BitStream.OutputBitStream; +import net.NetPacket.ExplodableUpdatePacket; +import collision.CollisionInfo; +import src.ParticleSystem.ParticleData; +import src.DtsObject; +import src.TimeState; +import src.Util; +import net.Net; +import src.MarbleWorld; +import src.ResourceLoader; +import src.AudioManager; + +abstract class Explodable extends DtsObject { + var particle:ParticleEmitterOptions; + var smokeParticle:ParticleEmitterOptions; + var sparksParticle:ParticleEmitterOptions; + + var particleData:ParticleData; + var smokeParticleData:ParticleData; + var sparkParticleData:ParticleData; + + var disappearTime = -1e8; + var lastContactTick:Int = -100000; + + var renewTime = 5000; + + var explodeSoundFile:String = "data/sound/explode1.wav"; + + var emitter1:ParticleEmitter; + var emitter2:ParticleEmitter; + var emitter3:ParticleEmitter; + + public var netId:Int; + + override function update(timeState:TimeState) { + super.update(timeState); + + if (Net.isMP) { + if (Net.isHost) { + if (timeState.ticks >= this.lastContactTick + (renewTime >> 5) || timeState.ticks < this.lastContactTick) { + this.setHide(false); + } else { + this.setHide(true); + } + + var opacity = Util.clamp((timeState.ticks - (this.lastContactTick + (renewTime >> 5))), 0, 1); + this.setOpacity(opacity); + } else { + if (@:privateAccess level.marble.serverTicks >= this.lastContactTick + (renewTime >> 5) || @:privateAccess level.marble.serverTicks < this.lastContactTick) { + this.setHide(false); + } else { + this.setHide(true); + } + + var opacity = Util.clamp((@:privateAccess level.marble.serverTicks - (this.lastContactTick + (renewTime >> 5))), 0, 1); + this.setOpacity(opacity); + } + } else { + if (timeState.timeSinceLoad >= this.disappearTime + (renewTime / 1000) || timeState.timeSinceLoad < this.disappearTime) { + this.setHide(false); + } else { + this.setHide(true); + } + + var opacity = Util.clamp((timeState.timeSinceLoad - (this.disappearTime + (renewTime / 1000))), 0, 1); + this.setOpacity(opacity); + } + } + + public override function init(level:MarbleWorld, onFinish:Void->Void) { + super.init(level, () -> { + ResourceLoader.load(explodeSoundFile).entry.load(onFinish); + }); + } + + override function onMarbleContact(marble:src.Marble, timeState:TimeState, ?contact:CollisionInfo) { + if (this.isCollideable && !this.level.rewinding) { + // marble.velocity = marble.velocity.add(vec); + this.disappearTime = timeState.timeSinceLoad; + if (Net.isClient) { + this.lastContactTick = @:privateAccess marble.serverTicks; + } else { + this.lastContactTick = timeState.ticks; + } + this.setCollisionEnabled(false); + + if (!this.level.rewinding && @:privateAccess !marble.isNetUpdate) + AudioManager.playSound(ResourceLoader.getResource(explodeSoundFile, ResourceLoader.getAudio, this.soundResources)); + if (@:privateAccess !marble.isNetUpdate) { + emitter1 = this.level.particleManager.createEmitter(particle, particleData, this.getAbsPos().getPosition()); + emitter2 = this.level.particleManager.createEmitter(smokeParticle, smokeParticleData, this.getAbsPos().getPosition()); + emitter3 = this.level.particleManager.createEmitter(sparksParticle, sparkParticleData, this.getAbsPos().getPosition()); + } + + // var minePos = this.getAbsPos().getPosition(); + // var off = marble.getAbsPos().getPosition().sub(minePos); + + // var strength = computeExplosionStrength(off.length()); + + // var impulse = off.normalized().multiply(strength); + applyImpulse(marble); + + if (Net.isHost) { + var packet = new ExplodableUpdatePacket(); + packet.explodableId = netId; + packet.serverTicks = timeState.ticks; + var os = new OutputBitStream(); + os.writeByte(ExplodableUpdate); + packet.serialize(os); + Net.sendPacketToIngame(os); + } + + // light = new h3d.scene.fwd.PointLight(MarbleGame.instance.scene); + // light.setPosition(minePos.x, minePos.y, minePos.z); + // light.enableSpecular = false; + + // for (collider in this.colliders) { + // var hull:CollisionHull = cast collider; + // hull.force = strength; + // } + } + // Normally, we would add a light here, but that's too expensive for THREE, apparently. + + // this.level.replay.recordMarbleContact(this); + } + + public function revertContactTicks(ticks:Int) { + this.lastContactTick = ticks; + if (level.timeState.ticks >= this.lastContactTick + (renewTime >> 5) || level.timeState.ticks < this.lastContactTick) { + if (emitter1 != null) { + this.level.particleManager.removeEmitter(emitter1); + emitter1 = null; + } + + if (emitter2 != null) { + this.level.particleManager.removeEmitter(emitter2); + emitter2 = null; + } + + if (emitter3 != null) { + this.level.particleManager.removeEmitter(emitter3); + emitter3 = null; + } + } + } + + abstract function applyImpulse(marble:Marble):Void; +} diff --git a/src/shapes/LandMine.hx b/src/shapes/LandMine.hx index 1b893693..057f0266 100644 --- a/src/shapes/LandMine.hx +++ b/src/shapes/LandMine.hx @@ -1,5 +1,8 @@ package shapes; +import net.BitStream.OutputBitStream; +import net.NetPacket.ExplodableUpdatePacket; +import net.Net; import src.AudioManager; import src.TimeState; import collision.CollisionHull; @@ -90,13 +93,7 @@ final landMineSparksParticle:ParticleEmitterOptions = { } }; -class LandMine extends DtsObject { - var disappearTime = -1e8; - - var landMineParticleData:ParticleData; - var landMineSmokeParticleData:ParticleData; - var landMineSparkParticleData:ParticleData; - +class LandMine extends Explodable { var light:h3d.scene.fwd.PointLight; public function new() { @@ -105,57 +102,21 @@ class LandMine extends DtsObject { this.identifier = "LandMine"; this.isCollideable = true; - landMineParticleData = new ParticleData(); - landMineParticleData.identifier = "landMineParticle"; - landMineParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); + particleData = new ParticleData(); + particleData.identifier = "landMineParticle"; + particleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); - landMineSmokeParticleData = new ParticleData(); - landMineSmokeParticleData.identifier = "landMineSmokeParticle"; - landMineSmokeParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); + smokeParticleData = new ParticleData(); + smokeParticleData.identifier = "landMineSmokeParticle"; + smokeParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); - landMineSparkParticleData = new ParticleData(); - landMineSparkParticleData.identifier = "landMineSparkParticle"; - landMineSparkParticleData.texture = ResourceLoader.getResource("data/particles/spark.png", ResourceLoader.getTexture, this.textureResources); - } + sparkParticleData = new ParticleData(); + sparkParticleData.identifier = "landMineSparkParticle"; + sparkParticleData.texture = ResourceLoader.getResource("data/particles/spark.png", ResourceLoader.getTexture, this.textureResources); - public override function init(level:MarbleWorld, onFinish:Void->Void) { - super.init(level, () -> { - ResourceLoader.load("sound/explode1.wav").entry.load(onFinish); - }); - } - - override function onMarbleContact(marble:src.Marble, timeState:TimeState, ?contact:CollisionInfo) { - 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)); - 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()); - - var minePos = this.getAbsPos().getPosition(); - var off = marble.getAbsPos().getPosition().sub(minePos); - - var strength = computeExplosionStrength(off.length()); - - var impulse = off.normalized().multiply(strength); - marble.applyImpulse(impulse); - - // light = new h3d.scene.fwd.PointLight(MarbleGame.instance.scene); - // light.setPosition(minePos.x, minePos.y, minePos.z); - // light.enableSpecular = false; - - // for (collider in this.colliders) { - // var hull:CollisionHull = cast collider; - // hull.force = strength; - // } - } - // Normally, we would add a light here, but that's too expensive for THREE, apparently. - - // this.level.replay.recordMarbleContact(this); + this.smokeParticle = landMineSmokeParticle; + this.sparksParticle = landMineSparksParticle; + this.particle = landMineParticle; } function computeExplosionStrength(r:Float) { @@ -172,28 +133,13 @@ class LandMine extends DtsObject { return v; } - override function update(timeState:TimeState) { - super.update(timeState); - if (timeState.timeSinceLoad >= this.disappearTime + 5 || timeState.timeSinceLoad < this.disappearTime) { - this.setHide(false); - } else { - this.setHide(true); - } + public function applyImpulse(marble:src.Marble) { + var minePos = this.getAbsPos().getPosition(); + var off = marble.getAbsPos().getPosition().sub(minePos); - // if (light != null) { - // var t = Util.clamp((timeState.timeSinceLoad - this.disappearTime) / 1.2, 0, 1); + var strength = computeExplosionStrength(off.length()); - // light.color = Util.lerpThreeVectors(new Vector(0.5, 0.5, 0), new Vector(0, 0, 0), t); - // var radius = Util.lerp(6, 3, t); - // light.params = new Vector(0, 1 / radius, 0); - - // if (t >= 1) { - // light.remove(); - // light = null; - // } - // } - - var opacity = Util.clamp((timeState.timeSinceLoad - (this.disappearTime + 5)), 0, 1); - this.setOpacity(opacity); + var impulse = off.normalized().multiply(strength); + marble.applyImpulse(impulse); } } diff --git a/src/shapes/Nuke.hx b/src/shapes/Nuke.hx index 8af41d36..f4cdf7ca 100644 --- a/src/shapes/Nuke.hx +++ b/src/shapes/Nuke.hx @@ -1,5 +1,7 @@ package shapes; +import net.BitStream.OutputBitStream; +import net.NetPacket.ExplodableUpdatePacket; import src.AudioManager; import src.TimeState; import collision.CollisionHull; @@ -11,6 +13,7 @@ import src.ParticleSystem.ParticleData; import h3d.Vector; import src.ResourceLoader; import src.MarbleWorld; +import net.Net; final nukeParticle:ParticleEmitterOptions = { ejectionPeriod: 0.2, @@ -89,71 +92,31 @@ final nukeSparksParticle:ParticleEmitterOptions = { } }; -class Nuke extends DtsObject { - var disappearTime = -1e8; - - var nukeParticleData:ParticleData; - var nukeSmokeParticleData:ParticleData; - var nukeSparkParticleData:ParticleData; - +class Nuke extends Explodable { public function new() { super(); dtsPath = "data/shapes/hazards/nuke/nuke.dts"; this.identifier = "Nuke"; this.isCollideable = true; - nukeParticleData = new ParticleData(); - nukeParticleData.identifier = "nukeParticle"; - nukeParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); + particleData = new ParticleData(); + particleData.identifier = "nukeParticle"; + particleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); - nukeSmokeParticleData = new ParticleData(); - nukeSmokeParticleData.identifier = "nukeSmokeParticle"; - nukeSmokeParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); + smokeParticleData = new ParticleData(); + smokeParticleData.identifier = "nukeSmokeParticle"; + smokeParticleData.texture = ResourceLoader.getResource("data/particles/smoke.png", ResourceLoader.getTexture, this.textureResources); - nukeSparkParticleData = new ParticleData(); - nukeSparkParticleData.identifier = "nukeSparkParticle"; - nukeSparkParticleData.texture = ResourceLoader.getResource("data/particles/spark.png", ResourceLoader.getTexture, this.textureResources); - } + sparkParticleData = new ParticleData(); + sparkParticleData.identifier = "nukeSparkParticle"; + sparkParticleData.texture = ResourceLoader.getResource("data/particles/spark.png", ResourceLoader.getTexture, this.textureResources); - public override function init(level:MarbleWorld, onFinish:Void->Void) { - super.init(level, () -> { - ResourceLoader.load("sound/nukeexplode.wav").entry.load(onFinish); - }); - } + particle = nukeParticle; + smokeParticle = nukeSmokeParticle; + sparksParticle = nukeSparksParticle; - override function onMarbleContact(marble:src.Marble, timeState:TimeState, ?contact:CollisionInfo) { - if (this.isCollideable && !this.level.rewinding) { - // marble.velocity = marble.velocity.add(vec); - this.disappearTime = timeState.timeSinceLoad; - this.setCollisionEnabled(false); - - // if (!this.level.rewinding) - if (@:privateAccess !marble.isNetUpdate) { - AudioManager.playSound(ResourceLoader.getResource("data/sound/nukeexplode.wav", ResourceLoader.getAudio, this.soundResources)); - this.level.particleManager.createEmitter(nukeParticle, nukeParticleData, this.getAbsPos().getPosition()); - this.level.particleManager.createEmitter(nukeSmokeParticle, nukeSmokeParticleData, this.getAbsPos().getPosition()); - this.level.particleManager.createEmitter(nukeSparksParticle, nukeSparkParticleData, this.getAbsPos().getPosition()); - } - - var minePos = this.getAbsPos().getPosition(); - var dtsCenter = this.dts.bounds.center(); - // dtsCenter.x = -dtsCenter.x; - // minePos.x += dtsCenter.x; - // minePos.y += dtsCenter.y; - // minePos.z += dtsCenter.z; - var off = marble.getAbsPos().getPosition().sub(minePos); - - var force = computeExplosionForce(off); - marble.applyImpulse(force, true); - - // for (collider in this.colliders) { - // var hull:CollisionHull = cast collider; - // hull.force = strength; - // } - } - // Normally, we would add a light here, but that's too expensive for THREE, apparently. - - // this.level.replay.recordMarbleContact(this); + renewTime = 15000; + explodeSoundFile = "data/sound/nukeexplode.wav"; } function computeExplosionForce(distVec:Vector) { @@ -169,15 +132,12 @@ class Nuke extends DtsObject { return distVec; } - override function update(timeState:TimeState) { - super.update(timeState); - if (timeState.timeSinceLoad >= this.disappearTime + 15 || timeState.timeSinceLoad < this.disappearTime) { - this.setHide(false); - } else { - this.setHide(true); - } + public function applyImpulse(marble:src.Marble) { + var minePos = this.getAbsPos().getPosition(); + var dtsCenter = this.dts.bounds.center(); + var off = marble.getAbsPos().getPosition().sub(minePos); - var opacity = Util.clamp((timeState.timeSinceLoad - (this.disappearTime + 15)), 0, 1); - this.setOpacity(opacity); + var force = computeExplosionForce(off); + marble.applyImpulse(force, true); } }