799 lines
24 KiB
C++
799 lines
24 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "ultima/ultima4/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
|