partial checkpoint and some macro refactoring

This commit is contained in:
RandomityGuy 2022-11-16 00:42:49 +05:30
parent b7e94aa027
commit 4a6c06bf3a
7 changed files with 297 additions and 170 deletions

48
src/Macros.hx Normal file
View file

@ -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<String>, className:haxe.macro.Expr, classEnum:ExprOf<MissionElementType>) {
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);
}
}
}

View file

@ -195,7 +195,7 @@ class MarbleGame {
world.setCursorLock(true);
}, (sender) -> {
canvas.popDialog(exitGameDlg);
world.restart();
world.restart(true);
// world.setCursorLock(true);
paused = !paused;
});

View file

@ -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<DtsObject> = [];
public var forceObjects:Array<ForceObject> = [];
public var triggers:Array<Trigger> = [];
public var gems:Array<Gem> = [];
public var namedObjects:Map<String, {obj:DtsObject, elem:MissionElementBase}> = [];
var shapeImmunity:Array<DtsObject> = [];
var shapeOrTriggerInside:Array<GameObject> = [];
@ -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<Gem, Bool> = [];
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) {

View file

@ -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 -> {

View file

@ -30,6 +30,8 @@ class MissionElementBase {
/** Is unique for every element in the mission file. */
var _id:Int;
var fields:Map<String, String>;
}
@:publicFields

37
src/shapes/Checkpoint.hx Normal file
View file

@ -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);
}
}

View file

@ -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);
}
}