diff --git a/data/base/audio/audio.json b/data/base/audio/audio.json index 43874ec5561..a6c9514e6ed 100644 --- a/data/base/audio/audio.json +++ b/data/base/audio/audio.json @@ -450,7 +450,8 @@ { "fileName": "scream.ogg", "loop": false, "range": 1800, "volume": 100 }, { "fileName": "scream2.ogg", "loop": false, "range": 1800, "volume": 100 }, { "fileName": "scream3.ogg", "loop": false, "range": 1800, "volume": 100 }, - { "fileName": "silence.ogg", "loop": false, "range": 1800, "volume": 100 } + { "fileName": "silence.ogg", "loop": false, "range": 1800, "volume": 100 }, + { "fileName": "shield-hit.ogg", "loop": false, "range": 1800, "volume": 70 } ] }, "Extra": { diff --git a/data/base/audio/sfx/misc/shield-hit.ogg b/data/base/audio/sfx/misc/shield-hit.ogg new file mode 100644 index 00000000000..d4d12671b48 Binary files /dev/null and b/data/base/audio/sfx/misc/shield-hit.ogg differ diff --git a/data/base/shaders/tcmask_instanced.frag b/data/base/shaders/tcmask_instanced.frag index 873941f5d96..59ee7f02c35 100644 --- a/data/base/shaders/tcmask_instanced.frag +++ b/data/base/shaders/tcmask_instanced.frag @@ -26,6 +26,7 @@ uniform int tcmask; // whether a tcmask texture exists for the model uniform int normalmap; // whether a normal map exists for the model uniform int specularmap; // whether a specular map exists for the model uniform int hasTangents; // whether tangents were calculated for model +uniform int shieldEffect; uniform float graphicsCycle; // a periodically cycling value for special effects uniform vec4 sceneColor; //emissive light @@ -74,6 +75,11 @@ out vec4 FragColor; #include "pointlights.frag" #endif +float random(vec2 uv) +{ + return fract(sin(dot(uv.xy, vec2(12.9898, 78.233))) * 43758.5453123); +} + float getShadowMapDepthComp(vec2 base_uv, float u, float v, vec2 shadowMapSizeInv, int cascadeIndex, float z) { vec2 uv = base_uv + vec2(u, v) * shadowMapSizeInv; @@ -300,6 +306,13 @@ vec3 blendAddEffectLighting(vec3 a, vec3 b) { return min(a + b, vec3(1.0)); } +vec4 applyShieldFuzzEffect(vec4 color) { + float cycle = 0.66 + 0.66 * graphicsCycle; + vec3 col = vec3(random(vec2(color.x * cycle, color.y * cycle))); + col.b *= 1.5; + return vec4(col, color.a / 6.0); +} + void main() { // unpack inputs @@ -418,8 +431,22 @@ void main() } #ifdef NEWGL - FragColor = fragColour; + if (shieldEffect == 1) + { + FragColor = applyShieldFuzzEffect(fragColour); + } + else + { + FragColor = fragColour; + } #else - gl_FragColor = fragColour; + if (shieldEffect == 1) + { + gl_FragColor = applyShieldFuzzEffect(fragColour); + } + else + { + gl_FragColor = fragColour; + } #endif } diff --git a/data/base/shaders/vk/tcmask_instanced.frag b/data/base/shaders/vk/tcmask_instanced.frag index d5258a8fe48..302ce276fc8 100644 --- a/data/base/shaders/vk/tcmask_instanced.frag +++ b/data/base/shaders/vk/tcmask_instanced.frag @@ -33,6 +33,11 @@ layout(location = 0) out vec4 FragColor; #include "pointlights.glsl" +float random(vec2 uv) +{ + return fract(sin(dot(uv.xy, vec2(12.9898, 78.233))) * 43758.5453123); +} + float getShadowMapDepthComp(vec2 base_uv, float u, float v, vec2 shadowMapSizeInv, int cascadeIndex, float z) { vec2 uv = base_uv + vec2(u, v) * shadowMapSizeInv; @@ -268,6 +273,13 @@ vec3 blendAddEffectLighting(vec3 a, vec3 b) { return min(a + b, vec3(1.0)); } +vec4 applyShieldFuzzEffect(vec4 color) { + float cycle = 0.66 + 0.66 * graphicsCycle; + vec3 col = vec3(random(vec2(color.x * cycle, color.y * cycle))); + col.b *= 1.5; + return vec4(col, color.a / 6.0); +} + void main() { // unpack inputs @@ -381,5 +393,12 @@ void main() fragColour = mix(fragColour, vec4(fogColor.xyz, fragColour.w), fogFactor); } - FragColor = fragColour; + if (shieldEffect == 1) + { + FragColor = applyShieldFuzzEffect(fragColour); + } + else + { + FragColor = fragColour; + } } diff --git a/data/base/shaders/vk/tcmask_instanced.glsl b/data/base/shaders/vk/tcmask_instanced.glsl index e8cde264b5b..0d54eeb9c0d 100644 --- a/data/base/shaders/vk/tcmask_instanced.glsl +++ b/data/base/shaders/vk/tcmask_instanced.glsl @@ -40,5 +40,6 @@ layout(std140, set = 1, binding = 0) uniform meshuniforms int normalmap; int specularmap; int hasTangents; + int shieldEffect; }; diff --git a/data/base/wrf/audio.wrf b/data/base/wrf/audio.wrf index 8c7f88b35c9..c857b834782 100644 --- a/data/base/wrf/audio.wrf +++ b/data/base/wrf/audio.wrf @@ -338,6 +338,7 @@ file WAV "scream.ogg" file WAV "scream2.ogg" file WAV "scream3.ogg" file WAV "silence.ogg" +file WAV "shield-hit.ogg" directory "audio/extra" file WAV "lndgzne.ogg" file WAV "nmedeted.ogg" diff --git a/data/mp/stats/brain.json b/data/mp/stats/brain.json index 4f5e85f3af0..d30cd50fec4 100644 --- a/data/mp/stats/brain.json +++ b/data/mp/stats/brain.json @@ -4,18 +4,32 @@ "droidType": "DROID_COMMAND", "hitpoints": 500, "id": "CommandBrain01", - "maxDroids": 6, - "maxDroidsMult": 2, + "maxDroids": 32, + "maxDroidsMult": 12, "name": "Command Turret", "ranks": [ "Rookie", "Green", "Trained", "Regular", "Professional", "Veteran", "Elite", "Special", "Hero" ], "thresholds": [ 0, 12, 24, 36, 48, 60, 72, 84, 96 ], - "turret": "CommandTurret1" + "turret": "CommandTurret1", + "initialShieldPointsPercent": 10, + "additiveShieldPointsPercent": 5, + "initialShieldRegenTime": 32, + "shieldRegenTimeDec": 2, + "initialShieldInterruptRegenTime": 2000, + "shieldInterruptRegenTimeDec": 100, + "shieldPointsPerStep": 4 }, "ZNULLBRAIN": { "id": "ZNULLBRAIN", "name": "Z NULL BRAIN", "ranks": [ "Rookie", "Green", "Trained", "Regular", "Professional", "Veteran", "Elite", "Special", "Hero" ], "thresholds": [ 0, 2, 4, 6, 8, 10, 12, 14, 16 ], - "turret": "ZNULLWEAPON" + "turret": "ZNULLWEAPON", + "initialShieldPointsPercent": 10, + "additiveShieldPointsPercent": 5, + "initialShieldRegenTime": 32, + "shieldRegenTimeDec": 2, + "initialShieldInterruptRegenTime": 2000, + "shieldInterruptRegenTimeDec": 100, + "shieldPointsPerStep": 4 } } diff --git a/lib/ivis_opengl/gfx_api.h b/lib/ivis_opengl/gfx_api.h index c22be0a6851..e403d97fd98 100644 --- a/lib/ivis_opengl/gfx_api.h +++ b/lib/ivis_opengl/gfx_api.h @@ -781,6 +781,7 @@ namespace gfx_api int normalMap; int specularMap; int hasTangents; + int shieldEffect; }; // interleaved vertex data diff --git a/lib/ivis_opengl/gfx_api_gl.cpp b/lib/ivis_opengl/gfx_api_gl.cpp index 4424808112a..4c98adf1c50 100644 --- a/lib/ivis_opengl/gfx_api_gl.cpp +++ b/lib/ivis_opengl/gfx_api_gl.cpp @@ -835,7 +835,7 @@ static const std::map shader_to_file_table = // per-frame global uniforms "ProjectionMatrix", "ViewMatrix", "ModelUVLightmapMatrix", "ShadowMapMVPMatrix", "lightPosition", "sceneColor", "ambient", "diffuse", "specular", "fogColor", "ShadowMapCascadeSplits", "ShadowMapSize", "fogEnd", "fogStart", "graphicsCycle", "fogEnabled", "PointLightsPosition", "PointLightsColorAndEnergy", "bucketOffsetAndSize", "PointLightsIndex", "bucketDimensionUsed", "viewportWidth", "viewportHeight", // per-mesh uniforms - "tcmask", "normalmap", "specularmap", "hasTangents" + "tcmask", "normalmap", "specularmap", "hasTangents", "shieldEffect", }, { {"shadowMap", 4}, @@ -860,7 +860,7 @@ static const std::map shader_to_file_table = // per-frame global uniforms "ProjectionMatrix", "ViewMatrix", "ModelUVLightmapMatrix", "ShadowMapMVPMatrix", "lightPosition", "sceneColor", "ambient", "diffuse", "specular", "fogColor", "ShadowMapCascadeSplits", "ShadowMapSize", "fogEnd", "fogStart", "graphicsCycle", "fogEnabled", "PointLightsPosition", "PointLightsColorAndEnergy", "bucketOffsetAndSize", "PointLightsIndex", "bucketDimensionUsed", "viewportWidth", "viewportHeight", // per-mesh uniforms - "tcmask", "normalmap", "specularmap", "hasTangents", + "tcmask", "normalmap", "specularmap", "hasTangents", "shieldEffect", }, { {"shadowMap", 4} @@ -2123,6 +2123,7 @@ void gl_pipeline_state_object::set_constants(const gfx_api::Draw3DShapeInstanced setUniforms(24, cbuf.normalMap); setUniforms(25, cbuf.specularMap); setUniforms(26, cbuf.hasTangents); + setUniforms(27, cbuf.shieldEffect); } void gl_pipeline_state_object::set_constants(const gfx_api::Draw3DShapeInstancedDepthOnlyGlobalUniforms& cbuf) diff --git a/lib/ivis_opengl/piedraw.cpp b/lib/ivis_opengl/piedraw.cpp index e99636e131a..13f6e8f429a 100644 --- a/lib/ivis_opengl/piedraw.cpp +++ b/lib/ivis_opengl/piedraw.cpp @@ -1115,6 +1115,11 @@ bool InstancedMeshRenderer::Draw3DShape(iIMDShape *shape, int frame, PIELIGHT te tshape.stretch = stretchDepth; tshape.modelMatrix = modelMatrix; + if (pieFlag & pie_SHIELD) + { + tshape.modelMatrix = glm::scale(tshape.modelMatrix, glm::vec3(pie_SHIELD_FACTOR, pie_SHIELD_FACTOR, pie_SHIELD_FACTOR)); + } + if (pieFlag & pie_HEIGHT_SCALED) // construct { tshape.modelMatrix = glm::scale(tshape.modelMatrix, glm::vec3(1.0f, (float)pieFlagData / (float)pie_RAISE_SCALE, 1.0f)); @@ -1463,7 +1468,7 @@ bool InstancedMeshRenderer::DrawAll(uint64_t currentGameFrame, const glm::mat4& } template -static void drawInstanced3dShapeTemplated_Inner(ShaderOnce& globalsOnce, const gfx_api::Draw3DShapeInstancedGlobalUniforms& globalUniforms, const iIMDShape * shape, gfx_api::buffer* instanceDataBuffer, size_t instanceBufferOffset, size_t instance_count, gfx_api::texture* lightmapTexture) +static void drawInstanced3dShapeTemplated_Inner(ShaderOnce& globalsOnce, const gfx_api::Draw3DShapeInstancedGlobalUniforms& globalUniforms, const iIMDShape * shape, gfx_api::buffer* instanceDataBuffer, size_t instanceBufferOffset, size_t instance_count, gfx_api::texture* lightmapTexture, bool shieldEffect) { const auto& textures = shape->getTextures(); auto* tcmask = textures.tcmaskpage != iV_TEX_INVALID ? &pie_Texture(textures.tcmaskpage) : nullptr; @@ -1471,7 +1476,7 @@ static void drawInstanced3dShapeTemplated_Inner(ShaderOnce& globalsOnce, const g auto* specularmap = textures.specularpage != iV_TEX_INVALID ? &pie_Texture(textures.specularpage) : nullptr; gfx_api::Draw3DShapeInstancedPerMeshUniforms meshUniforms { - tcmask ? 1 : 0, normalmap != nullptr, specularmap != nullptr, shape->buffers[VBO_TANGENT] != nullptr + tcmask ? 1 : 0, normalmap != nullptr, specularmap != nullptr, shape->buffers[VBO_TANGENT] != nullptr, shieldEffect ? 1 : 0 }; gfx_api::buffer* pTangentBuffer = (shape->buffers[VBO_TANGENT] != nullptr) ? shape->buffers[VBO_TANGENT] : getZeroedVertexBuffer(shape->vertexCount * 4 * sizeof(gfx_api::gfxFloat)); @@ -1525,43 +1530,45 @@ static void drawInstanced3dShapeDepthOnly(ShaderOnce& globalsOnce, const gfx_api template static void drawInstanced3dShapeTemplated(ShaderOnce& globalsOnce, const gfx_api::Draw3DShapeInstancedGlobalUniforms& globalUniforms, const iIMDShape * shape, int pieFlag, gfx_api::buffer* instanceDataBuffer, size_t instanceBufferOffset, size_t instance_count, gfx_api::texture* lightmapTexture) { + bool shieldEffect = pieFlag & pie_SHIELD; + /* Set tranlucency */ if (pieFlag & pie_ADDITIVE) { if (!(pieFlag & pie_NODEPTHWRITE)) { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } else { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } } else if (pieFlag & pie_TRANSLUCENT) { if (!(pieFlag & pie_NODEPTHWRITE)) { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } else { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } } else if (pieFlag & pie_PREMULTIPLIED) { if (!(pieFlag & pie_NODEPTHWRITE)) { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } else { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } } else { - return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture); + return drawInstanced3dShapeTemplated_Inner(globalsOnce, globalUniforms, shape, instanceDataBuffer, instanceBufferOffset, instance_count, lightmapTexture, shieldEffect); } } diff --git a/lib/ivis_opengl/pietypes.h b/lib/ivis_opengl/pietypes.h index 84e7ffd69cf..124b954c5d6 100644 --- a/lib/ivis_opengl/pietypes.h +++ b/lib/ivis_opengl/pietypes.h @@ -60,8 +60,10 @@ using nonstd::nullopt; #define pie_PREMULTIPLIED 0x200 #define pie_NODEPTHWRITE 0x400 #define pie_FORCELIGHT 0x800 +#define pie_SHIELD 0x1000 #define pie_RAISE_SCALE 256 +#define pie_SHIELD_FACTOR 1.125f enum LIGHTING_TYPE { diff --git a/lib/sound/audio_id.cpp b/lib/sound/audio_id.cpp index 7ab707b08fe..4d90178fb45 100644 --- a/lib/sound/audio_id.cpp +++ b/lib/sound/audio_id.cpp @@ -439,6 +439,7 @@ static AUDIO_ID_MAP asAudioID[] = {ID_SOUND_BARB_SCREAM2, "scream2.ogg"}, {ID_SOUND_BARB_SCREAM3, "scream3.ogg"}, {ID_SOUND_OF_SILENCE, "silence.ogg"}, + {ID_SOUND_SHIELD_HIT, "shield-hit.ogg"}, /* Extra */ {ID_SOUND_LANDING_ZONE, "lndgzne.ogg"}, diff --git a/lib/sound/audio_id.h b/lib/sound/audio_id.h index e33f820144a..7d88f8a5b26 100644 --- a/lib/sound/audio_id.h +++ b/lib/sound/audio_id.h @@ -428,6 +428,7 @@ enum INGAME_AUDIO ID_SOUND_BARB_SCREAM2, ID_SOUND_BARB_SCREAM3, ID_SOUND_OF_SILENCE, + ID_SOUND_SHIELD_HIT, /* Extra */ diff --git a/src/combat.cpp b/src/combat.cpp index a7da53cf35b..892a7e150f8 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -26,8 +26,14 @@ #include "lib/framework/frame.h" #include "lib/framework/fixedpoint.h" +#include "lib/framework/math_ext.h" #include "lib/netplay/sync_debug.h" +#include "lib/ivis_opengl/ivisdef.h" + +#include "lib/sound/audio.h" +#include "lib/sound/audio_id.h" + #include "action.h" #include "combat.h" #include "difficulty.h" @@ -38,6 +44,11 @@ #include "qtscript.h" #include "order.h" #include "objmem.h" +#include "effects.h" +#include "display3ddef.h" + +#define DROID_SHIELD_DAMAGE_SPREAD (16 - rand()%32) +#define DROID_SHIELD_PARTICLES (6 + rand()%8) /* Fire a weapon at something */ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot) @@ -395,12 +406,13 @@ int objArmour(const BASE_OBJECT *psObj, WEAPON_CLASS weaponClass) /* Deals damage to an object * \param psObj object to deal damage to + * \param psProjectile projectile which hit the object (may be nullptr) * \param damage amount of damage to deal * \param weaponClass the class of the weapon that deals the damage * \param weaponSubClass the subclass of the weapon that deals the damage * \return < 0 when the dealt damage destroys the object, > 0 when the object survives */ -int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond, int minDamage, bool empRadiusHit) +int32_t objDamage(BASE_OBJECT *psObj, PROJECTILE *psProjectile, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond, int minDamage, bool empRadiusHit) { int level = 0; int armour = objArmour(psObj, weaponClass); @@ -418,6 +430,15 @@ int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAP // EMP weapon radius should not do actual damage if (weaponSubClass == WSC_EMP && empRadiusHit) { + if (psObj->type == OBJ_DROID) + { + DROID *psDroid = castDroid(psObj); + + if (psDroid->shieldPoints != -1) { + // EMP weapons kills droid shields completely + psDroid->shieldPoints = 0; + } + } return 0; } @@ -485,6 +506,52 @@ int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAP return -(int64_t)65536 * psObj->body / originalhp; } + // Drain shields first + if (psObj->type == OBJ_DROID) + { + DROID *psDroid = castDroid(psObj); + + if (psDroid->shieldPoints > 0) + { + if (psDroid->shieldPoints > actualDamage) + { + psDroid->shieldPoints -= actualDamage; + actualDamage = 0; + } + else + { + actualDamage -= psDroid->shieldPoints; + psDroid->shieldPoints = 0; + } + + if (psDroid->shieldPoints == 0) + { + // shields are interrupted, wait for a while until regeneration starts again + psDroid->shieldInterruptRegenTime = psDroid->time; + } + + if (psProjectile != nullptr && + weaponSubClass != WSC_FLAME && + weaponSubClass != WSC_COMMAND && + droidGetMaxShieldPoints(psDroid) > 0 && + PERCENT(psDroid->shieldPoints, droidGetMaxShieldPoints(psDroid)) > 25 && + objPosDiffSq(psDroid->pos, psProjectile->pos) < TILE_WIDTH * TILE_WIDTH) + { + Vector3i dv; + dv.y = psProjectile->pos.z; + + for (int i = 0; i < DROID_SHIELD_PARTICLES; i++) + { + dv.x = psProjectile->pos.x + DROID_SHIELD_DAMAGE_SPREAD; + dv.z = psProjectile->pos.y + DROID_SHIELD_DAMAGE_SPREAD; + addEffect(&dv, EFFECT_FIREWORK, FIREWORK_TYPE_STARBURST, false, nullptr, 0, gameTime - deltaGameTime + 1); + } + + audio_PlayStaticTrack(psProjectile->pos.x, psProjectile->pos.y, ID_SOUND_SHIELD_HIT); + } + } + } + // Subtract the dealt damage from the droid's remaining body points psObj->body -= actualDamage; diff --git a/src/combat.h b/src/combat.h index da45e87bd8e..f5548bbe228 100644 --- a/src/combat.h +++ b/src/combat.h @@ -25,6 +25,7 @@ #define __INCLUDED_SRC_COMBAT_H__ #include "weapondef.h" +#include "objectdef.h" /* Fire a weapon at something added int weapon_slot*/ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot); @@ -33,7 +34,7 @@ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, in if any support a counter battery sensor*/ void counterBatteryFire(BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget); -int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond, int minDamage, bool empRadiusHit); +int32_t objDamage(BASE_OBJECT *psObj, PROJECTILE *psProjectile, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond, int minDamage, bool empRadiusHit); unsigned int objGuessFutureDamage(WEAPON_STATS *psStats, unsigned int player, BASE_OBJECT *psTarget); diff --git a/src/component.cpp b/src/component.cpp index 1cd1ae553d2..84941598cb8 100644 --- a/src/component.cpp +++ b/src/component.cpp @@ -427,11 +427,19 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM SDWORD iConnector; PROPULSION_STATS *psPropStats; SDWORD pieFlag, iPieData; + SDWORD shieldPieFlag = 0, iShieldPieData = 0; PIELIGHT brightness; UDWORD colour; size_t i = 0; bool didDrawSomething = false; + if (!bButton && psDroid->shieldPoints > 0 && droidGetMaxShieldPoints(psDroid) > 0) + { + float factor = static_cast(psDroid->shieldPoints) / droidGetMaxShieldPoints(psDroid); + iShieldPieData = static_cast(std::round(255.0f * factor)); + shieldPieFlag = pie_FORCELIGHT | pie_TRANSLUCENT | pie_SHIELD; + } + glm::mat4 modifiedModelMatrix = modelMatrix2; if (psDroid->timeLastHit - graphicsTime < ELEC_DAMAGE_DURATION && psDroid->lastHitWeapon == WSC_ELECTRONIC && !gamePaused()) @@ -492,6 +500,13 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM { didDrawSomething = true; } + if (!bButton && psDroid->shieldPoints > 0) + { + if (pie_Draw3DShape(psShapeProp->displayModel(), 0, colour, brightness, shieldPieFlag, iShieldPieData, modifiedModelMatrix, viewMatrix, -(psDroid->heightAboveMap))) + { + didDrawSomething = true; + } + } } /* set default components transparent */ @@ -526,6 +541,13 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM { didDrawSomething = true; } + if (!bButton && psDroid->shieldPoints > 0) + { + if (drawShape(strImd, psDroid->timeAnimationStarted, colour, brightness, shieldPieFlag, iShieldPieData, modifiedModelMatrix, viewMatrix, -(psDroid->heightAboveMap))) + { + didDrawSomething = true; + } + } strImd = strImd->next.get(); } } @@ -640,6 +662,13 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM { didDrawSomething = true; } + if (!bButton && psDroid->shieldPoints > 0) + { + if (pie_Draw3DShape(psShape, 0, colour, brightness, shieldPieFlag, iShieldPieData, localModelMatrix, viewMatrix, -localHeightAboveTerrain)) + { + didDrawSomething = true; + } + } } localModelMatrix *= glm::translate(glm::vec3(0, 0, recoilValue)); @@ -673,6 +702,13 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM { didDrawSomething = true; } + if (!bButton && psDroid->shieldPoints > 0) + { + if (pie_Draw3DShape(psShape, 0, colour, brightness, shieldPieFlag, iShieldPieData, localModelMatrix, viewMatrix, -localHeightAboveTerrain)) + { + didDrawSomething = true; + } + } auto flashBaseModel = MUZZLE_FLASH_PIE(psDroid, i); iIMDShape *pMuzzleFlash = (flashBaseModel) ? flashBaseModel->displayModel() : nullptr; drawMuzzleFlash(psDroid->asWeaps[i], psShape, pMuzzleFlash, brightness, pieFlag, iPieData, localModelMatrix, viewMatrix, localHeightAboveTerrain); @@ -827,6 +863,13 @@ static bool displayCompObj(DROID *psDroid, bool bButton, const glm::mat4& modelM { didDrawSomething = true; } + if (!bButton && psDroid->shieldPoints > 0) + { + if (pie_Draw3DShape(psShapeProp->displayModel(), 0, colour, brightness, shieldPieFlag, iShieldPieData, modifiedModelMatrix, viewMatrix, -(psDroid->heightAboveMap))) + { + didDrawSomething = true; + } + } } return didDrawSomething; diff --git a/src/display3d.cpp b/src/display3d.cpp index 5588583487d..72437bea3ae 100644 --- a/src/display3d.cpp +++ b/src/display3d.cpp @@ -3432,6 +3432,18 @@ bool eitherSelected(DROID *psDroid) static void queueDroidPowerBarsRects(DROID *psDroid, bool drawBox, BatchedMultiRectRenderer& batchedMultiRectRenderer, size_t rectGroup) { UDWORD damage = PERCENT(psDroid->body, psDroid->originalBody); + UDWORD shields = 0; + + if (psDroid->shieldPoints >= 0) + { + int maxShieldPoints = droidGetMaxShieldPoints(psDroid); + + if (maxShieldPoints > 0) + { + shields = PERCENT(psDroid->shieldPoints, maxShieldPoints); + shields = static_cast((float)psDroid->shieldPoints / (float)maxShieldPoints * (float)psDroid->sDisplay.screenR); + } + } PIELIGHT powerCol = WZCOL_BLACK; PIELIGHT powerColShadow = WZCOL_BLACK; @@ -3472,6 +3484,18 @@ static void queueDroidPowerBarsRects(DROID *psDroid, bool drawBox, BatchedMultiR batchedMultiRectRenderer.addRect(PIERECT_DrawRequest(psDroid->sDisplay.screenX - psDroid->sDisplay.screenR - 1, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 2, psDroid->sDisplay.screenX + psDroid->sDisplay.screenR + 1, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 6, WZCOL_RELOAD_BACKGROUND), rectGroup); batchedMultiRectRenderer.addRect(PIERECT_DrawRequest(psDroid->sDisplay.screenX - psDroid->sDisplay.screenR, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 3, psDroid->sDisplay.screenX - psDroid->sDisplay.screenR + damage, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 4, powerCol), rectGroup); batchedMultiRectRenderer.addRect(PIERECT_DrawRequest(psDroid->sDisplay.screenX - psDroid->sDisplay.screenR, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 4, psDroid->sDisplay.screenX - psDroid->sDisplay.screenR + damage, psDroid->sDisplay.screenY + psDroid->sDisplay.screenR + 5, powerColShadow), rectGroup); + batchedMultiRectRenderer.addRect(PIERECT_DrawRequest( + psDroid->sDisplay.screenX - psDroid->sDisplay.screenR - 3, + psDroid->sDisplay.screenY + psDroid->sDisplay.screenR, + psDroid->sDisplay.screenX - psDroid->sDisplay.screenR - 1, + psDroid->sDisplay.screenY + psDroid->sDisplay.screenR - shields, WZCOL_LBLUE), + rectGroup); + batchedMultiRectRenderer.addRect(PIERECT_DrawRequest( + psDroid->sDisplay.screenX - psDroid->sDisplay.screenR - 2, + psDroid->sDisplay.screenY + psDroid->sDisplay.screenR, + psDroid->sDisplay.screenX - psDroid->sDisplay.screenR, + psDroid->sDisplay.screenY + psDroid->sDisplay.screenR - shields, WZCOL_BLACK), + rectGroup); } static void queueDroidEnemyHealthBarsRects(DROID *psDroid, BatchedMultiRectRenderer& batchedMultiRectRenderer, size_t rectGroup) diff --git a/src/droid.cpp b/src/droid.cpp index 8d49baf58b0..dbf4b8005d9 100644 --- a/src/droid.cpp +++ b/src/droid.cpp @@ -280,6 +280,7 @@ void addDroidDeathAnimationEffect(DROID *psDroid) #define UNIT_LOST_DELAY (5*GAME_TICKS_PER_SEC) /* Deals damage to a droid * \param psDroid droid to deal damage to + * \param psProjectile projectile which hit the object (may be nullptr) * \param damage amount of damage to deal * \param weaponClass the class of the weapon that deals the damage * \param weaponSubClass the subclass of the weapon that deals the damage @@ -287,7 +288,7 @@ void addDroidDeathAnimationEffect(DROID *psDroid) * \return > 0 when the dealt damage destroys the droid, < 0 when the droid survives * */ -int32_t droidDamage(DROID *psDroid, unsigned damage, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, unsigned impactTime, bool isDamagePerSecond, int minDamage, bool empRadiusHit) +int32_t droidDamage(DROID *psDroid, PROJECTILE *psProjectile, unsigned damage, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, unsigned impactTime, bool isDamagePerSecond, int minDamage, bool empRadiusHit) { int32_t relativeDamage; @@ -299,7 +300,7 @@ int32_t droidDamage(DROID *psDroid, unsigned damage, WEAPON_CLASS weaponClass, W damage *= 3; } - relativeDamage = objDamage(psDroid, damage, psDroid->originalBody, weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); + relativeDamage = objDamage(psDroid, psProjectile, damage, psDroid->originalBody, weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); if (relativeDamage != 0 && psDroid->player == selectedPlayer && psDroid->timeLastHit == gameTime) { @@ -911,6 +912,10 @@ void droidUpdate(DROID *psDroid) droidUpdateDroidSelfRepair(psDroid); } + if (bMultiPlayer) + { + droidUpdateShields(psDroid); + } /* Update the fire damage data */ if (psDroid->periodicalDamageStart != 0 && psDroid->periodicalDamageStart != gameTime - deltaGameTime) // -deltaGameTime, since projectiles are updated after droids. @@ -925,7 +930,7 @@ void droidUpdate(DROID *psDroid) else { // do hardcoded burn damage (this damage automatically applied after periodical damage finished) - droidDamage(psDroid, BURN_DAMAGE, WC_HEAT, WSC_FLAME, gameTime - deltaGameTime / 2 + 1, true, BURN_MIN_DAMAGE, false); + droidDamage(psDroid, nullptr, BURN_DAMAGE, WC_HEAT, WSC_FLAME, gameTime - deltaGameTime / 2 + 1, true, BURN_MIN_DAMAGE, false); } } @@ -953,6 +958,59 @@ void droidUpdate(DROID *psDroid) CHECK_DROID(psDroid); } +void droidUpdateShields(DROID *psDroid) +{ + if (hasCommander(psDroid) || psDroid->droidType == DROID_COMMAND) + { + if (psDroid->shieldPoints < 0) + { + psDroid->shieldPoints = 0; + psDroid->shieldRegenTime = gameTime; + psDroid->shieldInterruptRegenTime = gameTime; + } + else + { + if (!((psDroid->lastHitWeapon == WSC_EMP) && ((gameTime - psDroid->timeLastHit) < EMP_DISABLE_TIME)) && + gameTime - psDroid->shieldInterruptRegenTime > droidCalculateShieldInterruptRegenTime(psDroid) && + gameTime - psDroid->shieldRegenTime > droidCalculateShieldRegenTime(psDroid)) + { + auto availableShieldPoints = droidGetMaxShieldPoints(psDroid) - psDroid->shieldPoints; + + if (availableShieldPoints > 0) + { + auto pointsToAdd = std::min(psDroid->getBrainStats()->shield.shieldPointsPerStep, availableShieldPoints); + psDroid->shieldPoints += pointsToAdd; + } + psDroid->shieldRegenTime = gameTime; + } + } + } + else + { + // unit has lost commander, shields are down! + psDroid->shieldPoints = -1; + } +} + +UDWORD droidCalculateShieldRegenTime(const DROID *psDroid) +{ + const auto &psStats = psDroid->getBrainStats()->shield; + return psStats.initialShieldRegenTime - (psStats.shieldRegenTimeDec * getDroidLevel(psDroid)); +} + +UDWORD droidCalculateShieldInterruptRegenTime(const DROID *psDroid) +{ + const auto &psStats = psDroid->getBrainStats()->shield; + return psStats.initialShieldInterruptRegenTime - (psStats.shieldInterruptRegenTimeDec * getDroidLevel(psDroid)); +} + +UDWORD droidGetMaxShieldPoints(const DROID *psDroid) +{ + const auto &psStats = psDroid->getBrainStats()->shield; + UDWORD percent = psDroid->originalBody / 100; + return percent * (psStats.initialShieldPointsPercent + psStats.additiveShieldPointsPercent * getDroidLevel(psDroid)); +} + /* See if a droid is next to a structure */ static bool droidNextToStruct(DROID *psDroid, STRUCTURE *psStruct) { @@ -1747,6 +1805,7 @@ DROID *reallyBuildDroid(const DROID_TEMPLATE *pTemplate, Position pos, UDWORD pl droid.experience = 0; } droid.kills = 0; + droid.shieldPoints = -1; droidSetBits(pTemplate, &droid); diff --git a/src/droid.h b/src/droid.h index 816b9a3b6b9..f7bc8c773f6 100644 --- a/src/droid.h +++ b/src/droid.h @@ -113,11 +113,23 @@ UDWORD calcTemplateBuild(const DROID_TEMPLATE *psTemplate); UDWORD calcTemplatePower(const DROID_TEMPLATE *psTemplate); /* Do damage to a droid */ -int32_t droidDamage(DROID *psDroid, unsigned damage, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, unsigned impactTime, bool isDamagePerSecond, int minDamage, bool empRadiusHit); +int32_t droidDamage(DROID *psDroid, PROJECTILE *psProjectile, unsigned damage, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, unsigned impactTime, bool isDamagePerSecond, int minDamage, bool empRadiusHit); /* The main update routine for all droids */ void droidUpdate(DROID *psDroid); +/* Update droid shields. */ +void droidUpdateShields(DROID *psDroid); + +/* Calculate the droid's shield regeneration step time */ +UDWORD droidCalculateShieldRegenTime(const DROID *psDroid); + +/* Calculate the droid's shield interruption time */ +UDWORD droidCalculateShieldInterruptRegenTime(const DROID *psDroid); + +/* Get droid maximum shield points */ +UDWORD droidGetMaxShieldPoints(const DROID *psDroid); + /* Set up a droid to build a structure - returns true if successful */ enum DroidStartBuild {DroidStartBuildFailed, DroidStartBuildSuccess, DroidStartBuildPending}; DroidStartBuild droidStartBuild(DROID *psDroid); diff --git a/src/droiddef.h b/src/droiddef.h index d84255fd1fe..e8c0aefe7f1 100644 --- a/src/droiddef.h +++ b/src/droiddef.h @@ -150,6 +150,9 @@ struct DROID : public BASE_OBJECT UDWORD baseSpeed; ///< the base speed dependent on propulsion type UDWORD originalBody; ///< the original body points uint32_t experience; + SDWORD shieldPoints; ///< Shield points of droid, which will be drained instead of health + UDWORD shieldRegenTime; ///< How long should it be before the next regeneration step + UDWORD shieldInterruptRegenTime; ///< Standby time in case the shield was destroyed to begin regenerating again uint32_t kills; UDWORD lastFrustratedTime; ///< Set when eg being stuck; used for eg firing indiscriminately at map features to clear the way SWORD resistance; ///< used in Electronic Warfare diff --git a/src/feature.cpp b/src/feature.cpp index dde621597dd..a76e36649a9 100644 --- a/src/feature.cpp +++ b/src/feature.cpp @@ -160,7 +160,7 @@ int32_t featureDamage(FEATURE *psFeature, unsigned damage, WEAPON_CLASS weaponCl debug(LOG_ATTACK, "feature (id %d): body %d armour %d damage: %d", psFeature->id, psFeature->body, psFeature->psStats->armourValue, damage); - relativeDamage = objDamage(psFeature, damage, psFeature->psStats->body, weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); + relativeDamage = objDamage(psFeature, nullptr, damage, psFeature->psStats->body, weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); // If the shell did sufficient damage to destroy the feature if (relativeDamage < 0) diff --git a/src/projectile.cpp b/src/projectile.cpp index 46d2800c9ab..b66aa8091b8 100644 --- a/src/projectile.cpp +++ b/src/projectile.cpp @@ -1687,7 +1687,7 @@ static int32_t objectDamageDispatch(DAMAGE *psDamage) switch (psDamage->psDest->type) { case OBJ_DROID: - return droidDamage((DROID *)psDamage->psDest, psDamage->damage, psDamage->weaponClass, psDamage->weaponSubClass, psDamage->impactTime, psDamage->isDamagePerSecond, psDamage->minDamage, psDamage->empRadiusHit); + return droidDamage((DROID *)psDamage->psDest, psDamage->psProjectile, psDamage->damage, psDamage->weaponClass, psDamage->weaponSubClass, psDamage->impactTime, psDamage->isDamagePerSecond, psDamage->minDamage, psDamage->empRadiusHit); break; case OBJ_STRUCTURE: diff --git a/src/stats.cpp b/src/stats.cpp index d74e53532a1..8c86d7ee913 100644 --- a/src/stats.cpp +++ b/src/stats.cpp @@ -720,6 +720,13 @@ bool loadBrainStats(WzConfig &ini) psStats->weight = ini.value("weight", 0).toInt(); psStats->base.maxDroids = ini.value("maxDroids").toInt(); psStats->base.maxDroidsMult = ini.value("maxDroidsMult").toInt(); + psStats->shield.initialShieldPointsPercent = ini.value("initialShieldPointsPercent").toInt(); + psStats->shield.additiveShieldPointsPercent = ini.value("additiveShieldPointsPercent").toInt(); + psStats->shield.initialShieldRegenTime = ini.value("initialShieldRegenTime").toInt(); + psStats->shield.shieldRegenTimeDec = ini.value("shieldRegenTimeDec").toInt(); + psStats->shield.initialShieldInterruptRegenTime = ini.value("initialShieldInterruptRegenTime").toInt(); + psStats->shield.shieldInterruptRegenTimeDec = ini.value("shieldInterruptRegenTimeDec").toInt(); + psStats->shield.shieldPointsPerStep = ini.value("shieldPointsPerStep").toInt(); auto rankNames = ini.json("ranks"); ASSERT(rankNames.is_array(), "ranks is not an array"); for (const auto& v : rankNames) diff --git a/src/statsdef.h b/src/statsdef.h index e47fc2bc40b..23ebb21dafa 100644 --- a/src/statsdef.h +++ b/src/statsdef.h @@ -486,6 +486,16 @@ struct BRAIN_STATS : public COMPONENT_STATS int maxDroidsMult = 0; ///< maximum number of controlled droids multiplied by level } upgrade[MAX_PLAYERS], base; std::vector rankNames; + struct + { + int initialShieldPointsPercent = 0; + int additiveShieldPointsPercent = 0; + int initialShieldRegenTime = 0; + int shieldRegenTimeDec = 0; + int initialShieldInterruptRegenTime = 0; + int shieldInterruptRegenTimeDec = 0; + int shieldPointsPerStep = 0; + } shield; }; /* diff --git a/src/structure.cpp b/src/structure.cpp index 9c81fa1500a..6f81dce1c32 100644 --- a/src/structure.cpp +++ b/src/structure.cpp @@ -832,7 +832,7 @@ int32_t structureDamage(STRUCTURE *psStructure, unsigned damage, WEAPON_CLASS we debug(LOG_ATTACK, "structure id %d, body %d, armour %d, damage: %d", psStructure->id, psStructure->body, objArmour(psStructure, weaponClass), damage); - relativeDamage = objDamage(psStructure, damage, psStructure->structureBody(), weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); + relativeDamage = objDamage(psStructure, nullptr, damage, psStructure->structureBody(), weaponClass, weaponSubClass, isDamagePerSecond, minDamage, empRadiusHit); // If the shell did sufficient damage to destroy the structure if (relativeDamage < 0)