mirror of
https://github.com/kevinbentley/Descent3.git
synced 2025-01-22 19:55:23 +00:00
b6ef3b591a
Due to ``#include "DallasFuncs.cpp"``, DF is recompiled 52 times. Rework it to build just once. The compile time goes down for me from 1m45.3s to 1m38.8s on my 1135G7 CPU running make -j8.
1523 lines
50 KiB
C++
1523 lines
50 KiB
C++
/*
|
|
* Descent 3
|
|
* Copyright (C) 2024 Parallax Software
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// aigame2.cpp
|
|
// 0.1
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "osiris_import.h"
|
|
#include "osiris_common.h"
|
|
#include "osiris_vector.h"
|
|
#include "DallasFuncs.h"
|
|
|
|
#include "module.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
DLLEXPORT char STDCALL InitializeDLL(tOSIRISModuleInit *func_list);
|
|
DLLEXPORT void STDCALL ShutdownDLL(void);
|
|
DLLEXPORT int STDCALL GetGOScriptID(const char *name, uint8_t isdoor);
|
|
DLLEXPORT void STDCALLPTR CreateInstance(int id);
|
|
DLLEXPORT void STDCALL DestroyInstance(int id, void *ptr);
|
|
DLLEXPORT int16_t STDCALL CallInstanceEvent(int id, void *ptr, int event, tOSIRISEventInfo *data);
|
|
DLLEXPORT int STDCALL SaveRestoreState(void *file_ptr, uint8_t saving_state);
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
static int String_table_size = 0;
|
|
static char **String_table = NULL;
|
|
static const char *_Error_string = "!!ERROR MISSING STRING!!";
|
|
static const char *_Empty_string = "";
|
|
const char *GetStringFromTable(int index) {
|
|
if ((index < 0) || (index >= String_table_size))
|
|
return _Error_string;
|
|
if (!String_table[index])
|
|
return _Empty_string;
|
|
return String_table[index];
|
|
}
|
|
#define TXT(x) GetStringFromTable(x)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Script IDs
|
|
|
|
#define NUM_IDS 5 // maximum number of IDs
|
|
#define FIRST_ID 50
|
|
|
|
#define ID_SAMIRPEST 50 // Samir's test pest.
|
|
#define ID_BLACKSTORMTROOP 51 // The CED Black Stormtrooper
|
|
#define ID_CREEPER 52 // The CREEPER
|
|
#define ID_LUKETURRET 53 // Luke's nifty security turret
|
|
#define ID_STBLACKBARREL 54 // The Black Stormtrooper's barrel pack
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Script names
|
|
|
|
static const char *const Script_names[NUM_IDS] = {"Samir's Pest", "StormTrooperBlack", "Creeper", "LukeTurret",
|
|
"STBlackBarrel"};
|
|
|
|
// Class definitions
|
|
|
|
#define N_SNIPE_POINTS 3
|
|
|
|
#define GOAL_UID_GETTOPOS 0x100
|
|
|
|
// ai base class
|
|
class aiObjScript {
|
|
public:
|
|
int16_t CallEvent(int event, tOSIRISEventInfo *data);
|
|
|
|
protected:
|
|
// Handles all possible OSIRIS events.
|
|
virtual void OnInit(int me_handle){}; // EVT_AI_INIT
|
|
virtual void OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt){}; // EVT_DESTROY
|
|
virtual void OnInterval(tOSIRISEventInfo *data){}; // EVT_AI_FRAME
|
|
virtual void OnCollide(int me_handle, int it_handle){}; // EVT_COLLIDE
|
|
virtual bool OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data) { return true; }; // EVT_AI_NOTIFY
|
|
virtual void OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt){}; // EVT_DAMAGED
|
|
virtual void OnMemRestore(void *ptr){}; // EVT_MEMRESTORE
|
|
};
|
|
|
|
// ai base class
|
|
#define PEST_STATE_ROAM 0
|
|
#define PEST_STATE_HIVE 1
|
|
|
|
class aiSamirPest : public aiObjScript {
|
|
public:
|
|
aiSamirPest();
|
|
~aiSamirPest();
|
|
|
|
private:
|
|
struct t_pest_memory {
|
|
int tail_handle;
|
|
float last_anim_frame;
|
|
int foot_sounds[3];
|
|
int state;
|
|
};
|
|
|
|
t_pest_memory *memory;
|
|
|
|
protected:
|
|
// Handles all possible OSIRIS events.
|
|
virtual void OnInit(int me_handle);
|
|
virtual void OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt);
|
|
virtual void OnInterval(tOSIRISEventInfo *data);
|
|
virtual void OnCollide(int me_handle, int it_handle);
|
|
virtual bool OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data);
|
|
virtual void OnMemRestore(void *ptr) { memory = (t_pest_memory *)ptr; };
|
|
};
|
|
|
|
#define STATE_IDLE 0 // waiting state
|
|
#define STATE_INIT 1 // initial state
|
|
#define STATE_BASE 2 // sentry state.
|
|
#define STATE_HIDE 3 // hiding, waiting...
|
|
#define STATE_SNIPE 4 // will attack player briefly.
|
|
#define STATE_ATTACK 5 // regular attack
|
|
#define STATE_STALK 6 // stalks player
|
|
|
|
class aiBlackStormTrooper : public aiObjScript {
|
|
public:
|
|
aiBlackStormTrooper();
|
|
~aiBlackStormTrooper();
|
|
|
|
private:
|
|
struct t_bst_memory {
|
|
float timer;
|
|
float snipe_timer;
|
|
float last_frame;
|
|
float laser_timer;
|
|
int target_handle;
|
|
int ammo_barrel;
|
|
int gun_sight_obj;
|
|
int camera_obj;
|
|
int8_t state;
|
|
int8_t snipe_points;
|
|
int8_t cur_snipe_point;
|
|
int8_t laser_targeted;
|
|
int16_t backpack_hp; // points before destroy backpack.
|
|
int16_t flee_hp;
|
|
|
|
// a Nx(N-1) array with a extra int for number of VALID connecting snipe points per snipe point.
|
|
// note: obvious cases ignored (ptA->ptA)
|
|
// used to create a quick map of snipe points for sniper to use.
|
|
int snipe_point_handles[N_SNIPE_POINTS];
|
|
int8_t access_array[N_SNIPE_POINTS][N_SNIPE_POINTS];
|
|
|
|
// used to keep track of targets black storm trooper knows about. storm trooper must
|
|
// SEE the target first before storing the target in memory.
|
|
int snipe_target_handles[N_SNIPE_POINTS];
|
|
};
|
|
|
|
t_bst_memory *memory;
|
|
|
|
void set_state(int me_handle, int state);
|
|
|
|
protected:
|
|
// Handles all possible OSIRIS events.
|
|
virtual void OnInit(int me_handle);
|
|
virtual void OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt);
|
|
virtual void OnInterval(tOSIRISEventInfo *data);
|
|
virtual bool OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data);
|
|
virtual void OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt);
|
|
virtual void OnMemRestore(void *ptr) { memory = (t_bst_memory *)ptr; };
|
|
|
|
private:
|
|
void find_targets(int me_handle, bool new_snipe_point);
|
|
};
|
|
|
|
class aiCreeper : public aiObjScript {
|
|
public:
|
|
aiCreeper();
|
|
~aiCreeper();
|
|
|
|
private:
|
|
struct t_creep_memory {
|
|
float timer;
|
|
float eye_timer;
|
|
int8_t state;
|
|
uint8_t melee_flags;
|
|
int16_t energy;
|
|
int16_t hits;
|
|
int eye_obj;
|
|
};
|
|
|
|
t_creep_memory *memory;
|
|
|
|
void set_state(int me_handle, int state);
|
|
|
|
protected:
|
|
// Handles all possible OSIRIS events.
|
|
virtual void OnInit(int me_handle);
|
|
virtual void OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt);
|
|
virtual void OnInterval(tOSIRISEventInfo *data);
|
|
virtual bool OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data);
|
|
virtual void OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt);
|
|
virtual void OnMemRestore(void *ptr) { memory = (t_creep_memory *)ptr; };
|
|
};
|
|
|
|
class aiLukeTurret : public aiObjScript {
|
|
struct t_lturret_memory {
|
|
float timer;
|
|
};
|
|
|
|
t_lturret_memory *memory;
|
|
|
|
public:
|
|
aiLukeTurret(){};
|
|
~aiLukeTurret(){};
|
|
|
|
protected:
|
|
virtual void OnInit(int me_handle);
|
|
virtual bool OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data);
|
|
virtual void OnInterval(tOSIRISEventInfo *data);
|
|
virtual void OnMemRestore(void *ptr) { memory = (t_lturret_memory *)ptr; };
|
|
};
|
|
|
|
class aiSTBlackBarrel : public aiObjScript {
|
|
struct t_stblackbarrel_memory {
|
|
matrix orient;
|
|
};
|
|
|
|
t_stblackbarrel_memory *memory;
|
|
|
|
public:
|
|
aiSTBlackBarrel(){};
|
|
~aiSTBlackBarrel(){};
|
|
|
|
protected:
|
|
virtual void OnInit(int me_handle);
|
|
virtual void OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt);
|
|
virtual void OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt);
|
|
virtual void OnMemRestore(void *ptr) { memory = (t_stblackbarrel_memory *)ptr; };
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// InitializeDLL
|
|
// Purpose:
|
|
// This function gets called when the DLL first gets loaded. It will only be called once (until the
|
|
// DLL is unloaded). Passed in is a struct of data passed from the game needed for the DLL to interact
|
|
// with D3. Usually this function will just call osicommon_Initialize(), which sets up the imported
|
|
// functions. However, you can alloc some memory or whatever in this function, and free it in ShutdownDLL().
|
|
// Note: You cannot call any imported functions until osicommon_Initialize() is called.
|
|
// Returns 1 if initialization went ok, 0 if there was an error and the DLL should not be loaded.
|
|
char STDCALL InitializeDLL(tOSIRISModuleInit *func_list) {
|
|
osicommon_Initialize((tOSIRISModuleInit *)func_list);
|
|
String_table_size = func_list->string_count;
|
|
String_table = func_list->string_table;
|
|
if (func_list->game_checksum != CHECKSUM) {
|
|
mprintf(0, "Game-Checksum FAIL!!! (%ul!=%ul)\n", func_list->game_checksum, CHECKSUM);
|
|
mprintf(0, "RECOMPILE YOUR SCRIPTS!!!\n");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// ShutdownDLL
|
|
// Purpose:
|
|
// This function is called right before a DLL is about to be unloaded from memory. You can free
|
|
// any unfree'd memory, or anything else you need to do. Don't worry about destroying any instances
|
|
// of scripts, as they will all be automatically destroyed before this function is called. The
|
|
// same goes for any memory allocated with Scrpt_MemAlloc(), as this will automatically be freed
|
|
// when a scripts instance is destroyed.
|
|
void STDCALL ShutdownDLL(void) {}
|
|
|
|
// GetGOScriptID
|
|
// Purpose:
|
|
// Given the name of the object (from its pagename), this function will search through its
|
|
// list of General Object Scripts for a script with a matching name (to see if there is a script
|
|
// for that type/id of object within this DLL). If a matching scriptname is found, a UNIQUE ID
|
|
// is to be returned back to Descent 3. This ID will be used from here on out for all future
|
|
// interaction with the DLL. Since doors are not part of the generic object's, it's possible
|
|
// for a door to have the same name as a generic object (OBJ_POWERUP, OBJ_BUILDING, OBJ_CLUTTER
|
|
// or OBJ_ROBOT), therefore, a 1 is passed in for isdoor if the given object name refers to a
|
|
// door, else it is a 0. The return value is the unique identifier, else -1 if the script
|
|
// does not exist in the DLL.
|
|
int STDCALL GetGOScriptID(const char *name, uint8_t isdoor) {
|
|
int i;
|
|
for (i = 0; i < NUM_IDS; i++) {
|
|
if (strcmp(name, Script_names[i]) == 0) {
|
|
return (FIRST_ID + i);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// CreateInstance
|
|
// Purpose:
|
|
// Given an ID from a call to GetGOScriptID(), this function will create a new instance for that
|
|
// particular script (by allocating and initializing memory, etc.). A pointer to this instance
|
|
// is to be returned back to Descent 3. This pointer will be passed around, along with the ID
|
|
// for CallInstanceEvent() and DestroyInstance(). Return NULL if there was an error.
|
|
void STDCALLPTR CreateInstance(int id) {
|
|
switch (id) {
|
|
case ID_SAMIRPEST:
|
|
return new aiSamirPest;
|
|
case ID_BLACKSTORMTROOP:
|
|
return new aiBlackStormTrooper;
|
|
case ID_CREEPER:
|
|
return new aiCreeper;
|
|
case ID_LUKETURRET:
|
|
return new aiLukeTurret;
|
|
case ID_STBLACKBARREL:
|
|
return new aiSTBlackBarrel;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// DestroyInstance
|
|
// Purpose:
|
|
// Given an ID, and a pointer to a particular instance of a script, this function will delete and
|
|
// destruct all information associated with that script, so it will no longer exist.
|
|
void STDCALL DestroyInstance(int id, void *ptr) {
|
|
switch (id) {
|
|
case ID_SAMIRPEST:
|
|
delete ((aiSamirPest *)ptr);
|
|
break;
|
|
case ID_BLACKSTORMTROOP:
|
|
delete ((aiBlackStormTrooper *)ptr);
|
|
break;
|
|
case ID_CREEPER:
|
|
delete ((aiCreeper *)ptr);
|
|
break;
|
|
case ID_LUKETURRET:
|
|
delete ((aiLukeTurret *)ptr);
|
|
break;
|
|
case ID_STBLACKBARREL:
|
|
delete ((aiSTBlackBarrel *)ptr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// CallInstanceEvent
|
|
// Purpose:
|
|
// Given an ID, a pointer to a script instance, an event and a pointer to the struct of
|
|
// information about the event, this function will translate who this event belongs to and
|
|
// passes the event to that instance of the script to be handled. Return a combination of
|
|
// CONTINUE_CHAIN and CONTINUE_DEFAULT, to give instructions on what to do based on the
|
|
// event. CONTINUE_CHAIN means to continue through the chain of scripts (custom script, level
|
|
// script, mission script, and finally default script). If CONTINUE_CHAIN is not specified,
|
|
// than the chain is broken and those scripts of lower priority will never get the event. Return
|
|
// CONTINUE_DEFAULT in order to tell D3 if you want process the normal action that is built into
|
|
// the game for that event. This only pertains to certain events. If the chain continues
|
|
// after this script, than the CONTINUE_DEFAULT setting will be overridden by lower priority
|
|
// scripts return value.
|
|
int16_t STDCALL CallInstanceEvent(int id, void *ptr, int event, tOSIRISEventInfo *data) {
|
|
return ((aiObjScript *)ptr)->CallEvent(event, data);
|
|
}
|
|
|
|
// SaveRestoreState
|
|
// Purpose:
|
|
// This function is called when Descent 3 is saving or restoring the game state. In this function
|
|
// you should save/restore any global data that you want preserved through load/save (which includes
|
|
// demos). You must be very careful with this function, corrupting the file (reading or writing too
|
|
// much or too little) may be hazardous to the game (possibly making it impossible to restore the
|
|
// state). It would be best to use version information to keep older versions of saved states still
|
|
// able to be used. IT IS VERY IMPORTANT WHEN SAVING THE STATE TO RETURN THE NUMBER OF _BYTES_ WROTE
|
|
// TO THE FILE. When restoring the data, the return value is ignored. saving_state is 1 when you should
|
|
// write data to the file_ptr, 0 when you should read in the data.
|
|
int STDCALL SaveRestoreState(void *file_ptr, uint8_t saving_state) { return 0; }
|
|
|
|
static int CreateAndAttach(int me, const char *child_name, uint8_t child_type, char parent_ap, char child_ap,
|
|
bool f_aligned = true, bool f_set_parent = false);
|
|
static void FlushGoal(int me_handle, int goal_priority);
|
|
static void SafeGoalClearAll(int obj_handle);
|
|
static void AI_SafeSetType(int obj_handle, int ai_type);
|
|
static int TurnOnSpew(int objref, int gunpoint, int effect_type, float mass, float drag, int gravity_type, uint8_t isreal,
|
|
float lifetime, float interval, float longevity, float size, float speed, uint8_t random);
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// HELPER FUNCTIONS
|
|
|
|
// Returns the new child's handle
|
|
int CreateAndAttach(int me, const char *child_name, uint8_t child_type, char parent_ap, char child_ap, bool f_aligned,
|
|
bool f_set_parent) {
|
|
int child_handle = OBJECT_HANDLE_NONE;
|
|
int child_id = Obj_FindID(child_name);
|
|
msafe_struct m;
|
|
m.objhandle = me;
|
|
MSafe_GetValue(MSAFE_OBJECT_POS, &m);
|
|
MSafe_GetValue(MSAFE_OBJECT_ROOMNUM, &m);
|
|
|
|
if (child_id >= 0) {
|
|
int parent;
|
|
if (f_set_parent)
|
|
parent = me;
|
|
else
|
|
parent = OBJECT_HANDLE_NONE;
|
|
|
|
child_handle = Obj_Create(child_type, child_id, m.roomnum, &m.pos, NULL, parent);
|
|
if (child_handle != OBJECT_HANDLE_NONE) {
|
|
if (!Obj_AttachObjectAP(me, parent_ap, child_handle, child_ap, f_aligned)) {
|
|
// chrishack (we need a way to instantly kill scripted objects)
|
|
}
|
|
}
|
|
}
|
|
|
|
return child_handle;
|
|
}
|
|
|
|
void FlushGoal(int me_handle, int goal_priority) {
|
|
bool used;
|
|
|
|
AI_GoalValue(me_handle, goal_priority, VF_GET, AIGV_B_USED, &used);
|
|
if (used) {
|
|
int flags;
|
|
AI_GoalValue(me_handle, 3, VF_GET, AIGV_I_FLAGS, &flags);
|
|
|
|
if (!(flags & GF_NONFLUSHABLE)) {
|
|
AI_ClearGoal(me_handle, goal_priority);
|
|
}
|
|
}
|
|
}
|
|
|
|
#define DALLAS_LOW_PRIORITY_GOAL_SLOT 0
|
|
#define DALLAS_HIGH_PRIORITY_GOAL_SLOT 3
|
|
|
|
// Wipes out all goals except slots 0 and 3 (used by Dallas)
|
|
void SafeGoalClearAll(int obj_handle) {
|
|
int i;
|
|
|
|
for (i = 0; i < MAX_GOALS; i++) {
|
|
if (i != DALLAS_LOW_PRIORITY_GOAL_SLOT && i != DALLAS_HIGH_PRIORITY_GOAL_SLOT) {
|
|
AI_ClearGoal(obj_handle, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Works like AI_SetType(), but won't clear out Dallas' goal slots 0 or 3
|
|
void AI_SafeSetType(int obj_handle, int ai_type) {
|
|
float aware = AWARE_BARELY;
|
|
float time = 25.0f;
|
|
|
|
SafeGoalClearAll(obj_handle);
|
|
|
|
switch (ai_type) {
|
|
case AIT_STALKER: {
|
|
vector goal_pos;
|
|
int goal_room;
|
|
|
|
Obj_Value(obj_handle, VF_GET, OBJV_V_POS, &goal_pos);
|
|
Obj_Value(obj_handle, VF_GET, OBJV_I_ROOMNUM, &goal_room);
|
|
|
|
AI_AddGoal(obj_handle, AIG_WANDER_AROUND, 1, 1.0f, -1, GF_NONFLUSHABLE | GF_KEEP_AT_COMPLETION, &goal_pos,
|
|
goal_room);
|
|
} break;
|
|
|
|
case AIT_EVADER2: {
|
|
vector goal_pos;
|
|
int goal_room;
|
|
float size, circle_distance;
|
|
|
|
Obj_Value(obj_handle, VF_GET, OBJV_V_POS, &goal_pos);
|
|
Obj_Value(obj_handle, VF_GET, OBJV_I_ROOMNUM, &goal_room);
|
|
Obj_Value(obj_handle, VF_GET, OBJV_F_SIZE, &size);
|
|
AI_Value(obj_handle, VF_GET, AIV_F_CIRCLE_DIST, &circle_distance);
|
|
|
|
AI_AddGoal(obj_handle, AIG_GUARD_AREA, 1, 1.0f, -1, GF_NONFLUSHABLE, &goal_pos, goal_room);
|
|
AI_SetGoalCircleDist(obj_handle, 1, size * 2.0f);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_GT_AWARENESS, 1.0, 0.0, &aware);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_LTE_LAST_SEE_TARGET_INTERVAL, 1.0, 0.0, &time);
|
|
|
|
int gi = AI_AddGoal(obj_handle, AIG_GET_AROUND_OBJ, ACTIVATION_BLEND_LEVEL, 3.0f, -1,
|
|
GF_NONFLUSHABLE | GF_OBJ_IS_TARGET, OBJECT_HANDLE_NONE);
|
|
AI_GoalAddEnabler(obj_handle, gi, AIE_NEAR, 1.0, 0.0, &circle_distance);
|
|
AI_SetGoalCircleDist(obj_handle, gi, circle_distance);
|
|
} break;
|
|
|
|
case AIT_EVADER1: {
|
|
float circle_distance;
|
|
AI_Value(obj_handle, VF_GET, AIV_F_CIRCLE_DIST, &circle_distance);
|
|
|
|
AI_AddGoal(obj_handle, AIG_MOVE_RELATIVE_OBJ, 1, 1.0f, -1, GF_OBJ_IS_TARGET | GF_SPEED_ATTACK | GF_NONFLUSHABLE,
|
|
OBJECT_HANDLE_NONE);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_GT_AWARENESS, 1.0, 0.0, &aware);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_LTE_LAST_SEE_TARGET_INTERVAL, 1.0, 0.0, &time);
|
|
|
|
if (circle_distance > 0.0f) {
|
|
int gi = AI_AddGoal(obj_handle, AIG_GET_AROUND_OBJ, ACTIVATION_BLEND_LEVEL, 3.0f, -1,
|
|
GF_NONFLUSHABLE | GF_OBJ_IS_TARGET, OBJECT_HANDLE_NONE);
|
|
|
|
float temp = circle_distance * 0.55f;
|
|
AI_GoalAddEnabler(obj_handle, gi, AIE_NEAR, 1.0, 0.0, &temp);
|
|
AI_SetGoalCircleDist(obj_handle, gi, circle_distance * 0.55f);
|
|
}
|
|
} break;
|
|
|
|
case AIT_MELEE1: {
|
|
int flags = AISR_MELEE;
|
|
int m_goal;
|
|
|
|
AI_AddGoal(obj_handle, AIG_MOVE_RELATIVE_OBJ, 1, 1.0f, -1,
|
|
GF_NONFLUSHABLE | GF_OBJ_IS_TARGET | GF_KEEP_AT_COMPLETION, OBJECT_HANDLE_NONE);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_GT_AWARENESS, 1.0, 0.0, &aware);
|
|
AI_GoalAddEnabler(obj_handle, 1, AIE_LTE_LAST_SEE_TARGET_INTERVAL, 1.0, 0.0, &time);
|
|
|
|
AI_AddGoal(obj_handle, AIG_GET_TO_OBJ, 2, 1.0f, -1,
|
|
GF_SPEED_ATTACK | GF_NONFLUSHABLE | GF_OBJ_IS_TARGET | GF_USE_BLINE_IF_SEES_GOAL | GF_KEEP_AT_COMPLETION,
|
|
OBJECT_HANDLE_NONE);
|
|
AI_GoalAddEnabler(obj_handle, 2, AIE_AI_STATUS_FLAG, 1.0, 0.0, &flags);
|
|
AI_SetGoalCircleDist(obj_handle, 2, -100.0f);
|
|
AI_GoalAddEnabler(obj_handle, 2, AIE_GT_AWARENESS, 1.0, 0.0, &aware);
|
|
AI_GoalAddEnabler(obj_handle, 2, AIE_LTE_LAST_SEE_TARGET_INTERVAL, 1.0, 0.0, &time);
|
|
|
|
m_goal = AI_AddGoal(obj_handle, AIG_MELEE_TARGET, ACTIVATION_BLEND_LEVEL, 1.0f, -1,
|
|
GF_NONFLUSHABLE | GF_OBJ_IS_TARGET | GF_KEEP_AT_COMPLETION, OBJECT_HANDLE_NONE);
|
|
AI_GoalAddEnabler(obj_handle, m_goal, AIE_GT_AWARENESS, 1.0, 0.0, &aware);
|
|
AI_GoalAddEnabler(obj_handle, m_goal, AIE_LTE_LAST_SEE_TARGET_INTERVAL, 1.0, 0.0, &time);
|
|
} break;
|
|
|
|
case AIT_BIRD_FLOCK1: {
|
|
} break;
|
|
|
|
case AIT_HERD1: {
|
|
} break;
|
|
|
|
case AIT_STATIONARY_TURRET:
|
|
break;
|
|
|
|
case AIT_AIS:
|
|
break;
|
|
|
|
case AIT_FLYLANDER:
|
|
break;
|
|
|
|
default: {
|
|
mprintf(0, "Invalid AI type passed into AI_SafeSetType()\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
int TurnOnSpew(int objref, int gunpoint, int effect_type, float mass, float drag, int gravity_type, uint8_t isreal,
|
|
float lifetime, float interval, float longevity, float size, float speed, uint8_t random) {
|
|
msafe_struct mstruct;
|
|
|
|
mstruct.objhandle = objref;
|
|
mstruct.gunpoint = gunpoint;
|
|
mstruct.effect_type = effect_type;
|
|
mstruct.mass = mass;
|
|
mstruct.drag = drag;
|
|
mstruct.phys_info = gravity_type;
|
|
mstruct.is_real = isreal;
|
|
mstruct.lifetime = lifetime;
|
|
mstruct.interval = interval;
|
|
mstruct.longevity = longevity;
|
|
mstruct.size = size;
|
|
mstruct.speed = speed;
|
|
mstruct.random = (random) ? SPEW_RAND_SIZE | SPEW_RAND_LIFETIME | SPEW_RAND_SPEED : 0;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_START_SPEW, &mstruct);
|
|
|
|
return mstruct.id;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// aiObjScript
|
|
// all ai scripts will have this as a base function.
|
|
int16_t aiObjScript::CallEvent(int event, tOSIRISEventInfo *data) {
|
|
switch (event) {
|
|
case EVT_AI_INIT:
|
|
OnInit(data->me_handle);
|
|
break;
|
|
case EVT_AI_FRAME:
|
|
OnInterval(data);
|
|
break;
|
|
case EVT_AI_NOTIFY:
|
|
if (!OnNotify(data->me_handle, &data->evt_ai_notify))
|
|
return 0;
|
|
break;
|
|
case EVT_DESTROY:
|
|
OnDestroy(data->me_handle, &data->evt_destroy);
|
|
break;
|
|
case EVT_COLLIDE:
|
|
OnCollide(data->me_handle, data->evt_collide.it_handle);
|
|
break;
|
|
case EVT_DAMAGED:
|
|
OnDamaged(data->me_handle, &data->evt_damaged);
|
|
break;
|
|
case EVT_MEMRESTORE:
|
|
OnMemRestore(data->evt_memrestore.memory_ptr);
|
|
break;
|
|
}
|
|
|
|
return CONTINUE_CHAIN | CONTINUE_DEFAULT;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// aiSamirPest
|
|
// The Samir Pest
|
|
|
|
// ai base class
|
|
aiSamirPest::aiSamirPest() { memory = NULL; }
|
|
|
|
aiSamirPest::~aiSamirPest() {}
|
|
|
|
void aiSamirPest::OnInit(int me_handle) {
|
|
tOSIRISMEMCHUNK ch;
|
|
|
|
mprintf(0, "OnInit\n");
|
|
|
|
ch.id = 4;
|
|
ch.size = sizeof(aiSamirPest::t_pest_memory);
|
|
ch.my_id.type = OBJECT_SCRIPT;
|
|
ch.my_id.objhandle = me_handle;
|
|
|
|
memory = (aiSamirPest::t_pest_memory *)Scrpt_MemAlloc(&ch);
|
|
|
|
memory->tail_handle = CreateAndAttach(me_handle, "Pesttail", OBJ_ROBOT, 0, 0);
|
|
memory->last_anim_frame = 0;
|
|
|
|
memory->foot_sounds[0] = Sound_FindId("RbtPestFoot1");
|
|
memory->foot_sounds[1] = Sound_FindId("RbtPestFoot2");
|
|
memory->foot_sounds[2] = Sound_FindId("RbtPestFoot3");
|
|
|
|
memory->state = PEST_STATE_ROAM;
|
|
|
|
AI_SetType(me_handle, AIT_EVADER1);
|
|
}
|
|
|
|
void aiSamirPest::OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt) {
|
|
mprintf(0, "OnDestroy\n");
|
|
|
|
msafe_struct mstruct;
|
|
|
|
mstruct.objhandle = memory->tail_handle;
|
|
mstruct.killer_handle = OBJECT_HANDLE_NONE;
|
|
mstruct.damage_type = GD_SCRIPTED;
|
|
mstruct.amount = 1000.0f;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_DAMAGE_OBJECT, &mstruct);
|
|
}
|
|
|
|
void aiSamirPest::OnInterval(tOSIRISEventInfo *data) {
|
|
float frame;
|
|
Obj_Value(data->me_handle, VF_GET, OBJV_F_ANIM_FRAME, &frame);
|
|
if ((frame > 9 && memory->last_anim_frame <= 9) || (frame > 10 && memory->last_anim_frame <= 10) ||
|
|
(frame > 13 && memory->last_anim_frame <= 13)) {
|
|
int index = rand() % 3;
|
|
|
|
Sound_Play3d(data->me_handle, memory->foot_sounds[index]);
|
|
}
|
|
|
|
memory->last_anim_frame = frame;
|
|
}
|
|
|
|
void aiSamirPest::OnCollide(int me_handle, int it_handle) {
|
|
int type;
|
|
|
|
Obj_Value(it_handle, VF_GET, OBJV_I_TYPE, &type);
|
|
|
|
if (type == OBJ_PLAYER && memory->state != PEST_STATE_HIVE) {
|
|
// tell pest to return to hive.
|
|
int id = Scrpt_FindObjectName("Hive");
|
|
int room_index;
|
|
vector pos;
|
|
|
|
// force scripting for this.
|
|
AI_SetType(me_handle, AIT_AIS);
|
|
|
|
// go to hive location!
|
|
Obj_Value(id, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(id, VF_GET, OBJV_I_ROOMNUM, &room_index);
|
|
AI_AddGoal(me_handle, AIG_GET_TO_POS, 1, 1.0, -1, GF_KEEP_AT_COMPLETION | GF_NOTIFIES, &pos, room_index);
|
|
AI_SetGoalCircleDist(me_handle, 1, 10.0f);
|
|
|
|
memory->state = PEST_STATE_HIVE;
|
|
}
|
|
}
|
|
|
|
bool aiSamirPest::OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data) { return true; }
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// aiBlackStormTrooper
|
|
// CED Black Stormtrooper
|
|
|
|
// ai base class
|
|
aiBlackStormTrooper::aiBlackStormTrooper() { memory = NULL; }
|
|
|
|
aiBlackStormTrooper::~aiBlackStormTrooper() {}
|
|
|
|
void aiBlackStormTrooper::OnInit(int me_handle) {
|
|
tOSIRISMEMCHUNK ch;
|
|
int n_pts, j, i;
|
|
|
|
ch.id = 4;
|
|
ch.size = sizeof(aiBlackStormTrooper::t_bst_memory);
|
|
ch.my_id.type = OBJECT_SCRIPT;
|
|
ch.my_id.objhandle = me_handle;
|
|
|
|
memory = (aiBlackStormTrooper::t_bst_memory *)Scrpt_MemAlloc(&ch);
|
|
|
|
// reset timer for robot.
|
|
memory->timer = 0;
|
|
|
|
for (i = 0; i < N_SNIPE_POINTS; i++) {
|
|
memory->snipe_target_handles[i] = OBJECT_HANDLE_NONE;
|
|
}
|
|
|
|
// get sniper points available for trooper.
|
|
memory->cur_snipe_point = -1;
|
|
for (memory->snipe_points = 0; memory->snipe_points < N_SNIPE_POINTS; memory->snipe_points++) {
|
|
char buf[64];
|
|
char name[48];
|
|
int id;
|
|
Obj_Value(me_handle, VF_GET, OBJV_S_NAME, name);
|
|
snprintf(buf, sizeof(buf), "Snipe%s-%c", name, 'A' + memory->snipe_points);
|
|
id = Scrpt_FindObjectName(buf);
|
|
if (id == OBJECT_HANDLE_NONE) {
|
|
break;
|
|
}
|
|
memory->snipe_point_handles[memory->snipe_points] = id;
|
|
}
|
|
|
|
// determine reachable snipe points for each snipe point!
|
|
for (i = 0; i < memory->snipe_points; i++) {
|
|
for (j = 0, n_pts = 0; j < memory->snipe_points; j++) {
|
|
if (i != j) {
|
|
if (AI_IsObjReachable(memory->snipe_point_handles[i], memory->snipe_point_handles[j])) {
|
|
memory->access_array[i][n_pts++] = (int8_t)j;
|
|
}
|
|
}
|
|
}
|
|
memory->access_array[i][N_SNIPE_POINTS - 1] = (int8_t)n_pts;
|
|
}
|
|
|
|
// initialize miscellaneous stuff
|
|
memory->backpack_hp = 200;
|
|
memory->flee_hp = 50;
|
|
memory->gun_sight_obj = CreateAndAttach(me_handle, "STEmitter", OBJ_ROBOT, 0, 0, true, true);
|
|
memory->ammo_barrel = CreateAndAttach(me_handle, "StormTrooperBlackBarrel", OBJ_ROBOT, 1, 0, true, true);
|
|
memory->camera_obj = OBJECT_HANDLE_NONE;
|
|
|
|
memory->last_frame = 0.0f;
|
|
memory->target_handle = OBJECT_HANDLE_NONE;
|
|
memory->laser_targeted = false;
|
|
|
|
// just wait for a target.
|
|
set_state(me_handle, STATE_INIT);
|
|
}
|
|
|
|
void aiBlackStormTrooper::find_targets(int me_handle, bool new_snipe_point) {
|
|
vector pos; // position of trooper
|
|
int room; // room of trooper
|
|
|
|
int nearby_objs[32]; // objhandles for nearby players (or robots)
|
|
int player_objs[8]; // nearby players.
|
|
int nobjs, np; // number of objects and players
|
|
int closest_player = OBJECT_HANDLE_NONE; // handle of closest player
|
|
float closest_player_dist = 10000.0f; // distance of closest player from trooper.
|
|
float current_player_dist = 10000.0f; // used to store distance of current targeted player
|
|
int i, j;
|
|
|
|
// now determine target, closest player?
|
|
// get nearby objects within 1000 units of trooper. our substitution for FOV check, since troopers
|
|
// are usually behind walls.
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room);
|
|
|
|
nobjs = AI_GetNearbyObjs(&pos, room, 500.0f, nearby_objs, 32,
|
|
false, // lightmaps, no.
|
|
true, // players and robots
|
|
false, // don't include non collide objects
|
|
true); // stop at doors, don't check behind them.
|
|
|
|
// get only players.
|
|
for (i = 0, np = 0; i < nobjs && np < 8; i++) {
|
|
int objtype = -1;
|
|
|
|
Obj_Value(nearby_objs[i], VF_GET, OBJV_I_TYPE, &objtype);
|
|
if (objtype == OBJ_PLAYER) {
|
|
// if a player, check if it's in the target list
|
|
// (the trooper must have seen the target at least once before to keep track of it)
|
|
for (j = 0; j < N_SNIPE_POINTS; j++) {
|
|
if (nearby_objs[i] == memory->snipe_target_handles[j]) {
|
|
float dist;
|
|
player_objs[np++] = nearby_objs[i];
|
|
dist = AI_GetDistToObj(me_handle, nearby_objs[i]);
|
|
if (memory->target_handle == nearby_objs[i]) {
|
|
current_player_dist = dist;
|
|
}
|
|
if (closest_player_dist > dist) {
|
|
closest_player_dist = dist;
|
|
closest_player = nearby_objs[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//// lost all targets, go back into idle state.
|
|
if (closest_player == OBJECT_HANDLE_NONE) {
|
|
memory->target_handle = OBJECT_HANDLE_NONE;
|
|
return;
|
|
}
|
|
|
|
// if we don't have a target yet, make this player a target.
|
|
if (np && (memory->target_handle == OBJECT_HANDLE_NONE ||
|
|
(current_player_dist > closest_player_dist && memory->target_handle != closest_player))) {
|
|
memory->target_handle = closest_player;
|
|
AI_Value(me_handle, VF_SET, AIV_I_TARGET_HANDLE, &memory->target_handle);
|
|
mprintf(0, "BStorm: acquired new target through clairvoyance.\n");
|
|
}
|
|
|
|
// do something with snipe point if new_snipe_point is true (like find an ideal one)
|
|
}
|
|
|
|
void aiBlackStormTrooper::OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt) {
|
|
msafe_struct mstruct;
|
|
|
|
mstruct.objhandle = memory->ammo_barrel;
|
|
mstruct.killer_handle = OBJECT_HANDLE_NONE;
|
|
mstruct.damage_type = GD_SCRIPTED;
|
|
mstruct.amount = 1000.0f;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_DAMAGE_OBJECT, &mstruct);
|
|
|
|
if (memory->camera_obj != OBJECT_HANDLE_NONE) {
|
|
mstruct.objhandle = memory->camera_obj;
|
|
MSafe_CallFunction(MSAFE_OBJECT_REMOVE, &mstruct);
|
|
}
|
|
|
|
mstruct.objhandle = memory->gun_sight_obj;
|
|
MSafe_CallFunction(MSAFE_OBJECT_REMOVE, &mstruct);
|
|
}
|
|
|
|
void aiBlackStormTrooper::OnInterval(tOSIRISEventInfo *data) {
|
|
msafe_struct mstruct;
|
|
int me_handle = data->me_handle;
|
|
float anim_frame;
|
|
vector pos;
|
|
int room;
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_F_ANIM_FRAME, &anim_frame);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room);
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
|
|
// laser gunsight
|
|
if (memory->laser_targeted) {
|
|
ray_info ray;
|
|
matrix orient;
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_M_ORIENT, &orient);
|
|
|
|
// Determine real start pos - room
|
|
vector end_pos = pos;
|
|
end_pos += orient.fvec * 2000.0f;
|
|
|
|
int fvi_flags = FQ_CHECK_OBJS | FQ_IGNORE_POWERUPS | FQ_IGNORE_WEAPONS | FQ_IGNORE_MOVING_OBJECTS |
|
|
FQ_IGNORE_NON_LIGHTMAP_OBJECTS;
|
|
int fate = FVI_RayCast(me_handle, &pos, &end_pos, room, 0.0f, fvi_flags, &ray);
|
|
|
|
Obj_Value(memory->camera_obj, VF_SET, OBJV_I_ROOMNUM, &ray.hit_room);
|
|
Obj_Value(memory->camera_obj, VF_SET, OBJV_V_POS, &ray.hit_point);
|
|
|
|
if (Game_GetTime() >= memory->laser_timer) {
|
|
mstruct.objhandle = memory->camera_obj;
|
|
MSafe_CallFunction(MSAFE_OBJECT_REMOVE, &mstruct);
|
|
|
|
memory->camera_obj = OBJECT_HANDLE_NONE;
|
|
memory->laser_targeted = false;
|
|
}
|
|
}
|
|
|
|
// check if the pack is gone
|
|
int attached_handle = Obj_GetAttachChildHandle(me_handle, 1);
|
|
if (attached_handle == OBJECT_HANDLE_NONE) {
|
|
aObjDestroy(me_handle);
|
|
return;
|
|
}
|
|
|
|
// state code.
|
|
switch (memory->state) {
|
|
case STATE_HIDE:
|
|
// we didn't find a valid snipe point? try to find one after a certain amount of time.
|
|
// this will ensure that every so often, if there is no valid snipe point specified,
|
|
// we will try to find one.
|
|
if (memory->snipe_points == 0) {
|
|
if (Game_GetTime() > memory->timer) {
|
|
set_state(data->me_handle, STATE_SNIPE);
|
|
}
|
|
} else if (memory->cur_snipe_point == -1) {
|
|
if (Game_GetTime() > memory->timer) {
|
|
set_state(data->me_handle, STATE_HIDE);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_SNIPE:
|
|
// snipe for a certain amount of time, then return to base.
|
|
if (memory->laser_targeted == false) {
|
|
int type;
|
|
|
|
memory->camera_obj = Obj_Create(OBJ_POWERUP, Obj_FindID("Invisiblepowerup"), room, &pos, NULL, me_handle);
|
|
mstruct.objhandle = memory->camera_obj;
|
|
MSafe_CallFunction(MSAFE_OBJECT_NO_RENDER, &mstruct);
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &mstruct.roomnum);
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &mstruct.pos);
|
|
Obj_Value(memory->camera_obj, VF_GET, OBJV_V_POS, &mstruct.pos2);
|
|
|
|
mstruct.objhandle = memory->gun_sight_obj;
|
|
mstruct.ithandle = memory->camera_obj;
|
|
mstruct.lifetime = 5.0f;
|
|
mstruct.size = 1.0f;
|
|
|
|
mstruct.interval = 1.0f;
|
|
mstruct.count = 2;
|
|
mstruct.index = 1;
|
|
mstruct.texnum = Scrpt_FindTextureName("Stlaser");
|
|
mstruct.color = ((128 >> 3) << 10) | ((128 >> 3) << 5) | (128 >> 3);
|
|
|
|
mstruct.state = 0;
|
|
mstruct.flags = 0;
|
|
|
|
memory->laser_timer = Game_GetTime() + mstruct.lifetime;
|
|
memory->laser_targeted = true;
|
|
|
|
MSafe_CallFunction(MSAFE_WEATHER_LIGHTNING_BOLT, &mstruct);
|
|
// mprintf(0, "BStorm: TARGETING!!!\n");
|
|
}
|
|
|
|
if (Game_GetTime() > memory->timer) {
|
|
set_state(data->me_handle, STATE_HIDE);
|
|
}
|
|
break;
|
|
|
|
case STATE_BASE:
|
|
if (Game_GetTime() > memory->timer) {
|
|
if (memory->target_handle == OBJECT_HANDLE_NONE) {
|
|
set_state(data->me_handle, STATE_IDLE);
|
|
} else {
|
|
int roll = (rand() % 100);
|
|
if (roll <= 50) {
|
|
find_targets(data->me_handle, false);
|
|
memory->timer = Game_GetTime() + ((memory->target_handle == OBJECT_HANDLE_NONE) ? 5.0f : 2.0f);
|
|
}
|
|
}
|
|
}
|
|
if (Game_GetTime() > memory->snipe_timer) {
|
|
set_state(data->me_handle, STATE_SNIPE);
|
|
memory->snipe_timer = Game_GetTime() + 3.0f + (rand() % 7) * 0.5f;
|
|
}
|
|
break;
|
|
}
|
|
|
|
memory->last_frame = anim_frame;
|
|
}
|
|
|
|
void aiBlackStormTrooper::OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt) {
|
|
if (memory->state == STATE_BASE || memory->state == STATE_SNIPE) {
|
|
memory->flee_hp -= (int16_t)evt->damage;
|
|
if (memory->flee_hp <= 0) {
|
|
memory->flee_hp = 50;
|
|
set_state(me_handle, STATE_HIDE);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool aiBlackStormTrooper::OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data) {
|
|
switch (data->notify_type) {
|
|
case AIN_FIRED_WEAPON:
|
|
// Create gun barrel smoke
|
|
TurnOnSpew(me_handle, 0, 7, 0.0f, 0.0f, 65536, false, 0.5f, 0.1f, 0.7f, 1.0f, 15.0f, true);
|
|
break;
|
|
|
|
case AIN_SEE_TARGET:
|
|
memory->snipe_target_handles[0] = data->it_handle;
|
|
// mprintf(0, "BStorm: See target!!\n");
|
|
if (memory->state == STATE_IDLE) {
|
|
// mprintf(0, "BStorm: Found target!!\n");
|
|
memory->target_handle = memory->snipe_target_handles[0];
|
|
set_state(me_handle, STATE_HIDE);
|
|
|
|
mprintf(0, "Black Stormtrooper Saw target!\n");
|
|
}
|
|
break;
|
|
|
|
//@@ case AIN_SEE_TARGET:
|
|
//@@ // snipe!!
|
|
//@@ if (memory->state == STATE_IDLE) {
|
|
//@@ mprintf(0, "Acquired!\n");
|
|
//@@ set_state(me_handle, STATE_HIDE);
|
|
//@@ }
|
|
//@@ else if (memory->state == STATE_BASE && memory->timer < Game_GetTime()) {
|
|
//@@ if ((rand() % 100) < 60) {
|
|
//@@ set_state(me_handle, STATE_SNIPE);
|
|
//@@ }
|
|
//@@ memory->timer = Game_GetTime() + 2.0f + (rand() % 6)*0.5f;
|
|
//// use timer to switch states.
|
|
//@@ }
|
|
break;
|
|
|
|
case AIN_NEAR_TARGET:
|
|
if (memory->state == STATE_BASE) {
|
|
// mprintf(0, "BStorm: Near target, must flee.\n");
|
|
set_state(me_handle, STATE_HIDE);
|
|
}
|
|
break;
|
|
|
|
case AIN_GOAL_COMPLETE:
|
|
if (data->goal_uid == GOAL_UID_GETTOPOS) {
|
|
// mprintf(0, "BStorm: At hiding position %d.\n", memory->cur_snipe_point);
|
|
set_state(me_handle, STATE_BASE);
|
|
}
|
|
break;
|
|
|
|
case AIN_HIT_BY_WEAPON:
|
|
// check if backpack hit. if so, deduct hitpoints, if 0, frag it.
|
|
// also, if hit enough, force another hide action.
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void aiBlackStormTrooper::set_state(int me_handle, int state) {
|
|
int room_number, id, i;
|
|
vector pos;
|
|
char buf[64];
|
|
char name[48];
|
|
float dist;
|
|
|
|
// SafeGoalClearAll(me_handle);
|
|
|
|
retry_set_state:
|
|
switch (state) {
|
|
case STATE_INIT:
|
|
mprintf(0, "Black Stormtrooper set to init mode.\n");
|
|
state = STATE_IDLE;
|
|
goto retry_set_state;
|
|
|
|
case STATE_IDLE:
|
|
mprintf(0, "Black Stormtrooper set to idle mode.\n");
|
|
|
|
// AI_SafeSetType(me_handle, AIT_EVADER2);
|
|
///*
|
|
FlushGoal(me_handle, 1);
|
|
FlushGoal(me_handle, 2);
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
|
|
dist = 10.0f;
|
|
AI_AddGoal(me_handle, AIG_GUARD_AREA, 0, 1.0f, -1, GF_NOTIFIES | GF_KEEP_AT_COMPLETION, &pos, room_number);
|
|
AI_GoalValue(me_handle, 0, VF_SET, AIGV_F_CIRCLE_DIST, &dist);
|
|
//*/
|
|
|
|
// mprintf(0, "BStorm: Idling around.\n");
|
|
break;
|
|
|
|
case STATE_BASE:
|
|
mprintf(0, "Black Stormtrooper set to base mode.\n");
|
|
|
|
// AI_SafeSetType(me_handle, AIT_EVADER2);
|
|
///*
|
|
// okay, we will stand still, wait to snipe, face target, try to acquire targets, etc.
|
|
FlushGoal(me_handle, 1);
|
|
FlushGoal(me_handle, 2);
|
|
if (memory->cur_snipe_point == -1) {
|
|
// mprintf(0, "BStorm: Eek! We should be hiding, not basing!");
|
|
state = STATE_HIDE;
|
|
goto retry_set_state;
|
|
}
|
|
id = memory->snipe_point_handles[memory->cur_snipe_point];
|
|
Obj_Value(id, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(id, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
|
|
dist = 0.0f;
|
|
AI_AddGoal(me_handle, AIG_GUARD_AREA, 2, 1.0f, -1, GF_KEEP_AT_COMPLETION, &pos, room_number);
|
|
AI_GoalValue(me_handle, 2, VF_SET, AIGV_F_CIRCLE_DIST, &dist);
|
|
//*/
|
|
|
|
/*
|
|
AI_AddGoal(me_handle, AIG_FIRE_AT_OBJ, ACTIVATION_BLEND_LEVEL, 1.0f, -1, GF_ORIENT_TARGET |
|
|
GF_SPEED_ATTACK |
|
|
GF_KEEP_AT_COMPLETION |
|
|
GF_OBJ_IS_TARGET,
|
|
NULL, GST_NEG_FVEC);
|
|
*/
|
|
|
|
memory->timer = Game_GetTime() + 2.0f; // use timer to reacquire targets.
|
|
memory->snipe_timer = Game_GetTime() + 2.0f + (rand() % 7) * 0.5f;
|
|
|
|
find_targets(me_handle, false);
|
|
// mprintf(0, "BStorm: Base\n");
|
|
break;
|
|
|
|
case STATE_HIDE:
|
|
mprintf(0, "Black Stormtrooper set to hide mode.\n");
|
|
// find a snipe point that's different from current one.
|
|
FlushGoal(me_handle, 1);
|
|
FlushGoal(me_handle, 2);
|
|
if (memory->snipe_points) {
|
|
if (memory->cur_snipe_point == -1) {
|
|
// find a snipe point now!
|
|
// bot is either fresh or has been wandering, looking for a place to snipe
|
|
for (i = 0; i < memory->snipe_points; i++) {
|
|
if (AI_IsObjReachable(me_handle, memory->snipe_point_handles[i])) {
|
|
break;
|
|
}
|
|
}
|
|
if (i == memory->snipe_points) {
|
|
// hmm, the bot is not in a good place. have him wander until he finds a snipe point.
|
|
memory->timer = Game_GetTime() + 4.0f;
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
AI_AddGoal(me_handle, AIG_WANDER_AROUND, 1, 1.0f, -1, GF_SPEED_FLEE | GF_NOTIFIES | GF_KEEP_AT_COMPLETION,
|
|
&pos, room_number);
|
|
mprintf(0, "BStorm: WARNING-0 no valid snipe point nearby. Design problem?\n");
|
|
} else {
|
|
memory->cur_snipe_point = i;
|
|
}
|
|
} else {
|
|
// this will get an index into the reachable snipe points.
|
|
int n_valid_pts = memory->access_array[memory->cur_snipe_point][N_SNIPE_POINTS - 1];
|
|
|
|
if (n_valid_pts > 0) {
|
|
i = rand() % n_valid_pts;
|
|
memory->cur_snipe_point = memory->access_array[memory->cur_snipe_point][i];
|
|
} else {
|
|
memory->cur_snipe_point = -1; // force a periodic check for a snipe point.
|
|
memory->timer = Game_GetTime() + 4.0f;
|
|
mprintf(0, "BStorm: WARNING-1 no valid snipe point nearby. Design problem?\n");
|
|
}
|
|
}
|
|
|
|
// by now, we either have a new valid snipe point, or no snipe point.
|
|
if (memory->cur_snipe_point != -1) {
|
|
id = memory->snipe_point_handles[memory->cur_snipe_point];
|
|
Obj_Value(id, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(id, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
AI_AddGoal(me_handle, AIG_GET_TO_POS, 2, 1.0, GOAL_UID_GETTOPOS, GF_NOTIFIES | GF_SPEED_FLEE | GF_NONFLUSHABLE,
|
|
&pos, room_number);
|
|
/*
|
|
AI_AddGoal(me_handle, AIG_FIRE_AT_OBJ, ACTIVATION_BLEND_LEVEL, 1.0f, -1, GF_ORIENT_TARGET |
|
|
GF_SPEED_ATTACK |
|
|
GF_KEEP_AT_COMPLETION |
|
|
GF_OBJ_IS_TARGET,
|
|
NULL, GST_NEG_FVEC);
|
|
*/
|
|
|
|
// mprintf(0, "BStorm: Hide to point %d\n", memory->cur_snipe_point+1);
|
|
}
|
|
} else {
|
|
// bot will track you with no snipe points..
|
|
memory->timer = Game_GetTime() + 3.0f;
|
|
dist = 80.0f + ((float)rand() / (float)RAND_MAX) * 10.0f;
|
|
AI_AddGoal(me_handle, AIG_MOVE_RELATIVE_OBJ, 1, 1.0f, -1, GF_OBJ_IS_TARGET | GF_KEEP_AT_COMPLETION,
|
|
OBJECT_HANDLE_NONE);
|
|
AI_GoalValue(me_handle, 1, VF_SET, AIGV_F_CIRCLE_DIST, &dist);
|
|
}
|
|
break;
|
|
|
|
case STATE_SNIPE:
|
|
mprintf(0, "Black Stormtrooper set to snipe mode.\n");
|
|
|
|
// AI_SafeSetType(me_handle, AIT_EVADER1);
|
|
///*
|
|
FlushGoal(me_handle, 2);
|
|
|
|
// dist = 80.0f + ((float)rand()/(float)RAND_MAX)*10.0f;
|
|
AI_AddGoal(me_handle, AIG_MOVE_RELATIVE_OBJ, 1, 1.0f, -1, GF_OBJ_IS_TARGET | GF_KEEP_AT_COMPLETION,
|
|
OBJECT_HANDLE_NONE);
|
|
// AI_GoalValue(me_handle, 1, VF_SET, AIGV_F_CIRCLE_DIST, &dist);
|
|
//*/
|
|
|
|
/*
|
|
AI_AddGoal(me_handle, AIG_FIRE_AT_OBJ, ACTIVATION_BLEND_LEVEL, 1.0f, -1, GF_ORIENT_TARGET |
|
|
GF_SPEED_ATTACK |
|
|
GF_KEEP_AT_COMPLETION |
|
|
GF_OBJ_IS_TARGET,
|
|
NULL, GST_NEG_FVEC);
|
|
*/
|
|
|
|
memory->timer = Game_GetTime() + 8.0f;
|
|
// mprintf(0, "BStorm: Snipe!\n");
|
|
break;
|
|
}
|
|
|
|
memory->state = state;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// aiCreeper
|
|
|
|
// ai base class
|
|
aiCreeper::aiCreeper() { memory = NULL; }
|
|
|
|
aiCreeper::~aiCreeper() {}
|
|
|
|
void aiCreeper::OnInit(int me_handle) {
|
|
tOSIRISMEMCHUNK ch;
|
|
int flags;
|
|
|
|
ch.id = 4;
|
|
ch.size = sizeof(aiCreeper::t_creep_memory);
|
|
ch.my_id.type = OBJECT_SCRIPT;
|
|
ch.my_id.objhandle = me_handle;
|
|
|
|
memory = (aiCreeper::t_creep_memory *)Scrpt_MemAlloc(&ch);
|
|
|
|
// reset timer for robot.
|
|
memory->timer = 0;
|
|
|
|
// initialize the creeper eye
|
|
memory->eye_obj = CreateAndAttach(me_handle, "CreeperEye", OBJ_ROBOT, 0, 0);
|
|
memory->eye_timer = 0.0f;
|
|
|
|
// get sniper points available for trooper. n
|
|
memory->state = -1;
|
|
memory->energy = 10;
|
|
memory->hits = 50;
|
|
|
|
AI_Value(me_handle, VF_GET, AIV_I_FLAGS, &flags);
|
|
memory->melee_flags = (uint8_t)(flags & (AIF_MELEE1 | AIF_MELEE2));
|
|
|
|
// set initial state of creeper
|
|
set_state(me_handle, STATE_IDLE);
|
|
}
|
|
|
|
void aiCreeper::OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt) {
|
|
if (memory->state == STATE_ATTACK) {
|
|
if (memory->hits > -25) {
|
|
memory->hits = memory->hits - (int8_t)evt->damage;
|
|
// mprintf(0, "[Creeper] hits = %d\n", memory->hits);
|
|
}
|
|
if (memory->hits < 0) {
|
|
set_state(me_handle, STATE_STALK);
|
|
}
|
|
}
|
|
}
|
|
|
|
void aiCreeper::OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt) {
|
|
msafe_struct mstruct;
|
|
|
|
mstruct.objhandle = memory->eye_obj;
|
|
mstruct.killer_handle = OBJECT_HANDLE_NONE;
|
|
mstruct.damage_type = GD_SCRIPTED;
|
|
mstruct.amount = 1000.0f;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_DAMAGE_OBJECT, &mstruct);
|
|
}
|
|
|
|
void aiCreeper::OnInterval(tOSIRISEventInfo *data) {
|
|
int flags;
|
|
|
|
switch (memory->state) {
|
|
case STATE_IDLE:
|
|
case STATE_STALK:
|
|
if (memory->timer < Game_GetTime()) {
|
|
if (memory->energy < 10) {
|
|
memory->energy += (memory->state == STATE_IDLE) ? 2 : 1;
|
|
memory->timer = Game_GetTime() + 1.0f;
|
|
if (memory->energy > 10) {
|
|
memory->energy = 10;
|
|
}
|
|
}
|
|
if (memory->hits < 25) {
|
|
memory->hits += (memory->state == STATE_IDLE) ? 10 : 7;
|
|
}
|
|
|
|
if (memory->state == STATE_STALK && memory->eye_timer != 0.0f) {
|
|
// now cloak rest of body 1 sec after eye cloaked.
|
|
msafe_struct m;
|
|
m.objhandle = data->me_handle;
|
|
m.lifetime = memory->eye_timer;
|
|
m.state = 1;
|
|
|
|
memory->eye_timer = 0.0f;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_CLOAK, &m);
|
|
}
|
|
}
|
|
|
|
if (memory->state == STATE_STALK && memory->eye_timer == 0.0f) {
|
|
if (!Obj_IsEffect(data->me_handle, EF_FADING_OUT | EF_CLOAKED)) {
|
|
set_state(data->me_handle, STATE_ATTACK);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_ATTACK:
|
|
if (memory->timer < Game_GetTime()) {
|
|
// recloak and recharge.
|
|
set_state(data->me_handle, STATE_STALK);
|
|
} else {
|
|
flags = AIF_MELEE2;
|
|
if (memory->energy <= 5 && (memory->melee_flags & AIF_MELEE2)) {
|
|
AI_Value(data->me_handle, VF_CLEAR_FLAGS, AIV_I_FLAGS, &flags);
|
|
memory->melee_flags &= (~AIF_MELEE2);
|
|
} else if (memory->energy > 5 && !(memory->melee_flags & AIF_MELEE2)) {
|
|
AI_Value(data->me_handle, VF_SET_FLAGS, AIV_I_FLAGS, &flags);
|
|
memory->melee_flags |= AIF_MELEE2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool aiCreeper::OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data) {
|
|
vector pos;
|
|
int room_number;
|
|
|
|
switch (data->notify_type) {
|
|
case AIN_SEE_TARGET:
|
|
case AIN_NEAR_TARGET:
|
|
// rules: leave's base state if recharged enough-->cloaks
|
|
if (memory->state == STATE_IDLE) {
|
|
set_state(me_handle, STATE_STALK);
|
|
}
|
|
// mprintf(0, "[Creeper] see notify %d\n", data->notify_type);
|
|
break;
|
|
|
|
case AIN_GOAL_COMPLETE:
|
|
case AIN_PLAYER_SEES_YOU:
|
|
case AIN_ANIM_COMPLETE:
|
|
case AIN_BUMPED_OBJ:
|
|
break;
|
|
|
|
case AIN_MELEE_ATTACK_FRAME:
|
|
// reduce energy accordingly depending on attack.
|
|
if (data->attack_num == 1) {
|
|
// blast ring attack
|
|
int wpnid;
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
Obj_GetGunPos(me_handle, 0, &pos);
|
|
|
|
wpnid = Wpn_FindID("CreeperBlastRing");
|
|
Obj_Create(OBJ_WEAPON, wpnid, room_number, &pos, NULL, me_handle);
|
|
|
|
memory->energy -= 5;
|
|
// mprintf(0, "[Creeper] Special attack.\n");
|
|
} else {
|
|
// standard attack
|
|
memory->energy -= 3;
|
|
}
|
|
|
|
if (memory->energy < 3) {
|
|
set_state(me_handle, STATE_STALK);
|
|
}
|
|
break;
|
|
|
|
// default:
|
|
// mprintf(0, "[Creeper] notify %d\n", data->notify_type);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void aiCreeper::set_state(int me_handle, int state) {
|
|
int room_number = 0, id, i, flags;
|
|
vector pos;
|
|
char buf[64];
|
|
char name[48];
|
|
msafe_struct m;
|
|
float circle_dist;
|
|
char cenable;
|
|
|
|
retry_set_state:
|
|
switch (state) {
|
|
case STATE_IDLE:
|
|
// stay at one position until ready. note that if attacked , it will flee in this state.
|
|
FlushGoal(me_handle, 2);
|
|
|
|
memory->timer = Game_GetTime() + 1.0f;
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room_number);
|
|
|
|
AI_AddGoal(me_handle, AIG_GET_TO_POS, 1, 1.0f, -1, GF_KEEP_AT_COMPLETION, &pos, room_number);
|
|
// mprintf(0, "[Creeper] Idle\n");
|
|
break;
|
|
|
|
case STATE_STALK:
|
|
// cloak and stalk player.
|
|
// FlushGoal(me_handle, 3); // take out any priority goals set.
|
|
FlushGoal(me_handle, 2); // take out any goals previously set
|
|
|
|
memory->timer = Game_GetTime() + 1.0f; // set timer for recharge!
|
|
// memory->timer = Game_GetTime() + 1.0f + (rand()%4) * 0.5f;
|
|
|
|
m.objhandle = memory->eye_obj;
|
|
m.lifetime = 5.0f + (rand() % 20) * 0.5f;
|
|
m.state = 1;
|
|
|
|
memory->eye_timer = m.lifetime;
|
|
|
|
MSafe_CallFunction(MSAFE_OBJECT_CLOAK, &m);
|
|
|
|
circle_dist = 40.0f;
|
|
AI_Value(me_handle, VF_SET, AIV_F_CIRCLE_DIST, &circle_dist);
|
|
// AI_AddGoal(me_handle, AIG_WANDER_AROUND, 3, 1.0f, -1, GF_ORIENT_VELOCITY | GF_NOTIFIES |
|
|
// GF_KEEP_AT_COMPLETION, &pos, room_number);
|
|
AI_AddGoal(me_handle, AIG_MOVE_RELATIVE_OBJ, 2, 1.0f, -1, GF_KEEP_AT_COMPLETION | GF_OBJ_IS_TARGET, NULL, 0);
|
|
|
|
flags = AIF_DISABLE_MELEE;
|
|
AI_Value(me_handle, VF_SET_FLAGS, AIV_I_FLAGS, &flags);
|
|
|
|
cenable = 1;
|
|
Obj_Value(me_handle, VF_SET, OBJV_C_NEGATIVE_LIGHT, &cenable);
|
|
// mprintf(0, "[Creeper] Stalk\n");
|
|
break;
|
|
|
|
case STATE_ATTACK:
|
|
// FlushGoal(me_handle, 3); // we've decloaked, attack.
|
|
FlushGoal(me_handle, 2);
|
|
|
|
memory->timer = Game_GetTime() + 4.0f + (float)(rand() % 3);
|
|
flags = AIF_DISABLE_MELEE;
|
|
AI_Value(me_handle, VF_CLEAR_FLAGS, AIV_I_FLAGS, &flags);
|
|
|
|
cenable = 1;
|
|
Obj_Value(me_handle, VF_SET, OBJV_C_NEGATIVE_LIGHT, &cenable);
|
|
// mprintf(0, "[Creeper] Attack\n");
|
|
break;
|
|
}
|
|
|
|
memory->state = state;
|
|
}
|
|
|
|
// aiLukeTurret
|
|
// Luke's funky turret
|
|
|
|
void aiLukeTurret::OnInit(int me_handle) {
|
|
tOSIRISMEMCHUNK ch;
|
|
|
|
ch.id = 4;
|
|
ch.size = sizeof(aiLukeTurret::t_lturret_memory);
|
|
ch.my_id.type = OBJECT_SCRIPT;
|
|
ch.my_id.objhandle = me_handle;
|
|
|
|
memory = (aiLukeTurret::t_lturret_memory *)Scrpt_MemAlloc(&ch);
|
|
memory->timer = -1.0f;
|
|
|
|
// turrets only activate when they 'hear' a player fire a weapon.
|
|
int flags = AIF_DISABLE_FIRING;
|
|
|
|
AI_SetType(me_handle, AIT_AIS);
|
|
AI_Value(me_handle, VF_SET_FLAGS, AIV_I_FLAGS, &flags);
|
|
}
|
|
|
|
bool aiLukeTurret::OnNotify(int me_handle, tOSIRISEVTAINOTIFY *data) {
|
|
int flags;
|
|
|
|
switch (data->notify_type) {
|
|
case AIN_OBJ_FIRED:
|
|
// set 5 seconds for turret awareness before it becomes inert.
|
|
memory->timer = Game_GetTime() + 12.0f;
|
|
AI_SetType(me_handle, AIT_STATIONARY_TURRET);
|
|
flags = AIF_DISABLE_FIRING;
|
|
AI_Value(me_handle, VF_CLEAR_FLAGS, AIV_I_FLAGS, &flags);
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void aiLukeTurret::OnInterval(tOSIRISEventInfo *data) {
|
|
if (memory->timer == -1.0f)
|
|
return;
|
|
|
|
if (memory->timer < Game_GetTime()) {
|
|
int flags = AIF_DISABLE_FIRING;
|
|
AI_SetType(data->me_handle, AIT_AIS);
|
|
AI_Value(data->me_handle, VF_SET_FLAGS, AIV_I_FLAGS, &flags);
|
|
memory->timer = -1.0f;
|
|
}
|
|
}
|
|
|
|
// aiSTBlackBarrel
|
|
// Black Stormtrooper's ammo pack
|
|
|
|
void aiSTBlackBarrel::OnInit(int me_handle) {
|
|
tOSIRISMEMCHUNK ch;
|
|
|
|
ch.id = 4;
|
|
ch.size = sizeof(aiSTBlackBarrel::t_stblackbarrel_memory);
|
|
ch.my_id.type = OBJECT_SCRIPT;
|
|
ch.my_id.objhandle = me_handle;
|
|
|
|
memory = (aiSTBlackBarrel::t_stblackbarrel_memory *)Scrpt_MemAlloc(&ch);
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_M_ORIENT, &memory->orient);
|
|
}
|
|
|
|
void aiSTBlackBarrel::OnDestroy(int me_handle, tOSIRISEVTDESTROY *evt) {
|
|
// Create a frag burst effect
|
|
vector pos, new_dir;
|
|
int room;
|
|
matrix orient, new_orient;
|
|
|
|
Obj_Value(me_handle, VF_GET, OBJV_V_POS, &pos);
|
|
Obj_Value(me_handle, VF_GET, OBJV_I_ROOMNUM, &room);
|
|
Obj_Value(me_handle, VF_GET, OBJV_M_ORIENT, &orient);
|
|
|
|
new_dir = -orient.fvec;
|
|
vm_VectorToMatrix(&new_orient, &new_dir, NULL, NULL);
|
|
|
|
Obj_Create(OBJ_WEAPON, Wpn_FindID("AlienBossFragBurst"), room, &pos, &new_orient, me_handle);
|
|
|
|
// mprintf(0,"Barrel destroyed.\n");
|
|
}
|
|
|
|
void aiSTBlackBarrel::OnDamaged(int me_handle, tOSIRISEVTDAMAGED *evt) {
|
|
// mprintf(0,"Barrel damaged...\n");
|
|
}
|