------------------------------------------- King of the Hill Weapon Writer's HOWTO v1.2 ------------------------------------------- Greetings! The purpose of of this document is to explain how to go about adding new weapons to KOTH. It is assumed you are a competent C programmer and are somewhat familiar with the GGI API, but are not otherwise terribly familiar with the KOTH source code. This document is primarily composed of several examples of how several weapons were written with explanation and justification, in a very "literate programming" style. As of this writing KOTH is at version 0.7.2, and hopefully the weapons system outlined here will be stable for a while. Now, without further ado, on to the show! Example #1: The MIRV --------------------- MIRV: Multiple Independently-Armed Reentry Vehicle This is a pretty simple weapon. Fire one shot, and at the apex of its arc it breaks up into five seperate shells, allowing you strike multiple targets with ease. However, if it strikes ground before arcing, it only explodes with the power of one shell. First, a little introduction to the weapons system. Here is the weapon data structure: typedef struct Weapon_wep { char* name; Shellstat_bal (*initguidance)(struct Projectilepos_bal *prjpos, void** guideshotinfo, Shellstat_bal (*initexplosion)(struct Projectilepos_bal *prjpos, void** explosioninfo), void** explosioninfo); Shellstat_bal(*doguidance)(void* info, struct Projectilepos_bal *prjpos, Shellstat_bal (*initexplosion)(struct Projectilepos_bal *prjpos, void** explosioninfo), void** explosioninfo); void (*drawshot)(struct Projectilepos_bal *prjpos, void* info); /* handles drawing/erasing of the shell */ Shellstat_bal (*initexplosion)(struct Projectilepos_bal *prjpos, void** explosioninfo); Shellstat_bal (*doexplosion)(void* info); void (*drawexplosion)(void* info); /* handles drawing/erasing of the explosion */ struct Weapon_wep *next; struct Weapon_wep *prev; int cost; int count; int id; } Weapon_wep; So, the basic properties of a weapon are: name Its name initguidance The initializer function; this is called when the weapon is shot doguidance As the weapon is in flight, this function is called to update its position and other information drawshot Draws the shell in flight initexplosion The initializer function; called when the weapons starts to explode doexplosion As the explosion occurs, this updates its effects drawexplosion Draws the explosive effect as it occurs next, prev For managing the linked list of all weapons. You probably won't need to mess with this. cost What it costs players to buy one of this weapon (if 0, they cannot buy it at all) count The number of this weapons the player must buy, so the actual market price is cost x count (it is debatable whether this is a good or bad thing, eventually it will probably be that people will be forced to buy weapons in packs, but they will be able to sell back individual weapons and recover the cost of the weaponry they didn't want) id Each weapon has a identifier number, this number is used by the wepLookupWeaponByID() function This is the static weapon structure. In addition, when a weapon is actually used it enters the ballistics code and the following two structures becomes important: typedef struct Projectilepos_bal { int id; /* player who fired this shot */ int wid; /* weapon which fired this shot */ double rox, roy; /* real ox and real oy (not modified by balEnvironmentAdjustProjPos when the wall type is wraparound) */ double ox, oy; double x, y; double vx, vy; } Projectilepos_bal; struct Projectilelist_bal { Shellstat_bal stat; int gen; Projectilepos_bal prjpos; void* guidanceinfo; void* explosioninfo; struct Weapon_wep *wpn; struct Projectilelist_bal *next; struct Projectilelist_bal *prev; }; I'll talk about these structures in more detail a little later. Now, let's go through the life (in code) of a MIRV. First the player buys it. The cost, as specified in the weapon structure, is deducted from their money and the appropriate number (the count field) is added to his or her stock of weapons. This is already handled, you don't have to worry about any of that. Then the game starts. The player goes decides to try out this spiffy new MIRV they have, and fires it. The following will happen: 1) The weapon is activated and begins its flight (initguidance) 2) It flys through the air a bit. doguidance is periodically called to update its position 3) It reaches its apex. We can tell by when the vertical velocity (stored in the projectilepos structure) flips from positive to negative. 4) Create five shells starting at the same position with slightly different horizontal velocities as the MIRV, and have the original shell explode. The "multiple" effect has now occured :) 5) Each new shell continues to travel through the air on its downward course and will explode when it finally hits something. Pretty straightforward. So, to implement this weapon it is up to us to provide the code to initialize, to guide, to initialize the explosion, and to do the explosion. Each is pointer to a function in the weapon structure, so we can easily mix and match functions to produce new weapons and re-use other code. We can re-use enough code, in fact, that all we'll have to write for the MIRV is a new guidance function! Time to get down and dirty with code. Weapons are added with the following function: void wepAddWeapon(char *name, int cost, int count, Shellstat_bal(*initguidance) (struct Projectilepos_bal * prjpos, void **guideshotinfo, Shellstat_bal (*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo), Shellstat_bal(*doguidance) (void *info, struct Projectilepos_bal * prjpos, Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo), Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), Shellstat_bal(*doexplosion) (void *info)); Don't worry, it's a lot simpler than it looks. Here's the actual function call we'll use to actually register our new MIRV: wepAddWeapon("MIRV", 300, 1, wepBasicInit, wepMIRVGuidance, wepBasicExplosionInit, wepSimpleExplosion); This is the shell that MIRV release (It's like a Basic Shell): wepAddWeapon("MIRV Shell", 0, 1, wepBasicInit, wepBasicGuidance, wepBasicExplosionInit, wepSimpleExplosion); So the function we need to implement is wepMIRVGuidance. Let's take a look at wepBasicGuidance in basic.c: Shellstat_bal wepBasicGuidance(void* info, struct Projectilepos_bal *prjpos, Shellstat_bal (*initexplosion)(struct Projectilepos_bal *prjpos, void** explosioninfo), void **explosioninfo) { Shellstat_bal res; Player_pl *plhit = NULL; int ix, iy; /* (1) */ prjpos->rox = prjpos->ox = prjpos->x; prjpos->roy = prjpos->oy = prjpos->y; /* (2) */ prjpos->x += prjpos->vx; prjpos->y += prjpos->vy; /* (3) */ prjpos->vy -= bal_grav/bal_lerp_tweak; /* (4) */ if((res = balEnvironmentAdjustProjPos(prjpos)) != FLYING /* (5) */ || balCheckIntersect(prjpos->ox, prjpos->oy, prjpos->x, prjpos->y, &plhit, &ix, &iy)) { switch(res) { case HOLDING: break; case FLYING: prjpos->x=ix; prjpos->y=iy; /* fall through */ case EXPLODING: return initexplosion(prjpos, explosioninfo); break; case FREEING: return FREEING; break; } } /* (6) */ return FLYING; } Here's what's going on: 1) Save the old projectile position values You can see that we have two old positions vars (ro[xy] and o[xy]); ox and oy is used for calculations, these vars has sometimes an false old position value when the wall type is 'wraparound'. So, drawing functions uses rox and roy for erasing the projectiles, because these vars always has the true old position value. 2) Update the projectile position based on current horizontal/vertical velocity 3) Update the vertical velocity to account for acceleration due to gravity 4) Apply environmental effects (wind, walls) 5) Check to see if the shot has hit anything (either dirt, a wall, or a tank) if so then adjust position to the exact point it hit and initialize explosion 6) otherwise, just keep flying through the air So, now we want to write modify wepBasicGuidance to get wepMIRVGuidance. As you recall, we want to check and see if we have reached the apex of our arc and split up when we have. We create the following new function in mirv.c: Shellstat_bal wepMIRVGuidance(void *info, struct Projectilepos_bal *prjpos, Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo) { Weapon_wep *bs; struct Projectilelist_bal *prj; Shellstat_bal res; int ix, iy; Player_pl *plhit = NULL; prjpos->rox = prjpos->ox = prjpos->x; prjpos->roy = prjpos->oy = prjpos->y; prjpos->x += prjpos->vx; prjpos->y += prjpos->vy; prjpos->vy -= bal_grav / bal_lerp_tweak; if((res = balEnvironmentAdjustProjPos(prjpos)) != FLYING || balCheckIntersect(prjpos->ox, prjpos->oy, prjpos->x, prjpos->y, &plhit, &ix, &iy)) { if(plLookupPlayer(prjpos->id)) { logPrintf(DEBUG, "Intersection at (%i, %i) of %s's shot\n", ix, iy, plLookupPlayer(prjpos->id)->name); } else { logPrintf(DEBUG, "Intersection at (%i, %i) of null's shot\n", ix, iy); } if(plhit != NULL) logPrintf(DEBUG, "and hit %s\n", plhit->name); switch (res) { case HOLDING: /* wtf ??? */ break; case FLYING: prjpos->x = ix; prjpos->y = iy; /* fall through */ case EXPLODING: return initexplosion(prjpos, explosioninfo); break; case FREEING: return FREEING; break; } } if(prjpos->vy > 0 && prjpos->vy - (bal_grav / bal_lerp_tweak) < 0) { bs = wepLookupWeapon("MIRV Shell"); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx, prjpos->vy, bs); prj->stat = INITSHOT(prj); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx + 5, prjpos->vy, bs); prj->stat = INITSHOT(prj); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx + 10, prjpos->vy, bs); prj->stat = INITSHOT(prj); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx - 5, prjpos->vy, bs); prj->stat = INITSHOT(prj); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx - 10, prjpos->vy, bs); prj->stat = INITSHOT(prj); return initexplosion(prjpos, explosioninfo); } return FLYING; } It's pretty straightforward. We check to see if we've reached the apex, then create the new shots. You're probably wondering about all that "prj->stat" stuff, though. Stat is the projectile's status - either holding, flying, exploding, or freeing. Because projectiles are initialized holding (this is because of the asynchrosity of the network protocol, they have to be activated seperately) we hold onto their status until actual activation. However, in this case we want to activate them immediatly, so we use the macro INITSHOT() which calls that shot's initguidance function and returns the new status. That's all the code we have to write to implement the MIRV! However, there is a little bit of housekeeping to do to add the weapon to the right lists so it can actually be used in the game. We need to put wepAddWeapon in the right place in weapon.c: void wepInit() { /* ... other weapon definitions ... */ wepAddWeapon("MIRV", 300, 1, wepBasicInit, wepMIRVGuidance, wepBasicExplosionInit, wepSimpleExplosion); wepAddWeapon("MIRV Shell", 0, 1, wepBasicInit, wepBasicGuidance, wepBasicExplosionInit, wepSimpleExplosion); } MIRV Shell is like a Basic Shell, so we don't need write extra-code to support it. Secondly, we need to set the weapon-drawing functions for the MIRV and MIRV Shell in weapongfx.c. We'll be using functions already included for our drawing; creating new weapon graphics will be covered in a later example. void wgxInit() { /* ... other weapon graphics definitions ... */ wgxSetWeaponDrawFunc("MIRV", wgxDrawSimpleShot, wgxDrawSimpleRedExplosion); wgxSetWeaponDrawFunc("MIRV Shell", wgxDrawSimpleShot, wgxDrawSimpleOrangeExplosion); } That's it! Now we have our MIRV weapon! Example #2: The dirt ball -------------------------- Unlike our previous example where we used pre-packaged explosions, for this one we'll use the standard guidance functions and write our own doexplosion and drawexplosion routines. The weapon will be a nice big clod of dirt you can use to bury your enemies. Whether to buy you some time to recover from getting pounded, force them to take damage blasting their way out, or to just piss them off, it's guaranteed to be a useful part of your strategic arsenal. Here's the function call we'll use to add the weapon in: wepAddWeapon("Dirt Ball", 300, 1, wepBasicInit, wepBasicGuidance, wepDirtExplosionInit, wepDirtExplosion); wepBasicInit and wepBasicGuidance provide basic ballistics for us, we're interested in what happens when it hits the ground. First a bit about how terrain is represented. On first thought, the obvious ways to represent terrain is with either a hightmap or a bitmap. There are problems with each, however. First, heightmaps cannot have terrain suspended in air. They're really easy to code for of course, but their limitations are quickly encountered. In this case, then, representing the terrain as a bitmap would seem like an obvious solution. However, KOTH usually represents terrain internally at a much higher resolution than the screen. The terrain map is by default 2000x1500, meaning 2000*1500=3000000 possible dirt points. If each point is an int, that's nearly twelve megabytes! If each point is a char we bring it down to about three megabytes and if we do some fancy bit packing we can get it down to about 366k. However, once the bit-packing sets in, the code to constantly pack and unpack the terrain structure gets to be very tricky, and things such as falling dirt turn out to be _always_ difficult and inefficient with some sort of terrain bitmap. So, KOTH uses something called "spans". Basically they are an extension of hightmaps into the second dimension. Each vertical column is a linked list of areas of dirt "spans" (contiguous dirt sections) where there is dirt; each node of the list stores a y coordinate and the height. This turns out to make transmitting the terrain structure fairly easy and calculating falling dirt REALLY easy. There are a few fairly tricky algorithms working behind the scenes to manipulate the span data structure, fortunatly they've already been written and now abstracted behind an easy to use API: we can check arbitrary positions with terCheckPos(), clear out spans with terDelSpan() and add new spans with terAddSpan(). The last one is the one we're interested in. Our dirt clod will be circular and be about same size (actually a bit bigger) as a tac nuke - 225 terrain units (TU's). What we want is a function that will create a filled circle of terrain of some arbitrary radius, so we can get that "expanding" effect that explosion have. Fortunatly, there is a function terClearCircle() that does just this, only in reverse: it is used by explosion functions to vaporize a disc of dirt. We just need to copy it and modify it to make our own version that fills in dirt instead of takes it away. void terCircleClearSpans(int xo, int yo, int x, int y) { if(xo+x>=0 && xo+x=0 && xo+y=0 && xo-x=0 && xo-yx) { if(d<0) { d+=deltaE; deltaE+=2; deltaSE+=2; } else { d+=deltaSE; deltaE+=2; deltaSE+=4; y--; } x++; terCircleClearSpans(xo, yo, x, y); } } And here's the new function. terAddSpan() and terDelSpan() handle all the messy special cases of possible overlap, so we can just use them freely to modify the terrain. void terCircleAddSpans(int xo, int yo, int x, int y) { if(xo+x>=0 && xo+x=0 && xo+y=0 && xo-x=0 && xo-yx) { if(d<0) { d+=deltaE; deltaE+=2; deltaSE+=2; } else { d+=deltaSE; deltaE+=2; deltaSE+=4; y--; } x++; terCircleAddSpans(xo, yo, x, y); } } So now we can add our dirt clod to the terrain! Now we need to write our initialization function for the Dirt Clod. Since it looks a lot like a tac nuke in reverse, take a look at the tac nuke explosion init: Shellstat_bal wepTacNukeExplosionInit(struct Projectilepos_bal *prjpos, void **explosioninfo) { struct SimpleExplosion_wep *e = (struct SimpleExplosion_wep *) malloc(sizeof(struct SimpleExplosion_wep)); e->x = prjpos->x; e->y = prjpos->y; e->r = 0; e->dx = 9; e->id = prjpos->id; e->max_radius = 250; e->wid = prjpos->wid; *((struct SimpleExplosion_wep **) explosioninfo) = e; return EXPLODING; } Now with a bit of slight of hand, it becomes a dirt ball init: Shellstat_bal wepDirtExplosionInit(struct Projectilepos_bal *prjpos, void **explosioninfo) { struct SimpleExplosion_wep *e = (struct SimpleExplosion_wep *) malloc(sizeof(struct SimpleExplosion_wep)); e->x = prjpos->x; e->y = prjpos->y; e->r = 0; e->dx = 8; e->id = prjpos->id; e->max_radius = 225; e->wid = prjpos->wid; *((struct SimpleExplosion_wep **) explosioninfo) = e; return EXPLODING; } Some explanation is in order. You may have noticed a couple void pointers called guidanceinfo and explosioninfo hanging about in the Projectilelist_bal structure. These contain "weapon specific" data. This could be extra state info for weapons with really weird guidance, and in the case of explosion all information is stored in this structure. SimpleExplosion_wep describes the position, maximum radius, current radius, source player and expansion rate (dx, technically should be dr, so sue me) for a simple circular explosion. We can use this for dirt explosions, as well. Now for doing the actual explosion itself. Let's take the standard simple explosion function: Shellstat_bal wepSimpleExplosion(void *info) { struct SimpleExplosion_wep *prj = (struct SimpleExplosion_wep *) info; Player_pl *pcur; prj->r += prj->dx; if(prj->r > prj->max_radius) { return FREEING; } else /* amazing how much the word "else" looks like the face of Yoda at 4:16 in the morning */ { terClearCircle(prj->x, prj->y, prj->r); for(pcur = pl_begin; pcur; pcur = pcur->next) { if(pcur->ready == READY && plPlayerInCircleArea(pcur, prj->x, prj->y, prj->r)) { pcur->last_hit = prj->id; plDamageTank(pcur, EXPLOSIVE, prj->id, prj->wid, 1); } } return EXPLODING; } } See the call to terClearCircle()? Now, change it to use our new function, and take out the player-damage code, and we get this: Shellstat_bal wepDirtExplosion(void *info) { struct SimpleExplosion_wep *prj = (struct SimpleExplosion_wep *) info; prj->r += prj->dx; if(prj->r > prj->max_radius) { return FREEING; } else /* amazing how much the word "else" looks like the face of Yoda at 4:16 in the morning */ { terAddCircle(prj->x, prj->y, prj->r); return EXPLODING; } } Easy! Just one more step left. We need to animate this expanding dirtball. It's easier than it sounds. Here's the drawing function for SimpleExplosion: void wgxDrawSimpleExplosion(void *info) { int i; ggi_color c; int tx = ((struct SimpleExplosion_wep *) info)->x; int ty = ((struct SimpleExplosion_wep *) info)->y; int tr = ((struct SimpleExplosion_wep *) info)->r; int sx = gfxTerrainToScreenXCoord(tx); int sy = gfxTerrainToScreenYCoord(ty); int sa = (tr * gfx_xsize) / ter_sizex; int sb = (tr * gfx_ysize) / ter_sizey; if(sa >= sb) { for(i = sa; i > 0; i--) { c.r = ((sa - i) * (255.0 / sa)) * 255.0; c.g = 0x24 << 8; c.b = 0x24 << 8; c.a = 0xFF << 8; ggiSetGCForeground(gfx_vis, ggiMapColor(gfx_vis, &c)); gfxDrawThickEllipse(sx, sy, i, (i * sb) / sa); } } else { for(i = sb; i > 0; i--) { c.r = ((sb - i) * (255.0 / sb)) * 255.0; c.g = 0x24 << 8; c.b = 0x24 << 8; c.a = 0xFF << 8; ggiSetGCForeground(gfx_vis, ggiMapColor(gfx_vis, &c)); gfxDrawThickEllipse(sx, sy, (i * sa) / sb, sb); } } if(tr + ((struct SimpleExplosion_wep *) info)->dx >= ((struct SimpleExplosion_wep *) info)->max_radius) { gfxDrawArea(tx - tr - 2 * ((struct SimpleExplosion_wep *) info)->dx, ty - tr - 2 * ((struct SimpleExplosion_wep *) info)->dx, tr * 2 + 4 * ((struct SimpleExplosion_wep *) info)->dx, tr * 2 + 4 * ((struct SimpleExplosion_wep *) info)->dx); } } Now, here's what we need to do to make it draw our dirt: void wgxDrawDirtExplosion(void *info) { int tx = ((struct SimpleExplosion_wep *) info)->x; int ty = ((struct SimpleExplosion_wep *) info)->y; int tr = ((struct SimpleExplosion_wep *) info)->r; gfxDrawArea(tx - tr - 2 * ((struct SimpleExplosion_wep *) info)->dx, ty - tr - 2 * ((struct SimpleExplosion_wep *) info)->dx, tr * 2 + 4 * ((struct SimpleExplosion_wep *) info)->dx, tr * 2 + 4 * ((struct SimpleExplosion_wep *) info)->dx); } Yep! That's it. The gfxDrawArea function takes care of all the sky, terrain and tank drawing for us. So our dirt clod is DONE! All we have to do is add it in like before: void wepInit() { /* ... other weapon definitions ... */ wepAddWeapon("Dirt Ball", 300, 1, wepBasicInit, wepBasicGuidance, wepDirtExplosionInit, wepDirtExplosion); } void wgxInit() { /* ... other weapon graphics definitions ... */ wgxSetWeaponDrawFunc("Dirt Ball", wgxDrawSimpleShot, wgxDrawDirtExplosion); } Example #3: The Roller ----------------------- Ahhh, the roller. The lazy man's weapon, that is, as long as your opponent is in a vally :) When the roller hits ground it does not explode, but instead rolls along until it hits a tank or an incline too high to scale. Then, and only then, does it unleash its might. The roller is an example of a completly twisted guidance system. Rolling along the ground requires completly different logic than flying through the air. The roller will consist of two parts. One part flys through the air, the other actually does the rolling. Here's how we'll add them: wepAddWeapon("Roller", 550, 1, wepBasicInit, wepBasicGuidance, wepRollerExplosionInit, NULL); wepAddWeapon("Roller Ball", 0, 1, wepRollerInit, wepRollerGuidance, wepTacNukeExplosionInit, wepSimpleExplosion); The basic plan of attack is this: the player shoots a roller and it flys through the air like a normal shell (wepBasicInit/wepBasicGuidance). When it hits the ground, wepRollerExplosionInit is called, the original shell will fizzle (which is why it has a NULL doexplosion function) and create the new shell, with our special guidance function. When it hits something it doesn't like, it explodes with the force of a tac nuke. So, the first thing to do is to write wepRollerExplosionInit(). We've seen what a normal explosion initializer looks like (wepTacNukeExplosionInit) and we've seen how to create new shells (wepMIRVGuidance) so let's put them together: Shellstat_bal wepRollerExplosionInit(struct Projectilepos_bal * prjpos, void **explosioninfo) { struct Projectilelist_bal *prj; Weapon_wep *wp; wp = wepLookupWeapon("Roller Ball"); prj = balNewShotXY(prjpos->id, 0, prjpos->x, prjpos->y, prjpos->vx, prjpos->vy, wp); prj->stat = INITSHOT(prj); return FREEING; } Now we get to the interesting functions: actually rolling over the terrain. We've already looked at the normal guidance function, but it won't help us much here. We'll decide our exact plan of attack and then implement it. So, how exactly should rolling over terrain work? How about this: 1) the direction the roller rolls is determined by the sign of prjpos->vx. positive for right, negative for left 2) When the roller first hits the ground, check the immediate surroundings. If it slopes left, the roller goes left. If it slopes right, the roller goes right. If there isn't any slope, then the roller continues in the direction it was traveling in the air. 3) With each guidance call, advance the roller some distance. How to determine that distance is harder that it sounds. Do we want to get the physics of rolling downhill roughly correct, or just do it the easy way? Well, I think we'll do it the easy way. Each frame we'll advance the roller a total of 20 TU's either horizontally and vertically. If the roller is not flush with the ground, it falls. If it is, it rolls forward. If it runs into low enough incline it climbs. 4) the roller will climb up to 1 TU. If it encounters anything higher, it explodes. If it crashes into a tank, it also explodes. Now for the implementation let's take a look at the wepBasicInit(): Shellstat_bal wepBasicInit(struct Projectilepos_bal *prjpos, void **guide, Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo) { Player_pl *plhit = NULL; int ix, iy; *guide = NULL; if(balCheckIntersect (prjpos->ox, prjpos->oy, prjpos->x, prjpos->y, &plhit, &ix, &iy)) { prjpos->x = ix; prjpos->y = iy; aihExplosionHook(prjpos); aihExplosionHook(prjpos); return initexplosion(prjpos, explosioninfo); } else { prjpos->rox = prjpos->ox = prjpos->x; prjpos->roy = prjpos->oy = prjpos->y; return FLYING; } } You'll notice that it looks an awful lot like wepBasicGuidance(). prjpos is the projectile position structure used in normal guidance, so we set prjpos's id to the appropriate source id, then we move the shell position one quanta and see if it hits anything. If it does, go straight to explosion, if it doesn't, then keep flying. The roller, however, needs to do different stuff: Shellstat_bal wepRollerInit(struct Projectilepos_bal *prjpos, void **guide, Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo) { Player_pl *plhit = NULL; int ix, iy; *guide = NULL; if(balCheckTankIntersect(prjpos->x, prjpos->y, &plhit, &ix, &iy)) { prjpos->x = ix; prjpos->y = iy; aihExplosionHook(prjpos); return initexplosion(prjpos, explosioninfo); } else { if(terCheckPos(ter_data, prjpos->x + 1, prjpos->y + 1) && !terCheckPos(ter_data, prjpos->x - 1, prjpos->y + 1)) { prjpos->vx = -1; } if(terCheckPos(ter_data, prjpos->x - 1, prjpos->y + 1) && !terCheckPos(ter_data, prjpos->x + 1, prjpos->y + 1)) { prjpos->vx = 1; } prjpos->rox = prjpos->ox = prjpos->x; prjpos->roy = prjpos->oy = prjpos->y; return FLYING; } } Check to see if we've hit a tank, if not, then try to figure out which direction to roll. Now, on to the rolling function! Shellstat_bal wepRollerGuidance(void *info, struct Projectilepos_bal * prjpos, Shellstat_bal(*initexplosion) (struct Projectilepos_bal * prjpos, void **explosioninfo), void **explosioninfo) { int i, ix, iy; Player_pl *plhit = NULL; /* (1) */ prjpos->rox = prjpos->ox = prjpos->x; prjpos->roy = prjpos->oy = prjpos->y; for(i = 0; i < 20; i++) { /* (2) */ if(balCheckTankIntersect(prjpos->x, prjpos->y + 5, &plhit, &ix, &iy)) { prjpos->x = ix; prjpos->y = iy; aihExplosionHook(prjpos); return initexplosion(prjpos, explosioninfo); } /* (3) */ if(prjpos->x <= 0 || prjpos->x >= ter_sizex) { prjpos->x = -1; prjpos->y = -1; return FREEING; } /* (4) */ if(!terCheckPos(ter_data, prjpos->x, prjpos->y - 1) && prjpos->vy == 0 && prjpos->y > 0) { prjpos->y--; continue; } /* (5) */ if(prjpos->vx > 0) { /* (6) */ if(!terCheckPos(ter_data, prjpos->x + 1, prjpos->y)) { prjpos->x++; prjpos->vy = 0; continue; } else { if(!terCheckSpan (&ter_data[(int) rint(prjpos->x) + 1], prjpos->y, 2)) { prjpos->y++; prjpos->vy = 1; continue; } else { aihExplosionHook(prjpos); return initexplosion(prjpos, explosioninfo); } } } /* (7) */ else { if(!terCheckPos(ter_data, prjpos->x - 1, prjpos->y)) { prjpos->x--; prjpos->vy = 0; continue; } else { if(!terCheckSpan (&ter_data[(int) rint(prjpos->x) - 1], prjpos->y, 2)) { prjpos->y++; prjpos->vy = 1; continue; } else { aihExplosionHook(prjpos); return initexplosion(prjpos, explosioninfo); } } } } return FLYING; } This function, on first glance, looks rather long and tricky. In fact it is't all that bad (although I admit it took a bit of tweaking to finally get right.) Here's a point-by-point description of what it does: 1) Save old position (for animation purposes) rox and roy is used for drawings ox and oy is used for calculations 2) check to see if it has hit anything. if so, explode 3) check to see if it has gone off the edge; free if so 4) check to see if it is on solid ground or climbing. if not, fall 5) decide which direction the roller is rolling 6) if the ground is clear, keep rolling. climb if necessary, and if we can't move at all, explode 7) same thing in the other direction. And that's that. Once we've added Roller and Roller Ball to the wepInit() and wgxInit() function, we have our roller weapon! Conclusion ---------- Well, that's about it for the second draft of the weapon-writer's guide. I haven't covered everything, but this should give you a good idea of where to go from here. If you still have questions, ask on the mailing list (see http://savannah.nongnu.org/mail/?group=koth) and I or someone else will be able to help you. If you have any suggestions for the Weaponry-HOWTO, please post it to the list or email me, constructive criticism is quite welcome. Have fun! Allan Douglas allan_douglas@gmx.net Mon Feb 3 21:22:55 BRST 2003