/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "efh/efh.h" namespace Efh { void EfhEngine::createOpponentList(int16 monsterTeamId) { debugC(3, kDebugFight, "createOpponentList %d", monsterTeamId); int16 counter = 0; if (monsterTeamId != -1 && countAliveMonsters(monsterTeamId) > 0) { counter = 1; _teamMonster[0]._id = monsterTeamId; } for (int counter2 = 1; counter2 <= 3; ++counter2) { if (counter >= 5) break; for (uint monsterId = 0; monsterId < 64; ++monsterId) { MapMonster *curMapMonst = &_mapMonsters[_techId][monsterId]; if (curMapMonst->_fullPlaceId == 0xFF) continue; if (((curMapMonst->_possessivePronounSHL6 & 0x3F) != 0x3F || isNpcATeamMember(curMapMonst->_npcId)) && (curMapMonst->_possessivePronounSHL6 & 0x3F) > 0x3D) continue; if (!checkIfMonsterOnSameLargeMapPlace(monsterId)) continue; bool found = false; for (uint subId = 0; subId < 9; ++subId) { if (curMapMonst->_hitPoints[subId] > 0) { found = true; break; } } if (found) { if (computeMonsterGroupDistance(monsterId) <= counter2 && !isMonsterAlreadyFighting(monsterId, counter)) { _teamMonster[counter]._id = monsterId; if (++counter >= 5) break; } } } } if (counter > 4) return; for (uint id = counter; id < 5; ++id) _teamMonster[id]._id = -1; } void EfhEngine::initFight(int16 monsterId) { debugC(3, kDebugFight, "initFight %d", monsterId); createOpponentList(monsterId); resetTeamMonsterEffects(); } bool EfhEngine::handleFight(int16 monsterId) { debugC(3, kDebugFight, "handleFight %d", monsterId); _ongoingFightFl = true; initFight(monsterId); if (_teamMonster[0]._id == -1) { resetTeamMonsterIdArray(); _ongoingFightFl = false; displayAnimFrames(0xFE, true); return true; } _sayMenu = false; drawCombatScreen(0, false, true); for (bool mainLoopCond = false; !mainLoopCond && !shouldQuit();) { if (isTPK()) { resetTeamMonsterIdArray(); _ongoingFightFl = false; displayAnimFrames(0xFE, true); return false; } if (_teamMonster[0]._id == -1) { resetTeamMonsterIdArray(); _ongoingFightFl = false; displayAnimFrames(0xFE, true); return true; } displayAnimFrames(getTeamMonsterAnimId(), true); for (int counter = 0; counter < _teamSize; ++counter) { _teamChar[counter]._pctVisible = 100; _teamChar[counter]._pctDodgeMiss = 65; } if (!getTeamAttackRoundPlans()) { resetTeamMonsterIdArray(); _ongoingFightFl = false; totalPartyKill(); displayAnimFrames(0xFE, true); return false; } for (int counter = 0; counter < _teamSize; ++counter) { if (_teamChar[counter]._lastAction == 0x52) // 'R' mainLoopCond = true; } computeInitiatives(); displayBoxWithText("", 2, 1, false, false); for (uint counter = 0; counter < 8; ++counter) { int16 monsterGroupIdOrMonsterId = _initiatives[counter]._id; if (monsterGroupIdOrMonsterId == -1) continue; if (monsterGroupIdOrMonsterId >= 1000) { // Magic number which determines if it's a Team Member monsterGroupIdOrMonsterId -= 1000; if (!isTeamMemberStatusNormal(monsterGroupIdOrMonsterId)) { handleFight_checkEndEffect(monsterGroupIdOrMonsterId); } else { switch (_teamChar[monsterGroupIdOrMonsterId]._lastAction) { case 0x41: // 'A'ttack handleFight_lastAction_A(monsterGroupIdOrMonsterId); break; case 0x44: // 'D'efend handleFight_lastAction_D(monsterGroupIdOrMonsterId); break; case 0x48: // 'H'ide handleFight_lastAction_H(monsterGroupIdOrMonsterId); break; case 0x55: // 'U'se mainLoopCond = handleFight_lastAction_U(monsterGroupIdOrMonsterId); break; default: break; } } } else if (checkMonsterMovementType(monsterGroupIdOrMonsterId, true)) { handleFight_MobstersAttack(monsterGroupIdOrMonsterId); } } handleMapMonsterMoves(); addNewOpponents(monsterId); } resetTeamMonsterIdArray(); _ongoingFightFl = false; displayAnimFrames(0xFE, true); return true; } void EfhEngine::handleFight_checkEndEffect(int16 charId) { debugC(3, kDebugFight, "handleFight_checkEndEffect %d", charId); // In the original, this function is part of handleFight. // It has been split for readability purposes. if (_teamChar[charId]._status._type == kEfhStatusNormal) return; if (--_teamChar[charId]._status._duration > 0) return; // At this point : The status is different to 0 (normal) and the effect duration is finally 0 (end of effect) _enemyNamePt2 = _npcBuf[_teamChar[charId]._id]._name; _enemyNamePt1 = getArticle(_npcBuf[_teamChar[charId]._id].getPronoun()); // End of effect message depends on the type of effect switch (_teamChar[charId]._status._type) { case kEfhStatusSleeping: _messageToBePrinted = Common::String::format("%s%s wakes up!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; case kEfhStatusFrozen: _messageToBePrinted = Common::String::format("%s%s thaws out!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; default: _messageToBePrinted = Common::String::format("%s%s recovers!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; } // The character status is back to normal _teamChar[charId]._status._type = kEfhStatusNormal; // Finally, display the message displayBoxWithText(_messageToBePrinted, 1, 2, true); } void EfhEngine::handleFight_lastAction_A(int16 teamCharId) { debugC(3, kDebugFight, "handleFight_lastAction_A %d", teamCharId); // In the original, this function is part of handleFight. // It has been split for readability purposes. int16 teamCharItemId = getEquippedExclusiveType(_teamChar[teamCharId]._id, 9, true); if (teamCharItemId == 0x7FFF) teamCharItemId = 0x3F; int16 minMonsterGroupId = _teamChar[teamCharId]._nextAttack; if (minMonsterGroupId == 0x64) minMonsterGroupId = 0; if (minMonsterGroupId == -1) return; int16 maxMonsterGroupId; if (_items[teamCharItemId]._range == 4) maxMonsterGroupId = 5; else maxMonsterGroupId = minMonsterGroupId + 1; int16 minTeamMemberId; int16 maxTeamMemberId; if (_items[teamCharItemId]._range < 3) { minTeamMemberId = getWeakestMobster(minMonsterGroupId); maxTeamMemberId = minTeamMemberId + 1; } else { minTeamMemberId = 0; maxTeamMemberId = 9; } if (minTeamMemberId == -1) return; bool var6E = true; for (int16 groupId = minMonsterGroupId; groupId < maxMonsterGroupId; ++groupId) { if (_teamMonster[groupId]._id == -1) continue; for (int16 ctrMobsterId = minTeamMemberId; ctrMobsterId < maxTeamMemberId; ++ctrMobsterId) { if (!isMonsterActive(groupId, ctrMobsterId) || !var6E) return; bool noticedFl; if (!checkMonsterMovementType(groupId, true)) { setMapMonsterAggressivenessAndMovementType(groupId, 9); _alertDelay += 500; noticedFl = true; } else noticedFl = false; int16 randomDamageAbsorbed = getRandom(_mapMonsters[_techId][_teamMonster[groupId]._id]._maxDamageAbsorption); int16 enemyPronoun = _npcBuf[_teamChar[teamCharId]._id].getPronoun(); int16 monsterId = _teamMonster[groupId]._id; int16 characterPronoun = kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._nameArticle; int16 charScore = getCharacterScore(_teamChar[teamCharId]._id, teamCharItemId); int16 hitPointsBefore = _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId]; int16 hitCount = 0; int16 originalDamage = 0; int16 damagePointsAbsorbed = 0; int16 attackSpeed = _items[teamCharItemId]._attacks * _npcBuf[_teamChar[teamCharId]._id]._speed; // Action A - Loop attackCounter - Start for (int attackCounter = 0; attackCounter < attackSpeed; ++attackCounter) { if (getRandom(100) < charScore) { ++hitCount; if (!hasAdequateDefense(_teamMonster[groupId]._id, _items[teamCharItemId]._attackType)) { int16 randomDamage = getRandom(_items[teamCharItemId]._damage); int16 residualDamage = randomDamage - randomDamageAbsorbed; if (residualDamage > 0) { originalDamage += residualDamage; damagePointsAbsorbed += randomDamageAbsorbed; } else { damagePointsAbsorbed += randomDamage; } } } } // Action A - Loop attackCounter - End if (originalDamage < 0) originalDamage = 0; int16 hitPoints = originalDamage + damagePointsAbsorbed; if (!checkSpecialItemsOnCurrentPlace(teamCharItemId)) hitCount = 0; if (hitCount > 0) { _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] -= originalDamage; if (hitCount > 1) { _attackBuffer = Common::String::format("%d times ", hitCount); } else { _attackBuffer = ""; } } int16 verbId = (3 * _items[teamCharItemId]._attackType + 1) + getRandom(3) - 1; _characterNamePt1 = getArticle(characterPronoun); _characterNamePt2 = kEncounters[_mapMonsters[_techId][_teamMonster[groupId]._id]._monsterRef]._name; _enemyNamePt1 = getArticle(enemyPronoun); _enemyNamePt2 = _npcBuf[_teamChar[teamCharId]._id]._name; _nameBuffer = _items[teamCharItemId]._name; if (checkSpecialItemsOnCurrentPlace(teamCharItemId)) { // Action A - Check damages - Start if (hitCount == 0) { _messageToBePrinted = Common::String::format("%s%s %s at %s%s with %s %s, but misses!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[verbId], _characterNamePt1.c_str(), _characterNamePt2.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } else if (hitPoints <= 0) { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s, but does no damage!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[verbId], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } else if (hitPoints == 1) { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s for 1 point", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[verbId], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] <= 0) { getDeathTypeDescription(groupId, teamCharId + 1000); getXPAndSearchCorpse(_teamChar[teamCharId]._id, _enemyNamePt1, _enemyNamePt2, _teamMonster[groupId]._id); } else { _messageToBePrinted += "!"; } } else { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s for %d points", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[verbId], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str(), hitPoints); if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] <= 0) { getDeathTypeDescription(groupId, teamCharId + 1000); getXPAndSearchCorpse(_teamChar[teamCharId]._id, _enemyNamePt1, _enemyNamePt2, _teamMonster[groupId]._id); } else { _messageToBePrinted += "!"; } } // Action A - Check damages - End // Action A - Add reaction text - Start if (hitCount != 0 && originalDamage > 0 && getRandom(100) <= 35 && _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] > 0) { if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] - 5 <= originalDamage) { addReactionText(kEfhReactionReels); } else if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] < hitPointsBefore / 8) { addReactionText(kEfhReactionCriesOut); } else if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] < hitPointsBefore / 4) { addReactionText(kEfhReactionFalters); // The original checked /2 before /3, making the code in /3 unreachable. // This check has been fixed so that it behaves as originally expected. } else if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] < hitPointsBefore / 3) { addReactionText(kEfhReactionScreams); } else if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] < hitPointsBefore / 2) { addReactionText(kEfhReactionWinces); } else if (hitPointsBefore / 8 >= originalDamage) { addReactionText(kEfhReactionChortles); } else if (getRandom(100) < 35) { // Note : The original has a bug as it was doing an (always false) check "originalDamage == 0". // This check has been removed so that it behaves as originally expected. addReactionText(kEfhReactionLaughs); } } // Action A - Add reaction text - End // Action A - Add armor absorb text - Start if (randomDamageAbsorbed && hitCount && _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] > 0) { if (damagePointsAbsorbed <= 1) _messageToBePrinted += Common::String::format(" %s%s's armor absorbs 1 point!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); else _messageToBePrinted += Common::String::format(" %s%s's armor absorbs %d points!", _characterNamePt1.c_str(), _characterNamePt2.c_str(), damagePointsAbsorbed); } // Action A - Add armor absorb text - End if (noticedFl) _messageToBePrinted += Common::String(" Your actions do not go un-noticed..."); // Action A - Check item durability - Start int16 npcId = _teamChar[teamCharId]._id; // get equipped inventory slot with exclusiveType == 9 uint16 exclusiveInventoryId = getEquippedExclusiveType(npcId, 9, false); if (exclusiveInventoryId != 0x7FFF && _npcBuf[npcId]._inventory[exclusiveInventoryId].getUsesLeft() != 0x7F) { int16 usesLeft = _npcBuf[npcId]._inventory[exclusiveInventoryId].getUsesLeft(); --usesLeft; if (usesLeft <= 0) { _messageToBePrinted += Common::String::format(" * %s%s's %s breaks!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), _nameBuffer.c_str()); setCharacterObjectToBroken(npcId, exclusiveInventoryId); var6E = false; } else { _npcBuf[npcId]._inventory[exclusiveInventoryId]._stat1 = (_npcBuf[npcId]._inventory[exclusiveInventoryId]._stat1 & 80) + usesLeft; } } // Action A - Check item durability - End // Action A - Check effect - Start if (_items[teamCharItemId]._specialEffect == 1 && _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] > 0) { if (getRandom(100) < 35) { _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._type = kEfhStatusSleeping; _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._duration = getRandom(10); _messageToBePrinted += Common::String::format(" %s%s falls asleep!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); } } else if (_items[teamCharItemId]._specialEffect == 2 && _mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] > 0) { _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._type = kEfhStatusFrozen; _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._duration = getRandom(10); _messageToBePrinted += Common::String::format(" %s%s is frozen!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); } // Action A - Check effect - End } else { _messageToBePrinted = Common::String::format("%s%s tries to use %s %s, but it doesn't work!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } genericGenerateSound(_items[teamCharItemId]._attackType, hitCount); displayBoxWithText(_messageToBePrinted, 1, 2, true); } } } void EfhEngine::handleFight_lastAction_D(int16 teamCharId) { // Fight - Action 'D' - Defend // In the original, this function is part of handleFight. // It has been split for readability purposes. debugC(3, kDebugFight, "handleFight_lastAction_D %d", teamCharId); _teamChar[teamCharId]._pctDodgeMiss -= 40; uint8 pronoun = _npcBuf[_teamChar[teamCharId]._id].getPronoun(); _enemyNamePt1 = getArticle(pronoun); _enemyNamePt2 = _npcBuf[_teamChar[teamCharId]._id]._name; _messageToBePrinted = Common::String::format("%s%s prepares to defend %sself!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kPersonal[pronoun]); displayBoxWithText(_messageToBePrinted, 1, 2, true); } void EfhEngine::handleFight_lastAction_H(int16 teamCharId) { // Fight - Action 'H' - Hide // In the original, this function is part of handleFight. // It has been split for readability purposes. debugC(3, kDebugFight, "handleFight_lastAction_H %d", teamCharId); _teamChar[teamCharId]._pctVisible -= 50; int16 pronoun = _npcBuf[_teamChar[teamCharId]._id].getPronoun(); _enemyNamePt1 = getArticle(pronoun); _enemyNamePt2 = _npcBuf[_teamChar[teamCharId]._id]._name; _messageToBePrinted = Common::String::format("%s%s attempts to hide %sself!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kPersonal[pronoun]); displayBoxWithText(_messageToBePrinted, 1, 2, true); } bool EfhEngine::handleFight_lastAction_U(int16 teamCharId) { // Fight - Action 'U' - Use Item // In the original, this function is part of handleFight. // It has been split for readability purposes. debugC(3, kDebugFight, "handleFight_lastAction_U %d", teamCharId); int16 itemId = _npcBuf[_teamChar[teamCharId]._id]._inventory[_teamChar[teamCharId]._lastInventoryUsed]._ref; _nameBuffer = _items[itemId]._name; int16 pronoun = _npcBuf[_teamChar[teamCharId]._id].getPronoun(); _enemyNamePt1 = getArticle(pronoun); _enemyNamePt2 = _npcBuf[_teamChar[teamCharId]._id]._name; _messageToBePrinted = Common::String::format("%s%s uses %s %s! ", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kPossessive[pronoun], _nameBuffer.c_str()); bool retVal = useObject(_teamChar[teamCharId]._id, _teamChar[teamCharId]._lastInventoryUsed, _teamChar[teamCharId]._nextAttack, teamCharId, 0, 3); displayBoxWithText(_messageToBePrinted, 1, 2, true); return retVal; } void EfhEngine::handleFight_MobstersAttack(int groupId) { // In the original, this function is part of handleFight. // It has been split for readability purposes. debugC(3, kDebugFight, "handleFight_MobstersAttack %d", groupId); // handleFight - Loop on mobsterId - Start for (uint ctrMobsterId = 0; ctrMobsterId < 9; ++ctrMobsterId) { if (isMonsterActive(groupId, ctrMobsterId)) { int16 monsterWeaponItemId = _mapMonsters[_techId][_teamMonster[groupId]._id]._weaponItemId; if (monsterWeaponItemId == 0xFF) monsterWeaponItemId = 0x3F; int16 minTeamMemberId = -1; int16 maxTeamMemberId; if (_items[monsterWeaponItemId]._range < 3) { for (uint attackTry = 0; attackTry < 10; ++attackTry) { minTeamMemberId = getRandom(_teamSize) - 1; if (checkWeaponRange(_teamMonster[groupId]._id, monsterWeaponItemId) && isTeamMemberStatusNormal(minTeamMemberId) && getRandom(100) < _teamChar[minTeamMemberId]._pctVisible) { break; } minTeamMemberId = -1; } maxTeamMemberId = minTeamMemberId + 1; } else { minTeamMemberId = 0; maxTeamMemberId = _teamSize; } if (minTeamMemberId <= -1) continue; // handleFight - Loop on targetId - Start for (int16 targetId = minTeamMemberId; targetId < maxTeamMemberId; ++targetId) { if (_teamChar[targetId]._id == -1 || !isTeamMemberStatusNormal(targetId)) continue; int16 randomDefense = getRandom(getEquipmentDefense(_teamChar[targetId]._id)); int16 enemyPronoun = kEncounters[_mapMonsters[_techId][_teamMonster[groupId]._id]._monsterRef]._nameArticle; int16 characterPronoun = _npcBuf[_teamChar[targetId]._id].getPronoun(); _teamChar[targetId]._pctDodgeMiss += (_items[monsterWeaponItemId]._agilityModifier * 5); int16 hitCount = 0; int16 originalDamage = 0; int16 damagePointsAbsorbed = 0; int16 var64 = _mapMonsters[_techId][_teamMonster[groupId]._id]._npcId * _items[monsterWeaponItemId]._attacks; for (int var84 = 0; var84 < var64; ++var84) { // handleFight - Loop var84 on var64 (objectId) - Start if (getRandom(100) > _teamChar[targetId]._pctDodgeMiss) continue; ++hitCount; if (hasAdequateDefenseNPC(_teamChar[targetId]._id, _items[monsterWeaponItemId]._attackType)) continue; int16 baseDamage = getRandom(_items[monsterWeaponItemId]._damage); int deltaDamage = baseDamage - randomDefense; if (deltaDamage > 0) { damagePointsAbsorbed += randomDefense; originalDamage += deltaDamage; } else { damagePointsAbsorbed += baseDamage; } // handleFight - Loop var84 on var64 (objectId) - End } if (originalDamage < 0) originalDamage = 0; int16 hitPoints = originalDamage + damagePointsAbsorbed; if (!checkSpecialItemsOnCurrentPlace(monsterWeaponItemId)) hitCount = 0; if (hitCount > 0) { _npcBuf[_teamChar[targetId]._id]._hitPoints -= originalDamage; if (hitCount > 1) _attackBuffer = Common::String::format("%d times ", hitCount); else _attackBuffer = ""; } int16 var68 = _items[monsterWeaponItemId]._attackType + 1; int16 var6A = getRandom(3); _enemyNamePt1 = getArticle(enemyPronoun); _enemyNamePt2 = kEncounters[_mapMonsters[_techId][_teamMonster[groupId]._id]._monsterRef]._name; _characterNamePt1 = getArticle(characterPronoun); _characterNamePt2 = _npcBuf[_teamChar[targetId]._id]._name; _nameBuffer = _items[monsterWeaponItemId]._name; if (checkSpecialItemsOnCurrentPlace(monsterWeaponItemId)) { // handleFight - check damages - Start if (hitCount == 0) { _messageToBePrinted = Common::String::format("%s%s %s at %s%s with %s %s, but misses!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[var68 * 3 + var6A], _characterNamePt1.c_str(), _characterNamePt2.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } else if (hitPoints <= 0) { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s, but does no damage!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[var68 * 3 + var6A], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } else if (hitPoints == 1) { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s for 1 point", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[var68 * 3 + var6A], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); if (_npcBuf[_teamChar[targetId]._id]._hitPoints <= 0) getDeathTypeDescription(targetId + 1000, groupId); else _messageToBePrinted += "!"; } else { _messageToBePrinted = Common::String::format("%s%s %s %s%s %swith %s %s for %d points", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kAttackVerbs[var68 * 3 + var6A], _characterNamePt1.c_str(), _characterNamePt2.c_str(), _attackBuffer.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str(), hitPoints); if (_npcBuf[_teamChar[targetId]._id]._hitPoints <= 0) getDeathTypeDescription(targetId + 1000, groupId); else _messageToBePrinted += "!"; } // handleFight - check damages - End // handleFight - Add reaction text - start if (hitCount != 0 && originalDamage > 0 && getRandom(100) <= 35 && _npcBuf[_teamChar[targetId]._id]._hitPoints > 0) { if (_npcBuf[_teamChar[targetId]._id]._hitPoints - 5 <= originalDamage) { addReactionText(kEfhReactionReels); } else if (_npcBuf[_teamChar[targetId]._id]._hitPoints < _npcBuf[_teamChar[targetId]._id]._maxHP / 8) { addReactionText(kEfhReactionCriesOut); } else if (_npcBuf[_teamChar[targetId]._id]._hitPoints < _npcBuf[_teamChar[targetId]._id]._maxHP / 4) { addReactionText(kEfhReactionFalters); } else if (_npcBuf[_teamChar[targetId]._id]._hitPoints < _npcBuf[_teamChar[targetId]._id]._maxHP / 2) { addReactionText(kEfhReactionWinces); } else if (_npcBuf[_teamChar[targetId]._id]._hitPoints < _npcBuf[_teamChar[targetId]._id]._maxHP / 3) { // CHECKME: Doesn't make any sense to check /3 after /2... I don't get it. Looks like an original bug addReactionText(kEfhReactionScreams); } else if (_npcBuf[_teamChar[targetId]._id]._maxHP / 8 >= originalDamage) { addReactionText(kEfhReactionChortles); } else if (getRandom(100) < 35) { // Note : The original had a bug as it was doing an (always false) check "originalDamage == 0". // This check has been removed so that it behaves as originally expected addReactionText(kEfhReactionLaughs); } } // handleFight - Add reaction text - end // handleFight - Check armor - start if (randomDefense != 0 && hitCount != 0 && _npcBuf[_teamChar[targetId]._id]._hitPoints > 0) { if (damagePointsAbsorbed <= 1) _messageToBePrinted += Common::String::format(" %s%s's armor absorbs 1 point!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); else _messageToBePrinted += Common::String::format(" %s%s's armor absorbs %d points!", _characterNamePt1.c_str(), _characterNamePt2.c_str(), damagePointsAbsorbed); int armorDamage = (originalDamage + damagePointsAbsorbed) / 10; handleDamageOnArmor(_teamChar[targetId]._id, armorDamage); } // handleFight - Check armor - end // handleFight - Check effect - start switch (_items[monsterWeaponItemId]._specialEffect) { case 1: if (getRandom(100) < 20) { _teamChar[targetId]._status._type = kEfhStatusSleeping; _teamChar[targetId]._status._duration = getRandom(10); _messageToBePrinted += Common::String::format(" %s%s falls asleep!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); } break; case 2: if (getRandom(100) < 20) { _teamChar[targetId]._status._type = kEfhStatusFrozen; _teamChar[targetId]._status._duration = getRandom(10); _messageToBePrinted += Common::String::format(" %s%s is frozen!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); } break; case 5: case 6: if (getRandom(100) < 20) { _messageToBePrinted += Common::String::format(" %s%s's life energy is gone!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); _npcBuf[_teamChar[targetId]._id]._hitPoints = 0; } break; default: break; } // handleFight - Check effect - end } else { _messageToBePrinted = Common::String::format("%s%s tries to use %s %s, but it doesn't work!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str(), kPossessive[enemyPronoun], _nameBuffer.c_str()); } genericGenerateSound(_items[monsterWeaponItemId]._attackType, hitCount); displayBoxWithText(_messageToBePrinted, 1, 2, true); } // handleFight - Loop on targetId - End } else if (_mapMonsters[_techId][_teamMonster[groupId]._id]._hitPoints[ctrMobsterId] > 0 && _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._type != kEfhStatusNormal) { --_teamMonster[groupId]._mobsterStatus[ctrMobsterId]._duration; if (_teamMonster[groupId]._mobsterStatus[ctrMobsterId]._duration <= 0) { _enemyNamePt1 = getArticle(kEncounters[_mapMonsters[_techId][_teamMonster[groupId]._id]._monsterRef]._nameArticle); _enemyNamePt2 = kEncounters[_mapMonsters[_techId][_teamMonster[groupId]._id]._monsterRef]._name; switch (_teamMonster[groupId]._mobsterStatus[ctrMobsterId]._type) { case kEfhStatusSleeping: _messageToBePrinted = Common::String::format("%s%s wakes up!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; case kEfhStatusFrozen: _messageToBePrinted = Common::String::format("%s%s thaws out!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; default: _messageToBePrinted = Common::String::format("%s%s recovers!", _enemyNamePt1.c_str(), _enemyNamePt2.c_str()); break; } _teamMonster[groupId]._mobsterStatus[ctrMobsterId]._type = kEfhStatusNormal; displayBoxWithText(_messageToBePrinted, 1, 2, true); } } } // handleFight - Loop on mobsterId - End } bool EfhEngine::isTPK() { debugC(6, kDebugFight, "isTPK"); int16 zeroedChar = 0; for (int counter = 0; counter < _teamSize; ++counter) { if (_npcBuf[_teamChar[counter]._id]._hitPoints <= 0) ++zeroedChar; } return zeroedChar == _teamSize; } bool EfhEngine::isMonsterAlreadyFighting(int16 monsterId, int16 teamMonsterId) { debugC(6, kDebugFight, "isMonsterAlreadyFighting %d %d", monsterId, teamMonsterId); for (int counter = 0; counter < teamMonsterId; ++counter) { if (_teamMonster[counter]._id == monsterId) return true; } return false; } void EfhEngine::resetTeamMonsterEffects() { debugC(6, kDebugFight, "resetTeamMonsterEffects"); for (uint ctrGroupId = 0; ctrGroupId < 5; ++ctrGroupId) { for (uint ctrMobsterId = 0; ctrMobsterId < 9; ++ctrMobsterId) { _teamMonster[ctrGroupId]._mobsterStatus[ctrMobsterId]._type = kEfhStatusNormal; _teamMonster[ctrGroupId]._mobsterStatus[ctrMobsterId]._duration = 0; } } } void EfhEngine::resetTeamMonsterIdArray() { debugC(6, kDebugFight, "resetTeamMonsterIdArray"); for (int i = 0; i < 5; ++i) { _teamMonster[i]._id = -1; } } bool EfhEngine::isTeamMemberStatusNormal(int16 teamMemberId) { debugC(6, kDebugFight, "isTeamMemberStatusNormal %d", teamMemberId); if (_npcBuf[_teamChar[teamMemberId]._id]._hitPoints > 0 && _teamChar[teamMemberId]._status._type == kEfhStatusNormal) return true; return false; } void EfhEngine::getDeathTypeDescription(int16 victimId, int16 attackerId) { debugC(3, kDebugFight, "getDeathTypeDescription %d %d", victimId, attackerId); uint8 pronoun = 0; if (victimId >= 1000) { // Magic value for team members int16 charId = _teamChar[victimId - 1000]._id; pronoun = _npcBuf[charId].getPronoun(); } else if (victimId < 5) { // Safeguard added int16 charId = _teamMonster[victimId]._id; pronoun = _mapMonsters[_techId][charId].getPronoun(); } if (pronoun > 2) pronoun = 2; int16 deathType; if (getRandom(100) < 20) { deathType = 0; } else if (attackerId >= 1000) { int16 charId = _teamChar[attackerId - 1000]._id; if (charId == -1) deathType = 0; else { int16 exclusiveItemId = getEquippedExclusiveType(charId, 9, true); if (exclusiveItemId == 0x7FFF) deathType = 0; else deathType = _items[exclusiveItemId]._attackType + 1; } // The check "attackerId >= 5" is a safeguard for a Coverity "OVERRUN" ticket, not present in the original } else if (attackerId >= 5 || _teamMonster[attackerId]._id == -1) { deathType = 0; } else { int16 itemId = _mapMonsters[_techId][_teamMonster[attackerId]._id]._weaponItemId; deathType = _items[itemId]._attackType + 1; } int16 rndDescrForDeathType = getRandom((3)) - 1; // [0..2] Common::String tmpStr = "DUDE IS TOAST!"; switch (deathType) { case 0: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", killing %s!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", slaughtering %s!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", annihilating %s!", kPersonal[pronoun]); break; default: break; } break; case 1: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", cutting %s in two!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", dicing %s into small cubes!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", butchering %s into lamb chops!", kPersonal[pronoun]); break; default: break; } break; case 2: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", piercing %s heart!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", leaving %s a spouting mass of blood!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", popping %s like a zit!", kPersonal[pronoun]); break; default: break; } break; case 3: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", pulping %s head over a wide area!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", smashing %s into a meat patty!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", squashing %s like a ripe tomato!", kPersonal[pronoun]); break; default: break; } break; case 4: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", totally incinerating %s!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", reducing %s to a pile of ash!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", leaving a blistered mass of flesh behind!"); break; default: break; } break; case 5: switch (rndDescrForDeathType) { case 0: // The original has a typo: popscicle tmpStr = Common::String::format(", turning %s into a popsicle!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", encasing %s in a block of ice!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", shattering %s into shards!", kPersonal[pronoun]); break; default: break; } break; case 6: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", leaving pudding for brains"); break; case 1: tmpStr = Common::String::format(", bursting %s head like a bubble!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", turning %s into a mindless vegetable", kPersonal[pronoun]); break; default: break; } break; case 7: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", reducing %s to an oozing pile of flesh!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", melting %s like an ice cube in hot coffee!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", vaporizing %s into a steaming cloud!", kPersonal[pronoun]); break; default: break; } break; case 8: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", engulfing %s in black smoke puffs!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", sucking %s into eternity!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", turning %s into a mindless zombie!", kPersonal[pronoun]); break; default: break; } break; case 9: case 10: case 11: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", completely disintegrating %s!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", spreading %s into a fine mist!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", leaving a smoking crater in %s place!", kPersonal[pronoun]); break; default: break; } break; case 12: case 13: case 14: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", tearing a chunk out of %s back!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", blowing %s brains out!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", exploding %s entire chest!", kPersonal[pronoun]); break; default: break; } break; case 15: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", choking %s to death!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", melting %s lungs!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", leaving %s gasping for air as %s collapses!", kPersonal[pronoun], kPersonal[pronoun]); break; default: break; } break; case 16: switch (rndDescrForDeathType) { case 0: tmpStr = Common::String::format(", tearing a chunk out of %s back!", kPersonal[pronoun]); break; case 1: tmpStr = Common::String::format(", piercing %s heart!", kPersonal[pronoun]); break; case 2: tmpStr = Common::String::format(", impaling %s brain!", kPersonal[pronoun]); break; default: break; } break; default: break; } _messageToBePrinted += tmpStr; } int16 EfhEngine::determineTeamTarget(int16 charId, int16 unkFied18Val, bool checkDistanceFl) { debugC(3, kDebugFight, "determineTeamTarget %d %d %d", charId, unkFied18Val, checkDistanceFl); int16 retVal = -1; int16 curItemId = getEquippedExclusiveType(charId, unkFied18Val, true); int16 rangeType = 0; int16 realRange = 0; if (curItemId != 0x7FFF) rangeType = _items[curItemId]._range; switch (rangeType) { case 3: case 2: ++realRange; // fall through case 1: ++realRange; // fall through case 0: ++realRange; break; case 4: return 100; default: return retVal; } do { if (_teamMonster[1]._id != -1) { _sayMenu = true; sayText("Select Monster Group:", kMenu); } for (uint counter = 0; counter < 2; ++counter) { drawCombatScreen(charId, true, false); if (_teamMonster[1]._id != -1) displayBoxWithText("Select Monster Group:", 3, 0, false, false); if (counter == 0) displayFctFullScreen(); } retVal = (_teamMonster[1]._id == -1) ? 0 : selectMonsterGroup(); if (!checkDistanceFl) { if (retVal == 27) // Esc retVal = 0; } else if (retVal != 27) { int16 monsterGroupDistance = computeMonsterGroupDistance(_teamMonster[retVal]._id); if (monsterGroupDistance > realRange) { retVal = 27; displayBoxWithText("That Group Is Out Of Range!", 3, 1, false); getLastCharAfterAnimCount(_guessAnimationAmount); } } } while (retVal == -1 && !shouldQuit()); if (retVal == 27) retVal = -1; return retVal; } bool EfhEngine::getTeamAttackRoundPlans() { debugC(3, kDebugFight, "getTeamAttackRoundPlans"); bool retVal = false; _sayLowStatusScreen = true; for (int charId = 0; charId < _teamSize; ++charId) { _sayMenu = true; _teamChar[charId]._lastAction = 0; if (!isTeamMemberStatusNormal(charId)) continue; retVal = true; do { drawCombatScreen(_teamChar[charId]._id, false, true); setKeymap(kKeymapFight); Common::CustomEventType input = handleAndMapInput(true); setKeymap(kKeymapDefault); switch (input) { case kActionAttack: // Attack sayText("Attack", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); _teamChar[charId]._lastAction = 'A'; _teamChar[charId]._nextAttack = determineTeamTarget(_teamChar[charId]._id, 9, true); if (_teamChar[charId]._nextAttack == -1) _teamChar[charId]._lastAction = 0; break; case kActionDefend: // Defend sayText("Defend", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); _teamChar[charId]._lastAction = 'D'; break; case kActionHide: // Hide sayText("Hide", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); _teamChar[charId]._lastAction = 'H'; break; case kActionRun: // Run sayText("Run", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); for (int counter2 = 0; counter2 < _teamSize; ++counter2) { _teamChar[counter2]._lastAction = 'R'; } return true; case kActionTeamStatus: { // Status sayText("Status", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); _sayMenu = true; int16 lastInvId = handleStatusMenu(2, _teamChar[charId]._id); redrawCombatScreenWithTempText(_teamChar[charId]._id); if (lastInvId >= 999) { if (lastInvId == 0x7D00) // Result of Equip, Give and Drop in combat mode(2) _teamChar[charId]._lastAction = 'S'; } else { _teamChar[charId]._lastAction = 'U'; _teamChar[charId]._lastInventoryUsed = lastInvId; int16 invEffect = _items[_npcBuf[_teamChar[charId]._id]._inventory[lastInvId]._ref]._specialEffect; switch (invEffect - 1) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 10: case 12: case 13: _teamChar[charId]._nextAttack = determineTeamTarget(_teamChar[charId]._id, 9, false); break; case 9: case 11: case 14: case 15: case 18: case 24: case 25: case 27: case 28: case 29: case 30: displayBoxWithText("Select Character:", 3, 1, false); _teamChar[charId]._nextAttack = selectOtherCharFromTeam(); break; case 16: case 17: case 26: _teamChar[charId]._nextAttack = 0xC8; break; case 19: case 20: case 21: case 22: case 23: default: _teamChar[charId]._lastInventoryUsed = lastInvId; _teamChar[charId]._nextAttack = -1; break; } } } break; case kActionTerrain: // Terrain redrawScreenForced(); sayText("Terrain", kNoRestriction, Common::TextToSpeechManager::INTERRUPT); waitForKey(); _sayMenu = true; drawCombatScreen(_teamChar[charId]._id, false, true); break; default: break; } } while (_teamChar[charId]._lastAction == 0 && !shouldQuit()); } return retVal; } void EfhEngine::drawCombatScreen(int16 charId, bool whiteFl, bool drawFl) { debugC(6, kDebugFight, "drawCombatScreen %d %s %s", charId, whiteFl ? "True" : "False", drawFl ? "True" : "False"); for (uint counter = 0; counter < 2; ++counter) { if (counter == 0 || drawFl) { drawMapWindow(); displayCenteredString("Combat", 128, 303, 9); drawColoredRect(200, 112, 278, 132, 0); displayCenteredString("'T' for Terrain", 128, 303, 117); displayBoxWithText("", 1, 0, false, false); if (!whiteFl) { sayText(_npcBuf[charId]._name, kMenu); } displayEncounterInfo(whiteFl); if (whiteFl) { _sayMenu = false; } else { sayText("'T' for Terrain", kMenu); } displayCombatMenu(charId); displayLowStatusScreen(false); _sayMenu = false; } if (counter == 0 && drawFl) displayFctFullScreen(); } } void EfhEngine::getXPAndSearchCorpse(int16 charId, Common::String namePt1, Common::String namePt2, int16 monsterId) { debugC(3, kDebugFight, "getXPAndSearchCorpse %d %s%s %d", charId, namePt1.c_str(), namePt2.c_str(), monsterId); uint16 oldXpLevel = getXPLevel(_npcBuf[charId]._xp); _npcBuf[charId]._xp += kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._xpGiven; if (getXPLevel(_npcBuf[charId]._xp) > oldXpLevel) { generateSound(15); int16 hpGain = getRandom(20) + getRandom(_npcBuf[charId]._infoScore[4]); // "Stamina" _npcBuf[charId]._hitPoints += hpGain; _npcBuf[charId]._maxHP += hpGain; // "Strength", _npcBuf[charId]._infoScore[0] += getRandom(3) - 1; // "Intelligence", _npcBuf[charId]._infoScore[1] += getRandom(3) - 1; // "Piety", _npcBuf[charId]._infoScore[2] += getRandom(3) - 1; // "Agility", _npcBuf[charId]._infoScore[3] += getRandom(3) - 1; // "Stamina", _npcBuf[charId]._infoScore[4] += getRandom(3) - 1; } _messageToBePrinted += Common::String::format(" %s%s gains %d experience", namePt1.c_str(), namePt2.c_str(), kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._xpGiven); if (!characterSearchesMonsterCorpse(charId, monsterId)) _messageToBePrinted += "!"; } bool EfhEngine::characterSearchesMonsterCorpse(int16 charId, int16 monsterId) { debugC(3, kDebugFight, "characterSearchesMonsterCorpse %d %d", charId, monsterId); int16 rndVal = getRandom(100); if (kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._dropOccurrencePct < rndVal) return false; rndVal = getRandom(5) - 1; int16 itemId = kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._dropItemId[rndVal]; if (itemId == -1 || itemId == 0) return false; if (!giveItemTo(charId, itemId, 0xFF)) return false; _messageToBePrinted += Common::String::format(" and finds a %s!", _items[itemId]._name); return true; } void EfhEngine::addReactionText(int16 id) { debugC(3, kDebugFight, "addReactionText %d", id); int16 rand3 = getRandom(3); switch (id) { case kEfhReactionReels: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s reels from the blow!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s sways from the attack!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s looks dazed!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionCriesOut: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s cries out in agony!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s screams from the abuse!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s wails terribly!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionFalters: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s is staggering!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s falters for a moment!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s is stumbling about!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionWinces: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s winces from the pain!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s cringes from the damage!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s shrinks from the wound!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionScreams: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s screams!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s bellows!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s shrills!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionChortles: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s chortles!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s seems amused!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s looks concerned!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; case kEfhReactionLaughs: switch (rand3) { case 1: _messageToBePrinted += Common::String::format(" %s%s laughs at the feeble attack!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 2: _messageToBePrinted += Common::String::format(" %s%s smiles at the pathetic attack!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; case 3: _messageToBePrinted += Common::String::format(" %s%s laughs at the ineffective assault!", _characterNamePt1.c_str(), _characterNamePt2.c_str()); break; default: break; } break; default: break; } } void EfhEngine::displayEncounterInfo(bool whiteFl) { debugC(5, kDebugFight, "displayEncounterInfo %s", whiteFl ? "True" : "False"); int16 textPosY = 20; for (uint counter = 0; counter < 5; ++counter) { if (_teamMonster[counter]._id == -1) continue; int16 monsterDistance = computeMonsterGroupDistance(_teamMonster[counter]._id); int16 mobsterCount = countMonsterGroupMembers(counter); if (whiteFl) setTextColorWhite(); else setTextColorGrey(); setTextPos(129, textPosY); Common::String buffer = Common::String::format("%c)", 'A' + counter); Common::String ttsMessage = buffer + " "; displayStringAtTextPos(buffer); setTextColorRed(); int16 var1 = _mapMonsters[_techId][_teamMonster[counter]._id]._possessivePronounSHL6 & 0x3F; if (var1 <= 0x3D) { buffer = Common::String::format("%d %s", mobsterCount, kEncounters[_mapMonsters[_techId][_teamMonster[counter]._id]._monsterRef]._name); ttsMessage += buffer; displayStringAtTextPos(buffer); if (mobsterCount > 1) { displayStringAtTextPos("s"); ttsMessage += "s"; } } else if (var1 == 0x3E) { displayStringAtTextPos("(NOT DEFINED)"); } else if (var1 == 0x3F) { Common::String stringToDisplay = _npcBuf[_mapMonsters[_techId][_teamMonster[counter]._id]._npcId]._name; displayStringAtTextPos(stringToDisplay); ttsMessage += stringToDisplay; } setTextPos(228, textPosY); if (checkMonsterMovementType(counter, true)) { _textColor = 0xE; displayStringAtTextPos("Hostile"); ttsMessage += ", Hostile, "; } else { _textColor = 0x2; displayStringAtTextPos("Friendly"); ttsMessage += ", Friendly, "; } setTextColorRed(); switch (monsterDistance) { case 1: displayCenteredString("S", 290, 302, textPosY); ttsMessage += "S"; break; case 2: displayCenteredString("M", 290, 302, textPosY); ttsMessage += "M"; break; case 3: displayCenteredString("L", 290, 302, textPosY); ttsMessage += "L"; break; default: displayCenteredString("?", 290, 302, textPosY); ttsMessage += "?"; break; } sayText(ttsMessage, kMenu); textPosY += 9; } } int16 EfhEngine::getWeakestMobster(int16 groupNumber) { debugC(3, kDebugFight, "getWeakestMobster %d", groupNumber); int16 weakestMobsterId = -1; int16 monsterId = _teamMonster[groupNumber]._id; if (monsterId == -1) return -1; for (uint counter = 0; counter < 9; ++counter) { if (isMonsterActive(groupNumber, counter)) { weakestMobsterId = counter; break; } } //Safeguard added if (weakestMobsterId < 0) return -1; for (int16 counter = weakestMobsterId + 1; counter < 9; ++counter) { if (!isMonsterActive(groupNumber, counter)) continue; if (_mapMonsters[_techId][monsterId]._hitPoints[weakestMobsterId] > _mapMonsters[_techId][monsterId]._hitPoints[counter]) weakestMobsterId = counter; } // Useless check on _hitPoints > 0 removed. It's covered by isMonsterActive() return weakestMobsterId; } int16 EfhEngine::getCharacterScore(int16 charId, int16 itemId) { debugC(3, kDebugFight, "getCharacterScore %d %d", charId, itemId); int16 totalScore = 0; switch (_items[itemId]._range) { case 0: totalScore = _npcBuf[charId]._passiveScore[5] + _npcBuf[charId]._passiveScore[3] + _npcBuf[charId]._passiveScore[4]; totalScore += _npcBuf[charId]._infoScore[0] / 5; totalScore += _npcBuf[charId]._infoScore[2] * 2, totalScore += _npcBuf[charId]._infoScore[6] / 5; totalScore += 2 * _npcBuf[charId]._infoScore[5] / 5; break; case 1: totalScore = _npcBuf[charId]._passiveScore[3] + _npcBuf[charId]._passiveScore[4]; totalScore += _npcBuf[charId]._infoScore[2] * 2; totalScore += _npcBuf[charId]._infoScore[1] / 5; totalScore += _npcBuf[charId]._infoScore[3] / 5; break; case 2: case 3: case 4: totalScore = _npcBuf[charId]._passiveScore[1]; totalScore += _npcBuf[charId]._infoScore[2] * 2; totalScore += _npcBuf[charId]._infoScore[1] / 5; totalScore += _npcBuf[charId]._infoScore[3] / 5; totalScore += _npcBuf[charId]._infoScore[8] / 5; default: break; } int16 extraScore = 0; switch (_items[itemId]._attackType) { case 0: case 1: case 2: if (itemId == 0x3F) extraScore = _npcBuf[charId]._passiveScore[2]; else if (itemId == 0x41 || itemId == 0x42 || itemId == 0x6A || itemId == 0x6C || itemId == 0x6D) extraScore = _npcBuf[charId]._passiveScore[0]; break; case 3: case 4: case 6: extraScore = _npcBuf[charId]._infoScore[7]; break; case 5: case 7: extraScore = _npcBuf[charId]._infoScore[9]; break; case 8: case 9: extraScore = _npcBuf[charId]._activeScore[12]; break; case 10: extraScore = _npcBuf[charId]._passiveScore[10]; break; case 11: extraScore = _npcBuf[charId]._passiveScore[6]; break; case 12: extraScore = _npcBuf[charId]._passiveScore[7]; break; case 13: extraScore = _npcBuf[charId]._passiveScore[8]; break; case 14: extraScore = _npcBuf[charId]._activeScore[13]; break; case 15: extraScore = _npcBuf[charId]._passiveScore[9]; break; default: break; } extraScore += _items[itemId]._agilityModifier; int16 grandTotalScore = CLIP(totalScore + extraScore + 30, 5, 90); return grandTotalScore; } bool EfhEngine::checkSpecialItemsOnCurrentPlace(int16 itemId) { debugC(3, kDebugFight, "checkSpecialItemsOnCurrentPlace %d", itemId); bool retVal = true; switch (_techDataArr[_techId][_techDataId_MapPosX * 64 + _techDataId_MapPosY]) { case 1: if ((itemId >= 0x58 && itemId <= 0x68) || (itemId >= 0x86 && itemId <= 0x89) || (itemId >= 0x74 && itemId <= 0x76) || itemId == 0x8C) retVal = false; break; case 2: if ((itemId >= 0x61 && itemId <= 0x63) || (itemId >= 0x74 && itemId <= 0x76) || (itemId >= 0x86 && itemId <= 0x89) || itemId == 0x5B || itemId == 0x5E || itemId == 0x66 || itemId == 0x68 || itemId == 0x8C) retVal = false; break; default: break; } return retVal; } bool EfhEngine::hasAdequateDefense(int16 monsterId, uint8 attackType) { debugC(3, kDebugFight, "hasAdequateDefense %d %d", monsterId, attackType); int16 itemId = _mapMonsters[_techId][monsterId]._weaponItemId; if (_items[itemId]._specialEffect != 0) return false; return _items[itemId]._defenseType == attackType; } bool EfhEngine::hasAdequateDefenseNPC(int16 charId, uint8 attackType) { debugC(3, kDebugFight, "hasAdequateDefenseNPC %d %d", charId, attackType); int16 itemId = _npcBuf[charId]._defaultDefenseItemId; if (_items[itemId]._specialEffect == 0 && _items[itemId]._defenseType == attackType) return true; for (uint counter = 0; counter < 10; ++counter) { if (_npcBuf[charId]._inventory[counter]._ref == 0x7FFF || !_npcBuf[charId]._inventory[counter].isEquipped()) continue; itemId = _npcBuf[charId]._inventory[counter]._ref; if (_items[itemId]._specialEffect == 0 && _items[itemId]._defenseType == attackType) return true; } return false; } // The parameter isn't used in the original void EfhEngine::addNewOpponents(int16 monsterId) { debugC(3, kDebugFight, "addNewOpponents %d", monsterId); // addNewOpponents - 1rst loop counter1_monsterId - Start for (uint ctrGroupId = 0; ctrGroupId < 5; ++ctrGroupId) { if (countMonsterGroupMembers(ctrGroupId)) continue; for (uint ctrMobster = 0; ctrMobster < 9; ++ctrMobster) { _mapMonsters[_techId][_teamMonster[ctrGroupId]._id]._hitPoints[ctrMobster] = 0; _teamMonster[ctrGroupId]._mobsterStatus[ctrMobster]._type = kEfhStatusNormal; _teamMonster[ctrGroupId]._mobsterStatus[ctrMobster]._duration = 0; } _teamMonster[ctrGroupId]._id = -1; // CHECKME: ctrGroupId is not incrementing, which is very, very suspicious as we are copying over and over to the same destination // if the purpose is compact the array, it should be handle differently for (uint counter2 = ctrGroupId + 1; counter2 < 5; ++counter2) { for (uint ctrMobsterId = 0; ctrMobsterId < 9; ++ctrMobsterId) { _teamMonster[ctrGroupId]._mobsterStatus[ctrMobsterId]._type = _teamMonster[counter2]._mobsterStatus[ctrMobsterId]._type; _teamMonster[ctrGroupId]._mobsterStatus[ctrMobsterId]._duration = _teamMonster[counter2]._mobsterStatus[ctrMobsterId]._duration; } _teamMonster[ctrGroupId]._id = _teamMonster[counter2]._id; } } // addNewOpponents - 1rst loop counter1_monsterId - End int16 teamMonsterId = -1; for (uint counter1 = 0; counter1 < 5; ++counter1) { if (_teamMonster[counter1]._id == -1) { teamMonsterId = counter1; break; } } if (teamMonsterId != -1) { // addNewOpponents - loop distCtr - Start for (int distCtr = 1; distCtr < 3; ++distCtr) { if (teamMonsterId >= 5) break; for (uint ctrMapMonsterId = 0; ctrMapMonsterId < 64; ++ctrMapMonsterId) { if (_mapMonsters[_techId][ctrMapMonsterId]._fullPlaceId == 0xFF) continue; if (((_mapMonsters[_techId][ctrMapMonsterId]._possessivePronounSHL6 & 0x3F) == 0x3F && !isNpcATeamMember(_mapMonsters[_techId][ctrMapMonsterId]._npcId)) || (_mapMonsters[_techId][ctrMapMonsterId]._possessivePronounSHL6 & 0x3F) <= 0x3D) { if (checkIfMonsterOnSameLargeMapPlace(ctrMapMonsterId)) { bool monsterActiveFound = false; for (uint ctrMobsterId = 0; ctrMobsterId < 9; ++ctrMobsterId) { if (_mapMonsters[_techId][ctrMapMonsterId]._hitPoints[ctrMobsterId] > 0) { monsterActiveFound = true; break; } } if (!monsterActiveFound) continue; if (computeMonsterGroupDistance(ctrMapMonsterId) > distCtr) continue; if (isMonsterAlreadyFighting(ctrMapMonsterId, teamMonsterId)) continue; _teamMonster[teamMonsterId]._id = ctrMapMonsterId; // The original at this point was doing a loop on counter1, which is not a good idea as // it was resetting the counter1 to 9 whatever its value before the loop. // I therefore decided to use another counter as it looks like an original misbehavior/bug. for (uint ctrMobsterId = 0; ctrMobsterId < 9; ++ctrMobsterId) { _teamMonster[teamMonsterId]._mobsterStatus[ctrMobsterId]._type = kEfhStatusNormal; } if (++teamMonsterId >= 5) break; } } } } // addNewOpponents - loop distCtr - End } if (teamMonsterId == -1 || teamMonsterId > 4) return; // Reset the unused groups for (int16 ctrTeamMonsterId = teamMonsterId; ctrTeamMonsterId < 5; ++ctrTeamMonsterId) _teamMonster[ctrTeamMonsterId].init(); } int16 EfhEngine::getTeamMonsterAnimId() { debugC(6, kDebugFight, "getTeamMonsterAnimId"); int16 retVal = 0xFF; for (uint counter = 0; counter < 5; ++counter) { int16 monsterId = _teamMonster[counter]._id; if (monsterId == -1) continue; if (!checkMonsterMovementType(monsterId, false)) continue; retVal = kEncounters[_mapMonsters[_techId][monsterId]._monsterRef]._animId; break; } if (retVal == 0xFF) retVal = kEncounters[_mapMonsters[_techId][_teamMonster[0]._id]._monsterRef]._animId; return retVal; } int16 EfhEngine::selectMonsterGroup() { debugC(3, kDebugFight, "selectMonsterGroup"); int16 retVal = -1; setKeymap(kKeymapEnemySelection); while (retVal == -1 && !shouldQuit()) { Common::CustomEventType input = handleAndMapInput(true); if (input == kActionCancelEnemySelection) { retVal = 27; } else { for (int i = 0; i < ARRAYSIZE(_efhEnemySelectionCodes); ++i) { if (input == _efhEnemySelectionCodes[i]._action) { retVal = _efhEnemySelectionCodes[i]._id; if (_teamMonster[retVal]._id == -1) retVal = -1; break; } } } } _sayMenu = true; stopTextToSpeech(); if (retVal != -1 && retVal != 27) { sayText(Common::String::format("%c", retVal + Common::KEYCODE_a), kNoRestriction); } setKeymap(kKeymapDefault); return retVal; } void EfhEngine::redrawCombatScreenWithTempText(int16 charId) { debugC(3, kDebugFight, "redrawCombatScreenWithTempText %d", charId); for (uint counter = 0; counter < 2; ++counter) { drawGameScreenAndTempText(false); displayLowStatusScreen(false); drawCombatScreen(charId, false, false); if (counter == 0) displayFctFullScreen(); } } void EfhEngine::handleDamageOnArmor(int16 charId, int16 damage) { debugC(3, kDebugFight, "handleDamageOnArmor %d %d", charId, damage); int16 destroyCounter = 0; int16 pronoun = _npcBuf[charId].getPronoun(); if (pronoun > 2) { pronoun = 2; } int16 curDamage = CLIP(damage, 0, 50); for (uint objectId = 0; objectId < 10; ++objectId) { if (_npcBuf[charId]._inventory[objectId]._ref == 0x7FFF || !_npcBuf[charId]._inventory[objectId].isEquipped() || _items[_npcBuf[charId]._inventory[objectId]._ref]._defense == 0) continue; int16 remainingDamage = curDamage - _npcBuf[charId]._inventory[objectId]._curHitPoints; // not in the original: this int16 is used to test if the result is negative. Otherwise _curHitPoints (uint8) turns it into a "large" positive value. int16 newDurability = _npcBuf[charId]._inventory[objectId]._curHitPoints - curDamage; _npcBuf[charId]._inventory[objectId]._curHitPoints = newDurability; if (newDurability <= 0) { Common::String buffer2 = _items[_npcBuf[charId]._inventory[objectId]._ref]._name; removeObject(charId, objectId); if (destroyCounter == 0) { _messageToBePrinted += Common::String::format(", but %s ", kPossessive[pronoun]) + buffer2; } else { _messageToBePrinted += Common::String(", ") + buffer2; } ++destroyCounter; } if (remainingDamage > 0) curDamage = remainingDamage; // The original doesn't contain this Else clause. But logically, if the remainingDamage is less than 0, it doesn't make sense to keep damaging equipment with the previous damage. // As it looks like an original bug, I just added the code to stop damaging equipped protections. else break; } if (destroyCounter == 0) { _messageToBePrinted += "!"; } else if (destroyCounter > 1 || _messageToBePrinted.lastChar() == 's' || _messageToBePrinted.lastChar() == 'S') { _messageToBePrinted += " are destroyed!"; } else { _messageToBePrinted += " is destroyed!"; } } } // End of namespace Efh