Last active
January 11, 2023 19:42
-
-
Save NovaSquirrel/0243b32b5db5a15b9ceea657d47d300a to your computer and use it in GitHub Desktop.
Second Life platformer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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