<file_sep>console(-1 >>> 1)

if( file("hiragana.res", 0) != 1 ){
    console("Could not find resources!");
    exit();
}

const list = [
    "a","e","i","o","u","n",
    "chi","fu","ha","he","hi","ho",
    "ka","ke","ki","ko","ku","ma",
    "me","mi","mo","mu","na","ne",
    "ni","no","nu","ra","re","ri",
    "ro","ru","sa","se","shi","so",
    "su","ta","te","to","tsu","wa",
    "we","wo","ya","yo","yu","ba",
    "be","bi","bo","bu","da","de",
    "do","ga","ge","gi","go","gu",
    "ja","ji","jo","ju","pa","pe",
    "pi","po","pu","za","ze","zo",
    "zu","bya","byo","byu","cha",
    "cho","chu","gya","gyo","gyu",
    "hya","hyo","hyu","kya","kyo",
    "kyu","mya","myo","myu","nya",
    "nyo","nyu","pya","pyo","pyu",
    "rya","ryo","ryu","sha","sho",
    "shu"
];
const UI = file("interface", 0);
const heart = builtin("sHeart");
const rightEmote = builtin("happyEmote");
const wrongEmote = builtin("dohEmote");
const boredEmote = builtin("boredEmote");
const words = [0,0,0,0];
var X;
var Y;
var stateTime = 0;
var level = 0;
var streak = 0;
var score = 0;
var maxStreak = 0;
var num = 0;
var img = null;
var state = nop;
var nextState = null;
var emote = rightEmote;
var lives = 3;
const spriteX = 80;
const spriteY = 16;
const waitColor = 23;
const rightColor = 7;
const wrongColor = 175;
var bgColor = 17;
var uiColor = 18;
var spriteColor = bgColor;
var targetColor = waitColor;

fill(bgColor);
color(10);
pick();

function nop(){}

function pick(){
    if((lives < 0) || !UI)
        exit();
    var newnum = random(0, level);
    if(newnum == num) newnum = random(0, level);
    num = newnum;
    img = file(list[num], img);
    for(var i in words){
        words[i] = random(0, level);
    }
    words[random(0, 4)] = num;
    targetColor = waitColor;
    state = tween;
    nextState = waitForInput;
    stateTime = 0;
}

function wrongFeedback(){
    if(stateTime == 0) stateTime = 60;
    else if(--stateTime == 0){
        targetColor = bgColor;
        nextState = pick;
        state = tween;
    }
    color(172);
    if(words[0] == num){
        cursor(25 + 2, 25 + 4);
        print(list[words[0]]);
    }
    if(words[1] == num){
        cursor(25 + 2, 73 + 4);
        print(list[words[1]]);
    }
    if(words[2] == num){
        cursor(5 + 2, 49 + 4);
        print(list[words[2]]);
    }
    if(words[3] == num){
        cursor(44 + 2, 49 + 4);
        print(list[words[3]]);
    }
}

function makeChoice(choice){
    if((choice != 4) && (words[choice] == num)){
        if(++level > length(list)) level = length(list) - 1;
        if(++streak > maxStreak){
            emote = rightEmote;
        }
        score += streak + 1;
        highscore(score);
        nextState = pick;
        state = tween;
        targetColor = bgColor;
        io("OVERDRIVE", 0);
        io("VOLUME", 128);
        io("DURATION", 100);
        sound(44);
    }else{
        io("OVERDRIVE", 1);
        io("VOLUME", 255);
        io("DURATION", 750);
        sound(20);
        --lives;
        level -= 4;
        if(level < 0) level = 0;
        streak = 0;
        targetColor = wrongColor;
        nextState = wrongFeedback;
        state = tween;
        stateTime = 0;
        emote = boredEmote;
    }
}

function waitForInput(){
    var choice = -1;
    if(stateTime == 0) stateTime = 60;
    else if(--stateTime == 0) choice = 4;
    if(justPressed("UP")){
        choice = 0;
    } else if(justPressed("DOWN")){
        choice = 1;
    } else if(justPressed("LEFT")){
        choice = 2;
    } else if(justPressed("RIGHT")){
        choice = 3;
    }
    if(justPressed("B")){
        uiColor = 12;
        bgColor = 10;
        level = length(list);
    }
    if(pressed("C"))
        exit();
    if(choice > -1){
        makeChoice(choice);
    } else {
        color(22);
        cursor(25 + 2, 25 + 4);
        print(list[words[0]]);
        cursor(25 + 2, 73 + 4);
        print(list[words[1]]);
        cursor(5 + 2, 49 + 4);
        print(list[words[2]]);
        cursor(44 + 2, 49 + 4);
        print(list[words[3]]);
    }
}

function tween(){
    var block = spriteColor >> 3;
    var lum = spriteColor & 7;
    var tblock = targetColor >> 3;
    var tlum = targetColor & 7;
    if(block != tblock){
        if(lum > 0) --spriteColor;
        else spriteColor = tblock << 3;
    } else {
        if(lum < tlum) ++spriteColor;
        else if(lum == tlum) state = nextState;
        else --spriteColor;
    }
}

function update(){
    color(uiColor - 7);
    sprite(0, 0, UI);

    if(emote == wrongEmote)
        color(91);
    else
        color(0);
    sprite(96, 146, emote);

    cursor(116, 148);
    color(22);
    printNumber(score);

    state();

    color(spriteColor - 7);
    sprite(spriteX, spriteY, img);

    color(0);
    for(var i = 0; i<lives; ++i){
        sprite(84 + (i * 8), 20, heart);
    }
}
<file_sep>
const abilities = file("save/abilities", 0);
const choices = [4];
const prefixes = [" \n  ", " \n->"];
var roll;
var abilityPoints;
var choice = 0;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

function f_goto(page, caption){
    splash(0, 0, "splash.565");
    window(0, 166, 220, 176);
    io("CLEARTEXT");
    cursor(1, 21);
    print("Rolled ");
    print(roll);
    print("+");
    print(abilityPoints);
    print(". ");
    print(caption);
    exec(page);
}

function success(ability){
    f_goto("Book1/562.js", "Success!");return;
}

function fail(){
    f_goto("Book1/438.js", "Failure!");return;
}

