#include "Projectile.h" #include "File.h" #include "PICReader.h" #include "Image.h" #include "Camera.h" #include "GameMap.h" #include "Game.h" #include "DanmakuExtras.h" #include "elementanims.h" #include "MiscGraphics.h" #include "ImageUtils.h" /** * ScriptProjectile */ int ScriptProjectile::sm_scriptProjectileCreated = 0; int ScriptProjectile::sm_scriptProjectileDestroyed = 0; ScriptProjectile::ScriptProjectile() { sm_scriptProjectileCreated++; } ScriptProjectile::~ScriptProjectile() { sm_scriptProjectileDestroyed++; } void* ScriptProjectile::operator new(size_t sz) { return MALLOC(sz); } void ScriptProjectile::operator delete(void* p) { SAFE_FREE(p); } void ScriptProjectile::debugPrintStats() { if (sm_scriptProjectileCreated != sm_scriptProjectileDestroyed) DEBUG_LOG("Created %d ScriptProjectile and destroyed %d ScriptProjectile.", sm_scriptProjectileCreated, sm_scriptProjectileDestroyed); } /** * ComplexProjectile */ int ComplexProjectile::sm_complexProjectileCreated = 0; int ComplexProjectile::sm_complexProjectileDestroyed = 0; ComplexProjectile::ComplexProjectile() { sm_complexProjectileCreated++; m_complexDanmakuType = -1; m_shutdownDelayMS = 0; m_shutdownDurationMS = 1; m_restartShouldReorient = false; m_restartTargetEnemy = false; m_restartMotionTilesMS = 2.0; m_restartMotionAngle = DOUBLE_INFINITY; m_restartAngleOffset = 0.0; m_restartAngleDeviation = 0.0; // adds randomness to the angle in any situation m_restartMotionCurveMS = 0.0; } ComplexProjectile::~ComplexProjectile() { sm_complexProjectileDestroyed++; } void* ComplexProjectile::operator new(size_t sz) { return MALLOC(sz); } void ComplexProjectile::operator delete(void* p) { SAFE_FREE(p); } void ComplexProjectile::update(int deltaMS) { // !!!NO MORE CODE IN THIS METHOD!!! complexProjectileUpdate(deltaMS); // !!!NO MORE CODE IN THIS METHOD!!! } void ComplexProjectile::complexProjectileUpdate(int deltaMS) { if (m_complexDanmakuType == CDT_STOP_START) { if (m_shutdownDelayMS > 0) m_shutdownDelayMS -= deltaMS; else if (m_shutdownDurationMS > 0) { m_shutdownDurationMS -= deltaMS; if (m_shutdownDurationMS <= 0) { // change basic settings and tick the remainder m_motionTilesPerMS = m_restartMotionTilesMS; // tiles per second...finally moved it here since it's needed for flee if (m_restartShouldReorient) { if (m_restartTargetEnemy) { double enemyX, enemyY; GameMap::CURRENT->getNearestSoulBattleEnemy(ownedByPlayer(), getX(), getY(), enemyX, enemyY); m_motionAngle = CDGMath::getLineAngle(getX(), getY(), enemyX, enemyY); } else if (m_restartMotionAngle == DOUBLE_INFINITY) m_motionAngle = CDGMath::fixRadAngle(m_restartAngleOffset + m_motionAngle); else m_motionAngle = m_restartMotionAngle; } m_angleCurvePerMS = m_restartMotionCurveMS; if (m_restartAngleDeviation > 0.0) m_motionAngle += Platform::randomDouble(-(m_restartAngleDeviation * 0.5), m_restartAngleDeviation * 0.5); if (m_shutdownDurationMS < 0) { projectileUpdate(deltaMS + m_shutdownDurationMS); // lifecycle code lies within deltaMS = -m_shutdownDurationMS; m_shutdownDurationMS = 0; } if (deltaMS > 0) basicProjectileUpdate(deltaMS); } else projectileUpdate(deltaMS); // lifecycle code lies within return; } if (m_shutdownDelayMS < 0) { // tweak the next update for formation perfection projectileUpdate(-m_shutdownDelayMS); // lifecycle-related code must execute deltaMS += m_shutdownDelayMS; m_shutdownDelayMS = 0; } if (deltaMS > 0) basicProjectileUpdate(deltaMS); } else if (m_complexDanmakuType == CDT_MIMIC) { // mimic projectiles being owned by player have to be a bug, since they have no reason to exist. if (ownedByPlayer()) { basicProjectileUpdate(deltaMS); return; } // there will never be a "basic" update for mimic projectiles, so only do a core update projectileUpdate(deltaMS); // grab the enemy's movement double enemyX, enemyY; GameMap::CURRENT->getSoulBattleCharacterMotion(DANMAKU_OWNER_PLAYER, enemyX, enemyY); // tweak it if (m_mimicInvertX) enemyX = -enemyX; if (m_mimicInvertY) enemyY = -enemyY; enemyX *= m_mimicRelativeSpeed; enemyY *= m_mimicRelativeSpeed; // pass it on to the projectile setXY(enemyX, enemyY); } else if (m_complexDanmakuType == CDT_ORBITAL) { // there'll never be a "basic" update for orbitals, so just do a core update projectileUpdate(deltaMS); // determine the distance away from the player that this projectile is if (m_orbitCenterOnPosition) { // looks like it can move... adjustSpecialPosition(deltaMS, m_orbitCenterOnX, m_orbitCenterOnY); m_orbitLastCenterX = m_orbitCenterOnX; m_orbitLastCenterY = m_orbitCenterOnY; } else { // if orbital projectiles don't orbit around a location, they ALWAYS orbit around the player // regardless of if they're owned by the enemy, causing damage, or if they're owned by the player, used as a killy "shield" double idealX, idealY; GameMap::CURRENT->getSoulBattleCharacterPos(DANMAKU_OWNER_PLAYER, idealX, idealY); double distFromIdeal = CDGMath::getLineLength(m_orbitLastCenterX, m_orbitLastCenterY, idealX, idealY); if (distFromIdeal > 0.0) { double tpms = CDGMath::MAX(m_orbitMinSpeedTPMS, m_orbitTPMSPerTile * distFromIdeal); double distToTravel = tpms * deltaMS; // move the current "center" of this projectile to be closer to (or on, if applicable) the player. if (distToTravel >= distFromIdeal) { m_orbitLastCenterX = idealX; m_orbitLastCenterY = idealY; } else { double travelAngle = CDGMath::getLineAngle(m_orbitLastCenterX, m_orbitLastCenterY, idealX, idealY); CDGMath::getLineMotionPoint(m_orbitLastCenterX, m_orbitLastCenterY, m_orbitLastCenterX, m_orbitLastCenterY, travelAngle, distToTravel); } } } // lets figure out the current angle double angleFromCenter = m_orbitAngleAtSpawn; if (m_orbitPeriod > 0) { if (m_orbitRotateCCW) angleFromCenter = CDGMath::fixRadAngle(m_orbitAngleAtSpawn - ((TAU * (m_uptimeMS % m_orbitPeriod)) / m_orbitPeriod)); else angleFromCenter = CDGMath::fixRadAngle(m_orbitAngleAtSpawn + ((TAU * (m_uptimeMS % m_orbitPeriod)) / m_orbitPeriod)); } // now determine actual distance double actualDistance = m_orbitDistance; if (m_orbitOscillationPeriodMS > 0) { // the cdgmath method expects it to start in center. trick it to start on the "left", but adding 3/4 instead of sub 1/4 so it doesn't go negative int adjLifetimeMS = m_uptimeMS + (m_orbitOscillationPeriodMS * 3 / 4); int orbitPosition = adjLifetimeMS % m_orbitOscillationPeriodMS; if (m_orbitHalfOscillate) orbitPosition = CDGMath::MIN(m_orbitOscillationPeriodMS>>1, adjLifetimeMS); double orbitDistanceToUse = m_orbitDistance; if (m_orbitStartAtZero && m_uptimeMS < (m_orbitOscillationPeriodMS>>1)) orbitDistanceToUse = 0.0; actualDistance = CDGMath::getOscillatePosition(orbitPosition, m_orbitOscillationPeriodMS, (m_orbitOscillateDistance + orbitDistanceToUse) / 2, CDGMath::abs(m_orbitOscillateDistance - orbitDistanceToUse) / 2, m_orbitOscillateSmooth); // for projectiles that go outward from center. if (m_orbitOscillateDistance > orbitDistanceToUse) actualDistance = orbitDistanceToUse + (m_orbitOscillateDistance - actualDistance); //DEBUG_LOG("%f %f %f", actualDistance, (m_orbitOscillateDistance + m_orbitDistance) / 2, CDGMath::abs(m_orbitOscillateDistance - m_orbitDistance) / 2); } // now determine location teleport the projectile there double newX, newY; CDGMath::getLineMotionPoint(newX, newY, m_orbitLastCenterX, m_orbitLastCenterY, angleFromCenter, actualDistance); setXY(newX, newY); // set facing angle...TODO maybe in the future, support CCW motion. m_subclassControlsRotation = true; //if (true) m_subclassFixedAngle = CDGMath::fixRadAngle(angleFromCenter + RAD_90); //else // m_subclassFixedAngle = CDGMath::fixRadAngle(angleFromCenter - RAD_90); } else { // just silently fall back to basic projectile behavior, no questions asked. basicProjectileUpdate(deltaMS); } } void ComplexProjectile::debugPrintStats() { if (sm_complexProjectileCreated != sm_complexProjectileDestroyed) DEBUG_LOG("Created %d ComplexProjectile and destroyed %d ComplexProjectile.", sm_complexProjectileCreated, sm_complexProjectileDestroyed); } /** * HomingProjectile */ int HomingProjectile::sm_homingProjectileCreated = 0; int HomingProjectile::sm_homingProjectileDestroyed = 0; HomingProjectile::HomingProjectile() { sm_homingProjectileCreated++; } HomingProjectile::~HomingProjectile() { sm_homingProjectileDestroyed++; } void* HomingProjectile::operator new(size_t sz) { return MALLOC(sz); } void HomingProjectile::operator delete(void* p) { SAFE_FREE(p); } void HomingProjectile::update(int deltaMS) { homingProjectileUpdate(deltaMS); } void HomingProjectile::homingProjectileUpdate(int deltaMS) { if (getOwner() == DANMAKU_OWNER_PLAYER && DanmakuProjectile::shouldDestroyPlayerHomingProjectiles()) { queueImmediateSelfDestruction(); return; } // fall back on basic if we haven't started homing yet if (m_delayToStartHomingMS > 0) { m_delayToStartHomingMS -= deltaMS; if (m_delayToStartHomingMS >= 0) basicProjectileUpdate(deltaMS); // fix negative velocity and acceleration if we're switching modes if (m_delayToStartHomingMS <= 0) { m_motionTilesPerMS = CDGMath::abs(m_motionTilesPerMS); m_linearAccelerationTilesPerMS = CDGMath::abs(m_linearAccelerationTilesPerMS); } if (m_delayToStartHomingMS >= 0) return; // so we have some leftovers. perform both basic _and_ homing code this tick! basicProjectileUpdate(deltaMS + m_delayToStartHomingMS); deltaMS = -m_delayToStartHomingMS; m_delayToStartHomingMS = 0; // yes I know this leak ignores the range check. I'm choosing not to care about such a miniscule issue } else if (m_homingRange > 0.0 && !GameMap::CURRENT->isSoulBattleEnemyInRange(ownedByPlayer(), getX(), getY(), m_homingRange)) { // revert to a basic projectile if range check failed basicProjectileUpdate(deltaMS); return; } else projectileUpdate(deltaMS); // this pertains to the projectile's life cycle // don't move at all if the player didn't move? also, executive decision: this property is ignored if player owns projectile if (m_homingMovesWhenPlayerDoes && !ownedByPlayer() && !GameMap::CURRENT->didSoulBattleCharacterMove(DANMAKU_OWNER_PLAYER)) return; // if applicable, alter the projectile's speed. I'm intentionally doing this after the "player moved" check for balance/predictability reasons. applyAcceleration(deltaMS); // first of all, we need to get the angle to the nearest player double enemyX, enemyY; GameMap::CURRENT->getNearestSoulBattleEnemy(ownedByPlayer(), getX(), getY(), enemyX, enemyY); double perfectAngle = CDGMath::getLineAngle(getX(), getY(), enemyX, enemyY); // perfect homing is obviously very cheap. if (m_isPerfectHoming) m_motionAngle = perfectAngle; else { // but most projectiles are more complicated... if (m_homingAnglePerMS > 0.0) m_motionAngle = CDGMath::closeAngleRadGap(m_motionAngle, perfectAngle, (deltaMS * m_homingAnglePerMS)); // optional wonky movement if (m_homingDeviationPerMS > 0.0) m_motionAngle += deltaMS * Platform::randomDouble(-m_homingDeviationPerMS, m_homingDeviationPerMS); } // once angle is set, perform motion double maxDistance = CDGMath::getLineLength(getX(), getY(), enemyX, enemyY); double distance = m_motionTilesPerMS * deltaMS; double newX, newY; CDGMath::getLineMotionPoint(newX, newY, getX(), getY(), m_motionAngle, CDGMath::MIN(distance, maxDistance)); setXY(newX, newY); } void HomingProjectile::debugPrintStats() { if (sm_homingProjectileCreated != sm_homingProjectileDestroyed) DEBUG_LOG("Created %d HomingProjectile and destroyed %d HomingProjectile.", sm_homingProjectileCreated, sm_homingProjectileDestroyed); } /** * BasicProjectile */ int BasicProjectile::sm_basicProjectileCreated = 0; int BasicProjectile::sm_basicProjectileDestroyed = 0; BasicProjectile::BasicProjectile() { sm_basicProjectileCreated++; m_angleCurvePerMS = 0.0f; m_linearAccelerationTilesPerMS = 0.0f; } BasicProjectile::~BasicProjectile() { sm_basicProjectileDestroyed++; } void* BasicProjectile::operator new(size_t sz) { return MALLOC(sz); } void BasicProjectile::operator delete(void* p) { SAFE_FREE(p); } void BasicProjectile::basicProjectileSettings(double motionTilesPerMS, double motionAngleRad, double angleCurvePerMS, bool tieVisualAndMotionAngles) { m_motionTilesPerMS = motionTilesPerMS; // clamps won't override the default settings. m_motionAngle = motionAngleRad; m_angleCurvePerMS = angleCurvePerMS; m_tieVisualAndMotionAngles = tieVisualAndMotionAngles; } void BasicProjectile::basicAccelerationSettings(double linearAccelerationTilesPerMS) { m_linearAccelerationTilesPerMS = linearAccelerationTilesPerMS; } void BasicProjectile::update(int deltaMS) { // !!!NO MORE CODE IN THIS METHOD!!! basicProjectileUpdate(deltaMS); // !!!NO MORE CODE IN THIS METHOD!!! } void BasicProjectile::applyAcceleration(int deltaMS) { if (m_linearAccelerationTilesPerMS != 0.0) { m_motionTilesPerMS += (m_linearAccelerationTilesPerMS * deltaMS); } m_motionTilesPerMS = CDGMath::MAX(m_minSpeedTPMS, CDGMath::MIN(m_maxSpeedTPMS, m_motionTilesPerMS)); } void BasicProjectile::adjustMotionAngle(int& deltaMS, double& newX, double& newY) { if (m_turnStyle == DANMAKU_TURN_NORMAL) m_motionAngle = CDGMath::fixRadAngle(m_motionAngle + (m_angleCurvePerMS * deltaMS)); else if (m_hardTurnIntervalMS > 0) { int oldUptimeMS = m_uptimeMS - deltaMS; int intervalCount = m_uptimeMS / m_hardTurnIntervalMS; if (oldUptimeMS >= 0 && intervalCount > (oldUptimeMS / m_hardTurnIntervalMS)) { // do that slight amount of movement with the old angle int leftoverDeltaMS = m_hardTurnIntervalMS - (oldUptimeMS % m_hardTurnIntervalMS); double distance = m_motionTilesPerMS * leftoverDeltaMS; CDGMath::getLineMotionPoint(newX, newY, newX, newY, m_motionAngle, distance); deltaMS -= leftoverDeltaMS; double angleAdjust = m_angleCurvePerMS * 1000.0; if (m_turnStyle == DANMAKU_TURN_CONTINUOUS) m_motionAngle = CDGMath::fixRadAngle(m_motionAngle + angleAdjust); else if (m_turnStyle == DANMAKU_TURN_HARD_NEG_POS_ZERO) { // this variant goes plus-minus-minus-plus int intervalPos = intervalCount & 0x03; if (intervalPos == 0 || intervalPos == 3) m_motionAngle = CDGMath::fixRadAngle(m_motionAngle + angleAdjust); else m_motionAngle = CDGMath::fixRadAngle(m_motionAngle - angleAdjust); } else if (m_turnStyle == DANMAKU_TURN_HARD_NEG_POS) { // this variant goes plus-minus-plus-minus int intervalPos = intervalCount & 0x01; if (intervalPos == 0) m_motionAngle = CDGMath::fixRadAngle(m_motionAngle + angleAdjust); else m_motionAngle = CDGMath::fixRadAngle(m_motionAngle - angleAdjust); } } } } void BasicProjectile::basicProjectileUpdate(int deltaMS) { projectileUpdate(deltaMS); // if applicable, alter the projectile's speed applyAcceleration(deltaMS); double newX = getX(); double newY = getY(); // all a basic projectile has is basic motion. projectiles can turn and move, but they can do nothing else. // first, turn the projectile if (m_angleCurvePerMS != 0.0) adjustMotionAngle(deltaMS, newX, newY); // need to do it this way to synchronize hard turns // finally, move the projectile. boring, isn't it? if (m_motionTilesPerMS != 0.0 && deltaMS > 0) { double distance = m_motionTilesPerMS * deltaMS; CDGMath::getLineMotionPoint(newX, newY, newX, newY, m_motionAngle, distance); } setXY(newX, newY); } void BasicProjectile::adjustSpecialPosition(int deltaMS, double& posToAdjustX, double& posToAdjustY) { // if applicable, alter the projectile's speed if (m_linearAccelerationTilesPerMS != 0.0) m_motionTilesPerMS += (m_linearAccelerationTilesPerMS * deltaMS); m_motionTilesPerMS = CDGMath::MAX(m_minSpeedTPMS, CDGMath::MIN(m_maxSpeedTPMS, m_motionTilesPerMS)); // all a basic projectile has is basic motion. projectiles can turn and move, but they can do nothing else. // first, turn the projectile if (m_angleCurvePerMS != 0.0) adjustMotionAngle(deltaMS, posToAdjustX, posToAdjustY); // finally, move the projectile. boring, isn't it? if (m_motionTilesPerMS != 0.0 && deltaMS > 0) { double distance = m_motionTilesPerMS * deltaMS; CDGMath::getLineMotionPoint(posToAdjustX, posToAdjustY, posToAdjustX, posToAdjustY, m_motionAngle, distance); } } void BasicProjectile::debugPrintStats() { if (sm_basicProjectileCreated != sm_basicProjectileDestroyed) DEBUG_LOG("Created %d BasicProjectile and destroyed %d BasicProjectile.", sm_basicProjectileCreated, sm_basicProjectileDestroyed); } /** * Projectile */ int DanmakuProjectile::sm_projectileCreated = 0; int DanmakuProjectile::sm_projectileDestroyed = 0; int DanmakuProjectile::sm_playerProjectileCount = 0; int DanmakuProjectile::sm_enemyProjectileCount = 0; Image* DanmakuProjectile::sm_projectileSheet = null; ProjectileSpec* DanmakuProjectile::sm_projectileSpecs = null; int DanmakuProjectile::sm_numSpecs = 0; int DanmakuProjectile::sm_imageFileLocation = 0; // linked lists DanmakuProjectile* DanmakuProjectile::sm_heads[NUM_PROJECTILE_LAYERS] = { null, null, null, null, null, null, null, null, null, null }; DanmakuProjectile* DanmakuProjectile::sm_tails[NUM_PROJECTILE_LAYERS] = { null, null, null, null, null, null, null, null, null, null }; // caching these saves calculations double DanmakuProjectile::sm_leftX = 0.0; double DanmakuProjectile::sm_topY = 0.0; double DanmakuProjectile::sm_rightX = 1.0; double DanmakuProjectile::sm_bottomY = 1.0; int DanmakuProjectile::sm_healingFeatures[DANMAKU_DIFFICULTY_COUNT]; AnimationData* DanmakuProjectile::sm_sparkle = null; // doesn't belong to this class bool DanmakuProjectile::sm_sparkleLoaded = false; ColorSet* DanmakuProjectile::sm_tmpColorSet = null; bool DanmakuProjectile::sm_shouldDestroyPlayerHomingProjectiles = false; DanmakuProjectile::DanmakuProjectile() { sm_projectileCreated++; m_damage = 1; m_persistOnHit = false; m_canTeamkill = false; m_x = m_prevX = m_y = m_prevY = UNDEFINED_FLOAT; // center X, even for rectangles. 1.0 is one tile. // set some good expire conditions for projectiles, lest the game crash ;) // naturally these will be overriden by the spawners m_expireTimeMS = 10000; m_shouldDieOffscreen = true; // in case these get accessed before they're formally set within the statics m_prev = null; m_next = null; // motion... m_motionTilesPerMS = 0.0f; m_motionAngle = 0.0f; m_angleCurvePerMS = 0.0; // fleeing which happens between life bars m_fleeing = false; // need to set this so I can differentiate reflect projectiles from normal projectiles when doing tallies. m_owner = DANMAKU_OWNER_INVALID; sm_enemyProjectileCount++; m_uptimeMS = 0; m_fixedAngle = 0.0; m_colorSet.setColor(0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, false); m_usesScale = false; m_immuneToBarriers = false; m_subclassControlsRotation = false; } DanmakuProjectile::~DanmakuProjectile() { sm_projectileDestroyed++; m_spec = null; // seriously, DO NOT mess with its contents! if (m_owner == DANMAKU_OWNER_PLAYER) sm_playerProjectileCount--; else sm_enemyProjectileCount--; //DEBUG_LOG("projectile destroyed, counts: %d, %d", sm_playerProjectileCount, sm_enemyProjectileCount); } void* DanmakuProjectile::operator new(size_t sz) { return MALLOC(sz); } void DanmakuProjectile::operator delete(void* p) { SAFE_FREE(p); } void DanmakuProjectile::setOwner(int owner) { if (owner == m_owner) return; if (m_owner == DANMAKU_OWNER_PLAYER) { // player lost ownership of one of their own sm_playerProjectileCount--; sm_enemyProjectileCount++; } else if (owner == DANMAKU_OWNER_PLAYER) { sm_playerProjectileCount++; // player got one spawned or via reflect sm_enemyProjectileCount--; // because all projectiles by default are the enemy's } m_owner = owner; } #ifdef PLATFORM_ANDROID void DanmakuProjectile::update(int deltaMS) { } #endif // very few things use this void DanmakuProjectile::updateBase(int deltaMS) { if (m_spec->useFixedRotation) { m_fixedAngle = CDGMath::fixRadAngle(m_fixedAngle + (m_spec->fixedAnglePerMS * deltaMS)); } } #define HEALING_TELL_OSCILLATE_MS 666 // must be even! #define PROJECTILE_FADE_TIME 500 void DanmakuProjectile::render() { if (sm_projectileSheet == null) return; // set the color set if (m_damage < 0 && (sm_healingFeatures[Game::getDanmakuDifficulty()] & DANMAKU_HEALING_TELL_BRIGHTNESS)) { uint32 color = m_colorSet.topLeft; uint32 blendVal = ((uint32)m_uptimeMS) % HEALING_TELL_OSCILLATE_MS; if (blendVal >= (HEALING_TELL_OSCILLATE_MS>>1)) blendVal = HEALING_TELL_OSCILLATE_MS - blendVal; blendVal = 0xc0 + (0x3f * blendVal / (HEALING_TELL_OSCILLATE_MS>>1)); color = ((((color>>24)&0xff) * blendVal / 0xff)<<24) | ((((color>>16)&0xff) * blendVal / 0xff)<<16) | ((((color>>8)&0xff) * blendVal / 0xff)<<8) | (color&0xff); sm_tmpColorSet->recolor(color); sm_projectileSheet->setColorSet(sm_tmpColorSet); } else if (m_expireTimeMS < PROJECTILE_FADE_TIME) { // fade projectiles out gracefully, rather than having them spontaneously disappear. uint32 color = m_colorSet.topLeft; color = (color & 0xffffff00) | ((color & 0x000000ff) * CDGMath::MAX(0, m_expireTimeMS) / PROJECTILE_FADE_TIME); sm_tmpColorSet->recolor(color); sm_projectileSheet->setColorSet(sm_tmpColorSet); } else sm_projectileSheet->setColorSet(m_colorSet); // add rotation. todo: add two more params in the event scaling is added. double x, y; Camera::MAIN->getRenderPositionFromTilesXY(m_x, m_y, x, y); // rotation angle double angle = 0.0; if (m_spec->useFixedRotation) // this trumps all other sources of rotation. angle = m_fixedAngle; else if (m_spec->canRotate && m_subclassControlsRotation) angle = m_subclassFixedAngle; else if (m_spec->canRotate && m_tieVisualAndMotionAngles) angle = m_motionAngle; if (m_usesScale) { // NOTE: This assumes x and y are CX and CY, which m_x and m_y already are. sm_projectileSheet->renderRotatedAndScaled((float)x, (float)y, (float)angle, (float)m_scaleBy, m_spec->imageCoords.x1, m_spec->imageCoords.y1, m_spec->imageCoords.getWidth(), m_spec->imageCoords.getHeight()); } else { x += m_imageOffsetRect.x1; y += m_imageOffsetRect.y1; //x -= (m_spec->imageCoords.getWidth() * 0.5); //y -= (m_spec->imageCoords.getHeight() * 0.5); if (angle != 0.0) sm_projectileSheet->renderRotatedRad((float)x, (float)y, angle, m_spec->imageCoords.x1, m_spec->imageCoords.y1, m_spec->imageCoords.getWidth(), m_spec->imageCoords.getHeight()); else sm_projectileSheet->render((float)x, (float)y, (float)m_spec->imageCoords.getWidth(), (float)m_spec->imageCoords.getHeight(), m_spec->imageCoords.x1, m_spec->imageCoords.y1, m_spec->imageCoords.getWidth(), m_spec->imageCoords.getHeight()); } // remove the color set. sm_projectileSheet->removeColorSet(); } void DanmakuProjectile::renderSpecialEffects() { // draw the sparkles if applicable if (m_damage < 0 && (sm_healingFeatures[Game::getDanmakuDifficulty()] & DANMAKU_HEALING_TELL_PARTICLE)) { double x, y; Camera::MAIN->getRenderPositionFromTilesXY(m_x, m_y, x, y); x -= (m_spec->imageCoords.getWidth() * 0.5); y -= (m_spec->imageCoords.getHeight() * 0.5); ImageUtils::renderSparkleTypeParticles(sm_sparkle, 583821, x, y, x + m_spec->imageCoords.getWidth() - 1, y + m_spec->imageCoords.getHeight() - 1, m_uptimeMS, 175); } } void DanmakuProjectile::setProjectileColor(uint32 colorRGBA) { m_colorSet.setColor(colorRGBA, colorRGBA, colorRGBA, colorRGBA, false); } void DanmakuProjectile::setProjectileColors(uint32 topLeftRGBA, uint32 bottomLeftRGBA, uint32 bottomRightRGBA, uint32 topRightRGBA) { m_colorSet.setColor(topLeftRGBA, bottomLeftRGBA, bottomRightRGBA, topRightRGBA, false); } void DanmakuProjectile::projectileUpdate(int deltaMS) { m_expireTimeMS -= deltaMS; // something else will check for expiration m_uptimeMS += deltaMS; } void DanmakuProjectile::setXY(double newX, double newY) { m_x = newX; m_y = newY; } void DanmakuProjectile::spawnSetXY(double x, double y) { m_x = m_prevX = x; m_y = m_prevY = y; } void DanmakuProjectile::postProcess() { if (m_spec == null) return; // not good... m_scaledRadius = m_spec->hitboxRadius; m_hitboxHalfWidth = m_spec->hitboxHalfWidth; m_hitboxHalfHeight = m_spec->hitboxHalfHeight; m_imageOffsetRect.x1 = -(m_spec->imageCoords.getWidth() * 0.5); m_imageOffsetRect.y1 = -(m_spec->imageCoords.getHeight() * 0.5); m_imageOffsetRect.x2 = -m_imageOffsetRect.x1; m_imageOffsetRect.y2 = -m_imageOffsetRect.y1; if (m_usesScale) { m_scaledRadius *= m_scaleBy; m_hitboxHalfWidth *= m_scaleBy; m_hitboxHalfHeight *= m_scaleBy; m_imageOffsetRect.x1 *= m_scaleBy; m_imageOffsetRect.y1 *= m_scaleBy; m_imageOffsetRect.x2 *= m_scaleBy; m_imageOffsetRect.y2 *= m_scaleBy; } } void DanmakuProjectile::loadProjectileData(File& inFile) { Rect dummyRect; // first load the projectile data. sm_numSpecs = inFile.readInt(DUMMY_BOOL); DEBUG_LOG("Will load %d projectile specs.", sm_numSpecs); sm_projectileSpecs = NEW_STRUCT(ProjectileSpec, sm_numSpecs); for (int i = 0; i < sm_numSpecs; i++) { sm_projectileSpecs[i].id = inFile.readInt(DUMMY_BOOL); sm_projectileSpecs[i].canRotate = inFile.readBool(DUMMY_BOOL); sm_projectileSpecs[i].isRectHitbox = inFile.readBool(DUMMY_BOOL); sm_projectileSpecs[i].hitboxRadius = inFile.readFloatFromString(DUMMY_BOOL); // NOTE: Although in java land these are integers, in C++ land they're more efficiently treated as doubles. sm_projectileSpecs[i].hitboxHalfWidth = inFile.readInt(DUMMY_BOOL) * 0.5; sm_projectileSpecs[i].hitboxHalfHeight = inFile.readInt(DUMMY_BOOL) * 0.5; sm_projectileSpecs[i].useFixedRotation = inFile.readBool(DUMMY_BOOL); sm_projectileSpecs[i].fixedAnglePerMS = DEG_TO_RAD(inFile.readFloatFromString(DUMMY_BOOL)) * 0.001; inFile.readRect(sm_projectileSpecs[i].imageCoords); inFile.readString(sm_projectileSpecs[i].name); // NOW THAT ALL IMPORTS ARE DONE... // lets calculate these two now for efficiency sm_projectileSpecs[i].imageHalfWidthInTiles = (sm_projectileSpecs[i].imageCoords.getWidth() * 0.5) / Game::getTileWH(); sm_projectileSpecs[i].imageHalfHeightInTiles = (sm_projectileSpecs[i].imageCoords.getHeight() * 0.5) / Game::getTileWH(); } // now we should be at the image. store it for later. sm_imageFileLocation = inFile.getStreamPosition(); // and skip past the image for now int fileSize = inFile.readInt(DUMMY_BOOL); if (fileSize > 0) inFile.skip(fileSize); } void DanmakuProjectile::loadProjectileImage(File& inFile) { // load the image using the previously provided coordinate. inFile.jumpTo(sm_imageFileLocation); int fileSize = inFile.readInt(DUMMY_BOOL); if (fileSize <= 0) { sm_projectileSheet = null; DEBUG_LOG("WARNING: Projectile sheet image is null. Danmaku will be invisible."); } else sm_projectileSheet = PICReader::createImageFromFIS(inFile, 0, 0, fileSize); } void DanmakuProjectile::destroyProjectileData() { // destroy all the specs. SAFE_DELETE(sm_projectileSheet); SAFE_FREE(sm_projectileSpecs); sm_numSpecs = 0; } void DanmakuProjectile::addProjectile(DanmakuProjectile* proj, ProjectileSpec* spec, int layer) { if (!sm_sparkleLoaded) { sm_healingFeatures[DANMAKU_DIFFICULTY_CASUAL] = DANMAKU_HEALING_TELL_BRIGHTNESS | DANMAKU_HEALING_TELL_PARTICLE; sm_healingFeatures[DANMAKU_DIFFICULTY_EASY] = DANMAKU_HEALING_TELL_BRIGHTNESS | DANMAKU_HEALING_TELL_PARTICLE; sm_healingFeatures[DANMAKU_DIFFICULTY_NORMAL] = DANMAKU_HEALING_TELL_BRIGHTNESS | DANMAKU_HEALING_TELL_PARTICLE; sm_healingFeatures[DANMAKU_DIFFICULTY_HARD] = DANMAKU_HEALING_TELL_PARTICLE; sm_healingFeatures[DANMAKU_DIFFICULTY_LUNATIC] = DANMAKU_HEALING_DESTROYED_BY_SHIELD; sm_healingFeatures[DANMAKU_DIFFICULTY_LOCK] = DANMAKU_HEALING_DESTROYED_BY_SHIELD; sm_sparkle = MiscGraphics::instance->getAnimForEntry(DANMAKU_HEALING_PARTICLE_NAME); sm_sparkleLoaded = true; } proj->m_spec = spec; proj->m_layer = layer; if (sm_heads[layer] == null) sm_heads[layer] = proj; if (sm_tails[layer] != null) { sm_tails[layer]->m_next = proj; proj->m_prev = sm_tails[layer]; } sm_tails[layer] = proj; } void DanmakuProjectile::updateAll(int deltaMS) { if (!GameMap::CURRENT) return; // with thousands of projectiles, why I'm doing it like this should be obvious GameMap::CURRENT->getSoulBattleBoundsAsTiles(sm_leftX, sm_topY, sm_rightX, sm_bottomY); // remove expired projectiles DanmakuProjectile* cur; for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { cur = sm_heads[layer]; while (cur) { if ((cur->m_shouldDieOffscreen && cur->isOOB() && (cur->m_fleeing || cur->m_uptimeMS >= cur->m_offscreenGraceMS)) || cur->m_expireTimeMS < 0) { DanmakuProjectile* newCur = cur->m_next; destroyProjectile(cur); cur = newCur; continue; } // if it survived that, store its location now before any manipulation can occur cur->storeLocation(); // iterate cur = cur->m_next; } } // now update all the survivors for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { cur = sm_heads[layer]; while (cur) { cur->m_prevX = cur->m_x; cur->m_prevY = cur->m_y; if (cur->m_fleeing) cur->updateFlee(deltaMS); else { // player projectiles are subject to modification by gravity wells // do it before the standard update. (this also allows orbitals to reject the new position) if (cur->m_owner == DANMAKU_OWNER_PLAYER) { cur->m_motionAngle = SoulGravityWell::getAdjustedProjectileAngle(deltaMS, cur->m_motionAngle, cur->m_x, cur->m_y); SoulGravityWell::adjustProjectilePosition(deltaMS, cur->m_x, cur->m_y, cur->m_x, cur->m_y); double oldMTMS = cur->m_motionTilesPerMS; cur->m_motionTilesPerMS = SoulGravityWell::getAdjustedProjectileSpeed(deltaMS, cur->m_motionTilesPerMS, cur->m_x, cur->m_y); if (cur->m_motionTilesPerMS == 0.0 && oldMTMS != 0.0) { // the gravity well effectively killed this projectile. trigger its destruction. cur->m_expireTimeMS = CDGMath::MIN(PROJECTILE_FADE_TIME, cur->m_expireTimeMS); } } cur->update(deltaMS); } // a few special things, like projectile's independent rotation stats cur->updateBase(deltaMS); cur = cur->m_next; } } // homing projectiles have queued their destruction. cheapness averted. sm_shouldDestroyPlayerHomingProjectiles = false; } void DanmakuProjectile::renderAll() { for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { DanmakuProjectile* cur = sm_heads[layer]; while (cur) { cur->render(); cur = cur->m_next; } // separate becuase frames will drop greatly if sparkles keep interrupting long sheets of projectiles // but layers are uncommon enough that no more than 9 interruptions are okay // (and it'll usually just be 1...layer 0's sparkles interrupting the player's layer) cur = sm_heads[layer]; while (cur) { cur->renderSpecialEffects(); cur = cur->m_next; } } } void DanmakuProjectile::destroyProjectile(DanmakuProjectile* proj) { // remove self from the list if (proj->m_prev != null) proj->m_prev->m_next = proj->m_next; if (proj->m_next != null) proj->m_next->m_prev = proj->m_prev; // fix the list's enders int layer = proj->m_layer; if (proj == sm_heads[layer]) sm_heads[layer] = proj->m_next; if (proj == sm_tails[layer]) sm_tails[layer] = proj->m_prev; // now delete this thing! SAFE_DELETE(proj); } void DanmakuProjectile::destroyAllProjectiles() { for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { while (sm_heads[layer]) { DanmakuProjectile* proj = sm_heads[layer]; sm_heads[layer] = proj->m_next; SAFE_DELETE(proj); } sm_tails[layer] = null; } } void DanmakuProjectile::handleShieldCollision(SoulShield* shield, int deltaMS) { if (GameMap::CURRENT == null) return; for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { DanmakuProjectile* cur = sm_heads[layer]; bool shouldDestroyHealingProjectiles = (sm_healingFeatures[Game::getDanmakuDifficulty()] & DANMAKU_HEALING_DESTROYED_BY_SHIELD) != 0; while (cur) { // skip this check if shields shouldn't destroy if (cur->m_damage < 0 && shield->getType() != SOUL_SHIELD_INCREASE_DAMAGE) { if (!shouldDestroyHealingProjectiles) { cur = cur->m_next; continue; } } // don't bother if current is immune to shields entirely if (cur->m_immuneToShields) { cur = cur->m_next; continue; } DanmakuProjectile* next = cur->m_next; if ((cur->getOwner() == DANMAKU_OWNER_PLAYER && shield->getOwner() != DANMAKU_OWNER_PLAYER) || (cur->getOwner() != DANMAKU_OWNER_PLAYER && shield->getOwner() == DANMAKU_OWNER_PLAYER) || (cur->m_canTeamkill && cur->getOwner() != shield->getOwner())) { // check collision bool collisionCheck = false; if (cur->m_spec->isRectHitbox) { double x = shield->getX(); double y = shield->getY(); double radius = shield->getRadiusInTiles(); double angle = (cur->m_spec->canRotate && cur->m_tieVisualAndMotionAngles) ? cur->m_motionAngle : 0.0; if (angle == 0.0) { // use this cheaper method if the rectangular projectile can't rotate double x1 = cur->m_x - (cur->m_hitboxHalfWidth + radius); double x2 = cur->m_x + (cur->m_hitboxHalfWidth + radius); double y1 = cur->m_y - (cur->m_hitboxHalfHeight + radius); double y2 = cur->m_y + (cur->m_hitboxHalfHeight + radius); if (x >= x1 && x <= x2 && y >= y1 && y <= y2) collisionCheck = true; } else collisionCheck = CDGMath::isPointWithinRotatedRect(x, y, radius, cur->m_x - cur->m_hitboxHalfWidth, cur->m_y - cur->m_hitboxHalfHeight, cur->m_x + cur->m_hitboxHalfWidth, cur->m_y + cur->m_hitboxHalfHeight, angle); } else { if (CDGMath::isInRange(cur->m_x, cur->m_y, shield->getX(), shield->getY(), cur->m_scaledRadius + shield->getRadiusInTiles())) collisionCheck = true; } if (collisionCheck) { if (shield->getType() == SOUL_SHIELD_DESTROY || shield->getType() == SOUL_SHIELD_OMNI) { // omni destroys persistent projectiles, but not normal projectiles. if (!(cur->m_persistOnHit && shield->getType() == SOUL_SHIELD_DESTROY)) destroyProjectile(cur); } else if (shield->getType() == SOUL_SHIELD_HP_ABSORB) { GameMap::CURRENT->handleSoulBattleHPChange(shield->getOwner(), deltaMS, -cur->m_damage * shield->getAbsorbOrEnhanceFactor(), false); // in this case, it makes sense for projectiles to not persist. destroyProjectile(cur); } else if (shield->getType() == SOUL_SHIELD_REFLECT) { // need to do a proper reflection, first get the angle from the projectile center to the shield's center double angleToShieldCenter = CDGMath::getLineAngle(cur->getX(), cur->getY(), shield->getX(), shield->getY()); // with a bit of messy angle math, determine the new angle double newAngle = cur->m_motionAngle; if ((cur->m_motionAngle <= angleToShieldCenter && cur->m_motionAngle + (PI * 0.5) > angleToShieldCenter) // no wraparound case || (cur->m_motionAngle - TAU < angleToShieldCenter && cur->m_motionAngle - TAU + (PI * 0.5) > angleToShieldCenter)) // wraparound case { double tmpMotionAngle = cur->m_motionAngle; if (cur->m_motionAngle - TAU < angleToShieldCenter && cur->m_motionAngle - TAU + (PI * 0.5) >= angleToShieldCenter) tmpMotionAngle -= TAU; newAngle = CDGMath::fixRadAngle(angleToShieldCenter + PI + (angleToShieldCenter - tmpMotionAngle)); } else if ((cur->m_motionAngle >= angleToShieldCenter && cur->m_motionAngle - (PI * 0.5) < angleToShieldCenter) // no wraparound case || (cur->m_motionAngle + TAU > angleToShieldCenter && cur->m_motionAngle + TAU - (PI * 0.5) < angleToShieldCenter)) // wraparound case { double tmpMotionAngle = cur->m_motionAngle; if (cur->m_motionAngle + TAU > angleToShieldCenter && cur->m_motionAngle + TAU - (PI * 0.5) < angleToShieldCenter) tmpMotionAngle += TAU; newAngle = CDGMath::fixRadAngle(angleToShieldCenter + PI + (angleToShieldCenter - tmpMotionAngle)); } // if it's outside that 180 degrees of acceptable range, it means the projectile moves so fast // or the shield is so small, that it passed through the center in one frame. it's best not to touch it. //DEBUG_LOG("Changing angle. center=%f in=%f out=%f", RAD_TO_DEG(angleToShieldCenter), RAD_TO_DEG(cur->m_motionAngle), RAD_TO_DEG(newAngle)); // if existing owner is the player, reduce damage to 1, lest the player get murdered by their high damage "can't miss" homing projectiles if (cur->getOwner() == DANMAKU_OWNER_PLAYER && cur->m_damage > 1.0) cur->m_damage = 1.0; // now make some intuitive changes to the projectile, including ownership, angle, and curve cur->m_motionAngle = newAngle; cur->setOwner(shield->getOwner()); cur->m_angleCurvePerMS = -(cur->m_angleCurvePerMS); // recolor as specified //cur->m_colorSet.invert(); if (shield->shouldRecolor()) cur->m_colorSet.recolor(shield->newProjectileColor()); } else if (shield->getType() == SOUL_SHIELD_INCREASE_DAMAGE) { bool isInMercy, hasMercy; GameMap::CURRENT->getSoulBattleMercyInfo(shield->getOwner(), isInMercy, hasMercy); double damageMod = (cur->m_persistOnHit && (!hasMercy || cur->m_damage < 0)) ? Game::modifyDamage(1, deltaMS) : 1.0; GameMap::CURRENT->handleSoulBattleHPChange(shield->getOwner(), deltaMS, cur->m_damage * damageMod * shield->getAbsorbOrEnhanceFactor(), true); if (!cur->m_persistOnHit) destroyProjectile(cur); } } } cur = next; } } } //#define CHEAP_COLLISION void DanmakuProjectile::handleCharCollision(int characterIdx, int deltaMS, double& outDamage, bool& tookDamage) { if (GameMap::CURRENT == null) return; double x, y; bool isInMercy, hasMercy; GameMap::CURRENT->getSoulBattleMercyInfo(characterIdx, isInMercy, hasMercy); double radius = GameMap::CURRENT->getSoulBattleHitboxRadius(characterIdx); GameMap::CURRENT->getSoulBattleCharacterPos(characterIdx, x, y); for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { DanmakuProjectile* cur = sm_heads[layer]; while (cur) { DanmakuProjectile* next = cur->m_next; // ownership valid for collision? if ((cur->getOwner() == DANMAKU_OWNER_PLAYER && characterIdx != DANMAKU_OWNER_PLAYER) || (cur->getOwner() != DANMAKU_OWNER_PLAYER && characterIdx == DANMAKU_OWNER_PLAYER) || (cur->m_canTeamkill && cur->getOwner() != characterIdx)) { // check collision bool collisionCheck = false; if (cur->m_spec->isRectHitbox) { double angle = (cur->m_spec->canRotate && cur->m_tieVisualAndMotionAngles) ? cur->m_motionAngle : 0.0; if (angle == 0.0) { // use this cheaper method if the rectangular projectile can't rotate double x1 = cur->m_x - (cur->m_hitboxHalfWidth + radius); double x2 = cur->m_x + (cur->m_hitboxHalfWidth + radius); double y1 = cur->m_y - (cur->m_hitboxHalfHeight + radius); double y2 = cur->m_y + (cur->m_hitboxHalfHeight + radius); if (x >= x1 && x <= x2 && y >= y1 && y <= y2) collisionCheck = true; } else collisionCheck = CDGMath::isPointWithinRotatedRect(x, y, radius, cur->m_x - cur->m_hitboxHalfWidth, cur->m_y - cur->m_hitboxHalfHeight, cur->m_x + cur->m_hitboxHalfWidth, cur->m_y + cur->m_hitboxHalfHeight, angle); } else { #ifdef CHEAP_COLLISION if (CDGMath::isInRange(cur->m_x, cur->m_y, x, y, cur->m_scaledRadius + radius)) collisionCheck = true; #else if (CDGMath::isPointInRangeOfLineSegment(x, y, cur->m_x, cur->m_y, cur->m_prevX, cur->m_prevY, cur->m_scaledRadius + radius)) collisionCheck = true; #endif } if (collisionCheck) { if (cur->m_phsIndex != -1) { MMObject* owner = GameMap::CURRENT->getObjectFromDanmakuEntRef(cur->m_owner); if (owner != null) owner->reportProjectileHit(cur->m_phsIndex); cur->m_phsIndex = -1; // in case of persist on hit. } // projectiles that persist must damage a player at a consistent rate // regardless of any slowdown. however, if the player has mercy invincibility, they take flat damage. double damageMod = (cur->m_persistOnHit && (!hasMercy || cur->m_damage < 0)) ? Game::modifyDamage(1, deltaMS) : 1.0; // if damage is less than 0, no mercy checks are required if (cur->m_damage < 0) { outDamage += cur->m_damage * damageMod; } else { // otherwise, gotta do mercy checks if (!isInMercy) { outDamage += cur->m_damage * damageMod; tookDamage = true; if (hasMercy) isInMercy = true; // might as well recycle variables } } // delete projectile if you should if (!cur->m_persistOnHit) destroyProjectile(cur); } } cur = next; } } } void DanmakuProjectile::handleBarrierCollision(SoulBarrier* barrier) { // get these now bool shouldLoop = barrier->shouldLoopProjectiles(); // yep, gonna loop projectiles too :D bool shouldRedirect = barrier->shouldRedirectProjectiles(); for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { DanmakuProjectile* cur = sm_heads[layer]; while (cur) { DanmakuProjectile* next = cur->m_next; if (cur->m_owner == DANMAKU_OWNER_PLAYER && !cur->m_immuneToBarriers) // enemy projectiles not subject to same rules { if (barrier->isMainBarrier()) { if (barrier->getShape() == SOUL_BARRIER_CIRCLE) { double currentRadius = barrier->getCurrentRadius(); double barrierX, barrierY; barrier->getXY(barrierX, barrierY); double projDist = CDGMath::getLineLength(cur->m_x, cur->m_y, barrierX, barrierY); if (projDist > currentRadius) { if (shouldLoop) { // intentionally starting from projectile's position double angle = CDGMath::getLineAngle(cur->m_x, cur->m_y, barrierX, barrierY); double newDist = currentRadius - (projDist - currentRadius); CDGMath::getLineMotionPoint(cur->m_x, cur->m_y, barrierX, barrierY, angle, newDist); } else if (shouldRedirect) { // intentionally starting from projectile's position double angle = CDGMath::getLineAngle(cur->m_x, cur->m_y, barrierX, barrierY); CDGMath::getLineMotionPoint(cur->m_x, cur->m_y, barrierX, barrierY, angle, projDist); cur->m_immuneToBarriers = true; // it can only be redirected once. // change projectile's motion angle cur->m_motionAngle = CDGMath::fixRadAngle(cur->m_motionAngle + RAD_180); } else destroyProjectile(cur); } } else if (barrier->getShape() == SOUL_BARRIER_RECT) { double areaX1, areaY1, areaX2, areaY2; barrier->getCurrentRectBounds(areaX1, areaY1, areaX2, areaY2); // lets separate looping from others, because others are just a min/max if (shouldLoop) { if (areaX1 > cur->m_x) cur->m_x = areaX2 - (areaX1 - cur->m_x); else if (areaX2 < cur->m_x) cur->m_x = areaX1 + (cur->m_x - areaX2); if (areaY1 > cur->m_y) cur->m_y = areaY2 - (areaY1 - cur->m_y); else if (areaY2 < cur->m_y) cur->m_y = areaY1 + (cur->m_y - areaY2); } else if (shouldRedirect) { if (areaX1 > cur->m_x) { cur->m_x = areaX2 + (areaX1 - cur->m_x); cur->m_immuneToBarriers = true; cur->m_motionAngle = CDGMath::invertAngleLR(cur->m_motionAngle); } else if (areaX2 < cur->m_x) { cur->m_x = areaX1 - (cur->m_x - areaX2); cur->m_immuneToBarriers = true; cur->m_motionAngle = CDGMath::invertAngleLR(cur->m_motionAngle); } if (areaY1 > cur->m_y) { cur->m_y = areaY2 + (areaY1 - cur->m_y); cur->m_immuneToBarriers = true; cur->m_motionAngle = CDGMath::invertAngleUD(cur->m_motionAngle); } else if (areaY2 < cur->m_y) { cur->m_y = areaY1 - (cur->m_y - areaY2); cur->m_immuneToBarriers = true; cur->m_motionAngle = CDGMath::invertAngleUD(cur->m_motionAngle); } } else if (areaX1 > cur->m_x || areaX2 < cur->m_x || areaY1 > cur->m_y || areaY2 < cur->m_y) destroyProjectile(cur); } } else { DEBUG_LOG("TODO handle projectile-barrier collision for secondary barriers."); } } cur = next; } } } bool DanmakuProjectile::isOOB() // are we hopelessly out of bounds of the valid area? { if (m_usesScale) return sm_leftX > m_x + m_imageOffsetRect.x2 || sm_rightX < m_x + m_imageOffsetRect.x1 || sm_topY > m_y + m_imageOffsetRect.y2 || sm_bottomY < m_y + m_imageOffsetRect.y1; return sm_leftX > m_x + m_spec->imageHalfWidthInTiles || sm_rightX < m_x - m_spec->imageHalfWidthInTiles || sm_topY > m_y + m_spec->imageHalfHeightInTiles || sm_bottomY < m_y - m_spec->imageHalfHeightInTiles; } ProjectileSpec* DanmakuProjectile::getProjectileSpecWithId(int id) { for (int i = 0; i < sm_numSpecs; i++) { if (sm_projectileSpecs[i].id == id) return &(sm_projectileSpecs[i]); } return null; } ProjectileSpec* DanmakuProjectile::getProjectileSpecWithName(const char* name) { for (int i = 0; i < sm_numSpecs; i++) { if (sm_projectileSpecs[i].name.equals(name)) return &(sm_projectileSpecs[i]); } return null; } ProjectileSpec* DanmakuProjectile::debugGetSpecByArrayIdx(int arrayIdx) { if (arrayIdx < 0 || arrayIdx >= sm_numSpecs) return null; return &sm_projectileSpecs[arrayIdx]; } ProjectileSpec* DanmakuProjectile::debugGetProjectileSpecWithArrayIdx(int arrayIdx) { if (arrayIdx < 0 || arrayIdx >= sm_numSpecs) return null; return &(sm_projectileSpecs[arrayIdx]); } void DanmakuProjectile::debugPrintStats() { if (sm_projectileCreated != sm_projectileDestroyed) DEBUG_LOG("Created %d DanmakuProjectile and destroyed %d DanmakuProjectile.", sm_projectileCreated, sm_projectileDestroyed); } int DanmakuProjectile::memGetNumProjectilesLoaded() { return sm_projectileCreated - sm_projectileDestroyed; } void DanmakuProjectile::triggerFlee(double tilesPerMS, double fromX, double fromY) { for (int layer = 0; layer < NUM_PROJECTILE_LAYERS; layer++) { DanmakuProjectile* cur = sm_heads[layer]; while (cur) { if (cur->m_owner != DANMAKU_OWNER_PLAYER) { cur->m_fleeing = true; cur->m_shouldDieOffscreen = true; cur->m_motionTilesPerMS = tilesPerMS; // intentionally ignores clamp cur->m_motionAngle = CDGMath::getLineAngle(fromX, fromY, cur->m_x, cur->m_y); } cur = cur->m_next; } } } void DanmakuProjectile::updateFlee(int deltaMS) { double distance = m_motionTilesPerMS * deltaMS; double newX, newY; CDGMath::getLineMotionPoint(newX, newY, getX(), getY(), m_motionAngle, distance); setXY(newX, newY); m_expireTimeMS -= deltaMS; m_uptimeMS += deltaMS; } /** * Cleanup info */ void Projectile_PrintAllStats() { ScriptProjectile::debugPrintStats(); ComplexProjectile::debugPrintStats(); HomingProjectile::debugPrintStats(); BasicProjectile::debugPrintStats(); DanmakuProjectile::debugPrintStats(); }