From 4a6c06bf3a39bba9bd47df265bc459f8a4b36275 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Wed, 16 Nov 2022 00:42:49 +0530 Subject: [PATCH] partial checkpoint and some macro refactoring --- src/Macros.hx | 48 ++++++++ src/MarbleGame.hx | 2 +- src/MarbleWorld.hx | 176 +++++++++++++++++++++++++++--- src/mis/MisParser.hx | 169 +++------------------------- src/mis/MissionElement.hx | 2 + src/shapes/Checkpoint.hx | 37 +++++++ src/triggers/CheckpointTrigger.hx | 33 ++++++ 7 files changed, 297 insertions(+), 170 deletions(-) create mode 100644 src/Macros.hx create mode 100644 src/shapes/Checkpoint.hx create mode 100644 src/triggers/CheckpointTrigger.hx diff --git a/src/Macros.hx b/src/Macros.hx new file mode 100644 index 00000000..3af34096 --- /dev/null +++ b/src/Macros.hx @@ -0,0 +1,48 @@ +package; + +import haxe.macro.Context; +import haxe.macro.ExprTools; +import haxe.macro.TypeTools; +import haxe.macro.Expr; +import haxe.macro.Expr.ExprOf; +import mis.MissionElement.MissionElementType; + +class MisParserMacros { + public static macro function parseObject(name:ExprOf, className:haxe.macro.Expr, classEnum:ExprOf) { + switch (className.expr) { + case EConst(c): + switch (c) { + case CIdent(s): + var classType = Context.getType(s); + switch (classType) { + case TInst(ctype, cparams): + var ct = ctype.get(); + var tfn:TypePath = { + pack: ct.pack, + name: ct.name + }; + return { + return macro { + var fn = () -> { + var obj = new $tfn(); + obj._type = $classEnum; + obj._name = name; + + copyFields(obj); + + return obj; + }; + element = fn(); + } + }; + case _: + throw 'Unsupported'; + } + case _: + throw 'Unsupported'; + } + case _: + throw 'Unsupported ' + Std.string(className); + } + } +} diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index 881ecd2a..7a65bd1c 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -195,7 +195,7 @@ class MarbleGame { world.setCursorLock(true); }, (sender) -> { canvas.popDialog(exitGameDlg); - world.restart(); + world.restart(true); // world.setCursorLock(true); paused = !paused; }); diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 17173cd6..716f9548 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,7 @@ package src; +import shapes.Checkpoint; +import triggers.CheckpointTrigger; import shapes.EasterEgg; import shapes.Sign; import triggers.TeleportTrigger; @@ -95,6 +97,8 @@ class MarbleWorld extends Scheduler { public var dtsObjects:Array = []; public var forceObjects:Array = []; public var triggers:Array = []; + public var gems:Array = []; + public var namedObjects:Map = []; var shapeImmunity:Array = []; var shapeOrTriggerInside:Array = []; @@ -136,6 +140,13 @@ class MarbleWorld extends Scheduler { public var newOrientationQuat = new Quat(); + // Checkpoint + var currentCheckpoint:{obj:DtsObject, elem:MissionElementBase} = null; + var currentCheckpointTrigger:CheckpointTrigger = null; + var checkpointCollectedGems:Map = []; + var checkpointHeldPowerup:PowerUp = null; + var checkpointUp:Vector = null; + // Replay public var replay:Replay; public var isWatching:Bool = false; @@ -326,18 +337,24 @@ class MarbleWorld extends Scheduler { } public function start() { - restart(); + restart(true); for (interior in this.interiors) interior.onLevelStart(); for (shape in this.dtsObjects) shape.onLevelStart(); } - public function restart() { + public function restart(full:Bool = false) { if (!this.isWatching) { this.replay.clear(); } else this.replay.rewind(); + + if (!full && this.currentCheckpoint != null) { + this.loadCheckpointState(); + return 0; // Load checkpoint + } + this.timeState.currentAttemptTime = 0; this.timeState.gameplayClock = 0; this.bonusTime = 0; @@ -346,6 +363,13 @@ class MarbleWorld extends Scheduler { this.finishTime = null; this.helpTextTimeState = Math.NEGATIVE_INFINITY; this.alertTextTimeState = Math.NEGATIVE_INFINITY; + + this.currentCheckpoint = null; + this.currentCheckpointTrigger = null; + this.checkpointCollectedGems.clear(); + this.checkpointHeldPowerup = null; + this.checkpointUp = null; + if (this.endPad != null) this.endPad.inFinish = false; if (this.totalGems > 0) { @@ -590,6 +614,7 @@ class MarbleWorld extends Scheduler { else if (StringTools.startsWith(dataBlockLowerCase, "gemitem")) { shape = new Gem(cast element); this.totalGems++; + this.gems.push(cast shape); } else if (dataBlockLowerCase == "superjumpitem") shape = new SuperJump(cast element); else if (StringTools.startsWith(dataBlockLowerCase, "signcaution")) @@ -604,6 +629,8 @@ class MarbleWorld extends Scheduler { shape = new Helicopter(cast element); else if (dataBlockLowerCase == "easteregg") shape = new EasterEgg(cast element); + else if (dataBlockLowerCase == "checkpoint") + shape = new Checkpoint(cast element); else if (dataBlockLowerCase == "ductfan") shape = new DuctFan(); else if (dataBlockLowerCase == "smallductfan") @@ -635,6 +662,13 @@ class MarbleWorld extends Scheduler { return; } + if (element._name != null && element._name != "") { + this.namedObjects.set(element._name, { + obj: shape, + elem: element + }); + } + var shapePosition = MisParser.parseVector3(element.position); shapePosition.x = -shapePosition.x; var shapeRotation = MisParser.parseRotation(element.rotation); @@ -678,6 +712,7 @@ class MarbleWorld extends Scheduler { else if (StringTools.startsWith(dataBlockLowerCase, "gemitem")) { shape = new Gem(cast element); this.totalGems++; + this.gems.push(cast shape); } else if (dataBlockLowerCase == "superjumpitem") shape = new SuperJump(cast element); else if (dataBlockLowerCase == "superbounceitem") @@ -690,6 +725,8 @@ class MarbleWorld extends Scheduler { shape = new Helicopter(cast element); else if (dataBlockLowerCase == "easteregg") shape = new EasterEgg(cast element); + else if (dataBlockLowerCase == "checkpoint") + shape = new Checkpoint(cast element); else if (dataBlockLowerCase == "ductfan") shape = new DuctFan(); else if (dataBlockLowerCase == "smallductfan") @@ -721,6 +758,13 @@ class MarbleWorld extends Scheduler { return; } + if (element._name != null && element._name != "") { + this.namedObjects.set(element._name, { + obj: shape, + elem: element + }); + } + var shapePosition = MisParser.parseVector3(element.position); shapePosition.x = -shapePosition.x; var shapeRotation = MisParser.parseRotation(element.rotation); @@ -749,21 +793,26 @@ class MarbleWorld extends Scheduler { public function addTrigger(element:MissionElementTrigger, onFinish:Void->Void) { var trigger:Trigger = null; + var datablockLowercase = element.datablock.toLowerCase(); + // Create a trigger based on type - if (element.datablock == "OutOfBoundsTrigger") { + if (datablockLowercase == "outofboundstrigger") { trigger = new OutOfBoundsTrigger(element, cast this); - } else if (element.datablock == "InBoundsTrigger") { + } else if (datablockLowercase == "inboundstrigger") { trigger = new InBoundsTrigger(element, cast this); - } else if (element.datablock == "HelpTrigger") { + } else if (datablockLowercase == "helptrigger") { trigger = new HelpTrigger(element, cast this); - } else if (element.datablock == "TeleportTrigger") { + } else if (datablockLowercase == "teleporttrigger") { trigger = new TeleportTrigger(element, cast this); - } else if (element.datablock == "DestinationTrigger") { + } else if (datablockLowercase == "destinationtrigger") { trigger = new DestinationTrigger(element, cast this); + } else if (datablockLowercase == "checkpointtrigger") { + trigger = new CheckpointTrigger(element, cast this); } else { onFinish(); return; } + trigger.init(() -> { this.triggers.push(trigger); this.collisionWorld.addEntity(trigger.collider); @@ -792,6 +841,13 @@ class MarbleWorld extends Scheduler { tsShape.identifier = shapeName; tsShape.isCollideable = true; + if (element._name != null && element._name != "") { + this.namedObjects.set(element._name, { + obj: tsShape, + elem: element + }); + } + var shapePosition = MisParser.parseVector3(element.position); shapePosition.x = -shapePosition.x; var shapeRotation = MisParser.parseRotation(element.rotation); @@ -946,16 +1002,10 @@ class MarbleWorld extends Scheduler { } if (this.outOfBounds && this.finishTime == null && Key.isDown(Settings.controlsSettings.powerup)) { - this.clearSchedule(); this.restart(); return; } - if (Key.isDown(Key.H)) { - this.isWatching = true; - this.restart(); - } - this.updateTexts(); } @@ -1268,7 +1318,7 @@ class MarbleWorld extends Scheduler { }, (sender) -> { MarbleGame.canvas.popDialog(egg); this.setCursorLock(true); - this.restart(); + this.restart(true); #if js pointercontainer.hidden = true; #end @@ -1283,6 +1333,8 @@ class MarbleWorld extends Scheduler { } public function pickUpPowerUp(powerUp:PowerUp) { + if (powerUp == null) + return false; if (this.marble.heldPowerup != null) if (this.marble.heldPowerup.identifier == powerUp.identifier) return false; @@ -1306,7 +1358,7 @@ class MarbleWorld extends Scheduler { return q; } - public function setUp(vec:Vector, timeState:TimeState) { + public function setUp(vec:Vector, timeState:TimeState, instant:Bool = false) { this.currentUp = vec; var currentQuat = this.getOrientationQuat(timeState.currentAttemptTime); var oldUp = new Vector(0, 0, 1); @@ -1349,7 +1401,7 @@ class MarbleWorld extends Scheduler { this.newOrientationQuat = quatChange; this.oldOrientationQuat = currentQuat; - this.orientationChangeTime = timeState.currentAttemptTime; + this.orientationChangeTime = instant ? -1e8 : timeState.currentAttemptTime; } public function goOutOfBounds() { @@ -1367,6 +1419,98 @@ class MarbleWorld extends Scheduler { this.schedule(this.timeState.currentAttemptTime + 2, () -> this.restart()); } + /** Sets a new active checkpoint. */ + public function saveCheckpointState(shape:{obj:DtsObject, elem:MissionElementBase}, trigger:CheckpointTrigger = null) { + if (this.currentCheckpoint != null) + if (this.currentCheckpoint.obj == shape.obj) + return; + var disableOob = false; + if (shape != null) { + if (shape.elem.fields.exists('disableOob')) { + disableOob = MisParser.parseBoolean(shape.elem.fields.get('disableOob')); + } + } + if (trigger != null) { + disableOob = trigger.disableOOB; + } + // (shape.srcElement as any) ?.disableOob || trigger?.element.disableOob; + if (disableOob && this.outOfBounds) + return; // The checkpoint is configured to not work when the player is already OOB + this.currentCheckpoint = shape; + this.currentCheckpointTrigger = trigger; + this.checkpointCollectedGems.clear(); + this.checkpointUp = this.currentUp.clone(); + // Remember all gems that were collected up to this point + for (gem in this.gems) { + if (gem.pickedUp) + this.checkpointCollectedGems.set(gem, true); + } + this.checkpointHeldPowerup = this.marble.heldPowerup; + this.displayAlert("Checkpoint reached!"); + AudioManager.playSound(ResourceLoader.getResource('data/sound/checkpoint.wav', ResourceLoader.getAudio, this.soundResources)); + } + + /** Resets to the last stored checkpoint state. */ + public function loadCheckpointState() { + var marble = this.marble; + // Determine where to spawn the marble + var offset = new Vector(0, 0, 3); + var add = ""; // (this.currentCheckpoint.srcElement as any)?.add || this.currentCheckpointTrigger?.element.add; + if (this.currentCheckpoint.elem.fields.exists('add')) { + add = this.currentCheckpoint.elem.fields.get('add'); + } + if (this.currentCheckpointTrigger != null) { + offset = this.currentCheckpointTrigger.add; + } + if (add != "") + offset = MisParser.parseVector3(add); + var mpos = this.currentCheckpoint.obj.getAbsPos().getPosition().add(offset); + this.marble.setPosition(mpos.x, mpos.y, mpos.z); + marble.velocity.load(new Vector(0, 0, 0)); + marble.omega.load(new Vector(0, 0, 0)); + // Set camera orienation + var euler = this.currentCheckpoint.obj.getRotationQuat().toEuler(); + this.marble.camera.CameraYaw = euler.z + Math.PI / 2; + this.marble.camera.CameraPitch = 0.45; + this.marble.camera.nextCameraYaw = this.marble.camera.CameraYaw; + this.marble.camera.nextCameraPitch = this.marble.camera.CameraPitch; + this.marble.camera.oob = false; + var gravityField = ""; // (this.currentCheckpoint.srcElement as any) ?.gravity || this.currentCheckpointTrigger?.element.gravity; + if (this.currentCheckpoint.elem.fields.exists('gravity')) { + gravityField = this.currentCheckpoint.elem.fields.get('gravity'); + } + if (this.currentCheckpointTrigger != null) { + if (@:privateAccess this.currentCheckpointTrigger.element.fields.exists('gravity')) { + gravityField = @:privateAccess this.currentCheckpointTrigger.element.fields.get('gravity'); + } + } + if (MisParser.parseBoolean(gravityField)) { + // In this case, we set the gravity to the relative "up" vector of the checkpoint shape. + var up = new Vector(0, 0, 1); + up.transform(this.currentCheckpoint.obj.getRotationQuat().toMatrix()); + this.setUp(up, this.timeState, true); + } else { + // Otherwise, we restore gravity to what was stored. + this.setUp(this.checkpointUp, this.timeState, true); + } + // Restore gem states + for (gem in this.gems) { + if (gem.pickedUp && !this.checkpointCollectedGems.exists(gem)) { + gem.reset(); + this.gemCount--; + } + } + this.playGui.formatGemCounter(this.gemCount, this.totalGems); + this.playGui.setCenterText('none'); + this.clearSchedule(); + this.outOfBounds = false; + this.deselectPowerUp(); // Always deselect first + // Wait a bit to select the powerup to prevent immediately using it incase the user skipped the OOB screen by clicking + if (this.marble.heldPowerup != null) + this.schedule(this.timeState.currentAttemptTime + 500, () -> this.pickUpPowerUp(this.marble.heldPowerup)); + AudioManager.playSound(ResourceLoader.getResource('data/sound/spawn.wav', ResourceLoader.getAudio, this.soundResources)); + } + public function setCursorLock(enabled:Bool) { this.cursorLock = enabled; if (enabled) { diff --git a/src/mis/MisParser.hx b/src/mis/MisParser.hx index ff501b99..c719e585 100644 --- a/src/mis/MisParser.hx +++ b/src/mis/MisParser.hx @@ -1,5 +1,6 @@ package mis; +import Macros.MisParserMacros; import haxe.Exception; import mis.MissionElement.MissionElementPathedInterior; import mis.MissionElement.MissionElementPath; @@ -144,35 +145,35 @@ class MisParser { case "SimGroup": element = this.readSimGroup(name); case "ScriptObject": - element = this.readScriptObject(name); + MisParserMacros.parseObject(name, MissionElementScriptObject, MissionElementType.ScriptObject); case "MissionArea": - element = this.readMissionArea(name); + MisParserMacros.parseObject(name, MissionElementMissionArea, MissionElementType.MissionArea); case "Sky": - element = this.readSky(name); + MisParserMacros.parseObject(name, MissionElementSky, MissionElementType.Sky); case "Sun": - element = this.readSun(name); + MisParserMacros.parseObject(name, MissionElementSun, MissionElementType.Sun); case "InteriorInstance": - element = this.readInteriorInstance(name); + MisParserMacros.parseObject(name, MissionElementInteriorInstance, MissionElementType.InteriorInstance); case "StaticShape": - element = this.readStaticShape(name); + MisParserMacros.parseObject(name, MissionElementStaticShape, MissionElementType.StaticShape); case "Item": - element = this.readItem(name); + MisParserMacros.parseObject(name, MissionElementItem, MissionElementType.Item); case "Path": element = this.readPath(name); case "Marker": - element = this.readMarker(name); + MisParserMacros.parseObject(name, MissionElementMarker, MissionElementType.Marker); case "PathedInterior": - element = this.readPathedInterior(name); + MisParserMacros.parseObject(name, MissionElementPathedInterior, MissionElementType.PathedInterior); case "Trigger": - element = this.readTrigger(name); + MisParserMacros.parseObject(name, MissionElementTrigger, MissionElementType.Trigger); case "AudioProfile": - element = this.readAudioProfile(name); + MisParserMacros.parseObject(name, MissionElementAudioProfile, MissionElementType.AudioProfile); case "MessageVector": - element = this.readMessageVector(name); + MisParserMacros.parseObject(name, MissionElementMessageVector, MissionElementType.MessageVector); case "TSStatic": - element = this.readTSStatic(name); + MisParserMacros.parseObject(name, MissionElementTSStatic, MissionElementType.TSStatic); case "ParticleEmitterNode": - element = this.readParticleEmitterNode(name); + MisParserMacros.parseObject(name, MissionElementParticleEmitterNode, MissionElementType.ParticleEmitterNode); default: trace("Unknown element type! " + type); // Still advance the index @@ -274,76 +275,8 @@ class MisParser { Reflect.setField(obj, key, value[0]); } } - } - function readScriptObject(name:String) { - var obj = new MissionElementScriptObject(); - obj._type = MissionElementType.ScriptObject; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readMissionArea(name:String) { - var obj = new MissionElementMissionArea(); - obj._type = MissionElementType.MissionArea; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readSky(name:String) { - var obj = new MissionElementSky(); - obj._type = MissionElementType.Sky; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readSun(name:String) { - var obj = new MissionElementSun(); - obj._type = MissionElementType.Sun; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readInteriorInstance(name:String) { - var obj = new MissionElementInteriorInstance(); - obj._type = MissionElementType.InteriorInstance; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readStaticShape(name:String) { - var obj = new MissionElementStaticShape(); - obj._type = MissionElementType.StaticShape; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readItem(name:String) { - var obj = new MissionElementItem(); - obj._type = MissionElementType.Item; - obj._name = name; - - copyFields(obj); - - return obj; + Reflect.setField(obj, "fields", values); } function readPath(name:String) { @@ -357,76 +290,6 @@ class MisParser { return obj; } - function readMarker(name:String) { - var obj = new MissionElementMarker(); - obj._type = MissionElementType.Marker; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readPathedInterior(name:String) { - var obj = new MissionElementPathedInterior(); - obj._type = MissionElementType.PathedInterior; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readTrigger(name:String) { - var obj = new MissionElementTrigger(); - obj._type = MissionElementType.Trigger; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readAudioProfile(name:String) { - var obj = new MissionElementAudioProfile(); - obj._type = MissionElementType.AudioProfile; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readMessageVector(name:String) { - var obj = new MissionElementMessageVector(); - obj._type = MissionElementType.MessageVector; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readTSStatic(name:String) { - var obj = new MissionElementTSStatic(); - obj._type = MissionElementType.TSStatic; - obj._name = name; - - copyFields(obj); - - return obj; - } - - function readParticleEmitterNode(name:String) { - var obj = new MissionElementParticleEmitterNode(); - obj._type = MissionElementType.ParticleEmitterNode; - obj._name = name; - - copyFields(obj); - - return obj; - } - /** Resolves a TorqueScript rvalue expression. Currently only supports the concatenation @ operator. */ function resolveExpression(expr:String) { var parts = Util.splitIgnoreStringLiterals(expr, ' @ ').map(x -> { diff --git a/src/mis/MissionElement.hx b/src/mis/MissionElement.hx index 31165b2b..a2fae18c 100644 --- a/src/mis/MissionElement.hx +++ b/src/mis/MissionElement.hx @@ -30,6 +30,8 @@ class MissionElementBase { /** Is unique for every element in the mission file. */ var _id:Int; + + var fields:Map; } @:publicFields diff --git a/src/shapes/Checkpoint.hx b/src/shapes/Checkpoint.hx new file mode 100644 index 00000000..3d7d0b7a --- /dev/null +++ b/src/shapes/Checkpoint.hx @@ -0,0 +1,37 @@ +package shapes; + +import collision.CollisionInfo; +import mis.MisParser; +import src.DtsObject; +import src.ResourceLoader; +import mis.MissionElement.MissionElementStaticShape; + +class Checkpoint extends DtsObject { + public var disableOOB = false; + + var element:MissionElementStaticShape; + + public function new(element:MissionElementStaticShape) { + super(); + this.dtsPath = "data/shapes/buttons/checkpoint.dts"; + this.isCollideable = true; + this.isTSStatic = false; + this.identifier = "Checkpoint"; + this.element = element; + + this.disableOOB = element.fields.exists('disableOob') ? MisParser.parseBoolean(element.fields['disableOob']) : false; + } + + public override function init(level:src.MarbleWorld, onFinish:() -> Void) { + super.init(level, () -> { + ResourceLoader.load("sound/checkpoint.wav").entry.load(onFinish); + }); + } + + public override function onMarbleContact(time:src.TimeState, ?contact:CollisionInfo) { + this.level.saveCheckpointState({ + obj: this, + elem: this.element + }, null); + } +} diff --git a/src/triggers/CheckpointTrigger.hx b/src/triggers/CheckpointTrigger.hx new file mode 100644 index 00000000..cecb26d4 --- /dev/null +++ b/src/triggers/CheckpointTrigger.hx @@ -0,0 +1,33 @@ +package triggers; + +import h3d.Vector; +import src.MarbleWorld; +import mis.MissionElement.MissionElementTrigger; +import src.ResourceLoader; +import mis.MisParser; + +class CheckpointTrigger extends Trigger { + public var disableOOB = false; + public var add:Vector = null; + + override public function new(element:MissionElementTrigger, level:MarbleWorld) { + super(element, level); + + this.disableOOB = element.fields.exists('disableOob') ? MisParser.parseBoolean(element.fields['disableOob']) : false; + this.add = element.fields.exists('add') ? MisParser.parseVector3(element.fields['add']) : null; + } + + public override function init(onFinish:() -> Void) { + super.init(() -> { + ResourceLoader.load("sound/checkpoint.wav").entry.load(onFinish); + }); + } + + public override function onMarbleEnter(time:src.TimeState) { + super.onMarbleEnter(time); + var shape = this.level.namedObjects.get(this.element.respawnpoint); + if (shape == null) + return; + this.level.saveCheckpointState(shape, this); + } +}