Lephisto/src/spfx.c
2014-07-23 20:16:02 +01:00

584 lines
16 KiB
C

/**
* @file spfx.c
*
* @brief Handle the specific effects.
*/
#include <inttypes.h>
#include <SDL/SDL.h>
#if SDL_VERSION_ATLEAST(1,3,0)
#include "SDL_haptic.h"
#endif
#include "lephisto.h"
#include "log.h"
#include "pilot.h"
#include "physics.h"
#include "opengl.h"
#include "pause.h"
#include "rng.h"
#include "ldata.h"
#include "lxml.h"
#include "spfx.h"
#define SPFX_XML_ID "spfxs" /**< XML Document tag. */
#define SPFX_XML_TAG "spfx" /**< SPFX XML node tag. */
#define SPFX_DATA "../dat/spfx.xml" /**< Location of the spfx datafile. */
#define SPFX_GFX_PRE "../gfx/spfx/" /**< Location of the graphic. */
#define SPFX_GFX_SUF ".png" /**< Suffix of graphics. */
#define CHUNK_SIZE 32 /**< Chunk size to allocate spfx bases. */
#define SPFX_CHUNK 32 /**< Chunk to allocate when needed. */
#define SHAKE_VEL_MOD 0.0008 /**< Shake modifier. */
#define HAPTIC_UPDATE_INTERVAL 0.1 /**< Time between haptic updates. */
/* Special hardcoded effects.. */
/* Shake, AKA RUMBLE! */
static double shake_rad = 0.; /**< Current shake radius (0 = no shake). */
Vec2 shake_pos = { .x = 0., .y = 0. }; /**< Current shake pos. Used in nebulae.c */
static Vec2 shake_vel = { .x = 0., .y = 0. }; /**< Current shake vel. */
static int shake_off = 1; /**< 1 if shake is not active. */
#if SDL_VERSION_ATLEAST(1,3,0)
extern SDL_Haptic* haptic; /**< From joystick.c */
extern unsigned int haptic_query; /**< From joystick.c */
static int haptic_rumble = -1; /**< Haptic rumble effect ID. */
static SDL_HapticEffect haptic_rumbleEffect; /**< Haptic rumble effect. */
static double haptic_lastUpdate = 0.; /**< Timer to update haptic effect again. */
#endif
/**
* @struct SPFX_Base
*
* @brief Generic special effect.
*/
typedef struct SPFX_Base_ {
char* name; /**< Name of the special effect. */
double ttl; /**< Time to live. */
double anim; /**< Total duration in ms. */
glTexture* gfx; /**< Will use each sprite as a frame. */
} SPFX_Base;
static SPFX_Base* spfx_effects = NULL; /**< Total special effects. */
static int spfx_neffects = 0; /**< Total number of special effects. */
/**
* @struct SPFX
*
* @brief An actual in-game active special effect.
*/
typedef struct SPFX_ {
Vec2 pos; /**< Current position. */
Vec2 vel; /**< Current velocity. */
int lastframe; /**< Need when pausing. */
int effect; /**< Actual effect. */
double timer; /**M Time left. */
} SPFX;
/* Front stack is for effects on player. */
/* Back is for everything else. */
static SPFX* spfx_stack_front = NULL; /**< Frontal special effect layer. */
static int spfx_nstack_front = 0; /**< Number of special effects in front. */
static int spfx_mstack_front = 0; /**< Memory allocated for frontal special effects. */
static SPFX* spfx_stack_back = NULL; /**< Back special effect layer. */
static int spfx_nstack_back = 0; /**< Number of special effects in the back. */
static int spfx_mstack_back = 0; /**< Memory allocated for special effects in back. */
/* General. */
static int spfx_base_parse(SPFX_Base* tmp, const xmlNodePtr parent);
static void spfx_base_free(SPFX_Base* effect);
static void spfx_destroy(SPFX* layer, int* nlayer, int spfx);
static void spfx_update_layer(SPFX* layer, int* nlayer, const double dt);
/* Haptic. */
static int spfx_hapticInit(void);
static void spfx_hapticRumble(double mod);
/**
* @brief Parse an xml node containing an spfx.
* @param tmp Address to load sfx into.
* @param parent XML node containing the spfx data.
* @return 0 on success.
*/
static int spfx_base_parse(SPFX_Base* tmp, const xmlNodePtr parent) {
xmlNodePtr node;
/* Clear data. */
memset(tmp, 0, sizeof(SPFX_Base));
/* Get the name (mallocs). */
tmp->name = xml_nodeProp(parent, "name");
/* Extract the data. */
node = parent->xmlChildrenNode;
do {
xmlr_float(node, "anim", tmp->anim);
xmlr_float(node, "ttl", tmp->ttl);
if(xml_isNode(node, "gfx"))
tmp->gfx = xml_parseTexture(node,
SPFX_GFX_PRE"%s"SPFX_GFX_SUF, 6, 5, 0);
} while(xml_nextNode(node));
/* Convert from ms to s. */
tmp->anim /= 1000.;
tmp->ttl /= 1000.;
if(tmp->ttl == 0.)
tmp->ttl = tmp->anim;
#define MELEMENT(o,s) \
if(o) WARN("SPFX '%s' missing/invalid '"s"' element", tmp->name) /**< Define to help check for data errors. */
MELEMENT(tmp->anim == 0., "anim");
MELEMENT(tmp->ttl == 0., "ttl");
MELEMENT(tmp->gfx == NULL, "gfx");
#undef ELEMENT
return 0;
}
/**
* @brief Free an SPFX_Base.
* @param effect SPFX_Base to free.
*/
static void spfx_base_free(SPFX_Base* effect) {
if(effect->name != NULL) {
free(effect->name);
effect->name = NULL;
}
if(effect->gfx != NULL) {
gl_freeTexture(effect->gfx);
effect->gfx = NULL;
}
}
/**
* @brief Get the id of an spfx based on name.
* @param name Name to match.
* @return ID of the special effect or -1 on error.
*/
int spfx_get(char* name) {
int i;
for(i = 0; i < spfx_neffects; i++)
if(strcmp(spfx_effects[i].name, name)==0)
return i;
WARN("SPFX '%s' not found!", name);
return -1;
}
/**
* @brief Load the spfx stack.
* @return 0 on success.
*
* @todo Make spfx not hardcoded.
*/
int spfx_load(void) {
int mem;
uint32_t bufsize;
char* buf;
xmlNodePtr node;
xmlDocPtr doc;
/* Load and read the data. */
buf = ldata_read(SPFX_DATA, &bufsize);
doc = xmlParseMemory(buf, bufsize);
/* Check to see if document exists. */
node = doc->xmlChildrenNode;
if(!xml_isNode(node, SPFX_XML_ID)) {
ERR("Malformed '"SPFX_DATA"' file: missing root element '"SPFX_XML_ID"'");
return -1;
}
/* Check to see if it's populated. */
node = node->xmlChildrenNode; /* First system node. */
if(node == NULL) {
ERR("Malformed '"SPFX_DATA"' file: does not contain elements");
return -1;
}
/* First pass, loads up ammunition. */
mem = 0;
do {
if(xml_isNode(node, SPFX_XML_TAG)) {
spfx_neffects++;
if(spfx_neffects > mem) {
mem += CHUNK_SIZE;
spfx_effects = realloc(spfx_effects, sizeof(SPFX_Base)*mem);
}
spfx_base_parse(&spfx_effects[spfx_neffects-1], node);
}
} while(xml_nextNode(node));
/* Shrink back to minimum - shouldn't change ever. */
spfx_effects = realloc(spfx_effects, sizeof(SPFX_Base) * spfx_neffects);
/* Clean up. */
xmlFreeDoc(doc);
free(buf);
/* Now initialize force feedback. */
spfx_hapticInit();
return 0;
}
/**
* @brief Free the spfx stack.
*/
void spfx_free(void) {
int i;
/* Get rid of all the particles and free the stacks. */
spfx_clear();
if(spfx_stack_front) free(spfx_stack_front);
spfx_stack_front = NULL;
spfx_mstack_front = 0;
if(spfx_stack_back) free(spfx_stack_back);
spfx_stack_back = NULL;
spfx_mstack_back = 0;
for(i = 0; i < spfx_neffects; i++)
spfx_base_free(&spfx_effects[i]);
free(spfx_effects);
spfx_effects = NULL;
spfx_neffects = 0;
}
/**
* @brief Create a new special effect.
* @param effect Base effect identifier to use.
* @param px X position of the effect.
* @param py Y position of the effect.
* @param vx X velocity of the effect.
* @param vy Y velocity of the effect.
* @param layer Layer to put the effect on.
*/
void spfx_add(int effect,
const double px, const double py,
const double vx, const double vy,
const int layer) {
SPFX* cur_spfx;
double ttl, anim;
if((effect < 0) || (effect > spfx_neffects)) {
WARN("Trying to add spfx with invalid effect!");
return;
}
/* Select the layer. */
if(layer == SPFX_LAYER_FRONT) {
/* Front layer. */
if(spfx_mstack_front < spfx_nstack_front+1) {
/* We need more memory. */
spfx_mstack_front += SPFX_CHUNK;
spfx_stack_front = realloc(spfx_stack_front, spfx_mstack_front*sizeof(SPFX));
}
cur_spfx = &spfx_stack_front[spfx_nstack_front];
spfx_nstack_front++;
}
else if(layer == SPFX_LAYER_BACK) {
/* Back layer. */
if(spfx_mstack_back < spfx_nstack_back+1) {
/* Need more memory. */
spfx_mstack_back += SPFX_CHUNK;
spfx_stack_back = realloc(spfx_stack_back, spfx_mstack_back*sizeof(SPFX));
}
cur_spfx = &spfx_stack_back[spfx_nstack_back];
spfx_nstack_back++;
}
/* The actualy adding of the spfx. */
cur_spfx->effect = effect;
vect_csetmin(&cur_spfx->pos, px, py);
vect_csetmin(&cur_spfx->vel, vx, vy);
/* timer magic if ttl != anim. */
ttl = spfx_effects[effect].ttl;
anim = spfx_effects[effect].anim;
if(ttl != anim)
cur_spfx->timer = ttl + RNGF()*anim;
else
cur_spfx->timer = ttl;
}
/**
* @brief Clear all the currently running effects.
*/
void spfx_clear(void) {
int i;
/* Clear front layer. */
for(i = spfx_nstack_front-1; i >= 0; i--)
spfx_destroy(spfx_stack_front, &spfx_nstack_front, i);
/* Clear back layer. */
for(i = spfx_nstack_back-1; i >= 0; i--)
spfx_destroy(spfx_stack_back, &spfx_nstack_back, i);
/* Clear rumble. */
shake_rad = 0.;
shake_pos.x = shake_pos.y = 0.;
shake_vel.x = shake_vel.y = 0.;
}
/**
* @brief Destroys an active spfx.
* @param layer Layer the spfx is on.
* @param nlayer Pointer to the number of elements in the layer.
* @param spfx Position of the spfx in the stack.
*/
static void spfx_destroy(SPFX* layer, int* nlayer, int spfx) {
(*nlayer)--;
memmove(&layer[spfx], &layer[spfx+1], (*nlayer-spfx)*sizeof(SPFX));
}
/**
* @brief Update all the spfx.
* @param dt Current delta tick.
*/
void spfx_update(const double dt) {
spfx_update_layer(spfx_stack_front, &spfx_nstack_front, dt);
spfx_update_layer(spfx_stack_back, &spfx_nstack_back, dt);
}
/**
* @brief Update an individual spfx.
* @param layer Layer the spfx is on.
* @param nlayer Pointer to the assosiated nlayer.
* @param dt Current delta tick.
*/
static void spfx_update_layer(SPFX* layer, int* nlayer, const double dt) {
int i;
for(i = 0; i < *nlayer; i++) {
layer[i].timer -= dt; /* Less time to live. */
/* Time to die!!! */
if(layer[i].timer < 0.) {
spfx_destroy(layer, nlayer, i);
i--;
continue;
}
/* Mkay. Update it now. */
vect_cadd(&layer[i].pos, dt*VX(layer[i].vel), dt*VY(layer[i].vel));
}
}
/**
* @brief Prepare the rendering for the special effects.
*
* Should be called at the beginning of the rendering loop.
* @param dt Current delta tick.
*/
void spfx_start(const double dt) {
GLdouble bx, by, x, y;
double inc;
if(shake_off == 1) return; /* Save the cycles. */
#if SDL_VERSION_ATLEAST(1,3,0)
/* Decrement the haptic timer. */
if(haptic_lastUpdate > 0.)
haptic_lastUpdate -= dt;
#endif
bx = SCREEN_W / 2;
by = SCREEN_H / 2;
if(!paused) {
inc = dt*100000.;
/* Calculate new position. */
if(shake_rad > 0.01) {
vect_cadd(&shake_pos, shake_vel.x * inc, shake_vel.y * inc);
if(VMOD(shake_pos) > shake_rad) {
/* Change direction. */
vect_pset(&shake_pos, shake_rad, VANGLE(shake_pos));
vect_pset(&shake_vel, SHAKE_VEL_MOD*shake_rad,
-VANGLE(shake_pos) + (RNGF()-0.5) * M_PI);
}
/* The shake decays over time. */
shake_rad -= SHAKE_DECAY * dt;
if(shake_rad < 0.) shake_rad = 0.;
x = shake_pos.x;
y = shake_pos.y;
} else {
shake_rad = 0.;
shake_off = 1;
x = 0.;
y = 0.;
}
}
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-bx+x, bx+x, -by+y, by+y, -1., 1.);
}
/**
* @brief Increases the current rumble level.
*
* Rumble will decay over time.
* @param mod Modifier to increase levely by.
*/
void spfx_shake(double mod) {
/* Add the modifier. */
shake_rad += mod;
if(shake_rad > SHAKE_MAX) shake_rad = SHAKE_MAX;
vect_pset(&shake_vel, SHAKE_VEL_MOD*shake_rad, RNGF() * 2. * M_PI);
/* Rumble if it wasn't rumbling before. */
spfx_hapticRumble(mod);
/* Notify that rumble is active. */
shake_off = 0;
}
/**
* @brief Initializes the rumble effect.
* @return 0 on success.
*/
static int spfx_hapticInit(void) {
#if SDL_VERSION_ATLEAST(1,3,0)
SDL_HapticEffect* efx;
/* Haptic must be enabled. */
if(haptic == NULL)
return 0;
efx = &haptic_rumbleEffect;
memset(efx, 0, sizeof(SDL_HapticEffect));
efx->type = SDL_HAPTIC_SINE;
efx->periodic.direction.type = SDL_HAPTIC_POLAR;
efx->periodic.length = 1000;
efx->periodic.period = 200;
efx->periodic.magnitude = 0x4000;
efx->periodic.fade_length = 1000;
efx->periodic.fade_level = 0;
haptic_rumble = SDL_HapticNewEffect(haptic, efx);
if(haptic_rumble < 0) {
WARN("Unable to upload haptic effect: %s.", SDL_GetError());
return -1;
}
#endif
return 0;
}
/**
* @brief Run a rumble effect.
* @param mod Current modifier being added.
*/
static void spfx_hapticRumble(double mod) {
#if SDL_VERSION_ATLEAST(1,3,0)
SDL_HapticEffect* efx;
double len, mag;
if(haptic_rumble >= 0) {
/* Not time to update yet. */
if((haptic_lastUpdate > 0.) || shake_off || (mod > SHAKE_MAX/3.))
return;
/* Stop the effect if it was playing. */
SDL_HapticStopEffect(haptic, haptic_rumble);
/* Get length and magnitude. */
len = 1000. * shake_rad / SHAKE_DECAY;
mag = 32767. * (shake_rad / SHAKE_MAX);
/* Update the effect. */
efx = &haptic_rumbleEffect;
efx->periodic.magnitude = (uint32_t)mag;
efx->periodic.length = (uint32_t)len;
efx->periodic.fade_length = MIN(efx->periodic.length, 1000);
if(SDL_HapticUpdateEffect(haptic, haptic_rumble, &haptic_rumbleEffect) < 0) {
WARN("Failed to update effect: %s.", SDL_GetError());
return;
}
/* Run the new effect. */
SDL_HapticRunEffect(haptic, haptic_rumble, 1);
/* Set timer again. */
haptic_lastUpdate = HAPTIC_UPDATE_INTERVAL;
}
#else
(void)mod;
#endif
}
/**
* @brief Set the cinematic mode.
*
* Should be run at the end of the render loop if needed.
*/
void spfx_cinematic(void) {
glMatrixMode(GL_MODELVIEW);
glPushMatrix(); /* Translation matrix. */
glTranslated(-(double)SCREEN_W/2., -(double)SCREEN_H/2., 0);
COLOUR(cBlack);
glBegin(GL_QUADS);
glVertex2d(0., 0.);
glVertex2d(0., SCREEN_H*0.2);
glVertex2d(SCREEN_W, SCREEN_H*0.2);
glVertex2d(SCREEN_W, 0.);
glVertex2d(0., SCREEN_H);
glVertex2d(SCREEN_W, SCREEN_H);
glVertex2d(SCREEN_W, SCREEN_H*0.8);
glVertex2d(0., SCREEN_H*0.8);
glEnd();
glPopMatrix(); /* Translation matrix. */
}
/**
* @brief Render the entire spfx layer.
* @param layer Layer to render.
*/
void spfx_render(const int layer) {
SPFX* spfx_stack;
int i, spfx_nstack;
SPFX_Base* effect;
int sx, sy;
double time;
/* Get the appropriate layer. */
switch(layer) {
case SPFX_LAYER_FRONT:
spfx_stack = spfx_stack_front;
spfx_nstack = spfx_nstack_front;
break;
case SPFX_LAYER_BACK:
spfx_stack = spfx_stack_back;
spfx_nstack = spfx_nstack_back;
break;
}
/* Now render the layer. */
for(i = spfx_nstack-1; i >= 0; i--) {
effect = &spfx_effects[spfx_stack[i].effect];
/* Simplifies. */
sx = (int)effect->gfx->sx;
sy = (int)effect->gfx->sy;
if(!paused) { /* Don't calculate frame if paused. */
time = fmod(spfx_stack[i].timer, effect->anim) / effect->anim;
spfx_stack[i].lastframe = sx * sy * MIN(time, 1.);
}
/* Render. */
gl_blitSprite(effect->gfx,
VX(spfx_stack[i].pos), VY(spfx_stack[i].pos),
spfx_stack[i].lastframe % sx,
spfx_stack[i].lastframe / sx,
NULL);
}
}