;--[[ ; since we include a Lua script at the end, the top half is placed in a Lua block comment ; ; Example synSpace:Drone Runners StarMap ; ; synSpace is an Arcadian toy, and now, an Android game, for up to 8 players. ; For more information, please visit http://www.synthetic-reality.com ; ; Windows Version (c) 2001 synthetic-reality.com all rights reserved ; Android Version (c) 2013 synthetic-reality.com all rights reserved ; ; ----- ; ; "Slaves of Improovium" ; ; (c) 2016 Dan Samuel, synthetic-reality.com, all rights reserved ; ; This particular starmap is both the tutorial and the API development/demo ; map. You are free to re-use it, in whole or in part, for your own starmaps, ; used with synSpace: Drone Runners. ; ; For purpose of this document, a starmap describes a 'star group' somewhere in ; our galaxy. It's a big galaxy, bigger than a single starmap. This document ; occasionally will say 'galaxy' where it ought to say 'star group'. ; ; The starmap describes a square 8192 units on a side. ; ; The 'visible grid lines' are spaced at 256 unit intervals. ; ; The (x, y) coordinate system looks like this: (a 'top' view of space) ; ; (0, 8191) (8191,8191) ; +---------N---------+ ; | | ; | | ; | | ; | | ; W o E ; | | ; | | ; | | ; | | ; +---------S---------+ ; (0, 0) (8191, 0) ; ; NOTE: barriers too close to the edge of the map will not work as expected. ; keep them at least 20 units from edge. ; ; In general, avoid the edges of the map. In specific, avoid negative coordinates ; as many tables will treat that as a command to "generate a random value" ; ; From the player's perspective, the map WRAPS when you cross an edge. ; ; Map files have two parts, a mandatory TOP HALF that contains a static description of a ; star system, and an optional BOTTOM HALF that includes whatever Lua scripting ; you feel like adding for a more dynamic environment. ; ; Things your map file TOP HALF can control: ; ; * Barriers (bouncy, transparent, hurtful, or healing) ; * Colors of barriers ; * Pain levels associated with barriers ; * Player "New Ship" locations ; * Power Up spawn locations and periods ; * Gravity Objects (suns, planets, black holes) ; * Rectangular Zones with special properties ; ; Things your optional BOTTOM HALF Lua Scripting can control ; ; * dynamically modify most of the settings from the top half ; * state machine based scenes, bots, and games of your own design ; * i.e. missions, quests, adventures ; * these can be solo, or multiplayer, with your copy of the ; script talking to other players copies of their scripts. ; ; --------------------------------------------------------------------------- ; ; START OF TOP HALF ; ; The general format is that of an 'INI' file with category section names in ; square brackets, followed by one or more name=value pairs in that ; category. Anything on a line after a semicolon is just a comment. ; ; Comments and blank lines are not allowed within a category, ; just in front of it. You know, since these are probably never going to ; be back-compatible with Arcadia SynSpace, I could really just improve ; the parser in this version. Someone should remind me to do that :-) ; ; Values are often comma-delmited strings with many individual field values ; in a single line. use double quotes, if required, in individual fields ; ;----------------- ; CATEGORY: CREDITS ; ; This first category is required and should do the best possible job of ; crediting everyone's work, without violating anyone's privacy. ; ; When crafting your own StarMap, you can declare as much info about yourself as ; you like, using whatever keywords you like, but the ; game engine will only use certain keywords (declared here) in the display. ; ; Also, please keep it PG-13. Not because The Man is holding you down, ; but because it's the decent way to treat strangers. ; ; The final tags and how they are each used, is somewhat still in flux, but... ; ; mapname The player-friendly name of your map, may include spaces ; maproot An id unique to you, the author, that defines what campaign this map is part of ; campaign's share player data (i.e. progress on one map can affect state on another ; if they are part of the same campaign) ; alphanumeric only, no spaces allowed ; mapdesc player-friendly description of your map. Is seen in the 'discovered by' section ; author What you, the author would llike to be called. Probably should accept longer strings for "Bill and Dave of SpaceX" authorships ; sernum in theory, the author sernum. But I don't use that (too weird typing in your own sernum anyway) ; version in theory, an author might want to track/control this. ; email traditinally, the email the author would like to be contacted at, if any ; info something so similar to mapdesc, that I have no idea which one will win in the coming UI wars ; tags another vague promise.. some sort of tag language that can be used to filter the server ; instance list, should it become long and unwieldy ; [credits] mapname = Alpha Debuggeri maproot = samsynAPI0 mapdesc = API Test/Demonstration/Tutorial map author = Samsyn sernum = 1 version = 1.001 email = dan@synthetic-reality.com info = You should be able to copy and paste the boilerplate into your map, then add your scenes and bots. tags = API ;--------------- ; CATEGORY: SPAWN ; ; New Ship Spawn Points ; ; Up to 8 human players may control drones (additional bot drones provided as needed for NPCs). ; ; NOTE: Players think of themselves as a 'color' and their index number is not displayed. However, ; each player DOES have an index number. I apologize in advance, but sometimes those indices are ; the range 1 - 8, and sometimes they are the range 0 - 7. You just have to read my mind and do ; the right thing. ; --- ; ; Each of the eight ships (1-8) can be assigned a spawn point. That means, when ship N ; enters the galaxy, it will always appear in a certain spot. ; ; This is probably only a good idea if that certain spot holds some protective features, since ; otherwise people will simply wait by the spawn point (or surround it with mines). ; ; If you don't call out a spawn point for a ship, then that ship will spawn at a random location ; (well, semi-random... around the edge of the galaxy, so you can keep your gravity objects ; near the center to minimize the chance of spawning inside a sun. ; ; Note, for TEAM-BASED maps, you probably want to put all the team-ships spawn points near each ; other (and perhaps near the team's home base thingy, which it ought to have). ; ; Format of an entry is: ; ; Ship# = x, y, heading ; ; use negative values for x and y to get a random location.. or just don't include the entry. ; ; The heading argument is optional, but should (if included) be in the range 0-359 (degrees from galactic North) ; ; This map doesn't want a spawn table... but I'll do one anyway, just for ship #1.. for now... ; This actually puts ship 1 at risk of spawning in a star, since -1 will randomly pick any value ; So, OMITTING the line is better, if you really want random startup. I should probably add a -2 ; to explicitly be random-avoiding-the-center. ; ; X Y HDG [spawn] 1= -1, -1, 0 ;----------------- ; CATEGORY: STARS ; ; adds STARS, Planets, etc. (Gravity Objects) ; ; Your map may have up to 16 gravity objects on it. But that would be INSANITY. ; ; It is recommended that they be kept near the center of the galaxy, since gravity does ; not wrap over the galaxy boundaries. ; ; However, as you will see, you have a lot of setup possibilities with each star ; ; There are many numbers associated with stars, so count carefully :-) ; ; Format of an entry is: ; ; StarID = style, x, y, mass, reach, temperature, radius, red, green, blue, imageId ; ; That's 11 parameters per star. ; ; * Style ; make this 0 to turn the star OFF, 1 to turn it ON. ; ; note: if you do not explicitly turn off star 0, the map will default to ; ; one star in the center of the galaxy. ; ; * x, y ; the location of the star. (negative values will NOT make random stars) ; ; * mass ; This controls the pull of its gravity. The star you are used to ; ; has a mass of 1000 ; ; * reach ; unlike real mass, gravity extends no further than this from the star ; ; The default star has a reach of 2048 ( 8 grid segments) ; ; * temp ; The temp of a star controls how much it hurts when you get close to it; ; ; A negative temperature will make a healing star. ; ; The default star has a temp of 30. ; ; * radius ; approximately the physical radius of the star. Please don't make huge stars ; ; because they will look goofy. The default star has a radius of 128 ; ; for an invisible star, use a radius of 0. ; ; * R,G,B ; The red,green,blue values control the color of the star. Each is a ; ; decimal number between 0 and 255. The android version is not paletted, so ; ; you can use any color you like. ; * imageId ; if this value is greater than 0, then a bitmap is drawn instead of a colored circle ; ; Image ids refer to assets baked into the game, over which you have no control. ; ; which means you could use weird images, in addition to the official planet images. ; ; ; ; currently ids 28 - 46 are various planet images. But all the art is in there ; ; (so hello, planet 'page 4 of users guide'!) ; ; if someone were to send me free art, I would not object to adding it to the game for all to share ; ; for now, that is the only way to include bitmaps of higher resolution than FACE assets. ; ; (though the dream remains to let you import art via url (which basically then all players ; ; would also do, when playing your map, with some appropriate degree of caching) But you would ; ; have to guarantee persistent urls. So, for now, when you see 'imageId' it always means a ; ; baked in piece of art. But someday some rule like "negative imageIds actually use an index ; ; into a map-provided url list which sources the individual images" ; ; but again, that's not today. ; ; style x y mass reach temp radius red grn blu img [stars] 0 = 1, 4096, 4096, 1000, 2048, 30, 128, 200, 200, 0, 28 ;1 = 1, 300, 300, -100, 2048, -30, 128, 200, 200, 200, 0 ; repulsive gravity, healing star, white, near fuel depot ;2 = 1, 4096, 3096, 1000, 2048, 30, 128, 100, 100, 50, 0 ;3 = 1, 3096, 3096, 1000, 2048, 30, 128, 100, 20, 150, 0 ;----------------- ; CATEGORY: COLORS ; ; You may define up to 64 colors (color indices 0-63) to be used with the barriers ; Some colors are hard-wired, but others you can set to any RGB value you like ; This also controls the colors of the NPC ships. ; ; 0 = anything you like, defaults to GOLD. Default barrier color. ; ; Ship colors 1-8 are hardwired ; ; RED 1 2 BLUE ; GOLD 3 4 GREEN ; YELLOW 5 6 PURPLE ; WHITE 7 8 'BLACK' (gray) ; ; 9 - 63, anything you like, but defaults to the FACE editor palette ; ; Colors are defined as three decimal numbers (0-255) in the order Red, Green, Blue. ; ; red grn blu [colors] 0 = 255, 204, 0 ; gold 9 = 204, 51, 104 ; reddish (west team) 10= 53, 103, 255 ; blueish (east team) ; ;----------------- ; CATEGORY: PAINS ; ; You may define up to 16 pain levels (pain indices 0-15) to be used with barriers. ; Each barrier can then be assigned to one of these pain levels (default to level 0) ; ; The ship is then damaged by this amount when it touches the barrier. Note that ; flying parallel and close to a barrier might inflict pain multiple times. ; ; It is advised to keep pain level 0 set to no damage. ; ; This pain is de-rated by the ship's shields. ; ; Note: Negative pain HEALS the ship! (and is NOT de-rated by shield) [pains] 0= 0 1= 100 2= 500 3=-1000 ;----------------- ; CATEGORY: HORIZONTAL ; ; horizontal barriers ; ; format is: id=left, right, top, color, xpar, pain, group ; ; id number should be from 0 to 99 (100 lines max) ; ; left (must be less than right) ; 0 (far left) to 8191 (far right) ; ; right ; 0 - 8191 ; ; top ('y' value of horizontal line) ; 0 (bottom-most) - 8191 (top-most) ; ; color (optional): ; 0 color table index 0 ; 1 color table index 1 ; ... ; 63 color table index 63 ; ; xpar (optional) ; 0 bouncy wall ; 1 wall that ship 1 can go right through (private door) ; 2 wall that ship 2 can go right through (private door) ; ... ; 8 wall that ship 8 can go right through (private door) ; 9 wall that WEST team can go through (ships 1, 3, 5, and 7) ; 10 wall that EAST team can go through (ships 2, 4, 6, and 8) ; 11 wall that ALL SHIPS can go through ; 12 wall that NW can go through ; 13 wall that NE can go through ; 14 wall that SW can go through ; 15 wall that SE can go through ; ; Then, sorry.. ADD ONE HUNDRED if bullets can go through it. ; ; So, 111 = transparent to all ships and bullets. More of an open window than a wall ; ; pain (optional) ; 0 pain table index 0 (usually you should set that to 0 pain) ; 1 pain table index 1 ; ... ; 15 pain table index 15 ; ; group (optional, assumed 0 - no group) ; individual barrier line segments (horz and vert) can be optionally ; bound to a group Id. The script can then treat all the barriers of ; a group as a unit. Basically so you can create a 'door' out of one ; or more barriers, then the script can enable/disable the group as ; needed to open/close the 'door'. ; ; You might prefer to leave this section mostly blank and then let ; your script create the barriers it needs programmatically, but ; this table is what can be edited in-game, visually (someday) ; ; On this map, we declare two FLAG zones(4/5 abd 6/7), ; and a 'garage' (0/1/2/3) where you can recharge. Note that ; there are also VERTICAL barriers for those same zones ; ; left, right, top, color, xpar, pain, group [horizontal] 0 = 19, 256, 20 1 = 19, 100, 255 2 = 100, 155, 255, 6, 111 3 = 155, 256, 255 4 = 1948, 2148, 1948, 9, 11, 0 5 = 1948, 2148, 2148, 9, 11, 0 6 = 6044, 6244, 6044, 10, 11, 0 7 = 6044, 6244, 6244, 10, 11, 0 ;----------------- ; CATEGORY: VERTICAL ; ; vertical barriers ; ; format is: id=bottom, top, left, color, xpar, pain, group ; ; Arguments are same as for horizontal barriers, except you provide the bottom and ; top of a vertical line segment, at 'x' position 'left' ; ; bottom must be less than top (increasing y is up the screen) ; ; bottom, top, left, color, xpar, pain, group [vertical] 0 = 19, 256, 20 1 = 19, 256, 255 2 = 100, 125, 45, 2, 0, 3 3 = 100, 125, 225, 1, 0, 2 4 = 1948, 2148, 1948, 9, 11, 0 5 = 1948, 2148, 2148, 9, 11, 0 6 = 6044, 6244, 6044, 10, 11, 0 7 = 6044, 6244, 6244, 10, 11, 0 ;----------------- ; CATEGORY: POWERUPS ; ; Powerups are objects which appear at specific or random locations at specified time intervals ; and which can be 'picked up' by passing ships. This table lets you 'schedule' which powerups ; are available on your map, where they spawn, and how long until they respawn. ; ; Think of this list as the source of the 'pyramids' themselves and their scheduled ; appearance times and locations. This is NOT where you define new kinds of weapons ; and such. This is just how you schedule their periodic appearance for pickup. ; ; Format is: id= style, x, y, seconds ; ; ID ; 0-255 (each pup occupies one 'slot' in a 256 entry table) ; ; Style (what sort of pup it is. You might have the same style of pup in several slots) ; 0 No such pup, only use this if you are too lazy to delete table entries ; 1 RESERVED ; 2 Pack of Homing Missiles ; 3 Pack of Plasma Mines ; 8 Ship WEAPON upgrade ; 9 Ship SHIELD upgrade ; 10 Ship ENGINE upgrade ; 11 100% Energy Restore ; 12 20% Energy Restore ; 13 Concealed Trap ; 14 Warp Coil ; 15 Plasma Shield ; 22 Encrypted Starmap ; ... reserved for future stock pups ; 100 android: start of map-defined pup Ids ; 199 android: absolute final map-defined value ; when in no teams mode ; 240 Team 0 Flag (no ships belong to this team) ; when in 8 team mode ; 241 Ship 1 Flag (usually only WEST and EAST flags should be used) ; ... ; 248 Ship 8 flag (only ship 8 belongs) ; ; when in 2 team mode: ; 249 WEST Team Flag (ships 1, 3, 5, and 7 belong) ; 250 EAST Team Flag (ships 2, 4, 6, and 8 belong) ; ; 251 ALL Team flag (all ships belong to this team, for collaborative maps) ; ; when in 4 team mode: ; 252 NW Team Flag (ships 1, 3) ; 253 NE Team Flag (ships 2, 4) ; 254 SW Team Flag (ships 5, 7) ; 255 SE Team Flag (ships 6, 8) ; ; (missing numbers represent things which are not yet implemented, but reserved for future development) ; ; X X-Location of powerup on map ; -1 Pick a random location ; 0-8160 Specific location (rounded to closest 1/256th of Galaxy) ; so valid values are 0, 32, 64, 96, ... 8128, 8160. ; any other values will be 'rounded down' to closest multiple of 32. ; ; Y Y-Location of powerup on map (same units as X) ; ; secs How many seconds elapse after the powerup is picked up, before a new one spawns in that slot ; 0-N ; ; Note: The first 20 slots or so are automatically filled for you with random powerups. So if you ; Don't override the first 20 slots, that is what you will get. Sort of like the description for ; STARS. If you *do* override those slots, then your map takes precedence over the defaults. ; ; on THIS map, we accept the default pups from slots 0 to 20, and add: ; ; 22 is encrypted map drop ; 249 WEST team flag, ; 250 EAST team flag ; ; pupId, X, Y, seconds [powerups] 22 = 22, -1, -1, 600 249 = 249, 2048, 2048, 600 250 = 250, 6144, 6144, 600 ;----------------- ; CATEGORY: ZONES ; ; Zones are rectangular regions with special properties applying to ships and bullets which ; pass in and out of them. ; ; Since the rectangles might overlap, this list is processed in reverse order and the ; first 'hit' controls the behaviour of that point. So the first entry on the list is the ; last to be considered and should be the 'largest' zone. If your zones do not overlap, then ; ignore this paragraph. But you might want the first zone in the list to do something like ; span the entire galaxy so you can set some 'global' behaviour (no bullets, for example) which ; is then overridden while in smaller zones defined later in the list. ; ; Just to keep things happy, number your zone IDs 0-N from top to bottom. Belt and suspenders that ; way. ; ; Format is: id= style, left, top, right, bottom, texture, team, pain, bullets, wayPtID, friction, cx, cy, group ; ; ID (0-99) Although you may define up to 100 zones, your map will be faster if you define fewer. ; 0-99 ; ; Style (what sort of zone it is, if it has any super special purpose) ; 0 Disabled Zone ; 1 Normal Zone, no special meaning (other than properties) ; 2 GOAL Zone (team set by 'team' property) (earn points by dropping FLAGs here) ; 3 WayPoint Zone (wayPtID property sets additional info) ; ; left, top, right, bottom (Coordinates of sides of zone rectangle) ; 0-8191 ; ; Texture (reserved) ; 0 ; ; Team (used to interpret pain and CTF HOME) ; 0 belongs to no one ; 1-8 Specific Ships 1-8 ; 9 WEST Team (valid for CTF HOME) ; 10 EAST Team (Valid for CTF HOME) ; 11 All Ships ; 12 NW Team Flag (ships 1, 3) ; 13 NE Team Flag (ships 2, 4) ; 14 SW Team Flag (ships 5, 7) ; 15 SE Team Flag (ships 6, 8) ; ; Pain (Applies only to ships of specified team) ; 0 No pain/heal ; >0 Hurts all ships EXCEPT those matching team property ; <0 Heals ONLY those ships which match team property ; Units are sweeten to taste, but 10 drains you pretty fast, and -100 charges you quickly ; ; Bullets (What happens to weapons inside this region) ; 0 No special effect ; 1 Ship trigger is disabled, but bullets can live ; 2 Ship trigger is OK, but bullets expire ; 3 Trigger and bullets are disabled. ; ; WayPtID (Valid for waypoint style only) ; 0 START position (stopwatch is cleared while in here, starts running when you exit) ; 1 FIRST Waypoint (waypoints must be crossed in order, stopwatch split time kept per waypt ; 2.. Additional Waypoints as needed ; -5 A negative waypoint means the END of the race and the final stopwatch is shown. ; So, a complete race would number its waypoint zones: 0, 1, 2, 3, 4, ..., 12, 13, -14 ; (You can have as many waypoint zones as fit) ; ; Friction (Volte6's cool idea) ; 0 Normal space ; 1-100 you coast to a stop if you don't apply thrust. At 100 you stop almost at once ; <0 Undefined, but I will try to make it boost your speed or do something 'interesting' ; ; cx, cy (river current) ; 0 Normal Space ; +/-N Adds this velocity component to your normal motion, to create a sort of conveyor belt/river ; Effect (hard to go upstream, for example) Good for that "pulling people towards danger" ; effect. A value of 50 is a slow current, 500 is medium. Sweeten to taste. ; ; group (assumed 0, no group) ; if you assign a zone to a group, then the script can refer to all zones in that group ; as a collection. Not sure if that means anything yet, but that's the plan ; id= style, left, top, right, bottom, texture, team, pain, bullets, wayPtID, friction, cx, cy, group ; ; For some reason, I seem to need a picture to work this out. ; ; * for a 2 team map, WEST (4 players on red side) ; vs EAST (4 players on blue side) ; ; ; 6k ; [EAST] zone 10 ; Team 10 ; Flag 250 ; ; 4k ; o ; STAR ; ; 2 k ; [WEST] zone 9 ; Team 9 ; Flag 249 ; ; [garage] ; ; ; Grab the enemy flag from THEIR zone, and drop it back in your OWN zone for a goal ; ; Format is: id= style, left, top, right, bottom, texture, team, pain, bullets, wayPtID, friction, cx, cy, group ; ; zones must be in priority order, larger earlier in list ; zone 1 a large zone around star ; zone 9 home/goal zone for WEST team, and home of WEST flag - 249 ; zone 10 home/goal zone for EAST team, and home of EAST flag - 250 ; ; ; st left top rigt btm tex team pain bul wp fr cx cy gp [zones] 1 = 2, 3000,3000,7000,7000, 0, 0, 0, 0, 0, 0, 0, 0 9 = 2, 1948,1948,2148,2148, 0, 9, 20, 3, 0, 0, 0, 0 10 = 2, 6044,6044,6244,6244, 0, 10, 20, 3, 0, 0, 0, 0 ;----------------- ; CATEGORY: PROPS ; (UNDER DEVELOPMENT) ; ; Basically this is a list of name-value pairs that control elements of ; the game engine. You cannot add props willy nilly, as we accept only ; stock property names ; ; --------- ; Property: NumTeams ; ; 0 - no teams (every man for himself) ; 1 - fully human coop (can't hurt other humans) ; 2 - classic Left vs Right ; 4 - NW vx NE vs SW vs SE ; 8 - every man for himself, but with flags possible ; ; In general, team-mates are treated the same as you l ; Thumbs(ships 0-7) are assigned to teams (0-3) like this ; ; [0:RED | BLUE:1] ; [2:GOLD 12 | 13 GREEN:3] ;----------------+------------------ <-- (numTeams=4) adds this split ; [4:YELLO 14 | 15 PURPLE:5] ; [6:WHITE | BLACK:7] ; ; Team IDs (must be interpreted in context of numTeams) ; ; all modes ; 0 'belongs to no team' ; 8 team mode ; 1-8 'belongs to team: player N-1' ; 2 team mode ; 9 West Team ; 10 East Team ; 1 team mode ; 11 Cooperative (all player ships) ; 4 team mode ; 12 NW Team (ships 0, 2) ; 13 NE Team (ships 1, 3) ; 14 SW Team (ships 4, 6) ; 15 SE Team (ships 5, 7) ; ; --------- ; Property: SafeWeapons ; ; Can your own bullets hurt you? ; ; Value ; ; 0 ; your weapons are only safe for a moment after you fire. ; 1 ; your weapons will never hurt you or your team mates ; [props] NumTeams = 2 SafeWeapons = 0 ShowHandbook = 0 ;------------------------------------------------------------------------------ ; START OF BOTTOM HALF OF STARMAP FILE ; (the goal is for this boilerplate to be easily reusable for other maps) ; ;----------------------------------------------------------------------------------------- ;--START OF BOILERPLATE-------------------------------------------------------------------- ;----------------------------------------------------------------------------------------- ; this last category declares the end of this starmap, as everything past this tag is lua syntax. ; this category is completely optional ; ;]]-- In theory, the entire top half is inside a lua block comment [SCRIPT] -- This will appear as line 1 of your Script Window SOURCE -- lua does not use semicolons, comments start with two dashes -- NOTE TO AUTHORS: The only thing you are STUCK with is the onMSG() handler by -- which the game invokes you (when something interesting has happened) -- How you handle those messages, is up to you. -- Here, I am developing a sort of object-centric view of the Starmap, where enough -- infrastructure is supplied that I can focus on just writing the story (scenes) -- and populating the characters (bots) -- http://www.synthetic-reality.com <-- rummage around there to find actual -- documentation of the game.API, which is likely to just be a copy of this file! -- please send us email if you have questions or suggestions -- synspace@synthetic-reality.com -- bots and scenes are defined (by you) as lua tables, with inheritance. -- so you can hierarchical-ly organize your properties with minimum typing. -- I'm trying to support a sort of 'sceneplay' feel to the file, but in this -- case the set directions can be executable code. -- Your code is only run when called by onMsg(), and you are expected to do your job and -- return as quickly as possible, to avoid jamming the message queue. But -- some things (cut scenes and AI algorithms) are better written out as if they -- were long-lived processes. For this, we use lua coroutines -- which cooperatively 'yield' when they know they are blocked. So they run -- only in short bursts, but get to resume automatically, and repeatedly, -- until the job is done. without callbacks required. It's a dream come true. -- While you are free to create solo experiences, you should think about how -- to manage multiplayer effectively. We assume/ensure that -- each player starts with the exact same script, so it is never needed to -- send large amounts of data, since anything you might need can be predeployed -- in a lua Table defined in the script you all have a copy of already. -- For any object (say a bot representing some enemy you need to conquer), -- there is one master copy, that is doing all the thinking, and then -- a slave copy on every other player's device, that just follows what the master -- is doing. In this scheme, each object is 'hosted' on one player's device. -- Normally an object is hosted by the player who triggered its entrance into the -- world. But if nothing else, the instance moderator will host. -- the master copy of the object then does all the thinking, which boils down -- to a small set of numbers indicating the recommended courses of action. -- these 'action requests' are then passed down into the NPC, run directly by -- the game engine. Such that the lua 'bot' brain is the neocortex, and the -- game engine 'action handler' is the hindbrain, and a 'ship' is the body that -- is being controlled by.. the lua script... indirectly. -- The SHIP object can be assigned (by this script) a target, a weapon -- a destination, and any other detail it needs to know. The -- ship then interacts with the map's barriers, zones, stars, and other -- players. Occasionally, things happen that the ship reports upstream to -- the forebrain through messages, like onENTER, onPUP, onDAMAGE and onDEAD. These act -- as sense inputs for the lua forebrain, who chooses new strategies and updates -- the NPCs action list. -- You can also 'work laterally' sending messages back and forth between the -- lua forebrains of all the players on the server. For example you can -- SEND( toObjectId, uMY_EVENT_NAME, args ) and that will be received on all other -- players machines as a message: uMY_EVENT_NAME with the original args attached. -- and can even be directed to all, individual, or sets of similar, objects in the -- destination game engine, which can be all players, one player, or some set of -- players. -- Stock game engine event messages start with 'on'. Messages you create -- should start with 'u'. THat way we don't step on each other's toes. -- there are limits imposed on SENDs to avoid (unintentional) denial of service -- attacks on the server, so try to send the least amount of info, as seldom as -- you can get away with it, in the fewest number of packets. -- while your code is only ever invoked as the result of a call to onMsg(), -- once you are running, you can use the 'game' object to request the game -- engine to perform tasks on your behalf, like spawning a new SHIP, or -- faking a radio message. The game object has a single method (sendMsg()) -- and the messages you send are mostly to the game engine and are actually -- requests for things to be done, like opening/closing some telltale or -- score panel. To simplify the architecture, and somewhat insulate it -- from the current API volatility, I have included helper functions for each -- game object message. -- when thinking about game state, think about where it should live -- for example, some state is by nature global to the starmap, like -- "number of teams" and "which thumb is Pirate Captain Jack?" But -- other state might belong directly inside a bot ( number of shots left -- in captain jack's cannon) or a scene (number of cannon shots left -- before the scene ends). -- There might be multiple scenes playing out at the same time, and -- there will almost certainly be multiple bots out at the same time. -- You want to be economical with state, and just send it to those -- who need it, and just the bits they can't work out for themselves. -- if the bot or scene maintain their own state, then you don't need some -- master function that needs to simultaneously understand all the scenes. -- Each scene can focus specifically on ITS needs and be completely -- insulated from everything else going on, unless you WANT it to know, -- in which case, send a uMESSAGE to all objects to wake them up. -- WARNING: I don't put a lot of effort into preventing you from breaking -- packets by using goofy characters, so do your own 'escaping' if you -- need anything weird to get passed in an 'args' table. -- I have decided that since ship Indices start at 0 in the game engine -- (in java) that the API will continue with that standard. So if you -- DO need an array index in lua, you will have to add 1 or something. Lua -- is not really array friendly, IMHO. But tables are better anyway. ---------------------- -- GLOBAL MAP STATE ---------------------- -- these are managed by my standard API, they should be read only, from your perspective local myShipIndex = -1 -- my shipIndex (0-7) local myShipName = "" -- also known as player name, since this is my real human pilot local myShipRankName = "" -- "fodder" etc. local mySerNum = 0 -- my player serial number local isModerator = 0 -- (1 = yes, I am the moderator) local myMixterSlot = -1 -- my server instance slot (not my ship color index) local myMapCRC = 0 -- the CRC of the starmap I am using (should match all players) local dlgMode = 0 -- non-zero when the user is seeing a dialog local tokens = {0,0,0,0,0,0,0,0} -- locally cached copy of map tokens, 31 bits each (avoiding sign bits) glShips = {} -- cache of last known info for every ship (updates every few seconds, not real time) -- some team names local TeamWest = 9 local TeamEast = 10 local TeamAll = 11 local TeamNone = 0 ------------------- -- onMSG HANDLER ------------------- -- this is the ONLY entry point from the game engine. The game will call this when -- something interesting happens. Pass the message to whomever is interested -- and then return asap. If you want to dilly dally, start a coroutine. function onMsg( from, to, command, argString ) -- turn the argstring into a nice table local args = processArgs( argString ) -- Outside of SCENES/BOTS, I only care about a few messages, mainly -- to collect interesting facts about my local player, or start a -- standard coroutine to carry out a long operation if ( command == 'onAWARE' ) then -- the local pilot has just become aware of the starmap -- they have not picked a ship color yet, but are connected to the server instance -- I feel only a single MAIN scene on each starmap should respond to this -- but that response should set the stage. I might even add the ability -- to block the thumb selection until after a cutscene has played. -- but here, I just remember some facts about my pilot myMixterSlot = args['slot'] mySerNum = 0 + (args['sernum'] or 0) -- watch out for numbers getting turned into strings myMapCRC = args['mapCRC'] myShipName = args['name'] log( 1, "Pilot: " .. myShipName .. " on mixterSlot " .. myMixterSlot .. " sn: " .. mySerNum .. " and mapCRC: " .. myMapCRC .. " is AWARE" ) elseif ( command == 'onLAUNCH' ) then -- the local pilot has just launched their drone -- grab some definitive info about myself. myShipIndex = args['ship'] myShipName = args['name'] myShipRankName = args['rankName'] log( 1, "Pilot: ".. myShipRankName .. " " .. myShipName .. " launched shipIndex " .. myShipIndex ) --tokenUnitTest() -- test only elseif( command == 'onBEAT' ) then -- (default 80 beats per minute) -- grab some more official data (these can change post launch) -- Also, I get onBeat before onLaunch, so sernum from here is best mySerNum = 0 + (args['mySerNum'] or 0) -- this should not change really. isModerator = 0 + (args['iAmMod' ] or 0) dlgMode = 0 + (args['dlgMode' ] or 0) -- step our coroutines, one yield each scheduler() -- give the coroutines a chance to advance one step elseif( command == 'onSHIP' ) then -- I get occasional ship updates, I just cache them. local index = 1 + (args['ship'] or 0) -- add one for lua 'array' if( glShips[ index ] == nil ) then glShips[ index ] = {} -- this needs to exist right now end local s = glShips[ index ] -- guaranteed non null, can be added to setIfNotNil( s, "state", args['state'] ) setIfNotNil( s, "name", args['name'] ) setIfNotNil( s, "rank", args['rank'] ) setIfNotNil( s, "sernum", args['sernum'] ) setIfNotNil( s, "mixslot", args['mixslot'] ) setIfNotNil( s, "shell", args['shell'] ) setIfNotNil( s, "face", args['face'] ) setIfNotNil( s, "podW", args['podW'] ) setIfNotNil( s, "podS", args['podS'] ) setIfNotNil( s, "podE", args['podE'] ) setIfNotNil( s, "x", args['x'] ) setIfNotNil( s, "y", args['y'] ) setIfNotNil( s, "z", args['z'] ) setIfNotNil( s, "vx", args['vx'] ) setIfNotNil( s, "vy", args['vy'] ) setIfNotNil( s, "vz", args['vz'] ) setIfNotNil( s, "speed", args['speed'] ) setIfNotNil( s, "engine", args['engine'] ) setIfNotNil( s, "target", args['target'] ) setIfNotNil( s, "wpnId", args['wpnId'] ) setIfNotNil( s, "isBot", args['isBot'] ) setIfNotNil( s, "team", args['team'] ) setIfNotNil( s, "energy", args['energy'] ) setIfNotNil( s, "time", args['time'] ) setIfNotNil( s, "groove", args['groove'] ) setIfNotNil( s, "mapcrc", args['mapcrc'] ) setIfNotNil( s, "mapId", args['mapId'] ) setIfNotNil( s, "leader", args['leader'] ) setIfNotNil( s, "deaths", args['deaths'] ) setIfNotNil( s, "kills", args['kills'] ) setIfNotNil( s, "rating", args['rating'] ) setIfNotNil( s, "ranknum", args['ranknum'] ) setIfNotNil( s, "lang", args['lang'] ) -- log(1, "saw onSHIP for ship " .. index-1 .. " " .. glShips[ index ].name ) elseif( command == 'onTOKEN' ) then -- game tells ME when the local player's map tokens have changed -- each of these 8 token values holds 31 individual -- token bits, so this update covers the entire -- valid token space from 0 to 8*31-1, or 0-247 -- So, we just update token cache for this map, so script can check cache later tokens[1] = 0 + (args['token0'] or 0) tokens[2] = 0 + (args['token1'] or 0) tokens[3] = 0 + (args['token2'] or 0) tokens[4] = 0 + (args['token3'] or 0) tokens[5] = 0 + (args['token4'] or 0) tokens[6] = 0 + (args['token5'] or 0) tokens[7] = 0 + (args['token6'] or 0) tokens[8] = 0 + (args['token7'] or 0) log( 1, "TOKEN CACHE: " .. tokens[1] .. "," .. tokens[2] .. "," .. tokens[3] .. "," .. tokens[4] .. "," .. tokens[5] .. "," .. tokens[6] .. "," .. tokens[7] .. "," .. tokens[8] ) summarizeTokens() -- log all the tokens owned by this player on this map end -- If the actual message is onSEND, we check -- the destination and rename the message to make script simpler toObjId = "" -- most messages go to everyone if( command == "onSEND" ) then toObjId = args.objId -- target just this id pattern command = args.cmd -- turn 'onSEND' into 'uXXXX' end -- -- Pass (translated) message to all SCENEs that have a handler for it -- and whose ids match the pattern handleSceneMessages( toObjId, command, args ) -- Pass message to all BOTs that have a handler for it -- and whose ids match the pattern handleBotMessages( toObjId, command, args ) --log( 1, 'script returning from onMsg ' ) end -- pass this message to all scenes that want it function handleSceneMessages( id, cmd, args ) for i,scene in ipairs( sceneList ) do -- log(1, "Applying cmd " .. cmd .. " to scene " .. i .. " in state " .. scene.state .. " to objId: " .. id ) if addressedToId(id, scene.id) and scene.handler[cmd] ~= nil then scene.handler[cmd]( scene, args ) -- invoke the scene specific function, if it exists end end end -- pass this message to all bots that want it function handleBotMessages( id, cmd, args ) for i,bot in ipairs( botList ) do --log(1, "Applying cmd " .. cmd .. " to bot " .. i .. " in state " .. bot.state ) if addressedToId(id, bot.id) and bot.handler[cmd] ~= nil then bot.handler[cmd]( bot, args ) -- invoke the bot specific function, if it exists end end end function addressedToId( toId, id ) return (toId == "") -- blank destination is 'to all' or (id == toId) -- addressed literally to us or startsWith(toId, id ) -- send to 'minion_' if you want to reach 'minion_1" "minion_2" etc end function startsWith( patt, id ) local i = string.find(id, patt) return (i == 1) -- must be found at the very front end -- Turn url arg string into nice lua args table function processArgs( argString ) local args = {} for p in string.gmatch( argString, "[^\&]+" ) do -- arcane, but great --log( 1, ' argString pair: ' .. p ) local left = string.find(p, "=", 1, true) if( left > 0 ) then local name = string.sub( p, 1, left-1) local value = string.sub( p, left+1 ) args[ name ] = value end end return args end -- this is for when you don't want to overwrite what might be -- better (old) data than what you're offering. function setIfNotNil( table, newName, newValue ) if( newValue ) then table[ newName ] = newValue end end ---------------------- -- API SUPPORT ---------------------- -- We offer a helper function for each game object request. You are -- free to send the same messages directly, but these might protect you -- from future api changes. Or screw you up. We'll see! It's an adventure! -------- Adds one line to the Script/LOG window function log (level, msg ) print( "" .. msg ) -- to stdout game.sendMsg( "", "LOG?msg=" .. msg ) -- to script window log end -------- test if I am the server instance moderator function iAmModerator() -- note that hacking this to return true does NOT make you the moderator -- in anyone's eyes but your own... return (1 == isModerator) -- nil for no end -------- test if I am the host of this object (scene or bot) function iAmSceneHost( scene ) if (scene.hostSerNum == 0) then return iAmModerator() -- if no scene host, default to moderator end -- otherwise, check if I am the scene host return mySerNum > 0 and mySerNum == scene.hostSerNum end -------- -- add a major center screen announcement -- only seen by the local pilot. If you need it seen by all, then -- send a uXXXX message instead and have all the receivers announce() -- locally. Same goes for most of this. -- These generally scroll upwards, vaguely like Star Wars exposition -- use textStory() if you have lots to say function announce( msg ) game.sendMsg( "", "ANNOUNCE?msg=" .. msg ) log( 1, "ANNOUNCE: " .. msg ) end function announce2( style, msg ) game.sendMsg( "", "ANNOUNCE?style=" .. style .."&msg=" .. msg ) log( 1, "ANNOUNCE: Style:" .. style .. ": " .. msg ) end -------- -- add a line of chat as appearing to come from the ship -- you can also use this to put words in the mouths of real players -- This is only seen by the local pilot. Nessage looks like -- normal text chat from a fellow player. function chat( bot, msg ) game.sendMsg( "", "CHAT?ship=" .. bot.ship .."&msg=" .. msg ) log( 1, "BOTCHAT: " .. msg ) end -- this opens a large dialog box between the joysticks that the user -- must click to clear. function radio( bot, msg ) dlgMode = 1 -- this opens a dialog game.sendMsg( "", "CHAT?ship=" .. bot.ship .."&marquee=0" .."&clear=3" -- discard anything older than this many seconds .."&popup=1" .."&msg=" .. msg ) log( 1, "RADIO: " .. msg ) end -- like radio, but clears itself after a few beats -- TODO let the user control the reading speed estimate function snapChat( beats, bot, msg ) dlgMode = 1 -- this opens a dialog if( beats < 0 ) then -- estimate reading time for this msg numWords = (#msg / 5) -- roughly beats = 2 + ( ( 60 * numWords ) / 100 ) if beats < 3 then beats = 3 -- minimum reading time end end game.sendMsg( "", "CHAT?ship=" .. bot.ship .."&marquee=0" .."&clear=3" -- discard anything older than this many seconds .."&popup=1" .."&autoclose=" .. beats .."&msg=" .. msg ) log( 1, "SNAPCHAT: beats:" .. beats .. ", ".. msg ) end -- sends a radio message 'about every N beats', but skips it if another message is already showing -- msg can be a single message, or an array of messages, which will be played in a loop function hint( bot, beats, msg ) -- increment the hint counter, no matter what if( bot.hintCount == nil ) then bot.hintCount = 0 -- start a new one end bot.hintCount = bot.hintCount + 1 -- do not bump existing message, we just lose our spot if( dlgMode ~= 0 ) then return -- there is already a message up, we're not important end if( (bot.hintCount % beats) == 0 ) then -- so it is time to say something, but which one? if ( type( msg ) == "table" ) then -- it's an array, use the next one if( bot.hintIndex == nil ) then bot.hintIndex = 0 end local numHints = #msg local i = bot.hintIndex % numHints bot.hintIndex = bot.hintIndex + 1 bot:say( msg[ i + 1 ] ) else bot:say( msg ) end end end -- puts a marker over the head of 'the ship' (also adds beacon to radar) -- use style = 1 for now. -- ix is 0-9 -- set ship to -1 to disable beacon -- color, shape and pulse pattern will come from style function setBeacon( ix, ship, style ) game.sendMsg( "", "BEACON?ix=" .. ix .."&ship=" .. ship .."&style=" .. style ) log( 1, "BEACON: " .. ix .. " ship " .. ship .. " style " .. style ) end ----- -- tell the game engine to send me a uMESSAGE when something happens -- trigType 1 means 'player stayed close to ship for msec' -- sends msg2 when player leaves area (with hysteresis) function setTrigger( trigType, shipIndex, dist, msec, ix, uMsg, uMsg2 ) game.sendMsg( "", "TRIGGER?trigType=" .. trigType .."&ship=" .. shipIndex .."&dist=" .. dist .."&msec=" .. msec .."&ix=" .. ix .."&uMsg=" .. uMsg .."&uMsg2=" .. uMsg2 ) log( 1, "setTRIGGER: " .. ix .. " ship: " .. shipIndex .. " type: " .. trigType .. " uMsg: " .. uMsg ) end function stopTrigger( ix ) setTrigger( 0, -1, 0, 0, ix, "", "" ) end -------- -- add or remove a small text/score panel function display( id, x, y, title, score ) game.sendMsg( "", "DISPLAY?id=" .. id .. "&x=" .. x .. "&y=" .. y .. "&title=" .. title .. "&score=" .. score ) end -------- -- shortcut to remove display panel function displayOff( id ) display( id, -1, -1, "", "" ) end ---------- -- put up a confirmation dialog, that sends a message if user OKs the operation function areYouSure( desc, yesLabel, uMsgYes ) log( 1, "areYouSure - if they press: " .. yesLabel .. ", will send msg: " .. uMsgYes ) game.sendMsg( "", "UI?style=0" .. "&uMsgYes=" .. uMsgYes .. "&yesLabel=" .. yesLabel .. "&desc=" .. desc ) end -------- -- give the local player a powerup (as if they just passed over a pyramid) -- not sure what happens if you give them a 'concealed trap' -- does it explode immediately, or appear on the button bar as some -- sort of self destruct? function givePup( pupId ) game.sendMsg( "", "GIVE?pupId=" .. pupId ) end -------- -- give or take a token by its' token index. In theory token indices are 0 to 247, -- values are 0 (does not have token) or 1 (has token) function setToken( ix, val ) -- update my local cache first, so if I try to read it back immediately, -- I get something close to the proper answer, but really my cache won't -- be updated until after a pause. local tix = tokenIndex( ix ) local bit = ix % 31; local mask = bit32.lshift( 1, bit ) log( 1, "SETTOKEN ix:" .. ix .. " to " .. val .. " tix: " .. tix .. " bit: " .. bit .. " mask: " .. mask ) if( val == 1 ) then tokens[ tix ] = bit32.bor( tokens[ tix ], mask ) else tokens[ tix ] = bit32.band( tokens[ tix ], bit32.bnot( mask ) ) end -- tell the game to set or clear the bit, it will eventually -- send us an onTOKEN message which will formally update our cache game.sendMsg( "", "TOKEN?ix=" .. ix .. "&val=" .. val ) end -- get array index of token (31 tokens per array entry) function tokenIndex( ix ) local tix = 1 -- lua 'arrays' start with 1 if ( ix >=0 and ix < 248 ) then tix = 1 + math.floor(ix/31) end return tix end -- check against cache function hasToken( ix ) local tix = tokenIndex( ix ) local bit = ix % 31 local mask = bit32.lshift( 1, bit ) if( 0 ~= bit32.band( tokens[ tix ], mask ) ) then return 1 -- in lua, even 0 is true else return nil -- lua for false end end myTestTokens = { 0, 1, 2, 4, 5, 8, 30, 31, 32, 33, 61, 62, 63, 64, 65 } function tokenUnitTest() -- clear all tokens for i=0,66 do setToken( i, 0 ) end -- set some tokens for i,v in ipairs( myTestTokens ) do log( 1, "TEST SETTOKEN " .. v .. " to 1" ) --setToken( v, 1 ) end end -- recap all the tokens this guy has function summarizeTokens() local i local out = "TOKENS OWNED: " for i=0,247 do if( hasToken(i) ) then out = out .. i .. ", " end end log( 1, out ) end -------- -- moderator-only -- causes a map pup to be rescheduled for appearance in a few seconds -- this assumes you have a matching pupId entry in the top-half -- powerups section. Ultimately, all players see this (if you were mod) function resetPup( ix, seconds ) game.sendMsg( "", "RESETPUP?ix=" .. ix .. "&sec=" .. seconds ) end function resetPupXY( ix, pupId, x, y, seconds, period ) game.sendMsg( "", "RESETPUP?ix=" .. ix .. "&pupId=" .. pupId .. "&x=" .. x .. "&y=" .. y .. "&sec=" .. seconds -- appear this soon .. "&period=" .. period ) -- reappear this often log( 1, "RESETPUP ix: " .. ix .. ", id: " .. pupId .. " at (" .. x .. "," .. y .. ")" ) end ------- -- Define a new powerup -- or overwrite an existing one -- function setPowerUp( pupId, wpnId, powerup ) game.sendMsg( "", "POWERUP?pupId=" .. pupId .. "&wpnId=" .. wpnId .. "&bundleName=" .. powerup.bundleName .. "&numInPack=" .. powerup.numInPack .. "&maxCanOwn=" .. powerup.maxCanOwn .. "&useMode=" .. powerup.useMode .. "&useMsg=" .. powerup.useMsg -- msg to send when used by local pilot .. "&iconId=" .. powerup.iconId .. "&desc=" .. powerup.desc .. "&wpnName=" .. powerup.wpn.wpnName .. "&usesAmmo=" .. powerup.wpn.usesAmmo .. "&singleShot=" .. powerup.wpn.singleShot .. "&homing=" .. powerup.wpn.homing .. "&needsTarget=" .. powerup.wpn.needsTarget .. "&autoRepeatMsec=" .. powerup.wpn.autoRepeatMsec .. "&lifetimeMsec=" .. powerup.wpn.lifetimeMsec .. "&speed=" .. powerup.wpn.speed .. "&power=" .. powerup.wpn.power .. "&soundLaunch=" .. powerup.wpn.soundLaunch .. "&soundContact=" .. powerup.wpn.soundContact .. "&soundFizzle=" .. powerup.wpn.soundFizzle .. "&shotsPerFullCharge=" .. powerup.wpn.shotsPerFullCharge ) end -------- -- adds a MENU option (to the STARMAP tab), which sends uXXX when tapped -- individual commands can be mod-only, or for everyone function option( id, modOnly, cmd, label ) game.sendMsg( "", "OPTION?id=" .. id .. "&modOnly=" .. modOnly .. "&cmd=" .. cmd .. "&label=" .. label ) end --------- -- plays one of the precompiled OGG files -- you will just have to experiment, but the standard midi percussion is nice! -- 14 incoming chat -- 16 metronome -- 35 - 82 percussive instrument hits -- 200 plot point -- 201 bongos transition -- 202 conclusion -- 203 message box -- 204 button click -- 205 chime 'dong' -- -- only heard by local player soundBase = 0 -- dunt esk SOUND_SHIP_EXPLODING = soundBase + 1 -- done, but not satisfied SOUND_SHIP_BARRIER_BOUNCE = soundBase + 2 -- boing SOUND_SHIP_BARRIER_PAIN = soundBase + 3 -- bzzt SOUND_WEAPON_OPEN = soundBase + 4 -- when the temp guage appears/disappears SOUND_WEAPON_CLOSE = soundBase + 5 -- when the temp guage appears/disappears -- SOUND_BULLET_LAUNCH = soundBase + 6 -- pyeww! pyeww! -- SOUND_BULLET_POP = soundBase + 7 SOUND_SHIP_SHIELD_POP = soundBase + 8 SOUND_PANEL_OPEN = soundBase + 9 -- zzzzt SOUND_PUP_PICKUP = soundBase + 10 -- ka-chick SOUND_PAGE_FLIP = soundBase + 11 -- bwork SOUND_PILOT_SPAWNS = soundBase + 12 -- gong SOUND_INSUFF_CHARGE = soundBase + 13 -- spark SOUND_CHAT_IN = soundBase + 14 -- alien voice SOUND_LAUNCH_DRONE = soundBase + 15 -- low swoosh SOUND_METRONOME = soundBase + 16 -- wood block SOUND_PUP_HEAL = soundBase + 17 -- zzzz up SOUND_PUP_TRAP = soundBase + 18 -- kerpow SOUND_CURSE = soundBase + 19 -- local pilot just said a bad word SOUND_SENT_DATA = soundBase + 20 -- SOUND_RECD_DATA = soundBase + 21 -- SOUND_REQ_DATA = soundBase + 22 -- SOUND_USE_MAP = soundBase + 23 -- connecting to new starmap via droneNet SOUND_CONFIRM = soundBase + 24 -- opening the clone confirm/name panel SOUND_SOCIAL = soundBase + 25 -- tap the social key SOUND_YES = soundBase + 26 -- YES on any confirm SOUND_NO = soundBase + 27 -- NO on any confirm SOUND_DELETE = soundBase + 28 -- Deleting a collectible SOUND_POP = soundBase + 29 -- Deleting a collectible SOUND_ENGINE_ON = soundBase + 30 -- SOUND_DEAD_SHIP = soundBase + 31 -- SOUND_RESTORE = soundBase + 32 -- SOUND_BARRIER_CHANGE = soundBase + 33 -- SOUND_ENGAGE = soundBase + 109 -- SOUND_DISENGAGE = soundBase + 110 -- function playSound( soundId ) game.sendMsg( "", "PLAYSOUND?soundId=" .. soundId ) end -- Flashes red around the outside edges of the screen -- if > 0 then flash pain warning for that many ms -- if < 0 -1 to -100 indicates percent damage of drone with increasing pain intensity function painFlash( msec ) game.sendMsg( "", "PLAYSOUND?glow=" .. msec ) end -- shakes the camera for duration indicated function shakeScreen( msec ) game.sendMsg( "", "PLAYSOUND?shake=" .. msec ) end -------- -- Send a map-defined message to a peer object on another player's device -- Any scene or bot object table with 'id' that 'startsWith' the objId, will get message -- An empty string means 'to all objects' -- I believe I support 'echo=1' in the argstring so you also receive/distribyte -- the message on your own machine. And I should also have a mode that ONLY -- sends to local objects, probably. function sendToObj( objId, cmd, argString ) game.sendMsg( "", "SEND?cmd=" .. cmd .. "&objId=" .. objId .. "&" .. argString ) end --------------------------------------------- -- When you need to deliver a long story and probably used -- [[a long multiline text]] to encapsulate it into something easy to type -- I will ignore the real line breaks inside your [[]] and -- instead I will break when I have to (to fit) and again -- when you insert a forward slash -- I will also wait some number of beats after each line emitted -- because the game engine scrolls slowly (for good reason) and I -- don't want to just flash fill its queue. -- -- You should only call me from inside a lua coroutine, since I -- will not return until the whole thing has been displayed. -- Note that the double bracket quoting stuff happens when the -- script is loaded, so it can't embed your player name directly -- inside the double brackets. For that sort of thing I should -- define some macros like %playerName% or whatever. Or you can -- break up your monolithic text. storyBuffer = "" myStoryStyle = 1 -- StarWars scrolling text function flushTextStory( style ) if #storyBuffer > 0 then --log( 1, "flushStoryText: " .. storyBuffer ) announce2( style, storyBuffer ) storyBuffer = "" wait( 4 ) -- current announcement is limited to this rate end end function textStory( style, story ) maxLength = 35 -- announce lines must be shortish, but we continue to space len = #story log( 1, "textStory asked to display ("..len.." chars): " .. story ) for i = 1, len do c = string.sub( story, i, i ) if c == "/" then -- flush what we have --log( 1, "saw a slash, starting new line by flushing what we have " .. storyBuffer ) flushTextStory( style ) else -- add it to the buffer --log( 1, "not a slash, buffer is " .. storyBuffer ) -- we need to turn those REAL lfs into spaces -- or not. if string.byte(c) < 32 then c = "" end -- then append storyBuffer = storyBuffer .. c -- and maybe flush again if( #storyBuffer > maxLength and c == " " ) then -- flush if buffer full flushTextStory( style ) end end end -- leave no text behind flushTextStory( style ) end -------- -- I offer several timer options, based on style, but generally this -- command starts a timer which displays either as a progress meter -- or a count down clock. It lasts for some number of seconds, and counts -- either up or down. If it gets to the end, it sends the doneMsg -- to the addressed script object. -- If you provide a non-empty progressMsg -- it will send that at approximately 10% intervals. -- If you want rto reset it before completion, just issue the command again. -- to remove it completely, set it again with the seconds = 0. -- for now, all styles are "horizontal progress meter down in the hint area" -- ==== will probably add some text label opportunities and color control function startTimer( style, seconds, direction, toObjId, doneMsg, progressMsg ) game.sendMsg( "", "TIMER?seconds=" .. seconds .. "&style=" .. style .. "&dir=" .. direction .. "&objId=" .. toObjId .. "&doneMsg=" .. doneMsg .. "&progressMsg=" .. progressMsg ) end -- shortcut to kill an existing timer function killTimer( style ) startTimer( style, 0, 0, "", "", "" ) end --=================== -- ok, here I added some api to support modifying the starmap top half settings -- In theory this -- means you do not need a top half, sort of. Is that schizophrenic or infinitely -- recursive? Let's pretend it's "powerful" and just say it's "how we do things -- that can open and close" In theory, you can animate them -- This allows you to ultimately have a different barrier configuration -- between players, which could be used for grief or glory. People won't -- continue to play grief maps (and they can read your source), so please -- avoid griefing, thank you. MAP_CAT_SPAWN = 1 MAP_CAT_STAR = 2 MAP_CAT_COLOR = 3 MAP_CAT_PAIN = 4 MAP_CAT_HORIZ = 5 MAP_CAT_VERT = 6 MAP_CAT_ZONE = 8 -- set a player spawn location (must be set before onLaunch) function setMapSpawn( ix, x, y, hdg ) game.sendMsg("", "STARMAP?cat=1&ix=" .. ix .. "&x=" .. x .. "&y=" .. y .. "&hdg=" .. hdg ) end -- set up a star or other gravity object function setMapStar( ix, style, x, y, mass, reach, temp, radius, red, green, blue, imageid ) game.sendMsg("", "STARMAP?cat=2&ix=" .. ix .. "&style=" .. style .. "&x=" .. x .. "&y=" .. y .. "&mass=" .. mass .. "&reach=" .. reach .. "&temp=" .. temp .. "&radius=".. radius .. "&red=" .. red .. "&green=" .. green .. "&blue=" .. blue .. "&imageid=" .. imageid ) end -- override the official color palette function setMapColor( ix, color ) game.sendMsg("", "STARMAP?cat=3&ix=" .. ix .. "&color=" .. color ) end -- set up pain classes function setMapPain( ix, pain ) game.sendMsg("", "STARMAP?cat=4&ix=" .. ix .. "&pain=" .. pain ) end -- modify a horizontal barrier function setMapBarrierH( ix, state, left, right, top, color, xpar, pain ) game.sendMsg("", "STARMAP?cat=5&ix=" .. ix .. "&state=" .. state .. "&left=" .. left .. "&right=" .. right .. "&top=" .. top .. "&color=" .. color .. "&xpar=" .. xpar .. "&pain=" .. pain ) end function setMapBarrierStateH( ix, state ) game.sendMsg("", "STARMAP?cat=5&ix=" .. ix .. "&state=" .. state ) end -- modify a vertical barrier function setMapBarrierV( ix, state, bottom, top, left, color, xpar, pain ) game.sendMsg("", "STARMAP?cat=6&ix=" .. ix .. "&state=" .. state .. "&bottom=" .. bottom .. "&top=" .. top .. "&left=" .. left .. "&color=" .. color .. "&xpar=" .. xpar .. "&pain=" .. pain ) end ----------------- -- state 0 - off, 1 - on, 2 - fading out to off, 3 - fading in to on function setMapBarrierStateV( ix, state ) game.sendMsg("", "STARMAP?cat=6&ix=" .. ix .. "&state=" .. state ) end -- modify a zone function setMapZone( ix, style, left, top, right, bottom, texture, team, pain, bullets, wayptid, friction, cx, cy, group ) game.sendMsg("", "STARMAP?cat=8&ix=" .. ix .. "&style=" .. style .. "&left=" .. left .. "&top=" .. top .. "&right=" .. right .. "&bottom=" .. bottom .. "&texture=" .. texture .. "&team=" .. team .. "&pain=" .. pain .. "&bullets=" .. bullets .. "&wayptid=" .. wayptid .. "&friction=" .. friction .. "&cx=" .. cx .. "&cy=" .. cy .. "&group=" .. group ) end --------------- -- some things you can override about a ship -- Repair Mask -- individual bits for different things that can break -- '1' means broken, '0' means all good to go -- or add together these codes to 'disable' a collection of systems -- (death auto-repairs everything, since you get a shiny new drone clone) REPAIR_MASK_ENGINES = 1 REPAIR_MASK_STEERING = 2 REPAIR_MASK_TRIGGER = 4 REPAIR_MASK_PUPS = 8 REPAIR_MASK_RADAR = 16 REPAIR_MASK_STOP = 32 REPAIR_MASK_SOCIAL = 64 REPAIR_MASK_MAPS = 128 REPAIR_MASK_CAM = 256 -- also suppresses OPTION button REPAIR_MASK_ALL = 65535 -- i suspect I could go to 31 bits glLastRepairMaskSet = 0 -- set the complete repair mask in one move (overwrites everything) function setShipRepairMask( ix, mask ) game.sendMsg("", "SHIP?ix=" .. ix .. "&repairs=" .. mask ) glLastRepairMaskSet = mask end -- 'fix' some combination of systems function makeRepairs( ix, mask ) local newMask = bit32.band( glLastRepairMaskSet, bit32.bnot(mask) ) -- clear bits provided setShipRepairMask( ix, newMask ) playSound( SOUND_PUP_HEAL ) end -- 'break' some combination of systems function breakStuff( ix, mask ) local newMask = bit32.bor( glLastRepairMaskSet, mask ) -- set bits provided setShipRepairMask( ix, newMask ) end -- I need the player to hold still for some dialogue to occur function freezePlayer() setShipVel( myShipIndex, 0, 0 ) -- stop them breakStuff( myShipIndex, REPAIR_MASK_ENGINES ) end function releasePlayer() makeRepairs( myShipIndex, REPAIR_MASK_ENGINES ) end -- tell a ship (presumably a bot) whom it should be targeting (with weapons) -- affects local copy of ship only function setShipTgt( ix, tgt ) game.sendMsg("", "SHIP?ix=" .. ix .. "&tgt=" .. tgt ) end -- set a ship's heading (the direction nose is pointing) function setShipHdg( ix, hdg ) game.sendMsg("", "SHIP?ix=" .. ix .. "&hdg=" .. hdg ) end -- set a ship's position. YOu can play with z, but really it should be 0. function setShipPos( ix, x, y, z ) game.sendMsg("", "SHIP?ix=" .. ix .. "&x=" .. x .. "&y=" .. y .. "&z=" .. z ) end -- set a ship's velocity (different from heading) function setShipVel( ix, vx, vy ) game.sendMsg("", "SHIP?ix=" .. ix .. "&vx=" .. vx .. "&vy=" .. vy ) end -- you can't usually just jam in new state, but jamming -- it ito exploding should halt the ship and make an explosion SHIP_STATE_IDLE = 0 -- no owner SHIP_STATE_CLAIMED = 1 -- owned but still configuring SHIP_STATE_PLAYING = 2 -- laynched and playing SHIP_STATE_EXPLODING = 3 -- just died, exploding SHIP_STATE_DEAD = 4 -- dead (not sure if useful) function setShipState( ix, state ) game.sendMsg("", "SHIP?ix=" .. ix .. "&state=" .. state ) end -- set a basket of ship values all at once. function setShip( ix, x, y, z, hdg, vx, vy, tgt, mask ) game.sendMsg("", "SHIP?ix=" .. ix .. "&x=" .. x .. "&y=" .. y .. "&z=" .. z .. "&hdg=" .. hdg .. "&vx=" .. vx .. "&vy=" .. vy .. "&tgt=" .. tgt .. "&repairs=" .. mask ) end -- Hilite a UI element for a few seconds -- not a mask HL_POD_WEAPON = 0 HL_POD_SHIELD = 1 HL_POD_ENGINE = 2 HL_STEERING = 3 HL_ENGINE = 4 HL_TRIGGER = 5 HL_RADAR = 6 function hiliteUI( hilite ) game.sendMsg("", "SHIP?ix=0" .. "&hilite=" .. hilite ) end ------------------------------------------- -- some things you can do about the '3d camera' -- popular values CamPitchDown = 85 -- camera looking down on world CamPitchSide = 25 -- normal side view CamHdgBehind = 0 -- directly behind ship CamZoomCloseUp = 7 -- target is prominent CamZoomGroup = 5 -- emphasis local ships CamZoomWide = 3 -- normal gameplay function resetCamera() setCamOrbit( 0 ) -- in case I was orbiting setCamSpy( myShipIndex ) -- I am the center of attention setCamZoom( CamZoomWide, 2800 ) -- default chase zoom setCamPitch( CamPitchSide, 2000 ) end -- tilt the camera up or down function setCamPitch( pitch, msec ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&pitch=" .. pitch .. "&msec=" .. msec ) end -- pan the camera left or right (or orbit the thing you're looking at) function setCamHdg( hdg, msec ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&hdg=" .. hdg .. "&msec=" .. msec ) end -- set the zoom value (1-10) where a bigger value is 'more zoomed in' function setCamZoom( zoom, msec ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&zoom=" .. zoom .. "&msec=" .. msec ) end -- set the veil percent value 0 means the veil has darkened everything, 100 means no veil. function setCamVeil( veil, msec ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&veil=" .. veil .. "&msec=" .. msec ) end -- tell the camera to chase another ship (-1 to revert to pilot's) function setCamSpy( ship ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&spy=" .. ship ) end -- focus on star or planet function setCamStar( starIx ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&star=" .. starIx ) end -- focus on Zone function setCamZone( zoneIx ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&zone=" .. zoneIx ) end -- focus on position function setCamPos( x, y, z ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&pos=1" .. "&x=" .. x .. "&y=" .. y .. "&z=" .. z ) end -- start the camera orbiting its current target -- set to 0 to stop (it continues until aligned with ship again) function setCamOrbit( dps ) game.sendMsg("", "CAMERA?ix=" .. 0 -- only one camera for now .. "&rate=" .. dps -- degrees per second ) end -- highlight a region of the map -- like: look at this clone factory function setCamToOrbitSpot( x, y, zoom ) setCamPos( x, y, 0 ) setCamOrbit(3) setCamZoom( zoom, 500 ) end -------- -- The game engine can manage up to 64 'npc' objects. The -- script can configure and activate any of these, for -- various purposes (set pieces, bases, factories, ships, meteors, etc) -- -- Ask the game engine to create or reassign one of the 64 -- NPC objects the game engine is capable of simulating -- -- Note that these arguments are all 'standard bot arguments' so -- you just pass in a reference to your bot's table. These -- NPCs might optionally act as the hindBrain for a SHIP with the -- matching index. -- Follow on commands to the NPC let you set its AI goals, which -- it will then obey until you change them. function createNpc( bot ) log( 1, "Create NPC " .. bot.id ) game.sendMsg( "", "NPC?id=" .. bot.id .. "&action=" .. "SPAWN" .. "&x=" .. bot.spawnX .. "&y=" .. bot.spawnY .. "&z=" .. bot.spawnZ .. "&ship=" .. bot.ship .. "&radarDist=" .. bot.radarDist .. "&radarColor=" .. bot.radarColor .. "&rank=" .. bot.pilotInfo.rank .. "&name=" .. bot.pilotInfo.name .. "&face=" .. bot.pilotInfo.face .. "&shell=" .. bot.pilotInfo.shell .. "&podW=" .. bot.pilotInfo.podW .. "&podS=" .. bot.pilotInfo.podS .. "&podE=" .. bot.pilotInfo.podE .. "&rating=" .. bot.pilotInfo.rating .. "&won=" .. bot.pilotInfo.won .. "&lost=" .. bot.pilotInfo.lost .. "&lang=" .. bot.pilotInfo.lang ) end -- Basic NPC behaviours. You can add these together to get a combination -- of behaviours BEHAVE_MASK_HUNT = 1 -- will seek out and hold position a short distance from navTarget BEHAVE_MASK_KILL = 2 -- will shoot at wpnTarget when possible BEHAVE_MASK_ALL = 65535 -- you're crazy, or the bot is -- set the behaveMask value first, then call this function updateNpcBehaviorMask( bot ) game.sendMsg("", "NPC?id=" .. bot.id .. "&action=" .. "UPDATE" .. "&behave=" .. bot.behaveMask .. "&wpnTgt=" .. bot.wpnTgt .. "&navTgt=" .. bot.navTgt .. "&guard=" .. bot.guarding .. "&origin=" .. (bot.origin or -1) .. "&dx=" .. (bot.dx or 0 ) .. "&dy=" .. (bot.dy or 0 ) .. "&dz=" .. (bot.dz or 0 ) .. "&radius=" .. (bot.radius or 200 ) .. "&msec=" .. (bot.msec or 300 ) .. "&secs=" .. (bot.secs or 3 ) .. "&canHitFirst=" .. (bot.canHitFirst or 0 ) .. "&uMsg=" .. (bot.uMsg or "" ) .. "&uMsg2=" .. (bot.uMsg2 or "" ) ) end -- set bot.targetIndex first, then call this function updateNpcTarget( bot ) game.sendMsg("", "NPC?id=" .. bot.id .. "&action=" .. "UPDATE" .. "&wpnTgt=" .. bot.wpnTgt .. "&navTgt=" .. bot.navTgt ) end -- we track when the player enters and exits zones, we then keep track -- of where he is.. glPlayerZoneIx = -1 -- the zone he last entered glPlayerZoneCache = {} -- sparse array function setPlayerZoneCache( zoneIx, entered ) if( entered ) then glPlayerZoneIx = zoneIx -- just entered this one else -- we don't know what zone they are in, other than they -- just left the one we most recently entered if( zoneIx == glPlayerZoneIx ) then glPlayerZoneIx = -1 -- assume not in any, but could search this cache end end glPlayerZoneCache[ zoneIx + 1 ] = entered end function isPlayerInZone( zoneIx ) return glPlayerZoneCache[ zoneIx + 1 ] end -------- -- Tells the game engine you no longer need this bot, so the engine -- can clean up the resources. Do this only after people have had -- a chance to loot the dead corpse :-) function destroyNpc( bot ) game.sendMsg("", "NPC?id=" .. bot.id .. "&action=" .. "DESTROY" ) end -- Standard Bot States (PROPOSED, NOT IMPLEMENTED) -- 0 - non existent, not spawned -- 1 - alive, stationary and thoughtless -- 2 - guarding/defending a point (until aggro to fighting) -- 3 - patrolling an array (until aggro to fighting) -- 4 - fighting -- 5 - at risk, escaping, looking for recharge, might reset with N heal pups. -- 6 - dead, a corpse with loot -- back to zero -- standard bot functions -- destroy -- disappear and deallocate into state 0 -- reset() -- return to starting config/loc, state 1 (must complete reset without yield) -- defend() -- start stationary defense coroutine, state 2 -- patrol() -- start roving patrol coroutine, state 3 -- fight( tgt ) -- start fight coroutine, state 4 -- escape() -- start flee coroutine, state 5 -- die() -- start death coroutine (loot) state 6 -- standard bot data -- initialPods (reset) -- currentPods -- state -- target -- timers -- guardPosition/radius -- chaseScale 10x radius (after which they break off pursuit) -- my target is set by -- total aggro, in some way, but -- * told by script -- * guy who tagged me -- * team-mate other than my own -- * weakest player -- * player attacking the guy I am protecting -- * strongest player -- * nearest player -- * guy I have the best/cheapest weapon for -- since I want all targeting decisions to come from the script, I dhoul -- also have a special case for scriptless bot pups of varying allegiances and skills -- (where the NPC starts with a default list of actions... hmmmm... yes, .. always -- do that, and script can overwrite!) --------- -- Set one of the Npc action goal slots -- NPCs will obey all current action goals -- fire engines when tgt is within N degrees and farther than D -- fire rev engines when tgt is within N degrees and closer than D -- fire weapon when tgt is within N degrees, and within shorterOf(wpn range, D) -- and with lead angle factored into the N -- turn to face tgt, with Lead Angle N when target in motion -- pick best weapon for tgt given range and inventory -- follow patrol pattern when not engaged with enemy -- turn left/right when further than D from zone Z center (should lissajous around point) -- pick best target for team (weakest, strongest, closest, furthest, taggedBy) -- set rules of engagement (when can I shoot back, when can I shoot proactively) -- (when do I get bored of the chase -- max distance from patrol center?) -- set minimum protection level (wave off and seek recharge) -- Dungeon monsters refill over time. no health pups needed. can I get away with that? -- I guess if they break off the attack and return to patrol, they get a refill -- maybe just widen my cone when I am turning? -- NPCs can start with a stock action list, for use in scriptless maps (bot pups) -- maybe I need a little config data on per pup basis... well, it would be a way to define -- a new pup in the top half, by reusing a stock pup, one that knew what to do with an extra column or 3 of data -- pupScheduleSlot = pupStyle, X, Y, reSpawnSeconds, numPods, aiType, name -- config data is remembered in the schedule until the pup is spawned, at which point -- it is a completely normal pup... no, this messes up existing.. function setNpcGoal( bot, slot, action, tgtShip, N, D, timeout ) end -------------------- -- COROUTINE SUPPORT -------------------- -- This uses stock lua coroutine support, with a simple 'scheduler' that -- just remembers a reference to one coroutine per bot (its brain) and -- one per scene (the current cutscene). -- A launcher helper is provided for each, to start a new bot or script -- coroutine. The actual function definitions are data in the bot or -- script's defining lua table co = nil -- coroutine global. this is the one that is running cutscene = nil -- I can only play one of these at a time -- this is a singleton co-routine that can play one of multiple animation functions, foo -- It immediately terminates any cut scene in progress function playCutscene( scene, foo ) log( 1, "creating coroutine for cut scene " .. scene.id ) if ( cutscene ) then log(1, "Note there was a pre-existing cutscene still in progress" ) end if( foo == nil ) then log( 1, "no courtine function provided for scene " .. scene.id ) return end cutscene = coroutine.create( foo ) prevCo = co co = cutscene -- set the global coroutine.resume(co, scene) -- first execution, runs to first yield co = prevCo -- restore prior value end -- add a new bot to the world. replaces existing NPC of same bot.id) -- this just starts the brain, a lua function. It is up to that function -- to create an NPC if it wants one (presumably it does) and the attributes -- of that NPC will be fully determined by the passed bot table contents function spawnBotBrain( bot ) log( 1, "creating bot brain coroutine for " .. bot.id ) -- init transients bot.isDead = 0 -- not dead yet -- make a new coroutine bot.brain = coroutine.create( bot.ai ) prevCo = co co = bot.brain -- set the global coroutine.resume( co, bot ) -- first execution, runs to the first yield co = prevCo end --- -- resume each active bot brain and let it run to its next 'yield' function stepBotBrains( ) for i,bot in ipairs( botList ) do if bot.brain then --log(1," stepping bot brain for " .. bot.id .. " " .. bot.name( bot )) co = bot.brain -- set global bot.brain = resumeOrKill() -- step brain end end end --- -- called from onBEAT at regular intervals (slowly - 80 bpm), gives -- each bot one 'yield', and ditto for a singleton cutscene, if any function scheduler() -- step our singleton cut scene if cutscene then co = cutscene -- set global cutscene = resumeOrKill() end -- the brains each get a step as well stepBotBrains() end -- assumes global co already set by scheduler -- lets the coroutine run to next yield, plus it -- detects terminated coroutines and returns nil so -- you can update your records and stop stepping a -- dead coroutine function resumeOrKill() if( co and coroutine.status( co ) == "suspended" ) then coroutine.resume( co ) -- pick up and run to next yield return co -- still alive, don't lose this ref! else if co then log( 1, "resumeOrKill ending coroutine in status " .. coroutine.status( co ) ) end return nil -- dead end end --- -- only to be called from within coroutines. Just wastes a significant -- amount of time before continuing function wait( beats ) -- really I just count calls to yield for this test if( co ) then local count = beats while count > 0 do --log( 1, "waiting for beats: " .. count ) count = count - 1 coroutine.yield( co ) end end end -- wait up to this many beats AFTER the dialog clears function waitOnDlg( beats ) if( co ) then dlgMode = 1 -- force it so we wait at least one beat local count = beats -- first wait forever for the dialog to clear while (dlgMode ~= 0) do coroutine.yield( co ) end -- then wait exactly n beats before returnng while (count > 0) do --log( 1, "waiting for beats: " .. count ) count = count - 1 coroutine.yield( co ) end end end -- wait up to this many beats for the function to return true function waitOnFunction( beats, object, fun ) if( co ) then if beats == 0 then beats = 5000 end local count = beats while (count > 0) and (fun( object ) ~= 0) do --log( 1, "waiting for beats: " .. count ) count = count - 1 coroutine.yield( co ) end end end -- Thank you stack exchange! -- merge table t2 into table t1, overwriting, retaining, or adding as needed (no deleting) -- table t1 is directly modified, table t2 is not altered function merge(t1, t2, level) --log(1, "-----MERGE_" .. level .. " START" ) for k, v in pairs( t2 ) do --log(1, "MERGE_".. level .." k= " .. k ) if (type(v) == "table") and (type(t1[k] or false) == "table") then -- log(1, " Recurse for subTable " .. k ) merge( t1[k], t2[k], level+1 ) elseif not t1[k] and type(v) == "table" then -- table did not exist in t1, we don't want to just -- get a reference to the source table, we want to recreate -- it. so -- log(1, " Recurse for missing subTable " .. k ) t1[k] = {} -- now it should really merge it merge( t1[k], t2[k], level+1 ) else safeValue = "" if type(v) ~= "table" and type(v) ~= "function" then safeValue = v end -- log(1, " SET" .. k .. " to " .. type(v) .. ": " .. safeValue ) t1[k] = v end end --log(1, "MERGE_" .. level .. " DONE" ) return t1 end -- make a new table with new values (not just references to old table) -- and let it inherit ALL the values from a base Table, plus overwrite -- those with the new table. This lets bots and scenes inherit from -- each other, so it is less tedious to maintain them function newTableWithBase( base, new ) obj = {} -- need a new table, we don't want to modify the originals obj = merge( obj, base, 0 ) -- apply base to empty obj obj = merge( obj, new, 10 ) -- apply new to obj-copy-of base return obj -- this is a reference to a new object, with new values, I hope end -- as we declare each new bot and scene, used in this map, we add them to these -- lists. We then iterate over these lists when looking for message handlers -- to deal with an incoming message. local glNumBots = 0 -- total bots defined local glNumScenes = 0 -- total scenes defined botList = {} -- master list of all bots on map sceneList = {} -- master list of all scenes on map function newScene( base, new ) -- ince first since ones-based glNumScenes = glNumScenes + 1 sceneList[ glNumScenes ] = newTableWithBase( base, new ) return sceneList[ glNumScenes ] -- you probably want to remember a global for this end function newBot( base, new ) -- inc first glNumBots = glNumBots + 1 botList[ glNumBots ] = newTableWithBase( base, new ) return botList[ glNumBots ] -- you probably want to remember a global for this end --- -- Occasionally you just have to know what's in a table. This dumps a table -- to the LOG tab of the SCRIPT panel function dumpTable ( t, indent ) for k, v in pairs(t) do --log( 1, indent .. " k= " .. k .. " v is a " .. type(v) ) if type( v ) == "table" then log( 1, indent .. k .. " is a Table --- " ) dumpTable( v, (indent .. " ") ) elseif type( v ) == "function" then log( 1, indent .. k .. " is a Function " ) else log( 1, indent .. k .. " = " .. v ) end end end -------------------- -- STANDARD OBJECT TEMPLATES -------------------- -- check the 'ship' argument and see if it is this 'bot' (table) -- probably should be called shipArgIsThisBot() -- note the coercion to 'number' by adding to 0, function isMyShip( bot, args ) return (0 + bot.ship) == (0 + args.ship) end -- form a numeric color value from RGB components (0-255 each) function RGB(r, g, b ) return (r * 65536) + (g * 512) + (b) end -- bots have a life, mostly tracking that of their associated npc/ship, -- but not the exact same bit of state. This is the official opinion of -- the bot itself as to whether it has been killed function botIsAlive( bot ) return (bot.isDead == 0) end ---- -- These are the fields present in all objects objRoot = { state = 0, -- mandatory, you own states > 0, I own 0 id = "", -- mandatory, must be unique among objects on this map hostSerNum = 0, -- playerSernum that is hosting this object } -- additional fields present in all bots: botRoot = newTableWithBase( objRoot, { ship = -1, -- no thumb spawnX = 1000, spawnY = 2000, spawnZ = 0, -- always 0, for now radarDist = 2000, -- past this, he stops appearing on radar radarColor = 0, -- RGB(200,45,187), -- small values of rgb have special meaning -- 0 black (does not appear on radar) -- 1-64 use the standard ship color -- transient stuff that spawnBot should overwrite on launch brain = nil, -- coroutine ref isDead = 0, -- I am just born, and have not died just yet behaveMask = 0, -- last behaviour mask we sent to npc wpnTgt = -1, -- the ship i want to kill navTgt = -1, -- the ship I want to follow guarding = 0, -- I want to face my nav target numHits = 0, -- num times the bot has been hit -- if you need a player friendly name, use this, and it will try to do the right thing name = function( bot ) return bot.pilotInfo.name end, -- virtual appearance of this bot pilotInfo = { rank = "", name = "root", face = "FACE_001", shell = "SHIP_000", podW = 2, podS = 2, podE = 2, rating = 1000, won = 0, lost = 0, lang = 1, -- G = 0 ? }, -- this is the bot brain, spawnBot() starts -- a coroutine running this function -- in particular, this is a completely default brain -- which you will likely override (or paramterize?) ai = function( bot ) -- birth --announce("You feel a disturbance in the force" ) wait( 1 ) createNpc( bot ) -- ask game to make NPC wait(3) bot:say( "I am ".. bot.name( bot ) .. "!" ) --radio( bot, "Touch me and die!" ) -- while alive while (bot.isDead == 0) do -- evaluate sensory changes -- (includes possible SCENE overrides) -- pick current GOAL(s) -- pick current Strategies -- pick current actions and set priorities -- send action change list to NPC -- (NPC will update us via messages, which we check as sense changes) wait( 1 ) end -- now we are dead -- drop some loot wait( 10 ) -- let them loot your corpse bot:exit( ) -- no longer in game announce("You hear the pop of a " .. bot.name( bot ) .. "-shaped bubble of spacetime" ) playSound( 49 ) -- 'crash' cymbal end, ------------------------------- -- These helper functions let you treat the bot as an object -- invoke them with a COLON not a PERIOD, so as to get the self arg added without having to maintain it yourself -- bot:say( "hello, I didn't have to make the self pointer, I just used a colon" ) alive = function( self ) return (self.isDead == 0) end, -- say something in a radio box, and auto-close after a normal reading time say = function( self, msg ) snapChat( -1, self, msg ) end, -- say something in a radio box that does not close until they tap the green arrow to ack it ask = function( self, msg ) radio( self, msg ) end, -- say something 'every so once in a while, but only if nothing else is being said' hint = function( self, beats, msg ) hint( self, beats, msg ) end, -- enter the room as it were. Mostly you preconfigure the bot (for example, its spawn location) -- before calling this. But you can provide a behaveMask and tgtIx override here, as a connvenience. -- I used to call this 'create/destroy' but it feels more story friendly to say enter/exit -- if toXY are provided, they override the spawn point in the bot;s table -- if fromXY are provided, they are a relative offset from toXY and the bot appears at that spot, then moves to toXY enter = function (self, behaveMask, tgtIx, toX, toY, fromX, fromY, uMsg, uMsg2 ) -- spawn bot as needed at spawn point (in bot's table) -- reset his helpful sense counters self.hintIndex = 0 -- rewind hints self.numHits = 0 -- we will count how much you hurt him self.isDead = 0 -- haven't killed him yet createNpc( self ) -- ask game to make NPC -- This is probably part of createNpc, -- set behaviour mask self.behaveMask = behaveMask self.wpnTgt = tgtIx self.navTgt = tgtIx self.guarding = 0 updateNpcBehaviorMask( self ) local shipIndex = self.ship if( toX and toY ) then if( fromX and fromY ) then setShipPos( shipIndex, fromX, fromY, 0 ) log( 1, "about to invoke go from inside enter " ) self:go( -1, -- tgtIx, 0, -- origin (galaxy coord) toX, -- dx, toY, -- dy, 0, -- dz, 30, -- radius, 100, -- msec, 10, -- secs, uMsg, -- uMsg, uMsg2 -- uMsg2 ); log( 1, "back from 'go' " ) else setShipPos( shipIndex, toX, toY, 0 ) end end end, -- leaves the map completely (no thumb, not on radar, no shell visible, etc) exit = function( self, toX, toY ) if( toX and toY ) then -- tgtIx, origin, dx, dy, dz, radius, msec, secs, uMsg, uMsg2 self:go( -1, 0, toX, toY, 0, 200, 100 ) wait( 3 ) -- watch them go end destroyNpc( self ) end, -- change the bots mood/behaviour -- probably should make some explicit emotions with complex parameterized activities. behave = function(self, behaveMask, tgtIx ) self.behaveMask = behaveMask self.wpnTgt = tgtIx self.navTgt = tgtIx updateNpcBehaviorMask( self ) end, --BEHAVE_MASK_HUNT --BEHAVE_MASK_KILL -- local newMask = bit32.bor( glLastRepairMaskSet, mask ) -- set bits provided ------------------ -- This is intended mostly for 'scripted' stageplay, with ships moving here and -- there, in order to deliver dialog. But the idea is that we command the ship -- autopilot to take the ship to a specific destination. Once it gets there, it -- stops and sends us a message. -- -- But the ship itself is running the same autopilot used in the various ai modes. -- -- We can declare the destination to either be an exact galaxy coordinate, or -- a relative offset from some object (another ship, star, or planet). -- -- origin -- 0 -- absoluteGalaxyLocation (0-7999) -- 1 -- near ship -- 2 -- near star.planet -- destIx -- the index of the ship or star which is the destination object -- dx, dy, dz -- an offset from object (or galaxy) center. The actual destination -- radius -- we have arrived when we are within this distance of destination -- msec -- we have to spend this much time inside radius, before it counts -- dur -- how long we want this to take (influences speed) -- hdg -- what do you want us to look at, at the end? -- 0 -- look towards destination -- 1 -- look away from destination -- -N -- look towards ship N -- follow -- 0 -- command expires when ship arrives -- 1 -- after arrival, continues to track ship, to meet the changing destination -- uMsg -- an optional message we will send when the ship arrives -- uMsg2 -- an optional message we will send when the ship departs (radius with hysteresis) -- -- will send optional messages upon arrival and departure -- go = function(self, tgtIx, origin, dx, dy, dz, radius, msec, secs, uMsg, uMsg2 ) self.behaveMask = bit32.bor( self.behaveMask, BEHAVE_MASK_HUNT ) self.navTgt = tgtIx -- or maybe this trumps wpnTgt for nav, just by being set. self.guarding = 0 self.origin = -1 -- no origin, assumes ship self.dx = 0 -- relative offset from object self.dy = 0 self.dz = 0 self.uMsg = nil self.uMsg2 = nil if( origin ~= nil ) then -- otherwise, use the provided style and all these self.origin = origin -- 0:galaxy 1:ship 2:star self.dx = dx -- relative offset from object self.dy = dy self.dz = dz self.radius = radius -- might live to regret name self.msec = msec self.secs = secs if( uMsg ~= nil ) then self.uMsg = uMsg end if( uMsg2 ~= nil ) then self.uMsg2 = uMsg2 end end updateNpcBehaviorMask( self ) end, guard = function(self, tgtIx ) --log(1,"guard setting behaviour") self.behaveMask = bit32.bor( self.behaveMask, BEHAVE_MASK_HUNT ) self.behaveMask = bit32.bor( self.behaveMask, BEHAVE_MASK_KILL ) self.navTgt = tgtIx -- hunt the guy i like, but facing outward (TODO) self.wpnTgt = myShipIndex -- in a solo game... self.guarding = 1 self.canHitFirst = 1 updateNpcBehaviorMask( self ) --log(1,"guard done setting behaviour") end, attack = function(self, tgtIx ) --log(1,"attack setting behaviour") self.behaveMask = bit32.bor( self.behaveMask, BEHAVE_MASK_HUNT ) self.behaveMask = bit32.bor( self.behaveMask, BEHAVE_MASK_KILL ) self.navTgt = tgtIx -- hunt the guy i like, but facing outward (TODO) self.wpnTgt = tgtIx -- in a solo game... self.guarding = 0 self.canHitFirst = 1 updateNpcBehaviorMask( self ) --log(1,"attack done setting behaviour") end, ---------------------------- -- optional BOT message handlers handler = { -- bot entered a zone, maybe it should think about it ['onENTRY'] = function( bot, args ) if isMyShip( bot, args ) then log( 1, "onENTRY handler in ".. bot.name( bot ) .. " for ship " .. args.ship .. ", zone" .. args.zone ) setPlayerZoneCache( args.zone, 1 ) log(1, "back from zone cache") if( botIsAlive( bot ) ) then --radio(bot, "I, ".. bot.name( bot ) .. ", claim Zone ".. args.zone .. "!") end log(1, "back from botIsAlive") end end, -- bot left a zone ['onEXIT'] = function( bot, args ) if isMyShip( bot, args ) then log( 1, "onEXIT handler in ".. bot.name( bot ) .. " for ship " .. args.ship .. ", left zone" .. args.prevZone .. " for zone " .. args.zone ) setPlayerZoneCache( args.zone, nil ) if( botIsAlive( bot ) ) then --radio(bot, "I, ".. bot.name( bot ) .. ", no longer care for Zone " .. args.prevZone.. "!") end end end, -- bot took damage ['onDAMAGE'] = function( bot, args ) if isMyShip( bot, args ) then log( 1, "onDAMAGE handler in ".. bot.name( bot ) .. " was called for ship " .. args.ship .. " by " .. args.attacker .. " pain: " .. args.damage ) --radio(bot, "'Ouch' That barely hurt!") -- this would be a good place to tag me and for me -- to remember who did it. -- so ai can easily detect new hits bot.numHits = bot.numHits + 1 end end, ['onDEAD'] = function( bot, args ) if isMyShip( bot, args ) then log( 1, "onDEAD handler in ".. bot.name( bot ) .. " was called for ship " .. args.ship .. ": " .. args.name .. " by " .. args.killer .. ": " .. args.killerName ) bot:say( "Ooof! You've pricked me to the core! I salute you, " .. args.killerName .. "!" ) bot.isDead = 1 end end, }, -- end of message handlers } ) --------------------- -- -- ROOT SCENE PROTOTYPE -- --------------------- sceneRoot = newTableWithBase( objRoot, { id = "", -- each OBJECT in map needs a unique tag to receive peer messages state = 0, hostSerNum = 0, -- playerSernum that is hosting this scene -- standard cutscenes, define the ones you want onAwareCutScene = nil, onLaunchCutScene = nil, -- optional message handlers handler = { ['onAWARE'] = function( scene, args ) log( 1, "onAWARE handler in scene id ".. scene.id .. " was called" ) -- Start the AWARE Cut Scene animation, if any. if scene.onAwareCutScene ~= nil then playCutscene( scene, scene.onAwareCutScene ) end end, ['onLAUNCH'] = function( scene, args ) log( 1, "onLAUNCH handler in scene id ".. scene.id .. " was called" ) -- standard init state function called on launch if scene.initState ~= nil then scene.initState( scene ) end -- Start the LAUNCH Cut Scene animation, if any. if scene.onLaunchCutScene ~= nil then playCutscene( scene, scene.onLaunchCutScene ) end end, ['onBEAT'] = function( scene, args ) -- log( 1, "onBEAT handler in scene id ".. scene.id .. " was called" ) if scene.maybeSendSceneState ~= nil then scene.maybeSendSceneState( scene ) end end, ['uSTATE'] = function( scene, args ) log( 1, "uSTATE handler in scene id ".. scene.id .. " was called" ) if scene.maybeConsumeState ~= nil then scene.maybeConsumeState(scene, args) end if scene.updateDisplays ~= nil then scene.updateDisplays( scene ) end log( 1, "uSTATE handler in sceneNPC received scene state " .. scene.state ) end, -- root SCENE does NOT take onDAMAGE, onDEATH. no default action }, -- standard init for the scene -- if you override this, you must provide full functionality initState = function( scene ) --log( 1, "initState in scene " .. scene.id .. " was called" ) scene.state = 0 scene.updateDisplays( scene ) end, -- I don't want to have to test for the existence of this one, so it is mandatory updateDisplays = function( scene ) --log( 1, "updateDisplays in scene " .. scene.id .. " was called" ) end, -- if we are the scene host, we periodically send status updates -- for this scene. state and scores in this case. These updates -- are only of interest to our peer scenes, so we need a sceneId lastSentState = 0, -- I could use a single 'dirty' flag if I was careful maybeSendSceneState = function( scene ) if ( iAmSceneHost( scene ) ) then if ( scene.lastSentState ~= scene.state ) then scene.lastSentState = scene.state sendToObj( scene.id , "uSTATE" , "state=" .. scene.state ) end end end, -- eventually we receive these state updates, so swallow it maybeConsumeState = function( scene, args ) local newState = 0 + args['state'] if( scene.state ~= newState ) then scene.state = newState end end, } ) -- end of sceneRoot table ----------------------------------------------------------------------------------------- --END OF BOILERPLATE-------------------------------------------------------------------- ----------------------------------------------------------------------------------------- ----------------------------------------------------------------------------- ----------------------------------------------------------------------------- ----------------------------------------------------------------------------- -- -- In theory, the standard infrastructure is all ABOVE this point and -- can be largely ignored by you, the map author. Here you get the -- benefit of that work, and can focus on crafting individual lua -- tables that tell your story in scenes and bots. -- REMEMBER YOUR COMMAs. When adding lines to a table, remember the commas. --================================================================================= -------------------- -- My own globals, if any -------------------- -- these match ids I used in the top half of the map glZoneIdStar = 10 -- this zone is around star, for detecting approach glKlausX = 100 glKlausY = 8000 glHomeX = 0 -- we will compute these later glHomeY = 0 glEnemyHomeX = 0 glEnemyHomeY = 0 -- beacons I will use beaconIdTutor = 0 beaconIdTarget = 1 beaconIdMe = 2 beaconIdKlaus = 3 beaconIdSlack = 4 -- pupIDs (not the same as pupIndices, use the range 100-199) PupIdMiner = 100 -- an escaped miner, disguised as a powerup -------------------- -- BOTS IN THIS STARMAP -------------------- -- Here we declare all the 'characters' we need on this starmap -- and configure each of them in all detail. You will find inheritance -- useful when making more than one of the same style bot. botSlack = newBot( botRoot, { id = "boss", -- must be unique among OBJECTs on this map (bots and scenes share this namespace) ship = 9, -- gets a thumb and looks like a player spawnX = 1200, -- gets a hard coded spawn location (does -1 here work for a random location?) spawnY = 2200, spawnZ = 0, brain = nil, -- spawnBot will set this to the coroutine ref when thinking radarColor= 8, -- if you have a thumb, I think that will take precedence, this is for -- things without thumbs that need to appear on the radar (enemy clone factory) -- if you have a thumb (first 12 NPCs get thumbs, but the first 8 are intended for human players) -- then you need this info for your PilotInfo panel to be interesting when people tap your thumb pilotInfo = { rank = "Smelly", -- not actually used yet name = "SLACK", -- use obj.name() and not this directly face = "FACE_025", -- stock FACE asset to use shell = "SHIP_007", -- stock SHIP asset to use podW = 4, -- starting pods podS = 4, podE = 4, rating = 1000, -- starting rating won = 0, -- starting stats lost = 0, lang = 2, -- G = 0 ? declared language rating of bot. }, } ) --dumpTable( botSlack, "SLACK BEFORE MERGE " ) -- Now, how simple can the SECOND bot be? -- only include fields that need to differ from parent botKlaus = newBot( botSlack, { id = "klaus", -- must be unique among OBJECTs on this map (bots and scenes share this namespace) ship = 10, -- gets a thumb and looks like a player spawnX = 1200, -- gets a hard coded spawn location (does -1 here work for a random location?) spawnY = 2200, spawnZ = 0, brain = nil, -- spawnBot will set this to the coroutine ref when thinking radarColor= 8, -- if you have a thumb, I think that will take precedence, this is for -- things without thumbs that need to appear on the radar (enemy clone factory) -- if you have a thumb (first 12 NPCs get thumbs, but the first 8 are intended for human players) -- then you need this info for your PilotInfo panel to be interesting when people tap your thumb pilotInfo = { rank = "Miner", -- not actually used yet name = "KLAUS", -- use obj.name() and not this directly face = "FACE_014", -- stock FACE asset to use shell = "SHIP_004", -- stock SHIP asset to use podW = 4, -- starting pods podS = 4, podE = 4, rating = 1000, -- starting rating won = 0, -- starting stats lost = 0, lang = 2, -- G = 0 ? declared language rating of bot. }, } ) botMinion1 = newBot( botSlack, { id = "minion1", -- must be unique among OBJECTs on this map ship = 11, -- gets a thumb and looks like a player spawnX = 3000, -- spawns in formation around SLACK spawnY = 3000, spawnZ = 0, brain = nil, pilotInfo = { rank = "Lt.", name = "SMITTY", face = "FACE_026", shell = "SHIP_004", podW = 0, -- nerf these guys podS = 2, podE = 2, -- rating = 1000, -- won = 0, -- lost = 0, -- lang = 2, -- G = 0 ? }, } ) --dumpTable ( botSlack, "SLACK AFTER MINION1 " ) botMinion2 = newBot( botMinion1, { id = "minion2", -- must be unique among OBJECTS on this map ship = 12, -- gets a thumb and looks like a player spawnX = 3100, spawnY = 3100, brain = nil, pilotInfo = { name = "JARJAR", face = "FACE_006", shell = "SHIP_001", }, } ) --- -- During the tutorial, let's have a friendly bot botTutor = newBot( botMinion1, { id = "tutor_1", -- must be unique among OBJECTS on this map ship = 8, -- gets a thumb and looks like a player spawnX = 1000, spawnY = 2200, brain = nil, pilotInfo = { name = "WATTE", face = "FACE_004", shell = "SHIP_006", }, ai = function( bot ) log( 1, 'bot ' .. bot.id .. 'started ai coroutine' ) -- as a tutor, I mainly just watch end, } ) ---- -- also we need a target drone. Note that we only see one of these, our -- own (and everyone uses the same index) botTarget = newBot( botRoot, { id = "tgt_1", -- must be unique among OBJECTS on this map ship = 20, -- no thumb spawnX = 1000, spawnY = 2200, brain = nil, pilotInfo = { name = "TARGET", face = "FACE_009", shell = "SHIP_008", podW = 2, podS = 2, podE = 2, }, ai = function( bot ) log( 1, 'bot ' .. bot.id .. 'started ai coroutine' ) -- as a target, I won't do much end, } ) -- log until we're sure merge is reliable log( 1, "------BOSS BOT --------" ) dumpTable( botSlack, "Slack ") log( 1, "------MINION1 BOT --------" ) dumpTable( botMinion1, "Minion1 " ) log( 1, "------MINION2 BOT --------" ) dumpTable( botMinion2, "Minion2 " ) log( 1, "------TUTOR_1 BOT --------" ) dumpTable( botTutor, "Tutor1 " ) log( 1, "------TARGET_1 BOT --------" ) dumpTable( botTarget, "Target1 " ) -- all the bots on this map. (bot tables must have been declared ahead of this) -- only bots in this list will receive messages or have their coroutines stepped. --botList = { botSlack, botKlaus, botMinion1, botMinion2, botTutor, botTarget } ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -------------------- -- SCENES IN THIS STARMAP -------------------- -- Each starmap should probably have a single scene, I am calling it Main for -- now, which is responsible for greeting the player and explaining the overall -- point of the map. -- For example, only the Main scene would respond to the onAWARE and onLAUNCH -- messages by playing cutscenes, while any scene might still want to be -- notified when those events take place. -- Again, this is all up to you. The only thing you are completely stuck with -- is onMsg() :-) You're free! Explore and create! Be creative and nice! --================================================================================= -- This map has several tutorial scenes on it, and one MAIN scene that -- coordinates things. -- -- the tutorials are not distributed, so each player gets their own copy, even -- though they are physically in the same place as other players, who see their -- copies, etc. Potentially confusing -- I do one tutorial after another, and increment this so I know which one I am doing -- note this is GLOBAL TO THE MAP. Each tutorial sets it upon completion, and -- starts the next tutorial, by sending a uMessage to a cleverly designed -- object id "tutorial_N" tutorialState = 0 nextTipOfTheDayIndex = 0 tips = { "Our struggle has been long, with few victories seen. Morale is low.", "Our pleas have gone unheeded for so long, we despair that no one hears our calls.", "Or perhaps they DO hear us. Darvon says they ignore us on purpose! ", "But Darvon is an idiot. He always overloads his drone with weapon pods, so he is slow and easy to destroy!", "You DO plan to help us, don't you?", "Because there are lots of other drone runners out there, you know. You're not the... Only!", "But we hear you are the BEST, so please don't let us down! We're really counting on you!", "Hello?", "... ...", "Darvon! Are you sure this thing is on? What do you mean its out of IMPROOVIUM! That's both impossible and ironic!", "If anyone is out there... if anyone can hear us... please join us. please pick a drone thumb color and join us.. before it is too late!", } function tipOfTheDay() numTips = #tips if( nextTipOfTheDayIndex >= numTips ) then nextTipOfTheDayIndex = 0 -- but we inc it before we use it end nextTipOfTheDayIndex = nextTipOfTheDayIndex + 1 return tips[ nextTipOfTheDayIndex ] end --------- -- MAIN SCENE --------- -- This scene greets the player, and introduces them to the challenge, -- which will take place over a handful of scenes that teach the player -- the basics of piloting and fighting, while conquering a boss and -- his minions, and setting free the imprisoned miners. -- And yes, there are coded secrets behind all the names and keywords used -- in this map :-) DALVIK being, of course, the java virtual machine -- used by android at this time. Other names are more self-referential, -- and I apologize for anything too cloying. -- onAware plays a cut scene before ship is launched -- "Here's the problem, will you help?" -- (launching is your acknowledgement) -- can then tutorialiate with tips until you launch and extoll you -- onLaunch plays a cut scene after ship is launched -- "Welcome recruit, ;et's get you checked out first" -- starts tutorial scene, which walks me through stuff in a solo -- experience not shared by other players, though they will see me do it. -- onDead maybe something, but only after a long delay to let -- all the other dead-related messaging scroll by -- I guess this scene is always hosted by the moderator, and its primary states -- are -- 0 idle, freshly loaded map where nothing has happened yet --------------------------------- ---- ---- MAIN SCENE ---- --------------------------------- -- our possible states STATE_IDLE = 0 STATE_LEARN_TRIGGER = 1 ------------ -- TOKENS USED ON THIS MAP TokenFinishedTutorial = 1 -- let's you skip straight to the open Pen TokenFoundPals = 2 -- let's you skip finding his friends TokenDisabledReactor = 3 -- let's you skip the reactor capture the flag TokenUnmaskedSlack = 4 -- let's you skip the Slack boss fight sceneMain = newScene( sceneRoot, { id = "Main", -- each OBJECT in map needs a unique tag to receive peer messages state = STATE_IDLE, -- tutorial data hasShotHimself = 0, -- count them since the map was loaded hasKilledHimself = 0, -- the root scene will play some cutscenes automatically, in response -- to certain game engine messages (onAWARE and onLAUNCH for example). -- So scenes like this one, can just override those cutscenes. ----------- -- onAWARE: i start this when they can first see it, before they pick color -- but after they connect to server instance onAwareCutScene = function( scene ) scene.state = STATE_IDLE -- back to the beginning wait( 3 ) -- they see map preview setCamVeil( 0, 500 ) -- fade map preview to some percent of normal wait( 1 ) -- let the preview fully disappear -- this initiates a full zoom in from galaxy view. I should probably fade that in as it is pretty abrupt setCamStar( 0 ) -- move camera to watch central star (planet DALVIK in this case) setCamZoom( CamZoomGroup, 2000 ) -- start zoomed in on the planet setCamOrbit( 5 ) -- gently orbit wait(3) -- Let there be Title announce('SLAVES OF IMPROOVIUM.' ) wait( 5 ) -- scroll the following text in fancy narration mode textStory( myStoryStyle, [[ / / Planet DALVIK drifts alone through this star system... arid, foul and dusty. But DALVIK's cold heart hides a secret: the planet's core is almost solid IMPROOVIUM, an element treasured throughout the galaxy! / / This has attracted the attention of villainous scum like Space Pirate SLACK, Nemesis of Free Space! / / SLACK has hijacked several drone factories which even now churn out robot minions to do his dark will. / / Even worse... he has enslaved thousands to work deep in the planet's IMPROOVIUM mines, where life is dark, brutal and short. / / We seek new DRONE RUNNERS, willing to help us free the slaves, vanquish the pirates and destroy their evil drone factories! / / / Will you help us? ]] ) wait( 7 ) announce( "Are you the... One, " .. myShipName .. "?" ) wait (20) -- let it scroll off screen --setCamVeil( 100, 500 ) -- turn the veil off -- try to be informative AND entertaining while they puzzle which thumb to tap -- right now, this will be aborted as soon as another cut scene starts, even one in -- another scene, and I think that is probably wrong. scenes probably also want -- persistent brain coroutines... but still, there is the concept of the singleton -- cut scene that puts up text and changes camera and such. while 1==1 do wait(15 + (15 * math.random()) ) -- between 15 and 30 beats, is the idea --setCamVeil( 25, 500 ) -- turn the veil on textStory( 0, tipOfTheDay() ) wait( 3 ) --setCamVeil( 100, 500 ) -- turn the veil off end end, ------------ -- onLAUNCH: this is the main scene, so it introduces our tutor and then -- hands us off to a series of task coroutines until I complete everything -- it remembers several tokens so I can skip completed sections on -- later visits. It also offers an OPTION command to help you forget onLaunchCutScene = function( scene ) log(1," Starting onLaunch cut scene") -- Again, the player just launched, so we just learned their ship index, -- so we have some math to do we could not do before. -- this is not currently used scene.state = STATE_LEARN_TRIGGER -- we introduce the trainer and the training pen scene.hasShotHimself = 0 -- fresh tutorial start -- now that we know our shipindex, so we can put SLACK on the opposing home zone if ( (myShipIndex % 2) == 0) then glHomeX = 1948 + 50 glHomeY = 1948 + 50 glEnemyHomeX = 6044 + 50 glEnemyHomeY = 6044 + 50 else glHomeX = 6044 + 50 glHomeY = 6044 + 50 glEnemyHomeX = 1948 + 50 glEnemyHomeY = 1948 + 50 end -- This is a tutorial, disable all controls and then introduce them -- one by one. We shove them inside their own personal training pen -- to ensure each player has an individual experience on a shared -- server forceShipToStart( myShipIndex ) -- Freeze the newbie in place, with broken controls -- create our Tutor right away, since the camera will start on his drone, not ours -- this also means the first drone the player sees is a cool one, and then a little -- joke when they get the one they are assigned as a newbie -- We spawn WATTE again, but this time he is zooming in from parts unknown -- possibly, that makes no sense, since he was just here a second ago -- I just wanted to test the 'from' arguments -- I only include the messages because I want him to stop when he gets there -- (and the presence of a message also enables that behaviour) -- this is surprisingly effective, zooming in on a speeding ship, while -- having the camera orbit it, slowing as we reach our destination. -- I wish I had some good music for this botTutor:enter( 0, -1, botTutor.spawnX, botTutor.spawnY, 4000, 4000, "HitMark", "LeftMark" ) -- behaveMask, tgtIx, toX, toY, fromX, fromY, uMsg, uMsg2 setCamSpy( botTutor.ship ) -- move camera to watch tutor setCamZoom( CamZoomCloseUp, 2000 ) -- start zoomed in on his ship setCamOrbit( 5 ) -- gently orbit his ship wait(10) -- they just entered and probably got slammed with notifications, let those drain -- This is just a link from the onAWARE scrpt, a final line of narration from the mystery voice announce( "Good. Welcome to the fight, " .. myShipName, "!" ) wait(3) -- close the gate, maybe they get a glimpse setMapBarrierStateV( ixDoorBarrierV, 3 ) -- animate to closed -- But now we get an incoming radio message (popup dialog) -- the radio is from our tutor, and he will set us our first task -- INTRODUCTION -- the colon here causes it to add the object as a hidden self pointer -- plus it looks a little like a screenplay, but don't be confused! -- Note that the script only pauses when you wait, When you 'say' or -- 'ask' something, it moves right along. Such messages are even -- queued for the reader, so you must periodically pause for that -- queue to be drained, or you will lose the interactivity. -- The only difference between 'say' and 'ask' is that a 'say' window -- will close automatically after the user has had time to read it, -- but a 'ask' window will remain open until the player clicks on its -- green arrow. Mission critical data should be delivered with 'ask' -- to be sure the user sees it. -- leave this set to 0 normally. Otherwise it will ONLY do the numbered section -- you specify, and will skip all the rest as if the player had the token -- set this to 0 for normal operation scene.overrideToken = 0 -- allows me to skip chunks in development if ( scene.overrideToken ~= 0 ) then botTutor:ask( "You realize, of course, you are forcing task " .. scene.overrideToken .. ". Don't ship like this!" ) end -------- -- TASK 1: handoff from launch, and learn controls log(1," Testing if they have done Task 1 ") if( (scene.overrideToken == 1) or not hasToken( TokenFinishedTutorial ) ) then -- you have not yet learned the controls scene:tut_LearnTheControls() setToken( TokenFinishedTutorial, 1 ) else -- you have learned the controls, but are still stuck in a pen -- until I release you. This leaves you with an open pen and a realiable -- source of full restore PUPs and a safe place. scene:tut_UnlockThePen() end -- in all paths, the pen must be open by now --------- -- TASK 2: Meet KLAUS and rescue his pals log(1," Testing if they have done Task 2 ") if( (scene.overrideToken == 2) or not hasToken( TokenFoundPals ) ) then scene:tut_HelpKlausFindPals() setToken( TokenFoundPals, 1 ) end -------- -- TASK 3: Disable the core of the enemy clone reactor log(1," Testing if they have done Task 3 ") if( (scene.overrideToken == 3) or not hasToken( TokenDisabledReactor ) ) then scene:tut_DisableCloneReactor() setToken( TokenDisabledReactor, 1 ) end ---------- -- TASK 4: unmask SLACK log(1," Testing if they have done Task 4 ") if( (scene.overrideToken == 4) or not hasToken( TokenUnmaskedSlack ) ) then scene:tut_UnmaskSlack() setToken( TokenUnmaskedSlack, 1 ) end ---------- -- REPLAY VALUE -- At this point, they have fully experienced the map's content, and we need -- to congratulate them log(1," Completed all programmed content") botTutor:say( "But you've proven yourself, " .. myShipName .. ", you really have what it takes!" ) botTutor:say( "You're HERO material! Nothing less than that!" ) botTutor:say( "Now get out there and defend our Galaxy!" ) -- Ideally we would now have random rare spawns pop up so there was a multiplayer -- reason to hang on this map wait( 10 ) -- switch to a hint of the day mode here botTutor:say( "You can melee here, or maybe use the MAP button to explore new star systems." ) botTutor:say( "You can use the OPTIONS/STARMAP menu to reset this map, to do it again." ) botTutor:say( "Or you can just search here for more encrypted starmaps." ) end, -------- -- Standard intro that leaves you in an open training pen, with a powerup recharge tut_UnlockThePen = function( scene ) log(1," Starting Unlock the Pen") -- acknowledge botTutor:ask( "Welcome back, " .. myShipName .. "! Ready for me to unlock the gate?" ) waitOnDlg( 0 ) -- camera to 'me' setCamOrbit( 0 ) -- stop orbiting wait(1) setCamZoom( CamZoomWide, 200 ) -- zoom out to normal scale wait(2) setCamSpy( myShipIndex ) -- repair all makeRepairs( myShipIndex, REPAIR_MASK_ALL ) -- zero defects! -- free gifts? -- recurring heal pup in pen -- does this work for non-moderator? resetPupXY( myPupIndexHeal, 11, myPenPupX, myPenPupY, 1, 20 ) -- remove pen wall wait( 3 ) -- let camera stabilize botTutor:say( "Great! Let's get you out of there!" ) wait( 2 ) setMapBarrierStateV( ixDoorBarrierV, 2 ) playSound( SOUND_BARRIER_CHANGE ) waitOnDlg(3) -- let them appreciate the wall disappearing -- give a hint/reminder, but this ends the cutscene botTutor:say( "Your drone is fully repaired, but you'll want more equipment!" ) botTutor:say( "Just avoid gravity long enough, and maybe you'll find a useful powerup!" ) waitOnDlg( 2 ) botTutor:exit( 4000, 4000 ) -- head off in the direction of DALVIK botSlack:exit() -- mainly so he isn't still there after we resurrect log(1," Finished: Unlock the Pen") end, -------------------------------------------------------------- -- Storyline (repeats on every visit) -- -- Locales -- * WEST and EAST each have a clone factory (with flags) -- SLACK appears to belong to the Player's opposing team (EAST vs WEST) -- Player's home factory is a safe place for the player, and WATTEs retreat -- Enemy home factory zone drains energy from player -- Enemy zone is SLACK's retreat, guarded by SLACK's minions -- * Behind the energy depot (Klaus Spawn) -- * slack-in-base location -- * wherever the slace/watte battle ends (watte death scene) -- -- -- Gameplay -- * There are escaped miner powerups which stack on your pup bar. -- click pup to drop off for points/rewards -- * at some point, minions begin guarding the enemy clone reactor -- 'flags' are actually 'clone reactor core crystals' and if you steal 3, you destroy the core -- * which makes it harder than usual to take enemy flag from center of factory -- * but if you do get the enemy flag back to your own base, it damages the enemy base 1 point -- * your goal is to destroy the enemy base, which exposes SLACK to you -- * you then have a final battle with SLACK to complete the map -- * you beat him, and he turns out to be WATTE in disguise -- * it was all a test, you're ready to move on -- but you can replay the map as much as you like ----------------------------------------------------------------- -- TUTORIAL: Learn the Ship Controls tut_LearnTheControls = function( scene ) log(1," Starting: Learn the Controls") botTutor:ask( "Can you hear me, " .. myShipName .. "?" ) -- does not auto-close -- I need the player to really be there, so will hold here until they close the dialog waitOnDlg( 1 ) -- wait for all pending messages to clear botTutor:say( "Great! I think your camera is on me! I'll turn my beacon on..." ) wait( 5 ) setBeacon( beaconIdTutor, botTutor.ship, 1 ) waitOnDlg(1) botTutor:say( "I'm just outside this bullet-proof training pen." ) setCamZoom( CamZoomCloseUp, 2000 ) botTutor:say( "YOUR drone is INSIDE the training pen, being repaired, all systems down." ) waitOnDlg(1) setCamSpy( myShipIndex ) -- back to pilot wait( 5 ) -- let them admire their ship up close setCamZoom( CamZoomWide, 2800 ) -- pull out slowly to show pen waitOnDlg(1) -- make my beacon flicker and go out botTutor:say( "Let's turn YOUR beacon on." ) wait( 1 ) setBeacon( beaconIdMe, myShipIndex, 1 ) -- put a beacon on myself wait( 6 ) -- this is also letting the camera catch up playSound( SOUND_INSUFF_CHARGE ) -- electric buzz setBeacon( beaconIdMe, -1, 1 ) -- turn off my own beacon wait( 3 ) setBeacon( beaconIdMe, myShipIndex, 1 ) -- turn on my own beacon wait( 2 ) playSound( SOUND_INSUFF_CHARGE ) -- electric buzz setBeacon( beaconIdMe, -1, 1 ) -- turn off my own beacon wait( 3 ) botTutor:say( "Well, we'll fix your beacon later, I guess. For now, you're the guy without a beacon." ) waitOnDlg(1) -- TRIGGER TEST (where we force the recruit to shoot themselves) -- Seems to work best if the repaired item appears first, then the announcement of repair botTutor:say( "I'm OUTSIDE the pen, in case you get trigger happy..." ) botTutor:say( "or, in case you get a trigger... we're still working on that!" ) waitOnDlg(4) setCamOrbit( 0 ) -- stop orbiting (and end up directly behind ship) makeRepairs( myShipIndex, REPAIR_MASK_TRIGGER ) -- trigger appears hiliteUI( HL_TRIGGER ) -- make it glow for a few seconds wait( 2 ) -- admire it a moment before dialog pops right on top of it (on narrow screens) botTutor:say( "Hold on... ok.. you've got trigger! It's that circle on the left." ) waitOnDlg(2) -- wait a moment so they can actually SEE it, if covered -- wait forever until he obeys, causing him to shoot himself in reflected fire botTutor:say( "Just tap (or hold) the trigger to fire! Watch out, your weapons are hot!" ) scene.maxWaits = 30 while scene.hasShotHimself == 0 do if( scene.maxWaits > 0 ) then scene.maxWaits = scene.maxWaits - 1 if( scene.maxWaits <= 0 ) then botTutor:say( "Just tap the trigger...that BIG circle on the left. What could go wrong?" ) hiliteUI( HL_TRIGGER ) scene.maxWaits = 35 end end wait(1) -- we hang until he obeys end botTutor:say( "Sorry, I guess I could have warned you about that. Bullets bounce." ) waitOnDlg(0) hiliteUI( HL_TRIGGER ) botTutor:say( "That BLUE bit on the side of your trigger, shows your CHARGE." ) waitOnDlg(3) -- let them see it botTutor:say( "You need CHARGE to fire weapons, but your drone recharges directly from the Aether!" ) waitOnDlg( 1 ) -- STEERING TEST makeRepairs( myShipIndex, REPAIR_MASK_STEERING ) hiliteUI( HL_STEERING ) -- tecnically i can only highlight NAV which is steering and engines together wait(1) botTutor:say( "OK, we've got your steering working! The circle on the right is your NAV stick. " ) waitOnDlg(1) botTutor:ask( "Slide your NAV stick left or right to turn. Give it a try!" ) botTutor:say( "That RED bit on the side of your NAV stick, shows your ENERGY!" ) hiliteUI( HL_STEERING ) -- closest I can get for now waitOnDlg(3) -- let them see it botTutor:say( "Your drone will explode if it runs out of ENERGY, so try not to let that happen!" ) botTutor:say( "If nothing else, losing your drone means losing your progress on this map." ) waitOnDlg( 3 ) -- MAP INTRODUCTION makeRepairs( myShipIndex, REPAIR_MASK_MAPS ) wait( 1 ) botTutor:ask( "I think we have your hyperdrive working! But leave the MAP button alone for now, please!" ) botTutor:say( "The MAP button opens your Starmap Selector, connecting you to drones all over the galaxy!" ) botTutor:say( "But only starmaps you have collected and unlocked!" ) botTutor:say( "If you switch maps now, you will lose your progress on this tutorial." ) waitOnDlg( 3 ) -- VIEW CONTROLS botTutor:ask( "Drag three fingers to pan and tilt the view, or use two-finger pinch-zoom... Here's a top view" ) wait(6) -- request a camera change msec = 1800 -- take this many milliseconds to reach new value setCamPitch( CamPitchDown, msec ) -- looking straight down, mostly (avoids singularity) setCamHdg( CamHdgBehind, msec ) -- aligned with ship heading setCamZoom( CamZoomWide, msec ) -- waitOnDlg(0) botTutor:ask( "Give it a try, I can wait.. for a minute. Use 3 fingers to pan and 2 fingers to zoom." ) waitOnDlg(0) -- toss in the only CAM button instruction they will get makeRepairs( myShipIndex, REPAIR_MASK_CAM ) botTutor:ask( "Tap that CAMERA button (left of trigger) to get your camera lined up behind your drone!" ) waitOnDlg(3) botTutor:say( "OK, back to work! I'm resetting your camera now!" ) wait( 6 ) msec = 500 -- take this many milliseconds to reach new value setCamPitch( CamPitchDown, msec ) -- looking straight down, mostly (avoids singularity) setCamHdg( CamHdgBehind, msec ) -- aligned with ship heading setCamZoom( CamZoomWide, msec ) -- estimated zoom to show just the pen -- AIMING AT STATIONARY TARGET botTarget:enter( 0, -1 ) setCamSpy( botTarget.ship ) -- give him the camera wait( 4 ) setBeacon( beaconIdTarget, botTarget.ship, 1 ) botTutor:ask( "I've set up a target drone in the other side of your pen. Can you see it?" ) waitOnDlg( 5 ) setCamSpy( myShipIndex ) -- back to me, and leave it alone now botTutor:say( "See if you can hit that drone without hitting yourself!" ) while ( botTarget.numHits == 0 ) do -- remind him of the task botTutor:hint( 25, { "Use your NAV to aim for that gap in the wall.", "Try to bounce your shot into the drone, watch out for bounces back to you.", "Use pinch-zoom if you can't see the drone, or the gap in the wall", "Imagine where that wall would hit, if it didn't stop short. Aim for that spot.", "Just fire one shot at a time, until you're safely aimed. No sense shooting yourself!", } ) wait(1) end waitOnDlg( 0 ) botTutor:say( "Great shot, now destroy that thing!" ) while( botIsAlive( botTarget ) ) do wait( 1 ) end -- turn off beacon 1 (target beacon) setBeacon( beaconIdTarget, -1, 1 ) waitOnDlg( 3 ) botTutor:say( "Good Work, "..myShipName.."! You saved the world from that cardboard model" ) waitOnDlg( 3 ) botTarget:exit() -- no longer in game -- AIMING AT MOVING TARGET botTutor:say( "See if you can do it again with the default camera!" ) wait( 1 ) -- request a camera change msec = 2000 -- take this many milliseconds to reach new value setCamPitch( CamPitchSide, msec ) -- closer to horizon setCamHdg( CamHdgBehind, msec ) -- aligned with ship heading setCamZoom( CamZoomWide, msec ) -- estimated zoom to show just the pen waitOnDlg( 3 ) -- bring the drone back botTarget:enter( 0, -1 ) wait(1) setBeacon( beaconIdTarget, botTarget.ship, 1 ) botTutor:say( "Destroy that drone! Before it comes completely online!" ) -- after this many beats, the bot will wake up and hunt the player -- but since the player is held EXACTLY OPPOSITE it will just -- jam against the wall, and it is then possible to shoot it with -- enough reflections. This is a PUZZLE and might be too hard -- Take pity on them if they are struggling scene.numBeatsUntilWakesUp = 10 -- make a function to set up targets more easily -- note I can just add data willy nilly to the bot, so long as I -- don't read it before I write it. Here I add some flags and -- counters to detect when my orders have been followed by -- the player. These counters are then incremented in message -- handlers. while( botIsAlive( botTarget ) ) do if( scene.numBeatsUntilWakesUp > 0 ) then scene.numBeatsUntilWakesUp = scene.numBeatsUntilWakesUp - 1 if( scene.numBeatsUntilWakesUp == 0 ) then botTutor:say( "Too late! Watch out, it's alive!" ) -- put the bot into a more evil mindset -- currently, this is too hard, so just let -- the target remain dumb -- botTarget:behave( BEHAVE_MASK_HUNT, myShipIndex ) end else botTutor:hint( 25, { "Try shooting the wall behind you, a little off center.", "I don't know why the training program insists on you passing this test.", "It's not really representative of the skill set you'll need out there.", "But you'll never get out of that Pen until you destroy that drone. I know that much!", } ) end wait( 1 ) end wait( 10 ) -- time to admire the corpse (maybe this 'drops a power up' botTarget:exit( ) -- no longer in game TODO: Exit with explosion particles and sound setBeacon( beaconIdTarget, -1, 1 ) -- turn off target beacon botTutor:say( "You're doing great, " .. myShipName .. "!" ) waitOnDlg( 2 ) -- ENGINE TEST makeRepairs( myShipIndex, REPAIR_MASK_ENGINES ) hiliteUI( HL_STEERING ) wait( 1 ) botTutor:say( "OK, we've got your engines on line! That circle on the RIGHT is your NAV stick!" ) waitOnDlg( 0 ) hiliteUI( HL_STEERING ) botTutor:ask( "Push your NAV stick forward to apply thrust! Pull your NAV stick back to apply reverse thrust!" ) waitOnDlg( 3 ) -- STOP TEST makeRepairs( myShipIndex, REPAIR_MASK_STOP ) wait( 1 ) botTutor:say( "OK, your inertial dampers are repaired!" ) botTutor:ask( "Tap the STOP button to cancel all motion instantly! But watch out, it uses a lot of charge!" ) botTutor:ask( "Hold the STOP button for several seconds, to disconnect from your drone (pick a new color)" ) waitOnDlg( 3 ) -- PYRAMID PICKUP TEST makeRepairs( myShipIndex, REPAIR_MASK_PUPS ) wait( 1 ) botTutor:say( "OK, your powerup bar is repaired! This is where powereups appear after you collect them." ) botTutor:say( "You find powerups all over the galaxy. They look like little pyramids" ) botTutor:ask( "Just fly right over a powerup pyramid to pick it up. What's inside is a surprise!" ) botTutor:ask( "Once you have a powerup on the bar, just tap it to use it (or select it as new trigger weapon)." ) waitOnDlg( 1 ) botTutor:say( "I can also just give you some stuff. Here's a full recharge!" ) waitOnDlg( 1 ) givePup( 11 ) -- full restore wait( 6 ) --resetPup( myPupIndexHeal, seconds ) resetPupXY( myPupIndexHeal, 11, myPenPupX, myPenPupY, 1, 20 ) wait( 2 ) botTutor:say( "Try to pick up that powerup inside your pen! Meanwhile, we'll work on fixing your radar!" ) waitOnDlg( 0 ) -- RADAR (with a fake self destruct sequence to test the countdown timer and klaxon) wait( 5 ) makeRepairs( myShipIndex, REPAIR_MASK_RADAR ) hiliteUI( HL_RADAR ) wait( 1 ) botTutor:say( "OK! You should have a RADAR/COMPASS now! It's that big circle around your drone!" ) waitOnDlg(0) setCamZoom( CamZoomGroup, 100 ) -- we zooom in a little to make the radar bigger botTutor:ask( "The colored lines point to other ships. You can also see nearby beacons, zones and gravity wells" ) waitOnDlg( 3 ) botTutor:say( "Can you see me on your RADAR? I'll bring up another drone!" ) waitOnDlg( 1 ) -- bring back the target botTarget:enter( 0, -1 ) botTutor:say( "Fly around and see how the RADAR lines move!" ) botTutor:say( "Watch how the drone's RADAR line changes when I turn on its beacon!" ) waitOnDlg( 2 ) setBeacon( beaconIdTarget, botTarget.ship, 1 ) botTutor:say( "What's a beacon for? Well, might be a distress call. You gotta check it out!" ) botTutor:say( "That LONG line shows the direction of your motion, which might differ from where you're pointed!" ) waitOnDlg( 0 ) -- WEAPON SELECTION - MINES -- USING MINES (target drone is orbiting you, as it were, you lead it into a mine field) -- MISSILES (shows targetting target the tutor, then the drone) -- BARRIERS -- ZONES -- PODS -- TEAM INFO (map offers two team melee outside of the training pen) -- put up a score panel for num wins on each side, distributed state by moderator -- SOCIAL -- CAM button makeRepairs( myShipIndex, REPAIR_MASK_SOCIAL ) -- clean up botTarget:exit( ) -- no longer in game -- repair the ship completely botTutor:say( "Well, " .. myShipName .. ", you've really impressed all of us!" ) botTutor:say( "I think we can trust you to the next level!" ) makeRepairs( myShipIndex, REPAIR_MASK_ALL ) resetCamera() -- give them a token that let's them skip all the above setToken( TokenFinishedTutorial, 1 ) -- set to 0 to re-experience the tutorial -- Give the player some free gifts botTutor:say( "Here's some stuff you might find useful out there!" ) waitOnDlg( 3 ) givePup(3) -- mines botTutor:ask( "These mines are safe for you and your friends, but will explode when enemies approach them." ) waitOnDlg( 2 ) givePup(2) -- homing missiles botTutor:ask( "These Homing Missiles will seek out any target. In a pinch they will pick their own target." ) botTutor:say( "I know what you're thinking, don't even try it!" ) waitOnDlg( 2 ) givePup(22) -- encrypted starmap botTutor:ask( "You'll probably find some encrypted starmaps. Decrypt them, and you might find a new star system." ) waitOnDlg( 2 ) givePup(15) -- shields botTutor:ask( "These shields protect you from all weapon fire, but only for a few seconds." ) waitOnDlg( 2 ) -- take down the wall botTutor:ask( "Now that you know how to control your drone, what do you say? Still in it to win it?" ) botTutor:say( "OK! I'm opening your pen now, but you're always welcome back! I'll keep a full restore out for you!" ) waitOnDlg( 2 ) -- State 2 will fade out (to state 0) and the wall will animate out of existence setMapBarrierStateV( ixDoorBarrierV, 2 ) playSound( SOUND_BARRIER_CHANGE ) log(1," Finished: Learn the Controls") end, ----------------------------------- -- TASK: Help Klaus tut_HelpKlausFindPals = function( scene ) log(1," Starting: Help Klaus") -- PART 1 -- -- * we pick up just after the pen wall opens botTutor:enter( 0, -1 ) -- stationary mode with no targets wait(6) -- * WATTE gives us the back story and challenge botTutor:ask( myShipName .. ", I need you to keep your eyes open for escaped miners." ) botTutor:say( "They might lead us to the dread pirate, SLACK!" ) botTutor:say( "I'll be around somewhere, if you need me... WATTE OUT!" ) waitOnDlg( 2 ) -- have Watte actually fly off somewhere (and then disappear) -- and maybe the target drone should follow him like a pet botTutor:exit( 4000, 4000 ) -- head off in the direction of DALVIK --resetCamera() -- return to normal player-centric view -- TODO: some interesting delay before klaus talks to us wait( 25 ) -- * KLAUS spawns in/near the fuel depot botKlaus:enter(0, -1, glKlausX, glKlausY ) -- Do we do a cut scene for this? I mean cut camera to fuel depot for a second as he arrives? -- * Player gets KLAUS's distress call -- must follow beacon to find him botKlaus:ask("Hello? Can anyone hear us? Anyone USEFUL, I mean?") waitOnDlg( 0 ) freezePlayer() setCamSpy( botKlaus.ship ) botKlaus:ask("If you can handle it, rendevous behind the fuel depot. I'll turn on my beacon") wait(4) -- probably the first meainingful use of a beacon, to find somebody setBeacon( beaconIdKlaus, botKlaus.ship, 1 ) waitOnDlg(0) releasePlayer() resetCamera() -- return to normal player-centric view -- * Player rendevous with KLAUS, an escaped miner -- must get close to Klaus (and engines freeze) -- onBEACON( minDist, maxDist ) - test local ship and remember state and report state changes -- * Klaus begs us to rescue N escaped miners -- ship is released from 'docking' wait( 3 ) -- jam here until the player gets close enough to Klaus, then freeze the player -- Note: we pass an array of 'hints' that are played periodically, in order, cyclically -- until the player completes the task. I do this a lot :-) scene:waitUntilPlayerNearBot( botKlaus, 200, 500, { "Just follow my beacon and get real close to my ship, real slow. Then we'll lock on.", "You're going to need access to my air lock, if this is to work.", "Your ship should stop automatically once you meet the profile... slow and close.", "Otherwise... Well, good thing we're near a clone factory, right?", "In the center of each factory is a clone reactor core. It's powered by three crystals." } ) -- stop his ship setShipVel( myShipIndex, 0, 0 ) freezePlayer() -- zoom in for this contact --setCamSpy( botKlaus.ship ) waitOnDlg( 1 ) setCamZoom( CamZoomCloseUp, 1000 ) -- so you just got close to Klaus, he greets you and explains his problem botKlaus:ask("Thanks for coming, " .. myShipName .. "! My name is KLAUS.") botKlaus:say("I stole this ship from SLACK, when we escaped his mines!") botKlaus:say("But... we got split up.. and some of my pals got left behind.") botKlaus:say("They're still out there, disguised as powerups so SLACK doesn't find them!") botKlaus:say("Can you help me rescue my pals? Just pick 'em up, and bring 'em to me here..") botKlaus:say("I know it's a lot to ask of a stranger, but these are hard times.") waitOnDlg( 2 ) -- i want to start the camera zoom out at the same time this last message appears botKlaus:ask("I have to wait here, at our meeting point. Just bring me my pals!") releasePlayer() resetCamera() waitOnDlg( 2 ) -- wait for user to clear that last one -- -- PART 2 -- -- (game starts spawning escaped-miner pups, you can pick up as normal) -- (they stack on your button bar, up to N) scene:startEjectPilotDetector( pupMiner ) -- increments rescued counter when pup is 'used' -- * Player rescues N escaped miner (powerups) and returns to KLAUS -- * has to rendevous again, locking ship again -- * Clicking miner pups near KLAUS transfer them to his ship -- * He says something witty and/or encouraging for each one -- they will be spending quite awhile in this probably scene:waitForAllMiners() -- does not get here until all miners have been rescued setToken( TokenFoundPals, 1 ) -- I am of two minds, cleaner to grant on exit, but just in case they crash... -- at this instant, we know they are close to SLACK, so let's freeze them now -- but not announce it yet local mask = REPAIR_MASK_ENGINES -- and disable engines and weapons + REPAIR_MASK_TRIGGER setShipRepairMask( myShipIndex, mask ) -- TODO: ship shutdown sound setShipVel( myShipIndex, 0, 0 ) -- hint that something is not right playSound( SOUND_DISENGAGE ) -- * On receipt of Nth miner, botKlaus:ask("Smitty! I'm so glad to see you! Thanks, " .. myShipName .. ", you rounded up all my pals!" ) -- * sound effect, player ship functions fail (weapons, steering, engines) -- * KLAUS turns out to be SLACK in disguise waitOnDlg( 2 ) botKlaus:ask("There's just one problem... My name isn't Klaus!" ) wait(1) -- 14 incoming chat -- 16 metronome -- 35 - 82 percussive instrument hits -- 200 plot point -- 201 bongos transition -- 202 conclusion -- 203 message box -- 204 button click -- 205 chime 'dong' playSound( 200 ) -- Major Plot Point setCamZoom( CamZoomCloseUp, 1000 ) -- close up on player (with Klaus nearby) waitOnDlg( 0 ) botKlaus:exit() -- klause disappears botSlack:enter( 0, -1, glKlausX, glKlausY ) -- slack appears in his place botSlack:ask("My name is SLACK, as in 'dread space pirate SLACK'!" ) botSlack:say("I believe that means YOUR name is 'idiot'!" ) botSlack:say("I especially thank you for recovering my minions after WATTE sent them flying!" ) botSlack:ask("I think you'll find your drone is quite broken. I have.. plans.. for you!" ) botSlack:say("For now, just sit back and watch! I have him on screen now..." ) waitOnDlg( 5 ) -- * SLACK takes back the miners and is about to destroy us -- -- PART 3 -- -- * but WATTE reappears and saves Player, -- WATTE and SLACK can't be on screen at same time, so -- slack is about to shoot us -- we see RADIO from WATTE saying he is GOING to help us -- TODO: needs a bit more suspense, and sound effects botTutor:ask( myShipName .. "! I saw an energy discharge, are you OK? I'm on my way!") waitOnDlg( 2 ) -- WATTE starts from our clone factory, and then makes a bee line towards SLACK botTutor:enter( 0, -1, glHomeX, glHomeY ) botTutor:go( botSlack.ship ) -- SLACK says this to us, presumably WATTE doesn't hear it. can I indicate whisper? botSlack:say( "on his way... to my TRAP, that is! And YOU'RE the BAIT!" ) -- SLACK leaves to set a trap for WATTE waitOnDlg( 2 ) botSlack:go( botTutor.ship ) -- We are still disabled, but we hear it all on radio as they battle it out -- We can hear them on the radio even though their ships are actually hidden botTutor:say("I see you up ahead! I'm almost there! Why don't you respond, " .. myShipName .. "?") botTutor:say("There you are! No wait, what's that?") botSlack:say("Surprised, WATTE? You shouldn't be!") botTutor:say("SLACK! You've escaped!") botSlack:say("Yes, with " .. myShipName .. "'s foolish help! Along with all my minions!") waitOnDlg( 2 ) -- pauses to think botTutor:say("Looks like you win THIS round, SLACK") botSlack:say("Too bad it's your LAST round, WATTE!") waitOnDlg( 2 ) -- some attack we can't see. TODO: sound effect botTutor:say("I doubt it. You'd need some sort of coupled energy beam to make ME worried!" ) waitOnDlg( 0 ) playSound( SOUND_ENGAGE ) botSlack:say("About that... ") botTutor:say("NO!!!.. not that... ... good luck, " .. myShipName .. " ... " ) -- After awhile, our systems come back online waitOnDlg( 5 ) -- restore engines and weapons setShipRepairMask( myShipIndex, 0 ) playSound( SOUND_ENGAGE ) wait(2) resetCamera() -- we don't see it, but SLACK skulks off to his clone factory at start of next section botSlack:exit() -- and WATTE teleports to our hacked clone factory at the start of the next -- section, but for now we wink him out botTutor:exit() waitOnDlg( 15 ) -- time to be worried about WATTE before starting next task log(1," Finished: Help Klaus") end, ------------------------- -- TASK disable clone reactor tut_DisableCloneReactor = function( scene ) log(1," Starting: Disable Reactor") -- put SLACK in home zone botSlack:enter(0, -1, glEnemyHomeX, glEnemyHomeY ) -- he is just eyecandy until repaired -- Put WATTE in home zone botTutor:enter(0, -1, glHomeX, glHomeY ) -- but moved to Team home zone botTutor:ask( myShipName .. "... We've hacked... one of his... clone.. factories. Meet me... there..... beacon... " ) wait(1) setBeacon( beaconIdTutor, botTutor.ship, 1 ) -- end of the off-screen battle, time for player to do something. -- do we have a Jarvis to tell us we're back on line (I want that to be cambot)? -- bases get configured (enemy base starts getting dangerous) -- We rendevous with WATTE, only to see his drone explode -- this routine doesn't return until the player has 'docked' with -- the target bot. (They have to get close, at low speed). They -- will then be stopped (but not frozen). -- The array of hint messages will be seen cyclically until they complete -- the task scene:waitUntilPlayerNearBot( botTutor, 200, 500, { "I'm in bad shape, but he's hurt, too. He's licking his wounds in his Clone Reactor!", "I'm sorry I blamed you, ".. myShipName ..". It wasn't your fault", "You weren't ready... I failed in your training.. It's my fault", "He's in no shape to stop you from stealing his crystals, but he'll have minions nearby.", "I'm sorry I won't be there for you, when you need me the most.", "You can't let him get fully repaired, or there will be no stopping him!", "You remember how to follow a beacon, right?" } ) -- stop his ship setShipVel( myShipIndex, 0, 0 ) freezePlayer(); -- we have arrived at WATTE in our home Factory waitOnDlg( 0 ) setCamZoom( CamZoomCloseUp, 1000 ) -- zoom on US, but we're close to WATTE -- WATTE 'dies' botTutor:ask( "OK, " .. myShipName .. " here's the deal. You're gonna have to do this on your own." ) waitOnDlg(0) botTutor:say( "There are two Clone Factories in this Star System. This is one of them, the one I hacked." ) setCamToOrbitSpot( glHomeX, glHomeY, CamZoomGroup ) botTutor:say( "Inside the reactor, you're safe. The walls reflect weapon fire." ) waitOnDlg(4) botTutor:say( "But there's another reactor, the one where SLACK is now." ) wait(3) setCamToOrbitSpot( glEnemyHomeX, glEnemyHomeY, CamZoomGroup ) botTutor:say( "They look identical, except for the colors. You can fly right through the walls." ) botTutor:say( "Also, there's a negative energy field, draining you while in the core." ) botTutor:say( "In the center is the Core Crystal, which the HUD shows as a FLAG, red or blue." ) botTutor:say( "You need to steal that crystal, and bring it back here." ) waitOnDlg(0) wait(2) botTutor:say( "Then go back twice more for the others.." ) waitOnDlg(0) setCamToOrbitSpot( glHomeX, glHomeY, CamZoomGroup ) botTutor:say( "Take out his minions first... but try not to hurt Smitty!" ) botTutor:ask( "Remember... THREE crystals... look like flags... pick up THERE.. and drop off HERE." ) waitOnDlg(2) setBeacon( beaconIdTutor, -1, 1 ) -- beacon off (symbolizes dying) botTutor:say( "Do it for ME, " .. myShipName .. ". You owe me, you know. You KNOW it." ) waitOnDlg( 2 ) botTutor:say( "WATTE.... out.... " ) wait(3) -- OK, try to make his ship explode setShipState( botTutor.ship, SHIP_STATE_EXPLODING ) wait(3) botTutor:exit() -- hopefully, when he re-enters, he will get new state automatically --setCamZoom( CamZoomWide, 1000 ) releasePlayer() resetCamera() -- * WATTE appears to have been destroyed (sad music) -- TODO: make WATTE play the explode (can I set state?) waitOnDlg( 2 ) -- * Meanwhile, SLACK has escaped to his home base, which then gets defensive minions -- we are held in protective custody by home base, fully healed -- SLACK taunts us, then the camera follows him back to his home -- we see his corner tower defenses get built (he brags on radio) -- he disappears, so we just see the zone and flag as normal, plus towers -- TODO: somehow keep SLACK alive during the stray weapon's power.. -- TODO: also use invulnerability to keep player safe from griefers while in dialog -- spawn the minion guards on top of SLACK and let them sort it out -- but I now declare that the factory square always holds 4 nodes (as many as nine maybe) -- which should be emphasized for RTS factory emulation botMinion1:enter(0, -1 ) log(1,"minion1 finished entering") botMinion1:guard( botSlack.ship ) log(1,"minion1 done starting guarding") botMinion2:enter(0, -1 ) log(1,"minion2 finished entering") botMinion2:guard( botSlack.ship ) log(1,"minion2 done started guarding") -- -- Part 4 -- -- * If a tower is left alone (not destroyed), it can tear off and be a free-flying minion that chases Player -- towers respawn after some number of seconds -- 5 seconds as non-shooting, but hard to kill -- 60 seconds of stationary tower, limited range and firepower -- then tears off to be a minion and can chase Player) -- * Player destroys towers faster than they are rebuilt or tear off -- I am changing my mind on the towers and instead will just have minion bots -- who have AI that nav targets SLACK, so they try to orbit him. Might need -- a GUARD nav where the goal is to point away from target instead of towards -- target -- it's nice to have a beacon kinda pointing at the next thing you need to do setBeacon( beaconIdSlack, botSlack.ship, 1 ) -- trash talk until he gets close, but not too close scene:waitUntilPlayerNearBot( botSlack, 600, 500, { -- getting anywhere near close the first time "Oh boo hoo. You owed him NOTHING! Nor did I!", "He was not the man you thought he was. Not the man at all.", "Just wait until my clone reactor repairs my engines! And my weapons!", "Enjoy what are surely your final moments in this star system, " .. myShipName } ) -- I do NOT stop him -- but that's it, no more auto-stop when approaching Slack stopTrigger( 1 ) -- some final invective before the fight starts -- maybe have the 'towers' appear one by one -- this is also how we know you approached close enough botSlack:say( "Smitty! Load a torpedo for me, and write '".. myShipName .. "' on the side!" ) waitOnDlg(2) setShipTgt( botMinion1.ship, myShipIndex ) -- they both hate the local player now setShipTgt( botMinion2.ship, myShipIndex ) -- handle the battle of the Clone Reactor -- returns when three flags have been successfully captured -- miniature capture the flag game scene:waitForReactorFailure() -- doesn't get here until player completes task, and we will -- grant token on return log(1," Finished: Disable Reactor") end, ---------------- -- TASK unmask Slack tut_UnmaskSlack = function( scene ) log(1," Starting: Unmask Slack") -- Slack should still be in his clone reactor botSlack:exit() -- we exit and re-enter, just in case you killed him by accident botSlack:enter(0, -1, glEnemyHomeX, glEnemyHomeY ) -- he is just eyecandy until repaired setBeacon( beaconIdSlack, botSlack.ship, 1 ) botSlack:say( "CURSES! You've destroyed my Clone Factory and all my minions! ... even Smitty!" ) botSlack:say( "But you have yet to defeat ME, and now I return to FULL STRENGTH!" ) -- trash talk until he gets close, but not too close scene:waitUntilPlayerNearBot( botSlack, 600, 500, { "Haven't you heard? This Star System is MINE!", "By the juice of Zaphod, I will control the spice!", "You know, WATTE, at least, showed some class.", "WATTE was actually a pretty swell guy, and I regret killing him.", "It must be hard on you that he blamed you to the end.", "I mean, after all, things were going pretty well for WATTE before YOU showed up.", } ) -- I do NOT stop him -- player frozen in home zone, gets recharge, watchs cut scene with camera work -- SLACK gets the first whack -- TODO: engine AI should not pull trigger, if inside no shooting zone botSlack:attack( myShipIndex ) -- NOW it's a fight botSlack:say( "THAT was for Smitty!" ) -- Part 5 -- -- * enemy base becomes safe-ish (no towers) -- * but SLACK appears, and any remaining tear-off minions -- * battle until all bots are destroyed -- maybe slack is invulnerable/deferred until you destroy minions -- Trash talk during boss fight -- The big boss fight scene:waitForBossDead( botSlack, { "Is that what you call a weapon? How cute!", "Seriously, I don't mean to pry, but are you sure you're cut out for this hero business?", "My mom wanted me to be a doctor. Mom's love having doctors in the family.", "Good old mom. Terrible cook, but excellent mother.", "That was surprisingly painful, I must admit. I think you're getting better!" } ) setToken( TokenUnmaskedSlack, 1 ) -- just in case, we grant token early -- * when SLACK is destroyed, he reveals he was WATTE all along -- * you pass the test -- TODO: need some camera work here, and a way to position -- everyone (fade to black, move everyone?) botSlack:say( "OK.. OK.. I think you've proven your point" ) botSlack:say( "No.. really.. you can stop shooting now. " ) botSlack:say( "Seriously, those aren't real weapons." ) botSlack:say( "You think we'd trust a raw recruit with real weapons?" ) waitOnDlg( 2 ) botSlack:say( "Oh sorry, you see. I'm not SLACK" ) playSound( 200 ) -- Major Plot Point -- TODO: closeup on slack? -- we need to freeze slack's ship, and grab his xy, so we can teleport watte to the same spot local si = botSlack.ship setShipVel( si, 0, 0 ) wait(2) local x = botSlack.spawnX local y = botSlack.spawnY if ( glShips[ si + 1 ] ) then x = glShips[ si + 1 ].x -- from cache, should be 'close' y = glShips[ si + 1 ].y end waitOnDlg(2) botSlack:exit() botTutor:enter(0, -1, x, y ) --setShipPos( botTutor.ship, x, y, 0 ) botTutor:say( "I'm WATTE, your tutor! And this has all just been part of your training!" ) botTutor:say( "Smitty is fine, by the way, and at his daughter's dance recital" ) botTutor:say( "Sorry we had to be so rough on you, but this is important stuff!" ) botTutor:say( "We have to be SURE you can handle it, before we really let you go." ) waitOnDlg( 2 ) log(1," finished: Unmask Slack") end, ----------------------------------------------------------------------- -- SCENE HELPER FUNCTIONS --------- -- waits indefinitely for the player to approach a specific ship -- The idea is to declare a beacon with a radius and duration -- the game engine will then send us a message if the player ever -- remains inside that zone for that duration, we catch that -- message and set a global, which we clear here before we start -- waiting senses = {}, -- i think I have to predeclare this waitUntilPlayerNearBot = function( scene, bot, dist, msec, hints ) local senseIndex = 1 -- in theory we have an array of these scene:startDistDetector( senseIndex, bot, dist, msec, "uGotCloseToKlaus", "uGotFarFromKlaus" ) scene.senses[ senseIndex ] = 0 -- clear old value while ( scene.senses[ senseIndex ] == 0 ) do bot:hint( 25, hints ) wait( 1 ) end end, -- we have to ask the game engine to notify us when the player's ship reaches -- its scripted destination. -- The engine increments a 'sense counter' when the required trigger is met. -- Coroutines then look at those counters, and reset them, as needed startDistDetector = function ( scene, senseIndex, bot, dist, msec, uMsg, uMsg2 ) local senseIndex = 1 -- TODO: pass this in args -- actually add a new message handler, dynamically. wewt. -- this message is sent when player gets close scene.handler[ uMsg ] = function( scene, args ) log( 1, uMsg .. " handler in scene id ".. scene.id .. " was called" ) bot.closeToPlayer = 1 -- player is currently close to this bot if ( scene.senses[ senseIndex ] ) then -- increment the sense and let the caller work out what to do scene.senses[ senseIndex ] = scene.senses[ senseIndex ] + 1 -- ok, maybe we also force player to stop, just to make -- rendevous less of a hassle, but script must immediately -- disable player's controls, if the goal is to hold them -- fixed in space. --setShipVel( myShipIndex, 0, 0 ) end end -- this message is sent when player gets 'distant' (double the sense radius) scene.handler[ uMsg2 ] = function( scene, args ) log( 1, uMsg2 .. " handler in scene id ".. scene.id .. " was called" ) bot.closeToPlayer = 0 -- no longer close end -- BUT we won't get EITHER of those until we declare a trigger. -- Only then will the game engine track the relationship. -- 1 = distance check from ship, send message when trigger hits setTrigger( 1, bot.ship, dist, msec, senseIndex, uMsg, uMsg2 ) end, -- This guy senses message sent when an escaped miner pup is 'used' by the player -- (meaning they tapped the PUP button on their PUP bar). In theory this launches -- the miner out some tube, and should only be done while near KLAUS, who has an -- open airlock. In actuality, it just increments the number of miners that -- have been rescured. the 'closeToPlayer' field is ONLY set by those trigger -- messages above, and is not some universal thing that's always available for -- all bots. startEjectPilotDetector = function( scene, pup ) -- we want to turn a message about the local pilot using a scripted powerup, into -- some counter changes and maybe some radio scene.handler[ pup.useMsg ] = function( scene, args ) log( 1, pup.useMsg .. " handler in scene id ".. scene.id .. " was called" ) if (botKlaus.closeToPlayer == 0) then -- no credit if not near Klaus botKlaus:ask("Dude! Don't just VENT guys to space! You have to be close to ME when you do that!") botKlaus:say( "Just approach me slowly and you will auto-stop when in position. Then tap the pup slot" ) else -- give credit, but no chat since we're not a coroutine scene.numMinersRescued = scene.numMinersRescued + 1 end end end, ------------ -- does all the logic of sensing your collection -- of escaped miners and does not return until that action is -- complete waitForAllMiners = function( scene ) -- this variable gets incremented by a message handler scene.numMinersRescued = 0 -- none saved yet scene.totMinersNeeded = 5 -- we need to rescue this many scene.lastNumMinersRescued = 0 -- so we detect changes log(1, "Starting search for " .. scene.totMinersNeeded .. " escaped miners") while ( scene.numMinersRescued < scene.totMinersNeeded ) do if( scene.numMinersRescued > scene.lastNumMinersRescued ) then scene.lastNumMinersRescued = scene.numMinersRescued local remaining = scene.totMinersNeeded - scene.numMinersRescued if( scene.numMinersRescued == scene.totMinersNeeded ) then botKlaus:say( "You did it! You found all my pals... even Smitty!" ) elseif ( scene.numMinersRescued < scene.totMinersNeeded ) then botKlaus:say( "Way to go, ".. myShipName .. "! Only " .. remaining .. " more to find!" ) else botKlaus:say( "Dude! That's not one of MY guys! Throw him back, quick!" ) end else botKlaus:hint( 25, { "My pals are probably all over this star system by now.", "Their suits hold enough air for a while, but the smell builds up pretty fast.", "You'll want to bring them back to me as fast as you can.", "Did I mention they are disguised AS POWERUPS? Yeah, the little pyramids.", "I think I saw one headed inside the Fuel Depot. But be careful in there. Unshielded panels.", "It's kinda fun in the depot, if you're into that sort of thing.", "After you pick them up, you'll need to sort of SHOOT them into my airlock.", "Be sure you're close to my ship when you release them.", "I'll try to keep an airlock open to catch them with." } ) end wait(1) end end, ----------- -- doesn't return until you have stolen N clone reactor crystals -- * Player dashes in, grabs flag, and takes it to home zone, does that N times waitForReactorFailure = function( scene ) -- initializes scene.numReactorCrystalsStolen = 0 scene.numReactorCrystalsNeeded = 3 scene.numReactorCrystalsStolenLast = 0 -- just in case you have been messing around, bring them back now resetPup( 249, 3 ) resetPup( 250, 3 ) -- hijack the flag message handler for this scene -- the engine only sends this when ship is in a team-friendly zone, -- we just want to know whose flag they had scene.handler['onFLAG'] = function( scene, args ) local ship = 0 + args['ship'] -- 0-7 local flag = 0 + args['flag'] -- likely 249 (west) or 250 (east) local zone = 0 + args['zone'] -- likely 9 (west) or 10 (east) local zoneTeam = 0 + args['team'] -- likely to be 0 (none), 9 (West), or 10 (East) log( 1, "saw onFLAG message. Ship " .. ship .. " dropped flag " .. flag .. " in zone " .. zone .. ", zTeam: " .. zoneTeam ) if( zone >= 0 and zone < 100 ) then -- we need to collapse 8 players into two teams local shipTeam = (ship % 2) -- Here, i prefer 0 = west, and 1 = east, (ship is 0-7 here) local flagTeam = ((flag+1) % 2) -- 249 -> 0, 250 -> 1 if ( shipTeam ~= flagTeam ) then -- and only when enemy flag log(1, "Flag dropped in its opponents zone. myShipIx is: " .. myShipIndex ) if (ship == (0+myShipIndex)) then log(1, "Flag was dropped by our player and earns credit" ) -- just increment when it was done by our player scene.numReactorCrystalsStolen = scene.numReactorCrystalsStolen + 1 log(1, "numXtals Stolen = " .. scene.numReactorCrystalsStolen ) end end end -- all paths need to restart the flag pup resetPup( flag, 3 ) end local smittyWasAlive = 1 -- assume he started alive -- wait for goal, hint/taunt while (scene.numReactorCrystalsStolen < scene.numReactorCrystalsNeeded ) do if( scene.numReactorCrystalsStolen > scene.numReactorCrystalsStolenLast ) then local remaining = scene.numReactorCrystalsNeeded - scene.numReactorCrystalsStolen scene.numReactorCrystalsStolenLast = scene.numReactorCrystalsStolen if( remaining > 1 ) then botSlack:say( "Fool, I still have " .. remaining .. " reactor crystals! More than I need!" ) else botSlack:say( "Even this LAST crystal is more than I need, to defeat a worm like YOU!" ) end else if (smittyWasAlive == 1) then if( not botMinion1:alive() ) then smittyWasAlive = 0 -- farewell smitty, we barely knew ye botSlack:say( "Smitty! You've destroyed Smitty! You MONSTER! You will PAY!" ) end end botSlack:hint( 25, { "To defeat me, you'd have to remove ALL the power crystals from my clone reactor!", "Plus, you'd have to carry each one, all the way back to your hacked clone reactor!", "If you even try it, The core of the reactor itself will burn you!!", "If my minions don't burn you first!!", "Fool, this is a CLONE FACTORY! Anything you destroy, I can rebuild!" } ) end wait( 1 ) end -- * base is destroyed, no more towers spawn, end, ------------ -- doesn't return until you have killed bot -- boss comes to life, no longer invulnerable, no longer stationary -- boss picks player as target and begins hunting -- lots of trash talk -- maybe a special weapon use -- otherwise, just battle until its beaten (boss is stronger than normal ship) waitForBossDead = function( scene, bot, hints ) bot:say( "Prepare yourself!" ) bot:attack( myShipIndex ) -- at this point he wants to hurt me, and will actively seek me while( botIsAlive( bot ) ) do if( nil ) then -- maybe react at partcular energy levels or something else -- otherwise, just periodically hurl a 'hint' (taunt, more likely) bot:hint( 35, hints ) end wait( 1 ) end wait( 10 ) -- time to admire the corpse (maybe this 'drops a power up' end, -- return to standard player-centric camera ---------------------------------- -- optional SCENE message handlers handler = { -- while I do not provide my own message handlers for -- onAWARE and onLAUNCH, I do override the cutscenes played -- by both of those. -- note that BOTs get their own notifications, this is the SCENE -- I use the SCENE notifications to update local state booleans the -- SCENE cares about (and the bot might have no idea over) -- However, in this process, I add state to the bot, that it doesn;t -- know anything about, and there is some danger I could step on the -- toes of another scene. This is just something you, the author, need -- to watch out for, the same as you allocating ship indices or barrier -- indices. It's your job. Luckily, the namespace is just this one -- starmap, so you shouldn't have too much trouble! I suggest making -- global values where to assign these shared resources all in one spot -- of the file, though that goes against my goal of each scene being -- easily copied and pasted (even more important in Lua, since there is -- no easy and obvious "missing variable" notification. You just get 'nil' -- and probably throw an exception, stopping the coroutine without any -- real error info.) -- Hmm, one could probably make an API emulator that ran in a pure lua -- environment, where the author could test their syntax in the presence -- of a good editor (one that detects missing commas in table entries, for example!) ['onDEAD'] = function( scene, args ) log( 1, "onDEAD handler in scene id ".. scene.id .. " was called" ) log( 1, "onDEAD ship ".. args.ship .. ": " .. args.name .. " killed by " .. args.killer .. ": " .. args.killerName ) if (args.ship == myShipIndex and args.killer == myShipIndex ) then if( scene.state == STATE_LEARN_TRIGGER ) then botTutor:ask( "Oops, " .. myShipName .. ", you've destroyed your own drone!" ) botTutor:ask( "Go ahead and launch a new drone. We'll try again!" ) end scene.hasKilledHimself = scene.hasKilledHimself + 1; end end, ['onDAMAGE'] = function( scene, args ) log( 1, "onDAMAGE handler in scene ".. scene.id .. " was called for ship: " .. args.ship .. " attacker: " .. args.attacker ) if ( (args.ship == myShipIndex) and (args.attacker == myShipIndex) ) then if( (scene.state == STATE_LEARN_TRIGGER) and (scene.hasShotHimself == 0) ) then botTutor:ask( "Watch out, " .. myShipName .. "! Bullets bounce off of most barriers! And your own weapons can hurt you!" ) end scene.hasShotHimself = scene.hasShotHimself + 1; end end, }, -- end of handler table }) -- end of scene table -------------------------------------------------------------------------------- -- Moderator should be able to start a CTF game and a RACING game as -- additional demonstration scenes, sending gate configuration as a state string -- xy, xy, xy, xy, , numLaps to complete -- keep some best time info around for that. starmap leaderboard maintained -- until instance is emptied, passed from moderator to moderator. --=============== ------------------------ -- capture the flag game sceneNPC = newScene( sceneRoot, { id = "NPC", updateDisplays = function( scene ) if ( scene.state == 0 ) then -- scene is asleep, all displays off displayOff( 0 ) displayOff( 1 ) displayOff( 2 ) option(0, 1, "uSTART_NPC", "Summon The Bots" ) else option(0, 1, "uSTOP_NPC", "Banish The Bots" ) if (scene.state == 2 ) then display(2, 50, 30, "(Check OPTIONS for starmap options)", "GAME OVER" ) else displayOff( 2 ) end end -- this one is always there option(1, 0, "uRESET_TUTORIAL", "Reset Tutorial Progress" ) end, ------------------------------ -- optional message handlers handler = { -- MENU commands ['uRESET_TUTORIAL'] = function( scene, args ) log( 1, "uRESET_TUTORIAL handler in scene id ".. scene.id .. " was called" ) areYouSure( "Forget all progress on this starmap, so you can redo it from the start?", "FORGET ALL", "uRESET_DOIT" ) end, ['uRESET_DOIT'] = function( scene, args ) log( 1, "uRESET_DOIT handler in scene id ".. scene.id .. " was called" ) -- actually do the deed when we get this frm the areYouSure botTutor:ask( "Your permanent record has been wiped clean! Now re-enter the sector, please." ) setToken( TokenFinishedTutorial, 0 ) -- strip them setToken( TokenFoundPals, 0 ) -- strip them setToken( TokenDisabledReactor, 0 ) -- strip them setToken( TokenUnmaskedSlack, 0 ) -- strip them -- they have to actually relaunch to see everthing change, maybe I should do something end, ['uSTART_NPC'] = function( scene, args ) log( 1, "uSTART_NPC handler in scene id ".. scene.id .. " was called" ) if( scene.state == 0 ) then playCutscene( scene, scene.enterTheBots ) scene.state = 1 -- start game scene.updateDisplays( scene ) -- startTimer( 0, 20, 1, "", "uTIMERDONE", "" ) end end, ['uSTOP_NPC'] = function( scene, args ) log( 1, "uSTOP_NPC handler in scene id ".. scene.id .. " was called" ) scene.initState( scene ) end, }, -- end of handler table enterTheBots = function( scene ) wait( 3 ) announce( "ENTER THE BOTS" ) wait( 2 ) announce( "by Danny Samuel" ) wait( 6 ) announce( 'Say hello to the master nemesis bot!' ) wait( 1 ) spawnBotBrain( botSlack ) wait( 20 ) announce( 'Meet his first minion!' ) wait( 1 ) spawnBotBrain( botMinion1 ) wait( 15 ) announce( 'Meet his second minion!' ) wait( 1 ) spawnBotBrain( botMinion2 ) wait( 15 ) announce( 'Meet his third minion!' ) wait( 1 ) spawnBotBrain( botMinion3 ) end, }) -- paranoiacally log some of these to make sure inheritance works log( 1, "------SCENE ROOT --------" ) dumpTable( sceneRoot, "SceneRoot ") log( 1, "------SCENE MAIN --------" ) dumpTable( sceneMain, "SceneMain ") log( 1, "------SCENE NPC --------" ) dumpTable( sceneNPC, "SceneNPC ") ------------------------------------------------------------------------------- -- Scripted PowerUps used on this map -- n escape miner (disguised as a powerup) pupMiner = { bundleName = "an Escaped Miner", desc = "Escapee from the Improovium mines of DALVIK, take to KLAUS asap!", numInPack = "1", maxCanOwn = "5", useMode = 2, -- 0:idle 1:onPickup 2:onTap 3:onTrigger useMsg = "uEjectedMiner", -- msg to be sent when local player uses one iconId = 30, wpn = { wpnName = "Escaped Miner", usesAmmo = 1, singleShot = 1, homing = 0, needsTarget = 1, autoRepeatMsec = 1000, lifetimeMsec = 20000, speed = 5, power = 23, soundLaunch = 15, soundContact = 16, soundFizzle = 17, shotsPerFullCharge = 20, }, } log( 1, "------Test Powerup --------" ) dumpTable( pupMiner, "pupMiner ") --============================================================================= -- stuff I do once when map is loaded, preferably nothing. But if I did -- need to precompute some tables, this would be the place to do it. -- In this map, I work out the dimensioins of the eight training pens, -- but remember this happens before the player gets here, so you don't -- know which slot they will pick. log( 1, "----- Modifying StarMap Geometry --------" ) -- I should really use some decoration for these globals, like 'glSide' instead of just 'side' border = 256 -- stay away from edge of galaxy glPenSide = 1000 -- side length of each pen rad = glPenSide/2 -- 'radius' of the pen vgap = rad -- vert dist between pens cx = 4000 cy = 4000 ixTopLine = 20 ixLeftLine = 20 ixDoorBarrierV = 20 -- this can open/close -- I try to keep a little info around for all the pens, stuff I can -- know before launch of any ships glPenPupX = {} glPenPupY = {} function getPenDimensions( ix ) -- work out the interesting coordinates row = math.floor(ix/2) -- 4 'rows' of pens -- cx = border + glPenSide/2 if (ix%2) == 1 then cx = 8191 - (border + glPenSide/2) end cy = 4096 - (row-2) * (glPenSide + vgap) - (glPenSide+vgap)/2 --cy = cy - (row * (glPenSide + vgap)) -- allocate some indices, we use more than one ixTopLine = 20 + ix * 4; ixLeftLine = 20 + ix * 4; ixDoorBarrierV = ixLeftLine if (ix%2) == 0 then ixDoorBarrierV = ixLeftLine + 1 -- I want the right line, for the left pens, open towards the star end end -- this happens BEFORE launch, so no player info function buildTrainingPen( ix ) getPenDimensions( ix ) -- work out some constants local pain = 0 local xpar = 0 local color = ix + 1 local state = 1 local left = cx - rad local right = cx + rad local top = cy + rad -- y increases upwards local bottom = cy - rad local f = glPenSide / 10 -- size of navigation gap setMapBarrierH( ixTopLine, state, left, right, top, color, xpar, pain ) -- top line (north) setMapBarrierH( ixTopLine+1, state, left, right, bottom, color, xpar, pain ) -- bottom line setMapBarrierH( ixTopLine+2, state, left+f, right-f, cy, color, xpar, pain ) -- mid line setMapBarrierV( ixLeftLine, state, bottom, top, left, color, xpar, pain ) -- west side setMapBarrierV( ixLeftLine+1, state, bottom, top, right, color, xpar, pain ) -- east side -- some ship stuff x = cx y = cy - rad/2 hdg = 0 setMapSpawn( ix, x, y, hdg ) -- the pup spawn location glPenPupX[ix+1] = cx - rad/2 glPenPupY[ix+1] = cy - rad/2 end -- this happens AFTER launch, so customize for THIS player -- it leaves behind some stuff valid for My Ship Only myPupIndexHeal = 100 -- 8 of these, one for each player myPupIndexMiner = 110 -- 8 of these, one for each player myPenCX = 0 -- eventually, the center of my pen myPenCY = 0 myPenRadius = 0 myPenYLow = 0 -- Y of south end of pen myPenYHi = 0 -- Y of north end of pen myPenPupX = 0 -- where my private pen PUP will go myPenPupY = 0 function forceShipToStart( ix ) -- force his ship to hold in position tgt = -1 -- take away any target he had mask = REPAIR_MASK_ALL -- and disable all his controls setShipRepairMask( ix, mask ) setShipTgt( ix, tgt ) -- set up a target dummy spawn point getPenDimensions( ix ) -- remember my personal settings myPenCX = cx myPenCY = cy myPenYLow = cy + rad/2 myPenYHi = cy - rad/2 myPenRadius = rad -- where my personal pup will spawn myPenPupX = cx - rad/2 myPenPupY = myPenYHi -- where my personal training drone will spawn botTarget.spawnX = cx botTarget.spawnY = myPenYLow -- and our tutor will show up here, outside our Pen botTutor.spawnX = cx + rad * 1.2 if (ix % 2) == 1 then botTutor.spawnX = cx - rad * 1.2 end botTutor.spawnY = cy + rad/2 -- pup spawn slots myPupIndexHeal = 100+ix -- each player gets one, in their pen myPupIndexMiner = 110+ix -- some number of these are randomly seeded -- declare the escaped miner PowerUp (adds new pup to game engine tables) wpnId = 1 -- TODO: make it so you actually launch a projectile (a little astrpnaut, ideally) setPowerUp( PupIdMiner, wpnId, pupMiner ) -- these only take effect when I am moderator, but I need to declare them -- or they cannot be reset later resetPupXY( myPupIndexMiner, PupIdMiner, -1, -1, 0, 20 ) resetPupXY( myPupIndexMiner+1, PupIdMiner, -1, -1, 0, 20 ) resetPupXY( myPupIndexMiner+2, PupIdMiner, -1, -1, 0, 20 ) resetPupXY( myPupIndexMiner+3, PupIdMiner, 100, 100, 0, 20 ) -- inside the fuel depot? -- ditto for the per-pen recharges local i for i=1,8 do resetPupXY( 100+i-1, 11, glPenPupX[i], glPenPupY[i], 1, 20 ) end -- and the flags? resetPup( 249, 3) resetPup( 250, 3) end -- build all 8 training pens (not knowing which is ours) function buildMapGeometry() for i=1,8 do -- for each possible player ix = i - 1 -- I want 0 to 7 here buildTrainingPen( ix ) end end -- now invoke that buildMapGeometry() -- let the log know we're done loading the starmap log(1, '------ NPC Starmap Script Loaded ------' ) -- end of script