mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* Add an "On Ship Damage" script hook Runs after resolved damage has been applied to a ship (lethal hits run before the destruction callback). Hook variables: Target, Source, Attacker, WeaponClass, WeaponSubtype, SourceKind (PROJECTILE, BEAM, SHOCKWAVE, COLLISION, OTHER), DamageType, ShieldDamage, HullDamage, Quadrant, Ballistic, BeamWeapon and LethalHit. Conditions can filter on the target ship. Nothing is computed unless a script has registered the hook. * Move the On Ship Damage hook call out of a lambda ship_do_damage() now calls a file-level static function, ship_run_damage_hook(), that takes the values it needs as arguments instead of capturing the function state. Hook behaviour is unchanged. * Report subsystem damage in the On Ship Damage hook Adds SubsystemDamage, the total subsystem strength a damage application removed, so a hit absorbed by subsystems no longer looks like a small hull hit. The hook description now says it only runs where damage is applied, which excludes multiplayer clients. * Address On Ship Damage review comments - Remove WeaponClass, WeaponSubtype, Ballistic and BeamWeapon from the hook. Scripts read these from Source when it is a weapon. - Add a "Weapon class" condition for the On Ship Damage hook. It matches only when the damage source is a weapon of that class; a weapon name that does not exist never matches. - SubsystemDamage is now a table keyed by subsystem handle that lists only the subsystems that lost strength. The total is removed. - Rename PROJECTILE to NON_BEAM_WEAPON. - Classify SourceKind by damage source: COLLISION for ship, debris and asteroid collisions; EXPLOSION for ship-death and docked-ship blasts and asteroid blasts; WASH for engine wash; SELF for kills where the ship damages itself; OTHER for other ship-sourced damage. - Reword the hook description. * Key On Ship Damage SubsystemDamage by canonical name Lua tables cannot use subsystem handles as keys in a usable way, so key the SubsystemDamage table by each subsystem's CanonicalName instead. * Tag ship-vs-prop damage as a collision and use in_bounds The ship-vs-prop collision call didn't pass DamageSource::Collision, so the On Ship Damage hook reported those hits as OTHER. Every other collision path was already tagged. Also use SCP_vector::in_bounds() for the Weapon_info and Damage_types index checks in ship_run_damage_hook().
2947 lines
91 KiB
C++
2947 lines
91 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 "particle/ParticleEffect.h"
|
|
#include "scripting/global_hooks.h"
|
|
#include "particle/hosts/EffectHostVector.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>
|
|
#include <random>
|
|
|
|
#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 particle::ParticleEffectHandle Asteroid_impact_explosion_ani;
|
|
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;
|
|
}
|
|
|
|
if (Fix_asteroid_bounding_box_check) {
|
|
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);
|
|
} else {
|
|
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;
|
|
|
|
if (Fix_asteroid_bounding_box_check) {
|
|
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);
|
|
} else {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Randomly select an asteroid subtype
|
|
*/
|
|
const SCP_string& pick_random_asteroid_type()
|
|
{
|
|
return Asteroid_field.field_asteroid_type[Random::next(static_cast<int>(Asteroid_field.field_asteroid_type.size()))];
|
|
}
|
|
|
|
int get_asteroid_subtype_index_by_name(const SCP_string &name, int asteroid_idx) {
|
|
asteroid_info* this_asteroid = &Asteroid_info[asteroid_idx];
|
|
for (int i = 0; i < static_cast<int>(this_asteroid->subtypes.size()); i++) {
|
|
if (this_asteroid->subtypes[i].type_name == name) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* 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 == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
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 nullptr;
|
|
}
|
|
|
|
if (!SCP_vector_inbounds(Asteroid_info, asteroid_type)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// HACK: multiplayer asteroid subtype always 0 to keep subtype in sync
|
|
if ( Game_mode & GM_MULTIPLAYER) {
|
|
asteroid_subtype = 0;
|
|
}
|
|
|
|
asip = &Asteroid_info[asteroid_type];
|
|
|
|
if (!SCP_vector_inbounds(asip->subtypes, asteroid_subtype)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// if the model is not loaded then abort
|
|
if(asip->subtypes[asteroid_subtype].model_number == -1) {
|
|
return nullptr;
|
|
}
|
|
|
|
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->subtypes[asteroid_subtype].model_number);
|
|
|
|
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->subtypes[asteroid_subtype].model_number)->flags & PM_FLAG_HAS_INTRINSIC_MOTION) {
|
|
asp->model_instance_num = model_create_instance(objnum, asip->subtypes[asteroid_subtype].model_number);
|
|
}
|
|
|
|
// 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 = model_get(asip->subtypes[asteroid_subtype].model_number)->rad * 700.0f;
|
|
objp->phys_info.I_body_inv.vec.rvec.xyz.x = 1.0f / (objp->phys_info.mass*model_get(asip->subtypes[asteroid_subtype].model_number)->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);
|
|
|
|
// New asteroid size doesn't have this subtype, so abort
|
|
if (new_objp == nullptr)
|
|
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
|
|
*/
|
|
void asteroid_load(int asteroid_info_index, int asteroid_subtype)
|
|
{
|
|
int i;
|
|
asteroid_info *asip;
|
|
|
|
Assertion(asteroid_info_index < static_cast<int>(Asteroid_info.size()),
|
|
"Command to load asteroid at index %i, but only %i asteroids exist",
|
|
asteroid_info_index,
|
|
static_cast<int>(Asteroid_info.size()));
|
|
|
|
// invalid asteroid index
|
|
if (!SCP_vector_inbounds(Asteroid_info, asteroid_info_index)) {
|
|
return;
|
|
}
|
|
|
|
asip = &Asteroid_info[asteroid_info_index];
|
|
|
|
// This asteroid does not have this subtype so just return
|
|
if (!SCP_vector_inbounds(asip->subtypes, asteroid_subtype)) {
|
|
return;
|
|
}
|
|
|
|
if ( !VALID_FNAME(asip->subtypes[asteroid_subtype].pof_filename) )
|
|
return;
|
|
|
|
//Check if the model is already loaded
|
|
if (asip->subtypes[asteroid_subtype].model_number >= 0)
|
|
return;
|
|
|
|
asip->subtypes[asteroid_subtype].model_number = model_load( asip->subtypes[asteroid_subtype].pof_filename );
|
|
|
|
if (asip->subtypes[asteroid_subtype].model_number >= 0)
|
|
{
|
|
polymodel *pm = model_get(asip->subtypes[asteroid_subtype].model_number);
|
|
|
|
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 ShipDebrisOdds {
|
|
float random_threshold;
|
|
int debris_type;
|
|
};
|
|
std::vector<ShipDebrisOdds> shipDebrisOddsTable(Asteroid_field.field_debris_type.size());
|
|
|
|
float max_weighted_range = 0.0f;
|
|
|
|
if (!Asteroids_enabled)
|
|
return;
|
|
|
|
if (Asteroid_field.num_initial_asteroids <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
if ((Asteroid_field.debris_genre == DG_DEBRIS && Asteroid_field.field_debris_type.empty()) ||
|
|
(Asteroid_field.debris_genre == DG_ASTEROID && Asteroid_field.field_asteroid_type.empty())) {
|
|
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_VALUE-Detail.asteroid_density)));
|
|
|
|
int num_debris_types = 0;
|
|
int num_valid = 0; // count of valid (non-(-1)) entries written into shipDebrisOddsTable
|
|
|
|
// get number of debris types
|
|
if (Asteroid_field.debris_genre == DG_DEBRIS) {
|
|
num_debris_types = static_cast<int>(Asteroid_field.field_debris_type.size());
|
|
|
|
// Calculate the odds table; skip invalid entries (field_debris_type[idx] == -1 means
|
|
// missionparse invalidated the entry because the type no longer exists in Asteroid_info)
|
|
for (idx=0; idx<num_debris_types; idx++) {
|
|
if (Asteroid_field.field_debris_type[idx] == -1)
|
|
continue;
|
|
float debris_weight = Asteroid_info[Asteroid_field.field_debris_type[idx]].spawn_weight;
|
|
shipDebrisOddsTable[num_valid].random_threshold = max_weighted_range + debris_weight;
|
|
shipDebrisOddsTable[num_valid].debris_type = Asteroid_field.field_debris_type[idx];
|
|
max_weighted_range += debris_weight;
|
|
num_valid++;
|
|
}
|
|
}
|
|
|
|
// Load Asteroid/debris models
|
|
if (Asteroid_field.debris_genre == DG_DEBRIS) {
|
|
for (idx=0; idx<num_debris_types; idx++) {
|
|
if (Asteroid_field.field_debris_type[idx] != -1)
|
|
asteroid_load(Asteroid_field.field_debris_type[idx], 0);
|
|
}
|
|
} else {
|
|
for (const auto& subtype : Asteroid_field.field_asteroid_type) {
|
|
asteroid_load(ASTEROID_TYPE_SMALL, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_SMALL));
|
|
asteroid_load(ASTEROID_TYPE_MEDIUM, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_MEDIUM));
|
|
asteroid_load(ASTEROID_TYPE_LARGE, get_asteroid_subtype_index_by_name(subtype, ASTEROID_TYPE_LARGE));
|
|
}
|
|
}
|
|
|
|
// 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 subtype = get_asteroid_subtype_index_by_name(pick_random_asteroid_type(), ASTEROID_TYPE_LARGE);
|
|
|
|
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 < num_valid; idx++) {
|
|
// for ship debris, choose type according to odds table
|
|
if (rand_choice < shipDebrisOddsTable[idx].random_threshold) {
|
|
asteroid_create(&Asteroid_field, shipDebrisOddsTable[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, vec3d o_min, vec3d o_max, bool inner_box, vec3d i_min, vec3d i_max, SCP_vector<SCP_string> asteroid_types)
|
|
{
|
|
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.clear();
|
|
|
|
Asteroid_field.field_asteroid_type.clear();
|
|
|
|
Asteroid_field.field_asteroid_type = std::move(asteroid_types);
|
|
|
|
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;
|
|
}
|
|
|
|
// Only create asteroids if we have some to create
|
|
if ((!Asteroid_field.field_asteroid_type.empty()) && (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, SCP_vector<int> debris_types, 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.clear();
|
|
|
|
Asteroid_field.field_asteroid_type.clear();
|
|
|
|
Asteroid_field.field_debris_type = std::move(debris_types);
|
|
|
|
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 ((!Asteroid_field.field_debris_type.empty()) && (num_asteroids > 0)) {
|
|
asteroid_create_all();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Init asteroid system for the level, called from ::game_level_init()
|
|
*/
|
|
void asteroid_level_init()
|
|
{
|
|
Asteroid_field = {};
|
|
|
|
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.field_asteroid_type.empty(), "maybe_throw_asteroid() called while field_asteroid_type.size 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));
|
|
|
|
// get a valid subtype
|
|
int subtype = get_asteroid_subtype_index_by_name(pick_random_asteroid_type(), ASTEROID_TYPE_LARGE);
|
|
|
|
// this really shouldn't happen but just in case...
|
|
if (subtype < 0)
|
|
return;
|
|
|
|
object *ast_objp = asteroid_create(&Asteroid_field, ASTEROID_TYPE_LARGE, subtype, Asteroid_field.enhanced_visibility_checks);
|
|
if (ast_objp != nullptr) {
|
|
asteroid_aim_at_target(target_objp, ast_objp, ASTEROID_MIN_COLLIDE_TIME + frand() * 20.0f);
|
|
|
|
// if asteroid is inside inner bound, kill it
|
|
if (asteroid_in_inner_bound(&Asteroid_field, &ast_objp->pos, 0.0f)) {
|
|
ast_objp->flags.set(Object::Object_Flags::Should_be_dead);
|
|
} else {
|
|
Asteroids[ast_objp->instance].target_objnum = target.objnum;
|
|
target.incoming_asteroids++;
|
|
|
|
if ( MULTIPLAYER_MASTER ) {
|
|
send_asteroid_throw( ast_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);
|
|
asp->model_instance_num = -1;
|
|
}
|
|
|
|
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].subtypes[asteroid_subtype].model_number; // 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.
|
|
mc.collision_checked.assign(pm->n_models, 0);
|
|
for (auto submodel: submodel_vector) {
|
|
mc.collision_checked[submodel] = 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) {
|
|
// turn on just one submodel for collision test
|
|
mc.collision_checked[submodel] = 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
|
|
mc.collision_checked[submodel] = true;
|
|
}
|
|
|
|
// Clear collision_checked before base model pass so it auto-inits fresh
|
|
mc.collision_checked.clear();
|
|
|
|
}
|
|
|
|
// 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].subtypes[asteroid_subtype].model_number; // 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.get();
|
|
else if ((shipp->flags[Ship::Ship_Flags::Depart_warp]) && (shipp->warpout_effect != nullptr))
|
|
warp_effect = shipp->warpout_effect.get();
|
|
|
|
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].subtypes[asp->asteroid_subtype].model_number);
|
|
|
|
model_render_params render_info;
|
|
|
|
uint64_t render_flags = MR_IS_ASTEROID;
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_light])
|
|
render_flags |= MR_NO_LIGHTING;
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Glowmaps_disabled]) {
|
|
render_flags |= MR_NO_GLOWMAPS;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Draw_as_wireframe]) {
|
|
render_flags |= MR_SHOW_OUTLINE_HTL | MR_NO_POLYS | MR_NO_TEXTURING;
|
|
render_info.set_color(Wireframe_color);
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_full_detail]) {
|
|
render_flags |= MR_FULL_DETAIL;
|
|
}
|
|
|
|
uint debug_flags = render_info.get_debug_flags();
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_diffuse]) {
|
|
debug_flags |= MR_DEBUG_NO_DIFFUSE;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_glowmap]) {
|
|
debug_flags |= MR_DEBUG_NO_GLOW;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_normalmap]) {
|
|
debug_flags |= MR_DEBUG_NO_NORMAL;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_ambientmap]) {
|
|
debug_flags |= MR_DEBUG_NO_AMBIENT;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_specmap]) {
|
|
debug_flags |= MR_DEBUG_NO_SPEC;
|
|
}
|
|
|
|
if (asp->render_flags[Object::Raw_Pof_Flags::Render_without_reflectmap]) {
|
|
debug_flags |= MR_DEBUG_NO_REFLECT;
|
|
}
|
|
|
|
render_info.set_object_number( OBJ_INDEX(obj) );
|
|
|
|
render_info.set_flags(render_flags);
|
|
render_info.set_debug_flags(debug_flags);
|
|
if (asp->model_instance_num >= 0)
|
|
render_info.set_replacement_textures(model_get_instance(asp->model_instance_num)->texture_replace);
|
|
|
|
model_render_queue(&render_info, scene, Asteroid_info[asp->asteroid_type].subtypes[asp->asteroid_subtype].model_number, &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);
|
|
auto fireball_radius = objp->radius * fireball_scale_multiplier;
|
|
|
|
if (Zero_radius_explosions_skip_fireballs && fl_near_zero(fireball_radius))
|
|
fireball_objnum = -1;
|
|
else
|
|
fireball_objnum = fireball_create( &objp->pos, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(objp), fireball_radius, 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, DamageSource::Explosion);
|
|
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 )
|
|
{
|
|
asteroid *asp = &Asteroids[pasteroid_obj->instance];
|
|
asteroid_info *asip = &Asteroid_info[Asteroids[pasteroid_obj->instance].asteroid_type];
|
|
|
|
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;
|
|
|
|
float explosion_life = 1.f;
|
|
int breakup_timestamp;
|
|
|
|
Assertion(!asip->end_particles.isValid() || asip->breakup_delay.has_value(), "Asteroid %s has end particles but no breakup delay. Parsing should not have allowed this!", asip->name);
|
|
|
|
if (asip->end_particles.isValid()) {
|
|
auto source = particle::ParticleManager::get()->createSource(asip->end_particles);
|
|
|
|
// Use the position since the asteroid is going to be invalid soon
|
|
auto host = std::make_unique<EffectHostVector>(pasteroid_obj->pos, pasteroid_obj->orient, pasteroid_obj->phys_info.vel);
|
|
host->setRadius(pasteroid_obj->radius);
|
|
source->setHost(std::move(host));
|
|
source->setNormal(pasteroid_obj->orient.vec.uvec);
|
|
source->finishCreation();
|
|
} else {
|
|
explosion_life = asteroid_create_explosion(pasteroid_obj);
|
|
}
|
|
|
|
if (asip->breakup_delay.has_value()) {
|
|
breakup_timestamp = fl2i(*asip->breakup_delay * MILLISECONDS_PER_SECOND);
|
|
} else {
|
|
breakup_timestamp = fl2i((explosion_life * MILLISECONDS_PER_SECOND) / 5.f);
|
|
}
|
|
|
|
asteroid_explode_sound(pasteroid_obj, asp->asteroid_type, play_loud_collision);
|
|
asteroid_do_area_effect(pasteroid_obj);
|
|
|
|
asp->final_death_time = _timestamp( breakup_timestamp ); // Wait till 20% of vclip time before breaking the asteroid up, or use the specified delay
|
|
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() && Asteroid_impact_explosion_ani.isValid()) {
|
|
auto source = particle::ParticleManager::get()->createSource(Asteroid_impact_explosion_ani);
|
|
source->setHost(std::make_unique<EffectHostVector>(*hitpos, vmd_identity_matrix, vmd_zero_vector));
|
|
source->finishCreation();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 (size_t i = 0; i < Asteroid_info.size(); i++) {
|
|
for (size_t j = 0; j < Asteroid_info[i].subtypes.size(); j++) {
|
|
Asteroid_info[i].subtypes[j].model_number = -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);
|
|
}
|
|
|
|
std::random_device rd;
|
|
std::shuffle(roids_to_create.begin(), roids_to_create.end(), std::mt19937(rd()));
|
|
|
|
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(std::move(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;
|
|
}
|
|
|
|
auto insert_or_replace_subtype_pof = [](SCP_vector<asteroid_subtype_info>& subtypes, const asteroid_subtype_info& new_subtype) {
|
|
for (auto& subtype : subtypes) {
|
|
if (lcase_equal(subtype.type_name, new_subtype.type_name)) {
|
|
strcpy_s(subtype.pof_filename, new_subtype.pof_filename);
|
|
return;
|
|
}
|
|
}
|
|
subtypes.push_back(new_subtype);
|
|
};
|
|
|
|
if (optional_string("$POF file1:")) {
|
|
asteroid_subtype_info thisType;
|
|
|
|
thisType.type_name = "Brown";
|
|
|
|
stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);
|
|
|
|
thisType.model_number = -1;
|
|
|
|
insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
|
|
}
|
|
|
|
if (optional_string("$POF file2:")) {
|
|
asteroid_subtype_info thisType;
|
|
|
|
thisType.type_name = "Blue";
|
|
stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);
|
|
|
|
thisType.model_number = -1;
|
|
|
|
insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
|
|
}
|
|
|
|
if (optional_string("$POF file3:")) {
|
|
asteroid_subtype_info thisType;
|
|
|
|
thisType.type_name = "Orange";
|
|
stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);
|
|
|
|
thisType.model_number = -1;
|
|
|
|
insert_or_replace_subtype_pof(asteroid_p->subtypes, thisType);
|
|
}
|
|
|
|
// For asteroid types we can have unlimited subtypes
|
|
if (asteroid_p->type != ASTEROID_TYPE_DEBRIS) {
|
|
while (optional_string("$Subtype:")) {
|
|
asteroid_subtype_info thisType;
|
|
|
|
stuff_string(thisType.type_name, F_NAME);
|
|
|
|
required_string("+POF file:");
|
|
stuff_string(thisType.pof_filename, F_NAME, MAX_FILENAME_LEN);
|
|
|
|
thisType.model_number = -1;
|
|
|
|
bool exists = false;
|
|
for (const auto& type : asteroid_p->subtypes) {
|
|
if (type.type_name == thisType.type_name) {
|
|
exists = true;
|
|
}
|
|
}
|
|
|
|
if (exists) {
|
|
error_display(0,
|
|
"Asteroid subtype %s already exists for asteroid %s",
|
|
thisType.type_name.c_str(),
|
|
asteroid_p->name);
|
|
} else {
|
|
asteroid_p->subtypes.push_back(std::move(thisType));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (optional_string("$Detail distance:")) {
|
|
asteroid_p->num_detail_levels = sz2i(stuff_int_list(asteroid_p->detail_distance, MAX_ASTEROID_DETAIL_LEVELS, ParseLookupType::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 Effect:")) {
|
|
asteroid_p->end_particles = particle::util::parseEffect(asteroid_p->name);
|
|
} else {
|
|
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("$Breakup Delay:")) {
|
|
stuff_float(&asteroid_p->breakup_delay.emplace());
|
|
}
|
|
|
|
if (asteroid_p->end_particles.isValid() && !asteroid_p->breakup_delay.has_value()) {
|
|
error_display(0, "Asteroid %s has an explosion effect but no breakup delay!", asteroid_p->name);
|
|
}
|
|
|
|
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 Effect:")) {
|
|
Asteroid_impact_explosion_ani = particle::util::parseEffect();
|
|
}
|
|
else {
|
|
char impact_ani_file[MAX_FILENAME_LEN] = {0};
|
|
float Asteroid_impact_explosion_radius = 0.0f;
|
|
int num_frames;
|
|
|
|
if (optional_string("$Impact Explosion:")) {
|
|
stuff_string(impact_ani_file, F_NAME, MAX_FILENAME_LEN);
|
|
}
|
|
if (optional_string("$Impact Explosion Radius:")) {
|
|
stuff_float(&Asteroid_impact_explosion_radius);
|
|
}
|
|
|
|
if(VALID_FNAME(impact_ani_file)) {
|
|
Asteroid_impact_explosion_ani = particle::ParticleManager::get()->addEffect(particle::ParticleEffect(
|
|
"", //Name
|
|
::util::UniformFloatRange(1.f), //Particle num
|
|
particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission
|
|
::util::UniformFloatRange(), //No duration
|
|
::util::UniformFloatRange(-1.f), //Single particle only
|
|
particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction
|
|
::util::UniformFloatRange(), //Velocity Inherit
|
|
false, //Velocity Inherit absolute?
|
|
nullptr, //Velocity volume
|
|
::util::UniformFloatRange(), //Velocity volume multiplier
|
|
particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling
|
|
std::nullopt, //Orientation-based velocity
|
|
std::nullopt, //Position-based velocity
|
|
nullptr, //Position volume
|
|
particle::ParticleEffectHandle::invalid(), //Trail
|
|
1.f, //Chance
|
|
false, //Affected by detail
|
|
-1.f, //Culling range multiplier
|
|
false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X
|
|
false, //Don't reverse animation
|
|
false, //parent local
|
|
false, //ignore velocity inherit if parented
|
|
false, //position velocity inherit absolute?
|
|
std::nullopt, //Local velocity offset
|
|
std::nullopt, //Local offset
|
|
::util::UniformFloatRange(-1.f), //Lifetime
|
|
::util::UniformFloatRange(Asteroid_impact_explosion_radius), //Radius
|
|
bm_load_animation(impact_ani_file, &num_frames, nullptr, nullptr, nullptr, true))); //Bitmap
|
|
}
|
|
}
|
|
|
|
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 < static_cast<int>(Asteroid_info.size()); i++) {
|
|
if (!stricmp(asteroid_name, Asteroid_info[i].name)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Returns the list of unique asteroid subtype names.
|
|
// List is cached after the first call since Asteroid_info cannot change during an engine instance.
|
|
const SCP_vector<SCP_string>& get_list_valid_asteroid_subtypes()
|
|
{
|
|
static SCP_vector<SCP_string> list;
|
|
|
|
if (list.empty()) {
|
|
for (const auto& this_asteroid : Asteroid_info) {
|
|
if (this_asteroid.type != ASTEROID_TYPE_DEBRIS) {
|
|
for (const auto& subtype : this_asteroid.subtypes) {
|
|
// Only add unique names
|
|
if (std::find(list.begin(), list.end(), subtype.type_name) == list.end()) {
|
|
list.push_back(subtype.type_name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
|
|
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 = std::move(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;
|
|
|
|
if (asteroid_list.empty()) {
|
|
mprintf(("%s asteroid not found and no fallback available in asteroid_list.\n", asteroid_size[i].c_str()));
|
|
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;
|
|
|
|
}
|
|
|
|
// Rest should be debris type
|
|
for (int i = 0; i < (int)asteroid_list.size(); i++) {
|
|
|
|
// Remove all subtypes except the first one
|
|
asteroid_list[i].subtypes.resize(1);
|
|
// Make sure the first subtype is set as debris
|
|
asteroid_list[i].subtypes[0].type_name = "Debris";
|
|
|
|
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].subtypes.size() == 0) {
|
|
Error(LOCATION, "Asteroid %s needs at least one subtype!", Asteroid_info[i].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called once, at game start. Do any one-time initializations here
|
|
*/
|
|
void asteroid_init()
|
|
{
|
|
Asteroid_impact_explosion_ani = particle::ParticleEffectHandle::invalid();
|
|
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()) {
|
|
mprintf(("No asteroids defined in asteroid.tbl or any modular tables. Asteroid fields will not function.\n"));
|
|
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.isValid()) {
|
|
// this will always be missing on a standalone
|
|
if ( !Is_standalone ) {
|
|
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].subtypes[0].pof_filename); // 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.field_asteroid_type.empty()) {
|
|
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 ) {
|
|
|
|
nprintf(( "Paging", "Paging in asteroids\n" ));
|
|
|
|
size_t num_debris_types = NUM_ASTEROID_SIZES;
|
|
if (Asteroid_field.debris_genre == DG_DEBRIS) {
|
|
num_debris_types = Asteroid_field.field_debris_type.size();
|
|
}
|
|
|
|
|
|
// for asteroids this will loop over all three possible sizes
|
|
// for debris this will loop over all selected types
|
|
for (size_t i = 0; i < num_debris_types; i++) {
|
|
asteroid_info *asip;
|
|
|
|
if (Asteroid_field.debris_genre == DG_ASTEROID) {
|
|
// asteroid
|
|
Assertion(i < NUM_ASTEROID_SIZES, "Got invalid call to load non-existent asteroid size!");
|
|
asip = &Asteroid_info[i];
|
|
} else {
|
|
// 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 (size_t k = 0; k < asip->subtypes.size(); k++) {
|
|
|
|
// DEBRIS FIELDS - always use subtype 0
|
|
if (Asteroid_field.debris_genre == DG_DEBRIS) {
|
|
if (k > 0) {
|
|
break;
|
|
}
|
|
} else {
|
|
// ASTEROID FIELDS - use subtype k, if active
|
|
bool active = false;
|
|
for (const auto& active_type : Asteroid_field.field_asteroid_type) {
|
|
if (active_type == asip->subtypes[k].type_name) {
|
|
active = true;
|
|
}
|
|
}
|
|
|
|
if (!active) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// model is already paged in
|
|
if (asip->subtypes[k].model_number < 0)
|
|
continue;
|
|
|
|
const auto mp = model_get(asip->subtypes[k].model_number);
|
|
|
|
// Page in textures
|
|
for (int j = 0; j < mp->n_textures; j++) {
|
|
mp->maps[j].PageIn();
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|