/* Copyright (C) 2015-2019,2022 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 "../common.h" #include "entities.h" #include "../system/lookup.h" #include "../battle/capitalShips.h" #include "../battle/fighters.h" #include "../battle/quadtree.h" #include "../battle/rope.h" #include "../system/atlas.h" #include "../game/trophies.h" #include "../battle/effects.h" #include "../system/draw.h" #define DISABLED_GLOW_MAX 255 #define DISABLED_GLOW_MIN 128 #define DISABLED_GLOW_SPEED 3 extern App app; extern Battle battle; extern Dev dev; extern Entity *player; extern Entity *self; static void drawEntity(Entity *e); static void doEntity(void); static void alignComponents(void); static void activateEpicFighters(int side); static void restrictToBattleArea(Entity *e); static void drawTargetRects(Entity *e); static void drawHealthBar(Entity *e); static int drawComparator(const void *a, const void *b); static void notifyNewArrivals(void); static int isComponent(Entity *e); static Entity deadHead; static Entity *deadTail; static int disabledGlow; static int disabledGlowDir; void initEntities(void) { memset(&deadHead, 0, sizeof(Entity)); deadTail = &deadHead; disabledGlow = DISABLED_GLOW_MAX; disabledGlowDir = -DISABLED_GLOW_SPEED; } Entity *spawnEntity(void) { Entity *e = malloc(sizeof(Entity)); memset(e, 0, sizeof(Entity)); e->active = 1; battle.entityTail->next = e; battle.entityTail = e; return e; } void doEntities(void) { int numAllies, numEnemies; int numActiveAllies, numActiveEnemies; int numSpawnedEnemies; Entity *e, *prev; prev = &battle.entityHead; battle.hasThreats = numAllies = numEnemies = numActiveAllies = numActiveEnemies = numSpawnedEnemies = 0; if (dev.playerImmortal) { player->health = player->maxHealth; player->shield = player->maxShield; } for (e = battle.entityHead.next ; e != NULL ; e = e->next) { removeFromQuadtree(e, &battle.quadtree); if (dev.allImmortal) { e->health = e->maxHealth; e->shield = e->maxShield; } if (e->active) { self = e; e->reload = MAX(e->reload - 1, 0); if (e->shieldRechargeRate) { if (e->shield >= 0) { if (--e->shieldRecharge <= 0) { e->shield = MIN(e->shield + 1, e->maxShield); e->shieldRecharge = e->shieldRechargeRate; } } else { e->shield++; } } e->armourHit = MAX(e->armourHit - 25, 0); e->shieldHit = MAX(e->shieldHit - 5, 0); e->systemHit = MAX(e->systemHit - 25, 0); e->aiDamageTimer = MAX(e->aiDamageTimer - 1, 0); if (!e->aiDamageTimer) { e->aiDamagePerSec = 0; e->aiFlags &= ~AIF_EVADE; } switch (e->type) { case ET_FIGHTER: doFighter(); break; case ET_CAPITAL_SHIP: doCapitalShip(); break; default: doEntity(); break; } if (e->alive == ALIVE_ALIVE || e->alive == ALIVE_DYING) { if (e->action != NULL) { if (dev.noEntityActions) { e->thinkTime = 2; } if (--e->thinkTime <= 0) { e->thinkTime = 0; e->action(); } } doRope(e); restrictToBattleArea(e); if (!e->speed) { e->dx = e->dy = 0; } e->x += e->dx; e->y += e->dy; addToQuadtree(e, &battle.quadtree); } else { if (e == battle.entityTail) { battle.entityTail = prev; } if (e == battle.missionTarget) { battle.missionTarget = NULL; } if (e->killedBy == player && battle.hasSuspicionLevel) { if (e->aiFlags & (AIF_AVOIDS_COMBAT|AIF_DEFENSIVE)) { battle.suspicionLevel -= (MAX_SUSPICION_LEVEL * 0.5); } else { battle.suspicionLevel -= (MAX_SUSPICION_LEVEL * 0.12); } } if (e == player) { battle.playerSelect = battle.isEpic; } cutRope(e); prev->next = e->next; e->next = NULL; deadTail->next = e; deadTail = e; /* actually just creates another fighter in this one's place */ if (e->type == ET_FIGHTER && battle.isEpic && e->side != player->side && battle.unlimitedEnemies) { resetFighter(e); } e = prev; } } if (e->type == ET_FIGHTER || e->type == ET_CAPITAL_SHIP) { if (e->side == player->side) { numAllies++; if (e->health > 0 && e->active) { numActiveAllies++; } } else { numEnemies++; if (e->health > 0 && e->active) { numActiveEnemies++; if (e->spawned) { numSpawnedEnemies++; } } if (!(e->flags & EF_DISABLED) || battle.isEpic) { battle.hasThreats = 1; } } } prev = e; } battle.numAllies = (battle.isEpic) ? numAllies : numActiveAllies; battle.numEnemies = (battle.isEpic) ? numEnemies : numActiveEnemies; if (battle.status == MS_IN_PROGRESS && battle.stats[STAT_TIME] % (FPS * 30) == 0) { SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "numEnemies=%d, numActiveEnemies=%d, hasThreats=%d", numEnemies, numActiveEnemies, battle.hasThreats); } if (battle.isEpic && battle.stats[STAT_TIME] % FPS == 0) { if (numActiveAllies < battle.epicFighterLimit) { activateEpicFighters(SIDE_ALLIES); } numActiveEnemies -= numSpawnedEnemies; if (numActiveEnemies < battle.epicFighterLimit) { activateEpicFighters(SIDE_NONE); } } alignComponents(); disabledGlow = MAX(DISABLED_GLOW_MIN, MIN(disabledGlow + disabledGlowDir, DISABLED_GLOW_MAX)); if (disabledGlow <= DISABLED_GLOW_MIN) { disabledGlowDir = DISABLED_GLOW_SPEED; } else if (disabledGlow >= DISABLED_GLOW_MAX) { disabledGlowDir = -DISABLED_GLOW_SPEED; } } static void restrictToBattleArea(Entity *e) { float force; if (e->x <= BATTLE_AREA_EDGE) { force = BATTLE_AREA_EDGE - e->x; e->dx += force * 0.001; e->dx *= 0.95; } if (e->y <= BATTLE_AREA_EDGE) { force = BATTLE_AREA_EDGE - e->y; e->dy += force * 0.001; e->dy *= 0.95; } if (e->x >= BATTLE_AREA_WIDTH - BATTLE_AREA_EDGE) { force = e->x - (BATTLE_AREA_WIDTH - BATTLE_AREA_EDGE); e->dx -= force * 0.001; e->dx *= 0.95; } if (e->y >= BATTLE_AREA_HEIGHT - BATTLE_AREA_EDGE) { force = e->y - (BATTLE_AREA_HEIGHT - BATTLE_AREA_EDGE); e->dy -= force * 0.001; e->dy *= 0.95; } } static void doEntity(void) { if (self->die) { if (self->health <= 0 && self->alive == ALIVE_ALIVE) { self->health = 0; self->alive = ALIVE_DYING; self->die(); if (self == battle.missionTarget) { battle.missionTarget = NULL; } } } else { if (self->alive == ALIVE_DYING) { self->alive = ALIVE_DEAD; } else if (self->health <= 0) { self->alive = ALIVE_DYING; } } } static void alignComponents(void) { Entity *e; float x, y; float c, s; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (isComponent(e)) { removeFromQuadtree(e, &battle.quadtree); s = sin(TO_RAIDANS(e->owner->angle)); c = cos(TO_RAIDANS(e->owner->angle)); x = (e->offsetX * c) - (e->offsetY * s); y = (e->offsetX * s) + (e->offsetY * c); x += e->owner->x; y += e->owner->y; e->x = x; e->y = y; if (e->flags & EF_STATIC) { e->angle = e->owner->angle; } addToQuadtree(e, &battle.quadtree); } } } static int isComponent(Entity *e) { return (e->type == ET_COMPONENT || e->type == ET_COMPONENT_GUN || e->type == ET_COMPONENT_ENGINE); } void drawEntities(void) { int i; Entity *e, **candidates; candidates = getAllEntsWithin(battle.camera.x, battle.camera.y, app.winWidth, app.winHeight, NULL); /* counting entities to draw */ for (i = 0, e = candidates[i] ; e != NULL ; e = candidates[++i]) {}; qsort(candidates, i, sizeof(Entity*), drawComparator); for (i = 0, e = candidates[i] ; e != NULL ; e = candidates[++i]) { self = e; if (e->draw) { e->draw(); } else { drawEntity(e); } drawHealthBar(e); drawTargetRects(e); drawRope(e); } } static void drawEntity(Entity *e) { setAtlasColor(255, 255, 255, 255); if (e->armourHit > 0) { setAtlasColor(255, 255 - e->armourHit, 255 - e->armourHit, 255); } if (e->systemHit > 0) { setAtlasColor(255 - e->systemHit, 255, 255, 255); } if (e->flags & EF_DISABLED) { setAtlasColor(disabledGlow, disabledGlow, 255, 255); } blitRotated(e->texture, e->x - battle.camera.x, e->y - battle.camera.y, e->angle); if (e->shieldHit > 0) { drawShieldHitEffect(e); } } static void drawHealthBar(Entity *e) { SDL_Rect r; if (app.gameplay.healthBars && !(e->flags & EF_NO_HEALTH_BAR) && e->health > 0) { r.x = e->x - (e->w / 2) - battle.camera.x; r.y = e->y - e->h - battle.camera.y; r.w = 32; r.h = 1; if (e->side == player->side || e->flags & EF_FRIENDLY_HEALTH_BAR) { SDL_SetRenderDrawColor(app.renderer, 0, 128, 0, 255); } else { SDL_SetRenderDrawColor(app.renderer, 128, 0, 0, 255); } SDL_RenderFillRect(app.renderer, &r); r.w = 32 * (e->health * 1.0f / e->maxHealth); if (e->side == player->side || e->flags & EF_FRIENDLY_HEALTH_BAR) { SDL_SetRenderDrawColor(app.renderer, 0, 255, 0, 255); } else { SDL_SetRenderDrawColor(app.renderer, 255, 0, 0, 255); } SDL_RenderFillRect(app.renderer, &r); } } static void drawTargetRects(Entity *e) { SDL_Rect r; int size = MAX(e->w, e->h) + 16; if (player->alive == ALIVE_ALIVE && e == player->target) { r.x = e->x - (size / 2) - battle.camera.x; r.y = e->y - (size / 2) - battle.camera.y; r.w = size; r.h = size; SDL_SetRenderDrawColor(app.renderer, 255, 0, 0, 255); SDL_RenderDrawRect(app.renderer, &r); } if ((e == battle.missionTarget || e->flags & EF_MISSION_TARGET) && (e->flags & EF_NO_MT_BOX) == 0) { r.x = e->x - (size / 2) - battle.camera.x - 4; r.y = e->y - (size / 2) - battle.camera.y - 4; r.w = size + 8; r.h = size + 8; SDL_SetRenderDrawColor(app.renderer, 0, 255, 0, 255); SDL_RenderDrawRect(app.renderer, &r); } if (e == battle.messageSpeaker && e != player && battle.stats[STAT_TIME] % 40 < 20) { r.x = e->x - (size / 2) - battle.camera.x; r.y = e->y - (size / 2) - battle.camera.y; r.w = size; r.h = size; SDL_SetRenderDrawColor(app.renderer, 255, 255, 255, 255); SDL_RenderDrawRect(app.renderer, &r); } } void activateEntities(char *names) { Entity *e; char *name; name = strtok(names, ";"); while (name) { for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (strcmp(e->name, name) == 0) { e->active = 1; if (e->type == ET_CAPITAL_SHIP) { updateCapitalShipComponentProperties(e, 0); } } } name = strtok(NULL, ";"); } notifyNewArrivals(); } void activateEntityGroups(char *groupNames) { Entity *e; char *groupName; groupName = strtok(groupNames, ";"); while (groupName) { for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (strcmp(e->groupName, groupName) == 0) { SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "Activated %s (%s)", e->name, groupName); e->active = 1; if (e->type == ET_CAPITAL_SHIP) { updateCapitalShipComponentProperties(e, 0); } } } groupName = strtok(NULL, ";"); } notifyNewArrivals(); } /* * Some craft, such as capital ships, might be performing a long action and won't notice new craft arrive for well over 30 seconds. * We'll knock the times down to a max of 1 second, so they can react faster. */ static void notifyNewArrivals(void) { Entity *e; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->active && (e->type == ET_FIGHTER || e->type == ET_CAPITAL_SHIP)) { e->aiActionTime = MIN(e->aiActionTime, FPS); } } } static void activateEpicFighters(int side) { Entity *e; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (!e->active && e->type == ET_FIGHTER && !(e->flags & EF_NO_EPIC) && ((side == SIDE_ALLIES && e->side == SIDE_ALLIES) || (side != SIDE_ALLIES && e->side != SIDE_ALLIES))) { e->active = 1; /* don't spring into existence in front of the player */ if (isOnBattleScreen(e->x, e->y, e->w, e->h)) { e->x = player->x; e->y = player->y; e->x += (rand() % 2) ? -app.winWidth : app.winWidth; e->y += (rand() % 2) ? -app.winHeight : app.winHeight; } return; } } } void countNumEnemies(void) { Entity *e; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->side != SIDE_ALLIES && (e->type == ET_FIGHTER || e->type == ET_CAPITAL_SHIP) && (!(e->flags & EF_NO_THREAT))) { battle.numInitialEnemies++; } } SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "battle.numInitialEnemies=%d", battle.numInitialEnemies); } void addAllToQuadtree(void) { Entity *e; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (e->active) { addToQuadtree(e, &battle.quadtree); } } } static int drawComparator(const void *a, const void *b) { Entity *e1 = *((Entity**)a); Entity *e2 = *((Entity**)b); return e2->type - e1->type; } void killEntity(char *name) { Entity *e; for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (strcmp(e->name, name) == 0) { e->health = 0; e->deathType = DT_INSTANT; /* prevent objectives and conditions from firing */ strcpy(e->name, ""); strcpy(e->groupName, ""); } } } void updateEntitySide(char *sideStr, char *name) { Entity *e; int side; side = lookup(sideStr); for (e = battle.entityHead.next ; e != NULL ; e = e->next) { if (strcmp(e->name, name) == 0) { e->side = side; if (e->side != player->side) { e->flags |= EF_MISSION_TARGET; } } } } void awardPandoranCraftTrophy(void) { Entity *e; for (e = deadHead.next ; e != NULL ; e = e->next) { if (e->killedBy == player && e->side == SIDE_PANDORAN) { awardTrophy("PANDORAN"); } } } void destroyEntities(void) { Entity *e; while (deadHead.next) { e = deadHead.next; deadHead.next = e->next; free(e); } deadTail = &deadHead; }