Skip to content

Instantly share code, notes, and snippets.

@NovaSquirrel
Last active January 11, 2023 19:42
Show Gist options
  • Save NovaSquirrel/0243b32b5db5a15b9ceea657d47d300a to your computer and use it in GitHub Desktop.
Save NovaSquirrel/0243b32b5db5a15b9ceea657d47d300a to your computer and use it in GitHub Desktop.
Second Life platformer
/*
Platformer demo thing
Copyright 2023, NovaSquirrel
Copying and distribution of this file, with or without modification, are permitted in any medium without royalty, provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty.
*/
// Config
// Amount of ticks (0.05 seconds) to wait before stopping the game due to inactivity. 500 = 25 seconds
integer inactivityTimeoutAmount = 500;
// Variables
list spriteLinks; // Link numbers for each sprite
list mapLinks; // Link numbers for the 8 rows that make up the level
key playerKey = NULL_KEY; // Key of the player who's currently playing
string playerName; // Name of the player who's currently playing
integer dialogChannel; // Channel to use for the dialog option
integer listenerHandle; // Handle for llListen
integer inactivity; // Number of ticks since the player pressed anything
integer keyDown = 0; // Currently pressed keys
integer keyNew = 0; // Keys that are pressed that weren't pressed last frame (not used)
// Map variables
integer playfieldOffset = 0; // What screen the player is currently on, multiplied by 64. Used to index into the level.
string playfield; // For gameplay
string playfieldInitial; // For restoring when restarting the level
list toggleLocations; // Places with a toggleable block
integer levelReady = 0; // 1 when the level has finished loading
list delayedMapEdits; // List of [time, position, value]. Keeps track of block changes that should happen after a delay.
// Notecard reading
key levelReadRequest = NULL_KEY;
integer notecardLine;
integer notecardLinesToGo;
// Information about the level pack
string levelPackName;
string levelPackAuthor;
string levelPackAbout;
key levelPackInfoRequest;
// For continuing
integer notecardLineSaved = 1;
integer coinsSaved;
// Gameplay variables
integer levelNumber = 1;
integer coins;
integer greenKeys;
integer redKeys;
integer health;
integer timeOnScreen; // Amount of ticks the player has been on the current screen. Used to make the game a little more fair, preventing damage when you haven't had long to see and react to enemies.
list activeEnemies; // Two enemies. Type, x, y, direction, vy, variable
list enemiesPerScreen; // The same, but a list of all enemimes on the level. There's two per screen, listed in order here.
// Current enemy variables
// They're copied here to avoid doing a bunch of list operations when processing an enemy
integer enemyType;
float enemyX;
float enemyY;
integer enemyDirection;
float enemyVY;
integer enemyVariable;
// Player variables
float playerX = 0.5; // X position, in grid tile units
float playerY = 0.5; // Y position, in grid tile units
float playerVY = 0; // Vertical speed
integer playerDirection = 0; // 0 or 8. Offset for the frame number for llSetLinkTextureAnim
integer jumpCanceled = 0; // Jump has already been canceled and can't be canceled until another jump
integer jumpGracePeriod = 0; // Ticks where you can still jump after moving off of a platform. "Coyote time"
integer invincible = 0; // Invincibility timer; set when hurt, decrements every tick
// Display functions
integer getLinkWithName(string name) {
integer i = llGetLinkNumber() != 0; // Start at zero (single prim) or 1 (two or more prims)
integer x = llGetNumberOfPrims() + i; // [0, 1) or [1, llGetNumberOfPrims()]
for (; i < x; ++i)
if (llGetLinkName(i) == name)
return i;
return -1;
}
// Get a texture offset corresponding to a given tile in the tileset.
vector tileImage(integer tile) {
return <0.0625+0.125*((tile ^ 4) & 7), 0.0625+0.125*(7 - ((tile^64) >> 4) ), 0>;
}
// Display status information above the game
updateTextForGame() {
llSetText("Level "+(string)levelNumber+"\nCoins: "+(string)coins+" | Health: "+(string)health+"\nRed keys: ️"+(string)redKeys+" | Green keys: "+(string)greenKeys, <1,1,1>, 1);
}
// Update all of the screen textures to display a new screen of the level
rerenderMap() {
if(levelReady)
updateTextForGame();
timeOnScreen = 0;
// Hide any enemies
spriteAlpha(1, 0);
spriteAlpha(2, 0);
// Get enemy data
activeEnemies = llCSV2List(llList2String(enemiesPerScreen, playfieldOffset / 64));
// Change the background between two alternating ones based on which screen you're on, to help give the impression you're moving through a world
string texture = "ac07c21c-db2b-d35b-ad6c-2d4650241205";
if(playfieldOffset & 64) {
texture = "b38d1f3e-9e06-35b8-29bf-1ce586d6f46f";
}
llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_TEXTURE, 1, texture, <1,1,0>, ZERO_VECTOR, 0]);
// Redraw all the tiles for the current screen of the level
integer row;
integer face;
for(row = 0; row < 8; row++) {
// Update the map one prim at a time; hopefully 8 calls is faster than 64?
list rules = [];
for(face = 0; face < 8; face++) {
integer tileType = llOrd(playfield, playfieldOffset+face*8+row)-1;
// Level tiles are split across two textures, in order to let me scale all of the tiles to 800%
string texture = "e75711f0-8ea6-e6d8-d640-6ae85bafbe10";
if(tileType & 8) {
texture = "53d5c299-b167-3f9b-5e63-e89be99cb10d";
}
rules = [PRIM_TEXTURE, face, texture, <0.125,0.125,0>, tileImage(tileType), 0, PRIM_COLOR, face, <1,1,1>, tileType!=0] + rules;
}
llSetLinkPrimitiveParamsFast(llList2Integer(mapLinks, row), rules);
}
}
// Move a sprite to a position relative to the top left corner of the map, where X and Y are in units of one map tile
moveSprite(integer which, float x, float y) {
vector scale = llGetScale();
llSetLinkPrimitiveParamsFast(llList2Integer(spriteLinks, which), [PRIM_POS_LOCAL, (<-scale.x/2+(x/8)*scale.x,-0.1,scale.z/2-(y/8)*scale.z>)]);
}
// Change the transparency of one sprite
spriteAlpha(integer which, float value) {
llSetLinkAlpha(llList2Integer(spriteLinks, which), value, ALL_SIDES);
}
// Change the animation on a sprite
spriteAnim(integer which, integer mode, integer start, integer length, float rate) {
llSetLinkTextureAnim(llList2Integer(spriteLinks, which), mode, 0, 8, 8, start, length, rate);
}
// Enemy functions
float square(float x) {
return x*x;
}
// Negate if the current enemy is facing left
float enemyNegIfLeft(float offset) {
if(enemyDirection)
return -offset;
return offset;
}
// Walk forward and bump into things, automatically turning around
enemyWalk(float amount) {
enemyX += enemyNegIfLeft(amount);
float wallOffset = enemyNegIfLeft(0.5);
if(solidOnAll(getMap(enemyX+wallOffset, enemyY-0.4)) || solidOnAll(getMap(enemyX+wallOffset, enemyY+0.4))) {
enemyX = (integer)enemyX + 0.5;
enemyDirection = enemyDirection ^ 8;
}
if(enemyX <= 0)
enemyDirection = 0;
if(enemyX >= 8)
enemyDirection = 8;
}
// Walk while trying to avoid falling off of platforms
enemyWalkOnPlatform(float amount) {
enemyWalk(amount);
if(!solidOnTop(getMap(enemyX+enemyNegIfLeft(0.5), enemyY+0.5)) ) {
enemyDirection = enemyDirection ^ 8;
}
}
// Add gravity, increase gravity, and check for collision with platforms
integer enemyFall() {
enemyY += enemyVY;
enemyVY += 0.05;
if(enemyVY > 0.8)
enemyVY = 0.8;
// If enemy falls off the bottom, wrap to the top
if(enemyY > 8) {
enemyY = 0;
enemyVY = 0;
}
return enemyCheckForGround();
}
// Check for collision with the ground
integer enemyCheckForGround() {
if(solidOnTop(getMap(enemyX-0.45, enemyY+0.5)) || solidOnTop(getMap(enemyX+0.45, enemyY+0.5))) {
enemyY = (integer)enemyY + 0.5;
enemyVY = 0;
return TRUE;
}
return FALSE;
}
// Run code for one enemy
runEnemy(integer which) {
integer base = (which-1) * 6;
integer oldHealth = health; // For detecting if player died
// type, x, y, vy, direction, variable
enemyType = llList2Integer(activeEnemies, base);
enemyX = llList2Float(activeEnemies, base+1);
enemyY = llList2Float(activeEnemies, base+2);
enemyDirection = llList2Integer(activeEnemies, base+3);
enemyVY = llList2Float(activeEnemies, base+4);
enemyVariable = llList2Integer(activeEnemies, base+5);
// Defaults
integer animStart;
integer animLength = 1;
integer animMode = ANIM_ON|LOOP;
if(enemyType == 1) jump EnemyRed;
if(enemyType == 2) jump EnemyGreen;
if(enemyType == 3) jump EnemyBall;
if(enemyType == 4) jump EnemyBlob;
if(enemyType == 5) jump EnemyHelmet;
if(enemyType == 6) jump EnemyBird;
if(enemyType == 7) jump EnemySpiky;
if(enemyType == 8) jump EnemyMissile;
if(enemyType == 9) jump EnemyBullet;
return;
@EnemyRed;
animStart = 34;
animLength = 6;
enemyWalkOnPlatform(0.05);
enemyFall();
jump EnemySkip;
@EnemyGreen;
animStart = 50;
animLength = 6;
enemyWalk(0.05);
enemyFall();
jump EnemySkip;
@EnemyBall;
animStart = 20;
// Use custom code instead of enemyFall, in order to go more slowly
enemyY += enemyVY;
enemyVY += 0.01;
if(enemyVY > 0.4)
enemyVY = 0.4;
if(enemyCheckForGround()) {
enemyVY = -0.3; //-2.5;
}
// Check against ceiling
if(solidOnAll(getMap(enemyX-0.45, enemyY-0.5)) || solidOnAll(getMap(enemyX+0.45, enemyY-0.5))) {
enemyY = (integer)enemyY + 0.5;
enemyVY = 0.01;
}
jump EnemySkip;
@EnemyBlob;
animStart = 48;
animLength = 2;
enemyWalk(0.05);
enemyFall();
if(llFrand(1) > 0.90) {
enemyDirection = 0;
if(playerX < enemyX)
enemyDirection = 8;
}
jump EnemySkip;
@EnemyHelmet;
animStart = 32;
animLength = 2;
enemyWalkOnPlatform(0.05);
enemyFall();
if(timeOnScreen >= 20 && !((timeOnScreen-20) % 60)) {
float angle = llAtan2(playerY-enemyY, playerX-enemyX);
integer missileDirection = 0;
if(llCos(angle) < 0)
missileDirection = 8;
activeEnemies = llListReplaceList(activeEnemies, [8, enemyX, enemyY, missileDirection, angle, 0], 6, 11);
}
jump EnemySkip;
@EnemyBird;
animMode = ANIM_ON|LOOP|PING_PONG;
animStart = 21;
animLength = 3;
enemyWalk(0.06);
jump EnemySkip;
@EnemySpiky;
animStart = 17;
animLength = 2;
if(timeOnScreen >= 20 && !((timeOnScreen-20) % 60)) {
float angle = llAtan2(playerY-enemyY, playerX-enemyX);
activeEnemies = llListReplaceList(activeEnemies, [9, enemyX, enemyY, 0, angle, 0], 6, 11);
}
jump EnemySkip;
@EnemyMissile;
animStart = 19;
++enemyVariable;
if(enemyVariable > 20) {
enemyX += llCos(enemyVY) * 0.2;
enemyY += llSin(enemyVY) * 0.2;
}
jump EnemySkip;
@EnemyBullet;
animStart = 26;
enemyX += llCos(enemyVY) * 0.1;
enemyY += llSin(enemyVY) * 0.1;
jump EnemySkip;
@EnemySkip;
spriteAlpha(which, 1);
moveSprite(which, enemyX, enemyY);
spriteAnim(which, animMode, animStart+enemyDirection, animLength, 6.4);
// Damage player if too close to the enemy
if((timeOnScreen > 15) && llSqrt(square(enemyX-playerX) + square(enemyY-playerY)) < 0.7) {
hurtPlayer();
// If this kills the player, don't write the new enemy data back
if(health > oldHealth)
return;
}
activeEnemies = llListReplaceList(activeEnemies, [enemyType, enemyX, enemyY, enemyDirection, enemyVY, enemyVariable], base, base+5);
}
// Gameplay functions
hurtPlayer() {
if(invincible)
return;
invincible = 30;
--health;
if(!health) {
llSetText("Ow!", <1,0,0>, 1);
llSleep(1);
startLevel();
} else
updateTextForGame();
}
integer solidOnTop(integer type) {
return type >= 58;
}
integer solidOnAll(integer type) {
return type >= 58 && type < 112;
}
integer getFromWholeMap(integer base, float yO) {
integer offset = (integer)(playerY+yO);
if(offset < 0 || offset >= 8)
return 0;
base += offset;
if(base < 0 || base > llStringLength(playfield))
return 0;
return llOrd(playfield, base)-1;
}
integer dontHaveClearanceForScreenCrossing(integer where) {
return solidOnAll(getFromWholeMap(where, -0.4)) || solidOnAll(getFromWholeMap(where, 0.4));
}
// Swap two block IDs throughout the level.
// Uses toggleLocations to have a smaller amount of stuff to look through.
doToggleBlock(integer a, integer b) {
integer i;
integer length = llGetListLength(toggleLocations);
for(i=0; i<length; i++) {
integer where = llList2Integer(toggleLocations, i);
integer t = llOrd(playfield, where)-1;
if(t == a) {
putMapAtIndex(where, b, 1);
} else if(t == b) {
putMapAtIndex(where, a, 1);
}
}
}
// Destroy a block, with a short poof animation first
becomePoof(integer x, integer y) {
putMap(x, y, 56);
putMapDelayed(x, y, 57, 2);
putMapDelayed(x, y, 0, 4);
}
integer interactWithBlockFromAbove(float oX) {
integer x = (integer)(playerX + oX);
integer y = (integer)(playerY + 0.5);
integer t = getMap(x, y);
if(t == 80) { // Crate
playerY = (integer)playerY + 0.5;
playerVY = -0.65;
jumpCanceled = 1; // Don't allow canceling this
becomePoof(x, y);
return 0;
} else if(t == 72) { // Exit
++levelNumber;
loadLevel();
}
return solidOnTop(t);
}
interactWithBlockOnSide(float oX, float oY) {
integer x = (integer)(oX + playerX);
integer y = (integer)(oY + playerY);
integer t = getMap(x, y);
if(t == 96 && greenKeys) {
--greenKeys;
becomePoof(x, y);
updateTextForGame();
return;
}
if(t == 97 && redKeys) {
--redKeys;
becomePoof(x, y);
updateTextForGame();
return;
}
// Not unlocking a door
if(solidOnAll(t)) {
playerX = (integer)playerX + 0.5;
}
}
interactWithBlockFromBelow(float oX) {
integer x = (integer)(oX + playerX);
integer y = (integer)(playerY - 0.5);
integer t = getMap(x, y);
// Bump into stuff first
if(solidOnAll(t)) {
playerVY = 0;
playerY = (integer)playerY + 0.5;
}
// Handle touching different things
if(t == 58) jump HitBreakable1;
if(t == 69) jump HitToggle1;
if(t == 70) jump HitToggle2;
if(t == 71) jump HitToggle3;
return;
@HitBreakable1;
putMap(x, y, 59);
putMapDelayed(x, y, 0, 3);
return;
@HitToggle1;
doToggleBlock(50, 66);
return;
@HitToggle2;
doToggleBlock(51, 67);
return;
@HitToggle3;
doToggleBlock(52, 68);
return;
}
getCollectibles(float x, float y) {
x += playerX;
y += playerY;
integer t = getMap(x, y);
// Handle touching different things
if(t == 32) jump GetCoin;
if(t == 33) jump GetGreenKey;
if(t == 34) jump GetRedKey;
return;
@GetCoin;
++coins;
jump RemoveAfter;
@GetGreenKey;
++greenKeys;
jump RemoveAfter;
@GetRedKey;
++redKeys;
@RemoveAfter;
putMap((integer)x, (integer)y, 0);
updateTextForGame();
}
integer getMap(float x, float y) {
if(x < 0 || x >= 8 || y < 0 || y >= 8)
return 0;
return llOrd(playfield, playfieldOffset+(integer)x*8+(integer)y)-1;
}
putMapDelayed(integer x, integer y, integer value, integer time) {
delayedMapEdits = [time, playfieldOffset+x*8+y, value] + delayedMapEdits;
}
putMap(integer x, integer y, integer value) {
integer i = playfieldOffset+x*8+y;
putMapAtIndex(i, value, 1);
}
putMapAtIndex(integer i, integer value, integer redraw) {
playfield = llInsertString(llDeleteSubString(playfield, i, i), i, llChar(value+1));
if(redraw && i >= playfieldOffset && i < (playfieldOffset + 64)) {
// Update the screen to account for this change
string texture = "e75711f0-8ea6-e6d8-d640-6ae85bafbe10";
if(value & 8) {
texture = "53d5c299-b167-3f9b-5e63-e89be99cb10d";
}
integer x = (i/8)&7;
llSetLinkPrimitiveParamsFast(llList2Integer(mapLinks, i&7), [PRIM_TEXTURE, x, texture, <0.125,0.125,0>, tileImage(value), 0, PRIM_COLOR, x, <1,1,1>, value!=0]);
}
}
// Miscellaneous functions
loadLevel() {
llSetText("Loading...", <1,1,1>, 1);
notecardLineSaved = notecardLine;
coinsSaved = coins;
levelReady = 0;
playfieldInitial = "";
notecardLinesToGo = -1; // First line read is for metadata, which will set this
levelReadRequest = llGetNotecardLine("Levels", notecardLine);
}
startLevel() {
playerX = 0.5;
playerY = 0.5;
playerVY = 0;
playfieldOffset = 0;
jumpGracePeriod = 0;
invincible = 0;
delayedMapEdits = [];
redKeys = 0;
greenKeys = 0;
health = 3;
coins = coinsSaved;
playfield = playfieldInitial;
levelReady = 1;
rerenderMap();
}
updateIdleFloatingText() {
string levelInfo;
if(levelPackName) {
levelInfo = "\n\nLevels: \""+levelPackName+"\" by "+levelPackAuthor;
}
llSetText("Silly mini platformer - by NovaSquirrel"+levelInfo, <1,1,1>, 1);
}
cleanup() {
//llScriptProfiler(PROFILE_NONE);
//llOwnerSay("Used "+(string)llGetSPMaxMemory());
updateIdleFloatingText();
if(playerKey) {
llWhisper(0, "Stopping the game");
}
keyDown = 0;
keyNew = 0;
levelReady = 0;
playerKey = NULL_KEY;
llReleaseControls();
llListenRemove(listenerHandle);
llSetTimerEvent(0);
spriteAlpha(0, 0); // Hide the player
// Sprites 1 and 2 already handled by rerenderMap()
// Reset the map to a blank screen
playfield = "";
playfieldOffset = 0;
integer i;
for(i=0;i<64;i++) {
playfield = playfield + llChar(1);
}
rerenderMap();
coins = 0;
}
// Event handlers and main code
default
{
on_rez(integer start_param)
{
llResetScript();
}
state_entry()
{
llSetTouchText("Play game");
integer i;
dialogChannel = -1 * (integer) llFrand(1000000.0) - 5;
// Find the links to the map and sprites, in order
mapLinks = [];
for(i=0;i<8;++i)
mapLinks += getLinkWithName("Map"+(string)i);
spriteLinks = [];
for(i=0;i<3;++i)
spriteLinks += getLinkWithName("Sprite"+(string)i);
cleanup();
levelPackInfoRequest = llGetNotecardLine("Levels", 0);
}
dataserver(key query_id, string data)
{
if(query_id == levelReadRequest) {
++notecardLine;
if(notecardLinesToGo == -1) { // Metadata
if(data == EOF) {
llSay(0, playerName + " beat the game! Coins collected: "+(string)coins);
// Start from the beginning next time
levelNumber = 1;
notecardLineSaved = 1;
coinsSaved = 0;
cleanup();
} else {
// First line contains metadata, including the number of notecard lines the level data has been split into, and enemy info
list linesAndEnemyInfo = llParseStringKeepNulls(data, ["|"], []);
notecardLinesToGo = llList2Integer(linesAndEnemyInfo, 0);
enemiesPerScreen = llParseStringKeepNulls(llList2String(linesAndEnemyInfo, 1), ["~"], []);
// Start reading the actual level data
levelReadRequest = llGetNotecardLine("Levels", notecardLine);
}
} else { // Level layout
list csv = llCSV2List(data);
// Convert it to a compact format now
integer length = llGetListLength(csv);
string packed;
integer i;
for(i=0; i<length; i++) {
packed += llChar(llList2Integer(csv, i)+1);
}
playfieldInitial += packed;
// Keep going if there's more level to read
--notecardLinesToGo;
if(notecardLinesToGo) {
levelReadRequest = llGetNotecardLine("Levels", notecardLine);
} else {
// Look for toggle locations first
toggleLocations = [];
integer length = llStringLength(playfieldInitial);
for(i=0; i<length; i++) {
integer t = llOrd(playfieldInitial, i)-1;
// if(llListFindList([50, 51, 52, 66, 67, 68], [t] ) != -1) {
if((t>=50 && t<=52) || (t>=66 && t<=68)) {
toggleLocations = [i] + toggleLocations;
}
}
// Now start the level
startLevel();
}
}
} else if(query_id == levelPackInfoRequest) {
list info = llParseStringKeepNulls(data, ["|"], []);
levelPackName = llList2String(info, 0);
levelPackAuthor = llList2String(info, 1);
levelPackAbout = llList2String(info, 2);
updateIdleFloatingText();
}
}
touch_start(integer total_number)
{
if(playerKey == NULL_KEY) {
//llOwnerSay((string)llGetUsedMemory() + " " + (string)llGetFreeMemory( ));
llListenRemove(listenerHandle);
listenerHandle = llListen(dialogChannel, "", llDetectedKey(0), "");
llDialog(llDetectedKey(0), "Start from level "+(string)(levelNumber)+"?", ["New game", "Continue", "How to play", "About"], dialogChannel);
llSetTimerEvent(60.0);
//llScriptProfiler(PROFILE_SCRIPT_MEMORY);
} else if(llDetectedKey(0) == playerKey) {
cleanup();
} else {
llSay(0, "Sorry, this game is in use by "+playerName+". Please wait.");
}
}
listen(integer chan, string name, key id, string msg)
{
llListenRemove(listenerHandle);
if(msg == "New game" || msg == "Continue") {
if(msg == "New game") {
notecardLine = 1;
coins = 0;
} else {
notecardLine = notecardLineSaved;
coins = coinsSaved;
}
llRequestPermissions(id, PERMISSION_TAKE_CONTROLS);
} else if(msg == "About") {
llDialog(id, "Level pack: \""+levelPackName+"\" by "+levelPackAuthor+".\n\""+levelPackAbout+"\"\n\nCode by NovaSquirrel.\nGraphics mostly by GrafxKid (thanks for submitting your work to OpenGameArt!), with some by NovaSquirrel.", [], dialogChannel);
} else if(msg == "How to play") {
llDialog(id, "Choose \"New game\" to start the game! After giving permission to use the controls, use Left and Right to move, and Up or PgUp to jump. If you want to stop, touch the game screen!\n\nThe goal is to get to the end of each level, marked with a purple colored block with a circle on it.\nThere are enemies to avoid, as well as keys that open locks, and toggle switches that toggle the solidity of corresponding blocks. Have fun!", [], dialogChannel);
}
}
run_time_permissions(integer perm)
{
if (perm & (PERMISSION_TAKE_CONTROLS)) {
playerKey = llGetPermissionsKey();
playerName = llGetDisplayName(playerKey);
if(playerName == "") {
playerName = llGetUsername(playerKey);
}
llTakeControls( CONTROL_FWD | CONTROL_BACK |
CONTROL_LEFT | CONTROL_RIGHT |
CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT |
CONTROL_UP | CONTROL_DOWN,
TRUE, FALSE);
inactivity = 0;
llSetTimerEvent(0.05);
// Load in the level data too
loadLevel();
}
}
control(key id, integer level, integer edge)
{
inactivity = 0;
keyDown = level;
keyNew = keyNew | (level & edge);
}
timer()
{
if(playerKey == NULL_KEY) {
cleanup();
} else if(levelReady) {
// Step game logic a tick
playerY += playerVY;
playerVY += 0.05;
if(playerVY > 0.8)
playerVY = 0.8;
// Tick
integer delayedLength = llGetListLength(delayedMapEdits);
integer i;
for(i=0; i<delayedLength; i+=3) {
integer time = llList2Integer(delayedMapEdits, i);
if(time) {
delayedMapEdits = llListReplaceList(delayedMapEdits, [time-1], i, i);
} else {
putMapAtIndex(llList2Integer(delayedMapEdits, i+1), llList2Integer(delayedMapEdits, i+2), 1);
delayedMapEdits = llDeleteSubList(delayedMapEdits, i, i+2);
i -= 3;
delayedLength -= 3;
}
}
// Let player move left and right
if(keyDown & (CONTROL_LEFT | CONTROL_ROT_LEFT)) {
playerX = playerX - 0.2;
interactWithBlockOnSide(-0.5, -0.4);
interactWithBlockOnSide(-0.5, 0.4);
playerDirection = 8;
}
if(keyDown & (CONTROL_RIGHT | CONTROL_ROT_RIGHT)) {
playerX = playerX + 0.2;
interactWithBlockOnSide(0.5, -0.4);
interactWithBlockOnSide(0.5, 0.4);
playerDirection = 0;
}
// Go left a screen
if(playerX <= -0) {
if(playfieldOffset == 0 || dontHaveClearanceForScreenCrossing(playfieldOffset-8)) {
playerX = 0;
} else {
playerX = 7.5;
playfieldOffset -= 64;
rerenderMap();
}
}
// Go right a screen
if(playerX >= 8) {
if(playfieldOffset + 64 >= llStringLength(playfield) || dontHaveClearanceForScreenCrossing(playfieldOffset+64)) {
playerX = 8;
} else {
playerX = 0.5;
playfieldOffset += 64;
rerenderMap();
}
}
// Check for ground collision
integer onGround = 0;
if(playerY > 7.5) {
llSetText("Whoops!", <1,0,0>, 1);
llSleep(1);
startLevel();
//playerY = 7.5;
//onGround = 1;
}
// Try to fix a problem with falling through the floors that are only soli on the top
if(playerVY > 0.5) {
if(solidOnTop(getMap(playerX-0.45, playerY)) || solidOnTop(getMap(playerX+0.45, playerY))) {
playerY = (integer)(playerY-1)+0.5;
playerVY = 0;
}
}
// Landing on something? (the normal case)
if(playerVY >= 0 /*&& ((integer)(playerY*2) & 1)*/ && (interactWithBlockFromAbove(-0.45) || interactWithBlockFromAbove(0.45))) {
playerY = (integer)playerY + 0.5;
onGround = 1;
jumpGracePeriod = 2;
// Stuck in the ground?
if(solidOnAll(getMap(playerX-0.45, playerY)) || solidOnAll(getMap(playerX+0.45, playerY))) {
playerY -= 1;
}
}
// Get collectibles on all four sides
getCollectibles(0.48, 0.48);
getCollectibles(-0.48, 0.48);
getCollectibles(0.48, -0.48);
getCollectibles(-0.48, -0.48);
// Bumping from below
interactWithBlockFromBelow(-0.45);
interactWithBlockFromBelow(0.45);
// Interact with blocks inside
integer t = getMap(playerX, playerY);
if(playerVY >= 0) {
if(t == 8) { // Spring
jumpCanceled = 1; // Don't allow canceling this
playerVY = -0.65;
onGround = 0;
putMap((integer)playerX, (integer)playerY, 9);
putMapDelayed((integer)playerX, (integer)playerY, 8, 2);
} else if(t == 55) { // Spikes
hurtPlayer();
}
}
if(onGround) {
playerVY = 0;
if(keyDown & (CONTROL_LEFT | CONTROL_ROT_LEFT | CONTROL_RIGHT | CONTROL_ROT_RIGHT))
spriteAnim(0, ANIM_ON | LOOP, 1+playerDirection, 6, 6.4);
else
spriteAnim(0, ANIM_ON | LOOP, 0+playerDirection, 1, 6.4);
} else {
if(playerVY > 0)
spriteAnim(0, ANIM_ON | LOOP, 16+playerDirection, 1, 6.4);
else
spriteAnim(0, ANIM_ON | LOOP, 7+playerDirection, 1, 6.4);
}
// React to touching the ground
if(onGround || jumpGracePeriod) {
if(keyDown & (CONTROL_FWD|CONTROL_UP)) {
playerVY = -0.5;
jumpCanceled = 0;
jumpGracePeriod = 0;
}
}
if(jumpGracePeriod)
--jumpGracePeriod;
// Allow canceling a jump early
if(playerVY < 0 && !jumpCanceled && !(keyDown&(CONTROL_FWD|CONTROL_UP))) {
jumpCanceled = 1;
playerVY = -0.01;
}
// Tick the enemies
runEnemy(1);
runEnemy(2);
if(invincible) {
--invincible;
spriteAlpha(0, 0.75);
} else {
spriteAlpha(0, 1);
}
keyNew = 0;
// Move the player to the new position
moveSprite(0, playerX, playerY);
++timeOnScreen;
// Detect a timeout
++inactivity;
if(inactivity > inactivityTimeoutAmount)
cleanup();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment