Files
2026-02-02 04:50:13 +01:00

863 lines
29 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ultima/ultima4/controllers/game_controller.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/debugger.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/death.h"
#include "ultima/ultima4/game/moongate.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/gfx/imagemgr.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/dungeon.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/map/shrine.h"
#include "ultima/ultima4/ultima4.h"
#include "common/system.h"
namespace Ultima {
namespace Ultima4 {
GameController *g_game = nullptr;
static const MouseArea MOUSE_AREAS[] = {
{ 3, { { 8, 8 }, { 8, 184 }, { 96, 96 } }, MC_WEST, DIR_WEST },
{ 3, { { 8, 8 }, { 184, 8 }, { 96, 96 } }, MC_NORTH, DIR_NORTH },
{ 3, { { 184, 8 }, { 184, 184 }, { 96, 96 } }, MC_EAST, DIR_EAST },
{ 3, { { 8, 184 }, { 184, 184 }, { 96, 96 } }, MC_SOUTH, DIR_SOUTH },
{ 0, { { 0, 0 }, { 0, 0 }, { 0, 0 } }, MC_NORTH, DIR_NONE }
};
GameController::GameController() :
_mapArea(BORDER_WIDTH, BORDER_HEIGHT, VIEWPORT_W, VIEWPORT_H),
_paused(false), _combatFinished(false), _pausedTimer(0) {
g_game = this;
}
void GameController::initScreen() {
Image *screen = imageMgr->get("screen")->_image;
screen->fillRect(0, 0, screen->width(), screen->height(), 0, 0, 0);
g_screen->update();
}
void GameController::initScreenWithoutReloadingState() {
g_music->playMapMusic();
imageMgr->get(BKGD_BORDERS)->_image->draw(0, 0);
g_context->_stats->update(); /* draw the party stats */
g_screen->screenMessage("Press Alt-h for help\n");
g_screen->screenPrompt();
eventHandler->pushMouseAreaSet(MOUSE_AREAS);
eventHandler->setScreenUpdate(&gameUpdateScreen);
}
void GameController::init() {
initScreen();
// initialize the state of the global game context
g_context->_line = TEXT_AREA_H - 1;
g_context->_col = 0;
g_context->_stats = new StatsArea();
g_context->_moonPhase = 0;
g_context->_windDirection = DIR_NORTH;
g_context->_windCounter = 0;
g_context->_windLock = false;
g_context->_aura = new Aura();
g_context->_horseSpeed = 0;
g_context->_opacity = 1;
g_context->_lastCommandTime = g_system->getMillis();
g_context->_lastShip = nullptr;
}
void GameController::setMap(Map *map, bool saveLocation, const Portal *portal, TurnCompleter *turnCompleter) {
int viewMode;
LocationContext context;
int activePlayer = g_context->_party->getActivePlayer();
MapCoords coords;
if (!turnCompleter)
turnCompleter = this;
if (portal)
coords = portal->_start;
else
coords = MapCoords(map->_width / 2, map->_height / 2);
/* If we don't want to save the location, then just return to the previous location,
as there may still be ones in the stack we want to keep */
if (!saveLocation)
exitToParentMap();
switch (map->_type) {
case Map::WORLD:
context = CTX_WORLDMAP;
viewMode = VIEW_NORMAL;
break;
case Map::DUNGEON:
context = CTX_DUNGEON;
viewMode = VIEW_DUNGEON;
if (portal)
g_ultima->_saveGame->_orientation = DIR_EAST;
break;
case Map::COMBAT:
coords = MapCoords(-1, -1); /* set these to -1 just to be safe; we don't need them */
context = CTX_COMBAT;
viewMode = VIEW_NORMAL;
activePlayer = -1; /* different active player for combat, defaults to 'None' */
break;
case Map::SHRINE:
context = CTX_SHRINE;
viewMode = VIEW_NORMAL;
break;
case Map::CITY:
default:
context = CTX_CITY;
viewMode = VIEW_NORMAL;
break;
}
g_context->_location = new Location(coords, map, viewMode, context, turnCompleter, g_context->_location);
g_context->_location->addObserver(this);
g_context->_party->setActivePlayer(activePlayer);
#ifdef IOS_ULTIMA4
U4IOS::updateGameControllerContext(c->location->context);
#endif
/* now, actually set our new tileset */
_mapArea.setTileset(map->_tileSet);
if (isCity(map)) {
City *city = dynamic_cast<City *>(map);
assert(city);
city->addPeople();
}
}
int GameController::exitToParentMap() {
if (!g_context->_location)
return 0;
if (g_context->_location->_prev != nullptr) {
// Create the balloon for Hythloth
if (g_context->_location->_map->_id == MAP_HYTHLOTH)
createBalloon(g_context->_location->_prev->_map);
// free map info only if previous location was on a different map
if (g_context->_location->_prev->_map != g_context->_location->_map) {
g_context->_location->_map->_annotations->clear();
g_context->_location->_map->clearObjects();
/* quench the torch of we're on the world map */
if (g_context->_location->_prev->_map->isWorldMap())
g_context->_party->quenchTorch();
}
locationFree(&g_context->_location);
// restore the tileset to the one the current map uses
_mapArea.setTileset(g_context->_location->_map->_tileSet);
#ifdef IOS_ULTIMA4
U4IOS::updateGameControllerContext(c->location->context);
#endif
return 1;
}
return 0;
}
void GameController::finishTurn() {
g_context->_lastCommandTime = g_system->getMillis();
Creature *attacker = nullptr;
while (1) {
/* adjust food and moves */
g_context->_party->endTurn();
/* count down the aura, if there is one */
g_context->_aura->passTurn();
gameCheckHullIntegrity();
/* update party stats */
//c->stats->setView(STATS_PARTY_OVERVIEW);
g_screen->screenUpdate(&this->_mapArea, true, false);
g_screen->screenWait(1);
/* Creatures cannot spawn, move or attack while the avatar is on the balloon */
if (!g_context->_party->isFlying()) {
// apply effects from tile avatar is standing on
g_context->_party->applyEffect(g_context->_location->_map->tileTypeAt(g_context->_location->_coords, WITH_GROUND_OBJECTS)->getEffect());
// WORKAROUND: This fixes infinite combat loop at the Shrine of Humility.
// I presume the original had code to show the game screen after combat
// without doing all the end turn logic
if (!_combatFinished) {
// Move creatures and see if something is attacking the avatar
attacker = g_context->_location->_map->moveObjects(g_context->_location->_coords);
// Something's attacking! Start combat!
if (attacker) {
gameCreatureAttack(attacker);
return;
}
// cleanup old creatures and spawn new ones
creatureCleanup();
checkRandomCreatures();
checkBridgeTrolls();
} else {
_combatFinished = false;
}
}
/* update map annotations */
g_context->_location->_map->_annotations->passTurn();
if (!g_context->_party->isImmobilized())
break;
if (g_context->_party->isDead()) {
g_death->start(0);
return;
} else {
g_screen->screenMessage("Zzzzzz\n");
g_screen->screenWait(4);
}
}
if (g_context->_location->_context == CTX_DUNGEON) {
Dungeon *dungeon = dynamic_cast<Dungeon *>(g_context->_location->_map);
assert(dungeon);
if (g_context->_party->getTorchDuration() <= 0)
g_screen->screenMessage("It's Dark!\n");
else
g_context->_party->burnTorch();
/* handle dungeon traps */
if (dungeon->currentToken() == DUNGEON_TRAP) {
dungeonHandleTrap((TrapType)dungeon->currentSubToken());
// a little kludgey to have a second test for this
// right here. But without it you can survive an
// extra turn after party death and do some things
// that could cause a crash, like Hole up and Camp.
if (g_context->_party->isDead()) {
g_death->start(0);
return;
}
}
}
/* draw a prompt */
g_screen->screenPrompt();
//g_screen->screenRedrawTextArea(TEXT_AREA_X, TEXT_AREA_Y, TEXT_AREA_W, TEXT_AREA_H);
}
void GameController::flashTile(const Coords &coords, MapTile tile, int frames) {
g_context->_location->_map->_annotations->add(coords, tile, true);
g_screen->screenTileUpdate(&g_game->_mapArea, coords);
g_screen->screenWait(frames);
g_context->_location->_map->_annotations->remove(coords, tile);
g_screen->screenTileUpdate(&g_game->_mapArea, coords, false);
}
void GameController::flashTile(const Coords &coords, const Common::String &tilename, int timeFactor) {
Tile *tile = g_context->_location->_map->_tileSet->getByName(tilename);
assertMsg(tile, "no tile named '%s' found in tileset", tilename.c_str());
flashTile(coords, tile->getId(), timeFactor);
}
void GameController::update(Party *party, PartyEvent &event) {
int i;
switch (event._type) {
case PartyEvent::LOST_EIGHTH:
// inform a player he has lost zero or more eighths of avatarhood.
g_screen->screenMessage("\n %cThou hast lost\n an eighth!%c\n", FG_YELLOW, FG_WHITE);
break;
case PartyEvent::ADVANCED_LEVEL:
g_screen->screenMessage("\n%c%s\nThou art now Level %d%c\n", FG_YELLOW, event._player->getName().c_str(), event._player->getRealLevel(), FG_WHITE);
gameSpellEffect('r', -1, SOUND_MAGIC); // Same as resurrect spell
break;
case PartyEvent::STARVING:
g_screen->screenMessage("\n%cStarving!!!%c\n", FG_YELLOW, FG_WHITE);
/* FIXME: add sound effect here */
// 2 damage to each party member for starving!
for (i = 0; i < g_ultima->_saveGame->_members; i++)
g_context->_party->member(i)->applyDamage(2);
break;
default:
break;
}
}
void GameController::update(Location *location, MoveEvent &event) {
switch (location->_map->_type) {
case Map::DUNGEON:
avatarMovedInDungeon(event);
break;
case Map::COMBAT: {
// FIXME: let the combat controller handle it
CombatController *ctl = dynamic_cast<CombatController *>(eventHandler->getController());
assert(ctl);
ctl->movePartyMember(event);
break;
}
default:
avatarMoved(event);
break;
}
}
void GameController::setActive() {
// The game controller has the keybindings enabled
MetaEngine::setKeybindingMode(KBMODE_NORMAL);
}
void GameController::keybinder(KeybindingAction action) {
MetaEngine::executeAction(action);
}
bool GameController::mousePressed(const Common::Point &mousePos) {
const MouseArea *area = eventHandler->mouseAreaForPoint(mousePos.x, mousePos.y);
if (area) {
keybinder(KEYBIND_INTERACT);
return true;
}
return false;
}
void GameController::initMoons() {
int trammelphase = g_ultima->_saveGame->_trammelPhase,
feluccaphase = g_ultima->_saveGame->_feluccaPhase;
assertMsg(g_context != nullptr, "Game context doesn't exist!");
assertMsg(g_ultima->_saveGame != nullptr, "Savegame doesn't exist!");
//assertMsg(mapIsWorldMap(c->location->map) && c->location->viewMode == VIEW_NORMAL, "Can only call gameInitMoons() from the world map!");
g_ultima->_saveGame->_trammelPhase = g_ultima->_saveGame->_feluccaPhase = 0;
g_context->_moonPhase = 0;
while ((g_ultima->_saveGame->_trammelPhase != trammelphase) ||
(g_ultima->_saveGame->_feluccaPhase != feluccaphase))
updateMoons(false);
}
void GameController::updateMoons(bool showmoongates) {
int realMoonPhase,
oldTrammel,
trammelSubphase;
const Coords *gate;
if (g_context->_location->_map->isWorldMap()) {
oldTrammel = g_ultima->_saveGame->_trammelPhase;
if (++g_context->_moonPhase >= MOON_PHASES * MOON_SECONDS_PER_PHASE * 4)
g_context->_moonPhase = 0;
trammelSubphase = g_context->_moonPhase % (MOON_SECONDS_PER_PHASE * 4 * 3);
realMoonPhase = (g_context->_moonPhase / (4 * MOON_SECONDS_PER_PHASE));
g_ultima->_saveGame->_trammelPhase = realMoonPhase / 3;
g_ultima->_saveGame->_feluccaPhase = realMoonPhase % 8;
if (g_ultima->_saveGame->_trammelPhase > 7)
g_ultima->_saveGame->_trammelPhase = 7;
if (showmoongates) {
/* update the moongates if trammel changed */
if (trammelSubphase == 0) {
gate = g_moongates->getGateCoordsForPhase(oldTrammel);
if (gate)
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x40));
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate)
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x40));
} else if (trammelSubphase == 1) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x40));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x41));
}
} else if (trammelSubphase == 2) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x41));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x42));
}
} else if (trammelSubphase == 3) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x42));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x43));
}
} else if ((trammelSubphase > 3) && (trammelSubphase < (MOON_SECONDS_PER_PHASE * 4 * 3) - 3)) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x43));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x43));
}
} else if (trammelSubphase == (MOON_SECONDS_PER_PHASE * 4 * 3) - 3) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x43));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x42));
}
} else if (trammelSubphase == (MOON_SECONDS_PER_PHASE * 4 * 3) - 2) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x42));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x41));
}
} else if (trammelSubphase == (MOON_SECONDS_PER_PHASE * 4 * 3) - 1) {
gate = g_moongates->getGateCoordsForPhase(g_ultima->_saveGame->_trammelPhase);
if (gate) {
g_context->_location->_map->_annotations->remove(*gate, g_context->_location->_map->translateFromRawTileIndex(0x41));
g_context->_location->_map->_annotations->add(*gate, g_context->_location->_map->translateFromRawTileIndex(0x40));
}
}
}
}
}
void GameController::avatarMoved(MoveEvent &event) {
if (event._userEvent) {
// is filterMoveMessages even used? it doesn't look like the option is hooked up in the configuration menu
if (!settings._filterMoveMessages) {
switch (g_context->_transportContext) {
case TRANSPORT_FOOT:
case TRANSPORT_HORSE:
g_screen->screenMessage("%s\n", getDirectionName(event._dir));
break;
case TRANSPORT_SHIP:
if (event._result & MOVE_TURNED)
g_screen->screenMessage("Turn %s!\n", getDirectionName(event._dir));
else if (event._result & MOVE_SLOWED)
g_screen->screenMessage("%cSlow progress!%c\n", FG_GREY, FG_WHITE);
else
g_screen->screenMessage("Sail %s!\n", getDirectionName(event._dir));
break;
case TRANSPORT_BALLOON:
g_screen->screenMessage("%cDrift Only!%c\n", FG_GREY, FG_WHITE);
break;
default:
error("bad transportContext %d in avatarMoved()", g_context->_transportContext);
}
}
/* movement was blocked */
if (event._result & MOVE_BLOCKED) {
/* if shortcuts are enabled, try them! */
if (settings._shortcutCommands) {
MapCoords new_coords = g_context->_location->_coords;
MapTile *tile;
new_coords.move(event._dir, g_context->_location->_map);
tile = g_context->_location->_map->tileAt(new_coords, WITH_OBJECTS);
if (tile->getTileType()->isDoor()) {
g_debugger->openAt(new_coords);
event._result = (MoveResult)(MOVE_SUCCEEDED | MOVE_END_TURN);
} else if (tile->getTileType()->isLockedDoor()) {
g_debugger->jimmyAt(new_coords);
event._result = (MoveResult)(MOVE_SUCCEEDED | MOVE_END_TURN);
} /*else if (mapPersonAt(c->location->map, new_coords) != nullptr) {
talkAtCoord(newx, newy, 1, nullptr);
event.result = MOVE_SUCCEEDED | MOVE_END_TURN;
}*/
}
/* if we're still blocked */
if ((event._result & MOVE_BLOCKED) && !settings._filterMoveMessages) {
soundPlay(SOUND_BLOCKED, false);
g_screen->screenMessage("%cBlocked!%c\n", FG_GREY, FG_WHITE);
}
} else if (g_context->_transportContext == TRANSPORT_FOOT || g_context->_transportContext == TRANSPORT_HORSE) {
/* movement was slowed */
if (event._result & MOVE_SLOWED) {
soundPlay(SOUND_WALK_SLOWED);
g_screen->screenMessage("%cSlow progress!%c\n", FG_GREY, FG_WHITE);
} else {
soundPlay(SOUND_WALK_NORMAL);
}
}
}
/* exited map */
if (event._result & MOVE_EXIT_TO_PARENT) {
g_screen->screenMessage("%cLeaving...%c\n", FG_GREY, FG_WHITE);
exitToParentMap();
g_music->playMapMusic();
}
/* things that happen while not on board the balloon */
if (g_context->_transportContext & ~TRANSPORT_BALLOON)
checkSpecialCreatures(event._dir);
/* things that happen while on foot or horseback */
if ((g_context->_transportContext & TRANSPORT_FOOT_OR_HORSE) &&
!(event._result & (MOVE_SLOWED | MOVE_BLOCKED))) {
if (checkMoongates())
event._result = (MoveResult)(MOVE_MAP_CHANGE | MOVE_END_TURN);
}
}
void GameController::avatarMovedInDungeon(MoveEvent &event) {
Direction realDir = dirNormalize((Direction)g_ultima->_saveGame->_orientation, event._dir);
Dungeon *dungeon = dynamic_cast<Dungeon *>(g_context->_location->_map);
assert(dungeon);
if (!settings._filterMoveMessages) {
if (event._userEvent) {
if (event._result & MOVE_TURNED) {
if (dirRotateCCW((Direction)g_ultima->_saveGame->_orientation) == realDir)
g_screen->screenMessage("Turn Left\n");
else
g_screen->screenMessage("Turn Right\n");
} else {
// Show 'Advance' or 'Retreat' in dungeons
g_screen->screenMessage("%s\n", realDir == g_ultima->_saveGame->_orientation ? "Advance" : "Retreat");
}
}
if (event._result & MOVE_BLOCKED)
g_screen->screenMessage("%cBlocked!%c\n", FG_GREY, FG_WHITE);
}
// If we're exiting the map, do this
if (event._result & MOVE_EXIT_TO_PARENT) {
g_screen->screenMessage("%cLeaving...%c\n", FG_GREY, FG_WHITE);
exitToParentMap();
g_music->playMapMusic();
}
// Check to see if we're entering a dungeon room
if (event._result & MOVE_SUCCEEDED) {
if (dungeon->currentToken() == DUNGEON_ROOM) {
int room = (int)dungeon->currentSubToken(); // Get room number
/**
* recalculate room for the abyss -- there are 16 rooms for every 2 levels,
* each room marked with 0xD* where (* == room number 0-15).
* for levels 1 and 2, there are 16 rooms, levels 3 and 4 there are 16 rooms, etc.
*/
if (g_context->_location->_map->_id == MAP_ABYSS)
room = (0x10 * (g_context->_location->_coords.z / 2)) + room;
Dungeon *dng = dynamic_cast<Dungeon *>(g_context->_location->_map);
assert(dng);
dng->_currentRoom = room;
// Set the map and start combat!
CombatController *cc = new CombatController(dng->_roomMaps[room]);
cc->initDungeonRoom(room, dirReverse(realDir));
cc->begin();
}
}
}
void GameController::timerFired() {
if (_pausedTimer > 0) {
_pausedTimer--;
if (_pausedTimer <= 0) {
_pausedTimer = 0;
_paused = false; /* unpause the game */
}
}
if (!_paused && !_pausedTimer) {
if (++g_context->_windCounter >= MOON_SECONDS_PER_PHASE * 4) {
if (xu4_random(4) == 1 && !g_context->_windLock)
g_context->_windDirection = dirRandomDir(MASK_DIR_ALL);
g_context->_windCounter = 0;
}
/* balloon moves about 4 times per second */
if ((g_context->_transportContext == TRANSPORT_BALLOON) &&
g_context->_party->isFlying()) {
g_context->_location->move(dirReverse((Direction) g_context->_windDirection), false);
}
updateMoons(true);
g_screen->screenCycle();
// Check for any right-button mouse movement
KeybindingAction action = eventHandler->getAction();
if (action != KEYBIND_NONE)
keybinder(action);
// Do game udpates to the screen, like tile animations
gameUpdateScreen();
/*
* force pass if no commands within last 20 seconds
*/
Controller *controller = eventHandler->getController();
if (controller != nullptr && (eventHandler->getController() == g_game ||
dynamic_cast<CombatController *>(eventHandler->getController()) != nullptr) &&
gameTimeSinceLastCommand() > 20) {
/* pass the turn, and redraw the text area so the prompt is shown */
MetaEngine::executeAction(KEYBIND_PASS);
g_screen->screenRedrawTextArea(TEXT_AREA_X, TEXT_AREA_Y, TEXT_AREA_W, TEXT_AREA_H);
}
}
}
void GameController::checkSpecialCreatures(Direction dir) {
int i;
Object *obj;
static const struct {
int x, y;
Direction dir;
} pirateInfo[] = {
{ 224, 220, DIR_EAST }, /* N'M" O'A" */
{ 224, 228, DIR_EAST }, /* O'E" O'A" */
{ 226, 220, DIR_EAST }, /* O'E" O'C" */
{ 227, 228, DIR_EAST }, /* O'E" O'D" */
{ 228, 227, DIR_SOUTH }, /* O'D" O'E" */
{ 229, 225, DIR_SOUTH }, /* O'B" O'F" */
{ 229, 223, DIR_NORTH }, /* N'P" O'F" */
{ 228, 222, DIR_NORTH } /* N'O" O'E" */
};
/*
* if heading east into pirates cove (O'A" N'N"), generate pirate
* ships
*/
if (dir == DIR_EAST &&
g_context->_location->_coords.x == 0xdd &&
g_context->_location->_coords.y == 0xe0) {
for (i = 0; i < 8; i++) {
obj = g_context->_location->_map->addCreature(creatureMgr->getById(PIRATE_ID), MapCoords(pirateInfo[i].x, pirateInfo[i].y));
obj->setDirection(pirateInfo[i].dir);
}
}
/*
* if heading south towards the shrine of humility, generate
* daemons unless horn has been blown
*/
if (dir == DIR_SOUTH &&
g_context->_location->_coords.x >= 229 &&
g_context->_location->_coords.x < 234 &&
g_context->_location->_coords.y >= 212 &&
g_context->_location->_coords.y < 217 &&
*g_context->_aura != Aura::HORN) {
for (i = 0; i < 8; i++)
g_context->_location->_map->addCreature(creatureMgr->getById(DAEMON_ID), MapCoords(231, g_context->_location->_coords.y + 1, g_context->_location->_coords.z));
}
}
bool GameController::checkMoongates() {
Coords dest;
if (g_moongates->findActiveGateAt(g_ultima->_saveGame->_trammelPhase, g_ultima->_saveGame->_feluccaPhase, g_context->_location->_coords, dest)) {
gameSpellEffect(-1, -1, SOUND_MOONGATE); // Default spell effect (screen inversion without 'spell' sound effects)
if (g_context->_location->_coords != dest) {
g_context->_location->_coords = dest;
gameSpellEffect(-1, -1, SOUND_MOONGATE); // Again, after arriving
}
if (g_moongates->isEntryToShrineOfSpirituality(g_ultima->_saveGame->_trammelPhase, g_ultima->_saveGame->_feluccaPhase)) {
Shrine *shrine_spirituality;
shrine_spirituality = dynamic_cast<Shrine *>(mapMgr->get(MAP_SHRINE_SPIRITUALITY));
assert(shrine_spirituality);
if (!g_context->_party->canEnterShrine(VIRT_SPIRITUALITY))
return true;
setMap(shrine_spirituality, 1, nullptr);
g_music->playMapMusic();
shrine_spirituality->enter();
}
return true;
}
return false;
}
void GameController::creatureCleanup() {
ObjectDeque::iterator i;
Map *map = g_context->_location->_map;
for (i = map->_objects.begin(); i != map->_objects.end();) {
Object *obj = *i;
MapCoords o_coords = obj->getCoords();
if ((obj->getType() == Object::CREATURE) && (o_coords.z == g_context->_location->_coords.z) &&
o_coords.distance(g_context->_location->_coords, g_context->_location->_map) > MAX_CREATURE_DISTANCE) {
/* delete the object and remove it from the map */
i = map->removeObject(i);
} else {
i++;
}
}
}
void GameController::checkRandomCreatures() {
int canSpawnHere = g_context->_location->_map->isWorldMap() || g_context->_location->_context & CTX_DUNGEON;
#ifdef IOS_ULTIMA4
int spawnDivisor = c->location->context & CTX_DUNGEON ? (53 - (c->location->coords.z << 2)) : 53;
#else
int spawnDivisor = g_context->_location->_context & CTX_DUNGEON ? (32 - (g_context->_location->_coords.z << 2)) : 32;
#endif
/* If there are too many creatures already,
or we're not on the world map, don't worry about it! */
if (!canSpawnHere ||
g_context->_location->_map->getNumberOfCreatures() >= MAX_CREATURES_ON_MAP ||
xu4_random(spawnDivisor) != 0)
return;
// If combat is turned off, then don't spawn any creator
if (g_debugger->_disableCombat)
return;
gameSpawnCreature(nullptr);
}
void GameController::checkBridgeTrolls() {
const Tile *bridge = g_context->_location->_map->_tileSet->getByName("bridge");
if (!bridge)
return;
// TODO: CHEST: Make a user option to not make chests block bridge trolls
if (!g_context->_location->_map->isWorldMap() ||
g_context->_location->_map->tileAt(g_context->_location->_coords, WITH_OBJECTS)->_id != bridge->getId() ||
xu4_random(8) != 0)
return;
g_screen->screenMessage("\nBridge Trolls!\n");
Creature *m = g_context->_location->_map->addCreature(creatureMgr->getById(TROLL_ID), g_context->_location->_coords);
CombatController *cc = new CombatController(MAP_BRIDGE_CON);
cc->init(m);
cc->begin();
}
bool GameController::createBalloon(Map *map) {
ObjectDeque::iterator i;
/* see if the balloon has already been created (and not destroyed) */
for (auto *obj : map->_objects) {
if (obj->getTile().getTileType()->isBalloon())
return false;
}
const Tile *balloon = map->_tileSet->getByName("balloon");
assertMsg(balloon, "no balloon tile found in tileset");
map->addObject(balloon->getId(), balloon->getId(), map->getLabel("balloon"));
return true;
}
void GameController::attack(Direction dir) {
g_screen->screenMessage("Attack: ");
if (g_context->_party->isFlying()) {
g_screen->screenMessage("\n%cDrift only!%c\n", FG_GREY, FG_WHITE);
return;
}
if (dir == DIR_NONE)
dir = gameGetDirection();
if (dir == DIR_NONE) {
g_screen->screenMessage("\n");
return;
}
Std::vector<Coords> path = gameGetDirectionalActionPath(
MASK_DIR(dir), MASK_DIR_ALL, g_context->_location->_coords,
1, 1, nullptr, true);
for (const auto &coords : path) {
if (attackAt(coords))
return;
}
g_screen->screenMessage("%cNothing to Attack!%c\n", FG_GREY, FG_WHITE);
}
bool GameController::attackAt(const Coords &coords) {
Object *under;
const Tile *ground;
Creature *m;
m = dynamic_cast<Creature *>(g_context->_location->_map->objectAt(coords));
// Nothing attackable: move on to next tile
if (m == nullptr || !m->isAttackable())
return false;
// Attack successful
/// TODO: CHEST: Make a user option to not make chests change battlefield
/// map (1 of 2)
ground = g_context->_location->_map->tileTypeAt(g_context->_location->_coords, WITH_GROUND_OBJECTS);
if (!ground->isChest()) {
ground = g_context->_location->_map->tileTypeAt(g_context->_location->_coords, WITHOUT_OBJECTS);
if ((under = g_context->_location->_map->objectAt(g_context->_location->_coords)) &&
under->getTile().getTileType()->isShip())
ground = under->getTile().getTileType();
}
// You're attacking a townsperson! Alert the guards!
if ((m->getType() == Object::PERSON) && (m->getMovementBehavior() != MOVEMENT_ATTACK_AVATAR))
g_context->_location->_map->alertGuards();
// Not good karma to be killing the innocent. Bad avatar!
if (m->isGood() || /* attacking a good creature */
/* attacking a docile (although possibly evil) person in town */
((m->getType() == Object::PERSON) && (m->getMovementBehavior() != MOVEMENT_ATTACK_AVATAR)))
g_context->_party->adjustKarma(KA_ATTACKED_GOOD);
CombatController *cc = new CombatController(CombatMap::mapForTile(ground, g_context->_party->getTransport().getTileType(), m));
cc->init(m);
cc->begin();
return false;
}
} // End of namespace Ultima4
} // End of namespace Ultima