Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
/* 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/game/armor.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/map/tile.h"
#include "common/algorithm.h"
namespace Ultima {
namespace Ultima4 {
Armors *g_armors;
Armors::Armors() : _confLoaded(false) {
g_armors = this;
}
Armors::~Armors() {
g_armors = nullptr;
}
const Armor *Armors::get(ArmorType a) {
// Load in XML if it hasn't been already
loadConf();
if (static_cast<unsigned>(a) >= size())
return nullptr;
return (*this)[a];
}
const Armor *Armors::get(const Common::String &name) {
// Load in XML if it hasn't been already
loadConf();
for (unsigned i = 0; i < size(); i++) {
if (scumm_stricmp(name.c_str(), (*this)[i]->_name.c_str()) == 0)
return (*this)[i];
}
return nullptr;
}
void Armors::loadConf() {
if (!_confLoaded)
_confLoaded = true;
else
return;
const Config *config = Config::getInstance();
Std::vector<ConfigElement> armorConfs = config->getElement("armors").getChildren();
for (const auto &i : armorConfs) {
if (i.getName() != "armor")
continue;
ArmorType armorType = static_cast<ArmorType>(size());
push_back(new Armor(armorType, i));
}
}
/*-------------------------------------------------------------------*/
Armor::Armor(ArmorType armorType, const ConfigElement &conf) :
_type(armorType), _canUse(0xff) /*, _mask(0) */ {
_name = conf.getString("name");
_defense = conf.getInt("defense");
Std::vector<ConfigElement> contraintConfs = conf.getChildren();
for (const auto &i : contraintConfs) {
byte useMask = 0;
if (i.getName() != "constraint")
continue;
for (int cl = 0; cl < 8; cl++) {
if (scumm_stricmp(i.getString("class").c_str(), getClassName(static_cast<ClassType>(cl))) == 0)
useMask = (1 << cl);
}
if (useMask == 0 && scumm_stricmp(i.getString("class").c_str(), "all") == 0)
useMask = 0xFF;
if (useMask == 0) {
error("malformed armor.xml file: constraint has unknown class %s",
i.getString("class").c_str());
}
if (i.getBool("canuse"))
_canUse |= useMask;
else
_canUse &= ~useMask;
}
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,94 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_ARMOR_H
#define ULTIMA4_GAME_ARMOR_H
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/shared/std/containers.h"
#include "ultima/shared/std/string.h"
namespace Ultima {
namespace Ultima4 {
class ConfigElement;
class Armors;
class Armor {
friend class Armors;
public:
// Getters
ArmorType getType() const {
return _type; /**< Returns the ArmorType of the armor */
}
const Common::String &getName() const {
return _name; /**< Returns the name of the armor */
}
int getDefense() const {
return _defense; /**< Returns the defense value of the armor */
}
/** Returns true if the class given can wear the armor */
bool canWear(ClassType klass) const {
return _canUse & (1 << klass);
}
private:
Armor(ArmorType armorType, const ConfigElement &conf);
ArmorType _type;
Common::String _name;
byte _canUse;
int _defense;
//unsigned short _mask;
};
class Armors : public Std::vector<Armor *> {
private:
void loadConf();
bool _confLoaded;
public:
/**
* Constructor
*/
Armors();
/**
* Destructor
*/
~Armors();
/**
* Returns armor by ArmorType.
*/
const Armor *get(ArmorType a);
/**
* Returns armor that has the given name
*/
const Armor *get(const Common::String &name);
};
extern Armors *g_armors;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,62 @@
/* 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/game/aura.h"
namespace Ultima {
namespace Ultima4 {
Aura::Aura() : _type(NONE), _duration(0) {}
void Aura::setDuration(int d) {
_duration = d;
setChanged();
notifyObservers(nullptr);
}
void Aura::set(Type t, int d) {
_type = t;
_duration = d;
setChanged();
notifyObservers(nullptr);
}
void Aura::setType(Type t) {
_type = t;
setChanged();
notifyObservers(nullptr);
}
void Aura::passTurn() {
if (_duration > 0) {
_duration--;
if (_duration == 0) {
_type = NONE;
setChanged();
notifyObservers(nullptr);
}
}
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,77 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_AURA_H
#define ULTIMA4_GAME_AURA_H
#include "ultima/ultima4/core/observable.h"
namespace Ultima {
namespace Ultima4 {
/**
* Aura class
*/
class Aura : public Observable<Aura *> {
public:
enum Type {
NONE,
HORN,
JINX,
NEGATE,
PROTECTION,
QUICKNESS
};
Aura();
int getDuration() const {
return _duration;
}
Aura::Type getType() const {
return _type;
}
bool isActive() const {
return _duration > 0;
}
void setDuration(int d);
void set(Type = NONE, int d = 0);
void setType(Type t);
bool operator==(const Type &t) const {
return _type == t;
}
bool operator!=(const Type &t) const {
return !operator==(t);
}
void passTurn();
private:
Type _type;
int _duration;
};
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,435 @@
/* 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/game/codex.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/item.h"
#include "ultima/ultima4/gfx/imagemgr.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/ultima4.h"
#include "ultima/ultima4/filesys/u4file.h"
#include "ultima/ultima4/core/utils.h"
namespace Ultima {
namespace Ultima4 {
Codex *g_codex;
Codex::Codex() {
g_codex = this;
}
Codex::~Codex() {
g_codex = nullptr;
}
int Codex::init() {
_virtueQuestions = u4read_stringtable("virtue");
_endgameText1 = u4read_stringtable("endgame1");
_endgameText2 = u4read_stringtable("endgame2");
return 1;
}
void Codex::deinit() {
_virtueQuestions.clear();
_endgameText1.clear();
_endgameText2.clear();
}
void Codex::start() {
init();
// Disable the whirlpool cursor and black out the screen
#ifdef IOS_ULTIMA4
U4IOS::IOSHideGameControllerHelper hideControllsHelper;
#endif
g_screen->screenDisableCursor();
g_screen->screenUpdate(&g_game->_mapArea, false, true);
// Make the avatar alone
g_context->_stats->setView(STATS_PARTY_OVERVIEW);
g_context->_stats->update(true); // show just the avatar
g_screen->update();
// Change the view mode so the dungeon doesn't get shown
gameSetViewMode(VIEW_CODEX);
g_screen->screenMessage("\n\n\n\nThere is a sudden darkness, and you find yourself alone in an empty chamber.\n");
EventHandler::sleep(4000);
// Check to see if you have the 3-part key
if ((g_ultima->_saveGame->_items & (ITEM_KEY_C | ITEM_KEY_L | ITEM_KEY_T)) != (ITEM_KEY_C | ITEM_KEY_L | ITEM_KEY_T)) {
eject(CODEX_EJECT_NO_3_PART_KEY);
return;
}
g_screen->screenDrawImageInMapArea(BKGD_KEY);
g_screen->screenRedrawMapArea();
g_screen->screenMessage("\nYou use your key of Three Parts.\n");
EventHandler::sleep(3000);
g_screen->screenMessage("\nA voice rings out:\n\"What is the Word of Passage?\"\n\n");
// Get the Word of Passage
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("What is the Word of Passage?");
#endif
handleWOP(gameGetInput());
}
void Codex::eject(CodexEjectCode code) {
const struct {
int x, y;
} startLocations[] = {
{ 231, 136 },
{ 83, 105 },
{ 35, 221 },
{ 59, 44 },
{ 158, 21 },
{ 105, 183 },
{ 23, 129 },
{ 186, 171 }
};
switch (code) {
case CODEX_EJECT_NO_3_PART_KEY:
g_screen->screenMessage("\nThou dost not have the Key of Three Parts.\n\n");
break;
case CODEX_EJECT_NO_FULL_PARTY:
g_screen->screenMessage("\nThou hast not proved thy leadership in all eight virtues.\n\n");
EventHandler::sleep(2000);
g_screen->screenMessage("\nPassage is not granted.\n\n");
break;
case CODEX_EJECT_NO_FULL_AVATAR:
g_screen->screenMessage("\nThou art not ready.\n");
EventHandler::sleep(2000);
g_screen->screenMessage("\nPassage is not granted.\n\n");
break;
case CODEX_EJECT_BAD_WOP:
g_screen->screenMessage("\nPassage is not granted.\n\n");
break;
case CODEX_EJECT_HONESTY:
case CODEX_EJECT_COMPASSION:
case CODEX_EJECT_VALOR:
case CODEX_EJECT_JUSTICE:
case CODEX_EJECT_SACRIFICE:
case CODEX_EJECT_HONOR:
case CODEX_EJECT_SPIRITUALITY:
case CODEX_EJECT_HUMILITY:
case CODEX_EJECT_TRUTH:
case CODEX_EJECT_LOVE:
case CODEX_EJECT_COURAGE:
g_screen->screenMessage("\nThy quest is not yet complete.\n\n");
break;
case CODEX_EJECT_BAD_INFINITY:
g_screen->screenMessage("\nThou dost not know the true nature of the Universe.\n\n");
break;
default:
g_screen->screenMessage("\nOops, you just got too close to beating the game.\nBAD AVATAR!\n");
break;
}
EventHandler::sleep(2000);
// Free memory associated with the Codex
deinit();
// Re-enable the cursor and show it
g_screen->screenEnableCursor();
g_screen->screenShowCursor();
// Return view to normal and exit the Abyss
gameSetViewMode(VIEW_NORMAL);
g_game->exitToParentMap();
g_music->playMapMusic();
/*
* if being ejected because of a missed virtue question,
* then teleport the party to the starting location for
* that virtue.
*/
if (code >= CODEX_EJECT_HONESTY && code <= CODEX_EJECT_HUMILITY) {
int virtue = code - CODEX_EJECT_HONESTY;
g_context->_location->_coords.x = startLocations[virtue].x;
g_context->_location->_coords.y = startLocations[virtue].y;
}
// finally, finish the turn
g_context->_location->_turnCompleter->finishTurn();
eventHandler->setController(g_game);
}
void Codex::handleWOP(const Common::String &word) {
static int tries = 1;
int i;
eventHandler->popKeyHandler();
// slight pause before continuing
g_screen->screenMessage("\n");
g_screen->screenDisableCursor();
EventHandler::sleep(1000);
// entered correctly
if (scumm_stricmp(word.c_str(), "veramocor") == 0) {
tries = 1; // reset 'tries' in case we need to enter this again later
// eject them if they don't have all 8 party members
if (g_ultima->_saveGame->_members != 8) {
eject(CODEX_EJECT_NO_FULL_PARTY);
return;
}
// eject them if they're not a full avatar at this point
for (i = 0; i < VIRT_MAX; i++) {
if (g_ultima->_saveGame->_karma[i] != 0) {
eject(CODEX_EJECT_NO_FULL_AVATAR);
return;
}
}
g_screen->screenMessage("\nPassage is granted.\n");
EventHandler::sleep(4000);
g_screen->screenEraseMapArea();
g_screen->screenRedrawMapArea();
// Ask the Virtue questions
g_screen->screenMessage("\n\nThe voice asks:\n");
EventHandler::sleep(2000);
g_screen->screenMessage("\n%s\n\n", _virtueQuestions[0].c_str());
handleVirtues(gameGetInput());
return;
}
// entered incorrectly - give 3 tries before ejecting
else if (tries++ < 3) {
impureThoughts();
g_screen->screenMessage("\"What is the Word of Passage?\"\n\n");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which virtue?");
#endif
handleWOP(gameGetInput());
}
// 3 tries are up... eject!
else {
tries = 1;
eject(CODEX_EJECT_BAD_WOP);
}
}
void Codex::handleVirtues(const Common::String &virtue) {
static const char *const codexImageNames[] = {
BKGD_HONESTY, BKGD_COMPASSN, BKGD_VALOR, BKGD_JUSTICE,
BKGD_SACRIFIC, BKGD_HONOR, BKGD_SPIRIT, BKGD_HUMILITY,
BKGD_TRUTH, BKGD_LOVE, BKGD_COURAGE
};
static int current = 0;
static int tries = 1;
eventHandler->popKeyHandler();
// slight pause before continuing
g_screen->screenMessage("\n");
g_screen->screenDisableCursor();
EventHandler::sleep(1000);
// answered with the correct one of eight virtues
if ((current < VIRT_MAX) &&
(scumm_stricmp(virtue.c_str(), getVirtueName(static_cast<Virtue>(current))) == 0)) {
g_screen->screenDrawImageInMapArea(codexImageNames[current]);
g_screen->screenRedrawMapArea();
current++;
tries = 1;
EventHandler::sleep(2000);
if (current == VIRT_MAX) {
g_screen->screenMessage("\nThou art well versed in the virtues of the Avatar.\n");
EventHandler::sleep(5000);
}
g_screen->screenMessage("\n\nThe voice asks:\n");
EventHandler::sleep(2000);
g_screen->screenMessage("\n%s\n\n", _virtueQuestions[current].c_str());
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString((current != VIRT_MAX) ? "Which virtue?" : "Which principle?");
#endif
handleVirtues(gameGetInput());
}
// answered with the correct base virtue (truth, love, courage)
else if ((current >= VIRT_MAX) &&
(scumm_stricmp(virtue.c_str(), getBaseVirtueName(static_cast<BaseVirtue>(1 << (current - VIRT_MAX)))) == 0)) {
g_screen->screenDrawImageInMapArea(codexImageNames[current]);
g_screen->screenRedrawMapArea();
current++;
tries = 1;
if (current < VIRT_MAX + 3) {
g_screen->screenMessage("\n\nThe voice asks:\n");
EventHandler::sleep(2000);
g_screen->screenMessage("\n%s\n\n", _virtueQuestions[current].c_str());
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which principle?");
#endif
handleVirtues(gameGetInput());
} else {
g_screen->screenMessage("\nThe ground rumbles beneath your feet.\n");
EventHandler::sleep(1000);
g_screen->screenShake(10);
EventHandler::sleep(3000);
g_screen->screenEnableCursor();
g_screen->screenMessage("\nAbove the din, the voice asks:\n\nIf all eight virtues of the Avatar combine into and are derived from the Three Principles of Truth, Love and Courage...");
#ifdef IOS_ULTIMA4
// Ugh, we now enter happy callback land, so I know how to do these things manually. Good thing I kept these separate functions.
U4IOS::beginChoiceConversation();
U4IOS::updateChoicesInDialog(" ", "", -1);
#endif
eventHandler->pushKeyHandler(&handleInfinityAnyKey);
}
}
// give them 3 tries to enter the correct virtue, then eject them!
else if (tries++ < 3) {
impureThoughts();
g_screen->screenMessage("%s\n\n", _virtueQuestions[current].c_str());
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which virtue?");
#endif
handleVirtues(gameGetInput());
}
// failed 3 times... eject!
else {
eject(static_cast<CodexEjectCode>(CODEX_EJECT_HONESTY + current));
tries = 1;
current = 0;
}
}
bool Codex::handleInfinityAnyKey(int key, void *data) {
eventHandler->popKeyHandler();
g_screen->screenMessage("\n\nThen what is the one thing which encompasses and is the whole of all undeniable Truth, unending Love, and unyielding Courage?\n\n");
#ifdef IOS_ULTIMA4
U4IOS::endChoiceConversation();
U4IOS::IOSConversationHelper::setIntroString("What is the whole of all undeniable Truth, unending Love, and unyielding Courage?");
#endif
g_codex->handleInfinity(gameGetInput());
return true;
}
void Codex::handleInfinity(const Common::String &answer) {
static int tries = 1;
eventHandler->popKeyHandler();
#ifdef IOS_ULTIMA4
U4IOS::IOSHideGameControllerHelper hideControllsHelper;
#endif
// slight pause before continuing
g_screen->screenMessage("\n");
g_screen->screenDisableCursor();
EventHandler::sleep(1000);
if (scumm_stricmp(answer.c_str(), "infinity") == 0) {
EventHandler::sleep(2000);
g_screen->screenShake(10);
g_screen->screenEnableCursor();
g_screen->screenMessage("\n%s", _endgameText1[0].c_str());
#ifdef IOS_ULTIMA4
// Ugh, we now enter happy callback land, so I know how to do these things manually. Good thing I kept these separate functions.
U4IOS::hideGameButtons();
U4IOS::beginChoiceConversation();
U4IOS::updateChoicesInDialog(" ", "", -1);
U4IOS::testFlightPassCheckPoint("Game won!");
#endif
eventHandler->pushKeyHandler(&handleEndgameAnyKey);
} else if (tries++ < 3) {
impureThoughts();
g_screen->screenMessage("\nAbove the din, the voice asks:\n\nIf all eight virtues of the Avatar combine into and are derived from the Three Principles of Truth, Love and Courage...");
eventHandler->pushKeyHandler(&handleInfinityAnyKey);
} else eject(CODEX_EJECT_BAD_INFINITY);
}
bool Codex::handleEndgameAnyKey(int key, void *data) {
static int index = 1;
eventHandler->popKeyHandler();
if (index < 10) {
if (index < 7) {
if (index == 6) {
g_screen->screenEraseMapArea();
g_screen->screenRedrawMapArea();
}
g_screen->screenMessage("%s", g_codex->_endgameText1[index].c_str());
} else if (index == 7) {
g_screen->screenDrawImageInMapArea(BKGD_STONCRCL);
g_screen->screenRedrawMapArea();
g_screen->screenMessage("\n\n%s", g_codex->_endgameText2[index - 7].c_str());
} else if (index > 7)
g_screen->screenMessage("%s", g_codex->_endgameText2[index - 7].c_str());
index++;
eventHandler->pushKeyHandler(&g_codex->handleEndgameAnyKey);
} else {
// CONGRATULATIONS!... you have completed the game in x turns
g_screen->screenDisableCursor();
g_screen->screenMessage("%s%d%s", g_codex->_endgameText2[index - 7].c_str(),
g_ultima->_saveGame->_moves, g_codex->_endgameText2[index - 6].c_str());
#ifdef IOS_ULTIMA4
U4IOS::endChoiceConversation();
#endif
eventHandler->pushKeyHandler(&KeyHandler::ignoreKeys);
}
return true;
}
void Codex::impureThoughts() {
g_screen->screenMessage("\nThy thoughts are not pure.\nI ask again.\n");
EventHandler::sleep(2000);
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,115 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_CODEX_H
#define ULTIMA4_GAME_CODEX_H
#include "common/array.h"
namespace Ultima {
namespace Ultima4 {
enum CodexEjectCode {
CODEX_EJECT_NO_3_PART_KEY,
CODEX_EJECT_BAD_WOP,
CODEX_EJECT_NO_FULL_PARTY,
CODEX_EJECT_NO_FULL_AVATAR,
CODEX_EJECT_HONESTY,
CODEX_EJECT_COMPASSION,
CODEX_EJECT_VALOR,
CODEX_EJECT_JUSTICE,
CODEX_EJECT_SACRIFICE,
CODEX_EJECT_HONOR,
CODEX_EJECT_SPIRITUALITY,
CODEX_EJECT_HUMILITY,
CODEX_EJECT_TRUTH,
CODEX_EJECT_LOVE,
CODEX_EJECT_COURAGE,
CODEX_EJECT_BAD_INFINITY
};
class Codex {
private:
Common::Array<Common::String> _virtueQuestions;
Common::Array<Common::String> _endgameText1;
Common::Array<Common::String> _endgameText2;
private:
/**
* Initializes the Chamber of the Codex sequence (runs from codexStart())
*/
int init();
/**
* Frees all memory associated with the Codex sequence
*/
void deinit();
/**
* Ejects you from the chamber of the codex (and the Abyss, for that matter)
* with the correct message.
*/
void eject(CodexEjectCode code);
/**
* Handles entering the Word of Passage
*/
void handleWOP(const Common::String &word);
/**
* Handles naming of virtues in the Chamber of the Codex
*/
void handleVirtues(const Common::String &virtue);
void handleInfinity(const Common::String &answer);
/**
* Pretty self-explanatory
*/
void impureThoughts();
/**
* Key handlers
*/
static bool handleInfinityAnyKey(int key, void *data);
static bool handleEndgameAnyKey(int key, void *data);
public:
/**
* Constructor
*/
Codex();
/**
* Destructor
*/
~Codex();
/**
* Begins the Chamber of the Codex sequence
*/
void start();
};
extern Codex *g_codex;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,70 @@
/* 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/game/context.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/map/location.h"
namespace Ultima {
namespace Ultima4 {
Context *g_context;
Context::Context() : _stats(nullptr), _aura(nullptr),
_party(nullptr), _location(nullptr) {
g_context = this;
reset();
}
Context::~Context() {
g_context = nullptr;
reset();
}
void Context::reset() {
delete _stats;
delete _aura;
delete _party;
while (_location)
locationFree(&_location);
_stats = nullptr;
_aura = nullptr;
_party = nullptr;
_location = nullptr;
_lastShip = nullptr;
_line = 9999;
_col = 0;
_moonPhase = 0;
_windDirection = 0;
_windCounter = 0;
_windLock = false;
_horseSpeed = 0;
_opacity = 0;
_lastCommandTime = 0;
_transportContext = TRANSPORT_ANY;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,97 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_CONTEXT_H
#define ULTIMA4_GAME_CONTEXT_H
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/game/aura.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/person.h"
#include "ultima/ultima4/game/script.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/shared/std/containers.h"
namespace Ultima {
namespace Ultima4 {
class Object;
class Party;
class Person;
class Script;
class StatsArea;
enum TransportContext {
TRANSPORT_FOOT = 0x1,
TRANSPORT_HORSE = 0x2,
TRANSPORT_SHIP = 0x4,
TRANSPORT_BALLOON = 0x8,
TRANSPORT_FOOT_OR_HORSE = TRANSPORT_FOOT | TRANSPORT_HORSE,
TRANSPORT_ANY = 0xffff
};
/**
* Context class
*/
class Context : public Script::Provider {
public:
Context();
~Context();
/**
* Reset the context
*/
void reset();
StatsArea *_stats;
Aura *_aura;
Party *_party;
Location *_location;
int _line, _col;
int _moonPhase;
int _windDirection;
int _windCounter;
bool _windLock;
int _horseSpeed;
int _opacity;
TransportContext _transportContext;
uint32 _lastCommandTime;
Object *_lastShip;
public:
/**
* Provides scripts with information
*/
Common::String translate(Std::vector<Common::String> &parts) override {
if (parts.size() == 1) {
if (parts[0] == "wind")
return getDirectionName(static_cast<Direction>(_windDirection));
}
return "";
}
};
extern Context *g_context;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_CREATURE_H
#define ULTIMA4_GAME_CREATURE_H
#include "ultima/ultima4/game/object.h"
#include "ultima/ultima4/map/movement.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/core/types.h"
namespace Ultima {
namespace Ultima4 {
class CombatController;
class ConfigElement;
class Tile;
typedef unsigned short CreatureId;
typedef Common::HashMap<CreatureId, class Creature *> CreatureMap;
typedef Std::vector<class Creature *> CreatureVector;
#define MAX_CREATURES 128
/* Creatures on world map */
#define MAX_CREATURES_ON_MAP 4
#define MAX_CREATURE_DISTANCE 16
/* Creature ids */
typedef enum {
HORSE1_ID = 0,
HORSE2_ID = 1,
MAGE_ID = 2,
BARD_ID = 3,
FIGHTER_ID = 4,
DRUID_ID = 5,
TINKER_ID = 6,
PALADIN_ID = 7,
RANGER_ID = 8,
SHEPHERD_ID = 9,
GUARD_ID = 10,
VILLAGER_ID = 11,
SINGINGBARD_ID = 12,
JESTER_ID = 13,
BEGGAR_ID = 14,
CHILD_ID = 15,
BULL_ID = 16,
LORDBRITISH_ID = 17,
PIRATE_ID = 18,
NIXIE_ID = 19,
GIANT_SQUID_ID = 20,
SEA_SERPENT_ID = 21,
SEAHORSE_ID = 22,
WHIRLPOOL_ID = 23,
STORM_ID = 24,
RAT_ID = 25,
BAT_ID = 26,
GIANT_SPIDER_ID = 27,
GHOST_ID = 28,
SLIME_ID = 29,
TROLL_ID = 30,
GREMLIN_ID = 31,
MIMIC_ID = 32,
REAPER_ID = 33,
INSECT_SWARM_ID = 34,
GAZER_ID = 35,
PHANTOM_ID = 36,
ORC_ID = 37,
SKELETON_ID = 38,
ROGUE_ID = 39,
PYTHON_ID = 40,
ETTIN_ID = 41,
HEADLESS_ID = 42,
CYCLOPS_ID = 43,
WISP_ID = 44,
EVILMAGE_ID = 45,
LICH_ID = 46,
LAVA_LIZARD_ID = 47,
ZORN_ID = 48,
DAEMON_ID = 49,
HYDRA_ID = 50,
DRAGON_ID = 51,
BALRON_ID = 52
} CreatureType;
typedef enum {
MATTR_STEALFOOD = 0x1,
MATTR_STEALGOLD = 0x2,
MATTR_CASTS_SLEEP = 0x4,
MATTR_UNDEAD = 0x8,
MATTR_GOOD = 0x10,
MATTR_WATER = 0x20,
MATTR_NONATTACKABLE = 0x40,
MATTR_NEGATE = 0x80,
MATTR_CAMOUFLAGE = 0x100,
MATTR_NOATTACK = 0x200,
MATTR_AMBUSHES = 0x400,
MATTR_RANDOMRANGED = 0x800,
MATTR_INCORPOREAL = 0x1000,
MATTR_NOCHEST = 0x2000,
MATTR_DIVIDES = 0x4000,
MATTR_SPAWNSONDEATH = 0x8000,
MATTR_FORCE_OF_NATURE = 0x10000
} CreatureAttrib;
typedef enum {
MATTR_STATIONARY = 0x1,
MATTR_WANDERS = 0x2,
MATTR_SWIMS = 0x4,
MATTR_SAILS = 0x8,
MATTR_FLIES = 0x10,
MATTR_TELEPORT = 0x20,
MATTR_CANMOVECREATURES = 0x40,
MATTR_CANMOVEAVATAR = 0x80
} CreatureMovementAttrib;
typedef enum {
MSTAT_DEAD,
MSTAT_FLEEING,
MSTAT_CRITICAL,
MSTAT_HEAVILYWOUNDED,
MSTAT_LIGHTLYWOUNDED,
MSTAT_BARELYWOUNDED
} CreatureStatus;
/**
* Creature Class Definition
* @todo
* <ul>
* <li>split into a CreatureType (all the settings for a
* particular creature e.g. orc) and Creature (a specific
* creature instance)</li>
* <li>creatures can be looked up by name, ids can probably go away</li>
* </ul>
*/
class Creature : public Object {
typedef Common::List<StatusType> StatusList;
public:
/**
* Creature class implementation
*/
Creature(MapTile tile = MapTile(0));
void load(const ConfigElement &conf);
// Accessor methods
virtual Common::String getName() const {
return _name;
}
virtual const Common::String &getHitTile() const {
return _rangedHitTile;
}
virtual const Common::String &getMissTile() const {
return _rangedMissTile;
}
CreatureId getId() const {
return _id;
}
CreatureId getLeader() const {
return _leader;
}
virtual int getHp() const {
return _hp;
}
virtual int getXp() const {
return _xp;
}
virtual const Common::String &getWorldrangedtile() const {
return _worldRangedTile;
}
SlowedType getSlowedType() const {
return _slowedType;
}
int getEncounterSize() const {
return _encounterSize;
}
byte getResists() const {
return _resists;
}
// Setters
void setName(Common::String s) {
_name = s;
}
void setHitTile(const Common::String &t) {
_rangedHitTile = t;
}
void setMissTile(const Common::String &t) {
_rangedMissTile = t;
}
virtual void setHp(int points) {
_hp = points;
}
// Query methods
bool isGood() const {
return _mAttr & MATTR_GOOD;
}
bool isEvil() const {
return !isGood();
}
bool isUndead() const {
return _mAttr & MATTR_UNDEAD;
}
bool leavesChest() const {
return !isAquatic() && !(_mAttr & MATTR_NOCHEST);
}
bool isAquatic() const {
return _mAttr & MATTR_WATER;
}
bool wanders() const {
return _movementAttr & MATTR_WANDERS;
}
bool isStationary() const {
return _movementAttr & MATTR_STATIONARY;
}
bool flies() const {
return _movementAttr & MATTR_FLIES;
}
bool teleports() const {
return _movementAttr & MATTR_TELEPORT;
}
bool swims() const {
return _movementAttr & MATTR_SWIMS;
}
bool sails() const {
return _movementAttr & MATTR_SAILS;
}
bool walks() const {
return !(flies() || swims() || sails());
}
bool divides() const {
return _mAttr & MATTR_DIVIDES;
}
bool spawnsOnDeath() const {
return _mAttr & MATTR_SPAWNSONDEATH;
}
bool canMoveOntoCreatures() const {
return _movementAttr & MATTR_CANMOVECREATURES;
}
bool canMoveOntoPlayer() const {
return _movementAttr & MATTR_CANMOVEAVATAR;
}
bool isAttackable() const;
bool willAttack() const {
return !(_mAttr & MATTR_NOATTACK);
}
bool stealsGold() const {
return _mAttr & MATTR_STEALGOLD;
}
bool stealsFood() const {
return _mAttr & MATTR_STEALFOOD;
}
bool negates() const {
return _mAttr & MATTR_NEGATE;
}
bool camouflages() const {
return _mAttr & MATTR_CAMOUFLAGE;
}
bool ambushes() const {
return _mAttr & MATTR_AMBUSHES;
}
bool isIncorporeal() const {
return _mAttr & MATTR_INCORPOREAL;
}
bool hasRandomRanged() const {
return _mAttr & MATTR_RANDOMRANGED;
}
bool leavesTile() const {
return _leavesTile;
}
bool castsSleep() const {
return _mAttr & MATTR_CASTS_SLEEP;
}
bool isForceOfNature() const {
return _mAttr & MATTR_FORCE_OF_NATURE;
}
int getDamage() const;
const Common::String &getCamouflageTile() const {
return _camouflageTile;
}
void setRandomRanged();
int setInitialHp(int hp = -1);
/**
* Performs a special action for the creature
* Returns true if the action takes up the creatures
* whole turn (i.e. it can't move afterwards)
*/
bool specialAction();
/**
* Performs a special effect for the creature
* Returns true if something special happened,
* or false if nothing happened
*/
bool specialEffect();
/* combat methods */
void act(CombatController *controller);
/**
* Add status effects to the creature, in order of importance
*/
virtual void addStatus(StatusType status);
void applyTileEffect(TileEffect effect);
virtual int getAttackBonus() const;
virtual int getDefense() const;
bool divide();
bool spawnOnDeath();
virtual CreatureStatus getState() const;
StatusType getStatus() const;
bool isAsleep() const;
/**
* Hides or shows a camouflaged creature, depending on its distance from
* the nearest opponent
*/
bool hideOrShow();
Creature *nearestOpponent(int *dist, bool ranged);
virtual void putToSleep();
virtual void removeStatus(StatusType status);
virtual void setStatus(StatusType status);
virtual void wakeUp();
/**
* Applies damage to the creature.
* Returns true if the creature still exists after the damage has been applied
* or false, if the creature was destroyed
*
* If byplayer is false (when a monster is killed by walking through
* fire or poison, or as a result of jinx) we don't report experience
* on death
*/
virtual bool applyDamage(int damage, bool byplayer = true);
virtual bool dealDamage(Creature *m, int damage);
// Properties
protected:
Common::String _name;
Common::String _rangedHitTile;
Common::String _rangedMissTile;
CreatureId _id;
Common::String _camouflageTile;
CreatureId _leader;
int _baseHp;
int _hp;
StatusList _status;
int _xp;
byte _ranged;
Common::String _worldRangedTile;
bool _leavesTile;
CreatureAttrib _mAttr;
CreatureMovementAttrib _movementAttr;
SlowedType _slowedType;
int _encounterSize;
byte _resists;
CreatureId _spawn;
};
/**
* CreatureMgr Class Definition
*/
class CreatureMgr {
public:
static CreatureMgr *getInstance();
void loadAll();
/**
* Returns a creature using a tile to find which one to create
* or nullptr if a creature with that tile cannot be found
*/
Creature *getByTile(MapTile tile);
/**
* Returns the creature that has the corresponding id
* or returns nullptr if no creature with that id could
* be found.
*/
Creature *getById(CreatureId id);
/**
* Returns the creature that has the corresponding name
* or returns nullptr if no creature can be found with
* that name (case insensitive)
*/
Creature *getByName(Common::String name);
/**
* Creates a random creature based on the tile given
*/
Creature *randomForTile(const Tile *tile);
/**
* Creates a random creature based on the dungeon level given
*/
Creature *randomForDungeon(int dnglevel);
/**
* Creates a random ambushing creature
*/
Creature *randomAmbushing();
private:
CreatureMgr() {}
// disallow assignments, copy construction
CreatureMgr(const CreatureMgr &);
const CreatureMgr &operator=(const CreatureMgr &);
static CreatureMgr *_instance;
CreatureMap _creatures;
};
bool isCreature(Object *punknown);
#define creatureMgr (CreatureMgr::getInstance())
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,148 @@
/* 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/game/death.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/game/portal.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/controllers/wait_controller.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/sound/music.h"
#include "ultima/ultima4/ultima4.h"
#include "common/system.h"
namespace Ultima {
namespace Ultima4 {
Death *g_death;
#define REVIVE_WORLD_X 86
#define REVIVE_WORLD_Y 107
#define REVIVE_CASTLE_X 19
#define REVIVE_CASTLE_Y 8
const struct {
int _timeout; ///< pause in seconds
const char *_text; ///< text of message
} DEATH_MSGS[] = {
{ 5, "\n\n\nAll is Dark...\n" },
{ 5, "\nBut wait...\n" },
{ 5, "Where am I?...\n" },
{ 5, "Am I dead?...\n" },
{ 5, "Afterlife?...\n" },
{ 5, "You hear:\n %s\n" },
{ 5, "I feel motion...\n" },
{ 5, "\nLord British says: I have pulled thy spirit and some possessions from the void. Be more careful in the future!\n\n\020" }
};
#define N_MSGS (sizeof(DEATH_MSGS) / sizeof(DEATH_MSGS[0]))
Death::Death() : timerCount(0), timerMsg(0), deathSequenceRunning(false) {
g_death = this;
}
Death::~Death() {
g_death = nullptr;
}
void Death::start(int delay) {
if (deathSequenceRunning)
return;
// stop playing music
g_music->fadeOut(1000);
deathSequenceRunning = 1;
timerCount = 0;
timerMsg = 0;
WaitController waitCtrl(delay * settings._gameCyclesPerSecond);
eventHandler->pushController(&waitCtrl);
waitCtrl.wait();
gameSetViewMode(VIEW_DEAD);
eventHandler->pushKeyHandler(&KeyHandler::ignoreKeys);
g_screen->screenDisableCursor();
eventHandler->getTimer()->add(&deathTimer, settings._gameCyclesPerSecond);
}
void Death::deathTimer(void *data) {
g_death->timerCount++;
if ((g_death->timerMsg < N_MSGS) && (g_death->timerCount > DEATH_MSGS[g_death->timerMsg]._timeout)) {
g_screen->screenMessage(DEATH_MSGS[g_death->timerMsg]._text, g_context->_party->member(0)->getName().c_str());
g_screen->screenHideCursor();
g_death->timerCount = 0;
g_death->timerMsg++;
if (g_death->timerMsg >= N_MSGS) {
eventHandler->getTimer()->remove(&deathTimer);
g_death->revive();
}
}
}
void Death::revive() {
while (!g_context->_location->_map->isWorldMap() && g_context->_location->_prev != nullptr) {
g_game->exitToParentMap();
}
eventHandler->setController(g_game);
deathSequenceRunning = false;
gameSetViewMode(VIEW_NORMAL);
// Move our world map location to Lord British's Castle
g_context->_location->_coords = g_context->_location->_map->_portals[0]->_coords;
// Now, move the avatar into the castle and put him in front of Lord British
g_game->setMap(mapMgr->get(100), 1, nullptr);
g_context->_location->_coords.x = REVIVE_CASTLE_X;
g_context->_location->_coords.y = REVIVE_CASTLE_Y;
g_context->_location->_coords.z = 0;
g_context->_aura->set();
g_context->_horseSpeed = 0;
g_context->_lastCommandTime = g_system->getMillis();
g_music->playMapMusic();
g_context->_party->reviveParty();
g_screen->screenEnableCursor();
g_screen->screenShowCursor();
g_context->_stats->setView(STATS_PARTY_OVERVIEW);
g_screen->update();
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,61 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_DEATH_H
#define ULTIMA4_GAME_DEATH_H
#include "common/scummsys.h"
namespace Ultima {
namespace Ultima4 {
class Death {
private:
int timerCount;
uint timerMsg;
bool deathSequenceRunning;
private:
/**
* Timer
*/
static void deathTimer(void *data);
void revive();
public:
/**
* Constructor
*/
Death();
/**
* Destructor
*/
~Death();
void start(int delay);
};
extern Death *g_death;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,831 @@
/* 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/ultima4.h"
#include "ultima/ultima4/controllers/camp_controller.h"
#include "ultima/ultima4/controllers/combat_controller.h"
#include "ultima/ultima4/controllers/intro_controller.h"
#include "ultima/ultima4/controllers/read_choice_controller.h"
#include "ultima/ultima4/controllers/read_dir_controller.h"
#include "ultima/ultima4/controllers/read_int_controller.h"
#include "ultima/ultima4/controllers/read_player_controller.h"
#include "ultima/ultima4/controllers/read_string_controller.h"
#include "ultima/ultima4/conversation/conversation.h"
#include "ultima/ultima4/core/config.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/filesys/savegame.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/armor.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/death.h"
#include "ultima/ultima4/game/item.h"
#include "ultima/ultima4/views/menu.h"
#include "ultima/ultima4/game/creature.h"
#include "ultima/ultima4/game/moongate.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/person.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/script.h"
#include "ultima/ultima4/game/weapon.h"
#include "ultima/ultima4/gfx/imagemgr.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/dungeon.h"
#include "ultima/ultima4/map/direction.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/map/movement.h"
#include "ultima/ultima4/map/shrine.h"
#include "ultima/ultima4/map/tilemap.h"
#include "ultima/ultima4/map/tileset.h"
#include "ultima/ultima4/sound/music.h"
#include "ultima/ultima4/sound/sound.h"
#include "ultima/ultima4/views/dungeonview.h"
#include "ultima/ultima4/metaengine.h"
#include "common/savefile.h"
#include "common/system.h"
namespace Ultima {
namespace Ultima4 {
/*-----------------*/
/* Functions BEGIN */
/* main game functions */
void gameAdvanceLevel(PartyMember *player);
void gameInnHandler();
void gameLostEighth(Virtue virtue);
void gamePartyStarving();
void mixReagentsSuper();
/* action functions */
void wearArmor(int player = -1);
/* Functions END */
/*---------------*/
void gameSetViewMode(ViewMode newMode) {
g_context->_location->_viewMode = newMode;
}
void gameUpdateScreen() {
switch (g_context->_location->_viewMode) {
case VIEW_NORMAL:
g_screen->screenUpdate(&g_game->_mapArea, true, false);
break;
case VIEW_GEM:
g_screen->screenGemUpdate();
break;
case VIEW_RUNE:
g_screen->screenUpdate(&g_game->_mapArea, false, false);
break;
case VIEW_DUNGEON:
g_screen->screenUpdate(&g_game->_mapArea, true, false);
break;
case VIEW_DEAD:
g_screen->screenUpdate(&g_game->_mapArea, true, true);
break;
case VIEW_CODEX: /* the screen updates will be handled elsewhere */
break;
case VIEW_MIXTURES: /* still testing */
break;
default:
error("invalid view mode: %d", g_context->_location->_viewMode);
}
}
void gameSpellEffect(int spell, int player, Sound sound) {
int time;
Spell::SpecialEffects effect = Spell::SFX_INVERT;
if (player >= 0)
g_context->_stats->highlightPlayer(player);
time = settings._spellEffectSpeed * 800 / settings._gameCyclesPerSecond;
soundPlay(sound, false, time);
///The following effect multipliers are not accurate
switch (spell) {
case 'g': /* gate */
case 'r': /* resurrection */
break;
case 't': /* tremor */
effect = Spell::SFX_TREMOR;
break;
default:
/* default spell effect */
break;
}
switch (effect) {
case Spell::SFX_TREMOR:
case Spell::SFX_INVERT:
gameUpdateScreen();
g_game->_mapArea.highlight(0, 0, VIEWPORT_W * TILE_WIDTH, VIEWPORT_H * TILE_HEIGHT);
g_screen->update();
EventHandler::sleep(time);
g_game->_mapArea.unhighlight();
g_screen->update();
if (effect == Spell::SFX_TREMOR) {
gameUpdateScreen();
g_screen->update();
soundPlay(SOUND_RUMBLE, false);
g_screen->screenShake(8);
}
break;
default:
break;
}
}
Common::String gameGetInput(int maxlen) {
g_screen->screenEnableCursor();
g_screen->screenShowCursor();
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper helper;
helper.beginConversation(U4IOS::UIKeyboardTypeDefault);
#endif
return ReadStringController::get(maxlen, TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
}
int gameGetPlayer(bool canBeDisabled, bool canBeActivePlayer) {
int player;
if (g_ultima->_saveGame->_members <= 1) {
player = 0;
} else {
if (canBeActivePlayer && (g_context->_party->getActivePlayer() >= 0)) {
player = g_context->_party->getActivePlayer();
} else {
ReadPlayerController readPlayerController;
eventHandler->pushController(&readPlayerController);
player = readPlayerController.waitFor();
}
if (player == -1) {
g_screen->screenMessage("None\n");
return -1;
}
}
g_context->_col--;// display the selected character name, in place of the number
if ((player >= 0) && (player < 8)) {
g_screen->screenMessage("%s\n", g_ultima->_saveGame->_players[player]._name); //Write player's name after prompt
}
if (!canBeDisabled && g_context->_party->member(player)->isDisabled()) {
g_screen->screenMessage("%cDisabled!%c\n", FG_GREY, FG_WHITE);
return -1;
}
assertMsg(player < g_context->_party->size(), "player %d, but only %d members\n", player, g_context->_party->size());
return player;
}
Direction gameGetDirection() {
ReadDirController dirController;
g_screen->screenMessage("Dir?");
#ifdef IOS_ULTIMA4
U4IOS::IOSDirectionHelper directionPopup;
#endif
eventHandler->pushController(&dirController);
Direction dir = dirController.waitFor();
g_screen->screenMessage("\b\b\b\b");
if (dir == DIR_NONE) {
g_screen->screenMessage(" \n");
return dir;
} else {
g_screen->screenMessage("%s\n", getDirectionName(dir));
return dir;
}
}
bool fireAt(const Coords &coords, bool originAvatar) {
bool validObject = false;
bool hitsAvatar = false;
bool objectHit = false;
Object *obj = nullptr;
MapTile tile(g_context->_location->_map->_tileSet->getByName("miss_flash")->getId());
GameController::flashTile(coords, tile, 1);
obj = g_context->_location->_map->objectAt(coords);
Creature *m = dynamic_cast<Creature *>(obj);
if (obj && obj->getType() == Object::CREATURE && m && m->isAttackable())
validObject = true;
/* See if it's an object to be destroyed (the avatar cannot destroy the balloon) */
else if (obj &&
(obj->getType() == Object::UNKNOWN) &&
!(obj->getTile().getTileType()->isBalloon() && originAvatar))
validObject = true;
/* Does the cannon hit the avatar? */
if (coords == g_context->_location->_coords) {
validObject = true;
hitsAvatar = true;
}
if (validObject) {
/* always displays as a 'hit' though the object may not be destroyed */
/* Is is a pirate ship firing at US? */
if (hitsAvatar) {
GameController::flashTile(coords, "hit_flash", 4);
if (g_context->_transportContext == TRANSPORT_SHIP)
gameDamageShip(-1, 10);
else gameDamageParty(10, 25); /* party gets hurt between 10-25 damage */
}
/* inanimate objects get destroyed instantly, while creatures get a chance */
else if (obj->getType() == Object::UNKNOWN) {
GameController::flashTile(coords, "hit_flash", 4);
g_context->_location->_map->removeObject(obj);
}
/* only the avatar can hurt other creatures with cannon fire */
else if (originAvatar) {
GameController::flashTile(coords, "hit_flash", 4);
if (xu4_random(4) == 0) /* reverse-engineered from u4dos */
g_context->_location->_map->removeObject(obj);
}
objectHit = true;
}
return objectHit;
}
bool gamePeerCity(int city, void *data) {
Map *peerMap;
peerMap = mapMgr->get((MapId)(city + 1));
if (peerMap != nullptr) {
g_game->setMap(peerMap, 1, nullptr);
g_context->_location->_viewMode = VIEW_GEM;
g_game->_paused = true;
g_game->_pausedTimer = 0;
g_screen->screenDisableCursor();
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationChoiceHelper continueHelper;
continueHelper.updateChoices(" ");
continueHelper.fullSizeChoicePanel();
#endif
ReadChoiceController::get("\015 \033");
g_game->exitToParentMap();
g_screen->screenEnableCursor();
g_game->_paused = false;
return true;
}
return false;
}
void peer(bool useGem) {
if (useGem) {
if (g_ultima->_saveGame->_gems <= 0) {
g_screen->screenMessage("%cPeer at What?%c\n", FG_GREY, FG_WHITE);
return;
}
g_ultima->_saveGame->_gems--;
g_screen->screenMessage("Peer at a Gem!\n");
}
g_game->_paused = true;
g_game->_pausedTimer = 0;
g_screen->screenDisableCursor();
g_context->_location->_viewMode = VIEW_GEM;
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationChoiceHelper continueHelper;
continueHelper.updateChoices(" ");
continueHelper.fullSizeChoicePanel();
#endif
ReadChoiceController::get("\015 \033");
g_screen->screenEnableCursor();
g_context->_location->_viewMode = VIEW_NORMAL;
g_game->_paused = false;
}
void gameCheckHullIntegrity() {
int i;
bool killAll = false;
/* see if the ship has sunk */
if ((g_context->_transportContext == TRANSPORT_SHIP) && g_ultima->_saveGame->_shipHull <= 0) {
g_screen->screenMessage("\nThy ship sinks!\n\n");
killAll = true;
}
if (!g_debugger->_collisionOverride && g_context->_transportContext == TRANSPORT_FOOT &&
g_context->_location->_map->tileTypeAt(g_context->_location->_coords, WITHOUT_OBJECTS)->isSailable() &&
!g_context->_location->_map->tileTypeAt(g_context->_location->_coords, WITH_GROUND_OBJECTS)->isShip() &&
!g_context->_location->_map->getValidMoves(g_context->_location->_coords, g_context->_party->getTransport())) {
g_screen->screenMessage("\nTrapped at sea without thy ship, thou dost drown!\n\n");
killAll = true;
}
if (killAll) {
for (i = 0; i < g_context->_party->size(); i++) {
g_context->_party->member(i)->setHp(0);
g_context->_party->member(i)->setStatus(STAT_DEAD);
}
g_screen->update();
g_death->start(5);
}
}
void gameFixupObjects(Map *map) {
int i;
Object *obj;
/* add stuff from the monster table to the map */
for (i = 0; i < MONSTERTABLE_SIZE; i++) {
SaveGameMonsterRecord *monster = &map->_monsterTable[i];
if (monster->_prevTile != 0) {
Coords coords(monster->_x, monster->_y);
// tile values stored in monsters.sav hardcoded to index into base tilemap
MapTile tile = g_tileMaps->get("base")->translate(monster->_tile),
oldTile = g_tileMaps->get("base")->translate(monster->_prevTile);
if (i < MONSTERTABLE_CREATURES_SIZE) {
const Creature *creature = creatureMgr->getByTile(tile);
/* make sure we really have a creature */
if (creature)
obj = map->addCreature(creature, coords);
else {
warning("A non-creature object was found in the creature section of the monster table. (Tile: %s)\n", tile.getTileType()->getName().c_str());
obj = map->addObject(tile, oldTile, coords);
}
} else {
obj = map->addObject(tile, oldTile, coords);
}
/* set the map for our object */
obj->setMap(map);
}
}
}
uint32 gameTimeSinceLastCommand() {
return (g_system->getMillis() - g_context->_lastCommandTime) / 1000;
}
void gameCreatureAttack(Creature *m) {
Object *under;
const Tile *ground;
g_screen->screenMessage("\nAttacked by %s\n", m->getName().c_str());
/// TODO: CHEST: Make a user option to not make chests change battlefield
/// map (2 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();
}
CombatController *cc = new CombatController(CombatMap::mapForTile(ground, g_context->_party->getTransport().getTileType(), m));
cc->init(m);
cc->begin();
}
bool creatureRangeAttack(const Coords &coords, Creature *m) {
// int attackdelay = MAX_BATTLE_SPEED - settings.battleSpeed;
// Figure out what the ranged attack should look like
MapTile tile(g_context->_location->_map->_tileSet->getByName((m && !m->getWorldrangedtile().empty()) ?
m->getWorldrangedtile() :
"hit_flash")->getId());
GameController::flashTile(coords, tile, 1);
// See if the attack hits the avatar
Object *obj = g_context->_location->_map->objectAt(coords);
m = dynamic_cast<Creature *>(obj);
// Does the attack hit the avatar?
if (coords == g_context->_location->_coords) {
/* always displays as a 'hit' */
GameController::flashTile(coords, tile, 3);
/* FIXME: check actual damage from u4dos -- values here are guessed */
if (g_context->_transportContext == TRANSPORT_SHIP)
gameDamageShip(-1, 10);
else
gameDamageParty(10, 25);
return true;
}
// Destroy objects that were hit
else if (obj) {
if ((obj->getType() == Object::CREATURE && m && m->isAttackable()) ||
obj->getType() == Object::UNKNOWN) {
GameController::flashTile(coords, tile, 3);
g_context->_location->_map->removeObject(obj);
return true;
}
}
return false;
}
Std::vector<Coords> gameGetDirectionalActionPath(int dirmask, int validDirections, const Coords &origin, int minDistance, int maxDistance, bool (*blockedPredicate)(const Tile *tile), bool includeBlocked) {
Std::vector<Coords> path;
Direction dirx = DIR_NONE,
diry = DIR_NONE;
/* Figure out which direction the action is going */
if (DIR_IN_MASK(DIR_WEST, dirmask))
dirx = DIR_WEST;
else if (DIR_IN_MASK(DIR_EAST, dirmask))
dirx = DIR_EAST;
if (DIR_IN_MASK(DIR_NORTH, dirmask))
diry = DIR_NORTH;
else if (DIR_IN_MASK(DIR_SOUTH, dirmask))
diry = DIR_SOUTH;
/*
* try every tile in the given direction, up to the given range.
* Stop when the range is exceeded, or the action is blocked.
*/
MapCoords t_c(origin);
if ((dirx <= 0 || DIR_IN_MASK(dirx, validDirections)) &&
(diry <= 0 || DIR_IN_MASK(diry, validDirections))) {
for (int distance = 0; distance <= maxDistance;
distance++, t_c.move(dirx, g_context->_location->_map), t_c.move(diry, g_context->_location->_map)) {
if (distance >= minDistance) {
/* make sure our action isn't taking us off the map */
if (MAP_IS_OOB(g_context->_location->_map, t_c))
break;
const Tile *tile = g_context->_location->_map->tileTypeAt(t_c, WITH_GROUND_OBJECTS);
/* should we see if the action is blocked before trying it? */
if (!includeBlocked && blockedPredicate &&
!(*(blockedPredicate))(tile))
break;
path.push_back(t_c);
/* see if the action was blocked only if it did not succeed */
if (includeBlocked && blockedPredicate &&
!(*(blockedPredicate))(tile))
break;
}
}
}
return path;
}
void gameDamageParty(int minDamage, int maxDamage) {
int i;
int damage;
int lastdmged = -1;
for (i = 0; i < g_context->_party->size(); i++) {
if (xu4_random(2) == 0) {
damage = ((minDamage >= 0) && (minDamage < maxDamage)) ?
xu4_random((maxDamage + 1) - minDamage) + minDamage :
maxDamage;
g_context->_party->member(i)->applyDamage(damage);
g_context->_stats->highlightPlayer(i);
lastdmged = i;
EventHandler::sleep(50);
}
}
g_screen->screenShake(1);
// Un-highlight the last player
if (lastdmged != -1) g_context->_stats->highlightPlayer(lastdmged);
}
void gameDamageShip(int minDamage, int maxDamage) {
int damage;
if (g_context->_transportContext == TRANSPORT_SHIP) {
damage = ((minDamage >= 0) && (minDamage < maxDamage)) ?
xu4_random((maxDamage + 1) - minDamage) + minDamage :
maxDamage;
g_screen->screenShake(1);
g_context->_party->damageShip(damage);
gameCheckHullIntegrity();
}
}
void gameSetActivePlayer(int player) {
if (player == -1) {
g_context->_party->setActivePlayer(-1);
g_screen->screenMessage("Set Active Player: None!\n");
} else if (player < g_context->_party->size()) {
g_screen->screenMessage("Set Active Player: %s!\n", g_context->_party->member(player)->getName().c_str());
if (g_context->_party->member(player)->isDisabled())
g_screen->screenMessage("Disabled!\n");
else
g_context->_party->setActivePlayer(player);
}
}
bool gameSpawnCreature(const Creature *m) {
int t, i;
const Creature *creature;
MapCoords coords = g_context->_location->_coords;
if (g_context->_location->_context & CTX_DUNGEON) {
/* FIXME: for some reason dungeon monsters aren't spawning correctly */
bool found = false;
MapCoords new_coords;
for (i = 0; i < 0x20; i++) {
new_coords = MapCoords(xu4_random(g_context->_location->_map->_width), xu4_random(g_context->_location->_map->_height), coords.z);
const Tile *tile = g_context->_location->_map->tileTypeAt(new_coords, WITH_OBJECTS);
if (tile->isCreatureWalkable()) {
found = true;
break;
}
}
if (!found)
return false;
coords = new_coords;
} else {
int dx = 0,
dy = 0;
bool ok = false;
int tries = 0;
static const int MAX_TRIES = 10;
while (!ok && (tries < MAX_TRIES)) {
dx = 7;
dy = xu4_random(7);
if (xu4_random(2))
dx = -dx;
if (xu4_random(2))
dy = -dy;
if (xu4_random(2)) {
t = dx;
dx = dy;
dy = t;
}
/* make sure we can spawn the creature there */
if (m) {
MapCoords new_coords = coords;
new_coords.move(dx, dy, g_context->_location->_map);
const Tile *tile = g_context->_location->_map->tileTypeAt(new_coords, WITHOUT_OBJECTS);
if ((m->sails() && tile->isSailable()) ||
(m->swims() && tile->isSwimable()) ||
(m->walks() && tile->isCreatureWalkable()) ||
(m->flies() && tile->isFlyable()))
ok = true;
else
tries++;
} else {
ok = true;
}
}
if (ok)
coords.move(dx, dy, g_context->_location->_map);
}
/* can't spawn creatures on top of the player */
if (coords == g_context->_location->_coords)
return false;
/* figure out what creature to spawn */
if (m)
creature = m;
else if (g_context->_location->_context & CTX_DUNGEON)
creature = creatureMgr->randomForDungeon(g_context->_location->_coords.z);
else
creature = creatureMgr->randomForTile(g_context->_location->_map->tileTypeAt(coords, WITHOUT_OBJECTS));
if (creature)
g_context->_location->_map->addCreature(creature, coords);
return true;
}
void gameDestroyAllCreatures(void) {
int i;
gameSpellEffect('t', -1, SOUND_MAGIC); /* same effect as tremor */
if (g_context->_location->_context & CTX_COMBAT) {
// Destroy all creatures in combat
for (i = 0; i < AREA_CREATURES; i++) {
CombatMap *cm = getCombatMap();
CreatureVector creatures = cm->getCreatures();
for (const auto *obj : creatures) {
if (obj->getId() != LORDBRITISH_ID)
cm->removeObject(obj);
}
}
} else {
// Destroy all creatures on the map
ObjectDeque::iterator current;
Map *map = g_context->_location->_map;
for (current = map->_objects.begin(); current != map->_objects.end();) {
Creature *m = dynamic_cast<Creature *>(*current);
if (m) {
// The skull does not destroy Lord British
if (m->getId() != LORDBRITISH_ID)
current = map->removeObject(current);
else
current++;
} else {
current++;
}
}
}
// Alert the guards! Really, the only one left should be LB himself :)
g_context->_location->_map->alertGuards();
}
// Colors assigned to reagents based on my best reading of them
// from the book of wisdom. Maybe we could use BOLD to distinguish
// the two grey and the two red reagents.
const int colors[] = {
FG_YELLOW, FG_GREY, FG_BLUE, FG_WHITE, FG_RED, FG_GREY, FG_GREEN, FG_RED
};
void showMixturesSuper(int page = 0) {
g_screen->screenTextColor(FG_WHITE);
for (int i = 0; i < 13; i++) {
const Spell *s = g_spells->getSpell(i + 13 * page);
int line = i + 8;
g_screen->screenTextAt(2, line, "%s", s->_name);
g_screen->screenTextAt(6, line, "%s", Common::String::format("%3d", g_ultima->_saveGame->_mixtures[i + 13 * page]).c_str());
g_screen->screenShowChar(32, 9, line);
int comp = s->_components;
for (int j = 0; j < 8; j++) {
g_screen->screenTextColor(colors[j]);
g_screen->screenShowChar(comp & (1 << j) ? CHARSET_BULLET : ' ', 10 + j, line);
}
g_screen->screenTextColor(FG_WHITE);
g_screen->screenTextAt(19, line, "%s", Common::String::format("%2d", s->_mp).c_str());
}
}
void mixReagentsSuper() {
g_screen->screenMessage("Mix reagents\n");
static int page = 0;
struct ReagentShop {
const char *name;
int price[6];
};
ReagentShop shops[] = {
{ "BuccDen", {6, 7, 9, 9, 9, 1} },
{ "Moonglo", {2, 5, 6, 3, 6, 9} },
{ "Paws", {3, 4, 2, 8, 6, 7} },
{ "SkaraBr", {2, 4, 9, 6, 4, 8} },
};
const int shopcount = sizeof(shops) / sizeof(shops[0]);
int oldlocation = g_context->_location->_viewMode;
g_context->_location->_viewMode = VIEW_MIXTURES;
g_screen->screenUpdate(&g_game->_mapArea, true, true);
g_screen->screenTextAt(16, 2, "%s", "<-Shops");
g_context->_stats->setView(StatsView(STATS_REAGENTS));
g_screen->screenTextColor(FG_PURPLE);
g_screen->screenTextAt(2, 7, "%s", "SPELL # Reagents MP");
for (int i = 0; i < shopcount; i++) {
int line = i + 1;
ReagentShop *s = &shops[i];
g_screen->screenTextColor(FG_WHITE);
g_screen->screenTextAt(2, line, "%s", s->name);
for (int j = 0; j < 6; j++) {
g_screen->screenTextColor(colors[j]);
g_screen->screenShowChar('0' + s->price[j], 10 + j, line);
}
}
for (int i = 0; i < 8; i++) {
g_screen->screenTextColor(colors[i]);
g_screen->screenShowChar('A' + i, 10 + i, 6);
}
bool done = false;
while (!done) {
showMixturesSuper(page);
g_screen->screenMessage("For Spell: ");
int spell = ReadChoiceController::get("abcdefghijklmnopqrstuvwxyz \033\n\r");
if (spell < 'a' || spell > 'z') {
g_screen->screenMessage("\nDone.\n");
done = true;
} else {
spell -= 'a';
const Spell *s = g_spells->getSpell(spell);
g_screen->screenMessage("%s\n", s->_name);
page = (spell >= 13);
showMixturesSuper(page);
// how many can we mix?
int mixQty = 99 - g_ultima->_saveGame->_mixtures[spell];
int ingQty = 99;
int comp = s->_components;
for (int i = 0; i < 8; i++) {
if (comp & 1 << i) {
int reagentQty = g_ultima->_saveGame->_reagents[i];
if (reagentQty < ingQty)
ingQty = reagentQty;
}
}
g_screen->screenMessage("You can make %d.\n", (mixQty > ingQty) ? ingQty : mixQty);
g_screen->screenMessage("How many? ");
int howmany = ReadIntController::get(2, TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
if (howmany == 0) {
g_screen->screenMessage("\nNone mixed!\n");
} else if (howmany > mixQty) {
g_screen->screenMessage("\n%cYou cannot mix that much more of that spell!%c\n", FG_GREY, FG_WHITE);
} else if (howmany > ingQty) {
g_screen->screenMessage("\n%cYou don't have enough reagents to mix %d spells!%c\n", FG_GREY, howmany, FG_WHITE);
} else {
g_ultima->_saveGame->_mixtures[spell] += howmany;
for (int i = 0; i < 8; i++) {
if (comp & 1 << i) {
g_ultima->_saveGame->_reagents[i] -= howmany;
}
}
g_screen->screenMessage("\nSuccess!\n\n");
}
}
g_context->_stats->setView(StatsView(STATS_REAGENTS));
}
g_context->_location->_viewMode = oldlocation;
return;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,153 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_GAME_H
#define ULTIMA4_GAME_GAME_H
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/controllers/game_controller.h"
#include "ultima/ultima4/core/observer.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/views/tileview.h"
#include "ultima/ultima4/sound/sound.h"
namespace Ultima {
namespace Ultima4 {
class Map;
struct Portal;
class Creature;
class Location;
class MoveEvent;
class Party;
class PartyEvent;
class PartyMember;
typedef enum {
VIEW_NORMAL,
VIEW_GEM,
VIEW_RUNE,
VIEW_DUNGEON,
VIEW_DEAD,
VIEW_CODEX,
VIEW_MIXTURES
} ViewMode;
/* map and screen functions */
/**
* Sets the view mode.
*/
void gameSetViewMode(ViewMode newMode);
void gameUpdateScreen();
/* spell functions */
void gameSpellEffect(int spell, int player, Sound sound);
/* action functions */
/**
* Peers at a city from A-P (Lycaeum telescope) and functions like a gem
*/
bool gamePeerCity(int city, void *data);
/**
* Peers at a gem
*/
void peer(bool useGem = true);
bool fireAt(const Coords &coords, bool originAvatar);
Direction gameGetDirection();
uint32 gameTimeSinceLastCommand();
/* checking functions */
/**
* Checks the hull integrity of the ship and handles
* the ship sinking, if necessary
*/
void gameCheckHullIntegrity();
/* creature functions */
/**
* Performs a ranged attack for the creature at x,y on the world map
*/
bool creatureRangeAttack(const Coords &coords, Creature *m);
void gameCreatureCleanup();
/**
* Spawns a creature (m) just offscreen of the avatar.
* If (m==nullptr) then it finds its own creature to spawn and spawns it.
*/
bool gameSpawnCreature(const class Creature *m);
/**
* Fixes objects initially loaded by saveGameMonstersRead,
* and alters movement behavior accordingly to match the creature
*/
void gameFixupObjects(Map *map);
/**
* Destroys all creatures on the current map.
*/
void gameDestroyAllCreatures();
/**
* Handles what happens when a creature attacks you
*/
void gameCreatureAttack(Creature *obj);
/* etc */
Common::String gameGetInput(int maxlen = 32);
int gameGetPlayer(bool canBeDisabled, bool canBeActivePlayer);
void gameGetPlayerForCommand(bool (*commandFn)(int player), bool canBeDisabled, bool canBeActivePlayer);
/**
* Deals an amount of damage between 'minDamage' and 'maxDamage'
* to each party member, with a 50% chance for each member to
* avoid the damage. If (minDamage == -1) or (minDamage >= maxDamage),
* deals 'maxDamage' damage to each member.
*/
void gameDamageParty(int minDamage, int maxDamage);
/**
* Deals an amount of damage between 'minDamage' and 'maxDamage'
* to the ship. If (minDamage == -1) or (minDamage >= maxDamage),
* deals 'maxDamage' damage to the ship.
*/
void gameDamageShip(int minDamage, int maxDamage);
/**
* Sets (or unsets) the active player
*/
void gameSetActivePlayer(int player);
/**
* Gets the path of coordinates for an action. Each tile in the
* direction specified by dirmask, between the minimum and maximum
* distances given, is included in the path, until blockedPredicate
* fails. If a tile is blocked, that tile is included in the path
* only if includeBlocked is true.
*/
Std::vector<Coords> gameGetDirectionalActionPath(int dirmask, int validDirections, const Coords &origin, int minDistance, int maxDistance, bool (*blockedPredicate)(const Tile *tile), bool includeBlocked);
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,761 @@
/* 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/game/item.h"
#include "ultima/ultima4/game/codex.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/game/portal.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/weapon.h"
#include "ultima/ultima4/controllers/alpha_action_controller.h"
#include "ultima/ultima4/controllers/combat_controller.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/dungeon.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/map/tileset.h"
#include "ultima/ultima4/ultima4.h"
namespace Ultima {
namespace Ultima4 {
Items *g_items;
const ItemLocation Items::ITEMS[N_ITEMS] = {
{
"Mandrake Root", nullptr, "mandrake1",
&Items::isReagentInInventory, &Items::putReagentInInventory, nullptr, REAG_MANDRAKE, SC_NEWMOONS | SC_REAGENTDELAY
},
{
"Mandrake Root", nullptr, "mandrake2",
&Items::isReagentInInventory, &Items::putReagentInInventory, nullptr, REAG_MANDRAKE, SC_NEWMOONS | SC_REAGENTDELAY
},
{
"Nightshade", nullptr, "nightshade1",
&Items::isReagentInInventory, &Items::putReagentInInventory, nullptr, REAG_NIGHTSHADE, SC_NEWMOONS | SC_REAGENTDELAY
},
{
"Nightshade", nullptr, "nightshade2",
&Items::isReagentInInventory, &Items::putReagentInInventory, nullptr, REAG_NIGHTSHADE, SC_NEWMOONS | SC_REAGENTDELAY
},
{
"the Bell of Courage", "bell", "bell",
&Items::isItemInInventory, &Items::putItemInInventory, &Items::useBBC, ITEM_BELL, 0
},
{
"the Book of Truth", "book", "book",
&Items::isItemInInventory, &Items::putItemInInventory, &Items::useBBC, ITEM_BOOK, 0
},
{
"the Candle of Love", "candle", "candle",
&Items::isItemInInventory, &Items::putItemInInventory, &Items::useBBC, ITEM_CANDLE, 0
},
{
"A Silver Horn", "horn", "horn",
&Items::isItemInInventory, &Items::putItemInInventory, &Items::useHorn, ITEM_HORN, 0
},
{
"the Wheel from the H.M.S. Cape", "wheel", "wheel",
&Items::isItemInInventory, &Items::putItemInInventory, &Items::useWheel, ITEM_WHEEL, 0
},
{
"the Skull of Modain the Wizard", "skull", "skull",
&Items::isSkullInInventory, &Items::putItemInInventory, &Items::useSkull, ITEM_SKULL, SC_NEWMOONS
},
{
"the Red Stone", "red", "redstone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_RED, 0
},
{
"the Orange Stone", "orange", "orangestone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_ORANGE, 0
},
{
"the Yellow Stone", "yellow", "yellowstone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_YELLOW, 0
},
{
"the Green Stone", "green", "greenstone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_GREEN, 0
},
{
"the Blue Stone", "blue", "bluestone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_BLUE, 0
},
{
"the Purple Stone", "purple", "purplestone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_PURPLE, 0
},
{
"the Black Stone", "black", "blackstone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_BLACK, SC_NEWMOONS
},
{
"the White Stone", "white", "whitestone",
&Items::isStoneInInventory, &Items::putStoneInInventory, &Items::useStone, STONE_WHITE, 0
},
/* handlers for using generic objects */
{ nullptr, "stone", nullptr, &Items::isStoneInInventory, nullptr, &Items::useStone, -1, 0 },
{ nullptr, "stones", nullptr, &Items::isStoneInInventory, nullptr, &Items::useStone, -1, 0 },
{ nullptr, "key", nullptr, &Items::isItemInInventory, nullptr, &Items::useKey, (ITEM_KEY_C | ITEM_KEY_L | ITEM_KEY_T), 0 },
{ nullptr, "keys", nullptr, &Items::isItemInInventory, nullptr, &Items::useKey, (ITEM_KEY_C | ITEM_KEY_L | ITEM_KEY_T), 0 },
/* Lycaeum telescope */
{ nullptr, nullptr, "telescope", nullptr, &Items::useTelescope, nullptr, 0, 0 },
{
"Mystic Armor", nullptr, "mysticarmor",
&Items::isMysticInInventory, &Items::putMysticInInventory, nullptr, ARMR_MYSTICROBES, SC_FULLAVATAR
},
{
"Mystic Swords", nullptr, "mysticswords",
&Items::isMysticInInventory, &Items::putMysticInInventory, nullptr, WEAP_MYSTICSWORD, SC_FULLAVATAR
},
{
"the sulfury remains of an ancient Sosarian Laser Gun. It turns to ash in your fingers", nullptr, "lasergun", // lol, where'd that come from?
//Looks like someone was experimenting with "maps.xml". It effectively increments sulfur ash by one due to '16' being an invalid weapon index.
&Items::isWeaponInInventory, &Items::putWeaponInInventory, nullptr, 16, 0
},
{
"the rune of Honesty", nullptr, "honestyrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_HONESTY, 0
},
{
"the rune of Compassion", nullptr, "compassionrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_COMPASSION, 0
},
{
"the rune of Valor", nullptr, "valorrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_VALOR, 0
},
{
"the rune of Justice", nullptr, "justicerune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_JUSTICE, 0
},
{
"the rune of Sacrifice", nullptr, "sacrificerune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_SACRIFICE, 0
},
{
"the rune of Honor", nullptr, "honorrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_HONOR, 0
},
{
"the rune of Spirituality", nullptr, "spiritualityrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_SPIRITUALITY, 0
},
{
"the rune of Humility", nullptr, "humilityrune",
&Items::isRuneInInventory, &Items::putRuneInInventory, nullptr, RUNE_HUMILITY, 0
}
};
Items::Items() : destroyAllCreaturesCallback(nullptr),
needStoneNames(0), stoneMask(0) {
g_items = this;
}
Items::~Items() {
g_items = nullptr;
}
void Items::setDestroyAllCreaturesCallback(DestroyAllCreaturesCallback callback) {
destroyAllCreaturesCallback = callback;
}
bool Items::isRuneInInventory(int virt) {
return g_ultima->_saveGame->_runes & virt;
}
void Items::putRuneInInventory(int virt) {
g_context->_party->member(0)->awardXp(100);
g_context->_party->adjustKarma(KA_FOUND_ITEM);
g_ultima->_saveGame->_runes |= virt;
#ifdef IOS_ULTIMA4
Common::String virtueName;
switch (virt) {
default:
case RUNE_HONESTY:
virtueName = "Honesty";
break;
case RUNE_HONOR:
virtueName = "Honor";
break;
case RUNE_HUMILITY:
virtueName = "Humility";
break;
case RUNE_JUSTICE:
virtueName = "Justice";
break;
case RUNE_SACRIFICE:
virtueName = "Sacrifice";
break;
case RUNE_SPIRITUALITY:
virtueName = "Spirituality";
break;
case RUNE_VALOR:
virtueName = "Valor";
break;
case RUNE_COMPASSION:
virtueName = "Compassion";
break;
}
U4IOS::testFlightPassCheckPoint("Player got stone: " + virtueName);
#endif
g_ultima->_saveGame->_lastReagent = g_ultima->_saveGame->_moves & 0xF0;
}
bool Items::isStoneInInventory(int virt) {
/* generic test: does the party have any stones yet? */
if (virt == -1)
return (g_ultima->_saveGame->_stones > 0);
/* specific test: does the party have a specific stone? */
else
return g_ultima->_saveGame->_stones & virt;
}
void Items::putStoneInInventory(int virt) {
g_context->_party->member(0)->awardXp(200);
g_context->_party->adjustKarma(KA_FOUND_ITEM);
g_ultima->_saveGame->_stones |= virt;
#ifdef IOS_ULTIMA4
Common::String stoneName;
switch (virt) {
default:
case STONE_BLACK:
stoneName = "Black";
break;
case STONE_BLUE:
stoneName = "Blue";
break;
case STONE_GREEN:
stoneName = "Green";
break;
case STONE_ORANGE:
stoneName = "Orange";
break;
case STONE_PURPLE:
stoneName = "Purple";
break;
case STONE_RED:
stoneName = "Red";
break;
case STONE_WHITE:
stoneName = "White";
break;
case STONE_YELLOW:
stoneName = "Yellow";
break;
}
U4IOS::testFlightPassCheckPoint("Player got rune: " + stoneName);
#endif
g_ultima->_saveGame->_lastReagent = g_ultima->_saveGame->_moves & 0xF0;
}
bool Items::isItemInInventory(int item) {
return g_ultima->_saveGame->_items & item;
}
bool Items::isSkullInInventory(int unused) {
return (g_ultima->_saveGame->_items & (ITEM_SKULL | ITEM_SKULL_DESTROYED));
}
void Items::putItemInInventory(int item) {
g_context->_party->member(0)->awardXp(400);
g_context->_party->adjustKarma(KA_FOUND_ITEM);
g_ultima->_saveGame->_items |= item;
#ifdef IOS_ULTIMA4
Common::String itemName;
switch (item) {
default:
case ITEM_BELL:
itemName = "Bell";
break;
case ITEM_BOOK:
itemName = "Book";
break;
case ITEM_CANDLE:
itemName = "Candle";
break;
case ITEM_HORN:
itemName = "Horn";
break;
case ITEM_KEY_C:
itemName = "Key Courage";
break;
case ITEM_KEY_L:
itemName = "Key Love";
break;
case ITEM_KEY_T:
itemName = "Key Truth";
break;
case ITEM_SKULL:
itemName = "Skull";
break;
case ITEM_WHEEL:
itemName = "Wheel";
break;
}
U4IOS::testFlightPassCheckPoint("Player got rune: " + itemName);
#endif
g_ultima->_saveGame->_lastReagent = g_ultima->_saveGame->_moves & 0xF0;
}
void Items::useBBC(int item) {
Coords abyssEntrance(0xe9, 0xe9);
/* on top of the Abyss entrance */
if (g_context->_location->_coords == abyssEntrance) {
/* must use bell first */
if (item == ITEM_BELL) {
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("The Bell rings on and on!");
#endif
g_screen->screenMessage("\nThe Bell rings on and on!\n");
g_ultima->_saveGame->_items |= ITEM_BELL_USED;
}
/* then the book */
else if ((item == ITEM_BOOK) && (g_ultima->_saveGame->_items & ITEM_BELL_USED)) {
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("The words resonate with the ringing!");
#endif
g_screen->screenMessage("\nThe words resonate with the ringing!\n");
g_ultima->_saveGame->_items |= ITEM_BOOK_USED;
}
/* then the candle */
else if ((item == ITEM_CANDLE) && (g_ultima->_saveGame->_items & ITEM_BOOK_USED)) {
g_screen->screenMessage("\nAs you light the Candle the Earth Trembles!\n");
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("As you light the Candle the Earth Trembles!");
#endif
g_ultima->_saveGame->_items |= ITEM_CANDLE_USED;
} else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
}
/* somewhere else */
else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
}
void Items::useHorn(int item) {
g_screen->screenMessage("\nThe Horn sounds an eerie tone!\n");
g_context->_aura->set(Aura::HORN, 10);
}
void Items::useWheel(int item) {
if ((g_context->_transportContext == TRANSPORT_SHIP) && (g_ultima->_saveGame->_shipHull == 50)) {
g_screen->screenMessage("\nOnce mounted, the Wheel glows with a blue light!\n");
g_context->_party->setShipHull(99);
} else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
}
void Items::useSkull(int item) {
/* FIXME: check to see if the abyss must be opened first
for the skull to be *able* to be destroyed */
/* We do the check here instead of in the table, because we need to distinguish between a
never-found skull and a destroyed skull. */
if (g_ultima->_saveGame->_items & ITEM_SKULL_DESTROYED) {
g_screen->screenMessage("\nNone owned!\n");
return;
}
/* destroy the skull! pat yourself on the back */
if (g_context->_location->_coords.x == 0xe9 && g_context->_location->_coords.y == 0xe9) {
g_screen->screenMessage("\n\nYou cast the Skull of Mondain into the Abyss!\n");
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("You cast the Skull of Mondain into the Abyss!");
#endif
g_ultima->_saveGame->_items = (g_ultima->_saveGame->_items & ~ITEM_SKULL) | ITEM_SKULL_DESTROYED;
g_context->_party->adjustKarma(KA_DESTROYED_SKULL);
}
/* use the skull... bad, very bad */
else {
g_screen->screenMessage("\n\nYou hold the evil Skull of Mondain the Wizard aloft...\n");
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("You hold the evil Skull of Mondain the Wizard aloft...");
#endif
/* destroy all creatures */
(*destroyAllCreaturesCallback)();
/* we don't lose the skull until we toss it into the abyss */
//c->saveGame->_items = (c->saveGame->_items & ~ITEM_SKULL);
g_context->_party->adjustKarma(KA_USED_SKULL);
}
}
void Items::useStone(int item) {
MapCoords coords;
byte stone = static_cast<byte>(item);
static const byte truth = STONE_WHITE | STONE_PURPLE | STONE_GREEN | STONE_BLUE;
static const byte love = STONE_WHITE | STONE_YELLOW | STONE_GREEN | STONE_ORANGE;
static const byte courage = STONE_WHITE | STONE_RED | STONE_PURPLE | STONE_ORANGE;
static const byte *attr = nullptr;
g_context->_location->getCurrentPosition(&coords);
/**
* Named a specific stone (after using "stone" or "stones")
*/
if (item != -1) {
CombatMap *cm = getCombatMap();
if (needStoneNames) {
/* named a stone while in a dungeon altar room */
if (g_context->_location->_context & CTX_ALTAR_ROOM) {
needStoneNames--;
switch (cm->getAltarRoom()) {
case VIRT_TRUTH:
attr = &truth;
break;
case VIRT_LOVE:
attr = &love;
break;
case VIRT_COURAGE:
attr = &courage;
break;
default:
break;
}
/* make sure we're in an altar room */
if (attr) {
/* we need to use the stone, and we haven't used it yet */
if ((*attr & stone) && (stone & ~stoneMask))
stoneMask |= stone;
/* we already used that stone! */
else if (stone & stoneMask) {
g_screen->screenMessage("\nAlready used!\n");
needStoneNames = 0;
stoneMask = 0; /* reset the mask so you can try again */
return;
}
} else {
error("Not in an altar room!");
}
/* see if we have all the stones, if not, get more names! */
if (attr && needStoneNames) {
g_screen->screenMessage("\n%c:", 'E' - needStoneNames);
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which Color?");
#endif
itemHandleStones(gameGetInput());
}
/* all the stones have been entered, verify them! */
else {
unsigned short key = 0xFFFF;
switch (cm->getAltarRoom()) {
case VIRT_TRUTH:
key = ITEM_KEY_T;
break;
case VIRT_LOVE:
key = ITEM_KEY_L;
break;
case VIRT_COURAGE:
key = ITEM_KEY_C;
break;
default:
break;
}
/* in an altar room, named all of the stones, and don't have the key yet... */
if (attr && (stoneMask == *attr) && !(g_ultima->_saveGame->_items & key)) {
#ifdef IOS_ULTIMA4
Common::String keyName;
switch (key) {
case ITEM_KEY_C:
keyName = "Key Courage";
break;
case ITEM_KEY_L:
keyName = "Key Love";
break;
case ITEM_KEY_T:
keyName = "Key Truth";
break;
}
U4IOS::testFlightPassCheckPoint("Receive a key: " + keyName);
#endif
g_screen->screenMessage("\nThou doth find one third of the Three Part Key!\n");
g_ultima->_saveGame->_items |= key;
} else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
stoneMask = 0; /* reset the mask so you can try again */
}
} else {
/* Otherwise, we're asking for a stone while in the abyss on top of an altar */
/* see if they entered the correct stone */
if (stone == (1 << g_context->_location->_coords.z)) {
if (g_context->_location->_coords.z < 7) {
/* replace the altar with a down-ladder */
MapCoords pos;
g_screen->screenMessage("\n\nThe altar changes before thyne eyes!\n");
g_context->_location->getCurrentPosition(&pos);
g_context->_location->_map->_annotations->add(pos, g_context->_location->_map->_tileSet->getByName("down_ladder")->getId());
} else {
// Start chamber of the codex sequence...
g_codex->start();
}
} else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
}
} else {
g_screen->screenMessage("\nNot a Usable Item!\n");
stoneMask = 0; /* reset the mask so you can try again */
}
}
/**
* in the abyss, on an altar to place the stones
*/
else if ((g_context->_location->_map->_id == MAP_ABYSS) &&
(g_context->_location->_context & CTX_DUNGEON) &&
(static_cast<Dungeon *>(g_context->_location->_map)->currentToken() == DUNGEON_ALTAR)) {
int virtueMask = getBaseVirtues((Virtue)g_context->_location->_coords.z);
if (virtueMask > 0)
g_screen->screenMessage("\n\nAs thou doth approach, a voice rings out: What virtue dost stem from %s?\n\n", getBaseVirtueName(virtueMask));
else
g_screen->screenMessage("\n\nA voice rings out: What virtue exists independently of Truth, Love, and Courage?\n\n");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which virtue?");
#endif
Common::String virtue = gameGetInput();
if (scumm_strnicmp(virtue.c_str(), getVirtueName((Virtue)g_context->_location->_coords.z), 6) == 0) {
/* now ask for stone */
g_screen->screenMessage("\n\nThe Voice says: Use thy Stone.\n\nColor:\n");
needStoneNames = 1;
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which color?");
#endif
itemHandleStones(gameGetInput());
} else {
g_screen->screenMessage("\nHmm...No effect!\n");
}
}
/**
* in a dungeon altar room, on the altar
*/
else if ((g_context->_location->_context & CTX_ALTAR_ROOM) &&
coords.x == 5 && coords.y == 5) {
needStoneNames = 4;
g_screen->screenMessage("\n\nThere are holes for 4 stones.\nWhat colors:\nA:");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper::setIntroString("Which color?");
#endif
itemHandleStones(gameGetInput());
} else {
g_screen->screenMessage("\nNo place to Use them!\n");
// This used to say "\nNo place to Use them!\nHmm...No effect!\n"
// That doesn't match U4DOS; does it match another?
}
}
void Items::useKey(int item) {
g_screen->screenMessage("\nNo place to Use them!\n");
}
bool Items::isMysticInInventory(int mystic) {
/* FIXME: you could feasibly get more mystic weapons and armor if you
have 8 party members and equip them all with everything,
then search for Mystic Weapons/Armor again
or, you could just sell them all and search again. What an easy
way to make some cash!
This would be a good candidate for an xu4 "extended" savegame
format.
*/
if (mystic == WEAP_MYSTICSWORD)
return g_ultima->_saveGame->_weapons[WEAP_MYSTICSWORD] > 0;
else if (mystic == ARMR_MYSTICROBES)
return g_ultima->_saveGame->_armor[ARMR_MYSTICROBES] > 0;
else
error("Invalid mystic item was tested in isMysticInInventory()");
return false;
}
void Items::putMysticInInventory(int mystic) {
g_context->_party->member(0)->awardXp(400);
g_context->_party->adjustKarma(KA_FOUND_ITEM);
if (mystic == WEAP_MYSTICSWORD)
g_ultima->_saveGame->_weapons[WEAP_MYSTICSWORD] += 8;
else if (mystic == ARMR_MYSTICROBES)
g_ultima->_saveGame->_armor[ARMR_MYSTICROBES] += 8;
else
error("Invalid mystic item was added in putMysticInInventory()");
g_ultima->_saveGame->_lastReagent = g_ultima->_saveGame->_moves & 0xF0;
}
bool Items::isWeaponInInventory(int weapon) {
if (g_ultima->_saveGame->_weapons[weapon])
return true;
else {
for (int i = 0; i < g_context->_party->size(); i++) {
if (g_context->_party->member(i)->getWeapon()->getType() == weapon)
return true;
}
}
return false;
}
void Items::putWeaponInInventory(int weapon) {
g_ultima->_saveGame->_weapons[weapon]++;
}
void Items::useTelescope(int notused) {
g_screen->screenMessage("You see a knob\non the telescope\nmarked A-P\nYou Select:");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationChoiceHelper telescopeHelper;
telescopeHelper.updateChoices("abcdefghijklmnop ");
#endif
int choice = AlphaActionController::get('p', "You Select:");
if (choice == -1)
return;
gamePeerCity(choice, nullptr);
}
bool Items::isReagentInInventory(int reag) {
return false;
}
void Items::putReagentInInventory(int reag) {
g_context->_party->adjustKarma(KA_FOUND_ITEM);
g_ultima->_saveGame->_reagents[reag] += xu4_random(8) + 2;
g_ultima->_saveGame->_lastReagent = g_ultima->_saveGame->_moves & 0xF0;
if (g_ultima->_saveGame->_reagents[reag] > 99) {
g_ultima->_saveGame->_reagents[reag] = 99;
g_screen->screenMessage("Dropped some!\n");
}
}
bool Items::itemConditionsMet(byte conditions) {
if ((conditions & SC_NEWMOONS) &&
!(g_ultima->_saveGame->_trammelPhase == 0 && g_ultima->_saveGame->_feluccaPhase == 0))
return false;
if (conditions & SC_FULLAVATAR) {
for (int i = 0; i < VIRT_MAX; i++) {
if (g_ultima->_saveGame->_karma[i] != 0)
return false;
}
}
if ((conditions & SC_REAGENTDELAY) &&
(g_ultima->_saveGame->_moves & 0xF0) == g_ultima->_saveGame->_lastReagent)
return false;
return true;
}
const ItemLocation *Items::itemAtLocation(const Map *map, const Coords &coords) {
for (uint i = 0; i < N_ITEMS; i++) {
if (!ITEMS[i]._locationLabel)
continue;
if (map->getLabel(ITEMS[i]._locationLabel) == coords &&
itemConditionsMet(ITEMS[i]._conditions))
return &(ITEMS[i]);
}
return nullptr;
}
void Items::itemUse(const Common::String &shortName) {
const ItemLocation *item = nullptr;
for (uint i = 0; i < N_ITEMS; i++) {
if (ITEMS[i]._shortName &&
scumm_stricmp(ITEMS[i]._shortName, shortName.c_str()) == 0) {
item = &ITEMS[i];
/* item name found, see if we have that item in our inventory */
if (!ITEMS[i]._isItemInInventory || (this->*(ITEMS[i]._isItemInInventory))(ITEMS[i]._data)) {
/* use the item, if we can! */
if (!item || !item->_useItem)
g_screen->screenMessage("\nNot a Usable item!\n");
else
(this->*(item->_useItem))(ITEMS[i]._data);
} else
g_screen->screenMessage("\nNone owned!\n");
/* we found the item, no need to keep searching */
break;
}
}
/* item was not found */
if (!item)
g_screen->screenMessage("\nNot a Usable item!\n");
}
void Items::itemHandleStones(const Common::String &color) {
bool found = false;
for (int i = 0; i < 8; i++) {
if (scumm_stricmp(color.c_str(), getStoneName((Virtue)i)) == 0 &&
isStoneInInventory(1 << i)) {
found = true;
itemUse(color.c_str());
}
}
if (!found) {
g_screen->screenMessage("\nNone owned!\n");
stoneMask = 0; /* make sure stone mask is reset */
}
}
bool Items::isAbyssOpened(const Portal *p) {
/* make sure the bell, book and candle have all been used */
int items = g_ultima->_saveGame->_items;
int isopened = (items & ITEM_BELL_USED) && (items & ITEM_BOOK_USED) && (items & ITEM_CANDLE_USED);
if (!isopened)
g_screen->screenMessage("Enter Can't!\n");
return isopened;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,150 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_ITEM_H
#define ULTIMA4_GAME_ITEM_H
#include "ultima/ultima4/core/types.h"
#include "common/str.h"
namespace Ultima {
namespace Ultima4 {
class Coords;
class Map;
struct Portal;
enum SearchCondition {
SC_NONE = 0x00,
SC_NEWMOONS = 0x01,
SC_FULLAVATAR = 0x02,
SC_REAGENTDELAY = 0x04
};
class Items;
typedef bool (Items::*IsInInventoryProc)(int item);
typedef void (Items::*InventoryActionProc)(int item);
struct
#ifndef NO_CXX11_ALIGNAS
alignas(8)
#endif
ItemLocation {
const char *_name;
const char *_shortName;
const char *_locationLabel;
IsInInventoryProc _isItemInInventory;
InventoryActionProc _putItemInInventory;
InventoryActionProc _useItem;
int _data;
byte _conditions;
};
typedef void (*DestroyAllCreaturesCallback)();
#define N_ITEMS 34
class Items {
private:
static const ItemLocation ITEMS[N_ITEMS];
DestroyAllCreaturesCallback destroyAllCreaturesCallback;
int needStoneNames;
byte stoneMask;
private:
bool isRuneInInventory(int virt);
void putRuneInInventory(int virt);
bool isStoneInInventory(int virt);
void putStoneInInventory(int virt);
bool isItemInInventory(int item);
bool isSkullInInventory(int item);
void putItemInInventory(int item);
/**
* Use bell, book, or candle on the entrance to the Abyss
*/
void useBBC(int item);
/**
* Uses the silver horn
*/
void useHorn(int item);
/**
* Uses the wheel (if on board a ship)
*/
void useWheel(int item);
/**
* Uses or destroys the skull of Mondain
*/
void useSkull(int item);
/**
* Handles using the virtue stones in dungeon altar rooms and on dungeon altars
*/
void useStone(int item);
void useKey(int item);
bool isMysticInInventory(int mystic);
void putMysticInInventory(int mystic);
bool isWeaponInInventory(int weapon);
void putWeaponInInventory(int weapon);
void useTelescope(int notused);
bool isReagentInInventory(int reag);
void putReagentInInventory(int reag);
/**
* Handles naming of stones when used
*/
void itemHandleStones(const Common::String &color);
/**
* Returns true if the specified conditions are met to be able to get the item
*/
bool itemConditionsMet(byte conditions);
public:
Items();
~Items();
void setDestroyAllCreaturesCallback(DestroyAllCreaturesCallback callback);
/**
* Returns an item location record if a searchable object exists at
* the given location. nullptr is returned if nothing is there.
*/
const ItemLocation *itemAtLocation(const Map *map, const Coords &coords);
/**
* Uses the item indicated by 'shortname'
*/
void itemUse(const Common::String &shortName);
/**
* Checks to see if the abyss was opened
*/
static bool isAbyssOpened(const Portal *p);
};
extern Items *g_items;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,77 @@
/* 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/core/coords.h"
#include "ultima/ultima4/game/moongate.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/shared/std/containers.h"
namespace Ultima {
namespace Ultima4 {
Moongates *g_moongates;
Moongates::Moongates() {
g_moongates = this;
}
Moongates::~Moongates() {
g_moongates = nullptr;
}
void Moongates::add(int phase, const Coords &coords) {
if (contains(phase))
error("Error: A moongate for phase %d already exists", phase);
(*this)[phase] = coords;
}
const Coords *Moongates::getGateCoordsForPhase(int phase) {
iterator moongate;
moongate = find(phase);
if (moongate != end())
return &moongate->_value;
return nullptr;
}
bool Moongates::findActiveGateAt(int trammel, int felucca, const Coords &src, Coords &dest) {
const Coords *moongate_coords;
moongate_coords = getGateCoordsForPhase(trammel);
if (moongate_coords && (src == *moongate_coords)) {
moongate_coords = getGateCoordsForPhase(felucca);
if (moongate_coords) {
dest = *moongate_coords;
return true;
}
}
return false;
}
bool Moongates::isEntryToShrineOfSpirituality(int trammel, int felucca) {
return (trammel == 4 && felucca == 4) ? true : false;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,54 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_MOONGATE_H
#define ULTIMA4_GAME_MOONGATE_H
#include "ultima/ultima4/core/coords.h"
#include "common/hashmap.h"
namespace Ultima {
namespace Ultima4 {
class Moongates : public Common::HashMap<int, Coords> {
public:
/**
* Constructor
*/
Moongates();
/**
* Destructor
*/
~Moongates();
void add(int phase, const Coords &coords);
const Coords *getGateCoordsForPhase(int phase);
bool findActiveGateAt(int trammel, int felucca, const Coords &src, Coords &dest);
bool isEntryToShrineOfSpirituality(int trammel, int felucca);
};
extern Moongates *g_moongates;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,178 @@
/* 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/game/names.h"
namespace Ultima {
namespace Ultima4 {
const char *getClassName(ClassType klass) {
switch (klass) {
case CLASS_MAGE:
return "Mage";
case CLASS_BARD:
return "Bard";
case CLASS_FIGHTER:
return "Fighter";
case CLASS_DRUID:
return "Druid";
case CLASS_TINKER:
return "Tinker";
case CLASS_PALADIN:
return "Paladin";
case CLASS_RANGER:
return "Ranger";
case CLASS_SHEPHERD:
return "Shepherd";
default:
return "???";
}
}
const char *getReagentName(Reagent reagent) {
static const char *const reagentNames[] = {
"Sulfur Ash", "Ginseng", "Garlic",
"Spider Silk", "Blood Moss", "Black Pearl",
"Nightshade", "Mandrake"
};
if (reagent < REAG_MAX)
return reagentNames[reagent - REAG_ASH];
else
return "???";
}
const char *getVirtueName(Virtue virtue) {
static const char *const virtueNames[] = {
"Honesty", "Compassion", "Valor",
"Justice", "Sacrifice", "Honor",
"Spirituality", "Humility"
};
if (virtue < 8)
return virtueNames[virtue - VIRT_HONESTY];
else
return "???";
}
const char *getBaseVirtueName(int virtueMask) {
if (virtueMask == VIRT_TRUTH) return "Truth";
else if (virtueMask == VIRT_LOVE) return "Love";
else if (virtueMask == VIRT_COURAGE) return "Courage";
else if (virtueMask == (VIRT_TRUTH | VIRT_LOVE)) return "Truth and Love";
else if (virtueMask == (VIRT_LOVE | VIRT_COURAGE)) return "Love and Courage";
else if (virtueMask == (VIRT_COURAGE | VIRT_TRUTH)) return "Courage and Truth";
else if (virtueMask == (VIRT_TRUTH | VIRT_LOVE | VIRT_COURAGE)) return "Truth, Love and Courage";
else return "???";
}
int getBaseVirtues(Virtue virtue) {
switch (virtue) {
case VIRT_HONESTY:
return VIRT_TRUTH;
case VIRT_COMPASSION:
return VIRT_LOVE;
case VIRT_VALOR:
return VIRT_COURAGE;
case VIRT_JUSTICE:
return VIRT_TRUTH | VIRT_LOVE;
case VIRT_SACRIFICE:
return VIRT_LOVE | VIRT_COURAGE;
case VIRT_HONOR:
return VIRT_COURAGE | VIRT_TRUTH;
case VIRT_SPIRITUALITY:
return VIRT_TRUTH | VIRT_LOVE | VIRT_COURAGE;
case VIRT_HUMILITY:
return 0;
default:
return 0;
}
}
const char *getVirtueAdjective(Virtue virtue) {
static const char *const virtueAdjectives[] = {
"honest",
"compassionate",
"valiant",
"just",
"sacrificial",
"honorable",
"spiritual",
"humble"
};
if (virtue < 8)
return virtueAdjectives[virtue - VIRT_HONESTY];
else
return "???";
}
const char *getStoneName(Virtue virtue) {
static const char *const virtueNames[] = {
"Blue", "Yellow", "Red",
"Green", "Orange", "Purple",
"White", "Black"
};
if (virtue < VIRT_MAX)
return virtueNames[virtue - VIRT_HONESTY];
else
return "???";
}
const char *getItemName(Item item) {
switch (item) {
case ITEM_SKULL:
return "Skull";
case ITEM_CANDLE:
return "Candle";
case ITEM_BOOK:
return "Book";
case ITEM_BELL:
return "Bell";
case ITEM_KEY_C:
return "Courage";
case ITEM_KEY_L:
return "Love";
case ITEM_KEY_T:
return "Truth";
case ITEM_HORN:
return "Horn";
case ITEM_WHEEL:
return "Wheel";
default:
return "???";
}
}
const char *getDirectionName(Direction dir) {
static const char *const directionNames[] = {
"West", "North", "East", "South"
};
if (dir >= DIR_WEST && dir <= DIR_SOUTH)
return directionNames[dir - DIR_WEST];
else
return "???";
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,48 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_NAMES_H
#define ULTIMA4_GAME_NAMES_H
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/map/direction.h"
namespace Ultima {
namespace Ultima4 {
/*
* These routines convert the various enumerations for classes, reagents,
* etc. into the textual representations used in the game.
*/
const char *getClassName(ClassType klass);
const char *getReagentName(Reagent reagent);
const char *getVirtueName(Virtue virtue);
const char *getBaseVirtueName(int virtueMask);
int getBaseVirtues(Virtue virtue);
const char *getVirtueAdjective(Virtue virtue);
const char *getStoneName(Virtue virtue);
const char *getItemName(Item item);
const char *getDirectionName(Direction dir);
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,66 @@
/* 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/game/object.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/game/game.h"
#include "common/algorithm.h"
namespace Ultima {
namespace Ultima4 {
bool Object::setDirection(Direction d) {
return _tile.setDirection(d);
}
void Object::setMap(class Map *m) {
if (Common::find(_maps.begin(), _maps.end(), m) == _maps.end())
_maps.push_back(m);
}
Map *Object::getMap() {
if (_maps.empty())
return nullptr;
return _maps.back();
}
void Object::remove() {
uint size = _maps.size();
uint i = 0;
for (auto *map : _maps) {
if (i == size - 1)
map->removeObject(this);
else map->removeObject(this, false);
i++;
}
}
void Object::animateMovement() {
//TODO abstract movement - also make screen.h and game.h not required
g_screen->screenTileUpdate(&g_game->_mapArea, _prevCoords, false);
if (g_screen->screenTileUpdate(&g_game->_mapArea, _coords, false))
g_screen->screenWait(1);
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,147 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_OBJECT_H
#define ULTIMA4_GAME_OBJECT_H
#include "ultima/ultima4/core/coords.h"
#include "ultima/ultima4/map/map_tile.h"
#include "ultima/ultima4/core/types.h"
namespace Ultima {
namespace Ultima4 {
class Object;
typedef Common::List<Object *> ObjectDeque;
enum ObjectMovementBehavior {
MOVEMENT_FIXED,
MOVEMENT_WANDER,
MOVEMENT_FOLLOW_AVATAR,
MOVEMENT_ATTACK_AVATAR
};
class Object {
public:
enum Type {
UNKNOWN,
CREATURE,
PERSON
};
Object(Type type = UNKNOWN) :
_tile(0),
_prevTile(0),
_movementBehavior(MOVEMENT_FIXED),
_objType(type),
_focused(false),
_visible(true),
_animated(true) {
}
virtual ~Object() {}
// Methods
MapTile &getTile() {
return _tile;
}
MapTile &getPrevTile() {
return _prevTile;
}
const Coords &getCoords() const {
return _coords;
}
const Coords &getPrevCoords() const {
return _prevCoords;
}
ObjectMovementBehavior getMovementBehavior() const {
return _movementBehavior;
}
Type getType() const {
return _objType;
}
bool hasFocus() const {
return _focused;
}
bool isVisible() const {
return _visible;
}
bool isAnimated() const {
return _animated;
}
void setTile(MapTile t) {
_tile = t;
}
void setTile(Tile *t) {
_tile = t->getId();
}
void setPrevTile(MapTile t) {
_prevTile = t;
}
void setCoords(Coords c) {
_prevCoords = _coords;
_coords = c;
}
void setPrevCoords(Coords c) {
_prevCoords = c;
}
void setMovementBehavior(ObjectMovementBehavior b) {
_movementBehavior = b;
}
void setType(Type t) {
_objType = t;
}
void setFocus(bool f = true) {
_focused = f;
}
void setVisible(bool v = true) {
_visible = v;
}
void setAnimated(bool a = true) {
_animated = a;
}
void setMap(class Map *m);
Map *getMap();
void remove(); /**< Removes itself from any maps that it is a part of */
bool setDirection(Direction d);
void animateMovement();
// Properties
protected:
MapTile _tile, _prevTile;
Coords _coords, _prevCoords;
ObjectMovementBehavior _movementBehavior;
Type _objType;
Common::List<class Map *> _maps; /**< A list of maps this object is a part of */
bool _focused;
bool _visible;
bool _animated;
};
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,613 @@
/* 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/game/person.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/script.h"
#include "ultima/ultima4/controllers/read_choice_controller.h"
#include "ultima/ultima4/controllers/read_int_controller.h"
#include "ultima/ultima4/controllers/read_player_controller.h"
#include "ultima/ultima4/conversation/conversation.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/sound/music.h"
#include "ultima/ultima4/ultima4.h"
namespace Ultima {
namespace Ultima4 {
int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines);
/**
* Returns true of the object that 'punknown' points
* to is a person object
*/
bool isPerson(Object *punknown) {
Person *p;
if ((p = dynamic_cast<Person *>(punknown)) != nullptr)
return true;
else
return false;
}
/**
* Splits a piece of response text into screen-sized chunks.
*/
Common::List<Common::String> replySplit(const Common::String &text) {
Common::String str = text;
int pos, real_lines;
Common::List<Common::String> reply;
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
uint num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
/* we only have one chunk, no need to split it up */
uint len = str.size();
if (num_chars == len)
reply.push_back(str);
else {
Common::String pre = str.substr(0, num_chars);
/* add the first chunk to the list */
reply.push_back(pre);
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
while (num_chars != str.size()) {
/* go to the rest of the text */
str = str.substr(num_chars);
/* skip over any initial newlines */
if ((pos = str.find("\n\n")) == 0)
str = str.substr(pos + 1);
/* find the next chunk and add it */
num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
pre = str.substr(0, num_chars);
reply.push_back(pre);
}
}
return reply;
}
Person::Person(MapTile tile) : Creature(tile), _start(0, 0) {
setType(Object::PERSON);
_dialogue = nullptr;
_npcType = NPC_EMPTY;
}
Person::Person(const Person *p) : Creature(p->_tile) {
*this = *p;
}
bool Person::canConverse() const {
return isVendor() || _dialogue != nullptr;
}
bool Person::isVendor() const {
return
_npcType >= NPC_VENDOR_WEAPONS &&
_npcType <= NPC_VENDOR_STABLE;
}
Common::String Person::getName() const {
if (_dialogue)
return _dialogue->getName();
else if (_npcType == NPC_EMPTY)
return Creature::getName();
else
return "(unnamed person)";
}
void Person::goToStartLocation() {
setCoords(_start);
}
void Person::setDialogue(Dialogue *d) {
_dialogue = d;
if (_tile.getTileType()->getName() == "beggar")
_npcType = NPC_TALKER_BEGGAR;
else if (_tile.getTileType()->getName() == "guard")
_npcType = NPC_TALKER_GUARD;
else
_npcType = NPC_TALKER;
}
void Person::setNpcType(PersonNpcType t) {
_npcType = t;
assertMsg(!isVendor() || _dialogue == nullptr, "vendor has dialogue");
}
Common::List<Common::String> Person::getConversationText(Conversation *cnv, const char *inquiry) {
Common::String text;
/*
* a convsation with a vendor
*/
if (isVendor()) {
static const Common::String ids[] = {
"Weapons", "Armor", "Food", "Tavern", "Reagents", "Healer", "Inn", "Guild", "Stable"
};
Script *script = cnv->_script;
/**
* We aren't currently running a script, load the appropriate one!
*/
if (cnv->_state == Conversation::INTRO) {
// unload the previous script if it wasn't already unloaded
if (script->getState() != Script::STATE_UNLOADED)
script->unload();
script->load("vendorScript.xml", ids[_npcType - NPC_VENDOR_WEAPONS], "vendor", g_context->_location->_map->getName());
script->run("intro");
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationChoiceHelper choiceDialog;
#endif
while (script->getState() != Script::STATE_DONE) {
// Gather input for the script
if (script->getState() == Script::STATE_INPUT) {
switch (script->getInputType()) {
case Script::INPUT_CHOICE: {
const Common::String &choices = script->getChoices();
// Get choice
#ifdef IOS_ULTIMA4
choiceDialog.updateChoices(choices, script->getTarget(), npcType);
#endif
char val = ReadChoiceController::get(choices);
if (Common::isSpace(val) || val == '\033')
script->unsetVar(script->getInputName());
else {
Common::String s_val;
s_val = val;
script->setVar(script->getInputName(), s_val);
}
}
break;
case Script::INPUT_KEYPRESS:
ReadChoiceController::get(" \015\033");
break;
case Script::INPUT_NUMBER: {
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper ipadNumberInput;
ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeNumberPad, "Amount?");
#endif
int val = ReadIntController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
script->setVar(script->getInputName(), val);
}
break;
case Script::INPUT_STRING: {
#ifdef IOS_ULTIMA4
U4IOS::IOSConversationHelper ipadNumberInput;
ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeDefault);
#endif
Common::String str = ReadStringController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
if (str.size()) {
lowercase(str);
script->setVar(script->getInputName(), str);
} else script->unsetVar(script->getInputName());
}
break;
case Script::INPUT_PLAYER: {
ReadPlayerController getPlayerCtrl;
eventHandler->pushController(&getPlayerCtrl);
int player = getPlayerCtrl.waitFor();
if (player != -1) {
Common::String player_str = xu4_to_string(player + 1);
script->setVar(script->getInputName(), player_str);
} else script->unsetVar(script->getInputName());
}
break;
default:
break;
} // } switch
// Continue running the script!
g_context->_line++;
script->_continue();
} // } if
} // } while
}
// Unload the script
script->unload();
cnv->_state = Conversation::DONE;
}
/*
* a conversation with a non-vendor
*/
else {
text = "\n\n\n";
switch (cnv->_state) {
case Conversation::INTRO:
text = getIntro(cnv);
break;
case Conversation::TALK:
text += getResponse(cnv, inquiry) + "\n";
break;
case Conversation::CONFIRMATION:
assertMsg(_npcType == NPC_LORD_BRITISH, "invalid state: %d", cnv->_state);
text += lordBritishGetQuestionResponse(cnv, inquiry);
break;
case Conversation::ASK:
case Conversation::ASKYESNO:
assertMsg(_npcType != NPC_HAWKWIND, "invalid state for hawkwind conversation");
text += talkerGetQuestionResponse(cnv, inquiry) + "\n";
break;
case Conversation::GIVEBEGGAR:
assertMsg(_npcType == NPC_TALKER_BEGGAR, "invalid npc type: %d", _npcType);
text = beggarGetQuantityResponse(cnv, inquiry);
break;
case Conversation::FULLHEAL:
case Conversation::ADVANCELEVELS:
/* handled elsewhere */
break;
default:
error("invalid state: %d", cnv->_state);
}
}
return replySplit(text);
}
Common::String Person::getPrompt(Conversation *cnv) {
if (isVendor())
return "";
Common::String prompt;
if (cnv->_state == Conversation::ASK)
prompt = getQuestion(cnv);
else if (cnv->_state == Conversation::GIVEBEGGAR)
prompt = "How much? ";
else if (cnv->_state == Conversation::CONFIRMATION)
prompt = "\n\nHe asks: Art thou well?";
else if (cnv->_state != Conversation::ASKYESNO)
prompt = _dialogue->getPrompt();
return prompt;
}
const char *Person::getChoices(Conversation *cnv) {
if (isVendor())
return cnv->_script->getChoices().c_str();
switch (cnv->_state) {
case Conversation::CONFIRMATION:
case Conversation::CONTINUEQUESTION:
return "ny\015 \033";
case Conversation::PLAYER:
return "012345678\015 \033";
default:
error("invalid state: %d", cnv->_state);
}
return nullptr;
}
Common::String Person::getIntro(Conversation *cnv) {
if (_npcType == NPC_EMPTY) {
cnv->_state = Conversation::DONE;
return Common::String("Funny, no\nresponse!\n");
}
// As far as I can tell, about 50% of the time they tell you their
// name in the introduction
Response *intro;
if (xu4_random(2) == 0)
intro = _dialogue->getIntro();
else
intro = _dialogue->getLongIntro();
cnv->_state = Conversation::TALK;
Common::String text = processResponse(cnv, intro);
return text;
}
Common::String Person::processResponse(Conversation *cnv, Response *response) {
Common::String text;
const Std::vector<ResponsePart> &parts = response->getParts();
for (const auto &i : parts) {
// check for command triggers
if (i.isCommand())
runCommand(cnv, i);
// otherwise, append response part to reply
else
text += i;
}
return text;
}
void Person::runCommand(Conversation *cnv, const ResponsePart &command) {
if (command == g_responseParts->ASK) {
cnv->_question = _dialogue->getQuestion();
cnv->_state = Conversation::ASK;
} else if (command == g_responseParts->END) {
cnv->_state = Conversation::DONE;
} else if (command == g_responseParts->ATTACK) {
cnv->_state = Conversation::ATTACK;
} else if (command == g_responseParts->BRAGGED) {
g_context->_party->adjustKarma(KA_BRAGGED);
} else if (command == g_responseParts->HUMBLE) {
g_context->_party->adjustKarma(KA_HUMBLE);
} else if (command == g_responseParts->ADVANCELEVELS) {
cnv->_state = Conversation::ADVANCELEVELS;
} else if (command == g_responseParts->HEALCONFIRM) {
cnv->_state = Conversation::CONFIRMATION;
} else if (command == g_responseParts->STARTMUSIC_LB) {
g_music->lordBritish();
} else if (command == g_responseParts->STARTMUSIC_HW) {
g_music->hawkwind();
} else if (command == g_responseParts->STOPMUSIC) {
g_music->playMapMusic();
} else if (command == g_responseParts->HAWKWIND) {
g_context->_party->adjustKarma(KA_HAWKWIND);
} else {
error("unknown command trigger in dialogue response: %s\n", Common::String(command).c_str());
}
}
Common::String Person::getResponse(Conversation *cnv, const char *inquiry) {
Common::String reply;
Virtue v;
const ResponsePart &action = _dialogue->getAction();
reply = "\n";
/* Does the person take action during the conversation? */
if (action == g_responseParts->END) {
runCommand(cnv, action);
return _dialogue->getPronoun() + " turns away!\n";
} else if (action == g_responseParts->ATTACK) {
runCommand(cnv, action);
return Common::String("\n") + getName() + " says: On guard! Fool!";
}
if (_npcType == NPC_TALKER_BEGGAR && scumm_strnicmp(inquiry, "give", 4) == 0) {
reply.clear();
cnv->_state = Conversation::GIVEBEGGAR;
}
else if (scumm_strnicmp(inquiry, "join", 4) == 0 &&
g_context->_party->canPersonJoin(getName(), &v)) {
CannotJoinError join = g_context->_party->join(getName());
if (join == JOIN_SUCCEEDED) {
reply += "I am honored to join thee!";
g_context->_location->_map->removeObject(this);
cnv->_state = Conversation::DONE;
} else {
reply += "Thou art not ";
reply += (join == JOIN_NOT_VIRTUOUS) ? getVirtueAdjective(v) : "experienced";
reply += " enough for me to join thee.";
}
}
else if ((*_dialogue)[inquiry]) {
Dialogue::Keyword *kw = (*_dialogue)[inquiry];
reply = processResponse(cnv, kw->getResponse());
}
else if (settings._debug && scumm_strnicmp(inquiry, "dump", 4) == 0) {
Std::vector<Common::String> words = split(inquiry, " \t");
if (words.size() <= 1)
reply = _dialogue->dump("");
else
reply = _dialogue->dump(words[1]);
}
else
reply += processResponse(cnv, _dialogue->getDefaultAnswer());
return reply;
}
Common::String Person::talkerGetQuestionResponse(Conversation *cnv, const char *answer) {
bool valid = false;
bool yes = false;
char ans = tolower(answer[0]);
if (ans == 'y' || ans == 'n') {
valid = true;
yes = ans == 'y';
}
if (!valid) {
cnv->_state = Conversation::ASKYESNO;
return "Yes or no!";
}
cnv->_state = Conversation::TALK;
return "\n" + processResponse(cnv, cnv->_question->getResponse(yes));
}
Common::String Person::beggarGetQuantityResponse(Conversation *cnv, const char *response) {
Common::String reply;
cnv->_quant = (int) strtol(response, nullptr, 10);
cnv->_state = Conversation::TALK;
if (cnv->_quant > 0) {
if (g_context->_party->donate(cnv->_quant)) {
reply = "\n";
reply += _dialogue->getPronoun();
reply += " says: Oh Thank thee! I shall never forget thy kindness!\n";
}
else
reply = "\n\nThou hast not that much gold!\n";
} else
reply = "\n";
return reply;
}
Common::String Person::lordBritishGetQuestionResponse(Conversation *cnv, const char *answer) {
Common::String reply;
cnv->_state = Conversation::TALK;
if (tolower(answer[0]) == 'y') {
reply = "Y\n\nHe says: That is good.\n";
}
else if (tolower(answer[0]) == 'n') {
reply = "N\n\nHe says: Let me heal thy wounds!\n";
cnv->_state = Conversation::FULLHEAL;
}
else
reply = "\n\nThat I cannot\nhelp thee with.\n";
return reply;
}
Common::String Person::getQuestion(Conversation *cnv) {
return "\n" + cnv->_question->getText() + "\n\nYou say: ";
}
/**
* Returns the number of characters needed to get to
* the next line of text (based on column width).
*/
int chars_to_next_line(const char *s, int columnmax) {
int chars = -1;
if (strlen(s) > 0) {
int lastbreak = columnmax;
chars = 0;
for (const char *str = s; *str; str++) {
if (*str == '\n')
return (str - s);
else if (*str == ' ')
lastbreak = (str - s);
else if (++chars >= columnmax)
return lastbreak;
}
}
return chars;
}
/**
* Counts the number of lines (of the maximum width given by
* columnmax) in the Common::String.
*/
int linecount(const Common::String &s, int columnmax) {
int lines = 0;
unsigned ch = 0;
while (ch < s.size()) {
ch += chars_to_next_line(s.c_str() + ch, columnmax);
if (ch < s.size())
ch++;
lines++;
}
return lines;
}
/**
* Returns the number of characters needed to produce a
* valid screen of text (given a column width and row height)
*/
int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines) {
int chars = 0,
totalChars = 0;
Common::String new_str = s;
const char *str = new_str.c_str();
// try breaking text into paragraphs first
Common::String text = s;
Common::String paragraphs;
uint pos;
int lines = 0;
while ((pos = text.find("\n\n")) < text.size()) {
Common::String p = text.substr(0, pos);
lines += linecount(p.c_str(), columnmax);
if (lines <= linesdesired)
paragraphs += p + "\n";
else
break;
text = text.substr(pos + 1);
}
// Seems to be some sort of clang compilation bug in this code, that causes this addition
// to not work correctly.
int totalPossibleLines = lines + linecount(text.c_str(), columnmax);
if (totalPossibleLines <= linesdesired)
paragraphs += text;
if (!paragraphs.empty()) {
*real_lines = lines;
return paragraphs.size();
} else {
// reset variables and try another way
lines = 1;
}
// gather all the line breaks
while ((chars = chars_to_next_line(str, columnmax)) >= 0) {
if (++lines >= linesdesired)
break;
int num_to_move = chars;
if (*(str + num_to_move) == '\n')
num_to_move++;
totalChars += num_to_move;
str += num_to_move;
}
*real_lines = lines;
return totalChars;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,109 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_PERSON_H
#define ULTIMA4_GAME_PERSON_H
#include "ultima/ultima4/game/creature.h"
#include "ultima/ultima4/core/types.h"
namespace Ultima {
namespace Ultima4 {
class Conversation;
class Dialogue;
class Response;
class ResponsePart;
typedef enum {
NPC_EMPTY,
NPC_TALKER,
NPC_TALKER_BEGGAR,
NPC_TALKER_GUARD,
NPC_TALKER_COMPANION,
NPC_VENDOR_WEAPONS,
NPC_VENDOR_ARMOR,
NPC_VENDOR_FOOD,
NPC_VENDOR_TAVERN,
NPC_VENDOR_REAGENTS,
NPC_VENDOR_HEALER,
NPC_VENDOR_INN,
NPC_VENDOR_GUILD,
NPC_VENDOR_STABLE,
NPC_LORD_BRITISH,
NPC_HAWKWIND,
NPC_MAX
} PersonNpcType;
class Person : public Creature {
public:
Person(MapTile tile);
Person(const Person *p);
bool canConverse() const;
bool isVendor() const;
Common::String getName() const override;
void goToStartLocation();
void setDialogue(Dialogue *d);
MapCoords &getStart() {
return _start;
}
PersonNpcType getNpcType() const {
return _npcType;
}
void setNpcType(PersonNpcType t);
Common::List<Common::String> getConversationText(Conversation *cnv, const char *inquiry);
/**
* Get the prompt shown after each reply.
*/
Common::String getPrompt(Conversation *cnv);
/**
* Returns the valid keyboard choices for a given conversation.
*/
const char *getChoices(Conversation *cnv);
Common::String getIntro(Conversation *cnv);
Common::String processResponse(Conversation *cnv, Response *response);
void runCommand(Conversation *cnv, const ResponsePart &command);
Common::String getResponse(Conversation *cnv, const char *inquiry);
Common::String talkerGetQuestionResponse(Conversation *cnv, const char *inquiry);
Common::String beggarGetQuantityResponse(Conversation *cnv, const char *response);
Common::String lordBritishGetQuestionResponse(Conversation *cnv, const char *answer);
Common::String getQuestion(Conversation *cnv);
private:
Dialogue *_dialogue;
MapCoords _start;
PersonNpcType _npcType;
};
bool isPerson(Object *punknown);
Common::List<Common::String> replySplit(const Common::String &text);
int linecount(const Common::String &s, int columnmax);
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_PLAYER_H
#define ULTIMA4_GAME_PLAYER_H
#include "ultima/ultima4/game/creature.h"
#include "ultima/ultima4/map/direction.h"
#include "ultima/ultima4/core/observable.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/game/script.h"
#include "ultima/ultima4/map/tile.h"
#include "ultima/ultima4/core/types.h"
namespace Ultima {
namespace Ultima4 {
class Armor;
class Party;
class Weapon;
typedef Std::vector<class PartyMember *> PartyMemberVector;
#define ALL_PLAYERS -1
enum KarmaAction {
KA_FOUND_ITEM,
KA_STOLE_CHEST,
KA_GAVE_TO_BEGGAR,
KA_GAVE_ALL_TO_BEGGAR,
KA_BRAGGED,
KA_HUMBLE,
KA_HAWKWIND,
KA_MEDITATION,
KA_BAD_MANTRA,
KA_ATTACKED_GOOD,
KA_FLED_EVIL,
KA_FLED_GOOD,
KA_HEALTHY_FLED_EVIL,
KA_KILLED_EVIL,
KA_SPARED_GOOD,
KA_DONATED_BLOOD,
KA_DIDNT_DONATE_BLOOD,
KA_CHEAT_REAGENTS,
KA_DIDNT_CHEAT_REAGENTS,
KA_USED_SKULL,
KA_DESTROYED_SKULL
};
enum HealType {
HT_NONE,
HT_CURE,
HT_FULLHEAL,
HT_RESURRECT,
HT_HEAL,
HT_CAMPHEAL,
HT_INNHEAL
};
enum InventoryItem {
INV_NONE,
INV_WEAPON,
INV_ARMOR,
INV_FOOD,
INV_REAGENT,
INV_GUILDITEM,
INV_HORSE
};
enum CannotJoinError {
JOIN_SUCCEEDED,
JOIN_NOT_EXPERIENCED,
JOIN_NOT_VIRTUOUS
};
enum EquipError {
EQUIP_SUCCEEDED,
EQUIP_NONE_LEFT,
EQUIP_CLASS_RESTRICTED
};
/**
* PartyMember class
*/
class PartyMember : public Creature, public Script::Provider {
public:
PartyMember(Party *p, SaveGamePlayerRecord *pr);
virtual ~PartyMember();
/**
* Notify the party that this player has changed somehow
*/
void notifyOfChange();
/**
* Used to translate script values into something useful
*/
Common::String translate(Std::vector<Common::String> &parts) override;
// Accessor methods
int getHp() const override;
int getMaxHp() const {
return _player->_hpMax;
}
int getExp() const {
return _player->_xp;
}
int getStr() const {
return _player->_str;
}
int getDex() const {
return _player->_dex;
}
int getInt() const {
return _player->_intel;
}
int getMp() const {
return _player->_mp;
}
/**
* Determine the most magic points a character could have
* given his class and intelligence.
*/
int getMaxMp() const;
const Weapon *getWeapon() const;
const Armor *getArmor() const;
Common::String getName() const override;
SexType getSex() const;
ClassType getClass() const;
CreatureStatus getState() const override;
/**
* Determine what level a character has.
*/
int getRealLevel() const;
/**
* Determine the highest level a character could have with the number
* of experience points he has.
*/
int getMaxLevel() const;
/**
* Adds a status effect to the player
*/
void addStatus(StatusType status) override;
/**
* Adjusts the player's mp by 'pts'
*/
void adjustMp(int pts);
/**
* Advances the player to the next level if they have enough experience
*/
void advanceLevel();
/**
* Apply an effect to the party member
*/
void applyEffect(TileEffect effect);
/**
* Award a player experience points. Maxs out the players xp at 9999.
*/
void awardXp(int xp);
/**
* Perform a certain type of healing on the party member
*/
bool heal(HealType type);
/**
* Remove status effects from the party member
*/
void removeStatus(StatusType status) override;
void setHp(int hp) override;
void setMp(int mp);
EquipError setArmor(const Armor *a);
EquipError setWeapon(const Weapon *w);
/**
* Applies damage to a player, and changes status to dead if hit
* points drop below zero.
*
* Byplayer is ignored for now, since it should always be false for U4. (Is
* there anything special about being killed by a party member in U5?) Also
* keeps interface consistent for virtual base function Creature::applydamage()
*/
bool applyDamage(int damage, bool byplayer = false) override;
int getAttackBonus() const override;
int getDefense() const override;
bool dealDamage(Creature *m, int damage) override;
/**
* Calculate damage for an attack.
*/
int getDamage();
/**
* Returns the tile that will be displayed when the party
* member's attack hits
*/
const Common::String &getHitTile() const override;
/**
* Returns the tile that will be displayed when the party
* member's attack fails
*/
const Common::String &getMissTile() const override;
bool isDead();
bool isDisabled();
/**
* Lose the equipped weapon for the player (flaming oil, ranged daggers, etc.)
* Returns the number of weapons left of that type, including the one in
* the players hand
*/
int loseWeapon();
/**
* Put the party member to sleep
*/
void putToSleep() override;
/**
* Wakes up the party member
*/
void wakeUp() override;
protected:
static MapTile tileForClass(int klass);
SaveGamePlayerRecord *_player;
class Party *_party;
};
/**
* Party class
*/
class PartyEvent {
public:
enum Type {
GENERIC,
LOST_EIGHTH,
ADVANCED_LEVEL,
STARVING,
TRANSPORT_CHANGED,
PLAYER_KILLED,
ACTIVE_PLAYER_CHANGED,
MEMBER_JOINED,
PARTY_REVIVED,
INVENTORY_ADDED
};
PartyEvent(Type type, PartyMember *partyMember) : _type(type), _player(partyMember) { }
Type _type;
PartyMember *_player;
};
typedef Std::vector<PartyMember *> PartyMemberVector;
class Party : public Observable<Party *, PartyEvent &>, public Script::Provider {
friend class PartyMember;
public:
Party(SaveGame *saveGame);
virtual ~Party();
/**
* Notify the party that something about it has changed
*/
void notifyOfChange(PartyMember *partyMember = 0, PartyEvent::Type = PartyEvent::GENERIC);
// Used to translate script values into something useful
Common::String translate(Std::vector<Common::String> &parts) override;
void adjustFood(int food);
void adjustGold(int gold);
/**
* Adjusts the avatar's karma level for the given action. Notify
* observers with a lost eighth event if the player has lost
* avatarhood.
*/
void adjustKarma(KarmaAction action);
/**
* Apply effects to the entire party
*/
void applyEffect(TileEffect effect);
/**
* Attempt to elevate in the given virtue
*/
bool attemptElevation(Virtue virtue);
/**
* Burns a torch's duration down a certain number of turns
*/
void burnTorch(int turns = 1);
/**
* Returns true if the party can enter the shrine
*/
bool canEnterShrine(Virtue virtue);
/**
* Returns true if the person can join the party
*/
bool canPersonJoin(Common::String name, Virtue *v);
/**
* Damages the party's ship
*/
void damageShip(uint pts);
/**
* Donates 'quantity' gold. Returns true if the donation succeeded,
* or false if there was not enough gold to make the donation
*/
bool donate(int quantity);
/**
* Ends the party's turn
*/
void endTurn();
/**
* Adds a chest worth of gold to the party's inventory
*/
int getChest();
/**
* Returns the number of turns a currently lit torch will last (or 0 if no torch lit)
*/
int getTorchDuration() const;
/**
* Heals the ship's hull strength by 'pts' points
*/
void healShip(uint pts);
/**
* Returns true if the balloon is currently in the air
*/
bool isFlying() const;
/**
* Whether or not the party can make an action.
*/
bool isImmobilized();
/**
* Whether or not all the party members are dead.
*/
bool isDead();
/**
* Returns true if the person with that name
* is already in the party
*/
bool isPersonJoined(Common::String name);
/**
* Attempts to add the person to the party.
* Returns JOIN_SUCCEEDED if successful.
*/
CannotJoinError join(Common::String name);
/**
* Lights a torch with a default duration of 100
*/
bool lightTorch(int duration = 100, bool loseTorch = true);
/**
* Extinguishes a torch
*/
void quenchTorch();
/**
* Revives the party after the entire party has been killed
*/
void reviveParty();
MapTile getTransport() const;
void setTransport(MapTile transport);
void setShipHull(int str);
Direction getDirection() const;
void setDirection(Direction dir);
void adjustReagent(int reagent, int amt);
int getReagent(int reagent) const;
short *getReagentPtr(int reagent) const;
void setActivePlayer(int p);
int getActivePlayer() const;
void swapPlayers(int p1, int p2);
/**
* Returns the size of the party
*/
int size() const;
/**
* Returns a pointer to the party member indicated
*/
PartyMember *member(int index) const;
private:
void syncMembers();
PartyMemberVector _members;
SaveGame *_saveGame;
MapTile _transport;
int _torchDuration;
int _activePlayer;
#ifdef IOS_ULTIMA4
friend void U4IOS::syncPartyMembersWithSaveGame();
#endif
};
bool isPartyMember(Object *punknown);
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,175 @@
/* 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/game/portal.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/map/dungeon.h"
#include "ultima/ultima4/game/game.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/shrine.h"
#include "ultima/ultima4/map/tile.h"
namespace Ultima {
namespace Ultima4 {
void createDngLadder(Location *location, PortalTriggerAction action, Portal *p) {
if (!p) return;
else {
p->_destid = location->_map->_id;
if (action == ACTION_KLIMB && location->_coords.z == 0) {
p->_exitPortal = true;
p->_destid = 1;
} else p->_exitPortal = false;
p->_message = "";
p->_portalConditionsMet = nullptr;
p->_portalTransportRequisites = TRANSPORT_FOOT_OR_HORSE;
p->_retroActiveDest = nullptr;
p->_saveLocation = false;
p->_start = location->_coords;
p->_start.z += (action == ACTION_KLIMB) ? -1 : 1;
}
}
int usePortalAt(Location *location, MapCoords coords, PortalTriggerAction action) {
Map *destination;
char msg[32] = {0};
const Portal *portal = location->_map->portalAt(coords, action);
Portal dngLadder;
/* didn't find a portal there */
if (!portal) {
/* if it's a dungeon, then ladders are predictable. Create one! */
if (location->_context == CTX_DUNGEON) {
Dungeon *dungeon = dynamic_cast<Dungeon *>(location->_map);
assert(dungeon);
if ((action & ACTION_KLIMB) && dungeon->ladderUpAt(coords))
createDngLadder(location, action, &dngLadder);
else if ((action & ACTION_DESCEND) && dungeon->ladderDownAt(coords))
createDngLadder(location, action, &dngLadder);
else
return 0;
portal = &dngLadder;
} else {
return 0;
}
}
/* conditions not met for portal to work */
if (portal && portal->_portalConditionsMet && !(*portal->_portalConditionsMet)(portal))
return 0;
/* must klimb or descend on foot! */
else if (g_context->_transportContext & ~TRANSPORT_FOOT && (action == ACTION_KLIMB || action == ACTION_DESCEND)) {
g_screen->screenMessage("%sOnly on foot!\n", action == ACTION_KLIMB ? "Klimb\n" : "");
return 1;
}
destination = mapMgr->get(portal->_destid);
if (portal->_message.empty()) {
switch (action) {
case ACTION_DESCEND:
Common::sprintf_s(msg, "Descend down to level %d\n", portal->_start.z + 1);
break;
case ACTION_KLIMB:
if (portal->_exitPortal)
Common::sprintf_s(msg, "Klimb up!\nLeaving...\n");
else
Common::sprintf_s(msg, "Klimb up!\nTo level %d\n", portal->_start.z + 1);
break;
case ACTION_ENTER:
switch (destination->_type) {
case Map::CITY: {
City *city = dynamic_cast<City *>(destination);
assert(city);
g_screen->screenMessage("Enter %s!\n\n%s\n\n", city->_type.c_str(), city->getName().c_str());
}
break;
case Map::SHRINE:
g_screen->screenMessage("Enter the %s!\n\n", destination->getName().c_str());
break;
case Map::DUNGEON:
#ifdef IOS_ULTIMA4
U4IOS::testFlightPassCheckPoint("Enter " + destination->getName());
#endif
g_screen->screenMessage("Enter dungeon!\n\n%s\n\n", destination->getName().c_str());
break;
default:
break;
}
break;
case ACTION_NONE:
default:
break;
}
}
/* check the transportation requisites of the portal */
if (g_context->_transportContext & ~portal->_portalTransportRequisites) {
g_screen->screenMessage("Only on foot!\n");
return 1;
}
/* ok, we know the portal is going to work -- now display the custom message, if any */
else if (!portal->_message.empty() || strlen(msg))
g_screen->screenMessage("%s", portal->_message.empty() ? msg : portal->_message.c_str());
/* portal just exits to parent map */
if (portal->_exitPortal) {
g_game->exitToParentMap();
g_music->playMapMusic();
return 1;
} else if (portal->_destid == location->_map->_id)
location->_coords = portal->_start;
else {
g_game->setMap(destination, portal->_saveLocation, portal);
g_music->playMapMusic();
}
/* if the portal changes the map retroactively, do it here */
/*
* note that we use c->location instead of location, since
* location has probably been invalidated above
*/
if (portal->_retroActiveDest && g_context->_location->_prev) {
g_context->_location->_prev->_coords = portal->_retroActiveDest->_coords;
g_context->_location->_prev->_map = mapMgr->get(portal->_retroActiveDest->_mapid);
}
if (destination->_type == Map::SHRINE) {
Shrine *shrine = dynamic_cast<Shrine *>(destination);
assert(shrine);
shrine->enter();
}
return 1;
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,82 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_PORTAL_H
#define ULTIMA4_GAME_PORTAL_H
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/map/map.h"
namespace Ultima {
namespace Ultima4 {
class Map;
class Location;
struct Portal;
typedef enum {
ACTION_NONE = 0x0,
ACTION_ENTER = 0x1,
ACTION_KLIMB = 0x2,
ACTION_DESCEND = 0x4,
ACTION_EXIT_NORTH = 0x8,
ACTION_EXIT_EAST = 0x10,
ACTION_EXIT_SOUTH = 0x20,
ACTION_EXIT_WEST = 0x40
} PortalTriggerAction;
typedef bool (*PortalConditionsMet)(const Portal *p);
struct PortalDestination {
MapCoords _coords;
MapId _mapid;
};
struct Portal {
MapCoords _coords;
MapId _destid;
MapCoords _start;
PortalTriggerAction _triggerAction;
PortalConditionsMet _portalConditionsMet;
PortalDestination *_retroActiveDest;
bool _saveLocation;
Common::String _message;
TransportContext _portalTransportRequisites;
bool _exitPortal;
int _tile;
};
/**
* Creates a dungeon ladder portal based on the action given
*/
void createDngLadder(Location *location, PortalTriggerAction action, Portal *p);
/**
* Finds a portal at the given (x,y,z) coords that will work with the action given
* and uses it. If in a dungeon and trying to use a ladder, it creates a portal
* based on the ladder and uses it.
*/
int usePortalAt(Location *location, MapCoords coords, PortalTriggerAction action);
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,443 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_SCRIPT_H
#define ULTIMA4_GAME_SCRIPT_H
#include "ultima/ultima4/core/types.h"
#include "ultima/shared/conf/xml_node.h"
#include "ultima/shared/std/containers.h"
#include "common/file.h"
#include "common/list.h"
namespace Ultima {
namespace Ultima4 {
/**
* An xml-scripting class. It loads and runs xml scripts that
* take information and interact with the game environment itself.
* Currently, it is mainly useful for writing vendor code; however,
* it should be possible to write scripts for other parts of the
* game.
*
* @todo
* <ul>
* <li>Strip vendor-specific code from the language</li>
* <li>Fill in some of the missing integration with the game</li>
* </ul>
*/
class Script {
public:
/**
* A class that provides information to a script. It is designed to
* translate qualifiers and identifiers in a script to another value.
* Each provider is assigned a qualifier that the script uses to
* select a provider. The provider then uses the rest of the information
* to translate the information to something useful.
*/
class Provider {
public:
virtual ~Provider() {}
virtual Common::String translate(Std::vector<Common::String> &parts) = 0;
};
private:
/**
* A class that represents a script variable
*/
class Variable {
public:
Variable();
Variable(const Common::String &v);
Variable(const int &v);
int &getInt();
Common::String &getString();
void setValue(const int &v);
void setValue(const Common::String &v);
void unset();
bool isInt() const;
bool isString() const;
bool isSet() const;
private:
int _iVal;
Common::String _sVal;
bool _set;
};
public:
/**
* A script return code
*/
enum ReturnCode {
RET_OK,
RET_REDIRECTED,
RET_STOP
};
/**
* The current state of the script
*/
enum State {
STATE_UNLOADED,
STATE_NORMAL,
STATE_DONE,
STATE_INPUT
};
/**
* The type of input the script is requesting
*/
enum InputType {
INPUT_CHOICE,
INPUT_NUMBER,
INPUT_STRING,
INPUT_DIRECTION,
INPUT_PLAYER,
INPUT_KEYPRESS
};
/**
* The action that the script is taking
*/
enum Action {
ACTION_SET_CONTEXT,
ACTION_UNSET_CONTEXT,
ACTION_END,
ACTION_REDIRECT,
ACTION_WAIT_FOR_KEY,
ACTION_WAIT,
ACTION_STOP,
ACTION_INCLUDE,
ACTION_FOR_LOOP,
ACTION_RANDOM,
ACTION_MOVE,
ACTION_SLEEP,
ACTION_CURSOR,
ACTION_PAY,
ACTION_IF,
ACTION_INPUT,
ACTION_ADD,
ACTION_LOSE,
ACTION_HEAL,
ACTION_CAST_SPELL,
ACTION_DAMAGE,
ACTION_KARMA,
ACTION_MUSIC,
ACTION_SET_VARIABLE,
ACTION_ZTATS
};
/**
* Constructs a script object
*/
Script();
~Script();
/**
* Adds an information provider for the script
*/
void addProvider(const Common::String &name, Provider *p);
/**
* Loads the vendor script
*/
bool load(const Common::String &filename, const Common::String &baseId, const Common::String &subNodeName = "", const Common::String &subNodeId = "");
/**
* Unloads the script
*/
void unload();
/**
* Runs a script after it's been loaded
*/
void run(const Common::String &script);
/**
* Executes the subscript 'script' of the main script
*/
ReturnCode execute(Shared::XMLNode *script, Shared::XMLNode *currentItem = nullptr, Common::String *output = nullptr);
/**
* Continues the script from where it left off, or where the last script indicated
*/
void _continue();
/**
* Set and retrieve property values
*/
void resetState();
void setState(State state);
State getState();
void setTarget(const Common::String &val);
void setChoices(const Common::String &val);
void setVar(const Common::String &name, const Common::String &val);
void setVar(const Common::String &name, int val);
void unsetVar(const Common::String &name);
Common::String getTarget();
InputType getInputType();
Common::String getInputName();
Common::String getChoices();
int getInputMaxLen();
private:
/**
* Translates a script Common::String with dynamic variables
*/
void translate(Common::String *script);
/**
* Finds a subscript of script 'node'
*/
Shared::XMLNode *find(Shared::XMLNode *node, const Common::String &script, const Common::String &choice = "", bool _default = false);
/**
* Gets a property as Common::String from the script, and
* translates it using scriptTranslate.
*/
Common::String getPropAsStr(Common::List<Shared::XMLNode *> &nodes, const Common::String &prop, bool recursive);
Common::String getPropAsStr(Shared::XMLNode *node, const Common::String &prop, bool recursive = false);
/**
* Gets a property as int from the script
*/
int getPropAsInt(Common::List<Shared::XMLNode *> &nodes, const Common::String &prop, bool recursive);
int getPropAsInt(Shared::XMLNode *node, const Common::String &prop, bool recursive = false);
/**
* Gets the content of a script node
*/
Common::String getContent(Shared::XMLNode *node);
/*
* Action Functions
*/
/**
* Sets a new translation context for the script
*/
ReturnCode pushContext(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Removes a node from the translation context
*/
ReturnCode popContext(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* End script execution
*/
ReturnCode end(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Wait for keypress from the user
*/
ReturnCode waitForKeypress(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Redirects script execution to another script
*/
ReturnCode redirect(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Includes a script to be executed
*/
ReturnCode include(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Waits a given number of milliseconds before continuing execution
*/
ReturnCode wait(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Executes a 'for' loop script
*/
ReturnCode forLoop(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Randomely executes script code
*/
ReturnCode randomScript(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Moves the player's current position
*/
ReturnCode move(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Puts the player to sleep. Useful when coding inn scripts
*/
ReturnCode sleep(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Enables/Disables the keyboard cursor
*/
ReturnCode cursor(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Pay gold to someone
*/
ReturnCode pay(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Perform a limited 'if' statement
*/
ReturnCode _if(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Get input from the player
*/
ReturnCode input(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Add item to inventory
*/
ReturnCode add(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Lose item
*/
ReturnCode lose(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Heals a party member
*/
ReturnCode heal(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Performs all of the visual/audio effects of casting a spell
*/
ReturnCode castSpell(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Apply damage to a player
*/
ReturnCode damage(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Apply karma changes based on the action taken
*/
ReturnCode karma(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Set the currently playing music
*/
ReturnCode music(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Sets a variable
*/
ReturnCode setVar(Shared::XMLNode *script, Shared::XMLNode *current);
/**
* Display a different ztats screen
*/
ReturnCode ztats(Shared::XMLNode *script, Shared::XMLNode *current);
/*
* Math and comparison functions
*/
/**
* Parses a math Common::String's children into results so
* there is only 1 equation remaining.
*
* ie. <math>5*<math>6/3</math></math>
*/
void mathParseChildren(Shared::XMLNode *math, Common::String *result);
/**
* Takes a simple equation Common::String and returns the value
*/
int mathValue(const Common::String &str);
/**
* Performs simple math operations in the script
*/
int math(int lval, int rval, Common::String &op);
/**
* Parses a Common::String into left integer value, right integer value,
* and operator. Returns false if the Common::String is not a valid
* math equation
*/
bool mathParse(const Common::String &str, int *lval, int *rval, Common::String *op);
/**
* Parses a Common::String containing an operator (+, -, *, /, etc.) into 3 parts,
* left, right, and operator.
*/
void parseOperation(const Common::String &str, Common::String *lval, Common::String *rval, Common::String *op);
/**
* Does a boolean comparison on a Common::String (math or Common::String),
* fails if the Common::String doesn't contain a valid comparison
*/
bool compare(const Common::String &str);
/**
* Parses a function into its name and contents
*/
void funcParse(const Common::String &str, Common::String *funcName, Common::String *contents);
/*
* Static variables
*/
private:
typedef Common::HashMap<Common::String, Action> ActionMap;
ActionMap _actionMap;
private:
void removeCurrentVariable(const Common::String &name);
Shared::XMLNode *_vendorScriptDoc;
Shared::XMLNode *_scriptNode;
bool _debug;
State _state; /**< The state the script is in */
Shared::XMLNode *_currentScript; /**< The currently running script */
Shared::XMLNode *_currentItem; /**< The current position in the script */
Common::List<Shared::XMLNode *> _translationContext; /**< A list of nodes that make up our translation context */
Common::String _target; /**< The name of a target script */
InputType _inputType; /**< The type of input required */
Common::String _inputName; /**< The variable in which to place the input (by default, "input") */
int _inputMaxLen; /**< The maximum length allowed for input */
Common::String _nounName; /**< The name that identifies a node name of noun nodes */
Common::String _idPropName; /**< The name of the property that uniquely identifies a noun node
and is used to find a new translation context */
Common::String _choices;
int _iterator;
Common::HashMap<Common::String, Variable *> _variables;
Common::HashMap<Common::String, Provider *> _providers;
};
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,798 @@
/* 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/game/game.h"
#include "ultima/ultima4/game/spell.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/creature.h"
#include "ultima/ultima4/game/moongate.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/controllers/combat_controller.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/core/debugger.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/gfx/screen.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/direction.h"
#include "ultima/ultima4/map/dungeon.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/map/mapmgr.h"
#include "ultima/ultima4/map/tile.h"
#include "ultima/ultima4/map/tileset.h"
#include "ultima/ultima4/ultima4.h"
namespace Ultima {
namespace Ultima4 {
/* masks for reagents */
#define ASH (1 << REAG_ASH)
#define GINSENG (1 << REAG_GINSENG)
#define GARLIC (1 << REAG_GARLIC)
#define SILK (1 << REAG_SILK)
#define MOSS (1 << REAG_MOSS)
#define PEARL (1 << REAG_PEARL)
#define NIGHTSHADE (1 << REAG_NIGHTSHADE)
#define MANDRAKE (1 << REAG_MANDRAKE)
/* spell error messages */
static const struct {
SpellCastError err;
const char *msg;
} SPELL_ERROR_MSGS[] = {
{ CASTERR_NOMIX, "None Mixed!\n" },
{ CASTERR_MPTOOLOW, "Not Enough MP!\n" },
{ CASTERR_FAILED, "Failed!\n" },
{ CASTERR_WRONGCONTEXT, "Not here!\n" },
{ CASTERR_COMBATONLY, "Combat only!\nFailed!\n" },
{ CASTERR_DUNGEONONLY, "Dungeon only!\nFailed!\n" },
{ CASTERR_WORLDMAPONLY, "Outdoors only!\nFailed!\n" }
};
const Spell Spells::SPELL_LIST[N_SPELLS] = {
{ "Awaken", GINSENG | GARLIC, CTX_ANY, TRANSPORT_ANY, &Spells::spellAwaken, Spell::PARAM_PLAYER, 5 },
{
"Blink", SILK | MOSS, CTX_WORLDMAP, TRANSPORT_FOOT_OR_HORSE,
&Spells::spellBlink, Spell::PARAM_DIR, 15
},
{ "Cure", GINSENG | GARLIC, CTX_ANY, TRANSPORT_ANY, &Spells::spellCure, Spell::PARAM_PLAYER, 5 },
{ "Dispell", ASH | GARLIC | PEARL, CTX_ANY, TRANSPORT_ANY, &Spells::spellDispel, Spell::PARAM_DIR, 20 },
{
"Energy Field", ASH | SILK | PEARL, (LocationContext)(CTX_COMBAT | CTX_DUNGEON),
TRANSPORT_ANY, &Spells::spellEField, Spell::PARAM_TYPEDIR, 10
},
{ "Fireball", ASH | PEARL, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellFireball, Spell::PARAM_DIR, 15 },
{
"Gate", ASH | PEARL | MANDRAKE, CTX_WORLDMAP, TRANSPORT_FOOT_OR_HORSE,
&Spells::spellGate, Spell::PARAM_PHASE, 40
},
{ "Heal", GINSENG | SILK, CTX_ANY, TRANSPORT_ANY, &Spells::spellHeal, Spell::PARAM_PLAYER, 10 },
{ "Iceball", PEARL | MANDRAKE, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellIceball, Spell::PARAM_DIR, 20 },
{
"Jinx", PEARL | NIGHTSHADE | MANDRAKE,
CTX_ANY, TRANSPORT_ANY, &Spells::spellJinx, Spell::PARAM_NONE, 30
},
{ "Kill", PEARL | NIGHTSHADE, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellKill, Spell::PARAM_DIR, 25 },
{ "Light", ASH, CTX_DUNGEON, TRANSPORT_ANY, &Spells::spellLight, Spell::PARAM_NONE, 5 },
{ "Magic missile", ASH | PEARL, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellMMissle, Spell::PARAM_DIR, 5 },
{ "Negate", ASH | GARLIC | MANDRAKE, CTX_ANY, TRANSPORT_ANY, &Spells::spellNegate, Spell::PARAM_NONE, 20 },
{ "Open", ASH | MOSS, CTX_ANY, TRANSPORT_ANY, &Spells::spellOpen, Spell::PARAM_NONE, 5 },
{ "Protection", ASH | GINSENG | GARLIC, CTX_ANY, TRANSPORT_ANY, &Spells::spellProtect, Spell::PARAM_NONE, 15 },
{ "Quickness", ASH | GINSENG | MOSS, CTX_ANY, TRANSPORT_ANY, &Spells::spellQuick, Spell::PARAM_NONE, 20 },
{
"Resurrect", ASH | GINSENG | GARLIC | SILK | MOSS | MANDRAKE,
CTX_NON_COMBAT, TRANSPORT_ANY, &Spells::spellRez, Spell::PARAM_PLAYER, 45
},
{ "Sleep", SILK | GINSENG, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellSleep, Spell::PARAM_NONE, 15 },
{ "Tremor", ASH | MOSS | MANDRAKE, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellTremor, Spell::PARAM_NONE, 30 },
{ "Undead", ASH | GARLIC, CTX_COMBAT, TRANSPORT_ANY, &Spells::spellUndead, Spell::PARAM_NONE, 15 },
{ "View", NIGHTSHADE | MANDRAKE, CTX_NON_COMBAT, TRANSPORT_ANY, &Spells::spellView, Spell::PARAM_NONE, 15 },
{ "Winds", ASH | MOSS, CTX_WORLDMAP, TRANSPORT_ANY, &Spells::spellWinds, Spell::PARAM_FROMDIR, 10 },
{ "X-it", ASH | SILK | MOSS, CTX_DUNGEON, TRANSPORT_ANY, &Spells::spellXit, Spell::PARAM_NONE, 15 },
{ "Y-up", SILK | MOSS, CTX_DUNGEON, TRANSPORT_ANY, &Spells::spellYup, Spell::PARAM_NONE, 10 },
{ "Z-down", SILK | MOSS, CTX_DUNGEON, TRANSPORT_ANY, &Spells::spellZdown, Spell::PARAM_NONE, 5 }
};
/*-------------------------------------------------------------------*/
Ingredients::Ingredients() {
memset(_reagents, 0, sizeof(_reagents));
}
bool Ingredients::addReagent(Reagent reagent) {
assertMsg(reagent < REAG_MAX, "invalid reagent: %d", reagent);
if (g_context->_party->getReagent(reagent) < 1)
return false;
g_context->_party->adjustReagent(reagent, -1);
_reagents[reagent]++;
return true;
}
bool Ingredients::removeReagent(Reagent reagent) {
assertMsg(reagent < REAG_MAX, "invalid reagent: %d", reagent);
if (_reagents[reagent] == 0)
return false;
g_context->_party->adjustReagent(reagent, 1);
_reagents[reagent]--;
return true;
}
int Ingredients::getReagent(Reagent reagent) const {
assertMsg(reagent < REAG_MAX, "invalid reagent: %d", reagent);
return _reagents[reagent];
}
void Ingredients::revert() {
int reg;
for (reg = 0; reg < REAG_MAX; reg++) {
g_ultima->_saveGame->_reagents[reg] += _reagents[reg];
_reagents[reg] = 0;
}
}
bool Ingredients::checkMultiple(int batches) const {
for (int i = 0; i < REAG_MAX; i++) {
/* see if there's enough reagents to mix (-1 because one is already counted) */
if (_reagents[i] > 0 && g_ultima->_saveGame->_reagents[i] < batches - 1) {
return false;
}
}
return true;
}
void Ingredients::multiply(int batches) {
assertMsg(checkMultiple(batches), "not enough reagents to multiply ingredients by %d\n", batches);
for (int i = 0; i < REAG_MAX; i++) {
if (_reagents[i] > 0) {
g_ultima->_saveGame->_reagents[i] -= batches - 1;
_reagents[i] += batches - 1;
}
}
}
/*-------------------------------------------------------------------*/
Spells *g_spells;
Spells::Spells() : spellEffectCallback(nullptr) {
g_spells = this;
}
Spells::~Spells() {
g_spells = nullptr;
}
void Spells::spellSetEffectCallback(SpellEffectCallback callback) {
spellEffectCallback = callback;
}
const char *Spells::spellGetName(uint spell) const {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
return SPELL_LIST[spell]._name;
}
int Spells::spellGetRequiredMP(uint spell) const {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
return SPELL_LIST[spell]._mp;
}
LocationContext Spells::spellGetContext(uint spell) const {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
return SPELL_LIST[spell]._context;
}
TransportContext Spells::spellGetTransportContext(uint spell) const {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
return SPELL_LIST[spell]._transportContext;
}
Common::String Spells::spellGetErrorMessage(uint spell, SpellCastError error) {
uint i;
SpellCastError err = error;
/* try to find a more specific error message */
if (err == CASTERR_WRONGCONTEXT) {
switch (SPELL_LIST[spell]._context) {
case CTX_COMBAT:
err = CASTERR_COMBATONLY;
break;
case CTX_DUNGEON:
err = CASTERR_DUNGEONONLY;
break;
case CTX_WORLDMAP:
err = CASTERR_WORLDMAPONLY;
break;
default:
break;
}
}
/* find the message that we're looking for and return it! */
for (i = 0; i < sizeof(SPELL_ERROR_MSGS) / sizeof(SPELL_ERROR_MSGS[0]); i++) {
if (err == SPELL_ERROR_MSGS[i].err)
return Common::String(SPELL_ERROR_MSGS[i].msg);
}
return Common::String();
}
int Spells::spellMix(uint spell, const Ingredients *ingredients) {
int regmask, reg;
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
regmask = 0;
for (reg = 0; reg < REAG_MAX; reg++) {
if (ingredients->getReagent((Reagent) reg) > 0)
regmask |= (1 << reg);
}
if (regmask != SPELL_LIST[spell]._components)
return 0;
g_ultima->_saveGame->_mixtures[spell]++;
return 1;
}
Spell::Param Spells::spellGetParamType(uint spell) const {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
return SPELL_LIST[spell]._paramType;
}
bool Spells::isDebuggerActive() const {
return g_ultima->getDebugger()->isActive();
}
SpellCastError Spells::spellCheckPrerequisites(uint spell, int character) {
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
assertMsg(character >= 0 && character < g_ultima->_saveGame->_members, "character out of range: %d", character);
// Don't bother checking mix count and map when the spell
// has been manually triggered from the debugger
if (!isDebuggerActive()) {
if (g_ultima->_saveGame->_mixtures[spell] == 0)
return CASTERR_NOMIX;
if (g_context->_party->member(character)->getMp() < SPELL_LIST[spell]._mp)
return CASTERR_MPTOOLOW;
}
if ((g_context->_location->_context & SPELL_LIST[spell]._context) == 0)
return CASTERR_WRONGCONTEXT;
if ((g_context->_transportContext & SPELL_LIST[spell]._transportContext) == 0)
return CASTERR_FAILED;
return CASTERR_NOERROR;
}
bool Spells::spellCast(uint spell, int character, int param, SpellCastError *error, bool spellEffect) {
int subject = (SPELL_LIST[spell]._paramType == Spell::PARAM_PLAYER) ? param : -1;
PartyMember *p = g_context->_party->member(character);
assertMsg(spell < N_SPELLS, "invalid spell: %d", spell);
assertMsg(character >= 0 && character < g_ultima->_saveGame->_members, "character out of range: %d", character);
*error = spellCheckPrerequisites(spell, character);
if (!isDebuggerActive())
// Subtract the mixture for even trying to cast the spell
AdjustValueMin(g_ultima->_saveGame->_mixtures[spell], -1, 0);
if (*error != CASTERR_NOERROR)
return false;
// If there's a negate magic aura, spells fail!
if (*g_context->_aura == Aura::NEGATE) {
*error = CASTERR_FAILED;
return false;
}
if (!isDebuggerActive())
// Subtract the mp needed for the spell
p->adjustMp(-SPELL_LIST[spell]._mp);
if (spellEffect) {
int time;
/* recalculate spell speed - based on 5/sec */
float MP_OF_LARGEST_SPELL = 45;
int spellMp = SPELL_LIST[spell]._mp;
time = int(10000.0 / settings._spellEffectSpeed * spellMp / MP_OF_LARGEST_SPELL);
soundPlay(SOUND_PREMAGIC_MANA_JUMBLE, false, time);
EventHandler::wait_msecs(time);
g_spells->spellEffect(spell + 'a', subject, SOUND_MAGIC);
}
if (!(g_spells->*SPELL_LIST[spell]._spellFunc)(param)) {
*error = CASTERR_FAILED;
return false;
}
return true;
}
CombatController *Spells::spellCombatController() {
CombatController *cc = dynamic_cast<CombatController *>(eventHandler->getController());
return cc;
}
void Spells::spellMagicAttack(const Common::String &tilename, Direction dir, int minDamage, int maxDamage) {
CombatController *controller = spellCombatController();
PartyMemberVector *party = controller->getParty();
MapTile tile = g_context->_location->_map->_tileSet->getByName(tilename)->getId();
int attackDamage = ((minDamage >= 0) && (minDamage < maxDamage)) ?
xu4_random((maxDamage + 1) - minDamage) + minDamage :
maxDamage;
Std::vector<Coords> path = gameGetDirectionalActionPath(MASK_DIR(dir), MASK_DIR_ALL, (*party)[controller->getFocus()]->getCoords(),
1, 11, Tile::canAttackOverTile, false);
for (const auto &coords : path) {
if (spellMagicAttackAt(coords, tile, attackDamage))
return;
}
}
bool Spells::spellMagicAttackAt(const Coords &coords, MapTile attackTile, int attackDamage) {
bool objectHit = false;
// int attackdelay = MAX_BATTLE_SPEED - settings.battleSpeed;
CombatMap *cm = getCombatMap();
Creature *creature = cm->creatureAt(coords);
if (!creature) {
GameController::flashTile(coords, attackTile, 2);
} else {
objectHit = true;
/* show the 'hit' tile */
soundPlay(SOUND_NPC_STRUCK);
GameController::flashTile(coords, attackTile, 3);
/* apply the damage to the creature */
CombatController *controller = spellCombatController();
controller->getCurrentPlayer()->dealDamage(creature, attackDamage);
GameController::flashTile(coords, attackTile, 1);
}
return objectHit;
}
int Spells::spellAwaken(int player) {
assertMsg(player < 8, "player out of range: %d", player);
PartyMember *p = g_context->_party->member(player);
if ((player < g_context->_party->size()) && (p->getStatus() == STAT_SLEEPING)) {
p->wakeUp();
return 1;
}
return 0;
}
int Spells::spellBlink(int dir) {
int i,
failed = 0,
distance,
diff,
*var;
Direction reverseDir = dirReverse((Direction)dir);
MapCoords coords = g_context->_location->_coords;
/* Blink doesn't work near the mouth of the abyss */
/* Note: This means you can teleport to Hythloth from the top of the map,
and that you can teleport to the abyss from the left edge of the map,
Unfortunately, this matches the bugs in the game. :( Consider fixing. */
if (coords.x >= 192 && coords.y >= 192)
return 0;
/* figure out what numbers we're working with */
var = (dir & (DIR_WEST | DIR_EAST)) ? &coords.x : &coords.y;
/* find the distance we are going to move */
distance = (*var) % 0x10;
if (dir == DIR_EAST || dir == DIR_SOUTH)
distance = 0x10 - distance;
/* see if we move another 16 spaces over */
diff = 0x10 - distance;
if ((diff > 0) && (xu4_random(diff * diff) > distance))
distance += 0x10;
/* test our distance, and see if it works */
for (i = 0; i < distance; i++)
coords.move((Direction)dir, g_context->_location->_map);
i = distance;
/* begin walking backward until you find a valid spot */
while ((i-- > 0) && !g_context->_location->_map->tileTypeAt(coords, WITH_OBJECTS)->isWalkable())
coords.move(reverseDir, g_context->_location->_map);
if (g_context->_location->_map->tileTypeAt(coords, WITH_OBJECTS)->isWalkable()) {
/* we didn't move! */
if (g_context->_location->_coords == coords)
failed = 1;
g_context->_location->_coords = coords;
} else {
failed = 1;
}
return (failed ? 0 : 1);
}
int Spells::spellCure(int player) {
assertMsg(player < 8, "player out of range: %d", player);
GameController::flashTile(g_context->_party->member(player)->getCoords(), "wisp", 1);
return g_context->_party->member(player)->heal(HT_CURE);
}
int Spells::spellDispel(int dir) {
MapTile *tile;
MapCoords field;
/*
* get the location of the avatar (or current party member, if in battle)
*/
g_context->_location->getCurrentPosition(&field);
/*
* find where we want to dispel the field
*/
field.move((Direction)dir, g_context->_location->_map);
GameController::flashTile(field, "wisp", 2);
/*
* if there is a field annotation, remove it and replace it with a valid
* replacement annotation. We do this because sometimes dungeon triggers
* create annotations, that, if just removed, leave a wall tile behind
* (or other unwalkable surface). So, we need to provide a valid replacement
* annotation to fill in the gap :)
*/
Annotation::List a = g_context->_location->_map->_annotations->allAt(field);
if (a.size() > 0) {
for (auto &i : a) {
if (i.getTile().getTileType()->canDispel()) {
/*
* get a replacement tile for the field
*/
MapTile newTile(g_context->_location->getReplacementTile(field, i.getTile().getTileType()));
g_context->_location->_map->_annotations->remove(i);
g_context->_location->_map->_annotations->add(field, newTile, false, true);
return 1;
}
}
}
/*
* if the map tile itself is a field, overlay it with a replacement tile
*/
tile = g_context->_location->_map->tileAt(field, WITHOUT_OBJECTS);
if (!tile->getTileType()->canDispel())
return 0;
/*
* get a replacement tile for the field
*/
MapTile newTile(g_context->_location->getReplacementTile(field, tile->getTileType()));
g_context->_location->_map->_annotations->add(field, newTile, false, true);
return 1;
}
int Spells::spellEField(int param) {
MapTile fieldTile(0);
int fieldType;
int dir;
MapCoords coords;
/* Unpack fieldType and direction */
fieldType = param >> 4;
dir = param & 0xF;
/* Make sure params valid */
switch (fieldType) {
case ENERGYFIELD_FIRE:
fieldTile = g_context->_location->_map->_tileSet->getByName("fire_field")->getId();
break;
case ENERGYFIELD_LIGHTNING:
fieldTile = g_context->_location->_map->_tileSet->getByName("energy_field")->getId();
break;
case ENERGYFIELD_POISON:
fieldTile = g_context->_location->_map->_tileSet->getByName("poison_field")->getId();
break;
case ENERGYFIELD_SLEEP:
fieldTile = g_context->_location->_map->_tileSet->getByName("sleep_field")->getId();
break;
default:
return 0;
break;
}
g_context->_location->getCurrentPosition(&coords);
coords.move((Direction)dir, g_context->_location->_map);
if (MAP_IS_OOB(g_context->_location->_map, coords))
return 0;
else {
/*
* Observed behaviour on Amiga version of Ultima IV:
* Field cast on other field: Works, unless original field is lightning
* in which case it doesn't.
* Field cast on creature: Works, creature remains the visible tile
* Field cast on top of field and then dispel = no fields left
* The code below seems to produce this behaviour.
*/
const Tile *tile = g_context->_location->_map->tileTypeAt(coords, WITH_GROUND_OBJECTS);
if (!tile->isWalkable()) return 0;
/* Get rid of old field, if any */
Annotation::List a = g_context->_location->_map->_annotations->allAt(coords);
if (a.size() > 0) {
for (auto &i : a) {
if (i.getTile().getTileType()->canDispel())
g_context->_location->_map->_annotations->remove(i);
}
}
g_context->_location->_map->_annotations->add(coords, fieldTile);
}
return 1;
}
int Spells::spellFireball(int dir) {
spellMagicAttack("hit_flash", (Direction)dir, 24, 128);
return 1;
}
int Spells::spellGate(int phase) {
const Coords *moongate;
GameController::flashTile(g_context->_location->_coords, "moongate", 2);
moongate = g_moongates->getGateCoordsForPhase(phase);
if (moongate)
g_context->_location->_coords = *moongate;
return 1;
}
int Spells::spellHeal(int player) {
assertMsg(player < 8, "player out of range: %d", player);
GameController::flashTile(g_context->_party->member(player)->getCoords(), "wisp", 1);
g_context->_party->member(player)->heal(HT_HEAL);
return 1;
}
int Spells::spellIceball(int dir) {
spellMagicAttack("magic_flash", (Direction)dir, 32, 224);
return 1;
}
int Spells::spellJinx(int unused) {
g_context->_aura->set(Aura::JINX, 10);
return 1;
}
int Spells::spellKill(int dir) {
spellMagicAttack("whirlpool", (Direction)dir, -1, 232);
return 1;
}
int Spells::spellLight(int unused) {
g_context->_party->lightTorch(100, false);
return 1;
}
int Spells::spellMMissle(int dir) {
spellMagicAttack("miss_flash", (Direction)dir, 64, 16);
return 1;
}
int Spells::spellNegate(int unused) {
g_context->_aura->set(Aura::NEGATE, 10);
return 1;
}
int Spells::spellOpen(int unused) {
g_debugger->getChest();
return 1;
}
int Spells::spellProtect(int unused) {
g_context->_aura->set(Aura::PROTECTION, 10);
return 1;
}
int Spells::spellRez(int player) {
assertMsg(player < 8, "player out of range: %d", player);
return g_context->_party->member(player)->heal(HT_RESURRECT);
}
int Spells::spellQuick(int unused) {
g_context->_aura->set(Aura::QUICKNESS, 10);
return 1;
}
int Spells::spellSleep(int unused) {
CombatMap *cm = getCombatMap();
CreatureVector creatures = cm->getCreatures();
/* try to put each creature to sleep */
for (auto *m : creatures) {
Coords coords = m->getCoords();
GameController::flashTile(coords, "wisp", 1);
if ((m->getResists() != EFFECT_SLEEP) &&
xu4_random(0xFF) >= m->getHp()) {
soundPlay(SOUND_POISON_EFFECT);
m->putToSleep();
GameController::flashTile(coords, "sleep_field", 3);
} else
soundPlay(SOUND_EVADE);
}
return 1;
}
int Spells::spellTremor(int unused) {
CombatController *ct = spellCombatController();
CreatureVector creatures = ct->getMap()->getCreatures();
for (auto *m : creatures) {
Coords coords = m->getCoords();
//GameController::flashTile(coords, "rocks", 1);
/* creatures with over 192 hp are unaffected */
if (m->getHp() > 192) {
soundPlay(SOUND_EVADE);
continue;
} else {
/* Deal maximum damage to creature */
if (xu4_random(2) == 0) {
soundPlay(SOUND_NPC_STRUCK);
GameController::flashTile(coords, "hit_flash", 3);
ct->getCurrentPlayer()->dealDamage(m, 0xFF);
}
/* Deal enough damage to creature to make it flee */
else if (xu4_random(2) == 0) {
soundPlay(SOUND_NPC_STRUCK);
GameController::flashTile(coords, "hit_flash", 2);
if (m->getHp() > 23)
ct->getCurrentPlayer()->dealDamage(m, m->getHp() - 23);
} else {
soundPlay(SOUND_EVADE);
}
}
}
return 1;
}
int Spells::spellUndead(int unused) {
CombatController *ct = spellCombatController();
CreatureVector creatures = ct->getMap()->getCreatures();
for (auto *m : creatures) {
if (m && m->isUndead() && xu4_random(2) == 0)
m->setHp(23);
}
return 1;
}
int Spells::spellView(int unsued) {
peer(false);
return 1;
}
int Spells::spellWinds(int fromdir) {
g_context->_windDirection = fromdir;
return 1;
}
int Spells::spellXit(int unused) {
if (!g_context->_location->_map->isWorldMap()) {
g_screen->screenMessage("Leaving...\n");
g_game->exitToParentMap();
g_music->playMapMusic();
return 1;
}
return 0;
}
int Spells::spellYup(int unused) {
MapCoords coords = g_context->_location->_coords;
Dungeon *dungeon = dynamic_cast<Dungeon *>(g_context->_location->_map);
/* can't cast in the Abyss */
if (g_context->_location->_map->_id == MAP_ABYSS)
return 0;
/* staying in the dungeon */
else if (coords.z > 0) {
assert(dungeon);
for (int i = 0; i < 0x20; i++) {
coords = MapCoords(xu4_random(8), xu4_random(8), g_context->_location->_coords.z - 1);
if (dungeon->validTeleportLocation(coords)) {
g_context->_location->_coords = coords;
return 1;
}
}
/* exiting the dungeon */
} else {
g_screen->screenMessage("Leaving...\n");
g_game->exitToParentMap();
g_music->playMapMusic();
return 1;
}
/* didn't find a place to go, failed! */
return 0;
}
int Spells::spellZdown(int unused) {
MapCoords coords = g_context->_location->_coords;
Dungeon *dungeon = dynamic_cast<Dungeon *>(g_context->_location->_map);
assert(dungeon);
/* can't cast in the Abyss */
if (g_context->_location->_map->_id == MAP_ABYSS)
return 0;
/* can't go lower than level 8 */
else if (coords.z >= 7)
return 0;
else {
for (int i = 0; i < 0x20; i++) {
coords = MapCoords(xu4_random(8), xu4_random(8), g_context->_location->_coords.z + 1);
if (dungeon->validTeleportLocation(coords)) {
g_context->_location->_coords = coords;
return 1;
}
}
}
/* didn't find a place to go, failed! */
return 0;
}
const Spell *Spells::getSpell(int i) const {
return &SPELL_LIST[i];
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,201 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_SPELL_H
#define ULTIMA4_GAME_SPELL_H
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/map.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/sound/sound.h"
namespace Ultima {
namespace Ultima4 {
enum SpellCastError {
CASTERR_NOERROR, /* success */
CASTERR_NOMIX, /* no mixture available */
CASTERR_MPTOOLOW, /* caster doesn't have enough mp */
CASTERR_FAILED, /* the spell failed */
CASTERR_WRONGCONTEXT, /* generic 'wrong-context' error (generrally finds the correct
context error message on its own) */
CASTERR_COMBATONLY, /* e.g. spell must be cast in combat */
CASTERR_DUNGEONONLY, /* e.g. spell must be cast in dungeons */
CASTERR_WORLDMAPONLY /* e.g. spell must be cast on the world map */
};
/**
* Field types for the Energy field spell
*/
enum EnergyFieldType {
ENERGYFIELD_NONE,
ENERGYFIELD_FIRE,
ENERGYFIELD_LIGHTNING,
ENERGYFIELD_POISON,
ENERGYFIELD_SLEEP
};
/**
* The ingredients for a spell mixture.
*/
class Ingredients {
public:
Ingredients();
bool addReagent(Reagent reagent);
bool removeReagent(Reagent reagent);
int getReagent(Reagent reagent) const;
void revert();
bool checkMultiple(int mixes) const;
void multiply(int mixes);
private:
unsigned short _reagents[REAG_MAX];
};
class Spells;
typedef int (Spells::*SpellProc)(int);
struct Spell {
enum Param {
PARAM_NONE, ///< None
PARAM_PLAYER, ///< number of a player required
PARAM_DIR, ///< direction required
PARAM_TYPEDIR, ///< type of field and direction required (energy field)
PARAM_PHASE, ///< phase required (gate)
PARAM_FROMDIR ///< direction from required (winds)
};
enum SpecialEffects {
SFX_NONE, ///< none
SFX_INVERT, ///< invert the screen (moongates, most normal spells)
SFX_TREMOR ///< tremor spell
};
const char *_name;
int _components;
LocationContext _context;
TransportContext _transportContext;
SpellProc _spellFunc;
Param _paramType;
int _mp;
};
typedef void (*SpellEffectCallback)(int spell, int player, Sound sound);
#define N_SPELLS 26
class Spells {
private:
static const Spell SPELL_LIST[N_SPELLS];
SpellEffectCallback spellEffectCallback;
private:
int spellAwaken(int player);
int spellBlink(int dir);
int spellCure(int player);
int spellDispel(int dir);
int spellEField(int param);
int spellFireball(int dir);
int spellGate(int phase);
int spellHeal(int player);
int spellIceball(int dir);
int spellJinx(int unused);
int spellKill(int dir);
int spellLight(int unused);
int spellMMissle(int dir);
int spellNegate(int unused);
int spellOpen(int unused);
int spellProtect(int unused);
int spellRez(int player);
int spellQuick(int unused);
int spellSleep(int unused);
int spellTremor(int unused);
int spellUndead(int unused);
int spellView(int unsued);
int spellWinds(int fromdir);
int spellXit(int unused);
int spellYup(int unused);
int spellZdown(int unused);
private:
CombatController *spellCombatController();
/**
* Makes a special magic ranged attack in the given direction
*/
void spellMagicAttack(const Common::String &tilename, Direction dir, int minDamage, int maxDamage);
bool spellMagicAttackAt(const Coords &coords, MapTile attackTile, int attackDamage);
LocationContext spellGetContext(uint spell) const;
TransportContext spellGetTransportContext(uint spell) const;
/**
* Returns true if the debugger is active
*/
bool isDebuggerActive() const;
public:
/**
* Constructor
*/
Spells();
/**
* Destructor
*/
~Spells();
void spellSetEffectCallback(SpellEffectCallback callback);
void spellEffect(int spell, int player, Sound sound) {
(spellEffectCallback)(spell, player, sound);
}
/**
* Mix reagents for a spell. Fails and returns false if the reagents
* selected were not correct.
*/
int spellMix(uint spell, const Ingredients *ingredients);
/**
* Casts spell. Fails and returns false if the spell cannot be cast.
* The error code is updated with the reason for failure.
*/
bool spellCast(uint spell, int character, int param, SpellCastError *error, bool spellEffect);
Common::String spellGetErrorMessage(uint spell, SpellCastError error);
const char *spellGetName(uint spell) const;
/**
* Checks some basic prerequistes for casting a spell. Returns an
* error if no mixture is available, the context is invalid, or the
* character doesn't have enough magic points.
*/
SpellCastError spellCheckPrerequisites(uint spell, int character);
Spell::Param spellGetParamType(uint spell) const;
int spellGetRequiredMP(uint spell) const;
const Spell *getSpell(int i) const;
};
extern Spells *g_spells;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif

View File

@@ -0,0 +1,164 @@
/* 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/game/weapon.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/game/names.h"
namespace Ultima {
namespace Ultima4 {
Weapons *g_weapons;
Weapons::Weapons() : _confLoaded(false) {
g_weapons = this;
}
Weapons::~Weapons() {
for (uint idx = 0; idx < size(); ++idx)
delete (*this)[idx];
g_weapons = nullptr;
}
const Weapon *Weapons::get(WeaponType w) {
// Load in XML if it hasn't been already
loadConf();
if (static_cast<unsigned>(w) >= size())
return nullptr;
return (*this)[w];
}
const Weapon *Weapons::get(const Common::String &name) {
// Load in XML if it hasn't been already
loadConf();
for (uint i = 0; i < size(); i++) {
if (scumm_stricmp(name.c_str(), (*this)[i]->_name.c_str()) == 0)
return (*this)[i];
}
return nullptr;
}
void Weapons::loadConf() {
if (_confLoaded)
return;
_confLoaded = true;
const Config *config = Config::getInstance();
Std::vector<ConfigElement> weaponConfs = config->getElement("weapons").getChildren();
for (const auto &i : weaponConfs) {
if (i.getName() != "weapon")
continue;
WeaponType weaponType = static_cast<WeaponType>(size());
push_back(new Weapon(weaponType, i));
}
}
/*-------------------------------------------------------------------*/
Weapon::Weapon(WeaponType weaponType, const ConfigElement &conf)
: _type(weaponType)
, _name(conf.getString("name"))
, _abbr(conf.getString("abbr"))
, _canUse(0xFF)
, _range(0)
, _damage(conf.getInt("damage"))
, _hitTile("hit_flash")
, _missTile("miss_flash")
, _leaveTile("")
, _flags(0) {
static const struct {
const char *name;
uint flag;
} booleanAttributes[] = {
{ "lose", WEAP_LOSE },
{ "losewhenranged", WEAP_LOSEWHENRANGED },
{ "choosedistance", WEAP_CHOOSEDISTANCE },
{ "alwayshits", WEAP_ALWAYSHITS },
{ "magic", WEAP_MAGIC },
{ "attackthroughobjects", WEAP_ATTACKTHROUGHOBJECTS },
{ "returns", WEAP_RETURNS },
{ "dontshowtravel", WEAP_DONTSHOWTRAVEL }
};
/* Get the range of the weapon, whether it is absolute or normal range */
Common::String range = conf.getString("range");
if (range.empty()) {
range = conf.getString("absolute_range");
if (!range.empty())
_flags |= WEAP_ABSOLUTERANGE;
}
if (range.empty())
error("malformed weapons.xml file: range or absolute_range not found for weapon %s", _name.c_str());
_range = atoi(range.c_str());
/* Load weapon attributes */
for (unsigned at = 0; at < sizeof(booleanAttributes) / sizeof(booleanAttributes[0]); at++) {
if (conf.getBool(booleanAttributes[at].name)) {
_flags |= booleanAttributes[at].flag;
}
}
/* Load hit tiles */
if (conf.exists("hittile"))
_hitTile = conf.getString("hittile");
/* Load miss tiles */
if (conf.exists("misstile"))
_missTile = conf.getString("misstile");
/* Load leave tiles */
if (conf.exists("leavetile")) {
_leaveTile = conf.getString("leavetile");
}
Std::vector<ConfigElement> contraintConfs = conf.getChildren();
for (const auto &i : contraintConfs) {
byte mask = 0;
if (i.getName() != "constraint")
continue;
for (int cl = 0; cl < 8; cl++) {
if (scumm_stricmp(i.getString("class").c_str(), getClassName(static_cast<ClassType>(cl))) == 0)
mask = (1 << cl);
}
if (mask == 0 && scumm_stricmp(i.getString("class").c_str(), "all") == 0)
mask = 0xFF;
if (mask == 0) {
error("malformed weapons.xml file: constraint has unknown class %s",
i.getString("class").c_str());
}
if (i.getBool("canuse"))
_canUse |= mask;
else
_canUse &= ~mask;
}
}
} // End of namespace Ultima4
} // End of namespace Ultima

View File

@@ -0,0 +1,157 @@
/* 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/>.
*
*/
#ifndef ULTIMA4_GAME_WEAPON_H
#define ULTIMA4_GAME_WEAPON_H
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/shared/std/containers.h"
namespace Ultima {
namespace Ultima4 {
class ConfigElement;
class Weapons;
class Weapon {
friend class Weapons;
public:
/**< Flags affecting weapon's behavior. @see Weapon::flags */
enum Flags {
WEAP_LOSE = 0x0001, /**< lost when used */
WEAP_LOSEWHENRANGED = 0x0002, /**< lost when used for ranged attack */
WEAP_CHOOSEDISTANCE = 0x0004, /**< allows player to choose attack distance */
WEAP_ALWAYSHITS = 0x0008, /**< always hits it's target */
WEAP_MAGIC = 0x0010, /**< is magical */
WEAP_ATTACKTHROUGHOBJECTS = 0x0040, /**< can attack through solid objects */
WEAP_ABSOLUTERANGE = 0x0080, /**< range is absolute (only works at specific distance) */
WEAP_RETURNS = 0x0100, /**< returns to user after used/thrown */
WEAP_DONTSHOWTRAVEL = 0x0200 /**< do not show animations when attacking */
};
public:
WeaponType getType() const {
return _type;
}
const Common::String &getName() const {
return _name;
}
const Common::String &getAbbrev() const {
return _abbr;
}
bool canReady(ClassType klass) const {
return (_canUse & (1 << klass)) != 0;
}
int getRange() const {
return _range;
}
int getDamage() const {
return _damage;
}
const Common::String &getHitTile() const {
return _hitTile;
}
const Common::String &getMissTile() const {
return _missTile;
}
const Common::String &leavesTile() const {
return _leaveTile;
}
unsigned short getFlags() const {
return _flags;
}
bool loseWhenUsed() const {
return _flags & WEAP_LOSE;
}
bool loseWhenRanged() const {
return _flags & WEAP_LOSEWHENRANGED;
}
bool canChooseDistance() const {
return _flags & WEAP_CHOOSEDISTANCE;
}
bool alwaysHits() const {
return _flags & WEAP_ALWAYSHITS;
}
bool isMagic() const {
return _flags & WEAP_MAGIC;
}
bool canAttackThroughObjects() const {
return _flags & WEAP_ATTACKTHROUGHOBJECTS;
}
bool rangeAbsolute() const {
return _flags & WEAP_ABSOLUTERANGE;
}
bool returns() const {
return _flags & WEAP_RETURNS;
}
bool showTravel() const {
return !(_flags & WEAP_DONTSHOWTRAVEL);
}
private:
Weapon(WeaponType weaponType, const ConfigElement &conf);
WeaponType _type;
Common::String _name;
Common::String _abbr; /**< abbreviation for the weapon */
byte _canUse; /**< bitmask of classes that can use weapon */
int _range; /**< range of weapon */
int _damage; /**< damage of weapon */
Common::String _hitTile; /**< tile to display a hit */
Common::String _missTile; /**< tile to display a miss */
Common::String _leaveTile; /**< if the weapon leaves a tile, the tile #, zero otherwise */
unsigned short _flags;
};
class Weapons : public Common::Array<Weapon *> {
private:
bool _confLoaded;
void loadConf();
public:
/**
* Constructor
*/
Weapons();
/**
* Destructor
*/
~Weapons();
/**
* Returns weapon by WeaponType.
*/
const Weapon *get(WeaponType w);
/**
* Returns weapon that has the given name
*/
const Weapon *get(const Common::String &name);
};
extern Weapons *g_weapons;
} // End of namespace Ultima4
} // End of namespace Ultima
#endif