Meanwhile I've written a routine into my game script that constructs a 9-digit simple save ID with each number representing a different parameter. I used a much more basic version of this with 2 digits when I made Ronja all those years ago. Anyway I can use this to manually reconfigure the level after loading it so it works a little more like a full game save state. At the moment it allows me to know which map, which music, which checkpoint to move the player to, what the player's health was, whether the player is carrying a rock, the player's model animation settings (he runs differently depending on what's going on), and whether bots are alerted to the player's presence or not.
It's a bit much to try incorporate stuff like enemy locations / health / if they're dead / what they were in the middle of, but I'm sure I'll come up with ways to squeeze more data in there

EDIT - I rounded this bit out. So yes I've made a simple save ID that clumps together basic data so loading a map after quitting the engine can put the player back where he was and with the same vitals / equipment, but all that remains of the bots' previous state is their collective awareness of the player...they sadly get sent back to their starting spots (because simple save IDs can't pack enough data into them to include the x/y/z/angY details of all the bots. What I HAVE therefore been able to do is, while you're still playing i.e. you die and respawn but you don't quit and reopen the engine, the game remembers where to put all the bots and restores them fairly faithfully to their states as of the time you hit the checkpoint.
It involves using For loops in the game script that call bots sequentially to gather important vitals/stats, and stores them in arrays inside the game script. I'm then able to run more For loops to manually return all those values to every bot when the player calls the game telling it to restore the bots' old state. Effectively, it's an auto-save that returns things essentially back to the way they were after the player hit the checkpoint but before the player died.
And it's fast! No visible lag whatsoever - it just seamlessly puts things back the way they were. Here's a messy look at how it works:
// SAVE ROUTINE - in player script
// Simple save ID will effectively form an array-like string - use JS string methods to read it. The positions will mean the following:
// 0 - Map // 0 is Yard, 1 is Road, 2 is Sewers
// 1 - Music // 0 is Prison, 1 is Prison_beat
// 2 - Checkpoint ID // used to allow searching for the checkpoint, which allows pinpointing its map position so the player may be placed there
// 3-5 - Health // occupies 3 places as the player default health is 200
// 6 - Holding rock or not // 0 - false, 1 - true
// 7 - Player move animation // 0 - Trudge, 1 - Limp, 2 - Run, 3 - Sprint
// 8 - Global bot awareness // 0 - Low, 1 - Medium, 2 - High
// When respawning without quitting, individual bot data can be restored. If loading a simple save, only the above parameters will be applied meaning
// only general data can be used to set bots up. This means their location and movement data won't restore, and they will all have the same awareness
function constructSaveString(game,x,y,z) // note the checkpoint includes its x/y/z position when calling the game script
{
pos0=data.get('map name'); // this is set when the map loads
pos0=pos0.toString(); // convert to a string - this will mean all other values added at end will be strings also
pos1=data.get('current music'); // this is set when the music starts
pos2=data.get('checkpoint'); // this is set when the checkpoint object spawns into the map
playerhealth=map.object.getHealth(0);
data.add('health',playerhealth);
playerhealth=playerhealth.toString(); // convert to string otherwise the JS slice method cannot be used
if (playerhealth>99) { // 3 digits
pos3=playerhealth.slice(0,1);
pos4=playerhealth.slice(1,2);
pos5=playerhealth.slice(2);
}
else if (playerhealth>9) { // 2 digits
pos3=0;
pos4=playerhealth.slice(0,1);
pos5=playerhealth.slice(1);
}
else { // 1 digit
pos3=0;
pos4=0;
pos5=playerhealth;
}
if (data.get('holding rock')==null) pos6=0; // data container won't exist if a rock was never picked up
else pos6=data.get('holding rock');
pos7=data.get('move animation'); // set whenever player walk animation changes
pos8=data.get('alert level'); // set globally by the last bot to change its alert level, default is 0
simpleSaveString=pos0+pos1+pos2+pos3+pos4+pos5+pos6+pos7+pos8;
map.action.setSimpleSave(simpleSaveString,map.info.title);
data.add('respawn x',x);
data.add('respawn y',y);
data.add('respawn z',z);
respawnAngle=map.object.getAngle(0);
data.add('respawn angle',respawnAngle.y.toFixed(0)); // Y angle with no decimals rounded up
for (i=0; i<botRoll.length; i++) { // runs bot data capture for each ID stored on the roll array
botX.push(game.event.callObjectById(botRoll[i],'captureX',null));
botY.push(game.event.callObjectById(botRoll[i],'captureY',null));
botZ.push(game.event.callObjectById(botRoll[i],'captureZ',null));
botYangle.push(game.event.callObjectById(botRoll[i],'captureYang',null));
botAlert.push(game.event.callObjectById(botRoll[i],'captureAwareness',null));
botPath.push(game.event.callObjectById(botRoll[i],'capturePath',null));
botMove.push(game.event.callObjectById(botRoll[i],'captureMovement',null));
botHealth.push(game.event.callObjectById(botRoll[i],'captureHealth',null));
}
iface.console.write('auto-save complete - '+simpleSaveString);
}
All those calls to the bots look like this:
// This is in each bot script
function captureX(obj)
{
return(obj.position.x);
}
function captureY(obj)
{
return(obj.position.y);
}
function captureZ(obj)
{
return(obj.position.z);
}
function captureYang(obj)
{
return(obj.angle.y.toFixed(0)); // Y angle rounded up with no decimals
}
function captureAwareness(obj)
{
return(awareness);
}
function capturePath(obj)
{
return(pathID);
}
function captureMovement(obj)
{
return(moving);
}
function captureHealth(obj)
{
return(obj.health.current);
}
From that, when the player dies it calls:
// this is in the player script - attached to the Die event
function die(obj)
{
hideHUD(obj);
freezePlayer(obj);
sound.stopMusic();
sound.playGlobalPlayer("Game Over",1);
obj.motionVector.stop();
obj.motionAngle.turnStop();
dying=true;
obj.model.animation.start('Die');
obj.event.chain(1,'playerFallOver');
}
function playerFallOver(obj)
{
obj.event.callGame('playerHasDied',null);
headTurn=new Angle(90,90,0);
camera.angle.turn(headTurn,2000);
obj.status.tintView(black,1,2000,3500,500);
obj.event.chain(25,'checkForSaveData');
}
function checkForSaveData(obj)
{
obj.setting.hidden=true;
if (data.get('health')==null) obj.event.chain(10,'resetCamera');
else obj.event.chain(10,'resumeFromCheckpoint');
}
function resumeFromCheckpoint(obj)
{
obj.event.callGame('restoreBotState',null);
camera.angle.x=0;
camera.angle.y=0;
camera.angle.z=0;
obj.event.chain(20,'raiseTheCurtain');
}
function raiseTheCurtain(obj)
{
posX=data.get('respawn x');
posY=data.get('respawn y');
posZ=data.get('respawn z');
spot=new Point(posX,posY,posZ);
ang=data.get('respawn angle');
obj.position.place(spot,ang);
obj.health.add(data.get('health'));
obj.model.animation.start('Breathing');
obj.setting.hidden=false;
if (obj.health.current<50) walkSlower(obj);
else if (obj.health.current<100) walkNormal(obj);
else if (obj.health.current<150) runSlower(obj);
else runNormal(obj);
iface.bitmap.hideAll();
dying=false;
unfreezePlayer(obj);
musicID=data.get('current music');
switch (musicID) {
case 0:
sound.startMusic('Prison');
return;
case 1:
sound.startMusic('Prison_beat');
return;
}
if (checkpoint1) checkpoint1=false;
}
You'll see in that second bit, the player calls the game after dying:
// this is in the game script
function playerHasDied(game)
{
for (i=0; i<botRoll.length; i++) game.event.callObjectById(botRoll[i],'stopAllActivity',null);
}
That cycles through every bot telling them to stop what they're doing. After they're all done, the player calls the game asking it to do this:
// this is also in the game script
function restoreBotState(game)
{
for (i=0; i<botRoll.length; i++) {
if (botHealth[i]<1) iface.console.write('skipping restore of bot '+botRoll[i]+' because he is dead');
else game.event.callObjectById(botRoll[i],'restoreState',botX[i],botY[i],botZ[i],botYangle[i],botAlert[i],botPath[i],botMove[i]);
}
iface.console.write('all bots restored');
}
Which checks the health the bot was saved with. If it has no health, it won't bother restoring its old state because it's dead so hasn't changed state. If it WASN'T dead at the time of hitting the checkpoint, it reconstructs all of the saved data and packages it up into a call to each bot script sequentially:
// this is in each bot script
function restoreState(obj,x,y,z,angY,prevAwareness,prevPathID,prevMoving)
{
obj.health.reset();
obj.setting.contact=true;
spot=new Point(x,y,z);
obj.position.place(spot,angY);
switch (prevAwareness) {
case 0:
lowAlert(obj);
break;
case 1:
mediumAlert(obj);
break;
case 2:
highAlert(obj);
break;
}
obj.watch.start(listeningDistance);
pathID=prevPathID; // revert to previous path iD
if (flashlight) useFlashlight(obj,false);
if (prevMoving) resumeInterruptedPath(obj); // if moving, resume previous path
else if (pathID==null) spawnOntoMap(obj);
else whereToNext(obj); // if not moving, decide on a new path based on previous awareness
}
Moral of the story? There's always a way, even for coding noobs like me