/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "ultima/nuvie/core/nuvie_defs.h" #include "ultima/nuvie/core/game.h" #include "ultima/nuvie/actors/actor.h" #include "ultima/nuvie/core/map.h" #include "ultima/nuvie/core/party.h" #include "ultima/nuvie/script/script.h" #include "ultima/nuvie/core/anim_manager.h" #include "ultima/nuvie/gui/widgets/map_window.h" #include "ultima/nuvie/core/tile_manager.h" #include "ultima/nuvie/core/game_clock.h" #include "ultima/nuvie/core/effect_manager.h" #include "ultima/nuvie/usecode/usecode.h" #include "ultima/nuvie/gui/widgets/msg_scroll.h" #include "ultima/nuvie/actors/actor_manager.h" #include "ultima/nuvie/sound/sound_manager.h" #include "ultima/nuvie/core/u6_objects.h" #include "ultima/nuvie/core/effect.h" #include "ultima/nuvie/core/player.h" #include "backends/keymapper/keymapper.h" namespace Ultima { namespace Nuvie { #define MESG_ANIM_HIT_WORLD ANIM_CB_HIT_WORLD #define MESG_ANIM_HIT ANIM_CB_HIT #define MESG_ANIM_DONE ANIM_CB_DONE #define MESG_EFFECT_COMPLETE EFFECT_CB_COMPLETE //#define MESG_INPUT_READY EVENT_CB_INPUT_READY #define MESG_INPUT_READY MSGSCROLL_CB_TEXT_READY #define TRANSPARENT_COLOR 0xFF /* transparent pixel color */ static const int EXP_EFFECT_TILE_NUM = 382; QuakeEffect *QuakeEffect::current_quake = nullptr; FadeEffect *FadeEffect::current_fade = nullptr; /* Add self to effect list (for future deletion). */ Effect::Effect() : defunct(false), retain_count(0) { game = Game::get_game(); effect_manager = game->get_effect_manager(); effect_manager->add_effect(this); } Effect::~Effect() { // FIXME: should we remove self from Callbacks' default targets? } /* Start managing new animation. (AnimMgr will do that actually, but we point to * it and can stop it when necessary) */ void Effect::add_anim(NuvieAnim *anim) { anim->set_target(this); // add self as callback target for anim game->get_map_window()->get_anim_manager()->new_anim(anim); } static const int CANNON_SPEED = 320; /* Fire from a cannon in direction: 0=north, 1=east, 2=south, 3=west, * -1=use cannon frame */ CannonballEffect::CannonballEffect(Obj *src_obj, sint8 direction) : obj(src_obj) { usecode = game->get_usecode(); target_loc = MapCoord(obj->x, obj->y, obj->z); if (direction == -1) direction = obj->frame_n; uint8 target_dist = 5; // distance that cannonball will fly if (direction == NUVIE_DIR_N) target_loc.y -= target_dist; else if (direction == NUVIE_DIR_E) target_loc.x += target_dist; else if (direction == NUVIE_DIR_S) target_loc.y += target_dist; else if (direction == NUVIE_DIR_W) target_loc.x -= target_dist; start_anim(); } /* Pause world & start animation. */ void CannonballEffect::start_anim() { MapCoord obj_loc(obj->x, obj->y, obj->z); game->pause_world(); game->pause_anims(); game->pause_user(); anim = new TossAnim(game->get_tile_manager()->get_tile(399), obj_loc, target_loc, CANNON_SPEED, TOSS_TO_BLOCKING | TOSS_TO_ACTOR | TOSS_TO_OBJECT); add_anim(anim); } /* Handle messages from animation. Hit actors & walls. */ uint16 CannonballEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { bool stop_effect = false; Actor *hit_actor = nullptr; switch (msg) { case MESG_ANIM_HIT_WORLD: { const MapCoord *hit_loc = static_cast(msg_data); const Tile *obj_tile = game->get_obj_manager()->get_obj_tile(hit_loc->x, hit_loc->y, hit_loc->z); const Tile *tile = game->get_game_map()->get_tile(hit_loc->x, hit_loc->y, hit_loc->z); if ((tile->flags2 & TILEFLAG_MISSILE_BOUNDARY) || (obj_tile && (obj_tile->flags2 & TILEFLAG_MISSILE_BOUNDARY))) { //new ExplosiveEffect(hit_loc->x, hit_loc->y, 2); new ExpEffect(EXP_EFFECT_TILE_NUM, *hit_loc); stop_effect = true; } break; } case MESG_ANIM_HIT: { MapEntity *hit_ent = static_cast(msg_data); if (hit_ent->entity_type == ENT_ACTOR) { //hit_ent->actor->hit(32); hit_actor = hit_ent->actor; stop_effect = true; } if (hit_ent->entity_type == ENT_OBJ) { DEBUG(0, LEVEL_DEBUGGING, "hit object %d at %x,%x,%x\n", hit_ent->obj->obj_n, hit_ent->obj->x, hit_ent->obj->y, hit_ent->obj->z); // FIX: U6 specific // FIX: hit any part of ship, and reduce qty of center if (hit_ent->obj->obj_n == 412) { uint8 f = hit_ent->obj->frame_n; if (f == 9 || f == 15 || f == 11 || f == 13) { // directions if (hit_ent->obj->qty < 20) hit_ent->obj->qty = 0; else hit_ent->obj->qty -= 20; if (hit_ent->obj->qty == 0) game->get_scroll()->display_string("Ship broke!\n"); stop_effect = true; } } } break; } case MESG_ANIM_DONE: //new ExplosiveEffect(target_loc.x, target_loc.y, 3); new ExpEffect(EXP_EFFECT_TILE_NUM, MapCoord(target_loc.x, target_loc.y, target_loc.z)); stop_effect = true; break; } if (stop_effect) { if (hit_actor) { anim->pause(); //pause to avoid recursive problems when animations are called from actor_hit() lua script. Game::get_game()->get_script()->call_actor_hit(hit_actor, 32, true); } if (msg != MESG_ANIM_DONE) // this msg means anim stopped itself anim->stop(); game->unpause_all(); usecode->message_obj(obj, MESG_EFFECT_COMPLETE, this); delete_self(); } return 0; } #define EXP_EFFECT_SPEED 3 ExpEffect::ExpEffect(uint16 tileNum, const MapCoord &location) : ProjectileEffect(), exp_tile_num(tileNum) { start_loc = location; start_anim(); } /* Pause world & start animation. */ void ExpEffect::start_anim() { game->pause_world(); game->pause_anims(); game->pause_user(); targets.resize(16); targets[0] = MapCoord(start_loc.x + 2, start_loc.y - 1, start_loc.z); targets[1] = MapCoord(start_loc.x + 1, start_loc.y + 2, start_loc.z); targets[2] = MapCoord(start_loc.x, start_loc.y - 2, start_loc.z); targets[3] = MapCoord(start_loc.x + 1, start_loc.y - 1, start_loc.z); targets[4] = MapCoord(start_loc.x - 1, start_loc.y + 2, start_loc.z); targets[5] = MapCoord(start_loc.x - 1, start_loc.y - 1, start_loc.z); targets[6] = MapCoord(start_loc.x - 2, start_loc.y, start_loc.z); targets[7] = MapCoord(start_loc.x - 1, start_loc.y + 1, start_loc.z); targets[8] = MapCoord(start_loc.x, start_loc.y + 2, start_loc.z); targets[9] = MapCoord(start_loc.x - 1, start_loc.y - 2, start_loc.z); targets[10] = MapCoord(start_loc.x - 2, start_loc.y - 1, start_loc.z); targets[11] = MapCoord(start_loc.x - 2, start_loc.y + 1, start_loc.z); targets[12] = MapCoord(start_loc.x + 2, start_loc.y + 1, start_loc.z); targets[13] = MapCoord(start_loc.x + 2, start_loc.y, start_loc.z); targets[14] = MapCoord(start_loc.x + 1, start_loc.y + 1, start_loc.z); targets[15] = MapCoord(start_loc.x + 1, start_loc.y - 2, start_loc.z); anim = new ProjectileAnim(exp_tile_num, &start_loc, targets, EXP_EFFECT_SPEED, true); add_anim(anim); } ProjectileEffect::ProjectileEffect(uint16 tileNum, MapCoord start, MapCoord target, uint8 speed, bool trailFlag, uint16 initialTileRotation, uint16 rotationAmount, uint8 src_y_offset) { vector t; t.push_back(target); init(tileNum, start, t, speed, trailFlag, initialTileRotation, rotationAmount, src_y_offset); } ProjectileEffect::ProjectileEffect(uint16 tileNum, MapCoord start, const vector &t, uint8 speed, bool trailFlag, uint16 initialTileRotation) { init(tileNum, start, t, speed, trailFlag, initialTileRotation, 0, 0); } void ProjectileEffect::init(uint16 tileNum, MapCoord start, const vector &t, uint8 speed, bool trailFlag, uint16 initialTileRotation, uint16 rotationAmount, uint8 src_y_offset) { finished_tiles = 0; tile_num = tileNum; start_loc = start; anim_speed = speed; trail = trailFlag; initial_tile_rotation = initialTileRotation; rotation_amount = rotationAmount; src_tile_y_offset = src_y_offset; targets = t; start_anim(); } /* Pause world & start animation. */ void ProjectileEffect::start_anim() { game->pause_world(); //game->pause_anims(); game->pause_user(); add_anim(new ProjectileAnim(tile_num, &start_loc, targets, anim_speed, trail, initial_tile_rotation, rotation_amount, src_tile_y_offset)); } /* Handle messages from animation. Hit actors & walls. */ uint16 ProjectileEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { bool stop_effect = false; switch (msg) { case MESG_ANIM_HIT_WORLD: { MapCoord *hit_loc = static_cast(msg_data); const Tile *tile = game->get_game_map()->get_tile(hit_loc->x, hit_loc->y, hit_loc->z); if (tile->flags1 & TILEFLAG_WALL) { //new ExplosiveEffect(hit_loc->x, hit_loc->y, 2); stop_effect = true; } break; } case MESG_ANIM_HIT: { MapEntity *hit_ent = static_cast(msg_data); hit_entities.push_back(*hit_ent); break; } case MESG_ANIM_DONE: //new ExplosiveEffect(target_loc.x, target_loc.y, 3); stop_effect = true; break; } if (stop_effect) { //finished_tiles++; if (msg != MESG_ANIM_DONE) // this msg means anim stopped itself ((NuvieAnim *)caller)->stop(); //if(finished_tiles == 16) // { game->unpause_world(); game->unpause_user(); game->unpause_anims(); //usecode->message_obj(obj, MESG_EFFECT_COMPLETE, this); delete_self(); // } } return 0; } /*** TimedEffect ***/ void TimedEffect::start_timer(uint32 delay) { if (!timer) timer = new TimedCallback(this, nullptr, delay, true); } void TimedEffect::stop_timer() { if (timer) { timer->clear_target(); timer = nullptr; } } /*** QuakeEffect ***/ /* Shake the visible play area around for `duration' milliseconds. Magnitude * determines the speed of movement. An actor may be selected to keep the * MapWindow centered on after the Quake. */ QuakeEffect::QuakeEffect(uint8 magnitude, uint32 duration, Actor *keep_on) : strength(magnitude), orig_actor(keep_on), sx(0), sy(0), map_window(nullptr), stop_time(0) { // single use only, so MapWindow doesn't keep moving away from center // ...and do nothing if magnitude isn't usable if (current_quake || magnitude == 0) { delete_self(); return; } current_quake = this; // cleared in timer function map_window = game->get_map_window(); stop_time = game->get_clock()->get_ticks() + duration; // get random direction (always move left-right more than up-down) init_directions(); map_window->get_pos(&orig.x, &orig.y); map_window->get_level(&orig.z); map_window->set_freeze_blacking_location(true); start_timer(strength * 5); } QuakeEffect::~QuakeEffect() { } /* On TIMED: Move map. */ uint16 QuakeEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { // uint8 twice_strength = strength * 2; if (msg != MESG_TIMED) return 0; if (game->get_clock()->get_ticks() >= stop_time) { stop_quake(); return 0; } recenter_map(); map_window->shiftMapRelative(sx, sy); // move in opposite direction on next call if (sx == -(4 * strength) || sx == (4 * strength)) sx = (sx == -(4 * strength)) ? (2 * strength) : -(2 * strength); else if (sx == -(2 * strength) || sx == (2 * strength)) sx = 0; if (sy == -(2 * strength) || sy == (2 * strength)) sy = 0; if (sx == 0 && sy == 0) init_directions(); return 0; } /* Finish effect. Move map back to initial position. */ void QuakeEffect::stop_quake() { current_quake = nullptr; map_window->set_freeze_blacking_location(false); recenter_map(); delete_self(); } /* Set sx,sy to a random direction. (always move left-right more than up-down) */ void QuakeEffect::init_directions() { NuvieDir dir = static_cast(NUVIE_RAND() % 8); sx = 0; sy = 0; switch (dir) { default: // can't happen, but make the analyzer happy. case NUVIE_DIR_N : sy = -(strength * 2); break; case NUVIE_DIR_NE : sx = (strength * 4); sy = -(strength * 2); break; case NUVIE_DIR_E : sx = (strength * 4); break; case NUVIE_DIR_SE : sx = (strength * 4); sy = (strength * 2); break; case NUVIE_DIR_S : sy = (strength * 2); break; case NUVIE_DIR_SW : sx = -(strength * 4); sy = (strength * 2); break; case NUVIE_DIR_W : sx = -(strength * 4); break; case NUVIE_DIR_NW : sx = -(strength * 4); sy = -(strength * 2); break; } } /* Center map on original actor or move to original location. */ void QuakeEffect::recenter_map() { if (orig_actor) map_window->centerMapOnActor(orig_actor); else map_window->moveMap(orig.x, orig.y, orig.z); } /*** HitEffect ***/ /* Hit target actor. FIXME: implement duration and hitting a location */ HitEffect::HitEffect(Actor *target, uint32 duration) { game->pause_user(); add_anim(new HitAnim(target)); Game::get_game()->get_sound_manager()->playSfx(NUVIE_SFX_HIT); //FIXME use NUVIE_SFX_SAMPLE defines here. } HitEffect::HitEffect(const MapCoord &location) { game->pause_user(); add_anim(new HitAnim(location)); Game::get_game()->get_sound_manager()->playSfx(NUVIE_SFX_HIT); //FIXME use NUVIE_SFX_SAMPLE defines here. } /* On ANIM_DONE: end */ uint16 HitEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { if (msg == MESG_ANIM_DONE) { game->unpause_user(); delete_self(); } return 0; } TextEffect::TextEffect(Std::string text) { // default somewhat centered on player for cheat messages const MapWindow *map_window = game->get_map_window(); if (!map_window || map_window->Status() != WIDGET_VISIBLE) // scripted sequence like intro and intro menu return; MapCoord loc = game->get_player()->get_actor()->get_location(); loc.x = (loc.x - map_window->get_cur_x() - 2) * 16; loc.y = (loc.y - map_window->get_cur_y() - 1) * 16; add_anim(new TextAnim(text, loc, 1500)); } /*** TextEffect ***/ /* Print Text to MapWindow for duration */ TextEffect::TextEffect(Std::string text, const MapCoord &location) { add_anim(new TextAnim(text, location, 1500)); } /* On ANIM_DONE: end */ uint16 TextEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { if (msg == MESG_ANIM_DONE) { delete_self(); } return 0; } /*** ExplosiveEffect ***/ ExplosiveEffect::ExplosiveEffect(uint16 x, uint16 y, uint32 size, uint16 dmg) : start_at(), anim(nullptr) { start_at.x = x; start_at.y = y; radius = size; hit_damage = dmg; start_anim(); } /* Pause world & start animation. */ void ExplosiveEffect::start_anim() { game->pause_world(); game->pause_user(); add_anim(new ExplosiveAnim(start_at, radius)); } /* Handle messages from animation. Hit actors & objects. */ uint16 ExplosiveEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { bool stop_effect = false; switch (msg) { case MESG_ANIM_HIT: { MapEntity *hit_ent = static_cast(msg_data); if (hit_ent->entity_type == ENT_ACTOR) { if (hit_damage != 0) // hit actor if effect causes damage hit_ent->actor->hit(hit_damage); } else if (hit_ent->entity_type == ENT_OBJ) { DEBUG(0, LEVEL_DEBUGGING, "Explosion hit object %d (%x,%x)\n", hit_ent->obj->obj_n, hit_ent->obj->x, hit_ent->obj->y); stop_effect = hit_object(hit_ent->obj); } break; } case MESG_ANIM_DONE: stop_effect = true; break; } if (stop_effect) { if (msg != MESG_ANIM_DONE) anim->stop(); game->unpause_world(); game->unpause_user(); delete_self(); } return 0; } /* UseCodeExplosiveEffect: before deleting send message to source object */ void UseCodeExplosiveEffect::delete_self() { if (obj) game->get_usecode()->message_obj(obj, MESG_EFFECT_COMPLETE, this); Effect::delete_self(); } /* The explosion hit an object. * Returns true if the effect should end, false to continue. */ bool UseCodeExplosiveEffect::hit_object(Obj *hit_obj) { // ignite & destroy powder kegs (U6) if (hit_obj->obj_n == 223 && hit_obj != original_obj) { // FIXME: this doesn't belong here (U6/obj specific) uint16 x = hit_obj->x, y = hit_obj->y; game->get_obj_manager()->remove_obj_from_map(hit_obj); delete_obj(hit_obj); if (obj) // pass our source obj on to next effect as original_obj new UseCodeExplosiveEffect(nullptr, x, y, 2, hit_damage, obj); else // pass original_obj on to next effect new UseCodeExplosiveEffect(nullptr, x, y, 2, hit_damage, original_obj); } return false; } /*** ThrowObjectEffect ***/ ThrowObjectEffect::ThrowObjectEffect() { obj_manager = game->get_obj_manager(); anim = nullptr; throw_obj = nullptr; throw_tile = nullptr; throw_speed = 0; degrees = 0; stop_flags = 0; } void ThrowObjectEffect::start_anim() { game->pause_anims(); game->pause_world(); game->pause_user(); assert(throw_tile || throw_obj); // make sure it was properly initialized assert(throw_speed != 0); if (throw_obj) anim = new TossAnim(throw_obj, degrees, start_at, stop_at, throw_speed, stop_flags); else anim = new TossAnim(throw_tile, start_at, stop_at, throw_speed, stop_flags); add_anim(anim); } /* Object has stopped. */ void ThrowObjectEffect::hit_target() { if (anim) anim->stop(); game->unpause_all(); delete_self(); } /* The animation will travel from original object location to drop location if * nullptr actor is specified. */ DropEffect::DropEffect(Obj *obj, uint16 qty, Actor *actor, MapCoord *drop_loc) { drop_from_actor = actor; start_at = drop_from_actor ? drop_from_actor->get_location() : MapCoord(obj->x, obj->y, obj->z); stop_at = *drop_loc; degrees = 90; get_obj(obj, qty); // remove from actor, set throw_obj if (start_at != stop_at) { throw_speed = 192; // animation speed start_anim(); } else hit_target(); // done already? why bother calling DropEffect? :p } /* Take `qty' objects of a stack if necessary, and remove from the actor's * inventory. Set `throw_obj'. */ void DropEffect::get_obj(Obj *obj, uint16 qty) { throw_obj = obj_manager->get_obj_from_stack(obj, qty); if (drop_from_actor) drop_from_actor->inventory_remove_obj(throw_obj); } /* On ANIM_HIT_WORLD: end at hit location * On ANIM_DONE: end */ uint16 DropEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { // if throw_obj is nullptr, object already hit target if (!throw_obj || (msg != MESG_ANIM_DONE && msg != MESG_ANIM_HIT_WORLD)) return 0; if (msg == MESG_ANIM_HIT_WORLD && stop_at == *(MapCoord *)msg_data && anim) anim->stop(); hit_target(); return 0; } /* Add object to map. (call before completing effect) */ void DropEffect::hit_target() { throw_obj->x = stop_at.x; throw_obj->y = stop_at.y; throw_obj->z = stop_at.z; //FIXME drop logic should probably be in lua script. if (drop_from_actor && obj_manager->is_breakable(throw_obj) && start_at.distance(stop_at) > 1) { nuvie_game_t game_type = game->get_game_type(); if (game_type == NUVIE_GAME_U6 && throw_obj->obj_n == OBJ_U6_DRAGON_EGG) { throw_obj->frame_n = 1; //brake egg. obj_manager->add_obj(throw_obj, OBJ_ADD_TOP); } else if (game_type == NUVIE_GAME_U6 && throw_obj->obj_n == OBJ_U6_MIRROR) { throw_obj->frame_n = 2; //break mirror. obj_manager->add_obj(throw_obj, OBJ_ADD_TOP); } else { // remove items from container if there is one if (game->get_usecode()->is_container(throw_obj)) { U6Link *link = throw_obj->container->start(); for (; link != nullptr; link = throw_obj->container->start()) { Obj *obj = (Obj *)link->data; obj_manager->moveto_map(obj, stop_at); } } obj_manager->unlink_from_engine(throw_obj); delete_obj(throw_obj); } Game::get_game()->get_scroll()->display_string("\nIt broke!\n"); Game::get_game()->get_sound_manager()->playSfx(NUVIE_SFX_BROKEN_GLASS); } else { Obj *dest_obj = obj_manager->get_obj(stop_at.x, stop_at.y, stop_at.z); if (obj_manager->can_store_obj(dest_obj, throw_obj)) obj_manager->moveto_container(throw_obj, dest_obj); else obj_manager->add_obj(throw_obj, OBJ_ADD_TOP); } throw_obj = nullptr; // set as dropped // not appropriate to do "Events::endAction(true)" from here to display // prompt, as we MUST unpause_user() in ThrowObjectEffect::hit_target, and // that would be redundant and may not unpause everything if wait mode was // already cancelled... so just prompt game->get_scroll()->display_string("\n"); game->get_scroll()->display_prompt(); game->get_map_window()->updateBlacking(); ThrowObjectEffect::hit_target(); // calls delete_self() } /*** MissileEffect ***/ MissileEffect::MissileEffect(uint16 tile_num, uint16 obj_n, const MapCoord &source, const MapCoord &target, uint8 dmg, uint8 intercept, uint16 speed) { actor_manager = game->get_actor_manager(); hit_actor = 0; hit_obj = 0; init(tile_num, obj_n, source, target, dmg, intercept, speed); } /* Start effect. If target is unset then the actor is the target. */ void MissileEffect::init(uint16 tile_num, uint16 obj_n, const MapCoord &source, const MapCoord &target, uint32 dmg, uint8 intercept, uint32 speed) { assert(tile_num || obj_n); // at least obj_n must be set // (although it might work if throw_obj is already set) assert(speed != 0); assert(intercept != 0); // must hit target if (obj_n != 0) throw_obj = new_obj(obj_n, 0, 0, 0, 0); if (tile_num != 0) throw_tile = game->get_tile_manager()->get_tile(tile_num); else if (throw_obj != 0) throw_tile = obj_manager->get_obj_tile(throw_obj->obj_n, 0); throw_speed = speed; hit_damage = dmg; start_at = source; stop_at = target; stop_flags = intercept; assert(stop_at != start_at); // Hmm, can't attack self with boomerang then // if (stop_at != start_at) { // start_at.x=WRAPPED_COORD(start_at.x+1,start_at.z); // start_at.y=WRAPPED_COORD(start_at.y-1,start_at.z); // } // set tile rotation here based on obj_num if (throw_obj != 0) { if (throw_obj->obj_n == OBJ_U6_SPEAR) degrees = 315; if (throw_obj->obj_n == OBJ_U6_THROWING_AXE) degrees = 0; if (throw_obj->obj_n == OBJ_U6_DAGGER) degrees = 315; if (throw_obj->obj_n == OBJ_U6_ARROW) degrees = 270; if (throw_obj->obj_n == OBJ_U6_BOLT) degrees = 270; } start_anim(); } /* On HIT: hit Actor or Obj and end * On HIT_WORLD: end at hit location, hit Actor or Obj, else place obj * On DONE: end */ uint16 MissileEffect::callback(uint16 msg, CallBack *caller, void *msg_data) { if (msg != MESG_ANIM_DONE && msg != MESG_ANIM_HIT_WORLD && msg != MESG_ANIM_HIT) return 0; if (msg == MESG_ANIM_DONE) { // will always hit anything at the target // FIXME: only hit breakable objects like doors // hit_obj = obj_manager->get_obj(stop_at.x,stop_at.y,stop_at.z); hit_actor = actor_manager->get_actor(stop_at.x, stop_at.y, stop_at.z); hit_target(); } else if (msg == MESG_ANIM_HIT && ((MapEntity *)msg_data)->entity_type == ENT_ACTOR) { if (hit_damage != 0) hit_actor = ((MapEntity *)msg_data)->actor; hit_target(); } else if (msg == MESG_ANIM_HIT && ((MapEntity *)msg_data)->entity_type == ENT_OBJ) { // FIXME: only hit breakable objects like doors /* if(hit_damage != 0) hit_obj = ((MapEntity*)msg_data)->obj; hit_target();*/ } // MESG_ANIM_HIT_WORLD hit_blocking(); return 0; } /* Hit target or add object to map. (call before completing effect) */ void MissileEffect::hit_target() { if (hit_actor) { hit_actor->hit(hit_damage, ACTOR_FORCE_HIT); delete_obj(throw_obj); throw_obj = 0; // don't drop } else if (hit_obj) { if (hit_obj->qty < hit_damage) hit_obj->qty = 0; else hit_obj->qty -= hit_damage; delete_obj(throw_obj); throw_obj = 0; // don't drop } if (throw_obj != 0) { throw_obj->x = stop_at.x; throw_obj->y = stop_at.y; throw_obj->z = stop_at.z; throw_obj->status |= OBJ_STATUS_OK_TO_TAKE | OBJ_STATUS_TEMPORARY; if (obj_manager->is_stackable(throw_obj)) throw_obj->qty = 1; // stackable objects must have a quantity obj_manager->add_obj(throw_obj, OBJ_ADD_TOP); throw_obj = 0; } ThrowObjectEffect::hit_target(); // calls delete_self() } void MissileEffect::hit_blocking() { delete_obj(throw_obj); ThrowObjectEffect::hit_target(); } /*** SleepEffect ***/ /* The TimedAdvance is started after the fade-out completes. */ SleepEffect::SleepEffect(Std::string until) : timer(nullptr), stop_hour(0), stop_minute(0), stop_time("") { stop_time = until; game->pause_user(); effect_manager->watch_effect(this, new FadeEffect(FADE_PIXELATED, FADE_OUT)); } SleepEffect::SleepEffect(uint8 to_hour) : timer(nullptr), stop_hour(to_hour), stop_minute(0), stop_time("") { game->pause_user(); effect_manager->watch_effect(this, new FadeEffect(FADE_PIXELATED, FADE_OUT)); } SleepEffect::~SleepEffect() { //if(timer) // make sure it doesn't try to call us again // timer->clear_target(); } /* As with TimedEffect, make sure the timer doesn't try to use callback again. */ void SleepEffect::delete_self() { //timer->clear_target(); // this will also stop/delete the TimedAdvance //timer = nullptr; Effect::delete_self(); } /* Resume normal play when requested time has been reached. */ //FIXME: need to handle TimedAdvance() errors and fade-in uint16 SleepEffect::callback(uint16 msg, CallBack *caller, void *data) { uint8 hour = Game::get_game()->get_clock()->get_hour(); uint8 minute = Game::get_game()->get_clock()->get_minute(); // waited for FadeEffect if (msg == MESG_EFFECT_COMPLETE) { if (timer == nullptr) { // starting if (stop_time != "") { // advance to start time timer = new TimedAdvance(stop_time, 360); // 6 hours per second FIXME: it isn't going anywhere near that fast timer->set_target(this); timer->get_time_from_string(stop_hour, stop_minute, stop_time); // stop_hour & stop_minute are checked each hour } else { // advance a number of hours uint16 advance_h = (hour == stop_hour) ? 24 : (hour < stop_hour) ? (stop_hour - hour) : (24 - (hour - stop_hour)); timer = new TimedAdvance(advance_h, 360); timer->set_target(this); stop_minute = minute; } } else { // stopping Party *party = game->get_party(); for (int s = 0; s < party->get_party_size(); s++) { Actor *actor = party->get_actor(s); //heal actors. uint8 hp_diff = actor->get_maxhp() - actor->get_hp(); if (hp_diff > 0) { if (hp_diff == 1) hp_diff = 2; actor->set_hp(actor->get_hp() + NUVIE_RAND() % (hp_diff / 2) + hp_diff / 2); } } game->unpause_user(); delete_self(); } return 0; } // assume msg == MESG_TIMED; will stop after effect completes if (hour == stop_hour && minute >= stop_minute) effect_manager->watch_effect(this, new FadeEffect(FADE_PIXELATED, FADE_IN)); return 0; } /*** FadeEffect ***/ static const int FADE_EFFECT_MAX_ITERATIONS = 20; FadeEffect::FadeEffect(FadeType fade, FadeDirection dir, uint32 color, uint32 speed) { speed = speed ? speed : game->get_map_window()->get_win_area() * 2116; // was 256000 init(fade, dir, color, nullptr, 0, 0, speed); } /* Takes an image to fade from/to. */ FadeEffect::FadeEffect(FadeType fade, FadeDirection dir, Graphics::ManagedSurface *capture, uint32 speed) { speed = speed ? speed : game->get_map_window()->get_win_area() * 1620; // was 196000 init(fade, dir, 0, capture, 0, 0, speed); // color=black } /* Localizes effect to specific coordinates. The size of the effect is determined * by the size of the image. */ FadeEffect::FadeEffect(FadeType fade, FadeDirection dir, Graphics::ManagedSurface *capture, uint16 x, uint16 y, uint32 speed) { speed = speed ? speed : 1024; init(fade, dir, 0, capture, x, y, speed); // color=black } void FadeEffect::init(FadeType fade, FadeDirection dir, uint32 color, Graphics::ManagedSurface *capture, uint16 x, uint16 y, uint32 speed) { if (current_fade) { delete_self(); return; } current_fade = this; // cleared in dtor screen = game->get_screen(); map_window = game->get_map_window(); viewport = new Common::Rect(map_window->GetRect()); fade_type = fade; fade_dir = dir; fade_speed = speed; // pixels-per-second (to check, not draw) evtime = prev_evtime = 0; fade_x = x; fade_y = y; fade_from = nullptr; fade_iterations = 0; if (capture) { fade_from = new Graphics::ManagedSurface(capture->w, capture->h, capture->format); fade_from->blitFrom(*capture); } if (fade_type == FADE_PIXELATED || fade_type == FADE_PIXELATED_ONTOP) { pixelated_color = color; init_pixelated_fade(); } else init_circle_fade(); } FadeEffect::~FadeEffect() { //moved to delete_self } void FadeEffect::delete_self() { if (current_fade == this) { // these weren't init. if FadeEffect didn't start delete viewport; if (fade_dir == FADE_IN) // overlay should be empty now, so just delete it map_window->set_overlay(nullptr); if (fade_from) { delete fade_from; fade_from = nullptr; } current_fade = nullptr; } TimedEffect::delete_self(); } /* Start effect. */ void FadeEffect::init_pixelated_fade() { int fillret = -1; // check error overlay = map_window->get_overlay(); if (overlay != nullptr) { pixel_count = fade_from ? (fade_from->w) * (fade_from->h) : (overlay->w - fade_x) * (overlay->h - fade_y); // clear overlay to fill color or transparent if (fade_dir == FADE_OUT) { if (fade_from) { // fade from captured surface to transparent // put surface on transparent background (not checked) fillret = SDL_FillRect(overlay, nullptr, uint32(TRANSPARENT_COLOR)); Common::Rect overlay_rect(fade_x, fade_y, fade_x, fade_y); fillret = SDL_BlitSurface(fade_from, nullptr, overlay, &overlay_rect); } else // fade from transparent to color fillret = SDL_FillRect(overlay, nullptr, uint32(TRANSPARENT_COLOR)); } else { if (fade_from) // fade from transparent to captured surface fillret = SDL_FillRect(overlay, nullptr, uint32(TRANSPARENT_COLOR)); else // fade from color to transparent fillret = SDL_FillRect(overlay, nullptr, uint32(pixelated_color)); } } if (fillret == -1) { DEBUG(0, LEVEL_DEBUGGING, "FadeEffect: error creating overlay surface\n"); delete_self(); return; } // if FADE_PIXELATED_ONTOP is set, place the effect layer above the map border map_window->set_overlay_level((fade_type == FADE_PIXELATED) ? MAP_OVERLAY_DEFAULT : MAP_OVERLAY_ONTOP); colored_total = 0; start_timer(1); // fire timer continuously } /* Start effect. */ void FadeEffect::init_circle_fade() { delete_self(); // FIXME } /* Called by the timer as frequently as possible. Do the appropriate * fade method and stop when the effect is complete. */ uint16 FadeEffect::callback(uint16 msg, CallBack *caller, void *data) { bool fade_complete = false; // warning: msg is assumed to be CB_TIMED and data is set evtime = *(uint32 *)(data); // do effect if (fade_type == FADE_PIXELATED || fade_type == FADE_PIXELATED_ONTOP) fade_complete = (fade_dir == FADE_OUT) ? pixelated_fade_out() : pixelated_fade_in(); else /* CIRCLE */ fade_complete = (fade_dir == FADE_OUT) ? circle_fade_out() : circle_fade_in(); // done if (fade_complete == true) { delete_self(); return 1; } return 0; } /* Scan the overlay, starting at pixel rnum, for a transparent pixel if fading * out, and a colored pixel if fading in. * Returns true if a free pixel was found and set as rnum. */ // FIXME: this probably doesn't work because it only handles 8bpp inline bool FadeEffect::find_free_pixel(uint32 &rnum, uint32 pixelCount) { uint8 scan_color = (fade_dir == FADE_OUT) ? TRANSPARENT_COLOR : pixelated_color; const uint8 *pixels = (const uint8 *)(overlay->getPixels()); for (uint32 p = rnum; p < pixelCount; p++) // check all pixels after rnum if (pixels[p] == scan_color) { rnum = p; return true; } for (uint32 q = 0; q < rnum; q++) // check all pixels before rnum if (pixels[q] == scan_color) { rnum = q; return true; } return false; } /* Returns the next pixel to check/colorize. */ #if 0 #warning this crashes if x,y is near boundary #warning make sure center_thresh does not go over boundary inline uint32 FadeEffect::get_random_pixel(uint16 center_thresh) { if (center_x == -1 || center_y == -1) return (NUVIE_RAND() % pixel_count); uint16 x = center_x, y = center_y; if (center_thresh == 0) center_thresh = overlay->w / 2; x += (NUVIE_RAND() % (center_thresh * 2)) - center_thresh, y += (NUVIE_RAND() % (center_thresh * 2)) - center_thresh; return ((y * overlay->w) + x); } #endif /* Randomly add pixels of the appropriate color to the overlay. If the color * is -1, it will be taken from the "fade_from" surface. * Returns true when the overlay is completely colored. */ bool FadeEffect::pixelated_fade_core(uint32 pixels_to_check, sint16 fade_to) { Graphics::Surface s = overlay->getSubArea(Common::Rect(0, 0, overlay->w, overlay->h)); uint8 *pixels = (uint8 *)s.getPixels(); const uint8 *from_pixels = fade_from ? (const uint8 *)(fade_from->getPixels()) : nullptr; uint32 p = 0; // scan counter uint32 rnum = 0; // pixel index uint32 colored = 0; // number of pixels that get colored uint16 fade_width = fade_from ? fade_from->w : overlay->w - fade_x; uint16 fade_height = fade_from ? fade_from->h : overlay->h - fade_y; uint8 color = fade_to; if (fade_to == -1 && fade_from == nullptr) { return false; } while (p < pixels_to_check) { uint16 x = uint16(float(NUVIE_RAND()) * (fade_width - 1) / NUVIE_RAND_MAX) + fade_x, y = uint16(float(NUVIE_RAND()) * (fade_height - 1) / NUVIE_RAND_MAX) + fade_y; if (x >= overlay->w) x = overlay->w - 1; // prevent overflow if fade_from is too big if (y >= overlay->h) y = overlay->h - 1; rnum = y * overlay->w + x; //ERIC rnum = y*overlay->pitch + x; if (fade_to == -1) { // get color from "fade_from" x -= fade_x; y -= fade_y; color = from_pixels[y * fade_from->w + x]; } if (pixels[rnum] != color) { pixels[rnum] = color; ++colored; ++colored_total; // another pixel was set } ++p; } (void)colored; // Fix warning about unused variable // all but two lines colored if (colored_total >= (pixel_count - fade_width * 2) || fade_iterations > FADE_EFFECT_MAX_ITERATIONS) { // fill the rest if (fade_to >= 0) SDL_FillRect(overlay, nullptr, (uint32)fade_to); else { // Note: assert(fade_from) if(fade_to < 0) Common::Rect fade_from_rect(fade_from->w, (int16)fade_from->h); Common::Rect overlay_rect(fade_x, fade_y, fade_x + fade_from->w, fade_y + fade_from->h); SDL_BlitSurface(fade_from, &fade_from_rect, overlay, &overlay_rect); } return true; } else return false; } /* Color some of the mapwindow. * Returns true when all pixels have been filled, and nothing is visible. */ bool FadeEffect::pixelated_fade_out() { if (fade_from) return (pixelated_fade_core(pixels_to_check(), TRANSPARENT_COLOR)); return (pixelated_fade_core(pixels_to_check(), pixelated_color)); } /* Clear some of the mapwindow. * Returns true when all colored pixels have been removed, and the MapWindow * is visible. */ bool FadeEffect::pixelated_fade_in() { if (fade_from) return (pixelated_fade_core(pixels_to_check(), -1)); return (pixelated_fade_core(pixels_to_check(), TRANSPARENT_COLOR)); } /* Returns the number of pixels that should be checked/colored (based on speed) * since the previous call. */ uint32 FadeEffect::pixels_to_check() { uint32 time_passed = (prev_evtime == 0) ? 0 : evtime - prev_evtime; uint32 fraction = 1000 / (time_passed > 0 ? time_passed : 1); // % of second passed, in milliseconds uint32 pixels_per_fraction = fade_speed / (fraction > 0 ? fraction : 1); prev_evtime = evtime; fade_iterations++; return pixels_per_fraction; } /* Reduce the MapWindow's ambient light level, according to the set speed. * Returns true when nothing is visible. */ bool FadeEffect::circle_fade_out() { // FIXME return false; } /* Add to the MapWindow's ambient light level, according to the set speed. * Returns true when the light level has returned to normal. */ bool FadeEffect::circle_fade_in() { // FIXME return false; } /* Pause game and do FadeEffect. */ GameFadeInEffect::GameFadeInEffect(uint32 color) : FadeEffect(FADE_PIXELATED_ONTOP, FADE_IN, color) { game->pause_user(); } GameFadeInEffect::~GameFadeInEffect() { } /* Identical to FadeEffect, but unpause game when finished. */ uint16 GameFadeInEffect::callback(uint16 msg, CallBack *caller, void *data) { // done if (FadeEffect::callback(msg, caller, data) != 0) game->unpause_user(); return 0; } FadeObjectEffect::FadeObjectEffect(Obj *obj, FadeDirection dir) { obj_manager = game->get_obj_manager(); fade_obj = obj; fade_dir = dir; Graphics::ManagedSurface *capture = game->get_map_window()->get_sdl_surface(); if (fade_dir == FADE_IN) { // fading IN to object, so fade OUT from capture effect_manager->watch_effect(this, /* call me */ new FadeEffect(FADE_PIXELATED, FADE_OUT, capture)); obj_manager->add_obj(fade_obj, OBJ_ADD_TOP); game->get_map_window()->updateBlacking(); // object is likely a moongate } else if (fade_dir == FADE_OUT) { effect_manager->watch_effect(this, /* call me */ new FadeEffect(FADE_PIXELATED, FADE_OUT, capture, 0, 0, game->get_map_window()->get_win_area() * 1058)); //was 128000 // obj_manager->remove_obj(fade_obj); game->get_map_window()->updateBlacking(); } delete capture; game->pause_user(); } FadeObjectEffect::~FadeObjectEffect() { game->unpause_user(); } /* Assume FadeEffect is complete. */ uint16 FadeObjectEffect::callback(uint16 msg, CallBack *caller, void *data) { delete_self(); return 0; } /* These types of local/vanish effects are slightly longer than a normal Fade. * FIXME: FadeEffect should take local effect area, or change speed to time. */ VanishEffect::VanishEffect(bool pause_user) : input_blocked(pause_user) { Graphics::ManagedSurface *capture = game->get_map_window()->get_sdl_surface(); // effect_manager->watch_effect(this, /* call me */ // new FadeEffect(FADE_PIXELATED, FADE_OUT, capture, 0, 0, 128000)); effect_manager->watch_effect(this, /* call me */ new FadeEffect(FADE_PIXELATED, FADE_OUT, capture)); delete capture; if (input_blocked == VANISH_WAIT) game->pause_user(); game->pause_anims(); } VanishEffect::~VanishEffect() { game->unpause_anims(); if (input_blocked == VANISH_WAIT) game->unpause_user(); } /* Assume FadeEffect is complete. */ uint16 VanishEffect::callback(uint16 msg, CallBack *caller, void *data) { delete_self(); return 0; } /* TileFadeEffect */ TileFadeEffect::TileFadeEffect(const MapCoord &loc, Tile *from, Tile *to, FadeType type, uint16 speed) : actor(nullptr), inc_reverse(false), spd(0) { add_anim(new TileFadeAnim(loc, from, to, speed)); num_anim_running = 1; } //Fade out actor. TileFadeEffect::TileFadeEffect(Actor *a, uint16 speed) : actor(a), inc_reverse(false), spd(speed), num_anim_running(0) { add_actor_anim(); actor->hide(); } TileFadeEffect::~TileFadeEffect() { } void TileFadeEffect::add_actor_anim() { MapCoord loc = actor->get_location(); Tile *from = actor->get_tile(); add_tile_anim(loc, from); const Std::list &surrounding_objs = actor->get_surrounding_obj_list(); for (Obj *obj : surrounding_objs) add_obj_anim(obj); } void TileFadeEffect::add_obj_anim(Obj *obj) { ObjManager *obj_manager = Game::get_game()->get_obj_manager(); MapCoord loc(obj->x, obj->y, obj->z); add_tile_anim(loc, obj_manager->get_obj_tile(obj->obj_n, obj->frame_n)); } void TileFadeEffect::add_fade_anim(const MapCoord &loc, Tile *tile) { add_anim(new TileFadeAnim(loc, tile, nullptr, spd)); num_anim_running++; } void TileFadeEffect::add_tile_anim(const MapCoord &loc_, Tile *tile) { MapCoord loc = loc_; TileManager *tile_manager = Game::get_game()->get_tile_manager(); uint16 tile_num = tile->tile_num; add_fade_anim(loc, tile); if (tile->dbl_width) { tile_num--; loc.x -= 1; add_fade_anim(loc, tile_manager->get_tile(tile_num)); loc.x += 1; } if (tile->dbl_height) { tile_num--; loc.y -= 1; add_fade_anim(loc, tile_manager->get_tile(tile_num)); loc.y += 1; } if (tile->dbl_width && tile->dbl_height) { tile_num--; loc.x -= 1; loc.y -= 1; add_fade_anim(loc, tile_manager->get_tile(tile_num)); loc.x += 1; loc.y += 1; } } uint16 TileFadeEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_ANIM_DONE) { num_anim_running--; } if (num_anim_running == 0) { if (inc_reverse) { inc_reverse = false; add_actor_anim(); return 0; } if (actor) actor->show(); delete_self(); } return 0; } TileBlackFadeEffect::TileBlackFadeEffect(Actor *a, uint8 fade_color, uint16 speed) { init(fade_color, speed); actor = a; actor->hide(); add_actor_anim(); } TileBlackFadeEffect::TileBlackFadeEffect(Obj *o, uint8 fade_color, uint16 speed) { init(fade_color, speed); obj = o; obj->set_invisible(true); add_obj_anim(obj); } void TileBlackFadeEffect::init(uint8 fade_color, uint16 speed) { fade_speed = speed; color = fade_color; actor = nullptr; obj = nullptr; reverse = false; num_anim_running = 0; } TileBlackFadeEffect::~TileBlackFadeEffect() { } void TileBlackFadeEffect::add_actor_anim() { MapCoord loc = actor->get_location(); Tile *from = actor->get_tile(); add_tile_anim(loc, from); const Std::list &surrounding_objs = actor->get_surrounding_obj_list(); for (Obj *o : surrounding_objs) add_obj_anim(o); } void TileBlackFadeEffect::add_obj_anim(Obj *o) { MapCoord loc(o); Tile *from = Game::get_game()->get_obj_manager()->get_obj_tile(o->obj_n, o->frame_n); add_tile_anim(loc, from); } void TileBlackFadeEffect::add_tile_anim(const MapCoord &loc_, Tile *tile) { TileManager *tile_manager = Game::get_game()->get_tile_manager(); uint16 tile_num = tile->tile_num; add_anim(new TileFadeAnim(loc_, tile, 0, color, reverse, fade_speed)); num_anim_running++; MapCoord loc = loc_; if (tile->dbl_width) { tile_num--; loc.x -= 1; add_anim(new TileFadeAnim(loc, tile_manager->get_tile(tile_num), 0, color, reverse, fade_speed)); num_anim_running++; loc.x += 1; } if (tile->dbl_height) { tile_num--; loc.y -= 1; add_anim(new TileFadeAnim(loc, tile_manager->get_tile(tile_num), 0, color, reverse, fade_speed)); num_anim_running++; loc.y += 1; } if (tile->dbl_width && tile->dbl_height) { tile_num--; loc.x -= 1; loc.y -= 1; add_anim(new TileFadeAnim(loc, tile_manager->get_tile(tile_num), 0, color, reverse, fade_speed)); num_anim_running++; loc.x += 1; loc.y += 1; } } uint16 TileBlackFadeEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_ANIM_DONE) { num_anim_running--; } if (num_anim_running == 0) { if (reverse == false) { reverse = true; if (actor) add_actor_anim(); else add_obj_anim(obj); return 0; } if (actor) actor->show(); else obj->set_invisible(false); delete_self(); } return 0; } XorEffect::XorEffect(uint32 eff_ms) : map_window(game->get_map_window()), length(eff_ms) { game->pause_user(); game->pause_anims(); init_effect(); } void XorEffect::init_effect() { capture = map_window->get_sdl_surface(); map_window->set_overlay_level(MAP_OVERLAY_DEFAULT); map_window->set_overlay(capture); xor_capture(0xd); // changes black to pink start_timer(length); } /* Timer finished. Cleanup. */ uint16 XorEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_TIMED) { stop_timer(); game->unpause_anims(); game->unpause_user(); map_window->set_overlay(nullptr); delete_self(); } return 0; } /* Do binary-xor on each pixel of the mapwindow image.*/ void XorEffect::xor_capture(uint8 mod) { Graphics::Surface s = capture->getSubArea(Common::Rect(0, 0, capture->w, capture->h)); uint8 *pixels = (uint8 *)s.getPixels(); for (int p = 0; p < (capture->w * capture->h); p++) pixels[p] ^= mod; } U6WhitePotionEffect::U6WhitePotionEffect(uint32 eff_ms, uint32 delay_ms, Obj *callback_obj) : map_window(game->get_map_window()), state(0), start_length(1000), eff1_length(eff_ms), eff2_length(800), xray_length(delay_ms), capture(0), potion(callback_obj) { game->pause_user(); game->pause_anims(); init_effect(); } void U6WhitePotionEffect::init_effect() { // FIXME: play sound, and change state to 1 when sound is complete capture = map_window->get_sdl_surface(); map_window->set_overlay_level(MAP_OVERLAY_DEFAULT); map_window->set_overlay(capture); start_timer(start_length); } /* The state is changed from current to next when this is called. */ uint16 U6WhitePotionEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_TIMED) { stop_timer(); if (state == 0) { // start/sound effect // FIXME: make start_length a timeout, force sound to stop xor_capture(0xd); // changes black to pink start_timer(eff1_length); state = 1; } else if (state == 1) { // xor-effect map_window->set_overlay(nullptr); start_timer(eff2_length); state = 2; } else if (state == 2) { // character outline game->unpause_anims(); map_window->set_x_ray_view(X_RAY_ON); map_window->updateBlacking(); start_timer(xray_length); state = 3; } else if (state == 3) { // x-ray map_window->set_x_ray_view(X_RAY_OFF); map_window->updateBlacking(); game->unpause_user(); if (potion) game->get_usecode()->message_obj(potion, MESG_EFFECT_COMPLETE, this); state = 4; // finished delete_self(); } } return 0; } /* Do binary-xor on each pixel of the mapwindow image.*/ void U6WhitePotionEffect::xor_capture(uint8 mod) { Graphics::Surface s = capture->getSubArea(Common::Rect(0, 0, capture->w, capture->h)); uint8 *pixels = (uint8 *)s.getPixels(); for (int p = 0; p < (capture->w * capture->h); p++) pixels[p] ^= mod; } XRayEffect::XRayEffect(uint32 eff_ms) { xray_length = eff_ms; init_effect(); } void XRayEffect::init_effect() { Game::get_game()->get_map_window()->set_x_ray_view(X_RAY_ON); start_timer(xray_length); } uint16 XRayEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_TIMED) { stop_timer(); Game::get_game()->get_map_window()->set_x_ray_view(X_RAY_OFF); delete_self(); } return 0; } PauseEffect::PauseEffect() { game->pause_world(); // FIXME: need a way to detect any keyboard/mouse input game->get_scroll()->set_input_mode(true, "\n", true); game->get_scroll()->request_input(this, 0); } /* The effect ends when this is called. (if input is correct) */ uint16 PauseEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_INPUT_READY) { game->unpause_world(); delete_self(); } return 0; } WizardEyeEffect::WizardEyeEffect(const MapCoord &location, uint16 duration) { // Disable keymapper so Wizard Eye can receive keyboard input. // FIXME: Remove this once the effect can use keymapper-bound keys. g_system->getEventManager()->getKeymapper()->setEnabled(false); game->get_map_window()->wizard_eye_start(location, duration, this); } uint16 WizardEyeEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_EFFECT_COMPLETE) { delete_self(); // FIXME: Remove this once the effect can use keymapper-bound keys. g_system->getEventManager()->getKeymapper()->setEnabled(true); } return 0; } TextInputEffect::TextInputEffect(const char *allowed_chars, bool can_escape) { game->pause_world(); // FIXME: need a way to detect any keyboard/mouse input game->get_gui()->unblock(); game->get_scroll()->set_input_mode(true, allowed_chars, can_escape); game->get_scroll()->request_input(this, 0); } /* The effect ends when this is called. (if input is correct) */ uint16 TextInputEffect::callback(uint16 msg, CallBack *caller, void *data) { if (msg == MESG_INPUT_READY) { input = *(Std::string *)data; game->unpause_world(); delete_self(); } return 0; } PeerEffect::PeerEffect(uint16 x, uint16 y, uint8 z, Obj *callback_obj) : map_window(game->get_map_window()), overlay(0), gem(callback_obj), area(x, y, z), tile_trans(0), map_pitch(0) { uint8 lvl = 0; map_window->get_level(&lvl); map_pitch = (lvl == 0) ? 1024 : 256; init_effect(); } void PeerEffect::init_effect() { overlay = map_window->get_sdl_surface(); map_window->set_overlay_level(MAP_OVERLAY_DEFAULT); map_window->set_overlay(overlay); assert(overlay->w % PEER_TILEW == 0); // overlay must be a multiple of tile size SDL_FillRect(overlay, nullptr, 0); peer(); } void PeerEffect::delete_self() { map_window->set_overlay(nullptr); if (gem) game->get_usecode()->message_obj(gem, MESG_EFFECT_COMPLETE, this); else // FIXME: I don't want prompt display here, so it's also in UseCode, // but it has to be here if no object was set. (until we have another // way to tell caller effect is complete, and return to player) { game->get_scroll()->display_string("\n"); game->get_scroll()->display_prompt(); } Effect::delete_self(); } void PeerEffect::peer() { uint16 w = overlay->w, h = overlay->h; // effect is limited to 48x48 area if (overlay->w > 48 * PEER_TILEW) w = 48 * PEER_TILEW; if (overlay->h > 48 * PEER_TILEW) h = 48 * PEER_TILEW; MapCoord player_loc = game->get_player()->get_actor()->get_location(); uint16 cx = player_loc.x - area.x; // rough center of area uint16 cy = player_loc.y - area.y; area.x %= map_pitch; // we have to wrap here because we use a map buffer area.y %= map_pitch; uint8 *mapbuffer = new uint8[48 * 48]; // array of tile types/colors memset(mapbuffer, 0x00, sizeof(uint8) * 48 * 48); // fill with black fill_buffer(mapbuffer, cx, cy); for (int x = 0; x < w; x += PEER_TILEW) for (int y = 0; y < h; y += PEER_TILEW) { uint16 wx = area.x + x / PEER_TILEW, wy = area.y + y / PEER_TILEW; uint8 tile_type = mapbuffer[(wy - area.y) * 48 + (wx - area.x)]; blit_tile(x, y, tile_type); if (tile_type != 0x00) { Actor *actor = game->get_actor_manager()->get_actor(wx, wy, area.z); if (actor) blit_actor(actor); } } delete [] mapbuffer; } void PeerEffect::fill_buffer(uint8 *mapbuffer, uint16 x, uint16 y) { uint16 wx = area.x + x, wy = area.y + y; uint8 *tile = &mapbuffer[y * 48 + x]; if (*tile != 0x00) return; // already filled wx %= map_pitch; // we have to wrap here because we use a map buffer wy %= map_pitch; *tile = get_tilemap_type(wx, wy, area.z); // stop at unpassable tiles // FIXME: stop at Nothing/black tiles if (*tile != peer_tilemap[2] || game->get_game_map()->get_tile(wx, wy, area.z, true)->passable) { if (y > 0) { if (x > 0) fill_buffer(mapbuffer, x - 1, y - 1); // +-+-+ if (y > 0) fill_buffer(mapbuffer, x, y - 1); // |\|/| if (x + 1 < 48) fill_buffer(mapbuffer, x + 1, y - 1); } if (x > 0) fill_buffer(mapbuffer, x - 1, y); // +-+-+ if (x + 1 < 48) fill_buffer(mapbuffer, x + 1, y); if (y + 1 < 48) { if (x > 0) fill_buffer(mapbuffer, x - 1, y + 1); // |/|\| fill_buffer(mapbuffer, x, y + 1); // +-+-+ if (x + 1 < 48) fill_buffer(mapbuffer, x + 1, y + 1); } } } inline void PeerEffect::blit_tile(uint16 x, uint16 y, uint8 c) { Graphics::Surface s = overlay->getSubArea(Common::Rect(0, 0, overlay->w, overlay->h)); uint8 *pixels = (uint8 *)s.getPixels(); for (int j = 0; j < PEER_TILEW && j < overlay->h; j++) for (int i = 0; i < PEER_TILEW && i < overlay->w; i++) { if (peer_tile[i * PEER_TILEW + j] != tile_trans) pixels[overlay->w * (y + j) + (x + i)] = c; } } inline void PeerEffect::blit_actor(Actor *actor) { tile_trans = 1; blit_tile((actor->get_location().x - area.x)*PEER_TILEW, (actor->get_location().y - area.y)*PEER_TILEW, 0x0F); tile_trans = 0; if (game->get_player()->get_actor() == actor) blit_tile((actor->get_location().x - area.x)*PEER_TILEW, (actor->get_location().y - area.y)*PEER_TILEW, 0x0F); } inline uint8 PeerEffect::get_tilemap_type(uint16 wx, uint16 wy, uint8 wz) { Map *map = game->get_game_map(); // ignore objects (bridges and docks), and show coasts as land if (map->is_water(wx, wy, wz, true) && !map->get_tile(wx, wy, wz, true)->passable) return peer_tilemap[1]; if (!map->is_passable(wx, wy, wz)) return peer_tilemap[2]; if (map->is_damaging(wx, wy, wz)) return peer_tilemap[3]; return peer_tilemap[0]; // ground/passable } WingStrikeEffect::WingStrikeEffect(Actor *target_actor) { actor = target_actor; add_anim(new WingAnim(actor->get_location())); } uint16 WingStrikeEffect::callback(uint16 msg, CallBack *caller, void *data) { switch (msg) { case MESG_ANIM_HIT : DEBUG(0, LEVEL_DEBUGGING, "hit target!\n"); Game::get_game()->get_script()->call_actor_hit(actor, (NUVIE_RAND() % 20) + 1); break; case MESG_ANIM_DONE : delete_self(); break; } return 0; } HailStormEffect::HailStormEffect(const MapCoord &target) { add_anim(new HailstormAnim(target)); } uint16 HailStormEffect::callback(uint16 msg, CallBack *caller, void *data) { switch (msg) { case MESG_ANIM_HIT : DEBUG(0, LEVEL_DEBUGGING, "hit target!\n"); Game::get_game()->get_script()->call_actor_hit((Actor *)data, 1); break; case MESG_ANIM_DONE : delete_self(); break; } return 0; } /*** AsyncEffect ***/ AsyncEffect::AsyncEffect(Effect *e) { effect_complete = false; effect = e; effect->retain(); effect_manager->watch_effect(this, effect); } AsyncEffect::~AsyncEffect() { effect->release(); } /* The effect is marked as defunct after run finishes and will be removed from the system.*/ void AsyncEffect::run(bool process_gui_input) { if (!process_gui_input) Game::get_game()->pause_user(); for (; effect_complete == false;) { //spin world Game::get_game()->update_once(process_gui_input); if (!effect_complete) Game::get_game()->update_once_display(); } if (!process_gui_input) Game::get_game()->unpause_user(); delete_self(); } uint16 AsyncEffect::callback(uint16 msg, CallBack *caller, void *data) { // effect complete if (msg == MESG_EFFECT_COMPLETE) { effect_complete = true; } return 0; } } // End of namespace Nuvie } // End of namespace Ultima