function update(){
  cursor(0,0);
  print("Make a SCOUTING roll at a difficulty of 12");

  print(prefixes[choice == 0]); print(" scouting: "); print(abilities[4]);

  cursor(15, 10);
  print("     ");
  cursor(15, 10);
  roll = random(2, 13);
  print(roll);

  choice = max(0, min(0, choice + justPressed("DOWN") - justPressed("UP")));

  if(!justPressed("A"))
    return;

  var ability = choices[ choice ];
  abilityPoints = abilities[ ability ];

  if((roll + abilityPoints) >= 12){
    success(ability);
  } else {
    fail();
  }
}
<file_sep>save("save/bookmark", "Book1/546.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  With a heroic effort, you manage
to get to the woman. Gathering her
up, you run for the door, and
stagger out into the cool night
air, smoke and flame licking at
your heels. The townsfolk give you
a rousing cheer and the girl
thanks you effusively for saving
her mother.

  Later, it transpires that the
woman is actually a powerful
sorceress whose experiments in
fire magic went slightly wrong.
Her name is <NAME>
and she gives you a gift - a pale
moonstone. Note _ The War-Torn
Kingdom _ , * 650 *`,
GOTO,
"Book1/650.js",
`go to 650`,
`on`,
MARK,
`your Adventure Sheet.

  Elissia tells that you can use
the moonstone by rubbing it, and
you will be instantly teleported
to the vicinity of the sunstone -
which she carries on a necklace.

  'I will be here in Marlock City.
Whenever you are desperate, at the
end of your tether, or just want
to get here fast, use the
moonstone and you will appear
beside me. I will do everything in
my power to aid you, and then my
debt to you will be repaid.'

  Whenever you want to use the
moonstone, turn to * 650 * in this
book. Now`,
GOTO,
"Book1/100.js",
`go to 100`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>save("save/bookmark", "Book1/454.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  If there is a tick in the box,`,
GOTO,
"Book1/188.js",
`go
to 188`,
`. If not, and read on.

  You are thrown into a stinking
prison cell. Your cell-mate, a
half-dead old man with long white
hair tells you his tale: 'The lair
of the gorlock! A hideous beast
that has backward-pointing feet -
Krine and I, fleeing with our
stolen treasure sought refuge in
the cave near Blessed Springs. The
tracks led us to believe it had
just left its cave, but we didn't
know about its feet - in fact it
was at home. It took Krine but I
managed to escape with my life,
only to be taken by the militia. I`,
MARK,
`will never live to see the
treasure but you, forewarned, may
defeat the gorlock and take the
riches it guards...'

  Gain the codeword Apache .

  A few days later, you learn your
own fate. You are to be sold into
slavery, and taken to <NAME>
to work in the tin mines.`,
GOTO,
"Book1/118.js",
`go to
118`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>if ( file("images.res", 0) != 1 ){
    console("Could not find resources!");
    exit();
}

var playerY1 = 150;
var score1 = 0;

var playerY2 = 150;
var score2 = 0;

var Chicken1Anim = 0;

// CHANGED
const CAR_IDX = 8;
const PLAYER1_IDX = 1;
const PLAYER2_IDX = 2;

const vCars = 4;//each car has 4 varialbes
const nCars = 12;
const cCars = vCars * nCars;// we have 12 cars and each car has 4 variables each (x, y, speed, type, show)
var tCars = Array( cCars );
//var carLength = [32, 32, 64];

//initialize cars

for(var carid = 0; carid < nCars; carid++) {
    
	var i = carid * vCars; //determine the starting index in array for target car
	var lane = carid / 2;
	var speed = random(2, 5);
	var offset = 0;

	if (carid % 2 == 1) { 
	    offset = 150; 
	    
	}
	else {
	    offset = 0;
	}

	if (carid < 6) {

        tCars[i] = 0 - offset;
		tCars[i+1] = 22 +(lane * 21);//set Car (cardid) Y coordinate - 22,43,64
		tCars[i+2] = -speed;//set Car (cardid) XSPEED
		
    }
    else {   
        
	    tCars[i] = 220 + offset;
	    tCars[i+1] = 92 + ((lane-3) * 21);//set Car (cardid) Y coordinate - 92,113,134
	    tCars[i+2] = speed;//set Car (cardid) XSPEED
    }
// console( tCars[i]);
	tCars[i+3] = random(1, 3); //show

}



var start = time()

const StreetBG = file("StreetBG", 0); 

const carImages =
    [
    file("FreewayCar2", 0), // Red car
    file("FreewayCar1", 0), // Green car
    file("FreewayCar3", 0)  // Truck
    ];

const playerOne = file("FreewayChicken_F1", 0);
const playerOneF2 = file("FreewayChicken_F2", 0);

const playerTwo = file("FreewayChicken2_F1", 0);
const playerTwoF2 = file("FreewayChicken2_F2", 0);

const Chicken1box = file("Chicken1box", 0);
const Chicken2box = file("Chicken2box", 0);


function reset() {

    start = time();
    
    playerY1 = 150;
    score1 = 0;
    
    playerY2 = 150;
    score2 = 0;

}

function waitForInput()
{
    // Player One Input
    if ((pressed("UP")) && (playerY1 > 0))          playerY1-=2;
    if ((pressed("DOWN")) && (playerY1 < 150))      playerY1+=2;
    
    // Player Two Input
    if ((pressed("A")) && (playerY2 > 0))           playerY2-=2;
    if ((pressed("B")) && (playerY2 < 150))         playerY2+=2;

    if (pressed("C"))                               exit();
    
}

// CHANGED
function collide(carIdx, playerIdx)
{
    if (playerIdx == 1) { music("pouletjade.raw"); playerY1 = 150}
    if (playerIdx == 2) { music("pouletpapa.raw"); playerY2 = 150}
}

function background()
{
 for( var x = 0; x < 220; x+=30 ) { sprite(x, 0, StreetBG); }
}

function scoreDisplay()
{
    color(7);
    cursor(15,5)
    print(score1);
    
    cursor(200,5)
    print(score2);
    color(0);  
}

function playerRender()
{
    io("COLLISION", PLAYER1_IDX, CAR_IDX, collide);
    sprite(50, playerY1 + 6, Chicken1box);
    Chicken1Anim++;

    if((Chicken1Anim/3)%2==0) {
        sprite(50, playerY1, playerOne);
    }
    else {
        sprite(50, playerY1, playerOneF2);
    }

    io("COLLISION", PLAYER2_IDX, CAR_IDX, collide);
    sprite(150, playerY2 + 6, Chicken2box);
        //Chicken1Anim++;
        if((Chicken1Anim/3)%2==0)
        sprite(150, playerY2, playerTwo);
    else
        sprite(150, playerY2, playerTwoF2);
        //sprite(150, playerY2, playerTwo);
}


function updateCars(){
    
	for( var carid = 0; carid < nCars; carid++ ) {
	    
		var i = carid * vCars;//determine the starting index in array for target car
		var offset = -4;

		if (carid % 2 == 0) {
		    offset = 4;
		}
		else {
		    offset = -4;
		}
		
		
		var otherCarX = tCars[i + offset];
		var x = tCars[i] + tCars[i+2];

		if (tCars[i+2] > 0) {//car is moving to the right

            if (x + 80 < otherCarX || otherCarX < x) {
        
        		tCars[i] = x; //move target cars x coordinate by the cars speed 
        		
        	}
        	else {
       
        		tCars[i] = otherCarX - 80;
        	    
        	}
        
        
            launchCar_Right(i, offset);
            
		} 
		
		if (tCars[i+2] < 0) { //car must be moving left

        	if (x - 80 > otherCarX || otherCarX > x) {
        
        		tCars[i] = x; //move target cars x coordinate by the cars speed 
        		
        	}
        	else {
        
        		tCars[i] = otherCarX + 80;
        	    
        	}
        
            launchCar_Left(i, offset);
            
		}
    	
	}
	
}

function launchCar_Right(i, offset) {
    
	if(tCars[i] > 220) {//car i x coordinate is greater than right edge of screen
	
		tCars[i] = tCars[i + offset] - 250;
		tCars[i+2] = random(2, 5);// set car speed to a random number between 2 and 4
		tCars[i+3] = random(1, 4);// set car type to a random number between 1 and 3

	}
	
}

function launchCar_Left(i, offset) {
    
    if(tCars[i] < -64){
        
        tCars[i] = tCars[i + offset] + 250;
        tCars[i+2] = -random(2, 5);// set car speed to a random number between 2 and 4 negative
        tCars[i+3] = random(1, 4);// set car type to a random number between 1 and 3
        
    }
    
}

function carRender() {
    
	for( var carid = 0; carid < nCars; carid++) {

		var i = carid * vCars;//determine the starting index in array for target car
		
		if(tCars[i+2]>0){//car is moving to the right
			mirror( false );
		}
		else {
			mirror( true );
		}
		
        io("COLLISION", CAR_IDX, 0); // CHANGED
	    var carType = tCars[i+3] - 1;
        sprite(tCars[i], tCars[i+1], carImages[carType]);

	}
	
}

function checkForPoint() {
    
    if (playerY1 <= 0) {
        score1 +=1;
        playerY1 = 150;
        playSound();
    }
    
    if (playerY2 <= 0) {
        score2 +=1;
        playerY2 = 150;
        playSound();
    }
    
}


function playSound() {
    io("VOLUME", 127);
    io("DURATION", 70);
    sound(random(44, 47));
}

function gameOver() {
    
    highscore(max(score1, score2))
    background();
    updateCars();
    carRender();
    color(7); 
    
    if (score1 > score2) {
        cursor(55,75);
        print("PLAYER 1 WINS!")

    }
    else if (score1 == score2) {
        cursor(45,75);
        print("IT'S A TIED GAME!")
    }
    else {
        cursor(55,75);
        print("PLAYER 2 WINS!")
    }

    cursor(15,95);
    print("B TO RESTART / C TO EXIT")
    color(0);
    
    if (timeSinceStart >=60) timeSinceStart = 60;
    if (pressed("C")) exit();
    if (justPressed("B")) reset();

}


function update() {
    
    var timeSinceStart = (time() - start) / 1000; 
    
    if(timeSinceStart >= 60) {
        gameOver();
    } 
    else {
        checkForPoint();
        waitForInput();
        background();
        updateCars();
        carRender();
        playerRender();
        color(175); cursor(105,5); print(60 - timeSinceStart); //color(0);
    }
    
    scoreDisplay();

} <file_sep>save("save/bookmark", "Book1/166.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  You are on the road between
Marlock City and the Shadar Tor.
Along most of the length of the
road, a thin sliver of a shanty
town has grown up. Tents and
lean-to's line the way. You find
out that the people living here
are refugees from Trefoille. The
city was burnt to the ground
during the recent civil war, in
which the old king was overthrown.
Roll a die . A pick-pocket! You
lose 10 Shards Nothing happens You
find a by the side of the road

  When you are finished, you can:
  `,
GOTO,
"Book1/100.js",
`Go to Marlock City`,
`
  `,
GOTO,
"Book1/35.js",
`Head for the Shadar Tor`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>save("save/bookmark", "Book1/427.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  If you have the codeword Attar,`,
GOTO,
"Book1/578.js",
`
go to 578`,
`immediately. If not, read
on.

  Venefax is a strange-looking
village. It looks like a single
gigantic building. All the houses
are joined together to form a
jumbled mass, and none of the
houses have doors - the only way
in is through holes in the
rooftop. Ladders lead up to the
roof, which in effect forms a
network of streets that the
inhabitants travel across to get
to certain buildings.

  'We had to build it that way, for`,
MARK,
`defence,' says a passing farmer.
'You see, the scorpion men from
the south cannot climb, so they
can't get inside the town!'
  `,
GOTO,
"Book1/152.js",
`Visit the market`,
`
  `,
GOTO,
"Book1/497.js",
`Visit the Scorpion's Sting
Tavern`,
`
  `,
GOTO,
"Book1/338.js",
`Visit the village healer`,
`
  `,
GOTO,
"Book1/63.js",
`Chat to villagers on the rooftop`,
`
  `,
GOTO,
"Book1/492.js",
`Go south into Scorpion Bight`,
`
  `,
GOTO,
"Book1/87.js",
`Take the road north to Blessed
Springs`,
`
  `,
GOTO,
"Book1/621.js",
`South west on the road to
Yellowport`,
`
  `,
GOTO,
"Book1/278.js",
`North into open countryside`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep># LoveRush-Pine2k
Pine2K version of Love Rush for Pokitto
<file_sep>save("save/bookmark", "Book1/637.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  Somehow you manage to evade the
creature's jaws, and swim back to
shore. Hauling yourself up on to
the beach, you are assaulted by an
angry villager.

  'Where's my boat?' he cries,
'You've sunk it, haven't you? You
city types don't know a thing!
Well, you'll have to pay me back -
fifty Shards at least!'

  'Hah!' you reply, 'The cost of
hiring it was more than its worth
in any case. As far as I'm
concerned, that boat was mine to
do with as I liked!'
`,
MARK,
`  The argument goes on all the way
back to the village but eventually
the fisherman gives up and storms
off.

  `,
GOTO,
"Book1/135.js",
`go to 135`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>autoAccelerate true
name Player
pod 0
color 5
<file_sep>save("save/bookmark", "Book1/122.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  If you have the codeword Acid,`,
GOTO,
"Book1/543.js",
`go
to 543`,
`immediately. If not, but
you have a * copper amulet * ,`,
GOTO,
"Book1/384.js",
`go
to 384`,
`immediately. Otherwise,
read on.

  Guildmaster Vernon of Yellowport
is surprisingly eager to see you.
He is a hugely fat and bejewelled
merchant, and he tells you that a
group of ratmen have made a base
in the sewers beneath the city.
They come out at night to raid the
warehouses and homes of the
merchants of Yellowport.

  'We need an adventurer like
yourself to destroy their king,'`,
MARK,
`explains the guildmaster. 'Without
him, the ratmen wouldn't be able
to organize a feast in a larder.
We will pay you 450 Shards if you
succeed.'

  Vernon tells you that the sewers
can be entered via an old disused
well in the poor quarter.

  Whenever you are ready to enter
the sewers, and you are in
Yellowport, turn to * 460 * .`,
GOTO,
"Book1/460.js",
`Note
this option`,
`on your Adventure
Sheet. Now you can return to the
city centre,`,
GOTO,
"Book1/10.js",
`go to 10`,
`, or`,
GOTO,
"Book1/460.js",
`go down
the sewers straight away`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>save("save/bookmark", "Book1/596.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  You thread your way through the
ranks of trees. In places, the
forest canopy almost blocks out
the sunlight completely, and you
are wandering in deep shadow,
haunted by the cries of the
creatures of the forest. If you
have an * oak staff * ,`,
GOTO,
"Book1/653.js",
`go to 653`,
`
immediately. If not, read on.

  You hack your way through the
undergrowth until you stumble
across an old ruin. Creepers, and
forest moss have grown over most
of it, but you can see mask-like
faces set into the walls, glaring
down at you as if in angry outrage
at your intrusion. You come to a`,
MARK,
`massive stone slab - clearly a
door. Set into the middle of it is
a round, granite face, the visage
of a sleeping demon. Suddenly, an
eye pops open, and regards you
curiously.

  'By the Tentacles of Tantallon!'
exclaims the face in a gravelly
voice. 'A human! I haven't seen
one of you lot for a hundred
years.' An expression of suspicion
forms on its rocky features. 'What
do you want, anyway?'

  'Just passing through,' you
comment airily.

  'Well you can't pass through me
unless you know the password. And`,
MARK,
`that was given to me by Astrallus
the Wizard King, umm... let me
see, now... a thousand years ago,
by Ebron! This is his tomb, you
know.'

  If you have the codeword Crag,`,
GOTO,
"Book1/160.js",
`go
to 160`,
`. Otherwise, read on.

  'How do I find the password
then?' you ask.

  'Well,' says the demon door,
'Astrallus was a wizard, so why
don't you ask some wizards?'

  'Where would I find some
wizards?' you ask.

  'How would I know?' replies the`,
MARK,
`door testily. 'I've been stuck
here for a thousand years!'

  You decide it is time to leave.
  `,
GOTO,
"Book1/110.js",
`North to the Bronze Hills`,
`
  `,
GOTO,
"Book1/333.js",
`West to the River Grimm`,
`
  `,
GOTO,
"Book1/560.js",
`South into the country`,
`
  `,
GOTO,
"Book1/387.js",
`East to the road`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>const minX = 6, maxX = 22;
const minY = 0, maxY = 20;
const boardWidth = maxX - minX;
const boardHeight = maxY - minY;
const heart = builtin("sHeart");
const lvl = builtin("sLvl");
const pts = builtin("sPts");
const ledOFF = builtin("shape2");
const ledON = builtin("sBtn");
const snakeColor = 112;
const bgColor = 113;
const appleColor = 87;
const maxLen = 100;
var snake = new Array(maxLen);
var begin;
var end;
var currentLen;
var len;
var x, y, vx, vy, ax, ay;
var dead, won = 0;
var stepTime;
var delay = 0;
var level = 0;
var inputEnabled = true;
var score = 0;
var prevScore = 0;
var lives = 3;

window(44, 0, 176, 176);
tileshift(2, 0);
io("DURATION", 50);
reset();

function reset(){
    if(lives < 0){
        highscore(score);
        exit();
        return;
    }
    color(bgColor);
    for(y = 0; y < boardHeight; ++y){
        for(x = 0; x < boardWidth; ++x){
            tile(x + minX, y + minY, ledOFF);
        }
    }
    t = 0;
    delay = 100 - (level * 5);
    x = boardWidth / 2;
    y = boardHeight / 2;
    currentLen = 1;
    len = 5;
    vx = 0;
    vy = 0;
    dead = false;
    end = 0;
    begin = end;
    snake[begin] = (x << 16) + y;
    ax = random(0, boardWidth);
    ay = random(0, boardHeight);
    inputEnabled = true;
}

function pattern(x, y, imgName){
    var img = builtin(imgName);
    var w = peek(img++);
    var h = peek(img++);
    x += minX;
    y += minY;
    for(var ty = 0; ty < h; ++ty){
        for(var tx = 0; tx < w; ++tx){
            var c = peek(img, tx);
            if(c){
                color(c - 7);
                tile(x + tx, y + ty, ledOFF);
            }
        }
        img += w;
    }
}

function plot(i, img){
    var c = snake[i];
    var ty = (c << 16) >> 16;
    tile((c >> 16) + minX, ty + minY, img);
}

function hud(){
    color(47);
    sprite(50, 165, lvl);
    cursor(60, 165);
    printNumber(level + 1);
    sprite(110, 165, pts);
    cursor(120, 165);
    printNumber(score);
    color(0);
    for(var i=0; i<lives; ++i)
        sprite(80 + (i * 8), 165, heart);
}

function update(){
    if(pressed("C"))
        exit();

    hud();

    if(inputEnabled){
        if(pressed("LEFT") && (vx != 1)){
            vx = -1;
            vy = 0;
            inputEnabled = false;
        }

        if(pressed("RIGHT") && (vx != -1)){
            vx = 1;
            vy = 0;
            inputEnabled = false;
        }

        if(pressed("UP") && (vy != 1)){
            vx = 0;
            vy = -1;
            inputEnabled = false;
        }

        if(pressed("DOWN") && (vy != -1)){
            vx = 0;
            vy = 1;
            inputEnabled = false;
        }
    }

    if((time() - stepTime) < delay)
        return;
    stepTime = time();
    inputEnabled = true;

    if(won){
        vx = 0;
        vy = 0;
        --won;
        if(!won){
            ++level;
            reset();
            return;
        }
    } else {
        x += vx;
        y += vy;

        if(x < 0) x = boardWidth - 1;
        else if(x >= boardWidth) x = 0;
        if(y < 0) y = boardHeight - 1;
        else if(y >= boardHeight) y = 0;

        if((x == ax) && (y == ay)){
            ++score;
            len += 3;
            if(len >= ((5 + level) * 5)){
                highscore(score);
                prevScore = score;
                delay = 20;
                won = 10;
                pattern(0, 2, "happyEmote");
                return;
            }
            ax = random(0, boardWidth);
            ay = random(0, boardHeight);
        }
    }

    color(bgColor);
    plot(begin, ledOFF);

    if(dead){
        vx = 0;
        vy = 0;
        if(score > prevScore) --score;
        --len;
        if(!len){
            score = prevScore;
            --lives;
            reset();
            return;
        }
        currentLen = len;
        if(++begin == maxLen) begin = 0;
    }

    var growing = !dead;

    if(vx || vy){
        if(++end == maxLen) end = 0;
        snake[end] = (x << 16) + y;
        if(++currentLen > len){
            growing = false;
            if(++begin == maxLen) begin = 0;
            currentLen--;
        }else{
            sound(40, 0);
        }
    }

    color(snakeColor);
    for(var i = begin; i != end;){
        if(growing)
            color(random(0, 255));
        if(snake[i] == snake[end]){
            delay = 20;
            if(!dead){
                dead = true;
                sound(60, 0);
                pattern(4, 7, "sSkull");
            }
        }
        plot(i, ledON);
        if(++i == maxLen) i = 0;
    }
    color(snakeColor-1);
    plot(end, ledON);

    color(appleColor);
    tile(ax + minX, ay + minY, ledON);
}
<file_sep>save("save/bookmark", "Book1/256.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  The king is overjoyed with the
news. 'Excellent! At this rate I
will be able to take my rightful
place in the throne room of Old
Sokar.'

  He rewards you with the title .
Note this in the Titles and
Honours box on your Adventure
Sheet. The title comes with a cash
gift of as well.

  You also go up one Rank . Roll
one die - the result is the number
of Stamina points you gain
permanently. Note the increase on
your Adventure Sheet. Also, don't
forget to increase your Defence by`,
MARK,
`1 as a result of your gain in
Rank.

  The king has another mission for
you. He explains that an army of
steppe Nomads, Trau, Mannekyn
people, and Sokaran troops still
loyal to Nergan, have gathered on
the steppes, and are moving to
siege the Citadel of Velis Corin,
which guards the Pass of Eagles
through the Spine of Harkun.
Nergan tells you that an alliance
of northern nations has declared
war on the new Sokaran regime. A
certain General Beladai leads the
Northern Alliance, and King Nergan
has joined forces with him. He
asks you to travel to the Steppes
and talk to General Beladai.`,
MARK,
`
  'An adventurer like yourself
might be able to steal into the
citadel, and bring about its
downfall from within, or some
such. If the citadel falls we will
have Sokara at our mercy.'

  If you want to take up this
mission for the King, record the
codeword Assault . When you are
ready, you are led back down to
the foothills of the mountains.`,
GOTO,
"Book1/474.js",
`go
to 474`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>var bookmark = file("save/bookmark", 0);

if( !bookmark ){
    exec("Book1/New.js");
} else {
    io("FILLER", 3, "TEXT", "5x7");
    splash(0, 0, "splash.565");
    window(0, 166, 220, 176);
    cursor(1, 21);
    print(bookmark);
    exec(bookmark);
}
<file_sep>save("save/bookmark", "Book1/400.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  <NAME> is a medium-sized
town, that acts as a way-station
between the citadel to the north,
and the rich towns of the south.
It is a garrison town; many
supplies, arms, and soldiers move
through <NAME> on the
north-south trail. Shops, temples,
traders, and the like have sprung
up here to serve the needs of the
military. There is also a sizeable
mining community, for the mines of
Sokara lie in the Bronze Hills,
just outside town, and a slave
market where poor unfortunates,
sold into slavery, are bought for
work in the mines.
`,
MARK,
`  You can buy a town house in Caran
Baru for 200 Shards. Owning a town
house gives you a place to rest,
and to store equipment. If you buy
one , tick the box by the town
house option.

  If you have the codeword
Barnacle,`,
GOTO,
"Book1/418.js",
`go to 418`,
`immediately.
Otherwise, pick from the following
options.
  `,
GOTO,
"Book1/215.js",
`Visit the marketplace`,
`
  `,
GOTO,
"Book1/112.js",
`Visit the merchants' guild`,
`
  `,
GOTO,
"Book1/473.js",
`Visit the slave market`,
`
  `,
GOTO,
"Book1/282.js",
`Visit the temple of Tyrnai`,
`
  `,
GOTO,
"Book1/615.js",
`Visit the temple of Lacuna`,
`
  `,
GOTO,
"Book1/86.js",
`Visit the temple of the Three
Fortunes`,
`
  `,
GOTO,
"Book1/177.js",
`Visit your town house {box} (if
box ticked)`,
MARK,
`  `,
GOTO,
"Book1/184.js",
`Visit the Blue Griffon Tavern`,
`
  `,
GOTO,
"Book1/201.js",
`Follow the road north to the
citadel`,
`
  `,
GOTO,
"Book1/110.js",
`Go west into the Bronze Hills`,
`
  `,
GOTO,
"Book1/60.js",
`Travel north east into the
country`,
`
  `,
GOTO,
"Book1/458.js",
`Take the east road to Fort
Mereth`,
`
  `,
GOTO,
"Book1/474.js",
`Head south east into the
mountains`,
`
  `,
GOTO,
"Book1/347.js",
`Take the south road`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>if (!file("resources.res")) {
    exit();
}


////////////////
// ANIMATIONS //
////////////////

// struct Animation
// {
//     AnimationStep* steps;
// }
// struct AnimationStep
// {
//     int untilTimer; // [0] - This steps is applied until timer reaches this value.
//     Bitmap spriteBitmap; // [1] - The bitmap to show. If null, the animation timer is reset.
// }
const animationStepSize = 2 * 4;

const EndAnimation =
[
    4, file("End1:8", null),
    8, file("End2:8", null),
    12, file("End3:8", null),
    99999, null
];


function renderAnimation(x256, y256, animation, animationTimer)
{
    var animation = animation;
    var animationStep = animation;
    
    while (animationTimer >= animationStep[0])
        animationStep += animationStepSize;
    
    var animationBitmap = animationStep[1];
        
    if (animationBitmap == null)
    {
        animationTimer = 0;
        animationBitmap = animation[1];
    }
    sprite(x256 >> 8 - 7, y256 >> 8 - 15, animationBitmap);
    return animationTimer;
}


////////////////
// CINEMATICS //
////////////////

var cinematicsTimer = 0;


const END_X256 = ((220 - 60) / 2 + 7) * 256;
const END_Y256 = ((176 - 25) / 2 + 15) * 256;

function cinematicsUpdateEnd()
{
    bubbleTimer = renderAnimation(END_X256, END_Y256, EndAnimation, bubbleTimer + 1);
}

var cinematicsUpdate = cinematicsUpdateEnd;


///////////////////
// INIT & UPDATE //
///////////////////

color(0);

function update()
{
    cinematicsUpdate();
}<file_sep>////////////
// LEVELS //
////////////

// struct Level
// {
//     STRING filename; // [0] - Name of the level's file.
//     int finishedTime; // [1] - Run Timestamp of the completion.
//     bool collectedCatnip; // [2] - If true, the Catnip was collected.
//     int reserved; [3] - ???
// };
const Level_SIZE = 4 * 4;

const levelsStart = file("_run.tmp", null);
const levelsEnd = levelsStart + length(levelsStart) * 4;

if (!file("resources.res")) {
    exit();
}


/////////////
// RESULTS //
/////////////

// An important end item.
var collectedCatnips = 0;
var availableCatnips = 0;

for (var level = levelsStart, levelNumber = 1; level[0] != null; level += Level_SIZE, levelNumber++)
{
    collectedCatnips += level[2];
    availableCatnips += level[3];
}

    
///////////
// TOOLS //
///////////

var timer = 0;

function clear()
{
    for (var i = 0; i < 30; i++)
        print(" \n");
    cursor(0, 0);
}


//////////////////
// STORY ENGINE //
//////////////////

var currentStep = steps;

function goToStep(step)
{
    currentStep = steps + step * 8 * 4;
    timer = 0;
}


////////////////////
// STEPS HANDLERS //
////////////////////

const stepsHandlers = [
    null,
    null,
    null,
    null,
    null,
    null,
    null,
    null,
    null
]

// Infinite loop.
// - All parameters are ignored.
const STEP_HANDLER_INFINITE = 0;
function infiniteStepHandler(step)
{
}
stepsHandlers[STEP_HANDLER_INFINITE] = infiniteStepHandler;

// Progressive Text appears, waiting A press..
// - Parameter 1 is the next step index.
// - Parameters 2-7 are used as text, until the first null.
//   - If COMMAND_CLEAR appears as a text, the screen will be cleared instead.
const TEXT_CLEAR = "<clear>";
const STEP_HANDLER_TEXT_PRESS = 1;
const TIMER_STEP = 5;
function textPressStepHandler(step)
{
    var expectedTimer = 0;
    
    for (var paramI = 2; (paramI < 8) && (step[paramI] != null); paramI++, expectedTimer += TIMER_STEP)
        if (expectedTimer == timer)
            if (step[paramI] == TEXT_CLEAR)
                clear();
            else
                print(step[paramI]);
    if ((timer > expectedTimer) && (justPressed("A")))
        goToStep(step[1]);
    else
        timer++;
}
stepsHandlers[STEP_HANDLER_TEXT_PRESS] = textPressStepHandler;

// Progressive Text appears.
// - Parameter 1 is the next step index.
// - Parameters 2-7 are used as text, until the first null.
//   - If COMMAND_CLEAR appears as a text, the screen will be cleared instead.
const STEP_HANDLER_TEXT = 2;
function textStepHandler(step)
{
    var expectedTimer = 0;
    
    for (var paramI = 2; (paramI < 8) && (step[paramI] != null); paramI++, expectedTimer += TIMER_STEP)
        if (expectedTimer == timer)
            if (step[paramI] == TEXT_CLEAR)
                clear();
            else
                print(step[paramI]);
    if (timer > expectedTimer)
        goToStep(step[1]);
    else
        timer++;
}
stepsHandlers[STEP_HANDLER_TEXT] = textStepHandler;

// Button to branch.
// - Parameters are mapped to a button press (justPressed) each:
//   - RIGHT, DOWN, LEFT, UP, A, B, C
// - Each null or 0 parameter is ignored.
// - Otherwise, when pressing said button, it'll branch to the corresponding step.
const STEP_HANDLER_PRESS_TO_BRANCH = 3;
const branchButtons = [
    "RIGHT",
    "DOWN",
    "LEFT",
    "UP",
    "A",
    "B",
    "C"
];
function textPressToBranchHandler(step)
{
    for (var buttonI in branchButtons)
        if ((step[buttonI + 1] != null) && (justPressed(branchButtons[buttonI])))
        {
            goToStep(step[buttonI + 1]);
            return ;
        }
}
stepsHandlers[STEP_HANDLER_PRESS_TO_BRANCH] = textPressToBranchHandler;

const STEP_HANDLER_END_DIRECT = 4;
function endDirectHandler(step)
{
    exec("blueend.js");
}
stepsHandlers[STEP_HANDLER_END_DIRECT] = endDirectHandler;

const STEP_HANDLER_RED_END = 5;
function redEndDirectHandler(step)
{
    exec("end.js");
}
stepsHandlers[STEP_HANDLER_RED_END] = redEndDirectHandler;

// Progressive Text appears, but only with catnips.
const STEP_HANDLER_TEXT_IF_CATNIPS = 6;
function textIfCatnipsStepHandler(step)
{
    if (collectedCatnips == 0)
        goToStep(step[1]);
    else
        textStepHandler(step);
}
stepsHandlers[STEP_HANDLER_TEXT_IF_CATNIPS] = textIfCatnipsStepHandler;

// Progressive Text appears, but only with full catnips.
const STEP_HANDLER_TEXT_IF_ALLCATNIPS = 7;
function textIfAllCatnipsStepHandler(step)
{
    if (collectedCatnips != availableCatnips)
        goToStep(step[1]);
    else
        textStepHandler(step);
}
stepsHandlers[STEP_HANDLER_TEXT_IF_ALLCATNIPS] = textIfAllCatnipsStepHandler;

// Jumps to the step indicated:
// - param1 if all catnips,
// - param2 if a few catnips,
// - param3 if no catnips.
const STEP_HANDLER_IF_CATNIPS = 8;
function ifCatnipsHandler(step)
{
    if (collectedCatnips == availableCatnips)
        goToStep(step[1]);
    else if (collectedCatnips > 0)
        goToStep(step[2]);
    else
        goToStep(step[3]);
}
stepsHandlers[STEP_HANDLER_IF_CATNIPS] = ifCatnipsHandler;


///////////
// STORY //
///////////

const STEP_INITIAL = 0;

const STEP_ROOMCHOICE_INIT = STEP_INITIAL + 1;
const STEP_ROOMCHOICE_CATNIPS = STEP_ROOMCHOICE_INIT + 1;
const STEP_ROOMCHOICE_CHOICES = STEP_ROOMCHOICE_CATNIPS + 1;
const STEP_ROOMCHOICE_BRANCHES = STEP_ROOMCHOICE_CHOICES + 1;

const STEP_CHECKGREYDOOR = STEP_ROOMCHOICE_BRANCHES + 1;

const STEP_CHECKREDDOOR_INIT = STEP_CHECKGREYDOOR + 1;
const STEP_CHECKREDDOOR_BRANCHES = STEP_CHECKREDDOOR_INIT + 1;

const STEP_CHECKBLUEDOOR_INIT = STEP_CHECKREDDOOR_BRANCHES + 1;
const STEP_CHECKBLUEDOOR_BRANCHES = STEP_CHECKBLUEDOOR_INIT + 1;

const STEP_CHECKWINDOW = STEP_CHECKBLUEDOOR_BRANCHES + 1;

const STEP_BLUE_FORCE_CHECK = STEP_CHECKWINDOW + 1;
const STEP_BLUE_FORCE_LAST = STEP_BLUE_FORCE_CHECK + 1;

const STEP_BLUE_EXIT_INIT = STEP_BLUE_FORCE_LAST + 1;
const STEP_BLUE_EXIT_CATNIPS = STEP_BLUE_EXIT_INIT + 1;
const STEP_BLUE_EXIT_LAST = STEP_BLUE_EXIT_CATNIPS + 1;

const STEP_BLUE_END = STEP_BLUE_EXIT_LAST + 1;

const STEP_RED_EXIT_INIT = STEP_BLUE_END + 1;
const STEP_RED_EXIT_CATNIPS = STEP_RED_EXIT_INIT + 1;
const STEP_RED_EXIT_ALLCATNIPS = STEP_RED_EXIT_CATNIPS + 1;
const STEP_RED_EXIT_LAST = STEP_RED_EXIT_ALLCATNIPS + 1;

const STEP_RED_END = STEP_RED_EXIT_LAST + 1;

// A step consists of a handler (see STEP_HANDLER_*) and 7 parameters (not all them are used.)
const steps =
[
    // STEP_INITIAL
    STEP_HANDLER_TEXT_PRESS,
        STEP_ROOMCHOICE_INIT,
        "You do not hear any more meows;\n",
        "merely some faint howling.\n\n",
        "Having fed all these cats leaves you\nsatisfied.\n",
        " \n",
        "... but hungry as well.\n \n",
        " \n                      [A - Continue]",



    // STEP_ROOMCHOICE_INIT
    STEP_HANDLER_TEXT,
        STEP_ROOMCHOICE_CATNIPS,
        TEXT_CLEAR,
        "You are in the middle of a quite\ndark room.\n \n",
        null,
        null,
        null,
        null,
    // STEP_ROOMCHOICE_CATNIPS
    STEP_HANDLER_TEXT_IF_CATNIPS,
        STEP_ROOMCHOICE_CHOICES,
        "Your pocket is full of catnips.\n \n",
        null,
        null,
        null,
        null,
        null,
    // STEP_ROOMCHOICE_CHOICES
    STEP_HANDLER_TEXT,
        STEP_ROOMCHOICE_BRANCHES,
        "          [UP - Grey Door]\n",
        "[< - Red Door]       [> - Blue Door]\n",
        "          [DOWN - Window]\n",
        null,
        null,
        null,
    // STEP_ROOMCHOICE_BRANCHES
    STEP_HANDLER_PRESS_TO_BRANCH,
        STEP_CHECKBLUEDOOR_INIT, // RIGHT
        STEP_CHECKWINDOW, // DOWN
        STEP_CHECKREDDOOR_INIT, // LEFT
        STEP_CHECKGREYDOOR, // UP
        null, // A
        null, // B
        null, // C



    // STEP_CHECKGREYDOOR
    STEP_HANDLER_TEXT_PRESS,
        STEP_ROOMCHOICE_INIT,
        TEXT_CLEAR,
        "You're looking at the grey door.\n \n",
        "You came from there, but it won't\nbudge anymore.\n \n",
        "Anyway, coming here was quite\ndangerous.\n",
        "Unlike cats, you got only one live, after all.\n",
        " \n                          [A - Back]",



    // STEP_CHECKREDDOOR_INIT
    STEP_HANDLER_TEXT,
        STEP_CHECKREDDOOR_BRANCHES,
        TEXT_CLEAR,
        "You're looking at the red door.\n \n",
        "It is covered with intricate golden patterns and surrounded by a\npowerful aura.\n",
        " \n[A - Back]               [C - Enter]",
        null,
        null,
    // STEP_CHECKREDDOOR_BRANCHES
    STEP_HANDLER_PRESS_TO_BRANCH,
        null, // RIGHT
        null, // DOWN
        null, // LEFT
        null, // UP
        STEP_ROOMCHOICE_INIT, // A
        null, // B
        STEP_RED_EXIT_INIT, // C



    // STEP_CHECKBLUEDOOR_INIT
    STEP_HANDLER_TEXT,
        STEP_CHECKBLUEDOOR_BRANCHES,
        TEXT_CLEAR,
        "You're looking at the blue door.\n \n",
        "It is a very nice door, finely\ncrafted.\n\n",
        "You can hear the city bustling\nthrough it.\n",
        "It's probably the fastest way to go home.\n",
        " \n[A - Back]               [C - Enter]",
    // STEP_CHECKBLUEDOOR_BRANCHES
    STEP_HANDLER_PRESS_TO_BRANCH,
        null, // RIGHT
        null, // DOWN
        null, // LEFT
        null, // UP
        STEP_ROOMCHOICE_INIT, // A
        null, // B
        STEP_BLUE_FORCE_CHECK, // C



    // STEP_CHECKWINDOW
    STEP_HANDLER_TEXT_PRESS,
        STEP_ROOMCHOICE_INIT,
        TEXT_CLEAR,
        "You're checking the window.\n \n",
        "While the immediate neighborhood is quiet, you can see the city being\nlively far away.\n \n",
        "However, it is way too high to exit this way.\n",
        " \n                      [A - Continue]",
        null,


    // STEP_BLUE_FORCE_CHECK
    STEP_HANDLER_IF_CATNIPS,
        STEP_BLUE_FORCE_LAST, // All catnips
        STEP_BLUE_EXIT_INIT, // Some catnips
        STEP_BLUE_EXIT_INIT, // No catnips.
        null, 
        null, 
        null, 
        null,
    // STEP_BLUE_FORCE_LAST
    STEP_HANDLER_TEXT_PRESS,
        STEP_ROOMCHOICE_INIT,
        TEXT_CLEAR,
        "... But somehow, you cannot resolve yourself into opening that door.\n \n",
        "You cannot explain why, but you know\nyou still got some business left to\ndo here.\n",
        " \n                      [A - Continue]",
        null,
        null,
    
    // STEP_BLUE_EXIT_INIT
    STEP_HANDLER_TEXT,
        STEP_BLUE_EXIT_CATNIPS,
        TEXT_CLEAR,
        "You open the door.\n \n",
        "After a long walk, you're finally\nback at your home, enjoying a good\nmeal.\n",
        " ",
        " ",
        " ",
    // STEP_BLUE_EXIT_CATNIPS
    STEP_HANDLER_TEXT_IF_CATNIPS,
        STEP_BLUE_EXIT_LAST,
        " ",
        " ",
        " ",
        " ",
        " \n",
        "Though you're still wondering what\nyou're going to do with all these\ncatnips...\n",
    // STEP_BLUE_EXIT_LAST
    STEP_HANDLER_TEXT_PRESS,
        STEP_BLUE_END,
        " ",
        " ",
        " ",
        " ",
        " \n",
        " \n                      [A - Continue]",



    // STEP_BLUE_END
    STEP_HANDLER_END_DIRECT,
        null,
        null,
        null,
        null,
        null,
        null,
        null,



    // STEP_RED_EXIT_INIT
    STEP_HANDLER_TEXT,
        STEP_RED_EXIT_CATNIPS,
        TEXT_CLEAR,
        "You open the door.\n \n",
        "The room inside is huge, and quite\nweird.\nA large hole is present at the\nopposite.\n \n",
        "You approach the hole.\n \n",
        null,
        null,
    // STEP_RED_EXIT_CATNIPS
    STEP_HANDLER_TEXT_IF_CATNIPS,
        STEP_RED_EXIT_ALLCATNIPS,
        "The catnips in your pocket start\nvibrating.",
        " ",
        " ",
        " ",
        " \n",
        "... and suddenly it becomes empty!\n \n",
    // STEP_RED_EXIT_ALLCATNIPS
    STEP_HANDLER_TEXT_IF_ALLCATNIPS,
        STEP_RED_EXIT_LAST,
        " ",
        " ",
        " ",
        " ",
        " \n",
        "The ground is rumbling!\n \n",
    // STEP_RED_EXIT_LAST
    STEP_HANDLER_TEXT_PRESS,
        STEP_RED_END,
        " \n                      [A - Continue]",
        null,
        null,
        null,
        null,
        null,



    // STEP_RED_END
    STEP_HANDLER_RED_END,
        null,
        null,
        null,
        null,
        null,
        null,
        null,


    
    // (Last)
    STEP_HANDLER_INFINITE,
        null,
        null,
        null,
        null,
        null,
        null,
        null
];

if (length(steps) % 8 != 0)
{
    // Steps must contain a multiple of 8 items.
    console("mismatching steps!\n");
    while(true);
}


///////////////////
// INIT & UPDATE //
///////////////////

io("FORMAT", 0, 0);

io("FILLER", 1, "TEXT", "5x7");
cursor(0, 0);
color(7);

goToStep(STEP_INITIAL);

function update()
{
    var stepUpdate = stepsHandlers[currentStep[0]];
    
    stepUpdate();
}<file_sep>if ( file("images.res", 0) != 1 ){
    console("Could not find resources!");
    exit();
}

var playerX = 60;
var playerY = 140;
var level = 4;

const NUMBER_OF_ENTITIES = 10;
const NUMBER_OF_HEARTS = 4;

const enemiesX = new Array(NUMBER_OF_ENTITIES);
const enemiesY = new Array(NUMBER_OF_ENTITIES);
const enemiesD = new Array(NUMBER_OF_ENTITIES);  // Direction where 0 = left, 1 = straight fown, 2 = right
const enemiesL = new Array(NUMBER_OF_ENTITIES);  // Length of travel

const bulletsX = new Array(NUMBER_OF_ENTITIES);
const bulletsY = new Array(NUMBER_OF_ENTITIES);

const heartsX = new Array(NUMBER_OF_HEARTS);
const heartsY = new Array(NUMBER_OF_HEARTS);

var bgLY = 0;
var topBgLY = 0;
var score = 0;
var lives = 59;
var shipAnim = 0;
var big = 0;

var timer = 0;

const HUD_BG = file("HUD_BG", 0); 
const gameOverImg = file("GameOver", 0);

const playerSprites =
    [
    file("PlayerShip_F1", 0), // load the ship frame1
    file("PlayerShip_F2", 0)  // load the ship frame 2
    ];
    
const playerShip_shadow = file("PlayerShip_shadow", 0); // load the ship shadow
const Bullet = file("Bullet", 0); // Bullet

const enemyImg = file("Enemy", 0); // load the enemy gfx
const heartImg = builtin("icon21"); 
const hudHeartImg = builtin("sHeart"); //Hearts for lives

const bgL = file("bgL", 0); // load the Left Background bottom layer image
const bgR = file("bgR", 0); // load the right Background bottom layer image
const topBgL = file("TopBgL",0); // load the Left Background Top layer image
const topBgR = file("TopBgR",0); // load the right Background Top layer image


//------------------------------------------------------------------------

// Position enemies and bullets before start of game ..

io("LOOP", 1);
music("lrmusicNew.raw");

fill(65);

for (var i = 0; i < NUMBER_OF_ENTITIES; ++i) {
    heartsY[i&3] = 200;
    bulletsY[i] = -5;
    enemySpawn(i);
}



//------------------------------------------------------------------------


function somethingHitBullet(otherIndex, bulletIndex) {
    if(otherIndex > 255){
        //lives -= 10;
        heartsY[otherIndex >> 8 - 1] = 200;
        playSound();
        score -= 20;
    } else {
        playSound();
        score += 10;
        enemySpawn(otherIndex - 1);
    }
    bulletsY[bulletIndex >> 4 - 1] = -5;    
}

function playerHitHeart(playerIndex, heartIndex) {

    lives += 10;
    playSound();
    score += 20;
    // heartSpawn(heartIndex >> 8 - 1);
    heartsY[heartIndex >> 8 - 1] = 200;
    //bulletsY[bulletIndex >> 4 - 1] = -5;    
    
}

function enemyHitPlayer(enemyIndex, playerIndex) {
    
    playSound();
    enemySpawn(enemyIndex - 1);
    lives -=20;

}


function moveEnemy(enemyIndex) {

    // Move down ..

    var y = enemiesY[enemyIndex] += (2 + (enemyIndex / 4));
    
    // Move left / right
    
    var direction = enemiesD[enemyIndex];
    var enemyX = enemiesX[enemyIndex];
    
    var newX = enemyX + direction;
    var newDirection = newX < 30 || newX > 200 || --enemiesL[enemyIndex] <= 0 || y > 176;

    // New Direction?

    if (newDirection) {
        enemySpawn(enemyIndex);
        if(y <= 176){
            newX = enemyX;
            enemiesY[enemyIndex] = y;
        } else {
            newX = enemiesX[enemyIndex];
        }
    }
    
    enemiesX[enemyIndex] = newX;

    io("SCALE", 1 + (enemyIndex < big));

    // Render enemy ..

    io("COLLISION", enemyIndex + 1, 0);
    sprite(newX, enemiesY[enemyIndex], enemyImg);

}


function enemySpawn(enemyIndex) {
    enemiesY[enemyIndex] = -random(16, 64);
    enemiesX[enemyIndex] = random(32, 192);
    enemiesD[enemyIndex] = random(-2, 3);
    enemiesL[enemyIndex] = random(16, 32);
}

function playSound() {
    io("VOLUME", 127);
    io("DURATION", 70);
    sound(random(44, 47));
}

function fireBullet() {
    
    for (var i = 0; i < NUMBER_OF_ENTITIES; ++i) {
        
       if (bulletsY[i] <= 0) {
            bulletsX[i] = playerX + 6;          
            bulletsY[i] = playerY - 2;   
            break;
       }
       
    }
    
}

function bgScroll() {
    
    for(var yOffset = -220; yOffset < (8 * 55 - 220); yOffset += 55) {
        
        var y = yOffset + bgLY; 
        sprite(32, y, bgL); 
        sprite(172, y, bgR);
        
        y = yOffset + topBgLY; 
        sprite(0, y, topBgL); 
        sprite(188, y, topBgR);
        
    }
    
    bgLY+=2;
    if (bgLY > 55)      {bgLY = 0;}
    topBgLY+=4;
    if (topBgLY > 55)   {topBgLY = 0;}
}

function HUD() {
    
    sprite(0, 0, HUD_BG);

    var x = 110;
    var heartsToDraw = lives / 10;

    while(heartsToDraw > 5){
        sprite(x, -1, heartImg);
        x += 16;
        heartsToDraw -= 5;
    }

    while(heartsToDraw--) {
        sprite(x, 4, hudHeartImg);
        x += 8;
    }
    
    color(7);
    cursor(8,4)
    print(("SCORE ")); 
    print(score);
    color(0);
}


function update() {

    bgScroll();
    
    var px = playerX + ((pressed("RIGHT") - pressed("LEFT")) << 2);
    if(px>20 && px<190) playerX = px;
    var py = playerY + ((pressed("DOWN") - pressed("UP")) << 2);
    if(py>18 && py<160) playerY = py;
    if (justPressed("A")) fireBullet();
    if (pressed("C")) exit();

    if (lives < 1) {
        highscore(score);
        sprite(45, 75, gameOverImg);
        HUD();
        return;
    }

    
    // Decrement the lives using the counter ..

    timer++;
    if (timer % 32 == 0) lives--;


    // Update the level based on the score ..

    if (score > 240){       level = 10; big = (score - 240) >> 7; }
    else if (score > 140)   level = 8;
    else if (score > 40)    level = 6;

    for(var i = 0; i < level; ++i)
        moveEnemy(i); io("SCALE", 1);


    // Render the player ..
    
    ++shipAnim;
    io("COLLISION", 1024, 0x0F, enemyHitPlayer);
    i = playerSprites[(shipAnim>>2)&1];
    sprite(playerX, playerY, i);
    sprite(playerX+20, playerY+20, playerShip_shadow);


    // Move and render the hearts ..

    for (var i = 0; i < NUMBER_OF_HEARTS; ++i){
        // Move down ..
    
        if (heartsY[i] > 176) {
            // heartSpawn(i);
            heartsY[i] = -random(0, 128);
            heartsX[i] = random(32, 192);
        }
        
        var y = heartsY[i]+=2;
        var x = heartsX[i];

        // Make hearts wobble
        x += sin(y+(i<<2)<<4) >> 5;
        
        // Render heart ..
    
        io("COLLISION", (i + 1) << 8, 1024, playerHitHeart);
        sprite(x, y, heartImg);
    }



    // Move and render the bullets ..

    for(var i = 0; i < level; ++i){
        bulletsY[i]-=5;  
        io("COLLISION", (1 + i) << 4, 0xF0F, somethingHitBullet);
        sprite(bulletsX[i], bulletsY[i], Bullet);
    }

    if (score < 0) score = 0;
    if (lives < 0) lives = 0;
    HUD();
}
<file_sep>save("save/bookmark", "Book1/20.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  'Well, well, well, what have we
here, friends?' asks the old man.
He seems to be talking to someone
next to him, although you are
certain he is alone. 'Looks like a
washed up adventurer to me!' he
says in answer to his own
question, 'all wet and out of
luck.'

  He carries on having a
conversation - a conversation that
quickly turns into a heated
debate. He is clearly quite mad.

  'Excuse me, umm, EXCUSE ME!,' you
shout above the hubbub in an
attempt to grab the old man's`,
MARK,
`attention. He stops and stares at
you.

  'Is this the Isle of the Druids?'
you ask impatiently.

  'Indeed it is,' says the old man,
'I see that you are from a far
land, so it is up to me to welcome
you to Harkuna. But I think you
may have much to do here as it is
written in the stars that someone
like you would come. Your destiny
awaits you! Follow me, young
adventurer.'

  The old man turns smartly about
and begins walking up a path
towards some hills. You can just
see some sort of monolithic stone`,
MARK,
`structure atop one of them.

  'Come on, come on, I'll show you
the Gates of the World,' the old
man babbles.
  `,
GOTO,
"Book1/192.js",
`Follow him`,
`
  `,
GOTO,
"Book1/128.js",
`Explore the coast`,
`
  `,
GOTO,
"Book1/257.js",
`Head into the nearby forest`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>save("save/bookmark", "Book1/522.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  That night several sea centaurs
emerge from the waters, their
spiny skins glittering with
phosphorescent flashes of light.

  One of them speaks in a burbling
voice. 'Where is our brother, whom
you caught in your cruel nets,
this day?'

  'He is dead, I'm afraid', you
reply, readying yourself for a
fight.

  The sea centaurs remain silent
for a few moments, then the leader
says, 'His destiny was always
bleak. We would be grateful if you`,
MARK,
`were to return his body to us.'

  You cannot think of a reason not
to, so you pass the body down to
them.

  'We thank you,' burbles the sea
centaur. 'If you wish, we will
give you the power to breathe the
waters, so that you may swim down
to the wreck that lies below, and
take its treasures, those things
that the surface-dwellers hold
dear.'
  `,
GOTO,
"Book1/513.js",
`Accept the offer`,
`
  `,
GOTO,
"Book1/408.js",
`Refuse the offer`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>save("save/bookmark", "Book1/1.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  The approach of dawn has turned
the sky a milky grey-green, like
jade. The sea is a luminous pane
of silver. Holding the tiller of
your sailing boat, you keep your
gaze fixed on the glittering
constellation known as the Spider.
It marks the north, and by keeping
it to port you know you are still
on course.

  The sun appears in a trembling
burst of red fire at the rim of
the world. Slowly the chill of
night gives way to brazen warmth.
You lick your parched lips. There
is a little water sloshing in the
bottom of the barrel by your feet,`,
MARK,
`but not enough to see you through
another day.

  Sealed in a scroll case tucked
into your jerkin is the parchment
map your grandfather gave you on
his death-bed. You remember his
stirring tales of far sea voyages,
of kingdoms beyond the western
horizon, of sorcerous islands and
ruined palaces filled with
treasure. As a child you dreamed
of nothing else but the magical
quests that were in store if you
too became an adventurer.

  You never expected to die in an
open boat before your adventures
even began.
`,
MARK,
`  Securing the tiller, you unroll
the map and study it again. You
hardly need to. Every detail is
etched into your memory by now.
According to your reckoning, you
should have reached the east coast
of Harkuna, the great northern
continent, days ago.

  A pasty grey blob splatters on to
the map. After a moment of stunned
surprise, you look up and curse
the seagull circling directly
overhead. Then it strikes you -
where there's a seagull, there may
be land.

  You leap to your feet and scan
the horizon. Sure enough, a line
of white cliffs lie a league to`,
MARK,
`the north. Have you been sailing
along the coast all this time
without realising the mainland was
so close?

  Steering towards the cliffs, you
feel the boat judder against rough
waves. A howling wind whips plumes
of spindrift across the sea.
Breakers pound the high cliffs.
The tiller is yanked out of your
hands. The little boat is spun
around, out of control, and goes
plunging in towards the coast.

  You leap clear at the last
second. There is the snap of
timber, the roaring crescendo of
the waves - and then silence as
you go under. Striking out wildly,`,
MARK,
`you try to swim clear of the
razor-sharp rocks. For a while the
undertow threatens to drag you
down, then suddenly a wave catches
you and flings you contemptuously
up on to the beach.

  Battered and bedraggled you lie
gasping for breath until you hear
someone walking along the shore
towards you. Wary of danger, you
lose no time in getting to your
feet. Confronting you is an old
man clad in a dirty loin-cloth.
His eyes have a feverish bright
look that is suggestive of either
a mystic or a madman.

  Now`,
GOTO,
"Book1/20.js",
`go to 20`,
`.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep># Charlotte et les 64 Miauleurs

## Introduction

Charlotte was walking back to her home.

That day, she decided to take a shortcut through the old industrial area.

It's a district with a lot of abandoned buildings, made mostly out of bricks, devoid of life.
... or so she thought.

She heard a lot of meowing inside an old factory. Cats kept calling her.
After a short passage through the pet store, She decided to enter the factory. Well-prepared.

It's time to feed those 64 cats!

But the old factories aren't exactly a safe place...

## Controls

- LEFT/RIGHT - Move Charlotte to the left or right.
- A - Charlotte will jump.

## Gameplay

Guide Charlotte through 10 levels in increasing difficulty!

Each level will features the following elements:

- Charlotte is your character. She'll appear out of a door.
- Meowing Cats are the one that should be visited.
  - Just touch them briefly with Charlotte to do so!
  - They'll purr and sleep after a few instants.
- The Cat Door is your exit to the next level.
  - It'll open when all the Cats are sleeping.
  - Just touch them after!
- Catnips are bonus items.
  - They're usually trickier to get.
  - There are 13 of them to retrieve.
  - They're worth a lot of points!
- Pits are bad things to fall into.
  - If you do, it'll reload the current level, so you'll have to redo it!

## Scoring

A timer is launched when you first move your character in Level 1.
When you're through the Level 10, you'll have a summary about your times, collected catnips - and the resulting score.

- The par-time is 4 minutes for the 10 levels.
- Each 2ms below the par-time is worth a point.
- Each collected catnip is worth 5000 points.
  - So you probably don't want to spend more than 10s getting a single catnip!

Falling in a pit will not reset the current level's timer!

## Important Notes

- The game will take a long time to compile in Pine2K.
  - 13s on my own device, we've seen 40s as well. It depends a lot on the SD card.
- On the Pine2K version it's designed to run for, it'll use 2040 bytes of PROGMEM.
  - Needless to say, chances are it might break in upcoming Pine2K updates.
- The game cannot be run after another game - some corruptions will ensue.
- Because of that, it cannot run itself again either, so the only way to exit it is to restart the Pokitto.

## Tech Notes & Tools

- FemtoIDE, Aseprite, Tiled were used to create this game.
- It was really challenging to pack that many features into 2K of compiled bytecode (PROGMEM).
- The whole code relies a lot on the weird pointer arithmetics that can be done in Pine2K.
- A lot of weird tricks were also used, on the basis that they would save 4, 8, 12, 16, 24 or more PROGMEM bytes.
  - Such as converting a `if (a && b)` into two if.
  - Or moving around the `var` declaration out of their loops because somehow it saves a bit.
  - The Character Entity is always at the end of the entities array, and it saved a lot of bytes to do so (a fixed pointer costs less than a pointer as parameter).
- The same animation structure was used wherever the entity was animated or not.
- The collision detection is based on the tile identifier - if it's above or equal to 20, it's colliding.

## Special Thanks

- FManga (https://github.com/felipemanga/FemtoIDE) for making Pine2K and FemtoIDE.
- Torbuntu (https://github.com/Torbuntu) for playtesting it.
- Jonne for the amazing device that is the Pokitto (https://pokitto.com - get this cute little device!).
- The Pokitto Community for their general support and enthusiasm.
- The Aseprite team. (https://www.aseprite.org/)
- The Tiled team. (https://www.mapeditor.org/)
- Anyone who contributed from faraway to the involved tools.

## References

- Github Repository - https://github.com/carbonacat/miaou-p2k
<file_sep>save("save/bookmark", "Book1/431.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  You notice that Oliphard has
erected his pavillion over a
verdigris trapdoor set into the
floor.

  'Ah, you have the key, I see!'
says Oliphard. 'Please, be my
guest - use the door.'

  You open it up and climb some
short stairs into a square chamber
with three doors.

  'They are doors of teleportation
- step through and you will be
taken to whatever land is
displayed!' says Oliphard.
`,
MARK,
`  The first door leads to a teeming
city of merchants - Metriciens in
Golnir.`,
GOTO,
"Book2/48.js",
`Turn to paragraph 48 in
Cities of Gold and Glory`,
`if you
enter this door.

  The second door leads to a huge
mountain - Sky Mountain in the
Great Steppes, far to the north.`,
GOTO,
"Book4/185.js",
`
Turn to paragraph 185 in The Plains
of Howling Darkness`,
`if you enter
this door.

  The third door leads to Dweomer,
the City of Sorcerers, on
Sorcerers' Isle.`,
GOTO,
"Book3/100.js",
`Turn to paragraph
100 in Over the Blood-Dark Sea`,
`if
you open this door.

  If you don't want to step through`,
MARK,
`any of the doors,`,
GOTO,
"Book1/656.js",
`go to 656`,
`and
choose again.`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>////////////
// LEVELS //
////////////

// struct Level
// {
//     STRING filename; // [0] - Name of the level's file.
//     int finishedTime; // [1] - Run Timestamp of the completion.
//     bool collectedCatnip; // [2] - If true, the Catnip was collected.
//     int reserved; [3] - ???
// };
const Level_SIZE = 4 * 4;

const levelsStart = file("_run.tmp", null);
const levelsEnd = levelsStart + length(levelsStart) * 4;

if (!file("resources.res")) {
    exit();
}


/////////////
// RESULTS //
/////////////

var collectedCatnips = 0;
var availableCatnips = 0;

for (var level = levelsStart, levelNumber = 1; level[0] != null; level += Level_SIZE, levelNumber++)
{
    collectedCatnips += level[2];
    availableCatnips += level[3];
}

var foundAllCatnips = collectedCatnips == availableCatnips;


/////////////
// SPRITES //
/////////////

// Otherwise the previous file might corrupt the CatGod.
const CatGod1Bitmap = file("CatGod1:8", null);
const CatGod2Bitmap = file("CatGod2:8", null);


////////////////
// ANIMATIONS //
////////////////

// struct Animation
// {
//     AnimationStep* steps;
// }
// struct AnimationStep
// {
//     int untilTimer; // [0] - This steps is applied until timer reaches this value.
//     Bitmap spriteBitmap; // [1] - The bitmap to show. If null, the animation timer is reset.
// }
const animationStepSize = 2 * 4;
const CharacterWalkAnimation =
[
    4, file("Character1:8", null),
    8, file("Character2:8", null),
    12, file("Character3:8", null),
    16, file("Character4:8", null),
    20, file("Character5:8", null),
    24, file("Character6:8", null),
    99999, null
];

const CatNipAnimation =
[
    1, file("CatNip:8", null),
    99999, null
];

const BubbleMiaouAnimation =
[
    4, file("BubbleMiaou1", null),
    8, file("BubbleMiaou2", null),
    12, file("BubbleMiaou3", null),
    99999, null
];

const ThreeDotsAnimation =
[
    4, file("ThreeDots1:8", null),
    8, file("ThreeDots2:8", null),
    12, file("ThreeDots3:8", null),
    99999, null
];

const EndAnimation =
[
    4, file("End1:8", null),
    8, file("End2:8", null),
    12, file("End3:8", null),
    99999, null
];


function renderAnimation(x256, y256, animation, animationTimer)
{
    var animation = animation;
    var animationStep = animation;
    
    while (animationTimer >= animationStep[0])
        animationStep += animationStepSize;
    
    var animationBitmap = animationStep[1];
        
    if (animationBitmap == null)
    {
        animationTimer = 0;
        animationBitmap = animation[1];
    }
    sprite(x256 >> 8 - 7, y256 >> 8 - 15, animationBitmap);
    return animationTimer;
}


/////////
// MAP //
/////////

const mapTiles = [
    // 0-3
    file("MiaouEndTiles1:8", null),
    file("MiaouEndTiles2:8", null),
    file("MiaouEndTiles3:8", null),
    file("MiaouEndTiles4:8", null),
    
    // 4-7
    file("MiaouEndTiles5:8", null),
    file("MiaouEndTiles6:8", null),
    file("MiaouEndTiles7:8", null),
    file("MiaouEndTiles8:8", null),
    
    // 8-11
    file("MiaouEndTiles9:8", null),
    file("MiaouEndTiles10:8", null),
    file("MiaouEndTiles11:8", null),
    file("MiaouEndTiles12:8", null)
];

const mapWidth = 28;
const mapHeight = 22;

const levelMapData = file("end.map", null);
const levelMap = levelMapData + 4;

function loadMap()
{
    var levelMapI = levelMap;
    
    for (var tileJ = 0; tileJ < mapHeight; tileJ++)
        for (var tileI = 0; tileI < mapWidth; tileI++, levelMapI++)
            tile(tileI, tileJ, mapTiles[peek(levelMapI)]);
}


////////////////
// CINEMATICS //
////////////////

var cinematicsTimer = 0;

function cinematicsUpdateStage1()
{
    if (mainCharacterX256 < MC_STAGE2X256)
    {
        mainCharacterX256 += MC_WALKSPEED;
        if (mainCharacterX256 >= MC_STAGE2X256)
        {
            mainCharacterX256 = MC_STAGE2X256;
            catnipsEnd = catnipsStart + min(catnipsCapacity, collectedCatnips) * Catnip_SIZE;
            cinematicsUpdate = cinematicsUpdateStage2;
        }
        mainCharacterAnimationTimer++;
    }
}

function cinematicsUpdateStage2()
{
    // Are all the catnips stable enough?
    for (var catnip = catnipsStart; catnip != catnipsEnd; catnip += Catnip_SIZE)
    {
        // If a catnip is too fast, or too high, we don't proceed.
        if ((abs(catnip[3]) > 128) || (catnip[2] > 1*256))
            return ;
    }
    if (foundAllCatnips)
        cinematicsUpdate = cinematicsUpdateStage3;
    else
        cinematicsUpdate = cinematicsUpdateStage3A;
}

function cinematicsUpdateStage3()
{
    catGodY256 = CATGOD_FINALY256 + (catGodY256 - CATGOD_FINALY256) * 31 / 32;
    if (abs(catGodY256 - CATGOD_FINALY256) < 256)
    {
        cinematicsTimer = 0;
        cinematicsUpdate = cinematicsUpdateStage4;
    }
}

function cinematicsUpdateStage4()
{
    cinematicsTimer++;
    if (cinematicsTimer > 30)
    {
        catGodBitmap = CatGod2Bitmap;
        cinematicsTimer = 0;
        cinematicsUpdate = cinematicsUpdateStage5;
    }
}

const THREEDOTS_X256 = 152 * 256;
const THREEDOTS_Y256 = 170 * 256;

function cinematicsUpdateStage3A()
{
    bubbleTimer = renderAnimation(THREEDOTS_X256, THREEDOTS_Y256, ThreeDotsAnimation, bubbleTimer + 1);
    cinematicsTimer++;
    if (cinematicsTimer > 30)
    {
        cinematicsTimer = 0;
        cinematicsUpdate = cinematicsUpdateStage4A;
    }
}

function cinematicsUpdateStage4A()
{
    mainCharacterMirrored = true;
    if (mainCharacterX256 > MC_INITIALX256)
    {
        mainCharacterX256 -= MC_WALKSPEED;
        if (mainCharacterX256 <= MC_INITIALX256)
        {
            mainCharacterX256 = MC_INITIALX256;
            cinematicsUpdate = cinematicsUpdateStage5;
        }
        mainCharacterAnimationTimer++;
    }
}

function cinematicsUpdateStage5()
{
    cinematicsTimer++;
    if (cinematicsTimer > 30)
    {
        // Clears the tilemap.
        color(0);
        for (var tileJ = 0; tileJ < mapHeight; tileJ++)
            for (var tileI = 0; tileI < mapWidth; tileI++)
                tile(tileI, tileJ, null);
        if (foundAllCatnips)
            cinematicsUpdate = cinematicsUpdateStage6;
        else
            cinematicsUpdate = cinematicsUpdateStage7;
        cinematicsTimer = 0;
        update2 = updateBlackScene;
    }
}

var bubbleTimer = 0;

const BUBBLE_X256 = 130 * 256;
const BUBBLE_Y256 = 39 * 256;

function cinematicsUpdateStage6()
{
    cinematicsTimer++;
    if (cinematicsTimer == 30)
        music("miaouslow.raw");
    if (cinematicsTimer >= 30)
    {
        io("SCALE", 2);
        bubbleTimer = renderAnimation(BUBBLE_X256, BUBBLE_Y256, BubbleMiaouAnimation, bubbleTimer + 1);
        io("SCALE", 1);
    }
    // At 60fps, it's roughly a bit more 2s.
    if (cinematicsTimer >= 130)
    {
        cinematicsUpdate = cinematicsUpdateStage7;
        cinematicsTimer = 0;
    }
}

const END_X256 = ((220 - 60) / 2 + 7) * 256;
const END_Y256 = ((176 - 25) / 2 + 15) * 256;

function cinematicsUpdateStage7()
{
    bubbleTimer = renderAnimation(END_X256, END_Y256, EndAnimation, bubbleTimer + 1);
}

var cinematicsUpdate = cinematicsUpdateStage1;


////////////////////
// MAIN CHARACTER //
////////////////////

const MC_INITIALX256 = -16 * 256;
const MC_Y256 = 165 * 256;
const MC_STAGE2X256 = 40 * 256;

const MC_WALKSPEED = 128;

var mainCharacterX256 = MC_INITIALX256;
var mainCharacterY256 = 165 * 256;
var mainCharacterAnimation = CharacterWalkAnimation;
var mainCharacterAnimationTimer = 0;
var mainCharacterMirrored = false;


function updateMainCharacter()
{
    mirror(mainCharacterMirrored);
    mainCharacterAnimationTimer = renderAnimation(mainCharacterX256, mainCharacterY256, mainCharacterAnimation, mainCharacterAnimationTimer);
    mirror(false);
}


/////////////
// CATNIPS //
/////////////

// struct CatNip
// {
//     int x256; // [0] - X on the screen *256.
//     int y256; // [1] - Y on the screen *256, for z256 = 0.
//     int z256; // [2] - Height of the catnip.
//     int dz256; // [3] - Altitude velocity of the catnip.
// }
const Catnip_SIZE = 4 * 4;
const catnipsStart = [
  61*256, 153*256, 351*256, 0,
  93*256, 155*256, 403*256, 0,
  73*256, 157*256, 282*256, 0,
  77*256, 159*256, 335*256, 0,
  83*256, 161*256, 240*256, 0,
  66*256, 163*256, 256*256, 0,
  98*256, 163*256, 392*256, 0,
  55*256, 165*256, 327*256, 0,
  73*256, 167*256, 210*256, 0,
  80*256, 169*256, 309*256, 0,
  91*256, 171*256, 403*256, 0,
  67*256, 174*256, 512*256, 0
];
var catnipsEnd = catnipsStart;
const catnipsCapacity = length(catnipsStart) / 4;
const CN_GRAVITY_ACCEL = -64;
const CN_BOUNCE_256 = 64;
const CN_BUMP_Z256MIN = 512;

function updateCatNips()
{
    for (var catnip = catnipsStart; catnip != catnipsEnd; catnip += Catnip_SIZE)
    {
        catnip[3] += CN_GRAVITY_ACCEL;
        catnip[2] += catnip[3];
        if (catnip[2] < 0)
        {
            catnip[2] = - catnip[2];
            if (catnip[3] < -CN_BUMP_Z256MIN)
                music("bump.raw");
            catnip[3] = -catnip[3] * CN_BOUNCE_256 / 256;
        }
        renderAnimation(catnip[0], catnip[1] - catnip[2], CatNipAnimation, 0);
    }
}


/////////////
// CAT GOD //
/////////////

const CATGOD_X256 = 130 * 256;
const CATGOD_INITIALY256 = 177 * 256;
const CATGOD_FINALY256 = 54 * 256;

var catGodX256 = CATGOD_X256;
var catGodY256 = CATGOD_INITIALY256;
var catGodBitmap = CatGod1Bitmap;

function updateCatGod()
{
    io("SCALE", 2);
    sprite(catGodX256 >> 8, catGodY256 >> 8, catGodBitmap);
    io("SCALE", 1);
}


///////////////////
// INIT & UPDATE //
///////////////////

color(0);
tileshift(0, 0);
loadMap();

var update2 = updateScene;

function updateScene()
{
    cinematicsUpdate();
    updateMainCharacter();
    updateCatGod();
    updateCatNips();
}

function updateBlackScene()
{
    cinematicsUpdate();
}

function update()
{
    update2();
}<file_sep>io("FORMAT", 0, 0);


////////////
// LEVELS //
////////////

// struct Level
// {
//     STRING filename; // [0] - Name of the level's file.
//     int finishedTime; // [1] - Run Timestamp of the completion.
//     bool collectedCatnip; // [2] - If true, the Catnip was collected.
//     int reserved; [3] - ???
// };
const Level_SIZE = 4 * 4;

const levelsStart = file("_run.tmp", null);
const levelsEnd = levelsStart + length(levelsStart) * 4;


////////////
// SCORES //
////////////

const SCORE_CATNIP_POINTS = 5000;
const SCORE_PAR_TIME_MS = 240000;
const SCORE_EXTRA_MS_DIVIDER = 2;

const TIMER_STEP_DURATION = 5;
var timer = 0;

var collectedCatnips = 0;
var availableCatnips = 0;
var score = 0;

function init()
{
    var cumulatedTime = 0;
    
    for (var level = levelsStart, levelNumber = 1; level[0] != null; level += Level_SIZE, levelNumber++)
    {
        collectedCatnips += level[2];
        availableCatnips += level[3];
        cumulatedTime = level[1];
    }
    score = collectedCatnips * SCORE_CATNIP_POINTS;
    if (cumulatedTime < SCORE_PAR_TIME_MS)
        score += (SCORE_PAR_TIME_MS - cumulatedTime) / SCORE_EXTRA_MS_DIVIDER;
    highscore(score);
}

io("FILLER", 1, "TEXT", "5x7");
cursor(0, 0);
color(7);
init();

function update()
{
    var cumulatedTime = 0
    var expectedTimer = TIMER_STEP_DURATION;
    
    if (timer == 0)
        print("You fed of all the cats!\n\n");
    
    expectedTimer += TIMER_STEP_DURATION;
    if (timer == 0)
        print("Times:\n");
    expectedTimer += TIMER_STEP_DURATION;
    for (var level = levelsStart, levelNumber = 1; level[0] != null; level += Level_SIZE, levelNumber++, expectedTimer += TIMER_STEP_DURATION)
    {
        if (timer == expectedTimer)
        {
            print("- #"); printNumber(levelNumber); print(": ");
            printNumber(level[1] - cumulatedTime);print("ms\n");
        }
        cumulatedTime = level[1];
    }
    expectedTimer += TIMER_STEP_DURATION;
    if (timer == expectedTimer)
    {
        print(" \nTOTAL: "); printNumber(cumulatedTime); print("ms");
    }
    expectedTimer += TIMER_STEP_DURATION;
    if ((collectedCatnips) && (timer == expectedTimer))
    {
        print(" \n   ... and "); printNumber(collectedCatnips); print("/"); printNumber(availableCatnips); print(" catnips!\n\n");
    }
    expectedTimer += TIMER_STEP_DURATION;
    if (timer == expectedTimer)
    {
        print("FINAL SCORE: "); printNumber(score); print(" \n\n");
    }
    
    expectedTimer += TIMER_STEP_DURATION;
    if (timer == expectedTimer)
        print("Press A to continue.\n");
    
    timer++;
    if ((justPressed("A")) && (timer >= expectedTimer))
        exec("preend.js");
}<file_sep>if (!file("resources.res")) {
    exit();
}


//////////
// MAPS //
//////////

// struct Level
// {
//     STRING filename; // [0] - Name of the level's file.
//     int finishedTime; // [1] - Run Timestamp of the completion.
//     int collectedCatnip; // [2]
//     int availableCatnips; [3]
// };
const Level_SIZE = 4 * 4;

// List of Levels.
// This list is also sent to results.js and end.js.
const levels = [
    // "debug01.map", 0, 0, 0,
    // "debug02.map", 0, 0, 0,
    
    "level01.map", 0, 0, 0,
    "level02.map", 0, 0, 0,
    "level03.map", 0, 0, 0,
    "level04.map", 0, 0, 0,
    "level05.map", 0, 0, 0,
    
    "level06.map", 0, 0, 0,
    "level07.map", 0, 0, 0,
    "level08.map", 0, 0, 0,
    "level09.map", 0, 0, 0,
    "level10.map", 0, 0, 0,
    
    null, 0, 0, 0
];

var currentLevel = levels;


////////////////
// OBJECTIVES //
////////////////

var catsCount;
var sleepingCatsCount;
var beginTime = 0;


////////////////
// ANIMATIONS //
////////////////

// struct AnimationStep
// {
//     int untilTimer; // [0] - This steps is applied until timer reaches this value.
//     Bitmap spriteBitmap; // [1] - The bitmap to show. If null, the animation timer is reset.
// }
const AnimationStep_SIZE = 2*4;

const characterWalkAnimation =
[
    4, file("Character1:8", null),
    8, file("Character2:8", null),
    12, file("Character3:8", null),
    16, file("Character4:8", null),
    20, file("Character5:8", null),
    24, file("Character6:8", null),
    99999, null
];

const catIdleAnimation =
[
    8, file("Cat1:8", null),
    16, file("Cat2:8", null),
    24, file("Cat3:8", null),
    32, file("Cat4:8", null),
    99999, null
];

const catFallingAsleepAnimationEnd = 40;
const catFallingAsleepAnimation =
[
    8, file("Cat5:8", null),
    16, file("Cat6:8", null),
    24, file("Cat7:8", null),
    catFallingAsleepAnimationEnd, file("Cat8:8", null),
    99999, null
];

const catSleepingAnimation =
[
    8, file("Cat9:8", null),
    16, file("Cat10:8", null),
    24, file("Cat11:8", null),
    32, file("Cat12:8", null),
    40, file("Cat13:8", null),
    48, file("Cat14:8", null),
    56, file("Cat15:8", null),
    99999, null
];

const bubbleMiaouAnimation =
[
    4, file("BubbleMiaou1", null),
    8, file("BubbleMiaou2", null),
    12, file("BubbleMiaou3", null),
    99999, null
];

const bubbleRonronAnimation =
[
    4, file("BubbleRonron1", null),
    8, file("BubbleRonron2", null),
    12, file("BubbleRonron3", null),
    99999, null
];

const bubbleDeathAnimation =
[
    4, file("BubbleDeath1", null),
    8, file("BubbleDeath2", null),
    12, file("BubbleDeath3", null),
    99999, null
];

const doorClosedAnimation =
[
    1, file("Door1:8", null),
    99999, null
];

const catNipAnimation =
[
    1, file("CatNip:8", null),
    99999, null
];

const doorOpeningAnimationEnd = 8;
const doorOpeningAnimation =
[
    2, file("Door3:8", null),
    4, file("Door4:8", null),
    6, file("Door5:8", null),
    doorOpeningAnimationEnd, file("Door6:8", null),
    99999, null
];

const doorOpenAnimation =
[
    1, file("Door7:8", null),
    2, file("Door8:8", null),
    99999, null
];


/////////
// MAP //
/////////

const TILE_MC = 16;
const TILE_CAT = 17;
const TILE_DOOR = 18;
const TILE_TREASURE = 19;
const TILE_FREE_MAX = 20;

const emptyBGTile = file("MiaouTiles1:8", null);
const defaultBGTile = file("MiaouTiles2:8", null);
const closedToorBLTile = file("MiaouTiles11:8", null);

const mapTiles = [
    // 0-3
    emptyBGTile,
    defaultBGTile,
    file("MiaouTiles3:8", null),
    file("MiaouTiles4:8", null),
    
    // 4-7
    file("MiaouTiles5:8", null),
    file("MiaouTiles6:8", null),
    file("MiaouTiles7:8", null),
    file("MiaouTiles8:8", null),
    
    // 8-11
    file("MiaouTiles9:8", null),
    file("MiaouTiles10:8", null),
    closedToorBLTile,
    file("MiaouTiles12:8", null),
    
    // 12-15
    file("MiaouTiles13:8", null),
    file("MiaouTiles14:8", null),
    file("MiaouTiles15:8", null),
    file("MiaouTiles16:8", null),
    
    //16-19
    closedToorBLTile, // Main Character
    defaultBGTile, // Cat
    defaultBGTile, // Door
    defaultBGTile, // (Reserved)
    
    // 20-23
    emptyBGTile, // This is an invisible block tile.
    file("MiaouTiles22:8", null),
    file("MiaouTiles23:8", null),
    file("MiaouTiles24:8", null),
    
    // 24-27
    file("MiaouTiles25:8", null),
    file("MiaouTiles26:8", null),
    file("MiaouTiles27:8", null),
    file("MiaouTiles28:8", null),
    
    // 28-31
    file("MiaouTiles29:8", null),
    file("MiaouTiles30:8", null),
    file("MiaouTiles31:8", null),
    file("MiaouTiles32:8", null),
    
    // 32-35
    file("MiaouTiles33:8", null),
    file("MiaouTiles34:8", null),
    file("MiaouTiles35:8", null),
    file("MiaouTiles36:8", null),
    
    // 36-39
    file("MiaouTiles37:8", null),
    file("MiaouTiles38:8", null),
    file("MiaouTiles39:8", null),
    file("MiaouTiles40:8", null),
    
    // 40-43
    file("MiaouTiles41:8", null),
    file("MiaouTiles42:8", null),
    file("MiaouTiles43:8", null),
    file("MiaouTiles44:8", null)
];

const entityAnimationForTile = [
    // 0-3
    null,
    null,
    null,
    null,
    
    // 4-7
    null,
    null,
    null,
    null,
    
    // 8-11
    null,
    null,
    null,
    null,
    
    // 12-15
    null,
    null,
    null,
    null,
    
    //16-19
    characterWalkAnimation,
    catIdleAnimation,
    doorClosedAnimation,
    catNipAnimation,
    
    // 20-23
    null,
    null,
    null,
    null,
    
    // 24-27
    null,
    null,
    null,
    null,
    
    // 28-31
    null,
    null,
    null,
    null,
    
    // 32-35
    null,
    null,
    null,
    null,
    
    // 36-39
    null,
    null,
    null,
    null
];

const mapWidth = 28;
const mapHeight = 22;
const mapFullHeight = 26;
const mapPitY256 = 192 * 256;

const levelMapData = new Array(1 + (mapWidth * mapFullHeight + 3) / 4); // 4 (header) + 28*22
const levelMap = levelMapData + 4;

function loadMap()
{
    var levelName = currentLevel[0];
    
    if (levelName == null)
    {
        save("_run.tmp", levels);
        exec("results.js");
        return ;
    }
    
    var levelMapI = levelMap;
    
    file(levelName, levelMapData);
    catsCount = 0;
    sleepingCatsCount = 0;
    currentLevel[2] = 0;
    currentLevel[3] = 0;
    for (var i = 0; i < length(entitiesStart); i++)
        entitiesStart[i] = 0;
    
    for (var tileJ = 0; tileJ < mapHeight; tileJ++)
        for (var tileI = 0; tileI < mapWidth; tileI++, levelMapI++)
        {
            var tileIdentifier = peek(levelMapI);
            var entityUpdate = null;
            
            if (tileIdentifier == TILE_MC)
                entityUpdate = updateMainCharacter;
            if (tileIdentifier == TILE_CAT)
            {
                entityUpdate = updateCat;
                catsCount++;
            }
            if (tileIdentifier == TILE_DOOR)
                entityUpdate = updateDoor;
            if (tileIdentifier == TILE_TREASURE)
            {
                currentLevel[3]++;
                entityUpdate = updateTreasure;
            }
            
            // If we're using a null entityUpdate, it won't be adding any actual entity.
            // addEntity(entityUpdateForTile[tileIdentifier], (tileI * 8 + 3) * 256, (tileJ * 8 + 7) * 256, entityAnimation);
            addEntity(entityUpdate, (tileI << 3 + 7) << 8, (tileJ << 3 + 7) << 8, entityAnimationForTile[tileIdentifier]);
            tile(tileI, tileJ, mapTiles[tileIdentifier]);
        }
}


//////////////
// ENTITIES //
//////////////

// struct Entity
// {
//     function Update; // [0] - Function called at each frame.
//     int x256; // [1] - Center of the entity.
//     int y256; // [2] - Bottom of the entity.
//     int dx256; // [3] - Horizontal velocity.
//     int dy256; // [4] - Vertical velocity.
//     Animation animation; // [5] - Animation compound.
//     int animationTimer; // [6] - Timer for the animation.
//     bool mirror; // [7] - If true, the sprite will be mirrored.
//     Unknown reserved[4]; // [8-11] - For extended structures.
// }
// Note: To search for field call, search for ntity[0] for ntity.update, ntity[1] for ntity.x256, etc.
const Entity_FIELDS = 10;
const Entity_SIZE = Entity_FIELDS * 4;

const entitiesCapacity = 24;
const entitiesStart = Array(entitiesCapacity * Entity_FIELDS);
const entitiesEnd = entitiesStart + entitiesCapacity * Entity_SIZE;
const mcEntity = entitiesEnd - Entity_SIZE;
// Every entity got its distance to the MC calculated even if they're not using it - costs less PROGMEM that way.
var distanceToMC256 = 0;

function addEntity(update, x256, y256, entityAnimation)
{
    for (var i = 0; i < entitiesCapacity * Entity_FIELDS; i += Entity_FIELDS)
        if (entitiesStart[i] == null)
        {
            if (update == updateMainCharacter)
                i = entitiesCapacity * Entity_FIELDS - Entity_FIELDS;

            // We're asserting we always have enough free entities!
            for (var j = 0; j < Entity_FIELDS; j++)
                entitiesStart[i + j] = 0;
            entitiesStart[i] = update;
            entitiesStart[i + 1] = x256;
            entitiesStart[i + 2] = y256;
            entitiesStart[i + 5] = entityAnimation;
            break ;
        }
}

function renderEntity(entity)
{
    var animation = entity[5];
    var animationStep = animation;
    
    while (entity[6] >= animationStep[0])
        animationStep += AnimationStep_SIZE;
    
    var animationBitmap = animationStep[1];
        
    if (animationBitmap == null)
    {
        entity[6] = 0;
        animationBitmap = animation[1];
    }
    mirror(entity[7]);
    sprite(entity[1] >> 8 - 7, entity[2] >> 8 - 15, animationBitmap);
}


////////////////////
// MAIN CHARACTER //
////////////////////

// struct Player: Entity
// {
//     int jumpTimer; // [8] - Extra.
// }


// struct TestPoint
// {
//     int rx256; [0]
//     int ry256; [1]
// }

const characterTestPointsStart = [
    -3*256, 0*256,
    -3*256, -8*256,
    -3*256, -14*256,
    +3*256, 0*256,
    +3*256, -8*256,
    +3*256, -14*256
];

const MC_JUMP_ACCEL256 = -512;
const MC_JUMP_MAINTAINED_ACCEL256 = -32;
const MC_JUMP_MAX = 9;
const MC_GRAVITY_ACCEL256 = 32;
const MC_VERT_BRAKE256 = 256;

const MC_HORIZ_ACCEL256 = 32;
const MC_HORIZ_BRAKE256 = 240;
const MC_DEATH_DURATION = 30;

function mcIsColliding(movementX256, movementY256)
{
    var entityX256 = mcEntity[1] + movementX256;
    var entityY256 = mcEntity[2] + movementY256;
    
    for (var i = 0; i < length(characterTestPointsStart); i += 2)
    {
        var tileI = (entityX256 + characterTestPointsStart[i]) >> 11; // / (8 * 256)
        var tileJ = (entityY256 + characterTestPointsStart[i + 1]) >> 11; // / (8 * 256)
        var tileIndex = tileJ * mapWidth + tileI;
        
        if (peek(levelMap + tileIndex) >= TILE_FREE_MAX)
            return true;
    }
    return false;
}

// Assumes mcEntity == entity.
function updateMainCharacter()
{
    var entityDX256 = mcEntity[3];
    var entityDY256 = mcEntity[4];
    var entityJumpTimer = mcEntity[8];
    
    for (var frame = 0; frame < 2; frame++)
    {
        var accel256 = (pressed("RIGHT") - pressed("LEFT")) * MC_HORIZ_ACCEL256;
        
        if (accel256 != 0)
            mcEntity[7] = accel256 < 0;
        entityDX256 += accel256;
        if (abs(entityDX256) >= 32)
        {
            mcEntity[6]++;
            if (beginTime == 0)
                beginTime = time();
        }
    
        // gravity
        entityDY256 += MC_GRAVITY_ACCEL256;
        // Maintained jump.
        if (entityJumpTimer > 0)
        {
            entityJumpTimer--;
            if (pressed("A"))
                entityDY256 += MC_JUMP_MAINTAINED_ACCEL256;
        }
        
        if (mcIsColliding(entityDX256, 0))
            entityDX256 = 0;
        else
        {
            mcEntity[1] += entityDX256;
            entityDX256 = entityDX256 * MC_HORIZ_BRAKE256 / 256;
        }
        if (mcIsColliding(0, entityDY256))
        {
            entityDY256 = 0;
            // Cancels any jump maintenance.
            entityJumpTimer = 0;
        }
        else
        {
            mcEntity[2] += entityDY256;
            entityDY256 = entityDY256 * MC_VERT_BRAKE256 / 256;
        }
    
        // Jump ?
        // && costs more.
        if (mcIsColliding(0, 1*256))
        {
            if (entityDY256 >= 0)
            {
                if (pressed("A"))
                {
                    entityDY256 = MC_JUMP_ACCEL256;
                    entityJumpTimer = MC_JUMP_MAX;
                }
            }
        }
    }
    
    if (mcEntity[2] > mapPitY256)
    {
        mcEntity[0] = updateDeadMainCharacter;
        addTempDecoration(mcEntity, bubbleDeathAnimation);
        music("fall.raw");
    }
    mcEntity[3] = entityDX256;
    mcEntity[4] = entityDY256;
    mcEntity[8] = entityJumpTimer;
    
    if (mcEntity[9])
    {
        currentLevel[1] = time() - beginTime;
        currentLevel += Level_SIZE;
        loadMap();
    }
}

function updateDeadMainCharacter()
{
    mcEntity[8]++;
    if (mcEntity[8] > MC_DEATH_DURATION)
        loadMap();
}


//////////
// CATS //
//////////

// struct Cat: Entity
// {
//     int meowTimer; // [8] - Extra.
// }
const CAT_MC_DISTANCE256 = 8 * 256;
const CAT_MEOW_TIMER_MAX = 90;
const CAT_OFFSET_TO_MEOW = 16 * 256;

var entityMeowTimer = 0;

function updateCat(entity)
{
    var entityAnimation = entity[5];
    var entityAnimationTimer = entity[6];
    
    entityAnimationTimer++;
    
    // && costs more.
    if (entityAnimation == catIdleAnimation)
    {
        if (entityMeowTimer <= 0)
            if (!random(0, 8))
            {
                entityMeowTimer = CAT_MEOW_TIMER_MAX;
                music("miaou.raw");
                addTempDecoration(entity, bubbleMiaouAnimation);
            }
        if (distanceToMC256 <= CAT_MC_DISTANCE256)
        {
            entityAnimation = catFallingAsleepAnimation;
            entityAnimationTimer = 0;
        }
    }
    // && costs more.
    if (entityAnimation == catFallingAsleepAnimation)
    {
        if (entityAnimationTimer == catFallingAsleepAnimationEnd)
        {
            entityAnimation = catSleepingAnimation;
            sleepingCatsCount++;
            addTempDecoration(entity, bubbleRonronAnimation);
        }
    }
    
    entity[5] = entityAnimation;
    entity[6] = entityAnimationTimer;
}


///////////
// DOORS //
///////////

// struct Door: Entity
// {
// }
const DOOR_MC_DISTANCE256 = 8 * 256;

function updateDoor(entity)
{
    var entityAnimation = entity[5];
    var entityAnimationTimer = entity[6];
    
    entityAnimationTimer++;
    // && costs more.
    if (entityAnimation == doorClosedAnimation)
    {
        if (sleepingCatsCount == catsCount)
        {
            entityAnimation = doorOpeningAnimation;
            entityAnimationTimer = 0;
        }
    }
    // && costs more.
    if (entityAnimation == doorOpeningAnimation)
        if (entityAnimationTimer == doorOpeningAnimationEnd)
            entityAnimation = doorOpenAnimation;
    // Assigning it directly would save 4 bytes, but at the cost of not having multiple doors.
    // && costs more.
    if (entityAnimation == doorOpenAnimation)
        if (distanceToMC256 < DOOR_MC_DISTANCE256)
            mcEntity[9] = true; // Tells to go to the next level.
    entity[5] = entityAnimation;
    entity[6] = entityAnimationTimer;
}


/////////////////////
// TEMP DECORATION //
/////////////////////

// // A decoration that vanishes after a while.
// struct TempDecoration: Entity
// {
//     int deathTimer; // [8] - If less than 30, the deco will vanish.
// }

function updateTempDecoration(entity)
{
    entity[6]++;
    entity[8]--;
    if (entity[8] < -30)
        entity[0] = null;
}

function addTempDecoration(entity, animation)
{
    addEntity(updateTempDecoration, entity[1], entity[2] - CAT_OFFSET_TO_MEOW, animation);
}


//////////////
// TREASURE //
//////////////

// // A treasure that vanishes after being picked up by the MC
// struct Treasure: Entity
// {
// }
const TREASURE_MC_DISTANCE256 = 8 * 256;

function updateTreasure(entity)
{
    if (distanceToMC256 < TREASURE_MC_DISTANCE256)
    {
        entity[0] = null;
        currentLevel[2]++;
    }
}


///////////////////
// INIT / UPDATE //
///////////////////

color(0);
tileshift(0, 0);
loadMap();

function update()
{
    entityMeowTimer--;
    for (var entity = entitiesStart; entity != entitiesEnd; entity += Entity_SIZE)
        if (entity[0] != null)
        {
            distanceToMC256 = abs(entity[1] - mcEntity[1]) + abs(entity[2] - mcEntity[2]);
            entity[0](entity);
            renderEntity(entity);
        }
}<file_sep>save("save/bookmark", "Book1/100.js");

const MARK = 0;
const GOTO = 1;
const text = [
MARK,
`

  Marlock City is a huge sprawling
metropolis, enclosed in a
fortified wall said to have been
built one thousand years ago by
the ancient Shadar Empire. It is
the capital city of Sokara.
Marlock City was once known as
Sokar, until General Grieve
Marlock led the army in bloody
revolt against the old king, Corin
VII, and had him executed. The
general renamed the city after
himself - it is now a crime to
call it Sokar.

  The general lives in the old
king's palace, and calls himself
the Protector-General of all`,
MARK,
`Sokara. Whereas the old king was
corrupt, the general rules with a
fist of iron. Some people like the
new regime; others are royalists,
still loyal to Nergan, the heir to
the throne, who has gone into
hiding somewhere.

  Outside the city gates hang the
bodies of many dead people -
labels around their necks read:
'Rebels, executed by the state for
the good of the people'.

  'You'd best behave yourself if
you don't want to end up like one
of them,' grates a guardsman,
nodding towards the swinging
corpses, as you pass through the
great eagle-headed gates of`,
MARK,
`Marlock City.

  You can buy a town house in
Marlock City for 200 Shards.
Owning a house gives you a place
to rest, and to store equipment.
If you buy one, cross off 200
Shards and tick the box by the
town house option.

  To leave Marlock City by sea, or
to buy or sell ships and cargo, go
the harbourmaster.
  `,
GOTO,
"Book1/158.js",
`Visit the Three Rings Tavern`,
`
  `,
GOTO,
"Book1/154.js",
`Visit the temple of Alvir and
Valmir`,
`
  `,
GOTO,
"Book1/71.js",
`Visit the temple of Nagil`,
`
  `,
GOTO,
"Book1/235.js",
`Visit the temple of Sig`,
`
  `,
GOTO,
"Book1/568.js",
`Visit the temple of Elnir`,
`
  `,
GOTO,
"Book1/396.js",
`Visit the market`,
MARK,
`  `,
GOTO,
"Book1/142.js",
`Visit the harbourmaster`,
`
  `,
GOTO,
"Book1/571.js",
`Go to the merchants' guild`,
`
  `,
GOTO,
"Book1/138.js",
`Explore the city`,
`
  `,
GOTO,
"Book1/434.js",
`Visit your town house {box} (if
box ticked)`,
`
  `,
GOTO,
"Book1/535.js",
`Visit the House of Priests`,
`
  `,
GOTO,
"Book1/601.js",
`Visit the general's palace`,
`
  `,
GOTO,
"Book1/377.js",
`Travel east towards Trefoille`,
`
  `,
GOTO,
"Book1/166.js",
`Head south-east towards the
Shadar Tor`,
`
  `,
GOTO,
"Book1/99.js",
`Follow the River Grimm north`,
`
  `,
GOTO,
"Book1/175.js",
`Journey north into the Curstmoor`,
`
  `,
GOTO,
"Book1/579.js",
`Head west to the River Grimm
delta`,
MARK
];

const funcs = new Array(5);
funcs[MARK] = f_mark;
funcs[GOTO] = f_goto;

window(0, 0, 220, 176);
io("FILLER", 3, "TEXT", "5x7");
io("FORMAT", 0, 0);

var index, selection = 0;
var position = 1, mark = 0;
var isLastMark = false;



render();

function f_mark(){
    position = index + 1;
    mark = index;
}

function f_goto(trigger){
    var page = text[index + 1];
    var caption = text[index + 2];
    var selected = ((index - mark) == selection);
    if(selected) print("[");
    else print(" ");
    print(caption);
    if(selected) print("]");
    else print(" ");

    index += 2;

    if(trigger){
        position = -1;
        splash(0, 0, "splash.565");
        window(0, 166, 220, 176);
        io("CLEARTEXT");
        cursor(1, 21);
        print(caption);
        exec(page);
    }
}

function isText(i){
    var line = text[i];
    return line < 0 || line >= length(funcs);
}

function render(){
    if(position < 0)
        return;
    io("CLEARTEXT");
    fill(0);
    cursor(0,0);
    color(32);
    index = position;
    for(; text[index] != MARK; ++index){
        var line = text[index];
        if(isText(index)) print(line);
        else funcs[line](false);
    }
    isLastMark = index == length(text) - 1;
    color(7);
}

function update(){
    if(justPressed("C"))
        exit();

    if(justPressed("A")){
        index = mark + selection;
        funcs[text[index]](true);
        render();
    }

    if(!isLastMark){
        flip(true);
        sprite(220-8, 176-8, builtin("cursor2"));
    }

    var direction = justPressed("DOWN") - justPressed("UP");
    if(direction == 0){
        return;
    }
    var prevPrev = 0;
    var prevMark = 0;
    var prevSelection = -1;
    var curSelection = -1;
    var absSelection = mark + selection + (direction > 0);
    index = (mark + selection) * (direction > 0);
    for( ; (prevSelection != absSelection) && (isText(curSelection) || curSelection < absSelection); ++index){
        if(index >= length(text)) return;
        if(isText(index)) continue;
        prevSelection = curSelection;
        curSelection = index;
        var line = text[index];
        if(line != MARK){
            funcs[line](false);
        } else {
            prevPrev = prevMark;
            prevMark = index;
        }
    }

    if(direction > 0){
        index = curSelection;
    }else{
        if(prevSelection < mark){
            if(mark == 0)
                return;
            index = prevPrev;
            funcs[MARK](false);
        }
        index = prevSelection;
    }

    if(text[index] == MARK){
        if(isLastMark && direction > 0){
            render();
            return;
        }
        selection = 0;
    }else
        selection = index - mark;

    funcs[text[index]](false);

    render();
}
<file_sep>const minX = 6, maxX = 22;
const minY = 0, maxY = 20;
const boardWidth = maxX - minX;
const boardHeight = maxY - minY;
const heart = builtin("sHeart");
const lvl = builtin("sLvl");
const pts = builtin("sPts");
const ledOFF = builtin("shape1");
const ledON = builtin("sBtn");
const paddleColor = 112;
const bgColor = 129;
const ballColor = 119;
var levels = new Array(27);
var paddleX, bx, by, vx, vy;
var dead, won = 0;
var stepTime;
var level = 0;
var score = 0;
var prevScore = 0;
var lives = 3;
var remaining = 0;
var paddleSize = 3;

levels[0] = builtin("icon1");
levels[1] = builtin("icon2");
levels[2] = builtin("icon3");
levels[3] = builtin("icon4");
levels[4] = builtin("icon5");
levels[5] = builtin("icon6");
levels[6] = builtin("icon7");
levels[7] = builtin("icon8");
levels[8] = builtin("icon9");
levels[9] = builtin("icon10");
levels[10] = builtin("icon11");
levels[11] = builtin("icon12");
levels[12] = builtin("icon13");
levels[13] = builtin("icon14");
levels[14] = builtin("icon15");
levels[15] = builtin("icon16");

window(44, 0, 176, 176);
tileshift(2, 0);
reset();

function reset(){
    if(lives < 0){
        highscore(score);
        exit();
        return;
    }
    score = prevScore;
    if(level > 15){
        paddleSize = 2;
    } else if(level > 30){
        paddleSize = 1;
    }
    color(bgColor);
    for(y = 0; y < boardHeight; ++y){
        for(var x = 0; x < boardWidth; ++x){
            tile(x + minX, y + minY, ledOFF);
        }
    }
    remaining = 0;
    pattern(0, -3, levels[level & 0xF]);
    bx = (boardWidth / 2) << 8;
    by = (boardHeight - 2) << 8;
    paddleX = boardWidth / 2;
    vx = (random(4, 10) + level) * 5;
    vy = 0;
    dead = false;
    won = 0;
}

function pattern(x, y, img){
    var w = peek(img++);
    var h = peek(img++);
    x += minX;
    y += minY;
    var t = peek(img, 0);
    if(y < 0){
        img += w * -y;
        h += y;
        y = 0;
    }
    for(var ty = 0; ty < h; ++ty){
        for(var tx = 0; tx < w; ++tx){
            var c = peek(img, tx);
            if(c && (c != t)){
                color(c - 7);
                tile(x + tx, y + ty, ledOFF);
                ++remaining;
            }
        }
        img += w;
    }
}

function hud(){
    color(47);
    sprite(50, 165, lvl);
    cursor(60, 165);
    printNumber(level + 1);
    sprite(110, 165, pts);
    cursor(120, 165);
    printNumber(score);
    color(0);
    for(var i=0; i<lives; ++i)
        sprite(80 + (i * 8), 165, heart);
}

function hitBlock(){
    var tx = ((bx + 128) >> 8) + minX;
    var ty = ((by + 128) >> 8) + minY;
    var c = io("COLOR", tx, ty);
    if(c == bgColor)
        return false;
    if(c != paddleColor){
        color(bgColor);
        tile(tx, ty, ledOFF);
        --remaining;
        ++score;
    } else {
        by -= 255;
        vx += ((bx >> 8) - paddleX) * random(4, 8) << 3;
        vx >>= 1;
    }
    io("VOLUME", 127);
    io("DURATION", 100);
    sound(random(44, 47));
    return true;
}

function update(){
    if(pressed("C"))
        exit();

    hud();

    if(pressed("LEFT")){
        paddleX--;
        if( (paddleX - paddleSize) < 0 ){
            paddleX = paddleSize;
        }
    }else if(pressed("RIGHT")){
        paddleX++;
        if( (paddleX + paddleSize) >= boardWidth ){
            paddleX = (boardWidth - 1) - paddleSize;
        }
    }else if(pressed("UP") && (vy == 0)){
        vy = -(20 + level) * 7;
    }

    for(var i=0; i<boardWidth; ++i){
        var led = ledON;
        if( (i < (paddleX - paddleSize)) || (i > (paddleX + paddleSize)) ){
            led = ledOFF;
            color(bgColor);
        }else{
            color(paddleColor);
        }
        tile(i + minX, minY + (boardHeight - 1), led);
    }

    by += vy;
    if( by < 0 ){
        by = 0;
        vy = -vy;
    } else if( ((by + 128) >> 8) >= boardHeight ){
        --lives;
        reset();
        return;
    }
    if(hitBlock())
        vy = -vy;
    bx += vx;
    if( bx < 0 ){
        bx = 0;
        vx = -vx;
    } else if( bx >= ((boardWidth - 1) << 8) ){
        bx = ((boardWidth - 1) << 8);
        if(vx > 0)
            vx = -vx;
    }
    if(hitBlock())
        vx = -vx;

    color(ballColor);
    sprite((bx >> 5) + (minX << 3), (by >> 5) + (minY << 3), ledON);

    if(remaining <= 0){
        prevScore = score;
        ++level;
        if(!(level % 5) && (lives < 3)){
            ++lives;
        }
        reset();
    }
}
