Files
fs2open.github.com/code/asteroid/asteroid.cpp
T
Goober5000 3851832db5 make fireball types dynamic
As requested by Event Horizon, this removes the limit on fireball types.  The fixed array has been replaced with a vector and the relevant code updated accordingly.  There is also a new version of `stuff_int_list()` to parse ints into a vector.

This also adds bounds checking for explosion animations that are parsed for ship classes, ship types, and asteroids, since previously there was no guarantee that the indexes would be in range.
2024-04-25 21:26:35 -04:00

2748 lines
83 KiB
C++

/*
* Copyright (C) Volition, Inc. 1999. All rights reserved.
*
* All source code herein is the property of Volition, Inc. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "asteroid/asteroid.h"
#include "debugconsole/console.h"
#include "fireball/fireballs.h"
#include "freespace.h"
#include "gamesnd/gamesnd.h"
#include "globalincs/linklist.h"
#include "globalincs/systemvars.h"
#include "hud/hud.h"
#include "hud/hudescort.h"
#include "hud/hudgauges.h"
#include "hud/hudtarget.h"
#include "iff_defs/iff_defs.h"
#include "io/timer.h"
#include "localization/localize.h"
#include "math/staticrand.h"
#include "math/vecmat.h"
#include "model/model.h"
#include "network/multi.h"
#include "network/multimsgs.h"
#include "network/multiutil.h"
#include "object/objcollide.h"
#include "object/object.h"
#include "parse/parselo.h"
#include "scripting/global_hooks.h"
#include "particle/particle.h"
#include "render/3d.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "ship/shiphit.h"
#include "stats/scoring.h"
#include "weapon/weapon.h"
#include <algorithm>
#define ASTEROID_OBJ_USED (1<<0) // flag used in asteroid_obj struct
#define MAX_ASTEROID_OBJS MAX_ASTEROIDS // max number of asteroids tracked in asteroid list
asteroid_obj Asteroid_objs[MAX_ASTEROID_OBJS]; // array used to store asteroid object indexes
asteroid_obj Asteroid_obj_list; // head of linked list of asteroid_obj structs
// used for randomly generating debris type when there are multiple sizes.
const float SMALL_DEBRIS_WEIGHT = 8.0f;
const float MEDIUM_DEBRIS_WEIGHT = 4.0f;
const float LARGE_DEBRIS_WEIGHT = 1.0f;
int Asteroids_enabled = 1;
int Num_asteroids = 0;
// Contains information for asteroid and debris objects for use in asteroid fields
SCP_vector< asteroid_info > Asteroid_info;
asteroid Asteroids[MAX_ASTEROIDS];
asteroid_field Asteroid_field;
static int Asteroid_impact_explosion_ani;
static float Asteroid_impact_explosion_radius;
char Asteroid_icon_closeup_model[NAME_LENGTH];
vec3d Asteroid_icon_closeup_position;
float Asteroid_icon_closeup_zoom;
#define ASTEROID_CHECK_WRAP_TIMESTAMP 2000 // how often an asteroid gets checked for wrapping
#define ASTEROID_UPDATE_COLLIDE_TIMESTAMP 2000 // how often asteroid is checked for impending collisions with escort ships
#define ASTEROID_MIN_COLLIDE_TIME 24 // time in seconds to check for asteroid colliding
typedef struct asteroid_target {
int objnum;
int signature;
TIMESTAMP throw_stamp;
int incoming_asteroids;
} asteroid_target;
// if default throwing behavior is enabled, then this should always have at most 1 target or possibly none, determined the retail way
// if not, then this is whatever number of mission-specified ships (after they arrive, list is sanitized when they exit)
SCP_vector<asteroid_target> Asteroid_targets;
/**
* Return number of asteroids expected to collide with a ship.
*/
static int count_incident_asteroids(int target_objnum)
{
object* asteroid_objp;
int count = 0;
Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "count_incident_asteroids() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");
for (auto aop: list_range(&Asteroid_obj_list)) {
asteroid_objp = &Objects[aop->objnum];
if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
if (asteroid_objp->type == OBJ_ASTEROID) {
asteroid* asp = &Asteroids[asteroid_objp->instance];
if (asp->target_objnum == target_objnum) {
count++;
}
}
}
return count;
}
// add a ship as a new asteroid target
void asteroid_add_target(object* objp) {
asteroid_target new_target;
new_target.objnum = OBJ_INDEX(objp);
new_target.signature = objp->signature;
new_target.throw_stamp = _timestamp(Random::next(500, 2000));
new_target.incoming_asteroids = count_incident_asteroids(OBJ_INDEX(objp)); // this *should* normally be 0
Asteroid_targets.push_back(new_target);
}
/**
* Force updating of pair stuff for asteroid *objp.
*/
static void asteroid_update_collide(object *objp)
{
// Asteroid has wrapped, update collide objnum and flags
Asteroids[objp->instance].collide_objnum = -1;
Asteroids[objp->instance].collide_objsig = -1;
OBJ_RECALC_PAIRS(objp);
}
/**
* Clear out the ::Asteroid_obj_list
*/
static void asteroid_obj_list_init()
{
int i;
list_init(&Asteroid_obj_list);
for ( i = 0; i < MAX_ASTEROID_OBJS; i++ ) {
Asteroid_objs[i].flags = 0;
}
}
/**
* Add a node from the Asteroid_obj_list.
* Only called from ::weapon_create()
*/
static int asteroid_obj_list_add(int objnum)
{
int index;
Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_obj_list_add() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");
asteroid *cur_asteroid = &Asteroids[Objects[objnum].instance];
index = (int)(cur_asteroid - Asteroids);
Assert(index >= 0 && index < MAX_ASTEROID_OBJS);
Assert(!(Asteroid_objs[index].flags & ASTEROID_OBJ_USED));
Asteroid_objs[index].flags = 0;
Asteroid_objs[index].objnum = objnum;
list_append(&Asteroid_obj_list, &Asteroid_objs[index]);
Asteroid_objs[index].flags |= ASTEROID_OBJ_USED;
return index;
}
/**
* Remove a node from the Asteroid_obj_list.
* Only called from ::weapon_delete()
*/
static void asteroid_obj_list_remove(object * obj)
{
int index = obj->instance;
Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_obj_list_remove() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");
Assert(index >= 0 && index < MAX_ASTEROID_OBJS);
Assert(Asteroid_objs[index].flags & ASTEROID_OBJ_USED);
list_remove(&Asteroid_obj_list, &Asteroid_objs[index]);
Asteroid_objs[index].flags = 0;
}
/**
* Prevent speed from getting too huge so it's hard to catch up to an asteroid.
*/
static float asteroid_cap_speed(int asteroid_info_index, float speed)
{
float max, double_max;
Assert( asteroid_info_index < (int)Asteroid_info.size() );
max = Asteroid_info[asteroid_info_index].max_speed;
double_max = max * 2;
while (speed > double_max){
speed *= 0.5f;
}
if (speed > max){
speed *= 0.75f;
}
return speed;
}
/**
* Check if asteroid is within inner bound
* @return 0 if not inside or no inner bound, 1 if inside inner bound
*/
static bool asteroid_in_inner_bound(asteroid_field *asfieldp, vec3d *pos, float delta) {
if (!asfieldp->has_inner_bound)
return false;
return (pos->xyz.x > asfieldp->inner_min_bound.xyz.x - delta) && (pos->xyz.x < asfieldp->inner_max_bound.xyz.x + delta) &&
(pos->xyz.y > asfieldp->inner_min_bound.xyz.y - delta) && (pos->xyz.y < asfieldp->inner_max_bound.xyz.y + delta) &&
(pos->xyz.z > asfieldp->inner_min_bound.xyz.z - delta) && (pos->xyz.z < asfieldp->inner_max_bound.xyz.z + delta);
}
static bool asteroid_is_ship_inside_field(asteroid_field* asfieldp, vec3d* pos, float radius) {
radius *= 2.0f;
return pos->xyz.x + radius > Asteroid_field.min_bound.xyz.x && pos->xyz.x - radius < Asteroid_field.max_bound.xyz.x &&
pos->xyz.y + radius > Asteroid_field.min_bound.xyz.y && pos->xyz.y - radius < Asteroid_field.max_bound.xyz.y &&
pos->xyz.z + radius > Asteroid_field.min_bound.xyz.z && pos->xyz.z - radius < Asteroid_field.max_bound.xyz.z &&
!asteroid_in_inner_bound(asfieldp, pos, radius);
}
/**
* Repositions asteroid outside the inner box on all 3 axes
*
* Moves to the other side of the inner box a distance delta from edge of box
*/
static void inner_bound_pos_fixup(asteroid_field *asfieldp, vec3d *pos)
{
if (!asteroid_in_inner_bound(asfieldp, pos, 0)) {
return;
}
float dist1, dist2;
int axis;
for (axis=0; axis<3; axis++) {
dist1 = pos->a1d[axis] - asfieldp->inner_min_bound.a1d[axis];
dist2 = asfieldp->inner_max_bound.a1d[axis] - pos->a1d[axis];
Assert(dist1 >= 0 && dist2 >= 0);
if (dist1 < dist2) {
pos->a1d[axis] = asfieldp->inner_max_bound.a1d[axis] + dist1;
} else {
pos->a1d[axis] = asfieldp->inner_min_bound.a1d[axis] - dist2;
}
}
}
/**
* Create a single asteroid
*/
object *asteroid_create(asteroid_field *asfieldp, int asteroid_type, int asteroid_subtype, bool check_visibility)
{
int n, objnum;
matrix orient;
object *objp;
asteroid *asp;
asteroid_info *asip;
vec3d pos, delta_bound;
angles angs;
float radius;
ushort signature;
int rand_base;
// bogus
if(asfieldp == NULL) {
return NULL;
}
for (n=0; n<MAX_ASTEROIDS; n++) {
if (!(Asteroids[n].flags & AF_USED)) {
break;
}
}
if (n >= MAX_ASTEROIDS) {
nprintf(("Warning","Could not create asteroid, no more slots left\n"));
return NULL;
}
if((asteroid_type < 0) || (asteroid_type >= (int)Asteroid_info.size())) {
return NULL;
}
if((asteroid_subtype < 0) || (asteroid_subtype >= NUM_ASTEROID_POFS)) {
return NULL;
}
// HACK: multiplayer asteroid subtype always 0 to keep subtype in sync
if ( Game_mode & GM_MULTIPLAYER) {
asteroid_subtype = 0;
}
asip = &Asteroid_info[asteroid_type];
// bogus
if(asip->modelp[asteroid_subtype] == NULL) {
return NULL;
}
asp = &Asteroids[n];
asp->asteroid_type = asteroid_type;
asp->asteroid_subtype = asteroid_subtype;
asp->flags = 0;
asp->flags |= AF_USED;
asp->check_for_wrap = _timestamp_rand(0, ASTEROID_CHECK_WRAP_TIMESTAMP);
asp->check_for_collide = _timestamp_rand(0, ASTEROID_UPDATE_COLLIDE_TIMESTAMP);
asp->final_death_time = TIMESTAMP::invalid();
asp->collide_objnum = -1;
asp->collide_objsig = -1;
asp->target_objnum = -1;
radius = model_get_radius(asip->model_num[asteroid_subtype]);
vm_vec_sub(&delta_bound, &asfieldp->max_bound, &asfieldp->min_bound);
// for multiplayer, we want to do a static_rand so that everything behaves the same on all machines
signature = 0;
rand_base = 0;
if ( Game_mode & GM_NORMAL ) {
pos.xyz.x = asfieldp->min_bound.xyz.x + delta_bound.xyz.x * frand();
pos.xyz.y = asfieldp->min_bound.xyz.y + delta_bound.xyz.y * frand();
pos.xyz.z = asfieldp->min_bound.xyz.z + delta_bound.xyz.z * frand();
inner_bound_pos_fixup(asfieldp, &pos);
angs.p = frand() * PI2;
angs.b = frand() * PI2;
angs.h = frand() * PI2;
} else {
signature = multi_assign_network_signature( MULTI_SIG_ASTEROID );
rand_base = signature;
pos.xyz.x = asfieldp->min_bound.xyz.x + delta_bound.xyz.x * static_randf( rand_base++ );
pos.xyz.y = asfieldp->min_bound.xyz.y + delta_bound.xyz.y * static_randf( rand_base++ );
pos.xyz.z = asfieldp->min_bound.xyz.z + delta_bound.xyz.z * static_randf( rand_base++ );
inner_bound_pos_fixup(asfieldp, &pos);
angs.p = static_randf( rand_base++ ) * PI2;
angs.b = static_randf( rand_base++ ) * PI2;
angs.h = static_randf( rand_base++ ) * PI2;
}
// If the generated asteroid position is within the player view then abort
// I'm unsure how Eyepoint is handled for multiplayer, so this may need to move up into the SP
// only section - Mjn
if (check_visibility && asteroid_is_within_view(&pos, -1.0f, true)) {
return nullptr;
}
vm_angles_2_matrix(&orient, &angs);
flagset<Object::Object_Flags> asteroid_default_flagset;
asteroid_default_flagset += Object::Object_Flags::Renders;
asteroid_default_flagset += Object::Object_Flags::Physics;
asteroid_default_flagset += Object::Object_Flags::Collides;
objnum = obj_create(OBJ_ASTEROID, -1, n, &orient, &pos, radius, asteroid_default_flagset, false);
if ( (objnum == -1) || (objnum >= MAX_OBJECTS) ) {
mprintf(("Couldn't create asteroid -- out of object slots\n"));
return NULL;
}
asp->objnum = objnum;
asp->model_instance_num = -1;
if (model_get(asip->model_num[asteroid_subtype])->flags & PM_FLAG_HAS_INTRINSIC_MOTION) {
asp->model_instance_num = model_create_instance(objnum, asip->model_num[asteroid_subtype]);
}
// Add to Asteroid_used_list
asteroid_obj_list_add(objnum);
objp = &Objects[objnum];
if ( Game_mode & GM_MULTIPLAYER ){
objp->net_signature = signature;
}
Num_asteroids++;
if (radius < 1.0) {
radius = 1.0f;
}
vec3d rotvel;
if ( Game_mode & GM_NORMAL ) {
vm_vec_rand_vec_quick(&rotvel);
vm_vec_scale(&rotvel, asip->rotational_vel_multiplier * (frand()/4.0f + 0.1f));
objp->phys_info.rotvel = rotvel;
vm_vec_rand_vec_quick(&objp->phys_info.vel);
} else {
static_randvec( rand_base++, &rotvel );
vm_vec_scale(&rotvel, asip->rotational_vel_multiplier * (static_randf(rand_base++)/4.0f + 0.1f));
objp->phys_info.rotvel = rotvel;
static_randvec( rand_base++, &objp->phys_info.vel );
}
float speed;
if ( Game_mode & GM_NORMAL ) {
speed = asteroid_cap_speed(asteroid_type, asfieldp->speed*frand_range(0.5f + (float) Game_skill_level/NUM_SKILL_LEVELS, 2.0f + (float) (2*Game_skill_level)/NUM_SKILL_LEVELS));
} else {
speed = asteroid_cap_speed(asteroid_type, asfieldp->speed*static_randf_range(rand_base++, 0.5f + (float) Game_skill_level/NUM_SKILL_LEVELS, 2.0f + (float) (2*Game_skill_level)/NUM_SKILL_LEVELS));
}
vm_vec_scale(&objp->phys_info.vel, speed);
objp->phys_info.desired_vel = objp->phys_info.vel;
// blow out his reverse thrusters. Or drag, same thing.
objp->phys_info.rotdamp = 10000.0f;
objp->phys_info.flags |= (PF_DEAD_DAMP | PF_BALLISTIC);
objp->phys_info.gravity_const = asip->gravity_const;
// Fill in the max_vel field, so the collision pair stuff knows
// how fast this can move maximum in order to throw out collisions.
// This is in local coordinates, so Z is forward velocity.
objp->phys_info.max_vel.xyz.x = 0.0f;
objp->phys_info.max_vel.xyz.y = 0.0f;
objp->phys_info.max_vel.xyz.z = vm_vec_mag(&objp->phys_info.desired_vel);
objp->phys_info.mass = asip->modelp[asteroid_subtype]->rad * 700.0f;
objp->phys_info.I_body_inv.vec.rvec.xyz.x = 1.0f / (objp->phys_info.mass*asip->modelp[asteroid_subtype]->rad);
objp->phys_info.I_body_inv.vec.uvec.xyz.y = objp->phys_info.I_body_inv.vec.rvec.xyz.x;
objp->phys_info.I_body_inv.vec.fvec.xyz.z = objp->phys_info.I_body_inv.vec.rvec.xyz.x;
objp->hull_strength = asip->initial_asteroid_strength * (0.8f + (float)Game_skill_level/NUM_SKILL_LEVELS)/2.0f;
// ensure vel is valid
Assert( !vm_is_vec_nan(&objp->phys_info.vel) );
if (scripting::hooks::OnAsteroidCreated->isActive()) {
scripting::hooks::OnAsteroidCreated->run(
scripting::hook_param_list(
scripting::hook_param("Asteroid", 'o', objp)));
}
return objp;
}
/**
* Create asteroids when parent_objp blows up.
*/
void asteroid_sub_create(object *parent_objp, int asteroid_type, vec3d *relvec)
{
object *new_objp;
float speed;
Assert(parent_objp->type == OBJ_ASTEROID);
int subtype = Asteroids[parent_objp->instance].asteroid_subtype;
new_objp = asteroid_create(&Asteroid_field, asteroid_type, subtype);
if (new_objp == NULL)
return;
if ( MULTIPLAYER_MASTER ){
send_asteroid_create( new_objp, parent_objp, asteroid_type, relvec );
}
// Now, bash some values.
vm_vec_scale_add(&new_objp->pos, &parent_objp->pos, relvec, 0.5f * parent_objp->radius);
float parent_speed = vm_vec_mag_quick(&parent_objp->phys_info.vel);
if ( parent_speed < 0.1f ) {
parent_speed = vm_vec_mag_quick(&Asteroid_field.vel);
}
new_objp->phys_info.vel = parent_objp->phys_info.vel;
if ( Game_mode & GM_NORMAL )
speed = asteroid_cap_speed(asteroid_type, (frand() + 2.0f) * parent_speed);
else
speed = asteroid_cap_speed(asteroid_type, (static_randf(new_objp->net_signature)+2.0f) * parent_speed);
vm_vec_scale_add2(&new_objp->phys_info.vel, relvec, speed);
if (vm_vec_mag_quick(&new_objp->phys_info.vel) > 80.0f)
vm_vec_scale(&new_objp->phys_info.vel, 0.5f);
new_objp->phys_info.desired_vel = new_objp->phys_info.vel;
vm_vec_scale_add(&new_objp->last_pos, &new_objp->pos, &new_objp->phys_info.vel, -flFrametime);
}
/**
* Load in an asteroid model
*/
static void asteroid_load(int asteroid_info_index, int asteroid_subtype)
{
int i;
asteroid_info *asip;
Assertion(asteroid_info_index < (int)Asteroid_info.size(), "Command to load asteroid at index %i, but only %i asteroids exist", asteroid_info_index, (int)Asteroid_info.size());
Assertion(asteroid_subtype < NUM_ASTEROID_POFS, "Command to load asteroid pof %i, but only %i pofs are allowed", asteroid_subtype, NUM_ASTEROID_POFS);
if ( (asteroid_info_index >= (int)Asteroid_info.size()) || (asteroid_subtype >= NUM_ASTEROID_POFS) ) {
return;
}
asip = &Asteroid_info[asteroid_info_index];
if ( !VALID_FNAME(asip->pof_files[asteroid_subtype]) )
return;
//Check if the model is already loaded
if (asip->model_num[asteroid_subtype] >= 0)
return;
asip->model_num[asteroid_subtype] = model_load( asip->pof_files[asteroid_subtype], 0, NULL );
if (asip->model_num[asteroid_subtype] >= 0)
{
polymodel *pm = asip->modelp[asteroid_subtype] = model_get(asip->model_num[asteroid_subtype]);
if ( asip->num_detail_levels != pm->n_detail_levels )
{
if ( !Is_standalone )
{
// just log to file for standalone servers
Warning(LOCATION, "For asteroid '%s', detail level mismatch (POF has %d, table has %d)", asip->name, pm->n_detail_levels, asip->num_detail_levels );
}
else
{
nprintf(("Warning", "For asteroid '%s', detail level mismatch (POF has %d, table has %d)\n", asip->name, pm->n_detail_levels, asip->num_detail_levels ));
}
}
// Stuff detail level distances.
for ( i=0; i<pm->n_detail_levels; i++ )
pm->detail_depth[i] = (i < asip->num_detail_levels) ? i2fl(asip->detail_distance[i]) : 0.0f;
}
}
/**
* Create all the asteroids for the mission
*/
void asteroid_create_all()
{
int i, idx;
// ship_debris_odds_table keeps track of debris type of the next debris piece
// each different type (size) of debris piece has a diffenent weight, smaller weighted more heavily than larger.
// choose next type from table ship_debris_odds_table by Random::next()%max_weighted_range,
// the threshold *below* which the debris type is selected.
struct {
float random_threshold;
int debris_type;
} ship_debris_odds_table[MAX_ACTIVE_DEBRIS_TYPES];
float max_weighted_range = 0.0f;
if (!Asteroids_enabled)
return;
if (Asteroid_field.num_initial_asteroids <= 0 ) {
return;
}
if (Asteroid_field.num_used_field_debris_types <= 0) {
Warning(LOCATION, "An asteroid field is enabled, but no asteroid types were enabled.");
return;
}
//Check that the asteroid field bounds are valid - Mjn
if (Asteroid_field.min_bound.xyz.x >= Asteroid_field.max_bound.xyz.x) {
Warning(LOCATION, "Asteroid field Outer Bound Min X is greater than or equal to Max X. Aborting asteroid creation!");
return;
}
if (Asteroid_field.min_bound.xyz.y >= Asteroid_field.max_bound.xyz.y) {
Warning(LOCATION, "Asteroid field Outer Bound Min Y is greater than or equal to Max Y. Aborting asteroid creation!");
return;
}
if (Asteroid_field.min_bound.xyz.z >= Asteroid_field.max_bound.xyz.z) {
Warning(LOCATION, "Asteroid field Outer Bound Min Z is greater than or equal to Max Z. Aborting asteroid creation!");
return;
}
if (Asteroid_field.inner_min_bound.xyz.x >= Asteroid_field.inner_max_bound.xyz.x) {
Warning(LOCATION, "Asteroid field Inner Bound Min X is greater than or equal to Max X. Aborting asteroid creation!");
return;
}
if (Asteroid_field.inner_min_bound.xyz.y >= Asteroid_field.inner_max_bound.xyz.y) {
Warning(LOCATION, "Asteroid field Inner Bound Min Y is greater than or equal to Max Y. Aborting asteroid creation!");
return;
}
if (Asteroid_field.inner_min_bound.xyz.z >= Asteroid_field.inner_max_bound.xyz.z) {
Warning(LOCATION, "Asteroid field Inner Bound Min Z is greater than or equal to Max Z. Aborting asteroid creation!");
return;
}
int max_asteroids = Asteroid_field.num_initial_asteroids; // * (1.0f - 0.1f*(MAX_DETAIL_LEVEL-Detail.asteroid_density)));
int num_debris_types = 0;
// get number of ship debris types
if (Asteroid_field.debris_genre == DG_DEBRIS) {
for (idx=0; idx<MAX_ACTIVE_DEBRIS_TYPES; idx++) {
if (Asteroid_field.field_debris_type[idx] != -1) {
num_debris_types++;
}
}
// Calculate the odds table
for (idx=0; idx<num_debris_types; idx++) {
float debris_weight = Asteroid_info[Asteroid_field.field_debris_type[idx]].spawn_weight;
ship_debris_odds_table[idx].random_threshold = max_weighted_range + debris_weight;
ship_debris_odds_table[idx].debris_type = Asteroid_field.field_debris_type[idx];
max_weighted_range += debris_weight;
}
}
// Load Asteroid/ship models
if (Asteroid_field.debris_genre == DG_DEBRIS) {
for (idx=0; idx<num_debris_types; idx++) {
asteroid_load(Asteroid_field.field_debris_type[idx], 0);
}
} else {
if (Asteroid_field.field_debris_type[0] != -1) {
asteroid_load(ASTEROID_TYPE_SMALL, 0);
asteroid_load(ASTEROID_TYPE_MEDIUM, 0);
asteroid_load(ASTEROID_TYPE_LARGE, 0);
}
if (Asteroid_field.field_debris_type[1] != -1) {
asteroid_load(ASTEROID_TYPE_SMALL, 1);
asteroid_load(ASTEROID_TYPE_MEDIUM, 1);
asteroid_load(ASTEROID_TYPE_LARGE, 1);
}
if (Asteroid_field.field_debris_type[2] != -1) {
asteroid_load(ASTEROID_TYPE_SMALL, 2);
asteroid_load(ASTEROID_TYPE_MEDIUM, 2);
asteroid_load(ASTEROID_TYPE_LARGE, 2);
}
}
// load all the asteroid/debris pieces
for (i=0; i<max_asteroids; i++) {
if (Asteroid_field.debris_genre == DG_ASTEROID) {
// For asteroid, load only large asteroids
// get a valid subtype
int counter = Random::next(Asteroid_field.num_used_field_debris_types);
int subtype = -1;
for (int j = 0; j < NUM_ASTEROID_POFS; j++) {
if (Asteroid_field.field_debris_type[j] >= 0) {
if (counter == 0) {
subtype = j;
break;
} else
counter--;
}
}
if (subtype >= 0)
asteroid_create(&Asteroid_field, ASTEROID_TYPE_LARGE, subtype);
} else {
Assert(num_debris_types > 0);
float rand_choice = frand() * max_weighted_range;
for (idx=0; idx<MAX_ACTIVE_DEBRIS_TYPES; idx++) {
// for ship debris, choose type according to odds table
if (rand_choice < ship_debris_odds_table[idx].random_threshold) {
asteroid_create(&Asteroid_field, ship_debris_odds_table[idx].debris_type, 0);
break;
}
}
}
}
}
// instantly deletes all asteroids in a mission before moving on.
// Uses obj_delete_all_that_should_be_dead() so this method has a side effect
// of also deleting non-asteroid objects marked as dead in the same frame. Usually this is
// desired but it's worth knowing ahead of time. -Mjn
void remove_all_asteroids()
{
for (int i = 0; i < MAX_ASTEROIDS; i++) {
if (Asteroids[i].flags & AF_USED) {
Asteroids[i].flags &= ~AF_USED;
Assert(Asteroids[i].objnum >= 0 && Asteroids[i].objnum < MAX_OBJECTS);
Objects[Asteroids[i].objnum].flags.set(Object::Object_Flags::Should_be_dead);
}
}
// This feels hackish, but we need to make sure all the asteroids are actually gone before we continue-Mjn
obj_delete_all_that_should_be_dead();
}
// will replace any existing asteroid or debris field with an asteroid field
void asteroid_create_asteroid_field(int num_asteroids, int field_type, int asteroid_speed, bool brown, bool blue, bool orange, vec3d o_min, vec3d o_max, bool inner_box, vec3d i_min, vec3d i_max, SCP_vector<SCP_string> targets)
{
remove_all_asteroids();
Asteroid_field.num_initial_asteroids = num_asteroids;
switch (field_type) {
case 0:
Asteroid_field.field_type = FT_PASSIVE;
Asteroid_field.enhanced_visibility_checks = false;
break;
case 1:
Asteroid_field.field_type = FT_ACTIVE;
Asteroid_field.enhanced_visibility_checks = false;
break;
case 2:
Asteroid_field.field_type = FT_PASSIVE;
Asteroid_field.enhanced_visibility_checks = true;
break;
case 3:
Asteroid_field.field_type = FT_ACTIVE;
Asteroid_field.enhanced_visibility_checks = true;
break;
default: //Any other number will just give default behavior
Asteroid_field.field_type = FT_PASSIVE;
Asteroid_field.enhanced_visibility_checks = false;
break;
}
vm_vec_rand_vec_quick(&Asteroid_field.vel);
vm_vec_scale(&Asteroid_field.vel, (float)asteroid_speed);
Asteroid_field.speed = (float)asteroid_speed;
Asteroid_field.debris_genre = DG_ASTEROID;
Asteroid_field.field_debris_type[0] = -1;
Asteroid_field.field_debris_type[1] = -1;
Asteroid_field.field_debris_type[2] = -1;
int count = 0;
if (brown) {
Asteroid_field.field_debris_type[0] = 1;
count++;
}
if (blue) {
Asteroid_field.field_debris_type[1] = 1;
count++;
}
if (orange) {
Asteroid_field.field_debris_type[2] = 1;
count++;
}
Asteroid_field.num_used_field_debris_types = count;
Asteroid_field.min_bound = o_min;
Asteroid_field.max_bound = o_max;
vec3d a_rad;
vm_vec_sub(&a_rad, &Asteroid_field.max_bound, &Asteroid_field.min_bound);
vm_vec_scale(&a_rad, 0.5f);
float b_rad = vm_vec_mag(&a_rad);
Asteroid_field.bound_rad = MAX(3000.0f, b_rad);
if (inner_box) {
Asteroid_field.has_inner_bound = true;
Asteroid_field.inner_min_bound = i_min;
Asteroid_field.inner_max_bound = i_max;
}
Asteroid_field.target_names = targets;
// Only create asteroids if we have some to create
if ((count > 0) && (num_asteroids > 0)) {
asteroid_create_all();
}
}
// will replace any existing asteroid or debris field with a debris field
void asteroid_create_debris_field(int num_asteroids, int asteroid_speed, int debris1, int debris2, int debris3, vec3d o_min, vec3d o_max, bool enhanced)
{
remove_all_asteroids();
Asteroid_field.num_initial_asteroids = num_asteroids;
Asteroid_field.field_type = FT_PASSIVE;
Asteroid_field.enhanced_visibility_checks = enhanced;
vm_vec_rand_vec_quick(&Asteroid_field.vel);
vm_vec_scale(&Asteroid_field.vel, (float)asteroid_speed);
Asteroid_field.speed = (float)asteroid_speed;
Asteroid_field.debris_genre = DG_DEBRIS;
Asteroid_field.field_debris_type[0] = -1;
Asteroid_field.field_debris_type[1] = -1;
Asteroid_field.field_debris_type[2] = -1;
int count = 0;
for (int i = 0; i < MAX_ACTIVE_DEBRIS_TYPES; i++) {
if (debris1 >= 0) {
Asteroid_field.field_debris_type[i] = debris1;
debris1 = -1;
count++;
continue;
}
if (debris2 >= 0) {
Asteroid_field.field_debris_type[i] = debris2;
debris2 = -1;
count++;
continue;
}
if (debris3 >= 0) {
Asteroid_field.field_debris_type[i] = debris3;
debris3 = -1;
count++;
continue;
}
}
Asteroid_field.num_used_field_debris_types = count;
Asteroid_field.min_bound = o_min;
Asteroid_field.max_bound = o_max;
vec3d a_rad;
vm_vec_sub(&a_rad, &Asteroid_field.max_bound, &Asteroid_field.min_bound);
vm_vec_scale(&a_rad, 0.5f);
float b_rad = vm_vec_mag(&a_rad);
Asteroid_field.bound_rad = MAX(3000.0f, b_rad);
// Only create debris if we have some to create
if ((count > 0) && (num_asteroids > 0)) {
asteroid_create_all();
}
}
/**
* Init asteroid system for the level, called from ::game_level_init()
*/
void asteroid_level_init()
{
Asteroid_field.num_initial_asteroids = 0; // disable asteroid field by default.
Asteroid_field.speed = 0.0f;
vm_vec_make(&Asteroid_field.min_bound, -1000.0f, -1000.0f, -1000.0f);
vm_vec_make(&Asteroid_field.max_bound, 1000.0f, 1000.0f, 1000.0f);
vm_vec_make(&Asteroid_field.inner_min_bound, -500.0f, -500.0f, -500.0f);
vm_vec_make(&Asteroid_field.inner_max_bound, 500.0f, 500.0f, 500.0f);
Asteroid_field.has_inner_bound = false;
Asteroid_field.field_type = FT_ACTIVE;
Asteroid_field.debris_genre = DG_ASTEROID;
Asteroid_field.field_debris_type[0] = -1;
Asteroid_field.field_debris_type[1] = -1;
Asteroid_field.field_debris_type[2] = -1;
Asteroid_field.num_used_field_debris_types = 0;
Asteroid_field.target_names.clear();
if (!Fred_running)
{
for (auto& ast : Asteroid_info)
ast.damage_type_idx = ast.damage_type_idx_sav;
Num_asteroids = 0;
asteroid_obj_list_init();
Asteroid_targets.clear();
}
}
/**
* Should asteroid wrap from one end of the asteroid field to the other.
* Multiplayer clients will always return 0 from this function. We will force a wrap on the clients when server tells us
*
* @return !0 if asteroid should be wrapped, 0 otherwise.
*/
static int asteroid_should_wrap(object *objp, asteroid_field *asfieldp)
{
if ( MULTIPLAYER_CLIENT )
return 0;
if (objp->pos.xyz.x < asfieldp->min_bound.xyz.x) {
return 1;
}
if (objp->pos.xyz.y < asfieldp->min_bound.xyz.y) {
return 1;
}
if (objp->pos.xyz.z < asfieldp->min_bound.xyz.z) {
return 1;
}
if (objp->pos.xyz.x > asfieldp->max_bound.xyz.x) {
return 1;
}
if (objp->pos.xyz.y > asfieldp->max_bound.xyz.y) {
return 1;
}
if (objp->pos.xyz.z > asfieldp->max_bound.xyz.z) {
return 1;
}
// check against inner bound
if (asfieldp->has_inner_bound) {
if ( (objp->pos.xyz.x > asfieldp->inner_min_bound.xyz.x) && (objp->pos.xyz.x < asfieldp->inner_max_bound.xyz.x)
&& (objp->pos.xyz.y > asfieldp->inner_min_bound.xyz.y) && (objp->pos.xyz.y < asfieldp->inner_max_bound.xyz.y)
&& (objp->pos.xyz.z > asfieldp->inner_min_bound.xyz.z) && (objp->pos.xyz.z < asfieldp->inner_max_bound.xyz.z) ) {
return 1;
}
}
return 0;
}
/**
* Generates a potential new position for an asteroid that might wrap
* from one end of the asteroid field to the other
*/
static void asteroid_wrap_pos(vec3d *pos, asteroid_field *asfieldp)
{
if (pos->xyz.x < asfieldp->min_bound.xyz.x) {
pos->xyz.x = asfieldp->max_bound.xyz.x + (pos->xyz.x - asfieldp->min_bound.xyz.x);
}
if (pos->xyz.y < asfieldp->min_bound.xyz.y) {
pos->xyz.y = asfieldp->max_bound.xyz.y + (pos->xyz.y - asfieldp->min_bound.xyz.y);
}
if (pos->xyz.z < asfieldp->min_bound.xyz.z) {
pos->xyz.z = asfieldp->max_bound.xyz.z + (pos->xyz.z - asfieldp->min_bound.xyz.z);
}
if (pos->xyz.x > asfieldp->max_bound.xyz.x) {
pos->xyz.x = asfieldp->min_bound.xyz.x + (pos->xyz.x - asfieldp->max_bound.xyz.x);
}
if (pos->xyz.y > asfieldp->max_bound.xyz.y) {
pos->xyz.y = asfieldp->min_bound.xyz.y + (pos->xyz.y - asfieldp->max_bound.xyz.y);
}
if (pos->xyz.z > asfieldp->max_bound.xyz.z) {
pos->xyz.z = asfieldp->min_bound.xyz.z + (pos->xyz.z - asfieldp->max_bound.xyz.z);
}
// wrap on inner bound, check all 3 axes as needed, use of rand ok for multiplayer with send_asteroid_throw()
inner_bound_pos_fixup(asfieldp, pos);
}
/**
* Is asteroid targeted?
*
* @return !0 if this asteroid is a target for any ship, otherwise return 0
*/
static int asteroid_is_targeted(object *objp)
{
ship_obj *so;
object *ship_objp;
int asteroid_obj_index;
asteroid_obj_index=OBJ_INDEX(objp);
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
ship_objp = &Objects[so->objnum];
if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
if ( Ai_info[Ships[ship_objp->instance].ai_index].target_objnum == asteroid_obj_index ) {
return 1;
}
}
return 0;
}
/**
* Create an asteroid that will hit object *objp in delta_time seconds
*/
static void asteroid_aim_at_target(object *objp, object *asteroid_objp, float delta_time)
{
vec3d predicted_center_pos;
vec3d rand_vec;
float speed;
vm_vec_scale_add(&predicted_center_pos, &objp->pos, &objp->phys_info.vel, delta_time);
vm_vec_rand_vec_quick(&rand_vec);
vm_vec_scale_add2(&predicted_center_pos, &rand_vec, objp->radius/2.0f);
vm_vec_add2(&rand_vec, &objp->orient.vec.fvec);
if (vm_vec_mag_quick(&rand_vec) < 0.1f)
vm_vec_add2(&rand_vec, &objp->orient.vec.rvec);
vm_vec_normalize(&rand_vec);
auto asp = &Asteroids[asteroid_objp->instance];
speed = Asteroid_info[asp->asteroid_type].max_speed * (frand()/2.0f + 0.5f);
vm_vec_copy_scale(&asteroid_objp->phys_info.vel, &rand_vec, -speed);
asteroid_objp->phys_info.desired_vel = asteroid_objp->phys_info.vel;
vm_vec_scale_add(&asteroid_objp->pos, &predicted_center_pos, &asteroid_objp->phys_info.vel, -delta_time);
vm_vec_scale_add(&asteroid_objp->last_pos, &asteroid_objp->pos, &asteroid_objp->phys_info.vel, -flFrametime);
}
/**
* Sanitizes the Asteroid targets list removing ships that have died/become invalid etc
*/
static void sanitize_asteroid_targets_list() {
for (size_t i = 0; i < Asteroid_targets.size();) {
object* target_objp = &Objects[Asteroid_targets[i].objnum];
if (target_objp->type != OBJ_SHIP || target_objp->signature != Asteroid_targets[i].signature) {
Asteroid_targets[i] = Asteroid_targets.back();
Asteroid_targets.pop_back();
}
else // if we needed to cull we should not advance because we just moved a new asteroid target into this spot
i++;
}
}
/**
* Checks if an asteroid will be within the player's view
*
**/
bool asteroid_is_within_view(vec3d *pos, float range, bool range_override)
{
vec3d vec_to_asteroid;
// Distance and view cone for the position in relation to the player
float dist = vm_vec_normalized_dir(&vec_to_asteroid, pos, &Eye_position);
float dot = vm_vec_dot(&Eye_matrix.vec.fvec, &vec_to_asteroid);
// if asteroid is within view or far enough away then wrap
if (dot > cos(g3_get_hfov(Proj_fov))) {
if (range_override) {
return true;
} else {
if (dist < range) {
return true;
}
}
}
return false;
}
/**
* Call once per frame to maybe throw some asteroids at one or more ships.
*
*/
static void maybe_throw_asteroid()
{
Assertion(Asteroid_field.num_used_field_debris_types > 0, "maybe_throw_asteroid() called while num_used_field_debris_types was 0; this should never happen, get a coder!");
for (asteroid_target& target : Asteroid_targets) {
if (!timestamp_elapsed(target.throw_stamp))
continue;
object* target_objp = &Objects[target.objnum];
if (!asteroid_is_ship_inside_field(&Asteroid_field, &target_objp->pos, target_objp->radius))
continue;
// This should've been greater equal, but alas...
if (target.incoming_asteroids > The_mission.ai_profile->max_incoming_asteroids[Game_skill_level])
continue;
nprintf(("AI", "Incoming asteroids to %s: %i\n", Ships[target_objp->instance].ship_name, target.incoming_asteroids));
target.throw_stamp = _timestamp(1000 + 1200 * target.incoming_asteroids / (Game_skill_level+1));
int counter = Random::next(Asteroid_field.num_used_field_debris_types);
int subtype = -1;
for (int i = 0; i < NUM_ASTEROID_POFS; i++) {
if (Asteroid_field.field_debris_type[i] >= 0) {
if (counter == 0) {
subtype = i;
break;
}
else
counter--;
}
}
// this really shouldn't happen but just in case...
if (subtype < 0)
return;
object *objp = asteroid_create(&Asteroid_field, ASTEROID_TYPE_LARGE, subtype, Asteroid_field.enhanced_visibility_checks);
if (objp != nullptr) {
asteroid_aim_at_target(target_objp, objp, ASTEROID_MIN_COLLIDE_TIME + frand() * 20.0f);
// if asteroid is inside inner bound, kill it
if (asteroid_in_inner_bound(&Asteroid_field, &objp->pos, 0.0f)) {
objp->flags.set(Object::Object_Flags::Should_be_dead);
} else {
Asteroids[objp->instance].target_objnum = target.objnum;
target.incoming_asteroids++;
if ( MULTIPLAYER_MASTER ) {
send_asteroid_throw( objp );
}
}
}
}
}
/**
* Delete asteroid from Asteroid_used_list
*/
void asteroid_delete( object * obj )
{
int num;
asteroid *asp;
num = obj->instance;
Assert( Asteroids[num].objnum == OBJ_INDEX(obj));
asp = &Asteroids[num];
if (asp->model_instance_num >= 0)
model_delete_instance(asp->model_instance_num);
if (asp->target_objnum >= 0) {
for (asteroid_target& target : Asteroid_targets) {
if (asp->target_objnum == target.objnum)
target.incoming_asteroids--;
}
}
asp->flags = 0;
Num_asteroids--;
Assert(Num_asteroids >= 0);
asteroid_obj_list_remove( obj );
}
/**
* See if we should reposition the asteroid.
* Only reposition if oustide the bounding volume and the player isn't looking towards the asteroid.
*/
static void asteroid_maybe_reposition(object *objp, asteroid_field *asfieldp)
{
// passive field does not wrap if there is no gravity
if (IS_VEC_NULL(&The_mission.gravity) && (asfieldp->field_type == FT_PASSIVE)) {
return;
}
if (asteroid_should_wrap(objp, asfieldp)) {
// Generate a possible new position if we do end up wrapping, but don't move the asteroid yet
vec3d new_pos = objp->pos;
asteroid_wrap_pos(&new_pos, asfieldp);
bool wrap = false;
bool reverse = false;
// If asteroid is targeted then we cannot wrap because they player would see it and say "SHENANIGANS!"
// So we bail
if (asteroid_is_targeted(objp)) {
return;
}
// if asteroid is not within view...
if (!asteroid_is_within_view(&objp->pos, asfieldp->bound_rad, asfieldp->enhanced_visibility_checks)) {
// if asteroid new position is within view then reverse velocity, otherwise wrap
if (asteroid_is_within_view(&new_pos, (asfieldp->bound_rad * 1.3f), asfieldp->enhanced_visibility_checks)) {
// if the mission has gravity, then we can't reverse. So make sure gravity is null
if (IS_VEC_NULL(&The_mission.gravity)) {
reverse = true;
}
} else {
wrap = true;
}
}
// we can wrap, but we have too many asteroids, so destroy this one instead
if (wrap && (Num_asteroids > MAX_ASTEROIDS - 10)) {
objp->flags.set(Object::Object_Flags::Should_be_dead);
return;
}
// Reverse instead of wrap!
if (reverse) {
vm_vec_copy_scale(&objp->phys_info.vel, &objp->phys_info.vel, -1.0f);
objp->phys_info.desired_vel = objp->phys_info.vel;
Asteroids[objp->instance].target_objnum = -1;
return;
}
// We can wrap, so wrap!
if (wrap) {
objp->pos = new_pos;
asteroid *astp = &Asteroids[objp->instance];
if (asfieldp->field_type == FT_ACTIVE) {
// this doesnt count as a thrown asteroid anymore
for (asteroid_target &target : Asteroid_targets)
if (target.objnum == astp->target_objnum)
target.incoming_asteroids--;
astp->target_objnum = -1;
}
// update last pos (after vel is known)
vm_vec_scale_add(&objp->last_pos, &objp->pos, &objp->phys_info.vel, -flFrametime);
asteroid_update_collide(objp);
if (asfieldp->field_type == FT_ACTIVE) {
if (MULTIPLAYER_MASTER)
send_asteroid_throw(objp);
}
}
}
}
static void lerp(float *goal, float f1, float f2, float scale)
{
*goal = (f2 - f1) * scale + f1;
}
void asteroid_process_pre( object *objp )
{
if (Asteroids_enabled) {
// Make vel chase desired_vel
lerp(&objp->phys_info.vel.xyz.x, objp->phys_info.vel.xyz.x, objp->phys_info.desired_vel.xyz.x, flFrametime);
lerp(&objp->phys_info.vel.xyz.y, objp->phys_info.vel.xyz.y, objp->phys_info.desired_vel.xyz.y, flFrametime);
lerp(&objp->phys_info.vel.xyz.z, objp->phys_info.vel.xyz.z, objp->phys_info.desired_vel.xyz.z, flFrametime);
}
}
int asteroid_check_collision(object *pasteroid, object *other_obj, vec3d *hitpos, collision_info_struct *asteroid_hit_info, vec3d* hitnormal)
{
if (!Asteroids_enabled) {
return 0;
}
mc_info mc;
int num, asteroid_subtype;
Assert( pasteroid->type == OBJ_ASTEROID );
num = pasteroid->instance;
Assert( num >= 0 );
Assert( Asteroids[num].objnum == OBJ_INDEX(pasteroid));
asteroid_subtype = Asteroids[num].asteroid_subtype;
// asteroid_hit_info NULL -- asteroid-weapon collision
if ( asteroid_hit_info == NULL ) {
// asteroid weapon collision
Assert( other_obj->type == OBJ_WEAPON );
mc.model_instance_num = Asteroids[num].model_instance_num;
mc.model_num = Asteroid_info[Asteroids[num].asteroid_type].model_num[asteroid_subtype]; // Fill in the model to check
mc.orient = &pasteroid->orient; // The object's orient
mc.pos = &pasteroid->pos; // The object's position
mc.p0 = &other_obj->last_pos; // Point 1 of ray to check
mc.p1 = &other_obj->pos; // Point 2 of ray to check
mc.flags = (MC_CHECK_MODEL);
if (model_collide(&mc))
{
*hitpos = mc.hit_point_world;
if (hitnormal)
{
vec3d normal;
if (mc.model_instance_num >= 0)
model_instance_local_to_global_dir(&normal, &mc.hit_normal, mc.model_instance_num, mc.hit_submodel, mc.orient);
else
model_local_to_global_dir(&normal, &mc.hit_normal, mc.model_num, mc.hit_submodel, mc.orient);
*hitnormal = normal;
}
}
return mc.num_hits;
}
// asteroid ship collision -- use asteroid_hit_info to calculate physics
object *pship_obj = other_obj;
Assert( pship_obj->type == OBJ_SHIP );
object* heavy = asteroid_hit_info->heavy;
object* lighter = asteroid_hit_info->light;
object *heavy_obj = heavy;
object *light_obj = lighter;
vec3d zero, p0, p1;
vm_vec_zero( &zero );
vm_vec_sub( &p0, &lighter->last_pos, &heavy->last_pos );
vm_vec_sub( &p1, &lighter->pos, &heavy->pos );
mc.pos = &zero; // The object's position
mc.p0 = &p0; // Point 1 of ray to check
mc.p1 = &p1; // Point 2 of ray to check
// find the light object's position in the heavy object's reference frame at last frame and also in this frame.
vec3d p0_temp, p0_rotated;
// Collision detection from rotation enabled if at rotation is less than 30 degree in frame
// This should account for all ships
if ( (vm_vec_mag_squared( &heavy->phys_info.rotvel ) * flFrametime*flFrametime) < (PI*PI/36) ) {
// collide_rotate calculate (1) start position and (2) relative velocity
asteroid_hit_info->collide_rotate = true;
vm_vec_rotate( &p0_temp, &p0, &heavy->last_orient );
vm_vec_unrotate( &p0_rotated, &p0_temp, &heavy->orient );
mc.p0 = &p0_rotated; // Point 1 of ray to check
vm_vec_sub( &asteroid_hit_info->light_rel_vel, &p1, &p0_rotated );
vm_vec_scale( &asteroid_hit_info->light_rel_vel, 1/flFrametime );
// HACK - this applies to big ships warping in/out of asteroid fields - not sure what it does
if (vm_vec_mag(&asteroid_hit_info->light_rel_vel) > 300) {
asteroid_hit_info->collide_rotate = false;
vm_vec_sub( &asteroid_hit_info->light_rel_vel, &lighter->phys_info.vel, &heavy->phys_info.vel );
}
} else {
asteroid_hit_info->collide_rotate = false;
vm_vec_sub( &asteroid_hit_info->light_rel_vel, &lighter->phys_info.vel, &heavy->phys_info.vel );
}
int mc_ret_val = 0;
if ( asteroid_hit_info->heavy == pship_obj ) { // ship is heavier, so asteroid is sphere. Check sphere collision against ship poly model
mc.model_instance_num = Ships[pship_obj->instance].model_instance_num;
mc.model_num = Ship_info[Ships[pship_obj->instance].ship_info_index].model_num; // Fill in the model to check
mc.orient = &pship_obj->orient; // The object's orient
mc.radius = pasteroid->radius;
mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);
// copy important data
int orig_flags = mc.flags; // make a copy of start end positions of sphere in big ship RF
vec3d orig_p0 = *mc.p0;
vec3d orig_p1 = *mc.p1;
// first test against the sphere - if this fails then don't do any submodel tests
mc.flags = MC_ONLY_SPHERE | MC_CHECK_SPHERELINE;
if (model_collide(&mc)) {
// Set earliest hit time
asteroid_hit_info->hit_time = FLT_MAX;
auto pmi = model_get_instance(Ships[heavy_obj->instance].model_instance_num);
auto pm = model_get(pmi->model_num);
// Do collision the cool new way
if ( asteroid_hit_info->collide_rotate ) {
// We collide with the sphere, find the list of moving submodels and test one at a time
SCP_vector<int> submodel_vector;
model_get_moving_submodel_list(submodel_vector, heavy_obj);
// turn off all moving submodels, collide against only 1 at a time.
// turn off collision detection for all moving submodels
for (auto submodel: submodel_vector) {
pmi->submodel[submodel].collision_checked = true;
}
// Only check single submodel now, since children of moving submodels are handled as moving as well
mc.flags = orig_flags | MC_SUBMODEL;
// check each submodel in turn
for (auto submodel: submodel_vector) {
auto smi = &pmi->submodel[submodel];
// turn on just one submodel for collision test
smi->collision_checked = false;
// find the start and end positions of the sphere in submodel RF
model_instance_global_to_local_point(&p0, &light_obj->last_pos, pm, pmi, submodel, &heavy_obj->last_orient, &heavy_obj->last_pos, true);
model_instance_global_to_local_point(&p1, &light_obj->pos, pm, pmi, submodel, &heavy_obj->orient, &heavy_obj->pos);
mc.p0 = &p0;
mc.p1 = &p1;
mc.orient = &vmd_identity_matrix;
mc.submodel_num = submodel;
if ( model_collide(&mc) ) {
if ( mc.hit_dist < asteroid_hit_info->hit_time ) {
mc_ret_val = 1;
// set up asteroid_hit_info common
set_hit_struct_info(asteroid_hit_info, &mc, true);
// set up asteroid_hit_info for rotating submodel
if (!asteroid_hit_info->edge_hit) {
model_instance_local_to_global_dir(&asteroid_hit_info->collision_normal, &mc.hit_normal, pm, pmi, mc.hit_submodel, &heavy_obj->orient);
}
// find position in submodel RF of light object at collison
vec3d int_light_pos, diff;
vm_vec_sub(&diff, mc.p1, mc.p0);
vm_vec_scale_add(&int_light_pos, mc.p0, &diff, mc.hit_dist);
model_instance_local_to_global_point(&asteroid_hit_info->light_collision_cm_pos, &int_light_pos, pm, pmi, mc.hit_submodel, &heavy_obj->orient, &zero);
}
}
// Don't look at this submodel again
smi->collision_checked = true;
}
}
// Now complete base model collision checks that do not take into account rotating submodels.
mc.flags = orig_flags;
mc.p0 = &orig_p0;
mc.p1 = &orig_p1;
mc.orient = &heavy_obj->orient;
// usual ship_ship collision test
if ( model_collide(&mc) ) {
// check if this is the earliest hit
if (mc.hit_dist < asteroid_hit_info->hit_time) {
mc_ret_val = 1;
set_hit_struct_info(asteroid_hit_info, &mc, false);
// get collision normal if not edge hit
if (!asteroid_hit_info->edge_hit) {
model_instance_local_to_global_dir(&asteroid_hit_info->collision_normal, &mc.hit_normal, pm, pmi, mc.hit_submodel, &heavy_obj->orient);
}
// find position in submodel RF of light object at collison
vec3d diff;
vm_vec_sub(&diff, mc.p1, mc.p0);
vm_vec_scale_add(&asteroid_hit_info->light_collision_cm_pos, mc.p0, &diff, mc.hit_dist);
}
}
}
} else {
// Asteroid is heavier obj
mc.model_instance_num = Asteroids[num].model_instance_num;
mc.model_num = Asteroid_info[Asteroids[num].asteroid_type].model_num[asteroid_subtype]; // Fill in the model to check
mc.orient = &pasteroid->orient; // The object's orient
mc.radius = model_get_core_radius(Ship_info[Ships[pship_obj->instance].ship_info_index].model_num);
// check for collision between asteroid model and ship sphere
mc.flags = (MC_CHECK_MODEL | MC_CHECK_SPHERELINE);
mc_ret_val = model_collide(&mc);
if (mc_ret_val) {
set_hit_struct_info(asteroid_hit_info, &mc, false);
// set normal if not edge hit
if ( !asteroid_hit_info->edge_hit ) {
vm_vec_unrotate(&asteroid_hit_info->collision_normal, &mc.hit_normal, &heavy->orient);
}
// find position in submodel RF of light object at collison
vec3d diff;
vm_vec_sub(&diff, mc.p1, mc.p0);
vm_vec_scale_add(&asteroid_hit_info->light_collision_cm_pos, mc.p0, &diff, mc.hit_dist);
}
}
// check if the hit point is beyond the clip plane if the ship is warping.
if (mc_ret_val) {
WarpEffect* warp_effect = nullptr;
ship* shipp = &Ships[pship_obj->instance];
// this is extremely confusing but mc.hit_point_world isn't actually in world coords
// everything above was calculated relative to the heavy's position
vec3d actual_world_hit_pos = mc.hit_point_world + heavy_obj->pos;
if ((shipp->is_arriving()) && (shipp->warpin_effect != nullptr))
warp_effect = shipp->warpin_effect;
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr))
warp_effect = shipp->warpout_effect;
if (warp_effect != nullptr && point_is_clipped_by_warp(&actual_world_hit_pos, warp_effect))
mc_ret_val = 0;
}
if ( mc_ret_val ) {
// SET PHYSICS PARAMETERS
// already have (hitpos - heavy) and light_cm_pos
// get r_heavy and r_light
asteroid_hit_info->r_heavy = asteroid_hit_info->hit_pos;
vm_vec_sub(&asteroid_hit_info->r_light, &asteroid_hit_info->hit_pos, &asteroid_hit_info->light_collision_cm_pos);
// set normal for edge hit
if ( asteroid_hit_info->edge_hit ) {
vm_vec_copy_normalize(&asteroid_hit_info->collision_normal, &asteroid_hit_info->r_light);
vm_vec_negate(&asteroid_hit_info->collision_normal);
}
// get world hitpos
vm_vec_add(hitpos, &asteroid_hit_info->heavy->pos, &asteroid_hit_info->r_heavy);
return 1;
} else {
// no hit
return 0;
}
}
void asteroid_render(object * obj, model_draw_list *scene)
{
if (Asteroids_enabled) {
int num;
asteroid *asp;
num = obj->instance;
Assert((num >= 0) && (num < MAX_ASTEROIDS));
asp = &Asteroids[num];
Assert( asp->flags & AF_USED );
model_clear_instance( Asteroid_info[asp->asteroid_type].model_num[asp->asteroid_subtype]);
model_render_params render_info;
render_info.set_object_number( OBJ_INDEX(obj) );
render_info.set_flags(MR_IS_ASTEROID);
model_render_queue(&render_info, scene, Asteroid_info[asp->asteroid_type].model_num[asp->asteroid_subtype], &obj->orient, &obj->pos); // Replace MR_NORMAL with 0x07 for big yellow blobs
}
}
/**
* Create a normalized vector generally in the direction from *hitpos to other_obj->pos
*/
static void asc_get_relvec(vec3d *relvec, object *other_obj, vec3d *hitpos)
{
vec3d tvec, rand_vec;
int count = 0;
vm_vec_normalized_dir(&tvec, &other_obj->pos, hitpos);
// Try up to three times to get a good vector.
while (count++ < 3) {
vm_vec_rand_vec_quick(&rand_vec);
vm_vec_add(relvec, &tvec, &rand_vec);
float mag = vm_vec_mag_quick(relvec);
if ((mag > 0.2f) && (mag < 1.7f))
break;
}
vm_vec_normalize_quick(relvec);
}
/**
* Return multiplier on asteroid radius for fireball
*/
static float asteroid_get_fireball_scale_multiplier(int num)
{
if (Asteroids[num].flags & AF_USED) {
asteroid_info *asip = &Asteroid_info[Asteroids[num].asteroid_type];
if (asip->fireball_radius_multiplier >= 0) {
return asip->fireball_radius_multiplier;
} else {
switch(Asteroids[num].asteroid_type) {
case ASTEROID_TYPE_LARGE:
return 1.5f;
break;
default:
return 1.0f;
break;
}
}
}
Int3(); // this should not happen. asteroid should be used.
return 1.0f;
}
/**
* Create asteroid explosion
* @return expected time for explosion anim to last, in seconds
*/
static float asteroid_create_explosion(object *objp)
{
int fireball_objnum;
float explosion_life, fireball_scale_multiplier;
asteroid_info *asip = &Asteroid_info[Asteroids[objp->instance].asteroid_type];
int fireball_type = fireball_asteroid_explosion_type(asip);
if (fireball_type < 0) {
fireball_type = FIREBALL_ASTEROID;
}
if (fireball_type >= static_cast<int>(Fireball_info.size())) {
Warning(LOCATION, "Invalid fireball type %i specified for an asteroid; only " SIZE_T_ARG " fireball types are defined.", fireball_type, Fireball_info.size());
return 0;
}
fireball_scale_multiplier = asteroid_get_fireball_scale_multiplier(objp->instance);
fireball_objnum = fireball_create( &objp->pos, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(objp), objp->radius*fireball_scale_multiplier, false, &objp->phys_info.vel );
if ( fireball_objnum > -1 ) {
explosion_life = fireball_lifeleft(&Objects[fireball_objnum]);
} else {
explosion_life = 0.0f;
}
return explosion_life;
}
/**
* Play sound when asteroid explodes
*/
static void asteroid_explode_sound(object *objp, int type, int play_loud)
{
gamesnd_id sound_index;
float range_factor = 1.0f; // how many times sound should traver farther than normal
if (type % NUM_ASTEROID_SIZES <= 1)
{
sound_index = gamesnd_id(GameSounds::ASTEROID_EXPLODE_SMALL);
range_factor = 5.0;
}
else
{
sound_index = gamesnd_id(GameSounds::ASTEROID_EXPLODE_LARGE);
range_factor = 10.0f;
}
Assert(sound_index.isValid());
if ( !play_loud ) {
range_factor = 1.0f;
}
snd_play_3d( gamesnd_get_game_sound(sound_index), &objp->pos, &Eye_position, objp->radius, NULL, 0, 1.0f, SND_PRIORITY_MUST_PLAY, NULL, range_factor );
}
/**
* Do the area effect for an asteroid exploding
*
* @param asteroid_objp object pointer to asteroid causing explosion
*/
static void asteroid_do_area_effect(object *asteroid_objp)
{
object *ship_objp;
float damage, blast;
ship_obj *so;
asteroid *asp;
asteroid_info *asip;
asp = &Asteroids[asteroid_objp->instance];
asip = &Asteroid_info[asp->asteroid_type];
if ( asip->damage <= 0 ) { // do a quick out if there is no damage to apply
return;
}
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
ship_objp = &Objects[so->objnum];
if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
// don't blast no-collide or navbuoys
if ( !ship_objp->flags[Object::Object_Flags::Collides] || ship_get_SIF(ship_objp->instance)[Ship::Info_Flags::Navbuoy] ) {
continue;
}
if ( weapon_area_calc_damage(ship_objp, &asteroid_objp->pos, asip->inner_rad, asip->outer_rad, asip->blast, asip->damage, &blast, &damage, asip->outer_rad) == -1 )
continue;
ship_apply_global_damage(ship_objp, asteroid_objp, &asteroid_objp->pos, damage, asip->damage_type_idx);
weapon_area_apply_blast(nullptr, ship_objp, &asteroid_objp->pos, blast, false);
} // end for
}
/**
* Upon asteroid asteroid_obj being hit. Apply damage and maybe make it break into smaller asteroids.
*
* @param pasteroid_obj pointer to asteroid object getting hit
* @param other_obj object that hit asteroid, can be NULL if asteroid hit by area effect
* @param hitpos world position asteroid was hit, can be NULL if hit by area effect
* @param damage amount of damage to apply to asteroid
*/
void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, float damage, vec3d* force )
{
float explosion_life;
asteroid *asp;
asp = &Asteroids[pasteroid_obj->instance];
if (pasteroid_obj->flags[Object::Object_Flags::Should_be_dead]){
return;
}
if ( MULTIPLAYER_MASTER ){
send_asteroid_hit( pasteroid_obj, other_obj, hitpos, damage, force );
}
if (hitpos && force && The_mission.ai_profile->flags[AI::Profile_Flags::Whackable_asteroids]) {
vec3d rel_hit_pos = *hitpos - pasteroid_obj->pos;
physics_calculate_and_apply_whack(force, &rel_hit_pos, &pasteroid_obj->phys_info, &pasteroid_obj->orient, &pasteroid_obj->phys_info.I_body_inv);
pasteroid_obj->phys_info.desired_vel = pasteroid_obj->phys_info.vel;
}
pasteroid_obj->hull_strength -= damage;
if (pasteroid_obj->hull_strength < 0.0f) {
if ( !asp->final_death_time.isValid() ) {
int play_loud_collision = 0;
explosion_life = asteroid_create_explosion(pasteroid_obj);
asteroid_explode_sound(pasteroid_obj, asp->asteroid_type, play_loud_collision);
asteroid_do_area_effect(pasteroid_obj);
asp->final_death_time = _timestamp( fl2i(explosion_life*MILLISECONDS_PER_SECOND)/5 ); // Wait till 30% of vclip time before breaking the asteroid up.
if ( hitpos ) {
asp->death_hit_pos = *hitpos;
} else {
asp->death_hit_pos = pasteroid_obj->pos;
// randomize hit pos a bit, otherwise we will get a NULL vector when trying to find direction to toss child asteroids
vec3d rand_vec;
vm_vec_rand_vec_quick(&rand_vec);
vm_vec_add2(&asp->death_hit_pos, &rand_vec);
}
}
} else if ( other_obj ) {
if ( other_obj->type == OBJ_WEAPON ) {
weapon_info *wip;
wip = &Weapon_info[Weapons[other_obj->instance].weapon_info_index];
// If the weapon didn't play any impact animation, play custom asteroid impact animation
if (!wip->impact_weapon_expl_effect.isValid()) {
particle::create( hitpos, &vmd_zero_vector, 0.0f, Asteroid_impact_explosion_radius, particle::PARTICLE_BITMAP, Asteroid_impact_explosion_ani );
}
}
}
// evaluate any relevant player scoring implications
scoring_eval_hit(pasteroid_obj,other_obj);
}
/**
* De-init asteroids, called from ::game_level_close()
*/
void asteroid_level_close()
{
for (int i=0; i<MAX_ASTEROIDS; i++) {
if (Asteroids[i].flags & AF_USED) {
Asteroids[i].flags &= ~AF_USED;
Assert(Asteroids[i].objnum >=0 && Asteroids[i].objnum < MAX_OBJECTS);
Objects[Asteroids[i].objnum].flags.set(Object::Object_Flags::Should_be_dead);
}
}
//when a level is closed, all models are cleared, so let's make sure that
//is tracked for asteroids as well -Mjn
for (int i = 0; i < (int)Asteroid_info.size(); i++) {
for (int j = 0; j < NUM_ASTEROID_POFS; j++) {
Asteroid_info[i].model_num[j] = -1;
}
}
Asteroid_field.num_initial_asteroids=0;
}
DCF_BOOL2(asteroids, Asteroids_enabled, "enables or disables asteroids", "Usage: asteroids [bool]\nTurns asteroid system on/off. If nothing passed, then toggles it.\n");
/**
* Return the number of active asteroids
*/
int asteroid_count()
{
return Num_asteroids;
}
/**
* See if asteroid should split up.
* We delay splitting up to allow the explosion animation to play for a bit.
*/
static void asteroid_maybe_break_up(object *pasteroid_obj)
{
asteroid *asp;
asteroid_info *asip;
asp = &Asteroids[pasteroid_obj->instance];
asip = &Asteroid_info[asp->asteroid_type];
if ( timestamp_elapsed(asp->final_death_time) ) {
vec3d relvec, vfh, tvec;
bool skip = false;
bool hooked = scripting::hooks::OnDeath->isActive();
if (hooked) {
skip = scripting::hooks::OnDeath->isOverride(scripting::hooks::ObjectDeathConditions{ pasteroid_obj },
scripting::hook_param_list(scripting::hook_param("Self", 'o', pasteroid_obj)));
}
if (!skip)
{
pasteroid_obj->flags.set(Object::Object_Flags::Should_be_dead);
// multiplayer clients won't go through the following code. asteroid_sub_create will send
// a create packet to the client in the above named function
// if the second condition isn't true it's just debris, and debris doesn't break up
if ( !MULTIPLAYER_CLIENT && (asp->asteroid_type <= ASTEROID_TYPE_LARGE)) {
if (asip->split_info.empty()) {
switch (asp->asteroid_type) {
case ASTEROID_TYPE_SMALL:
break;
case ASTEROID_TYPE_MEDIUM:
asc_get_relvec(&relvec, pasteroid_obj, &asp->death_hit_pos);
asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &relvec);
vm_vec_normalized_dir(&vfh, &pasteroid_obj->pos, &asp->death_hit_pos);
vm_vec_copy_scale(&tvec, &vfh, 2.0f);
vm_vec_sub2(&tvec, &relvec);
asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &tvec);
break;
case ASTEROID_TYPE_LARGE:
asc_get_relvec(&relvec, pasteroid_obj, &asp->death_hit_pos);
asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_MEDIUM, &relvec);
vm_vec_normalized_dir(&vfh, &pasteroid_obj->pos, &asp->death_hit_pos);
vm_vec_copy_scale(&tvec, &vfh, 2.0f);
vm_vec_sub2(&tvec, &relvec);
asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_MEDIUM, &tvec);
while (frand() > 0.6f) {
vec3d rvec, tvec2;
vm_vec_rand_vec_quick(&rvec);
vm_vec_scale_add(&tvec2, &vfh, &rvec, 0.75f);
asteroid_sub_create(pasteroid_obj, ASTEROID_TYPE_SMALL, &tvec2);
}
break;
default: // this isn't going to happen.. really
break;
}
} else {
SCP_vector<int> roids_to_create;
SCP_vector<asteroid_split_info>::iterator split;
for (split = asip->split_info.begin(); split != asip->split_info.end(); ++split) {
int num_roids = split->min;
int num_roids_var = split->max - split->min;
if (num_roids_var > 0)
num_roids += Random::next(num_roids_var);
if (num_roids > 0)
for (int i=0; i<num_roids; i++)
roids_to_create.push_back(split->asteroid_type);
}
random_shuffle(roids_to_create.begin(), roids_to_create.end());
size_t total_roids = roids_to_create.size();
for (size_t i = 0; i < total_roids; i++) {
vec3d dir_vec,final_vec;
vec3d parent_vel,hit_rel_vec;
// The roid directions are picked from the so-called
// "golden section spiral" to prevent them from
// clustering and thus clipping
float inc = PI * (3.0f - sqrt(5.0f));
float offset = 2.0f / total_roids;
float y = i * offset - 1 + (offset / 2);
float r = sqrt(1.0f - y*y);
float phi = i * inc;
dir_vec.xyz.x = cosf(phi)*r;
dir_vec.xyz.y = sinf(phi)*r;
dir_vec.xyz.z = y;
// Randomize the direction a bit
vec3d tempv = dir_vec;
vm_vec_random_cone(&dir_vec, &tempv, (360.0f / total_roids / 2));
// Make the roid inherit half of parent's directional movement
if (!IS_VEC_NULL(&pasteroid_obj->phys_info.vel)) {
vm_vec_copy_normalize(&parent_vel, &pasteroid_obj->phys_info.vel);
} else {
vm_vec_rand_vec_quick(&parent_vel);
}
vm_vec_scale(&parent_vel, 0.5f);
// Make the hit position affect the direction, but only a little
vm_vec_sub(&hit_rel_vec, &pasteroid_obj->pos, &asp->death_hit_pos);
vm_vec_normalize(&hit_rel_vec);
vm_vec_scale(&hit_rel_vec, 0.25f);
vm_vec_avg3(&final_vec, &parent_vel, &hit_rel_vec, &dir_vec);
vm_vec_normalize(&final_vec);
asteroid_sub_create(pasteroid_obj, roids_to_create[i], &final_vec);
}
}
}
asp->final_death_time = TIMESTAMP::invalid();
}
if (hooked) {
scripting::hooks::OnDeath->run(scripting::hooks::ObjectDeathConditions{ pasteroid_obj },
scripting::hook_param_list(scripting::hook_param("Self", 'o', pasteroid_obj)));
}
if (scripting::hooks::OnAsteroidDeath->isActive()) {
scripting::hooks::OnAsteroidDeath->run(
scripting::hook_param_list(
scripting::hook_param("Asteroid", 'o', pasteroid_obj),
scripting::hook_param("Hitpos", 'o', asp->death_hit_pos)));
}
}
}
static void asteroid_test_collide(object *pasteroid_obj, object *pship_obj, mc_info *mc, bool lazy = false)
{
float asteroid_ray_dist;
vec3d asteroid_fvec, terminus;
// See if ray from asteroid intersects bounding box of escort ship
asteroid_ray_dist = vm_vec_mag_quick(&pasteroid_obj->phys_info.desired_vel) * ASTEROID_MIN_COLLIDE_TIME;
asteroid_fvec = pasteroid_obj->phys_info.desired_vel;
if(IS_VEC_NULL_SQ_SAFE(&asteroid_fvec)){
terminus = pasteroid_obj->pos;
} else {
vm_vec_normalize(&asteroid_fvec);
vm_vec_scale_add(&terminus, &pasteroid_obj->pos, &asteroid_fvec, asteroid_ray_dist);
}
Assert(pship_obj->type == OBJ_SHIP);
mc->model_instance_num = Ships[pship_obj->instance].model_instance_num;
mc->model_num = Ship_info[Ships[pship_obj->instance].ship_info_index].model_num; // Fill in the model to check
mc->orient = &pship_obj->orient; // The object's orientation
mc->pos = &pship_obj->pos; // The object's position
mc->p0 = &pasteroid_obj->pos; // Point 1 of ray to check
mc->p1 = &terminus; // Point 2 of ray to check
if (lazy) {
mc->flags = MC_CHECK_MODEL | MC_ONLY_BOUND_BOX;
} else {
mc->flags = MC_CHECK_MODEL | MC_CHECK_SPHERELINE;
}
mc->radius = pasteroid_obj->radius;
model_collide(mc);
// PVS-Studio says that mc->p1 will become invalid... on the one hand, it will; on the other hand, we won't use it again; on the *other* other hand, we should keep things proper in case of future changes
mc->p1 = NULL;
}
/**
* Test if asteroid will collide with escort ship within ASTEROID_MIN_COLLIDE_TIME seconds
*
* @return !0 if the asteroid will collide with the escort
*/
static int asteroid_will_collide(object *pasteroid_obj, object *escort_objp)
{
mc_info mc;
asteroid_test_collide(pasteroid_obj, escort_objp, &mc);
if ( !mc.num_hits ) {
return 0;
}
return 1;
}
/**
* Warn if asteroid on collision path with ship
* @return !0 if we should warn about asteroid hitting ship, otherwise return 0
*/
static int asteroid_valid_ship_to_warn_collide(ship *shipp)
{
if ( !(Ship_info[shipp->ship_info_index].is_big_or_huge()) ) {
return 0;
}
if ( shipp->flags[Ship::Ship_Flags::Dying] || shipp->flags[Ship::Ship_Flags::Depart_warp] ) {
return 0;
}
// Goober5000 used to be if teams were unequal and player was not traitor, but this works for allies not on your team
if ( iff_x_attacks_y(Player_ship->team, shipp->team) ) {
return 0;
}
return 1;
}
/**
* See if asteroid will collide with a large ship on the escort list in the next
* ASTEROID_MIN_COLLIDE_TIME seconds.
*/
static void asteroid_update_collide_flag(object *asteroid_objp)
{
int i, num_escorts, escort_objnum, will_collide=0;
ship *escort_shipp;
asteroid *asp;
asp = &Asteroids[asteroid_objp->instance];
asp->collide_objnum = -1;
asp->collide_objsig = -1;
// multiplayer dogfight
if(MULTI_DOGFIGHT){
return;
}
num_escorts = hud_escort_num_ships_on_list();
if ( num_escorts <= 0 ) {
return;
}
for ( i = 0; i < num_escorts; i++ ) {
escort_objnum = hud_escort_return_objnum(i);
if ( escort_objnum >= 0 ) {
escort_shipp = &Ships[Objects[escort_objnum].instance];
if ( asteroid_valid_ship_to_warn_collide(escort_shipp) ) {
will_collide = asteroid_will_collide(asteroid_objp, &Objects[escort_objnum]);
if ( will_collide ) {
asp->collide_objnum = escort_objnum;
asp->collide_objsig = Objects[escort_objnum].signature;
}
}
}
}
}
/**
* Ensure that the collide objnum for the asteroid is still valid
*/
static void asteroid_verify_collide_objnum(asteroid *asp)
{
if ( asp->collide_objnum >= 0 ) {
if ( Objects[asp->collide_objnum].signature != asp->collide_objsig ) {
asp->collide_objnum = -1;
asp->collide_objsig = -1;
}
}
}
void asteroid_process_post(object * obj)
{
if (Asteroids_enabled) {
int num;
num = obj->instance;
asteroid *asp = &Asteroids[num];
//Passive fields might wrap if there's gravity so do
//this for all fields now-Mjn
if ( timestamp_elapsed(asp->check_for_wrap) ) {
asteroid_maybe_reposition(obj, &Asteroid_field);
asp->check_for_wrap = _timestamp(ASTEROID_CHECK_WRAP_TIMESTAMP);
}
asteroid_verify_collide_objnum(asp);
if ( timestamp_elapsed(asp->check_for_collide) ) {
asteroid_update_collide_flag(obj);
asp->check_for_collide = _timestamp(ASTEROID_UPDATE_COLLIDE_TIMESTAMP);
}
asteroid_maybe_break_up(obj);
}
}
/**
* Find the object number of the object the asteroid is about to impact
* @return the object number that the asteroid is about to impact
*/
int asteroid_collide_objnum(object *asteroid_objp)
{
return Asteroids[asteroid_objp->instance].collide_objnum;
}
/**
* Find the time until asteroid will collide with object
* @return the time until the asteroid will impact its collide_objnum
*/
float asteroid_time_to_impact(object *asteroid_objp)
{
float time=-1.0f, total_dist, speed;
asteroid *asp;
mc_info mc;
asp = &Asteroids[asteroid_objp->instance];
if ( asp->collide_objnum < 0 ) {
return time;
}
asteroid_test_collide(asteroid_objp, &Objects[asp->collide_objnum], &mc, true);
if ( mc.num_hits ) {
total_dist = vm_vec_dist(&mc.hit_point_world, &asteroid_objp->pos) - asteroid_objp->radius;
if ( total_dist < 0 ) {
total_dist = 0.0f;
}
speed = vm_vec_mag(&asteroid_objp->phys_info.vel);
time = total_dist/speed;
}
return time;
}
static SCP_string setup_display_name(SCP_string name)
{
size_t split = std::string::npos;
char thisSpecies[NAME_LENGTH];
for (auto& species : Species_info) {
if (name.compare(0, strlen(species.species_name), species.species_name) == 0) {
split = strlen(species.species_name);
strcpy_s(thisSpecies, species.species_name);
break;
}
}
if (split == std::string::npos)
return XSTR("Asteroid", 431);
char newName[NAME_LENGTH];
strcpy_s(newName, thisSpecies);
strcat(newName, " ");
strcat(newName, XSTR("debris", 348));
return newName;
}
// Temporary list used to grab parsed data. After verification, the list is cleared -Mjn
static SCP_vector<asteroid_info> asteroid_list;
// Gets the asteroid pointer for asteroid_list NOT Asteroid_info. Use only during parsing! - Mjn
static asteroid_info* get_asteroid_pointer(const char* asteroid_name)
{
for (int i = 0; i < (int)asteroid_list.size(); i++) {
if (!stricmp(asteroid_name, asteroid_list[i].name)) {
return &asteroid_list[i];
}
}
// Didn't find anything.
return nullptr;
}
/**
* Read in a single asteroid section from asteroid.tbl
*/
static void asteroid_parse_section()
{
bool create_if_not_found = true;
asteroid_info asteroid_t;
required_string("$Name:");
stuff_string(asteroid_t.name, F_NAME, NAME_LENGTH);
if (optional_string("+nocreate")) {
if (!Parsing_modular_table) {
Warning(LOCATION, "+nocreate flag used for asteroid in non-modular table\n");
}
create_if_not_found = false;
}
// Does this asteroid exist already?
// If so, load this new info into it
asteroid_info* asteroid_p = get_asteroid_pointer(asteroid_t.name);
if (asteroid_p != nullptr) {
if (!Parsing_modular_table) {
error_display(1,
"Error: Asteroid %s already exists. All asteroid names must be unique.",
asteroid_t.name);
}
} else {
// Don't create asteroid if it has +nocreate and is in a modular table.
if (!create_if_not_found && Parsing_modular_table) {
if (!skip_to_start_of_string_either("$Name:", "#end")) {
error_display(1, "Missing [#end] or [$Name] after asteroid %s", asteroid_t.name);
}
return;
}
asteroid_list.push_back(asteroid_t);
asteroid_p = &asteroid_list[asteroid_list.size() - 1];
}
if (optional_string("$Display Name:")) {
stuff_string(asteroid_p->display_name, F_NAME);
}
// setup display name only if the string is empty - Mjn
if (asteroid_p->display_name.empty()) {
asteroid_p->display_name = setup_display_name(asteroid_p->name);
}
if (optional_string("$Type:")) {
stuff_int(&asteroid_p->type);
}
switch (asteroid_p->type) {
case ASTEROID_TYPE_SMALL:
case ASTEROID_TYPE_MEDIUM:
case ASTEROID_TYPE_LARGE:
case ASTEROID_TYPE_DEBRIS:
break;
default:
error_display(0, "Unknown asteroid type [%i] found for asteroid %s. Assuming debris", asteroid_p->type, asteroid_p->name);
asteroid_p->type = ASTEROID_TYPE_DEBRIS;
}
if (optional_string("$POF file1:")) {
stuff_string(asteroid_p->pof_files[0], F_NAME, MAX_FILENAME_LEN);
}
if (optional_string("$POF file2:")) {
stuff_string(asteroid_p->pof_files[1], F_NAME, MAX_FILENAME_LEN);
}
if (optional_string("$POF file3:")) {
stuff_string(asteroid_p->pof_files[2], F_NAME, MAX_FILENAME_LEN);
}
if (optional_string("$Detail distance:")) {
asteroid_p->num_detail_levels = (int)stuff_int_list(asteroid_p->detail_distance, MAX_ASTEROID_DETAIL_LEVELS, RAW_INTEGER_TYPE);
}
if (optional_string("$Max Speed:")) {
stuff_float(&asteroid_p->max_speed);
}
if (optional_string("$Rotational Velocity Multiplier:")) {
stuff_float(&asteroid_p->rotational_vel_multiplier);
}
if(optional_string("$Damage Type:")) {
char buf[NAME_LENGTH];
stuff_string(buf, F_NAME, NAME_LENGTH);
asteroid_p->damage_type_idx_sav = damage_type_add(buf);
asteroid_p->damage_type_idx = asteroid_p->damage_type_idx_sav;
}
if(optional_string("$Explosion Animations:")){
stuff_fireball_index_list(asteroid_p->explosion_bitmap_anims, asteroid_p->name);
}
if (optional_string("$Explosion Radius Mult:")) {
stuff_float(&asteroid_p->fireball_radius_multiplier);
}
if (optional_string("$Expl inner rad:")){
stuff_float(&asteroid_p->inner_rad);
}
if (optional_string("$Expl outer rad:")) {
stuff_float(&asteroid_p->outer_rad);
}
if (optional_string("$Expl damage:")){
stuff_float(&asteroid_p->damage);
}
if (optional_string("$Expl blast:")){
stuff_float(&asteroid_p->blast);
}
if (optional_string("$Hitpoints:")){
stuff_float(&asteroid_p->initial_asteroid_strength);
}
while(optional_string("$Split:")) {
int split_index;
stuff_int(&split_index);
if (split_index >=0 && split_index < (int)asteroid_list.size()) {
asteroid_split_info new_split;
new_split.asteroid_type = split_index;
strcpy(new_split.name, asteroid_list[split_index].name);
if(optional_string("+Min:"))
stuff_int(&new_split.min);
else
new_split.min = 0;
if(optional_string("+Max:"))
stuff_int(&new_split.max);
else
new_split.max = 0;
asteroid_p->split_info.push_back(new_split);
} else
Warning(LOCATION, "Invalid asteroid reference %i used for $Split in asteroids table, ignoring.", split_index);
}
// provide a way to define splits by name instead of index, verified after all parsing is done - Mjn
while (optional_string("$Split name:")) {
char split_name[NAME_LENGTH];
stuff_string(split_name, F_NAME, NAME_LENGTH);
asteroid_split_info new_split;
new_split.asteroid_type = -1;
strcpy(new_split.name, split_name);
if (optional_string("+Min:"))
stuff_int(&new_split.min);
else
new_split.min = 0;
if (optional_string("+Max:"))
stuff_int(&new_split.max);
else
new_split.max = 0;
asteroid_p->split_info.push_back(new_split);
}
if (optional_string("$Spawn Weight:")) {
if (asteroid_p->type == ASTEROID_TYPE_DEBRIS) {
stuff_float(&asteroid_p->spawn_weight);
if (asteroid_p->spawn_weight <= 0.0f) {
Warning(LOCATION, "Spawn weight for asteroid '%s' must be greater than 0", asteroid_p->name);
asteroid_p->spawn_weight = 1.0f;
}
} else {
Warning(LOCATION, "Spawn weight defined for non-debris type asteroid '%s', but can only be defined for debris types.", asteroid_p->name);
}
}
if (optional_string("$Gravity Const:")) {
stuff_float(&asteroid_p->gravity_const);
}
}
/**
Read in data from asteroid.tbl into Asteroid_info[] array.
After this function is complete, the Asteroid_info[] array
will have at least 3 generic asteroids plus whatever else
special ones were added in the table. The generic asteroids are
a set of small, medium, and large asteroids in that exact order.
Note that by saying "asteroid" this code is talking about
the asteroids and the debris that make up asteroid fields
and debris fields. Which means that these debris have nothing
to do with the debris of ships that explode, however these
are the same debris and asteroids that get flung at a ship
that is being protected.
*/
static void asteroid_parse_tbl(const char* filename)
{
try
{
read_file_text(filename, CF_TYPE_TABLES);
reset_parse();
required_string("#Asteroid Types");
// parse each asteroid
while (required_string_either("#End", "$Name:"))
{
asteroid_parse_section();
}
required_string("#End");
if (optional_string("$Impact Explosion:")) {
char impact_ani_file[MAX_FILENAME_LEN];
stuff_string(impact_ani_file, F_NAME, MAX_FILENAME_LEN);
if (VALID_FNAME(impact_ani_file)) {
int num_frames;
Asteroid_impact_explosion_ani = bm_load_animation(impact_ani_file, &num_frames, nullptr, nullptr, nullptr, true);
}
}
if (optional_string("$Impact Explosion Radius:")) {
stuff_float(&Asteroid_impact_explosion_radius);
}
if (optional_string("$Briefing Icon Closeup Model:")) {
stuff_string(Asteroid_icon_closeup_model, F_NAME, NAME_LENGTH);
}
if (optional_string("$Briefing Icon Closeup Position:")) {
stuff_vec3d(&Asteroid_icon_closeup_position);
}
if (optional_string("$Briefing Icon Closeup Zoom:")) {
stuff_float(&Asteroid_icon_closeup_zoom);
}
}
catch (const parse::ParseException& e)
{
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
return;
}
}
// Gets the index of an asteroid by name from Asteroid_info
int get_asteroid_index(const char* asteroid_name)
{
for (int i = 0; i < (int)Asteroid_info.size(); i++) {
if (!stricmp(asteroid_name, Asteroid_info[i].name)) {
return i;
}
}
return -1;
}
static void verify_asteroid_splits()
{
for (int i = 0; i < (int)Asteroid_info.size(); i++) {
SCP_vector<asteroid_split_info> splits_t;
for (int j = 0; j < (int)Asteroid_info[i].split_info.size(); j++) {
int asteroid_index = get_asteroid_index(Asteroid_info[i].split_info[j].name);
if (asteroid_index >= 0) {
Asteroid_info[i].split_info[j].asteroid_type = asteroid_index;
splits_t.push_back(Asteroid_info[i].split_info[j]);
} else {
Warning(LOCATION,
"Unknown asteroid %s defined as split type for asteroid %s, removing!",
Asteroid_info[i].split_info[j].name,
Asteroid_info[i].name);
}
}
// Replace splits with only valid splits
Asteroid_info[i].split_info = splits_t;
}
}
static void verify_asteroid_list()
{
SCP_string asteroid_size[NUM_ASTEROID_SIZES] = {"Small", "Medium", "Large"};
float asteroid_spawn_weight[NUM_ASTEROID_SIZES] = {SMALL_DEBRIS_WEIGHT, MEDIUM_DEBRIS_WEIGHT, LARGE_DEBRIS_WEIGHT};
// Set the three asteroid sizes
for (int i = 0; i < NUM_ASTEROID_SIZES; i++) {
bool found = false;
for (int j = 0; j < (int)asteroid_list.size(); j++) {
if (asteroid_list[j].type == i) {
mprintf(("%s asteroid parsed with name %s\n", asteroid_size[i].c_str(), asteroid_list[j].name));
asteroid_list[j].spawn_weight = asteroid_spawn_weight[i]; //Be absolutely sure we're using the right spawn weight for asteroids
Asteroid_info.push_back(asteroid_list[j]);
asteroid_list.erase(asteroid_list.begin() + j);
found = true;
break;
}
}
if (found)
continue;
//Left this as a log print instead of a Warning because of retail-Mjn
mprintf(("%s asteroid not found. Using asteroid %s\n", asteroid_size[i].c_str(), asteroid_list[0].name));
asteroid_list[0].type = i;
asteroid_list[0].spawn_weight = asteroid_spawn_weight[i]; //Be absolutely sure we're using the right spawn weight for asteroids
Asteroid_info.push_back(asteroid_list[0]);
asteroid_list.erase(asteroid_list.begin());
continue;
}
for (int i = 0; i < (int)asteroid_list.size(); i++) {
if (asteroid_list[i].type < 0) {
Asteroid_info.push_back(asteroid_list[i]);
} else {
Warning(LOCATION, "Additional %s asteroid type found with name %s. Setting as debris type instead!", asteroid_size[asteroid_list[i].type].c_str(), asteroid_list[i].name);
asteroid_list[i].type = ASTEROID_TYPE_DEBRIS;
Asteroid_info.push_back(asteroid_list[i]);
}
}
asteroid_list.clear();
//Check that we have the appropriate POF files-Mjn
for (int i = 0; i < (int)Asteroid_info.size(); i++) {
if (Asteroid_info[i].pof_files[0][0] == '\0')
Error(LOCATION, "Missing [POF File 1] for %s", Asteroid_info[i].name);
if (Asteroid_info[i].type != ASTEROID_TYPE_DEBRIS) {
if (Asteroid_info[i].pof_files[1][0] == '\0')
Error(LOCATION, "Missing [POF File 2] for %s", Asteroid_info[i].name);
if (Asteroid_info[i].pof_files[2][0] == '\0')
Error(LOCATION, "Missing [POF File 3] for %s", Asteroid_info[i].name);
}
}
}
/**
* Called once, at game start. Do any one-time initializations here
*/
void asteroid_init()
{
Asteroid_impact_explosion_ani = -1;
Asteroid_impact_explosion_radius = 20.0; // retail value
Asteroid_icon_closeup_model[0] = '\0';
vm_vec_make(&Asteroid_icon_closeup_position, 0.0f, 0.0f, -334.0f); // magic numbers from retail
Asteroid_icon_closeup_zoom = 0.5f; // magic number from retail
// first parse the default table
asteroid_parse_tbl("asteroid.tbl");
// parse any modular tables
parse_modular_table("*-ast.tbm", asteroid_parse_tbl);
//No asteroids defined. Bail!
if (asteroid_list.empty())
return;
// now verify the asteroids were found and put them in the correct order
verify_asteroid_list();
// now that Asteroid_info is filled in we can verify the asteroid splits and set their indecies
verify_asteroid_splits();
if (Asteroid_impact_explosion_ani == -1) {
Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!");
}
if (Asteroid_icon_closeup_model[0] == '\0')
strcpy_s(Asteroid_icon_closeup_model, Asteroid_info[ASTEROID_TYPE_LARGE].pof_files[0]); // magic file from retail
}
/**
* Pick object to throw asteroids at.
* Pick any capital or big ship inside the bounds of the asteroid field.
*/
static int set_asteroid_throw_objnum()
{
if (Asteroid_field.num_initial_asteroids < 1)
return -1;
ship_obj *so;
object *ship_objp;
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) {
ship_objp = &Objects[so->objnum];
if (ship_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
if (Ship_info[Ships[ship_objp->instance].ship_info_index].is_big_or_huge()) {
if (asteroid_is_ship_inside_field(&Asteroid_field, &ship_objp->pos, ship_objp->radius))
return so->objnum;
}
}
return -1;
}
void asteroid_frame()
{
if (Num_asteroids < 1)
return;
// Only throw if active field
if (Asteroid_field.field_type == FT_PASSIVE) {
return;
}
// If no asteroid types are defined for the field, abort.
if (Asteroid_field.num_used_field_debris_types <= 0) {
return;
}
// If there are no explicit targets, fall back to default retail targeting behavior
if (Asteroid_field.target_names.empty()) {
int objnum = set_asteroid_throw_objnum();
if (Asteroid_targets.empty() || Asteroid_targets[0].objnum != objnum) {
Asteroid_targets.clear();
if (objnum >= 0)
asteroid_add_target(&Objects[objnum]);
}
}
sanitize_asteroid_targets_list();
maybe_throw_asteroid();
}
extern bool Extra_target_info;
/**
* Draw brackets around on-screen asteroids that are about to collide, otherwise draw an offscreen indicator
*/
void asteroid_show_brackets()
{
vertex asteroid_vertex;
object *asteroid_objp, *player_target;
asteroid *asp;
Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_show_brackets() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");
// get pointer to player target, so we don't need to take OBJ_INDEX() of asteroid_objp to compare to target_objnum
if ( Player_ai->target_objnum >= 0 ) {
player_target = &Objects[Player_ai->target_objnum];
} else {
player_target = NULL;
}
for (auto aop: list_range(&Asteroid_obj_list)) {
asteroid_objp = &Objects[aop->objnum];
if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
if (asteroid_objp->type != OBJ_ASTEROID ) {
continue;
}
asp = &Asteroids[asteroid_objp->instance];
if ( asp->collide_objnum < 0 ) {
continue;
}
if ( asteroid_objp == player_target ) {
continue;
}
g3_rotate_vertex(&asteroid_vertex,&asteroid_objp->pos);
g3_project_vertex(&asteroid_vertex);
if ( Extra_target_info ) {
hud_target_add_display_list(asteroid_objp, &asteroid_vertex, &asteroid_objp->pos, 0, NULL, NULL, TARGET_DISPLAY_DIST | TARGET_DISPLAY_LEAD);
} else {
hud_target_add_display_list(asteroid_objp, &asteroid_vertex, &asteroid_objp->pos, 0, NULL, NULL, TARGET_DISPLAY_DIST);
}
}
}
/**
* Target the closest danger asteroid to the player
*/
void asteroid_target_closest_danger()
{
object *asteroid_objp, *closest_asteroid_objp = NULL;
asteroid *asp;
float dist, closest_dist = 999999.0f;
Assertion(GET_FIRST(&Asteroid_obj_list) != nullptr, "asteroid_target_closest_danger() called before Asteroid_obj_list was initialized; this shouldn't happen. Get a coder!");
for (auto aop: list_range(&Asteroid_obj_list)) {
asteroid_objp = &Objects[aop->objnum];
if (asteroid_objp->flags[Object::Object_Flags::Should_be_dead])
continue;
if (asteroid_objp->type != OBJ_ASTEROID ) {
continue;
}
asp = &Asteroids[asteroid_objp->instance];
if ( asp->collide_objnum < 0 ) {
continue;
}
dist = vm_vec_dist_quick(&Player_obj->pos, &asteroid_objp->pos);
if ( dist < closest_dist ) {
closest_dist = dist;
closest_asteroid_objp = asteroid_objp;
}
}
if ( closest_asteroid_objp ) {
set_target_objnum( Player_ai, OBJ_INDEX(closest_asteroid_objp) );
}
}
void asteroid_page_in()
{
if (Asteroid_info.empty())
return;
if (Asteroid_field.num_initial_asteroids > 0 ) {
int i, j, k;
nprintf(( "Paging", "Paging in asteroids\n" ));
// max of MAX_ACTIVE_DEBRIS_TYPES possible debris field models
for (i=0; i<MAX_ACTIVE_DEBRIS_TYPES; i++) {
asteroid_info *asip;
if (Asteroid_field.debris_genre == DG_ASTEROID) {
// asteroid
Assertion(i < NUM_ASTEROID_SIZES, "Got invalid call to load a debris type instead of asteroid type!");
asip = &Asteroid_info[i];
} else {
// ship debris - always full until empty
if (Asteroid_field.field_debris_type[i] != -1) {
asip = &Asteroid_info[Asteroid_field.field_debris_type[i]];
} else {
break;
}
}
for (k=0; k<NUM_ASTEROID_POFS; k++) {
// SHIP DEBRIS - use subtype 0
if (Asteroid_field.debris_genre == DG_DEBRIS) {
if (k > 0) {
break;
}
} else {
// ASTEROID DEBRIS - use subtype (Asteroid_field.field_debris_type[] != -1)
if (Asteroid_field.field_debris_type[k] == -1) {
continue;
}
}
if (asip->model_num[k] < 0)
continue;
asip->modelp[k] = model_get(asip->model_num[k]);
// Page in textures
for (j=0; j<asip->modelp[k]->n_textures; j++ ) {
asip->modelp[k]->maps[j].PageIn();
}
}
}
}
}