/* 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 "ultima/ultima4/controllers/combat_controller.h" #include "ultima/ultima4/controllers/read_choice_controller.h" #include "ultima/ultima4/controllers/read_dir_controller.h" #include "ultima/ultima4/controllers/ztats_controller.h" #include "ultima/ultima4/map/annotation.h" #include "ultima/ultima4/map/dungeon.h" #include "ultima/ultima4/map/location.h" #include "ultima/ultima4/map/mapmgr.h" #include "ultima/ultima4/map/movement.h" #include "ultima/ultima4/map/tileset.h" #include "ultima/ultima4/core/debugger.h" #include "ultima/ultima4/core/settings.h" #include "ultima/ultima4/core/utils.h" #include "ultima/ultima4/events/event_handler.h" #include "ultima/ultima4/game/context.h" #include "ultima/ultima4/game/creature.h" #include "ultima/ultima4/game/death.h" #include "ultima/ultima4/game/game.h" #include "ultima/ultima4/game/item.h" #include "ultima/ultima4/game/names.h" #include "ultima/ultima4/game/object.h" #include "ultima/ultima4/game/player.h" #include "ultima/ultima4/game/portal.h" #include "ultima/ultima4/game/spell.h" #include "ultima/ultima4/views/stats.h" #include "ultima/ultima4/game/weapon.h" #include "ultima/ultima4/gfx/screen.h" #include "ultima/shared/std/containers.h" #include "ultima/ultima4/ultima4.h" #include "common/system.h" namespace Ultima { namespace Ultima4 { struct PointerHash { Common::Hash hash; uint operator()(const void *ptr) const { Common::String str = Common::String::format("%p", ptr); return hash.operator()(str.c_str()); } }; CombatController *g_combat; /** * Returns true if 'map' points to a Combat Map */ bool isCombatMap(Map *punknown) { CombatMap *ps; if ((ps = dynamic_cast(punknown)) != nullptr) return true; else return false; } /** * Returns a CombatMap pointer to the map * passed, or a CombatMap pointer to the current map * if no arguments were passed. * * Returns nullptr if the map provided (or current map) * is not a combat map. */ CombatMap *getCombatMap(Map *punknown) { Map *m = punknown ? punknown : g_context->_location->_map; if (!isCombatMap(m)) return nullptr; else return dynamic_cast(m); } /** * CombatController class implementation */ CombatController::CombatController() : _map(nullptr) { init(); g_context->_party->addObserver(this); } CombatController::CombatController(CombatMap *m) : _map(m) { init(); g_game->setMap(_map, true, nullptr, this); g_context->_party->addObserver(this); } CombatController::CombatController(MapId id) : _map(nullptr) { init(); _map = getCombatMap(mapMgr->get(id)); g_game->setMap(_map, true, nullptr, this); g_context->_party->addObserver(this); _forceStandardEncounterSize = false; } CombatController::~CombatController() { g_context->_party->deleteObserver(this); g_combat = nullptr; } void CombatController::init() { g_combat = this; _focus = 0; Common::fill(&_creatureTable[0], &_creatureTable[AREA_CREATURES], (const Creature *)nullptr); _creature = nullptr; _camping = false; _forceStandardEncounterSize = false; _placePartyOnMap = false; _placeCreaturesOnMap = false; _winOrLose = false; _showMessage = false; _exitDir = DIR_NONE; } void CombatController::setActive() { MetaEngine::setKeybindingMode(KBMODE_COMBAT); g_context->_horseSpeed = 0; } // Accessor Methods bool CombatController::isCamping() const { return _camping; } bool CombatController::isWinOrLose() const { return _winOrLose; } Direction CombatController::getExitDir() const { return _exitDir; } byte CombatController::getFocus() const { return _focus; } CombatMap *CombatController::getMap() const { return _map; } Creature *CombatController::getCreature() const { return _creature; } PartyMemberVector *CombatController::getParty() { return &_party; } PartyMember *CombatController::getCurrentPlayer() { return _party[_focus]; } void CombatController::setExitDir(Direction d) { _exitDir = d; } void CombatController::setCreature(Creature *m) { _creature = m; } void CombatController::setWinOrLose(bool worl) { _winOrLose = worl; } void CombatController::showCombatMessage(bool show) { _showMessage = show; } void CombatController::init(class Creature *m) { int i; _creature = m; _placeCreaturesOnMap = (m == nullptr) ? false : true; _placePartyOnMap = true; _winOrLose = true; _map->setDungeonRoom(false); _map->setAltarRoom(VIRT_NONE); _showMessage = true; _camping = false; /* initialize creature info */ for (i = 0; i < AREA_CREATURES; i++) { _creatureTable[i] = nullptr; } for (i = 0; i < AREA_PLAYERS; i++) { _party.push_back(nullptr); } /* fill the creature table if a creature was provided to create */ fillCreatureTable(m); /* initialize focus */ _focus = 0; } void CombatController::initDungeonRoom(int room, Direction from) { int i; init(nullptr); assertMsg(g_context->_location->_prev->_context & CTX_DUNGEON, "Error: called initDungeonRoom from non-dungeon context"); { Dungeon *dng = dynamic_cast(g_context->_location->_prev->_map); assert(dng); DngRoom &dngRoom = dng->_rooms[room]; /* load the dungeon room properties */ _winOrLose = false; _map->setDungeonRoom(true); _exitDir = DIR_NONE; /* FIXME: this probably isn't right way to see if you're entering an altar room... but maybe it is */ if ((g_context->_location->_prev->_map->_id != MAP_ABYSS) && (room == 0xF)) { /* figure out which dungeon room they're entering */ if (g_context->_location->_prev->_coords.x == 3) _map->setAltarRoom(VIRT_LOVE); else if (g_context->_location->_prev->_coords.x <= 2) _map->setAltarRoom(VIRT_TRUTH); else _map->setAltarRoom(VIRT_COURAGE); } /* load in creatures and creature start coordinates */ for (i = 0; i < AREA_CREATURES; i++) { if (dng->_rooms[room]._creatureTiles[i] > 0) { _placeCreaturesOnMap = true; _creatureTable[i] = creatureMgr->getByTile(dng->_rooms[room]._creatureTiles[i]); } _map->creature_start[i].x = dng->_rooms[room]._creatureStart[i].x; _map->creature_start[i].y = dng->_rooms[room]._creatureStart[i].y; } // Validate direction switch (from) { case DIR_WEST: case DIR_NORTH: case DIR_EAST: case DIR_SOUTH: break; case DIR_ADVANCE: case DIR_RETREAT: default: error("Invalid 'from' direction passed to initDungeonRoom()"); } for (i = 0; i < AREA_PLAYERS; i++) { _map->player_start[i].x = dngRoom._partyStart[i][from].x; _map->player_start[i].y = dngRoom._partyStart[i][from].y; } } } void CombatController::applyCreatureTileEffects() { CreatureVector creatures = _map->getCreatures(); for (auto *m : creatures) { TileEffect effect = _map->tileTypeAt(m->getCoords(), WITH_GROUND_OBJECTS)->getEffect(); m->applyTileEffect(effect); } } void CombatController::begin() { bool partyIsReadyToFight = false; /* place party members on the map */ if (_placePartyOnMap) placePartyMembers(); /* place creatures on the map */ if (_placeCreaturesOnMap) placeCreatures(); /* if we entered an altar room, show the name */ if (_map->isAltarRoom()) { g_screen->screenMessage("\nThe Altar Room of %s\n", getBaseVirtueName(_map->getAltarRoom())); g_context->_location->_context = static_cast(g_context->_location->_context | CTX_ALTAR_ROOM); } /* if there are creatures around, start combat! */ if (_showMessage && _placeCreaturesOnMap && _winOrLose) g_screen->screenMessage("\n%c****%c COMBAT %c****%c\n", FG_GREY, FG_WHITE, FG_GREY, FG_WHITE); /* FIXME: there should be a better way to accomplish this */ if (!_camping) { g_music->playMapMusic(); } /* Set focus to the first active party member, if there is one */ for (int i = 0; i < AREA_PLAYERS; i++) { if (setActivePlayer(i)) { partyIsReadyToFight = true; break; } } if (!_camping && !partyIsReadyToFight) g_context->_location->_turnCompleter->finishTurn(); eventHandler->pushController(this); } void CombatController::end(bool adjustKarma) { eventHandler->popController(); /* The party is dead -- start the death sequence */ if (g_context->_party->isDead()) { /* remove the creature */ if (_creature) g_context->_location->_map->removeObject(_creature); g_death->start(5); } else { /* need to get this here because when we exit to the parent map, all the monsters are cleared */ bool won = isWon(); g_game->exitToParentMap(); g_music->playMapMusic(); if (_winOrLose) { if (won) { if (_creature) { if (_creature->isEvil()) g_context->_party->adjustKarma(KA_KILLED_EVIL); awardLoot(); } g_screen->screenMessage("\nVictory!\n\n"); } else if (!g_context->_party->isDead()) { /* minus points for fleeing from evil creatures */ if (adjustKarma && _creature && _creature->isEvil()) { g_screen->screenMessage("\nBattle is lost!\n\n"); g_context->_party->adjustKarma(KA_FLED_EVIL); } else if (adjustKarma && _creature && _creature->isGood()) g_context->_party->adjustKarma(KA_FLED_GOOD); } } /* exiting a dungeon room */ if (_map->isDungeonRoom()) { g_screen->screenMessage("Leave Room!\n"); if (_map->isAltarRoom()) { PortalTriggerAction action = ACTION_NONE; /* when exiting altar rooms, you exit to other dungeons. Here it goes... */ switch (_exitDir) { case DIR_NORTH: action = ACTION_EXIT_NORTH; break; case DIR_EAST: action = ACTION_EXIT_EAST; break; case DIR_SOUTH: action = ACTION_EXIT_SOUTH; break; case DIR_WEST: action = ACTION_EXIT_WEST; break; case DIR_NONE: break; case DIR_ADVANCE: case DIR_RETREAT: default: error("Invalid exit dir %d", _exitDir); break; } if (action != ACTION_NONE) usePortalAt(g_context->_location, g_context->_location->_coords, action); } else { g_screen->screenMessage("\n"); } if (_exitDir != DIR_NONE) { g_ultima->_saveGame->_orientation = _exitDir; /* face the direction exiting the room */ // XXX: why north, shouldn't this be orientation? g_context->_location->move(DIR_NORTH, false); /* advance 1 space outside of the room */ } } /* remove the creature */ if (_creature) g_context->_location->_map->removeObject(_creature); /* Make sure finishturn only happens if a new combat has not begun */ if (!eventHandler->getController()->isCombatController()) g_context->_location->_turnCompleter->finishTurnAfterCombatEnds(); } delete this; } void CombatController::fillCreatureTable(const Creature *creature) { int i, j; if (creature != nullptr) { const Creature *baseCreature = creature, *current; int numCreatures = initialNumberOfCreatures(creature); if (baseCreature->getId() == PIRATE_ID) baseCreature = creatureMgr->getById(ROGUE_ID); for (i = 0; i < numCreatures; i++) { current = baseCreature; /* find a free spot in the creature table */ do { j = xu4_random(AREA_CREATURES) ; } while (_creatureTable[j] != nullptr); /* see if creature is a leader or leader's leader */ if (creatureMgr->getById(baseCreature->getLeader()) != baseCreature && /* leader is a different creature */ i != (numCreatures - 1)) { /* must have at least 1 creature of type encountered */ if (xu4_random(32) == 0) /* leader's leader */ current = creatureMgr->getById(creatureMgr->getById(baseCreature->getLeader())->getLeader()); else if (xu4_random(8) == 0) /* leader */ current = creatureMgr->getById(baseCreature->getLeader()); } /* place this creature in the creature table */ _creatureTable[j] = current; } } } int CombatController::initialNumberOfCreatures(const Creature *creature) const { int ncreatures; Map *map = g_context->_location->_prev ? g_context->_location->_prev->_map : g_context->_location->_map; /* if in an unusual combat situation, generally we stick to normal encounter sizes, (such as encounters from sleeping in an inn, etc.) */ if (_forceStandardEncounterSize || map->isWorldMap() || (g_context->_location->_prev && g_context->_location->_prev->_context & CTX_DUNGEON)) { ncreatures = xu4_random(8) + 1; if (ncreatures == 1) { if (creature && creature->getEncounterSize() > 0) ncreatures = xu4_random(creature->getEncounterSize()) + creature->getEncounterSize() + 1; else ncreatures = 8; } while (ncreatures > 2 * g_ultima->_saveGame->_members) { ncreatures = xu4_random(16) + 1; } } else { if (creature && creature->getId() == GUARD_ID) ncreatures = g_ultima->_saveGame->_members * 2; else ncreatures = 1; } return ncreatures; } bool CombatController::isWon() const { CreatureVector creatures = _map->getCreatures(); if (creatures.size()) return false; return true; } bool CombatController::isLost() const { PartyMemberVector party = _map->getPartyMembers(); if (party.size()) return false; return true; } void CombatController::moveCreatures() { Creature *m; CreatureVector creatures = _map->getCreatures(); // IMPORTANT: We need to keep regenerating the creatures list, // because monsters may be removed if a jinxed monster kills another for (int i = 0; i < (int)creatures.size(); ++i) { m = creatures[i]; m->act(this); creatures = _map->getCreatures(); if (i < (int)creatures.size() && creatures[i] != m) { // Don't skip a later creature when an earlier one flees --i; } } } void CombatController::placeCreatures() { int i; for (i = 0; i < AREA_CREATURES; i++) { const Creature *m = _creatureTable[i]; if (m) _map->addCreature(m, _map->creature_start[i]); } } void CombatController::placePartyMembers() { int i; // The following line caused a crash upon entering combat (MSVC8 binary) // party.clear(); for (i = 0; i < g_context->_party->size(); i++) { PartyMember *p = g_context->_party->member(i); p->setFocus(false); // take the focus off of everyone /* don't place dead party members */ if (p->getStatus() != STAT_DEAD) { /* add the party member to the map */ p->setCoords(_map->player_start[i]); p->setMap(_map); _map->_objects.push_back(p); _party[i] = p; } } } bool CombatController::setActivePlayer(int player) { PartyMember *p = _party[player]; if (p && !p->isDisabled()) { if (_party[_focus]) _party[_focus]->setFocus(false); p->setFocus(); _focus = player; g_screen->screenMessage("\n%s with %s\n\020", p->getName().c_str(), p->getWeapon()->getName().c_str()); g_context->_stats->highlightPlayer(_focus); return true; } return false; } void CombatController::awardLoot() { Coords coords = _creature->getCoords(); const Tile *ground = g_context->_location->_map->tileTypeAt(coords, WITHOUT_OBJECTS); /* add a chest, if the creature leaves one */ if (_creature->leavesChest() && ground->isCreatureWalkable() && (!(g_context->_location->_context & CTX_DUNGEON) || ground->isDungeonFloor())) { MapTile chest = g_context->_location->_map->_tileSet->getByName("chest")->getId(); g_context->_location->_map->addObject(chest, chest, coords); } /* add a ship if you just defeated a pirate ship */ else if (_creature->getTile().getTileType()->isPirateShip()) { MapTile ship = g_context->_location->_map->_tileSet->getByName("ship")->getId(); ship.setDirection(_creature->getTile().getDirection()); g_context->_location->_map->addObject(ship, ship, coords); } } bool CombatController::attackHit(Creature *attacker, Creature *defender) { assertMsg(attacker != nullptr, "attacker must not be nullptr"); assertMsg(defender != nullptr, "defender must not be nullptr"); int attackValue = xu4_random(0x100) + attacker->getAttackBonus(); int defenseValue = defender->getDefense(); return attackValue > defenseValue; } bool CombatController::attackAt(const Coords &coords, PartyMember *attacker, int dir, int range, int distance) { const Weapon *weapon = attacker->getWeapon(); bool wrongRange = weapon->rangeAbsolute() && (distance != range); MapTile hittile = _map->_tileSet->getByName(weapon->getHitTile())->getId(); MapTile misstile = _map->_tileSet->getByName(weapon->getMissTile())->getId(); // Check to see if something hit Creature *creature = _map->creatureAt(coords); /* If we haven't hit a creature, or the weapon's range is absolute and we're testing the wrong range, stop now! */ if (!creature || wrongRange) { /* If the weapon is shown as it travels, show it now */ if (weapon->showTravel()) { GameController::flashTile(coords, misstile, 1); } // no target found return false; } /* Did the weapon miss? */ if ((g_context->_location->_prev->_map->_id == MAP_ABYSS && !weapon->isMagic()) || /* non-magical weapon in the Abyss */ !attackHit(attacker, creature)) { /* player naturally missed */ g_screen->screenMessage("Missed!\n"); /* show the 'miss' tile */ GameController::flashTile(coords, misstile, 1); } else { /* The weapon hit! */ /* show the 'hit' tile */ GameController::flashTile(coords, misstile, 1); soundPlay(SOUND_NPC_STRUCK, false, -1); // NPC_STRUCK, melee hit GameController::flashTile(coords, hittile, 3); /* apply the damage to the creature */ if (!attacker->dealDamage(creature, attacker->getDamage())) { creature = nullptr; GameController::flashTile(coords, hittile, 1); } } return true; } bool CombatController::rangedAttack(const Coords &coords, Creature *attacker) { MapTile hittile = _map->_tileSet->getByName(attacker->getHitTile())->getId(); MapTile misstile = _map->_tileSet->getByName(attacker->getMissTile())->getId(); Creature *target = isCreature(attacker) ? _map->partyMemberAt(coords) : _map->creatureAt(coords); /* If we haven't hit something valid, stop now */ if (!target) { GameController::flashTile(coords, misstile, 1); return false; } /* Get the effects of the tile the creature is using */ TileEffect effect = hittile.getTileType()->getEffect(); /* Monster's ranged attacks never miss */ GameController::flashTile(coords, misstile, 1); /* show the 'hit' tile */ GameController::flashTile(coords, hittile, 3); /* These effects happen whether or not the opponent was hit */ switch (effect) { case EFFECT_ELECTRICITY: /* FIXME: are there any special effects here? */ soundPlay(SOUND_PC_STRUCK, false); g_screen->screenMessage("\n%s %cElectrified%c!\n", target->getName().c_str(), FG_BLUE, FG_WHITE); attacker->dealDamage(target, attacker->getDamage()); break; case EFFECT_POISON: case EFFECT_POISONFIELD: /* see if the player is poisoned */ if ((xu4_random(2) == 0) && (target->getStatus() != STAT_POISONED)) { // POISON_EFFECT, ranged hit soundPlay(SOUND_POISON_EFFECT, false); g_screen->screenMessage("\n%s %cPoisoned%c!\n", target->getName().c_str(), FG_GREEN, FG_WHITE); target->addStatus(STAT_POISONED); } // else g_screen->screenMessage("Failed.\n"); break; case EFFECT_SLEEP: /* see if the player is put to sleep */ if (xu4_random(2) == 0) { // SLEEP, ranged hit, plays even if sleep failed or PC already asleep soundPlay(SOUND_SLEEP, false); g_screen->screenMessage("\n%s %cSlept%c!\n", target->getName().c_str(), FG_PURPLE, FG_WHITE); target->putToSleep(); } // else g_screen->screenMessage("Failed.\n"); break; case EFFECT_LAVA: case EFFECT_FIRE: /* FIXME: are there any special effects here? */ soundPlay(SOUND_PC_STRUCK, false); g_screen->screenMessage("\n%s %c%s Hit%c!\n", target->getName().c_str(), FG_RED, effect == EFFECT_LAVA ? "Lava" : "Fiery", FG_WHITE); attacker->dealDamage(target, attacker->getDamage()); break; default: /* show the appropriate 'hit' message */ // soundPlay(SOUND_PC_STRUCK, false); if (hittile == g_tileSets->findTileByName("magic_flash")->getId()) g_screen->screenMessage("\n%s %cMagical Hit%c!\n", target->getName().c_str(), FG_BLUE, FG_WHITE); else g_screen->screenMessage("\n%s Hit!\n", target->getName().c_str()); attacker->dealDamage(target, attacker->getDamage()); break; } GameController::flashTile(coords, hittile, 1); return true; } void CombatController::rangedMiss(const Coords &coords, Creature *attacker) { /* If the creature leaves a tile behind, do it here! (lava lizard, etc) */ const Tile *ground = _map->tileTypeAt(coords, WITH_GROUND_OBJECTS); if (attacker->leavesTile() && ground->isWalkable()) _map->_annotations->add(coords, _map->_tileSet->getByName(attacker->getHitTile())->getId()); } bool CombatController::returnWeaponToOwner(const Coords &coords, int distance, int dir, const Weapon *weapon) { MapCoords new_coords = coords; MapTile misstile = _map->_tileSet->getByName(weapon->getMissTile())->getId(); /* reverse the direction of the weapon */ Direction returnDir = dirReverse(dirFromMask(dir)); for (int i = distance; i > 1; i--) { new_coords.move(returnDir, _map); GameController::flashTile(new_coords, misstile, 1); } gameUpdateScreen(); return true; } void CombatController::finishTurn() { PartyMember *player = getCurrentPlayer(); int quick; /* return to party overview */ g_context->_stats->setView(STATS_PARTY_OVERVIEW); if (isWon() && _winOrLose) { end(true); return; } /* make sure the player with the focus is still in battle (hasn't fled or died) */ if (player) { /* apply effects from tile player is standing on */ player->applyEffect(g_context->_location->_map->tileTypeAt(player->getCoords(), WITH_GROUND_OBJECTS)->getEffect()); } quick = (*g_context->_aura == Aura::QUICKNESS) && player && (xu4_random(2) == 0) ? 1 : 0; /* check to see if the player gets to go again (and is still alive) */ if (!quick || player->isDisabled()) { do { g_context->_location->_map->_annotations->passTurn(); /* put a sleeping person in place of the player, or restore an awakened member to their original state */ if (player) { if (player->getStatus() == STAT_SLEEPING && (xu4_random(8) == 0)) player->wakeUp(); /* remove focus from the current party member */ player->setFocus(false); /* eat some food */ g_context->_party->adjustFood(-1); } /* put the focus on the next party member */ _focus++; /* move creatures and wrap around at end */ if (_focus >= g_context->_party->size()) { /* reset the focus to the avatar and start the party's turn over again */ _focus = 0; gameUpdateScreen(); EventHandler::sleep(50); /* give a slight pause in case party members are asleep for awhile */ /* adjust moves */ g_context->_party->endTurn(); /* count down our aura (if we have one) */ g_context->_aura->passTurn(); /** * ==================== * HANDLE CREATURE STUFF * ==================== */ /* first, move all the creatures */ moveCreatures(); /* then, apply tile effects to creatures */ applyCreatureTileEffects(); /* check to see if combat is over */ if (isLost()) { end(true); return; } /* end combat immediately if the enemy has fled */ else if (isWon() && _winOrLose) { end(true); return; } } /* get the next party member */ player = getCurrentPlayer(); } while (!player || player->isDisabled() || /* dead or sleeping */ ((g_context->_party->getActivePlayer() >= 0) && /* active player is set */ (_party[g_context->_party->getActivePlayer()]) && /* and the active player is still in combat */ !_party[g_context->_party->getActivePlayer()]->isDisabled() && /* and the active player is not disabled */ (g_context->_party->getActivePlayer() != _focus))); } else { g_context->_location->_map->_annotations->passTurn(); } #if 0 if (focus != 0) { getCurrentPlayer()->act(); finishTurn(); } else setActivePlayer(focus); #else /* display info about the current player */ setActivePlayer(_focus); #endif } void CombatController::movePartyMember(MoveEvent &event) { /* active player left/fled combat */ if ((event._result & MOVE_EXIT_TO_PARENT) && (g_context->_party->getActivePlayer() == _focus)) { g_context->_party->setActivePlayer(-1); /* assign active player to next available party member */ for (int i = 0; i < g_context->_party->size(); i++) { if (_party[i] && !_party[i]->isDisabled()) { g_context->_party->setActivePlayer(i); break; } } } g_screen->screenMessage("%s\n", getDirectionName(event._dir)); if (event._result & MOVE_MUST_USE_SAME_EXIT) { soundPlay(SOUND_ERROR); // ERROR move, all PCs must use the same exit g_screen->screenMessage("All must use same exit!\n"); } else if (event._result & MOVE_BLOCKED) { soundPlay(SOUND_BLOCKED); // BLOCKED move g_screen->screenMessage("%cBlocked!%c\n", FG_GREY, FG_WHITE); } else if (event._result & MOVE_SLOWED) { soundPlay(SOUND_WALK_SLOWED); // WALK_SLOWED move g_screen->screenMessage("%cSlow progress!%c\n", FG_GREY, FG_WHITE); } else if (_winOrLose && getCreature()->isEvil() && (event._result & (MOVE_EXIT_TO_PARENT | MOVE_MAP_CHANGE))) { soundPlay(SOUND_FLEE); // FLEE move } else { soundPlay(SOUND_WALK_COMBAT); // WALK_COMBAT move } } void CombatController::keybinder(KeybindingAction action) { MetaEngine::executeAction(action); } void CombatController::attack(Direction dir, int distance) { g_screen->screenMessage("Dir: "); ReadDirController dirController; #ifdef IOS_ULTIMA4 U4IOS::IOSDirectionHelper directionPopup; #endif if (dir == DIR_NONE) { eventHandler->pushController(&dirController); dir = dirController.waitFor(); if (dir == DIR_NONE) return; } g_screen->screenMessage("%s\n", getDirectionName(dir)); PartyMember *attacker = getCurrentPlayer(); const Weapon *weapon = attacker->getWeapon(); int range = weapon->getRange(); if (weapon->canChooseDistance()) { g_screen->screenMessage("Range: "); if (distance == -1) { int choice = ReadChoiceController::get("123456789"); distance = choice - '0'; } if (distance >= 1 && distance <= weapon->getRange()) { range = distance; g_screen->screenMessage("%d\n", range); } else { return; } } // the attack was already made, even if there is no valid target // so play the attack sound soundPlay(SOUND_PC_ATTACK, false); // PC_ATTACK, melee and ranged Std::vector path = gameGetDirectionalActionPath(MASK_DIR(dir), MASK_DIR_ALL, attacker->getCoords(), 1, range, weapon->canAttackThroughObjects() ? nullptr : &Tile::canAttackOverTile, false); bool foundTarget = false; int targetDistance = path.size(); Coords targetCoords(attacker->getCoords()); if (path.size() > 0) targetCoords = path.back(); distance = 1; for (const auto &coords : path) { if (attackAt(coords, attacker, MASK_DIR(dir), range, distance)) { foundTarget = true; targetDistance = distance; targetCoords = coords; break; } distance++; } // is weapon lost? (e.g. dagger) if (weapon->loseWhenUsed() || (weapon->loseWhenRanged() && (!foundTarget || targetDistance > 1))) { if (!attacker->loseWeapon()) g_screen->screenMessage("Last One!\n"); } // does weapon leave a tile behind? (e.g. flaming oil) const Tile *ground = _map->tileTypeAt(targetCoords, WITHOUT_OBJECTS); if (!weapon->leavesTile().empty() && ground->isWalkable()) _map->_annotations->add(targetCoords, _map->_tileSet->getByName(weapon->leavesTile())->getId()); /* show the 'miss' tile */ if (!foundTarget) { GameController::flashTile(targetCoords, weapon->getMissTile(), 1); /* This goes here so messages are shown in the original order */ g_screen->screenMessage("Missed!\n"); } // does weapon returns to its owner? (e.g. magic axe) if (weapon->returns()) returnWeaponToOwner(targetCoords, targetDistance, MASK_DIR(dir), weapon); } void CombatController::update(Party *party, PartyEvent &event) { if (event._type == PartyEvent::PLAYER_KILLED) g_screen->screenMessage("\n%c%s is Killed!%c\n", FG_RED, event._player->getName().c_str(), FG_WHITE); } /*-------------------------------------------------------------------*/ CombatMap::CombatMap() : Map(), _dungeonRoom(false), _altarRoom(VIRT_NONE), _contextual(false) {} CreatureVector CombatMap::getCreatures() { CreatureVector creatures; for (auto *obj : _objects) { if (isCreature(obj) && !isPartyMember(obj)) creatures.push_back(dynamic_cast(obj)); } return creatures; } PartyMemberVector CombatMap::getPartyMembers() { PartyMemberVector party; for (auto *obj : _objects) { if (isPartyMember(obj)) party.push_back(dynamic_cast(obj)); } return party; } PartyMember *CombatMap::partyMemberAt(Coords coords) { PartyMemberVector party = getPartyMembers(); for (auto *member : party) { if (member->getCoords() == coords) return member; } return nullptr; } Creature *CombatMap::creatureAt(Coords coords) { CreatureVector creatures = getCreatures(); for (auto *c : creatures) { if (c->getCoords() == coords) return c; } return nullptr; } MapId CombatMap::mapForTile(const Tile *groundTile, const Tile *transport, Object *obj) { bool fromShip = false, toShip = false; Object *objUnder = g_context->_location->_map->objectAt(g_context->_location->_coords); static Common::HashMap tileMap; if (!tileMap.size()) { tileMap[g_tileSets->get("base")->getByName("horse")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("swamp")] = MAP_MARSH_CON; tileMap[g_tileSets->get("base")->getByName("grass")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("brush")] = MAP_BRUSH_CON; tileMap[g_tileSets->get("base")->getByName("forest")] = MAP_FOREST_CON; tileMap[g_tileSets->get("base")->getByName("hills")] = MAP_HILL_CON; tileMap[g_tileSets->get("base")->getByName("dungeon")] = MAP_DUNGEON_CON; tileMap[g_tileSets->get("base")->getByName("city")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("castle")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("town")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("lcb_entrance")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("bridge")] = MAP_BRIDGE_CON; tileMap[g_tileSets->get("base")->getByName("balloon")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("bridge_pieces")] = MAP_BRIDGE_CON; tileMap[g_tileSets->get("base")->getByName("shrine")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("chest")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("brick_floor")] = MAP_BRICK_CON; tileMap[g_tileSets->get("base")->getByName("moongate")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("moongate_opening")] = MAP_GRASS_CON; tileMap[g_tileSets->get("base")->getByName("dungeon_floor")] = MAP_GRASS_CON; } static Common::HashMap dungeontileMap; if (!dungeontileMap.size()) { dungeontileMap[g_tileSets->get("dungeon")->getByName("brick_floor")] = MAP_DNG0_CON; dungeontileMap[g_tileSets->get("dungeon")->getByName("up_ladder")] = MAP_DNG1_CON; dungeontileMap[g_tileSets->get("dungeon")->getByName("down_ladder")] = MAP_DNG2_CON; dungeontileMap[g_tileSets->get("dungeon")->getByName("up_down_ladder")] = MAP_DNG3_CON; // dungeontileMap[g_tileSets->get("dungeon")->getByName("chest")] = MAP_DNG4_CON; // chest tile doesn't work that well dungeontileMap[g_tileSets->get("dungeon")->getByName("dungeon_door")] = MAP_DNG5_CON; dungeontileMap[g_tileSets->get("dungeon")->getByName("secret_door")] = MAP_DNG6_CON; } if (g_context->_location->_context & CTX_DUNGEON) { if (dungeontileMap.find(groundTile) != dungeontileMap.end()) return dungeontileMap[groundTile]; return MAP_DNG0_CON; } if (transport->isShip() || (objUnder && objUnder->getTile().getTileType()->isShip())) fromShip = true; if (obj->getTile().getTileType()->isPirateShip()) toShip = true; if (fromShip && toShip) return MAP_SHIPSHIP_CON; /* We can fight creatures and townsfolk */ if (obj->getType() != Object::UNKNOWN) { const Tile *tileUnderneath = g_context->_location->_map->tileTypeAt(obj->getCoords(), WITHOUT_OBJECTS); if (toShip) return MAP_SHORSHIP_CON; else if (fromShip && tileUnderneath->isWater()) return MAP_SHIPSEA_CON; else if (tileUnderneath->isWater()) return MAP_SHORE_CON; else if (fromShip && !tileUnderneath->isWater()) return MAP_SHIPSHOR_CON; } if (tileMap.find(groundTile) != tileMap.end()) return tileMap[groundTile]; return MAP_BRICK_CON; } } // End of namespace Ultima4 } // End of namespace Ultima