/* Copyright (C) 2015-2019 Parallel Realities 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "capitalShips.h" static int steer(void); static void think(void); static void gunThink(void); static void gunDie(void); static void handleDisabled(void); static void componentDie(void); static void engineThink(void); static void engineDie(void); static void findAITarget(void); static void loadCapitalShipDef(char *filename); static void loadComponents(Entity *parent, cJSON *components); static void loadGuns(Entity *parent, cJSON *guns); static void loadEngines(Entity *parent, cJSON *engines); static void disable(void); static void issueEnginesDestroyedMessage(Entity *cap); static void issueGunsDestroyedMessage(Entity *cap); static void issueDamageMessage(Entity *cap); static Entity defHead, *defTail; Entity *spawnCapitalShip(char *name, int x, int y, int side) { Entity *def, *e, *capitalShip; capitalShip = NULL; for (def = defHead.next ; def != NULL ; def = def->next) { if ((strcmp(def->name, name) == 0) || (def->owner != NULL && strcmp(def->owner->name, name) == 0)) { e = spawnEntity(); memcpy(e, def, sizeof(Entity)); e->next = NULL; e->x = x; e->y = y; e->side = side; if (e->type == ET_CAPITAL_SHIP) { capitalShip = e; } else { e->owner = capitalShip; e->x = -SCREEN_WIDTH; e->y = -SCREEN_HEIGHT; } } } if (!capitalShip) { printf("Error: no such capital ship '%s'\n", name); exit(1); } return capitalShip; } void doCapitalShip(void) { if (self->alive == ALIVE_ALIVE) { handleDisabled(); if (self->health <= 0) { self->health = 0; self->alive = ALIVE_DYING; self->die(); if (self == battle.missionTarget) { battle.missionTarget = NULL; } if (self->side == player->side) { battle.stats[STAT_CAPITAL_SHIPS_LOST]++; runScriptFunction("CAPITAL_SHIPS_LOST %d", battle.stats[STAT_CAPITAL_SHIPS_LOST]); } else { battle.stats[STAT_CAPITAL_SHIPS_DESTROYED]++; runScriptFunction("CAPITAL_SHIPS_DESTROYED %d", battle.stats[STAT_CAPITAL_SHIPS_DESTROYED]); } runScriptFunction(self->name); } } } static void think(void) { float dir; int wantedAngle; if (--self->aiActionTime <= 0) { findAITarget(); } wantedAngle = steer(); if (fabs(wantedAngle - self->angle) > TURN_THRESHOLD) { dir = ((int)(wantedAngle - self->angle + 360)) % 360 > 180 ? -1 : 1; dir *= TURN_SPEED; self->angle += dir; self->angle = mod(self->angle, 360); } applyFighterThrust(); } static void findAITarget(void) { Entity *e; unsigned int dist, closest; self->target = NULL; dist = closest = MAX_TARGET_RANGE; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->active && e->side != self->side && e->flags & EF_AI_TARGET) { dist = getDistance(self->x, self->y, e->x, e->y); if (!self->target || dist < closest) { self->target = e; closest = dist; } } } if (self->target) { self->targetLocation.x = self->target->x + (rand() % 1000 - rand() % 1000); self->targetLocation.y = self->target->y + (rand() % 1000 - rand() % 1000); self->aiActionTime = FPS * 15; } else { self->targetLocation.x = 500 + (rand() % (BATTLE_AREA_WIDTH - 1000)); self->targetLocation.y = 500 + (rand() % (BATTLE_AREA_HEIGHT - 1000)); self->aiActionTime = FPS * (30 + (rand() % 120)); } } static int steer(void) { int wantedAngle; int angle; int distance; float dx, dy, force; int count; Entity *e, **candidates; int i; dx = dy = 0; count = 0; force = 0; candidates = getAllEntsInRadius(self->x, self->y, 2000, self); for (i = 0, e = candidates[i] ; e != NULL ; e = candidates[++i]) { if (e->type == ET_CAPITAL_SHIP) { distance = getDistance(e->x, e->y, self->x, self->y); if (distance > 0 && distance < self->separationRadius) { angle = getAngle(self->x, self->y, e->x, e->y); dx += sin(TO_RAIDANS(angle)); dy += -cos(TO_RAIDANS(angle)); force += (self->separationRadius - distance); count++; } } } if (count > 0) { dx /= count; dy /= count; dx *= force; dy *= force; self->dx -= (dx * 0.001); self->dy -= (dy * 0.001); self->targetLocation.x -= dx; self->targetLocation.y -= dy; self->aiActionTime = FPS * 10; } wantedAngle = getAngle(self->x, self->y, self->targetLocation.x, self->targetLocation.y); wantedAngle %= 360; return wantedAngle; } static void gunThink(void) { doAI(); handleDisabled(); } static void componentDie(void) { self->alive = ALIVE_DEAD; addSmallExplosion(); playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y); addDebris(self->x, self->y, 3 + rand() % 4); self->owner->health--; if (self->owner->health > 0) { runScriptFunction("CAP_HEALTH %s %d", self->owner->name, self->owner->health); if (self->side != SIDE_PANDORAN && self->side == player->side) { issueDamageMessage(self->owner); } } } static void gunDie(void) { Entity *e; self->alive = ALIVE_DEAD; addSmallExplosion(); playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y); addDebris(self->x, self->y, 3 + rand() % 4); for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e != self && e->health > 0 && e->owner == self->owner && e->type == ET_COMPONENT_GUN) { return; } } runScriptFunction("CAP_GUNS_DESTROYED %s", self->owner->name); if (self->side != SIDE_PANDORAN && self->side == player->side) { issueGunsDestroyedMessage(self->owner); } if (--self->owner->systemPower == 1) { disable(); } } static void engineThink(void) { handleDisabled(); addLargeEngineEffect(); } static void engineDie(void) { Entity *e; self->alive = ALIVE_DEAD; addSmallExplosion(); playBattleSound(SND_EXPLOSION_1 + rand() % 4, self->x, self->y); addDebris(self->x, self->y, 4 + rand() % 9); for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e != self && e->health > 0 && e->owner == self->owner && e->type == ET_COMPONENT_ENGINE) { return; } } /* no more engines - stop moving */ if (self->owner->health > 0) { self->owner->speed = 0; self->owner->action = NULL; self->owner->dx = self->owner->dy = 0; runScriptFunction("CAP_ENGINES_DESTROYED %s", self->owner->name); if (self->side != SIDE_PANDORAN && self->side == player->side) { issueEnginesDestroyedMessage(self->owner); } } if (--self->owner->systemPower == 1) { disable(); } } static void die(void) { Entity *e; self->alive = ALIVE_DEAD; playBattleSound(SND_CAP_DEATH, self->x, self->y); addLargeExplosion(); addDebris(self->x, self->y, 12); for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->owner == self) { e->alive = ALIVE_DEAD; } } updateObjective(self->name, TT_DESTROY); updateObjective(self->groupName, TT_DESTROY); updateCondition(self->name, TT_DESTROY); updateCondition(self->groupName, TT_DESTROY); } static void handleDisabled(void) { if (self->systemPower <= 0 || (self->flags & EF_DISABLED)) { self->dx *= 0.99; self->dy *= 0.99; self->thrust = 0; self->shield = self->maxShield = 0; self->action = NULL; } } static void disable(void) { Entity *e; runScriptFunction("CAP_DISABLED %s", self->owner->name); for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->owner == self->owner || e == self->owner) { e->systemPower = 0; e->flags |= EF_DISABLED; } } updateObjective(self->owner->name, TT_DISABLE); updateObjective(self->owner->groupName, TT_DISABLE); } void loadCapitalShipDefs(void) { char **filenames; char path[MAX_FILENAME_LENGTH]; int count, i; memset(&defHead, 0, sizeof(Entity)); defTail = &defHead; filenames = getFileList("data/capitalShips", &count); for (i = 0 ; i < count ; i++) { sprintf(path, "data/capitalShips/%s", filenames[i]); loadCapitalShipDef(path); free(filenames[i]); } free(filenames); } static void loadCapitalShipDef(char *filename) { cJSON *root; char *text; Entity *e; SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, "Loading %s", filename); text = readFile(filename); root = cJSON_Parse(text); if (root) { e = malloc(sizeof(Entity)); memset(e, 0, sizeof(Entity)); defTail->next = e; defTail = e; e->type = ET_CAPITAL_SHIP; e->active = 1; STRNCPY(e->name, cJSON_GetObjectItem(root, "name")->valuestring, MAX_NAME_LENGTH); STRNCPY(e->defName, e->name, MAX_NAME_LENGTH); e->shield = e->maxShield = cJSON_GetObjectItem(root, "shield")->valueint; e->shieldRechargeRate = cJSON_GetObjectItem(root, "shieldRechargeRate")->valueint; e->texture = getAtlasImage(cJSON_GetObjectItem(root, "texture")->valuestring); e->speed = 1; e->systemPower = 3; e->flags = EF_NO_HEALTH_BAR; e->action = think; e->die = die; e->w = e->texture->rect.w; e->h = e->texture->rect.h; e->separationRadius = MAX(e->w, e->h); loadComponents(e, cJSON_GetObjectItem(root, "components")); loadGuns(e, cJSON_GetObjectItem(root, "guns")); loadEngines(e, cJSON_GetObjectItem(root, "engines")); cJSON_Delete(root); } else { SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Failed to load '%s'", filename); } free(text); } static void loadComponents(Entity *parent, cJSON *components) { Entity *e; cJSON *component; parent->health = 0; if (components) { component = components->child; while (component) { e = malloc(sizeof(Entity)); memset(e, 0, sizeof(Entity)); defTail->next = e; defTail = e; e->active = 1; e->type = ET_COMPONENT; e->health = e->maxHealth = cJSON_GetObjectItem(component, "health")->valueint; e->offsetX = cJSON_GetObjectItem(component, "x")->valueint; e->offsetY = cJSON_GetObjectItem(component, "y")->valueint; e->texture = getAtlasImage(cJSON_GetObjectItem(component, "texture")->valuestring); e->w = e->texture->rect.w; e->h = e->texture->rect.h; if (cJSON_GetObjectItem(component, "flags")) { e->flags = flagsToLong(cJSON_GetObjectItem(component, "flags")->valuestring, NULL); } if (cJSON_GetObjectItem(component, "aiFlags")) { e->aiFlags = flagsToLong(cJSON_GetObjectItem(component, "aiFlags")->valuestring, NULL); } e->systemPower = MAX_SYSTEM_POWER; e->die = componentDie; e->owner = parent; component = component->next; parent->health++; } } parent->maxHealth = parent->health; } static void loadGuns(Entity *parent, cJSON *guns) { Entity *e; cJSON *gun; if (guns) { gun = guns->child; while (gun) { e = malloc(sizeof(Entity)); memset(e, 0, sizeof(Entity)); defTail->next = e; defTail = e; e->active = 1; e->type = ET_COMPONENT_GUN; e->health = e->maxHealth = cJSON_GetObjectItem(gun, "health")->valueint; e->reloadTime = cJSON_GetObjectItem(gun, "reloadTime")->valueint; e->offsetX = cJSON_GetObjectItem(gun, "x")->valueint; e->offsetY = cJSON_GetObjectItem(gun, "y")->valueint; e->texture = getAtlasImage(cJSON_GetObjectItem(gun, "texture")->valuestring); e->guns[0].type = lookup(cJSON_GetObjectItem(gun, "type")->valuestring); e->missiles = getJSONValue(gun, "missiles", 0); if (cJSON_GetObjectItem(gun, "flags")) { e->flags = flagsToLong(cJSON_GetObjectItem(gun, "flags")->valuestring, NULL); } if (cJSON_GetObjectItem(gun, "aiFlags")) { e->aiFlags = flagsToLong(cJSON_GetObjectItem(gun, "aiFlags")->valuestring, NULL); } e->w = e->texture->rect.w; e->h = e->texture->rect.h; e->systemPower = MAX_SYSTEM_POWER; e->action = gunThink; e->die = gunDie; e->owner = parent; gun = gun->next; } } } static void loadEngines(Entity *parent, cJSON *engines) { Entity *e; cJSON *engine; if (engines) { engine = engines->child; while (engine) { e = malloc(sizeof(Entity)); memset(e, 0, sizeof(Entity)); defTail->next = e; defTail = e; e->active = 1; e->type = ET_COMPONENT_ENGINE; e->health = e->maxHealth = cJSON_GetObjectItem(engine, "health")->valueint; e->offsetX = cJSON_GetObjectItem(engine, "x")->valueint; e->offsetY = cJSON_GetObjectItem(engine, "y")->valueint; e->texture = getAtlasImage(cJSON_GetObjectItem(engine, "texture")->valuestring); if (cJSON_GetObjectItem(engine, "flags")) { e->flags = flagsToLong(cJSON_GetObjectItem(engine, "flags")->valuestring, NULL); } e->w = e->texture->rect.w; e->h = e->texture->rect.h; e->systemPower = MAX_SYSTEM_POWER; e->action = engineThink; e->die = engineDie; e->owner = parent; engine = engine->next; } } } void updateCapitalShipComponentProperties(Entity *parent, long flags) { Entity *e; if (flags != -1) { flags &= ~EF_AI_LEADER; } for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->owner == parent) { if (flags != -1) { e->flags |= flags; } switch (e->type) { case ET_COMPONENT_ENGINE: sprintf(e->name, _("%s (Engine)"), parent->name); e->flags &= ~EF_AI_IGNORE; break; case ET_COMPONENT: sprintf(e->name, _("%s (Component)"), parent->name); break; case ET_COMPONENT_GUN: sprintf(e->name, _("%s (Gun)"), parent->name); e->flags &= ~EF_AI_IGNORE; if (parent->aiFlags & AIF_ASSASSIN) { e->aiFlags |= AIF_ASSASSIN; } break; } e->active = parent->active; } } } void loadCapitalShips(cJSON *node) { Entity *e; char **types, *name, *groupName, *type; int side, scatter, number, active; int i, numTypes, addFlags; long flags; float x, y; if (node) { node = node->child; while (node) { name = NULL; groupName = NULL; flags = -1; types = toTypeArray(cJSON_GetObjectItem(node, "types")->valuestring, &numTypes); side = lookup(cJSON_GetObjectItem(node, "side")->valuestring); x = (cJSON_GetObjectItem(node, "x")->valuedouble / BATTLE_AREA_CELLS) * BATTLE_AREA_WIDTH; y = (cJSON_GetObjectItem(node, "y")->valuedouble / BATTLE_AREA_CELLS) * BATTLE_AREA_HEIGHT; name = getJSONValueStr(node, "name", NULL); groupName = getJSONValueStr(node, "groupName", NULL); number = getJSONValue(node, "number", 1); scatter = getJSONValue(node, "scatter", 1); active = getJSONValue(node, "active", 1); if (cJSON_GetObjectItem(node, "flags")) { flags = flagsToLong(cJSON_GetObjectItem(node, "flags")->valuestring, &addFlags); } for (i = 0 ; i < number ; i++) { type = types[rand() % numTypes]; e = spawnCapitalShip(type, x, y, side); if (scatter > 1) { e->x += (rand() % scatter) - (rand() % scatter); e->y += (rand() % scatter) - (rand() % scatter); } e->active = active; if (name) { STRNCPY(e->name, name, MAX_NAME_LENGTH); } if (groupName) { STRNCPY(e->groupName, groupName, MAX_NAME_LENGTH); } if (flags != -1) { if (addFlags) { e->flags |= flags; } else { e->flags = flags; SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Flags for '%s' (%s) replaced", e->name, e->defName); } } updateCapitalShipComponentProperties(e, flags); SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "%s (%d / %d)", e->name, e->health, e->maxHealth); } node = node->next; for (i = 0 ; i < numTypes ; i++) { free(types[i]); } free(types); } } } static void issueEnginesDestroyedMessage(Entity *cap) { addMessageBox(cap->name, _("We've lost engines! We're a sitting duck!"), MB_IMPORTANT); } static void issueGunsDestroyedMessage(Entity *cap) { addMessageBox(cap->name, _("Our guns have been shot out! We have no defences!"), MB_IMPORTANT); } static void issueDamageMessage(Entity *cap) { if (cap->health == cap->maxHealth - 1) { addMessageBox(cap->name, _("Be advised, we're taking damage here. Please step up support."), MB_IMPORTANT); } else if (cap->health == cap->maxHealth / 2) { addMessageBox(cap->name, _("We're sustaining heavy damage! All fighters, please assist, ASAP!"), MB_IMPORTANT); } else if (cap->health == 1) { addMessageBox(cap->name, _("Mayday! Mayday! Defences are critical. We can't hold out much longer!"), MB_IMPORTANT); } } void destroyCapitalShipDefs(void) { Entity *e; while (defHead.next) { e = defHead.next; defHead.next = e->next; free(e); } }