-
Notifications
You must be signed in to change notification settings - Fork 2
Obstacle Course Tutorial: Scripting
The scripting might be a little overwhelming since there's a lot of it. But it's fairly straightforward if you're working with premade assets. The biggest issue is the amount of code needed due to the philosophy of "every stage is its own mini game". This means that the Game Controller holds all the cards and makes all decisions for how things work in your world.
Let's go through the 3 user defined Level Controller scripts from the split obstacle course.
The dialog manager controls the dialog popups when you click the Level Controller. But don't fret, there's template code that you can use to significantly reduce the amount of code you need to write. You are, however, free to not use that if you want to make something completely custom.
In these tutorials, I will first paste the full code from the level. And then break that down into sections, explaining how it works. If you haven't already seen the module breakdown. Please check that out as well, as it will help you understand the main parts of an ObstacleScript module.
#define USE_STATE_ENTRY
#define USE_LISTEN
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/resources/DialogHelper.lsl"
// Custom menus
#define MENU_TEAM -1
integer TEAM_PAGE;
// In the configuration global, first value contains team settings
#define GCONF_TEAMS 0
integer TEAMS; // Each bit corresponds to the
list onDialogOpen( integer dialog ){
string text; list buttons;
if( dialog == MENU_MAIN ){
text = "TEAMS:";
forPlayer( i, player )
text += "\n "+llGetSubString(llGetDisplayName(player), 0, 8)+". "+(str)((TEAMS&(1<<i))>0);
end
if( ~GSETTINGS & GS_GAME_STARTED ){
buttons += "Teams";
buttons += "Shuffle Teams";
}
}
else if( dialog == MENU_TEAM ){
buttons += "BACK";
if( count(PLAYERS) > 10 )
buttons += ">>";
if( TEAM_PAGE > count(PLAYERS)/10 )
TEAM_PAGE = 0;
integer i;
for( i = TEAM_PAGE*10; i < TEAM_PAGE*10+10 && i < count(PLAYERS); ++i ){
key player = l2k(PLAYERS, i);
text += "\n "+llGetDisplayName(player)+". "+(str)((TEAMS&(1<<i))>0);
buttons += llGetSubString(llGetDisplayName(player), 0, 8);
}
}
// Put your custom dialog open buttons and text here
return (list)text + buttons;
}
onDialogButton( int menu, string button ){
//qd("Button pressed:" + menu + button);
if( menu == MENU_MAIN ){
if( button == "Teams" ){
TEAM_PAGE = 0;
openDialog(MENU_TEAM);
}
else if( button == "Shuffle Teams" ){
list shuffle;
int i;
for( ; i < count(PLAYERS); ++i )
shuffle += i < count(PLAYERS)/2;
shuffle = llListRandomize(shuffle, 1);
TEAMS = 0;
for( i = 0; i < count(shuffle); ++i )
TEAMS = TEAMS | (l2i(shuffle, i) << i);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_MAIN);
}
}
else if( menu == MENU_TEAM ){
if( button == ">>" ){
++TEAM_PAGE;
openDialog(MENU_TEAM);
}
else if( button == "BACK" ){
openDialog(MENU_MAIN);
}
else{
forPlayer( index, player )
if( llGetSubString(llGetDisplayName(player), 0, 8) == button ){
TEAMS = TEAMS^(1<<index);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_TEAM);
return;
}
end
}
}
}
// Lets you override the hover text
// Use the global GSCORE to get the score list from #GAME
string onTextUpdate(){
if( GSETTINGS & GS_RECENT_GAME_END ){
// Return winner text here
integer winner = l2f(GSCORE, 1) > 0 && l2f(GSCORE, 1) < l2f(GSCORE, 0);
string text = "TEAM "+(str)winner+" WINS!\n";
text += "Average times:\n";
text += "Team 0: "+l2s(GSCORE, 0)+" sec\n";
text += "Team 1: "+l2s(GSCORE, 1)+" sec\n";
return text;
}
else if( GSETTINGS & GS_GAME_STARTED && GSETTINGS & GS_GAME_LOADED ){
// Output the game mode text here
return "Lowest average time of each team wins!";
}
else if( ~GSETTINGS & GS_GAME_STARTED ){
string txt = "-- PLAYERS --\n";
if( count(PLAYERS) > 6 )
txt += (str)count(PLAYERS)+" Joined\n";
else{
forPlayer( index, player )
txt += "["+(string)((TEAMS&(1<<index)) > 0)+"] "+llGetDisplayName(player)+"\n";
end
}
return txt;
}
return "";
}
#include "ObstacleScript/begin.lsl"
// Sets up the click handler
dialogHelperHandler()
onStateEntry()
dialogHelperSetup();
end
#include "ObstacleScript/end.lsl"
#define USE_STATE_ENTRY
#define USE_LISTEN
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/resources/DialogHelper.lsl"
Setup dependencies and also include the DialogHelper script, which automates a lot for you.
#define MENU_TEAM -1
integer TEAM_PAGE;
Here we define a custom menu page for setting the team of a player, since the DialogHelper isn't setup to handle teams. And also creates a global value TEAM_PAGE since the nr of players may exceed 12, which means we'd run out of dialog options otherwise. Any custom menus should use a negative integer.
#define GCONF_TEAMS 0
DialogHelper includes a global list called GCONF. This list contains configuration data to send to the #Game script when the owner picks the START GAME option. You don't have to use it if you don't need the game to support any custom configuration. But in our case we will store the player teams in a single integer (bitwise operation), and it will be passed as the first element in the GCONF list.
integer TEAMS;
Sets up a global variable to save the player teams. Each bit corresponds to the team of that player. This works because there's only 2 teams in this level.
list onDialogOpen( integer dialog ){
string text; list buttons;
if( dialog == MENU_MAIN ){
text = "TEAMS:";
forPlayer( i, player )
text += "\n "+llGetSubString(llGetDisplayName(player), 0, 8)+". "+(str)((TEAMS&(1<<i))>0);
end
if( ~GSETTINGS & GS_GAME_STARTED ){
buttons += "Teams";
buttons += "Shuffle Teams";
}
}
else if( dialog == MENU_TEAM ){
buttons += "BACK";
if( count(PLAYERS) > 10 )
buttons += ">>";
if( TEAM_PAGE > count(PLAYERS)/10 )
TEAM_PAGE = 0;
integer i;
for( i = TEAM_PAGE*10; i < TEAM_PAGE*10+10 && i < count(PLAYERS); ++i ){
key player = l2k(PLAYERS, i);
text += "\n "+llGetDisplayName(player)+". "+(str)((TEAMS&(1<<i))>0);
buttons += llGetSubString(llGetDisplayName(player), 0, 8);
}
}
// Put your custom dialog open buttons and text here
return (list)text + buttons;
}
onDialogOpen is a function that's called before a dialog is opened on the user. It should return a list where the first element is a string containing the text that should be put in the dialog (or "" if you want to use default). Followed by any custom buttons to append to the dialog page. The DialogHelper header file contains 3 default menus (as of writing): MENU_MAIN, MENU_MAINTENANCE, MENU_INVITE_PLAYER.
Since we want to present teams in the default menu, we'll set a custom text which shows the players and teams. We also want two custom buttons to manage or shuffle teams, so we add those to buttons.
If the dialog is our custom team menu, we add a button for each player, and also add some rudimentary pagination in case the nr of players are too many for one page. We also output a list of players and their teams again.
onDialogButton( int menu, string button ){
//qd("Button pressed:" + menu + button);
if( menu == MENU_MAIN ){
if( button == "Teams" ){
TEAM_PAGE = 0;
openDialog(MENU_TEAM);
}
else if( button == "Shuffle Teams" ){
list shuffle;
int i;
for( ; i < count(PLAYERS); ++i )
shuffle += i < count(PLAYERS)/2;
shuffle = llListRandomize(shuffle, 1);
TEAMS = 0;
for( i = 0; i < count(shuffle); ++i )
TEAMS = TEAMS | (l2i(shuffle, i) << i);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_MAIN);
}
}
else if( menu == MENU_TEAM ){
if( button == ">>" ){
++TEAM_PAGE;
openDialog(MENU_TEAM);
}
else if( button == "BACK" ){
openDialog(MENU_MAIN);
}
else{
forPlayer( index, player )
if( llGetSubString(llGetDisplayName(player), 0, 8) == button ){
TEAMS = TEAMS^(1<<index);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_TEAM);
return;
}
end
}
}
}
onDialogButton is called when the user presses any button. Here we can add custom behavior. Other than the bitwise operations, this should be fairly straight forward. We check what the menu is, and what button was pressed. Then handle that.
string onTextUpdate(){
if( GSETTINGS & GS_RECENT_GAME_END ){
// Return winner text here
integer winner = l2f(GSCORE, 1) > 0 && l2f(GSCORE, 1) < l2f(GSCORE, 0);
string text = "TEAM "+(str)winner+" WINS!\n";
text += "Average times:\n";
text += "Team 0: "+l2s(GSCORE, 0)+" sec\n";
text += "Team 1: "+l2s(GSCORE, 1)+" sec\n";
return text;
}
else if( GSETTINGS & GS_GAME_STARTED && GSETTINGS & GS_GAME_LOADED ){
// Output the game mode text here
return "Lowest average time of each team wins!";
}
else if( ~GSETTINGS & GS_GAME_STARTED ){
string txt = "-- PLAYERS --\n";
if( count(PLAYERS) > 6 )
txt += (str)count(PLAYERS)+" Joined\n";
else{
forPlayer( index, player )
txt += "["+(string)((TEAMS&(1<<index)) > 0)+"] "+llGetDisplayName(player)+"\n";
end
}
return txt;
}
return "";
}
onTextUpdate is called whenever the hover text should be refreshed. GSETTINGS is a global integer that's handled automatically for you when using DialogHelper. It contains stat information like GS_GAME_STARTED, GS_RECENT_GAME_END, GS_GAME_LOADED, to help you output the correct text. See DialogHelper.lsl for more information. If G_RECENT_GAME_END is set, we want to present the scores. Scores are passed as a list to the global GSCORE from #Game whenever the game ends. You control what should go in this list in #Game when ending a game. In this case the list contains 2 elements: Team 0 total time, team 1 total time. We use these two values to show what team won, and what times they had.
The other two states are for when the level is loaded, and when waiting for the game to start. The loading messages while loading a level is handled automatically by returning "".
#include "ObstacleScript/begin.lsl"
// Sets up the click handler
dialogHelperHandler()
onStateEntry()
dialogHelperSetup();
end
#include "ObstacleScript/end.lsl"
Finally we setup the event handler. In it we call dialogHelperHandler() to allow DialogHelper to setup and handle any events it may need, and in onStateEntry we call dialogHelperSetup to activate it when the script starts.
The purpose of Obstacles is to handle any obstacles that don't need any settings from #Game to run. For an instance crusher walls that should run automatically.
#define USE_STATE_ENTRY
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/begin.lsl"
onSpawnerGameLoad()
_CRUSHERS_setup(6, 2.25);
end
onStateEntry()
setInterval("TENT", 2);
end
onTimer( id )
_CRUSHERS_onTimer(id);
end
onRezzerRezzed( obj )
_CRUSHERS_onSpawn(obj);
end
handleTimer("TENT")
Trap$attackAll( "*", [] );
end
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
if( llKey2Name(stair) == "TumblingBeam" ){
if( seated )
Qte$start( hud, QteConst$QTE_GAUGE, "TUMBL" );
else
Qte$end( hud, TRUE );
}
end
onPlayerQteEnded( hud, success, callback )
if( callback == "TUMBL" && !success ){
Rlv$unSit( hud, TRUE );
}
end
#include "ObstacleScript/end.lsl"
#define USE_STATE_ENTRY
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/begin.lsl"
Setup features we want to use, include the main header file (index.lsl) and start the event handler.
onSpawnerGameLoad()
_CRUSHERS_setup(6, 2.25);
end
onSpawnerGameLoad is raised when you're rezzing the level. We'll use my algorithm for crusher walls defined in CrusherWall.lsh instead of writing a custom algorithm for them by calling _CRUSHERS_setup(6, 2.25);
where 6 indicate the nr of walls, and 2.25 is the time between movements.
onStateEntry()
setInterval("TENT", 2);
end
When the script loads, we'll setup a repeating timer every 2 sec called "TENT" and will be used to trigger the lashing tentacles.
onTimer( id )
_CRUSHERS_onTimer(id);
end
Here we pass the timer handler for my crusher wall algorithm, allowing it to handle the wall timers automatically.
onRezzerRezzed( obj )
_CRUSHERS_onSpawn(obj);
end
The crusher walls need to send message to them directly by ID, so when an object is rezzed, let the call this function to let the CrusherWall helper automatically check if it was a wall and whether to store it in the script or not.
handleTimer("TENT")
Trap$attackAll( "*", [] );
end
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
if( llKey2Name(stair) == "TumblingBeam" ){
if( seated )
Qte$start( hud, QteConst$QTE_GAUGE, "TUMBL" );
else
Qte$end( hud, TRUE );
}
end
When a player sits on a stair (ladder, shimmy wall, tumbler beam etc), this event is raised. We check if the object name was TumblingBeam and in that case start a quick time event on their HUD. We'll specify that it should use the GAUGE type event, and callback "TUMBL" when the quicktime event finishes or fails. If the player is unseated, we clear the event.
onPlayerQteEnded( hud, success, callback )
if( callback == "TUMBL" && !success ){
Rlv$unSit( hud, TRUE );
}
end
When the quicktime event ends, and if the callback matches ours and was not successful, we unsit the player to let them fall off.
#include "ObstacleScript/end.lsl"
Ends the event handler and the script.
The game script is where most of the game logic takes place. And will generally be the longest script as it contains the scores and logic of the actual gameplay. We can use the GameHelper.lsl macros to abstract some of it away.
#define USE_STATE_ENTRY
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#define CUSTOM_TEXTURE "37c0bfa7-d9dc-a94d-2892-ac08ca6bd730"
#define CUSTOM_TEXTURE_X 4
#define CUSTOM_TEXTURE_Y 2
// The root prim is not exactly centered
#define X_OFFSET 2.6
// Position relative to root of the balloon platform
#define PLATFORM_A <-8.21873, -2.88553, 12.56812>
#define PLATFORM_B <8.21873+X_OFFSET, -2.88553, 12.56812>
// Start positions relative to root
#define START_A <-15.21873, 13.47408, 7.89429>
#define START_B <15.21873+X_OFFSET, 13.47408, 7.89429>
#define STARTROT llEuler2Rot(<0,0,-PI_BY_TWO>)
integer CLOCKSTATE; // When all players on one team have finished, start the clock
// The game data is stored in a list. Index 0 is ALWAYS the uuid of the player.
// We can define the other data we need to store about each player here, starting from 1
#define PD_CHECKPOINT 1 // Vector checkpoint position
#define PD_INVUL_UNTIL 2 // We'll put an llGetTime timestamp here for invul
#define PD_SCORE 3 // How long it took them to finish, in seconds
#define PD_AROUSAL 4 // int Arousal value
#define PD_OBSTACLE 5 // key UUID of the obstacle player is sitting on
#define PD_TEAM 6
#define PD_PENALTY 7 // Penalty seconds added when you reach the end
#define PD_ARMOR 8 // 2= dressed, 1 = underwear, 0 = bits
#define PD_CHECKPOINT_ROT 9 // Checkpoint rotation
// Put default values for above here
#define PD_DEFAULTS [ZERO_VECTOR, 0, 0, 0, "", 0, 0, 2, ZERO_ROTATION]
// Set this to the max value of our data fields +1. So if the max field index was 5, we set 6
#define PD_STRIDE 10
// Include the gamehelper here so it can use the values above
#include "ObstacleScript/resources/GameHelper.lsl"
// Make some helper macros
#define INVUL_DURATION 5
#define isPlayerInvul( uuid ) \
(getPlayerDataInt(uuid, PD_INVUL_UNTIL) > llGetTime())
#define setPlayerInvul( uuid, duration ) \
setPlayerData(uuid, PD_INVUL_UNTIL, llGetTime()+duration)
#define getPlayerPenalty( uuid ) \
getPlayerDataFloat(uuid, PD_PENALTY)
#define addPlayerPenalty( uuid, seconds ) \
setPlayerData(uuid, PD_PENALTY, getPlayerPenalty(uuid)+seconds)
#define getPlayerArmor( uuid ) \
getPlayerDataInt(uuid, PD_ARMOR)
#define setPlayerArmor( uuid, amount ) \
setPlayerData(uuid, PD_ARMOR, amount)
#define setPlayerObstacle( uuid, obstacle ) \
setPlayerData(uuid, PD_OBSTACLE, obstacle)
#define getPlayerObstacle( uuid ) \
getPlayerDataKey(uuid, PD_OBSTACLE)
#define setPlayerScore( uuid, score ) \
setPlayerData(uuid, PD_SCORE, (float)score)
#define getPlayerScore( uuid ) \
getPlayerDataFloat(uuid, PD_SCORE)
// Scoreboards
integer SCOREBOARDS; // 8 bit array
updateScore(){
list scores = [0,0];
integer i;
for(; i < count(PLAYER_DATA); i += PD_STRIDE ){
integer team = l2i(PLAYER_DATA, i+PD_TEAM);
scores = llListReplaceList(
scores,
(list)(l2i(scores, team)+l2i(PLAYER_DATA, i+PD_PENALTY)),
team,
team
);
}
string texture = "d72fed9b-e077-6ea1-fd98-9b28d879fd21";
for(i = 0; i < 2; ++i ){
integer prim = ((SCOREBOARDS >> (i*8))&0xFF);
string score = (string)l2i(scores, i);
integer f;
for( f = llStringLength(score); f < 6; ++f )
score = "0"+score;
list data;
for( f = 0; f < 6; ++f ){
integer n = (integer)llGetSubString(score, f, f);
data += (list)PRIM_TEXTURE + (2+f) + texture +
<0.0625, 1, 0> +
<-0.46875+n*0.0625, 0, 0> +
0
;
}
llSetLinkPrimitiveParamsFast(prim, data);
}
}
#define MAX_AROUSAL 3
setPlayerArousal( key id, int amount ){
if( amount > MAX_AROUSAL )
return;
// Update the arousal bar
Gui$setBarPerc( id, "AROUSAL", (float)amount/MAX_AROUSAL );
setPlayerData(id, PD_AROUSAL, amount);
if( amount == MAX_AROUSAL ){
float score = getPlayerScore(id);
if( score > 0 )
setPlayerScore(id, score+30); // Add to the total score if they're on the platform
addPlayerPenalty(id, 30); // Always add to penalty. It's used both for the meter and added to score when they finish their run.
updateScore();
// Player reached max arousal
AnimHandler$anim(id, "got_pain_highpri", TRUE, 5, 0);
Rlv$setFlags(id, RlvFlags$IMMOBILE, TRUE);
// Set a timer to reset their position
setTimeout("UF:"+(str)id, 5);
integer armor = getPlayerArmor(id);
if( armor > 0 ){
int a = armor;
--armor;
setPlayerArmor(id, armor);
Rlv$setClothes( id, a, a, a, a, a );
}
}
}
#define getPlayerArousal( uuid ) \
getPlayerDataInt(uuid, PD_AROUSAL)
#define setPlayerCheckpoint( uuid, pos, rot ) \
setPlayerData(uuid, PD_CHECKPOINT, pos); \
setPlayerData(uuid, PD_CHECKPOINT_ROT, rot)
#define getPlayerCheckpoint( uuid ) \
getPlayerDataVec( uuid, PD_CHECKPOINT )
#define getPlayerCheckpointRot( uuid ) \
getPlayerDataRot( uuid, PD_CHECKPOINT_ROT )
#define setPlayerTeam( uuid, team ) \
setPlayerData(uuid, PD_TEAM, team)
#define getPlayerTeam( uuid ) \
getPlayerDataInt(uuid, PD_TEAM)
// Callback to check player trap viability
integer canTrapPlayer( key trap, key player ){
return !isPlayerInvul(player);
}
// Checks if every player has a score assigned to them
// returns 2 if all players are done, returns 1 if all players on one team are done
integer isGameCompleted(){
integer teamsCompleted = 3;
forPlayer( index, player )
if( getPlayerScore(player) <= 0 ){
integer team = getPlayerTeam(player);
teamsCompleted = teamsCompleted &~ (1<<team);
if( !teamsCompleted )
return FALSE;
}
end
if( teamsCompleted == 3 )
return 2;
return 1;
}
// Events
onGameStart(){
updateScore();
// Called when the game starts loading the meshes.
integer teams = l2i(GCONF, 0); // GCONF is set by DialogHelper on game start
// See #Dialog for GCONF definitions
CLOCKSTATE = 0;
// We can initiate the players here, by configuring them, setting up bars etc
forPlayer( index, player )
setPlayerTeam( player, (teams & (1 << index))>0 );
Rlv$setMaxSprint( player, 3 );
integer armor = 3;
Rlv$setClothes( player, armor, armor, armor, armor, armor );
// Create arousal bar
Gui$createBar( player, "AROUSAL", "<1,0,1>", "<1,.75,1>" );
Gui$setBarPerc( player, "AROUSAL", 0 );
// Preload custom instruction
Gui$preloadInstruction( player, CUSTOM_TEXTURE, 1 );
end
}
list onGameEnd(){
unsetTimer("CLOCK");
// Scores are sent to #Dialog, let's calculate an average for both teams
list scores = [0,0];
list numPlayers = [0,0];
forPlayer( index, player )
integer team = getPlayerTeam(player);
float score = getPlayerScore(player);
if( team == 0 || team == 1 ){
scores = llListReplaceList(
scores,
(list)(l2f(scores,team)+score),
team,
team
);
numPlayers = llListReplaceList(
numPlayers,
(list)(l2i(numPlayers, team)+1),
team,
team
);
}
end
integer i;
for( ; i < count(scores); ++i ){
if( l2i(numPlayers, i) ){
scores = llListReplaceList(
scores,
(list)(l2f(scores, i)/l2i(numPlayers, i)),
i, i
);
}
}
integer winner = l2f(scores, 1) > 0 && l2f(scores, 0) > l2f(scores, 1);
string winnerText = "TEAM "+(str)winner+" wins!\n";
winnerText += "Average Scores:\n";
winnerText += "Team 0: "+l2s(scores, 0)+" sec\n";
winnerText += "Team 1: "+l2s(scores, 1)+" sec\n";
forPlayer( _idx, player )
llRegionSayTo(player, 0, winnerText);
Gui$removeBars( player, "AROUSAL" + "CD" );
LevelRepo$detach( player, "HUD:Water Balloon Att" );
Gui$clearInstruction( player, 0 );
Gui$clearInstruction( player, 1 );
end
return scores;
}
onRoundStart(){
// We only have one round in this level
// So in this case, it's only raised when the level has loaded
forPlayer( index, player )
integer team = getPlayerTeam(player);
vector startPos = START_A;
if( team )
startPos = START_B;
startPos += llGetRootPosition();
// Set the sprint instruction on screen
Gui$setDefaultInstruction( player, Gui$INSTRUCTION_SPRINT, 0, 6 );
Gui$setInstruction( player, 1, CUSTOM_TEXTURE, 1, CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y, 6);
// Warp player to the start
setPlayerCheckpoint(player, startPos, STARTROT);
warpPlayerToSurface( player, startPos, STARTROT, FALSE );
end
}
onCountdownFinished(){
// Unsit is handle automatically
}
updateClock(){
if( !CLOCKSTATE || CLOCKSTATE > 3)
return;
integer inst = 2;
if( CLOCKSTATE == 2 )
inst = 3;
else if( CLOCKSTATE == 3 )
inst = 4;
forPlayer( index, player )
Gui$setInstruction(
player,
inst,
CUSTOM_TEXTURE,
2,
CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y,
6
);
end
}
assignScore( key player ){
// Here we use the automatic value ROUND_START_TIME to calculate how long it took
float score = getPlayerScore(player);
if( score > 0 )
return;
setPlayerScore( player, llGetTime()-ROUND_START_TIME+getPlayerPenalty(player) );
}
// ObstacleScript events start
#include "ObstacleScript/begin.lsl"
// Sets up automation of the game handler
gameHelperEventHandler();
//gameHelperAutoWater(5); // Can be used to automate falling in the water
//gameHelperHandleBalloonHit( 4, isPlayerInvul ) // Can be used to automat balloon hits
onStateEntry()
gameHelperStateEntry();
setInterval("_WATER", 3);
links_each(nr, name,
if( llGetSubString(name, 0, 9) == "ScoreBoard" ){
integer n = (integer)llGetSubString(name, -1, -1);
SCOREBOARDS = SCOREBOARDS | nr << (n*8);
}
)
updateScore();
end
onTrapHit( trap, players )
// Pass a callback function
_TRAPS_autoSeat( canTrapPlayer );
end
// Handle unsitting from a trap
onTrapUnseated( trap, players )
integer i;
for(; i < count(players); ++i ){
key player = l2k(players, i);
setPlayerInvul(player, INVUL_DURATION);
setPlayerArousal(player, getPlayerArousal(player)+1);
}
end
onShimmyWallHit( object, start, players )
if( !start )
return;
integer i;
for(; i < count(players); ++i )
setPlayerArousal(l2k(players, i), getPlayerArousal(l2k(players, i))+1);
end
onProjectileHit( projectile, obj )
if( llGetAgentSize(obj) != ZERO_VECTOR ){
if( llGetAgentInfo(obj) & AGENT_SITTING || isPlayerInvul(obj) )
return;
float score = getPlayerScore(obj);
vector owner = prPos(llGetOwnerKey(projectile));
vector spos = prPos(obj);
vector offs = spos-owner;
offs.z = 0;
float time = 0.5;
float push = 2;
if( score > 0 )
push = 4;
spos += llVecNorm(offs)*push;
Rlv$damageSprint( obj, 1 );
Rlv$target(
obj,
spos,
.1,
time
);
// Add a time penalty for getting hit on the platform
if( score > 0 ){
setPlayerArousal(obj, getPlayerArousal(obj)+1);
setPlayerInvul(obj, 5); // Prevents being sent to checkpoint
setPlayerScore(obj, score+10);
addPlayerPenalty(obj, 10);
updateScore();
}
}
end
// When player uses a climbable object
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
// Store the key of the object they sat on
if( !seated )
stair = "";
setPlayerObstacle( hud, stair );
if( llKey2Name(stair) == "Shimmy Wall" )
Rlv$exitMouselook(player);
end
onTrigger( object, player, label )
integer team = getPlayerTeam(player);
if(
(label == "FINISH" && team == 0) ||
(label == "BFINISH" && team == 1)
){
assignScore(player);
if( GSETTINGS & GS_ROUND_STARTED ){
integer completion = isGameCompleted();
if( completion == 2 ){
GSETTINGS = GSETTINGS&~GS_ROUND_STARTED;
setTimeout("END_GAME", 3);
}
else if( completion == 1 && !CLOCKSTATE ){
++CLOCKSTATE;
setTimeout("CLOCK", 60);
updateClock();
}
}
// Set checkpoint and teleport the player
vector checkpoint = PLATFORM_A;
rotation rot;
if( team ){
checkpoint = PLATFORM_B;
rot = llEuler2Rot(<0,0,PI>);
}
checkpoint += llGetRootPosition();
setPlayerInvul(player, 5);
setPlayerCheckpoint(player, checkpoint, rot);
warpPlayerToSurface( player, checkpoint, rot, TRUE );
// Attach the balloon and create a balloon bar
Gui$createBar( player, "CD", "<.5,1,1>", "<.75,1,1>" );
Gui$tweenBar( player, "CD", 0, 0, 0 ); // Make it a smooth tween bar
LevelRepo$attach( player, "HUD:Water Balloon Att" );
Gui$setDefaultInstruction( player, Gui$INSTRUCTION_LCLICK_THROW, 0, 6 );
Gui$setInstruction( player, 0, CUSTOM_TEXTURE, 1, CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y, 6);
setTimeout("IN:"+(str)player, 6);
}
// Makes sure you can only use a checkpoint for your team
else if(
(llGetSubString(label, 2, 2) != "B" && team == 0) ||
(llGetSubString(label, 2, 2) == "B" && team == 1)
){
// Don't change for players on a platform
if( getPlayerScore(player) > 0 )
return;
setPlayerCheckpoint( player, prPos(object), prRot(player) );
}
end
handleTimer( "END_GAME" )
endGame();
end
handleTimer( "_WATER" )
vector gpos = llGetRootPosition();
if( GSETTINGS & GS_ROUND_STARTED ){
forPlayer( index, player )
float Z = 5;
if( getPlayerScore(player) > 0 )
Z = 11; // Knocked off the platform
vector pos = prPos(player);
if( pos.z < gpos.z+Z && !isPlayerInvul(player) ){
warpPlayerToSurface(
player,
getPlayerCheckpoint(player),
getPlayerCheckpointRot(player),
TRUE
);
}
end
}
end
handleTimer( "CLOCK" )
++CLOCKSTATE;
updateClock();
float t;
if( CLOCKSTATE == 2 )
t = 50;
else if( CLOCKSTATE == 3 )
t = 10;
else{
// Add penalties and end game
forPlayer( index, player )
int score = (int)getPlayerScore(player);
if( score == 0 )
addPlayerPenalty(player, 60);
assignScore(player);
end
updateScore();
endGame();
return;
}
setTimeout("CLOCK", t);
end
onTimer( id )
// Max arousal faded
if( llGetSubString(id, 0, 2) == "UF:" ){
key player = llGetSubString(id, 3, -1);
Rlv$unsetFlags(player, RlvFlags$IMMOBILE, TRUE);
Rlv$unSit( player, TRUE );
warpPlayerToSurface(
player,
getPlayerCheckpoint(player),
getPlayerCheckpointRot(player),
TRUE
);
setPlayerInvul(player, 5);
setTimeout("AR:"+(str)player, 2);
}
else if( llGetSubString(id, 0, 2) == "AR:" ){
key player = llGetSubString(id, 3, -1);
setPlayerArousal(player, 0);
Gui$setBarPerc( player, "AROUSAL", 0 );
}
end
#include "ObstacleScript/end.lsl"
#define USE_STATE_ENTRY
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
Select what functionality to use, and include the header files.
#define CUSTOM_TEXTURE "37c0bfa7-d9dc-a94d-2892-ac08ca6bd730"
#define CUSTOM_TEXTURE_X 4
#define CUSTOM_TEXTURE_Y 2
// The root prim is not exactly centered
#define X_OFFSET 2.6
// Position relative to root of the balloon platform
#define PLATFORM_A <-8.21873, -2.88553, 12.56812>
#define PLATFORM_B <8.21873+X_OFFSET, -2.88553, 12.56812>
// Start positions relative to root
#define START_A <-15.21873, 13.47408, 7.89429>
#define START_B <15.21873+X_OFFSET, 13.47408, 7.89429>
#define STARTROT llEuler2Rot(<0,0,-PI_BY_TWO>)
integer CLOCKSTATE; // When all players on one team have finished, start the clock
We setup some constants for a custom texture to set on the HUD as tooltips. This texture includes the timers and penalties. We default to the left side of the arena and define X_OFFSET for the right side since this is a mirrored arena. If you're not making a mirror, you probably won't need to bother with that.
PLATFORM_A/PLATFORM_B are positions of the two sky platforms. START_A, START_B, STARTROT are the starting positions for the players on each team.
CLOCKSTATE is a global that tracks the 2 min ending timer once a team has reached the final platform.
// The game data is stored in a list. Index 0 is ALWAYS the uuid of the player.
// We can define the other data we need to store about each player here, starting from 1
#define PD_CHECKPOINT 1 // Vector checkpoint position
#define PD_INVUL_UNTIL 2 // We'll put an llGetTime timestamp here for invul
#define PD_SCORE 3 // How long it took them to finish, in seconds
#define PD_AROUSAL 4 // int Arousal value
#define PD_OBSTACLE 5 // key UUID of the obstacle player is sitting on
#define PD_TEAM 6
#define PD_PENALTY 7 // Penalty seconds added when you reach the end
#define PD_ARMOR 8 // 2= dressed, 1 = underwear, 0 = bits
#define PD_CHECKPOINT_ROT 9 // Checkpoint rotation
// Put default values for above here
#define PD_DEFAULTS [ZERO_VECTOR, 0, 0, 0, "", 0, 0, 2, ZERO_ROTATION]
// Set this to the max value of our data fields +1. So if the max field index was 5, we set 6
#define PD_STRIDE 10
// Include the gamehelper here so it can use the values above
#include "ObstacleScript/resources/GameHelper.lsl"
GameHelper includes a list called PLAYER_DATA which tracks various things about each player. Such as their current checkpoint, invulnerability, arousal, etc. We get to defined this ourselves by setting up what the values should be, what the default values should look like, and how many elements we need to store.
The first part defines what the different indexes of this list should be, starting from 1 because 0 is ALWAYS the UUID of the player.
PD_DEFAULTS is a list that should contain the default value of each setting except the UUID. In this case the checkpoint value is set to ZERO_VECTOR, armor is set to 2 (fully dressed), default rotation is set to ZERO_ROTATION etc.
PD_STRIDE needs to be set to the total number of properties for each player. Basically take the last value (PD_CHECKPOINT_ROT) plus 1. So in this case since we defined 9 values, we set 10.
Finally we include the GameHelper so it can read the values we defined.
// Make some helper macros
#define INVUL_DURATION 5
#define isPlayerInvul( uuid ) \
(getPlayerDataInt(uuid, PD_INVUL_UNTIL) > llGetTime())
#define setPlayerInvul( uuid, duration ) \
setPlayerData(uuid, PD_INVUL_UNTIL, llGetTime()+duration)
#define getPlayerPenalty( uuid ) \
getPlayerDataFloat(uuid, PD_PENALTY)
#define addPlayerPenalty( uuid, seconds ) \
setPlayerData(uuid, PD_PENALTY, getPlayerPenalty(uuid)+seconds)
#define getPlayerArmor( uuid ) \
getPlayerDataInt(uuid, PD_ARMOR)
#define setPlayerArmor( uuid, amount ) \
setPlayerData(uuid, PD_ARMOR, amount)
#define setPlayerObstacle( uuid, obstacle ) \
setPlayerData(uuid, PD_OBSTACLE, obstacle)
#define getPlayerObstacle( uuid ) \
getPlayerDataKey(uuid, PD_OBSTACLE)
#define setPlayerScore( uuid, score ) \
setPlayerData(uuid, PD_SCORE, (float)score)
#define getPlayerScore( uuid ) \
getPlayerDataFloat(uuid, PD_SCORE)
This is optional, but making some macros for the GameHelper setPlayerData and getPlayerData functions for each index of the PLAYER_DATA list can make your code more readable. The \ is just a way to split a definition into multiple lines.
setPlayerData( key uuid, int index, var value)
stores some information about a player.
getPlayerData<type>( key uuid, int index )
retrieves it.
// Scoreboards
integer SCOREBOARDS; // 8 bit array
updateScore(){
list scores = [0,0];
integer i;
for(; i < count(PLAYER_DATA); i += PD_STRIDE ){
integer team = l2i(PLAYER_DATA, i+PD_TEAM);
scores = llListReplaceList(
scores,
(list)(l2i(scores, team)+l2i(PLAYER_DATA, i+PD_PENALTY)),
team,
team
);
}
string texture = "d72fed9b-e077-6ea1-fd98-9b28d879fd21";
for(i = 0; i < 2; ++i ){
integer prim = ((SCOREBOARDS >> (i*8))&0xFF);
string score = (string)l2i(scores, i);
integer f;
for( f = llStringLength(score); f < 6; ++f )
score = "0"+score;
list data;
for( f = 0; f < 6; ++f ){
integer n = (integer)llGetSubString(score, f, f);
data += (list)PRIM_TEXTURE + (2+f) + texture +
<0.0625, 1, 0> +
<-0.46875+n*0.0625, 0, 0> +
0
;
}
llSetLinkPrimitiveParamsFast(prim, data);
}
}
This was a special for this level in particular in that it has scoreboards that shows team penalties. SCOREBOARDS stores the two prims using bitwise operations. updateScore() just draws the score on the scoreboards. If you don't need scoreboards in your level you can disregard this.
#define MAX_AROUSAL 3
setPlayerArousal( key id, int amount ){
if( amount > MAX_AROUSAL )
return;
// Update the arousal bar
Gui$setBarPerc( id, "AROUSAL", (float)amount/MAX_AROUSAL );
setPlayerData(id, PD_AROUSAL, amount);
if( amount == MAX_AROUSAL ){
float score = getPlayerScore(id);
if( score > 0 )
setPlayerScore(id, score+30); // Add to the total score if they're on the platform
addPlayerPenalty(id, 30); // Always add to penalty. It's used both for the meter and added to score when they finish their run.
updateScore();
// Player reached max arousal
AnimHandler$anim(id, "got_pain_highpri", TRUE, 5, 0);
Rlv$setFlags(id, RlvFlags$IMMOBILE, TRUE);
// Set a timer to reset their position
setTimeout("UF:"+(str)id, 5);
integer armor = getPlayerArmor(id);
if( armor > 0 ){
int a = armor;
--armor;
setPlayerArmor(id, armor);
Rlv$setClothes( id, a, a, a, a, a );
}
}
}
#define getPlayerArousal( uuid ) \
getPlayerDataInt(uuid, PD_AROUSAL)
In our level we want to use an arousal bar. We define a MAX_AROUSAL
constant, and a function for setting max arousal. When MAX_AROUSAL is reached on a player, we add a penalty to them, decrease their armor level, make them immobile, start an animation on them, and set a timeout to bring them back to their last checkpoint.
#define setPlayerCheckpoint( uuid, pos, rot ) \
setPlayerData(uuid, PD_CHECKPOINT, pos); \
setPlayerData(uuid, PD_CHECKPOINT_ROT, rot)
#define getPlayerCheckpoint( uuid ) \
getPlayerDataVec( uuid, PD_CHECKPOINT )
#define getPlayerCheckpointRot( uuid ) \
getPlayerDataRot( uuid, PD_CHECKPOINT_ROT )
#define setPlayerTeam( uuid, team ) \
setPlayerData(uuid, PD_TEAM, team)
#define getPlayerTeam( uuid ) \
getPlayerDataInt(uuid, PD_TEAM)
More macros for dealing with checkpoints and teams.
// Callback to check player trap viability
integer canTrapPlayer( key trap, key player ){
return !isPlayerInvul(player);
}
// Checks if every player has a score assigned to them
// returns 2 if all players are done, returns 1 if all players on one team are done
integer isGameCompleted(){
integer teamsCompleted = 3;
forPlayer( index, player )
if( getPlayerScore(player) <= 0 ){
integer team = getPlayerTeam(player);
teamsCompleted = teamsCompleted &~ (1<<team);
if( !teamsCompleted )
return FALSE;
}
end
if( teamsCompleted == 3 )
return 2;
return 1;
}
We define some helper functions to see if we a player can be trapped, and to check how many teams have reached the finish line.
// Events
onGameStart(){
updateScore();
// Called when the game starts loading the meshes.
integer teams = l2i(GCONF, 0); // GCONF is set by DialogHelper on game start
// See #Dialog for GCONF definitions
CLOCKSTATE = 0;
// We can initiate the players here, by configuring them, setting up bars etc
forPlayer( index, player )
setPlayerTeam( player, (teams & (1 << index))>0 );
Rlv$setMaxSprint( player, 3 );
integer armor = 3;
Rlv$setClothes( player, armor, armor, armor, armor, armor );
// Create arousal bar
Gui$createBar( player, "AROUSAL", "<1,0,1>", "<1,.75,1>" );
Gui$setBarPerc( player, "AROUSAL", 0 );
// Preload custom instruction
Gui$preloadInstruction( player, CUSTOM_TEXTURE, 1 );
end
}
Next are some functions that are triggered automatically by GameHelper. First off is onGameStart which is triggered when the game starts. If the game isn't loaded, it's triggered when the game starts loading, and not when it's finished loading. Use this to prepare the players by setting up bars on the HUD, RLV rules, restoring clothes and setting up tooltips etc.
list onGameEnd(){
unsetTimer("CLOCK");
// Scores are sent to #Dialog, let's calculate an average for both teams
list scores = [0,0];
list numPlayers = [0,0];
forPlayer( index, player )
integer team = getPlayerTeam(player);
float score = getPlayerScore(player);
if( team == 0 || team == 1 ){
scores = llListReplaceList(
scores,
(list)(l2f(scores,team)+score),
team,
team
);
numPlayers = llListReplaceList(
numPlayers,
(list)(l2i(numPlayers, team)+1),
team,
team
);
}
end
integer i;
for( ; i < count(scores); ++i ){
if( l2i(numPlayers, i) ){
scores = llListReplaceList(
scores,
(list)(l2f(scores, i)/l2i(numPlayers, i)),
i, i
);
}
}
integer winner = l2f(scores, 1) > 0 && l2f(scores, 0) > l2f(scores, 1);
string winnerText = "TEAM "+(str)winner+" wins!\n";
winnerText += "Average Scores:\n";
winnerText += "Team 0: "+l2s(scores, 0)+" sec\n";
winnerText += "Team 1: "+l2s(scores, 1)+" sec\n";
forPlayer( _idx, player )
llRegionSayTo(player, 0, winnerText);
Gui$removeBars( player, "AROUSAL" + "CD" );
LevelRepo$detach( player, "HUD:Water Balloon Att" );
Gui$clearInstruction( player, 0 );
Gui$clearInstruction( player, 1 );
end
return scores;
}
onGameEnd is triggered when the game ends. It should return a list of scores that are automatically passed to #Dialog. In this function we clean up attachments, bars, unset timers and RLV restrictions from players. We can also output text of the winners to each player in chat.
onRoundStart(){
// We only have one round in this level
// So in this case, it's only raised when the level has loaded
forPlayer( index, player )
integer team = getPlayerTeam(player);
vector startPos = START_A;
if( team )
startPos = START_B;
startPos += llGetRootPosition();
// Set the sprint instruction on screen
Gui$setDefaultInstruction( player, Gui$INSTRUCTION_SPRINT, 0, 6 );
Gui$setInstruction( player, 1, CUSTOM_TEXTURE, 1, CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y, 6);
// Warp player to the start
setPlayerCheckpoint(player, startPos, STARTROT);
warpPlayerToSurface( player, startPos, STARTROT, FALSE );
end
}
onCountdownFinished(){
// Unsit is handle automatically
}
onRoundStart is triggered when a new round starts (when the countdown is first presented), including the first round of the game. Set player positions and instructions.
onCountdownFinished is triggered when the countdown is finished. There's not much that needs to be done as the GameHelper will automatically unsit all players when the round starts.
updateClock(){
if( !CLOCKSTATE || CLOCKSTATE > 3)
return;
integer inst = 2;
if( CLOCKSTATE == 2 )
inst = 3;
else if( CLOCKSTATE == 3 )
inst = 4;
forPlayer( index, player )
Gui$setInstruction(
player,
inst,
CUSTOM_TEXTURE,
2,
CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y,
6
);
end
}
assignScore( key player ){
// Here we use the automatic value ROUND_START_TIME to calculate how long it took
float score = getPlayerScore(player);
if( score > 0 )
return;
setPlayerScore( player, llGetTime()-ROUND_START_TIME+getPlayerPenalty(player) );
}
More helper functions. updateClock() is handled to send the "x minutes left" tooltips to players. assignScore combines the obstacle course run time of a player with any penalties they had when they reached the end of the course.
// ObstacleScript events start
#include "ObstacleScript/begin.lsl"
Finally we reach the obstacleScript event handler.
// Sets up automation of the game handler
gameHelperEventHandler();
//gameHelperAutoWater(5); // Can be used to automate falling in the water
//gameHelperHandleBalloonHit( 4, isPlayerInvul ) // Can be used to automat balloon hits
gameHelperEventHandler() is important as it handles a bunch of events automatically for you through the GameHelper.
gameHelperAutoWater() lets you automatically handle players falling into the water. It can't be used on this level though as it needs to track players falling off the balloon platforms as well.
gameHelperHandleBallonHit() can be used to handle pushing players hit by balloons. This level uses a custom handler though since it needs different logic based on if the player is on a platform or is running the obstacle course.
onStateEntry()
gameHelperStateEntry();
setInterval("_WATER", 3);
links_each(nr, name,
if( llGetSubString(name, 0, 9) == "ScoreBoard" ){
integer n = (integer)llGetSubString(name, -1, -1);
SCOREBOARDS = SCOREBOARDS | nr << (n*8);
}
)
updateScore();
end
gameHelperStateEntry(); is required to initialize the gameHelper
setInterval("_WATER", 3) sets up a timer we'll use for checking when a player has fallen into the water
the links_each loop checks for the scoreboard prims and saves them to SCOREBOARDS
updateScore(); updates the numbers on the scoreboards back to 0
onTrapHit( trap, players )
// Pass a callback function
_TRAPS_autoSeat( canTrapPlayer );
end
// Handle unsitting from a trap
onTrapUnseated( trap, players )
integer i;
for(; i < count(players); ++i ){
key player = l2k(players, i);
setPlayerInvul(player, INVUL_DURATION);
setPlayerArousal(player, getPlayerArousal(player)+1);
}
end
Here we handle the traps. _TRAPS_autoSeat( function filter )
is defined in headers/Obstacles/Trap.lsh (the TrapHelper) and it automatically sits the player on the trap if they're viable. Filter should be a function that returns true or false whether a player is allowed to be trapped. Usually an invulnerability check. In our case we use the canTrapPlayer function we defined earlier. When a player unsits from a trap we add an invulnerability so they don't get immediately re-trapped and add 1 arousal.
onShimmyWallHit( object, start, players )
if( !start )
return;
integer i;
for(; i < count(players); ++i )
setPlayerArousal(l2k(players, i), getPlayerArousal(l2k(players, i))+1);
end
Handles the shimmy wall and adds arousal when a player gets hit by a water beam.
onProjectileHit( projectile, obj )
if( llGetAgentSize(obj) != ZERO_VECTOR ){
if( llGetAgentInfo(obj) & AGENT_SITTING || isPlayerInvul(obj) )
return;
float score = getPlayerScore(obj);
vector owner = prPos(llGetOwnerKey(projectile));
vector spos = prPos(obj);
vector offs = spos-owner;
offs.z = 0;
float time = 0.5;
float push = 2;
if( score > 0 )
push = 4;
spos += llVecNorm(offs)*push;
Rlv$damageSprint( obj, 1 );
Rlv$target(
obj,
spos,
.1,
time
);
// Add a time penalty for getting hit on the platform
if( score > 0 ){
setPlayerArousal(obj, getPlayerArousal(obj)+1);
setPlayerInvul(obj, 5); // Prevents being sent to checkpoint
setPlayerScore(obj, score+10);
addPlayerPenalty(obj, 10);
updateScore();
}
}
end
Handles the water balloons. Pushes a player and if they're on the platform we push them a larger distance and add a penalty. Also damages sprint.
// When player uses a climbable object
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
// Store the key of the object they sat on
if( !seated )
stair = "";
setPlayerObstacle( hud, stair );
if( llKey2Name(stair) == "Shimmy Wall" )
Rlv$exitMouselook(player);
end
When a player uses a ladder type obstacle, this event is raised. We simply store the obstacle in their player data. If the obstacle was a shimmy wall we also force the player out of mouselook.
onTrigger( object, player, label )
integer team = getPlayerTeam(player);
if(
(label == "FINISH" && team == 0) ||
(label == "BFINISH" && team == 1)
){
assignScore(player);
if( GSETTINGS & GS_ROUND_STARTED ){
integer completion = isGameCompleted();
if( completion == 2 ){
GSETTINGS = GSETTINGS&~GS_ROUND_STARTED;
setTimeout("END_GAME", 3);
}
else if( completion == 1 && !CLOCKSTATE ){
++CLOCKSTATE;
setTimeout("CLOCK", 60);
updateClock();
}
}
// Set checkpoint and teleport the player
vector checkpoint = PLATFORM_A;
rotation rot;
if( team ){
checkpoint = PLATFORM_B;
rot = llEuler2Rot(<0,0,PI>);
}
checkpoint += llGetRootPosition();
setPlayerInvul(player, 5);
setPlayerCheckpoint(player, checkpoint, rot);
warpPlayerToSurface( player, checkpoint, rot, TRUE );
// Attach the balloon and create a balloon bar
Gui$createBar( player, "CD", "<.5,1,1>", "<.75,1,1>" );
Gui$tweenBar( player, "CD", 0, 0, 0 ); // Make it a smooth tween bar
LevelRepo$attach( player, "HUD:Water Balloon Att" );
Gui$setDefaultInstruction( player, Gui$INSTRUCTION_LCLICK_THROW, 0, 6 );
Gui$setInstruction( player, 0, CUSTOM_TEXTURE, 1, CUSTOM_TEXTURE_X, CUSTOM_TEXTURE_Y, 6);
setTimeout("IN:"+(str)player, 6);
}
// Makes sure you can only use a checkpoint for your team
else if(
(llGetSubString(label, 2, 2) != "B" && team == 0) ||
(llGetSubString(label, 2, 2) == "B" && team == 1)
){
// Don't change for players on a platform
if( getPlayerScore(player) > 0 )
return;
setPlayerCheckpoint( player, prPos(object), prRot(player) );
}
end
Here we handle the triggers (checkpoints). When a player reaches the finish, we merge their penalties with their end time, set their checkpoint to the platform and warp them there. Then we create the balloon throwing bar and attach a balloon to them. We also check if all players have reached the end. If all players of one team has reached the end, we set the 2 min timer. If everyone has reached the end, we set a 3 sec timer to finish the game.
If the checkpoint wasn't the end one, we just update their checkpoint position.
handleTimer( "END_GAME" )
endGame();
end
handleTimer( "_WATER" )
vector gpos = llGetRootPosition();
if( GSETTINGS & GS_ROUND_STARTED ){
forPlayer( index, player )
float Z = 5;
if( getPlayerScore(player) > 0 )
Z = 11; // Knocked off the platform
vector pos = prPos(player);
if( pos.z < gpos.z+Z && !isPlayerInvul(player) ){
warpPlayerToSurface(
player,
getPlayerCheckpoint(player),
getPlayerCheckpointRot(player),
TRUE
);
}
end
}
end
handleTimer( "CLOCK" )
++CLOCKSTATE;
updateClock();
float t;
if( CLOCKSTATE == 2 )
t = 50;
else if( CLOCKSTATE == 3 )
t = 10;
else{
// Add penalties and end game
forPlayer( index, player )
int score = (int)getPlayerScore(player);
if( score == 0 )
addPlayerPenalty(player, 60);
assignScore(player);
end
updateScore();
endGame();
return;
}
setTimeout("CLOCK", t);
end
These are 3 specific timer handlers.
- "END_GAME" just ends the game.
- "_WATER" is used to check if a player has fallen into the water or off their platform. And returns them to their last checkpoint.
- "CLOCK" is used for the "2 minutes remaining" clock
onTimer( id )
// Max arousal faded
if( llGetSubString(id, 0, 2) == "UF:" ){
key player = llGetSubString(id, 3, -1);
Rlv$unsetFlags(player, RlvFlags$IMMOBILE, TRUE);
Rlv$unSit( player, TRUE );
warpPlayerToSurface(
player,
getPlayerCheckpoint(player),
getPlayerCheckpointRot(player),
TRUE
);
setPlayerInvul(player, 5);
setTimeout("AR:"+(str)player, 2);
}
else if( llGetSubString(id, 0, 2) == "AR:" ){
key player = llGetSubString(id, 3, -1);
setPlayerArousal(player, 0);
Gui$setBarPerc( player, "AROUSAL", 0 );
}
end
This is a generic timer event that handles all timers and puts their id into the string variable id
.
- UF: is triggered a few sec after a player has reached max arousal, and is used to teleport them back to a checkpoint.
- AR: is triggered a few sec after above and resets their arousal bar.
#include "ObstacleScript/end.lsl"
Ends the script.