/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "ultima/ultima4/gfx/screen.h"
#include "common/system.h"
#include "engines/util.h"
#include "graphics/cursorman.h"
#include "ultima/ultima4/controllers/intro_controller.h"
#include "ultima/ultima4/core/config.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/context.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/object.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/gfx/imagemgr.h"
#include "ultima/ultima4/gfx/textcolor.h"
#include "ultima/ultima4/map/annotation.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/map/tileanim.h"
#include "ultima/ultima4/map/tileset.h"
#include "ultima/ultima4/ultima4.h"
#include "ultima/ultima4/views/dungeonview.h"
#include "ultima/ultima4/views/tileview.h"
namespace Ultima {
namespace Ultima4 {
#define MOUSE_CURSOR_SIZE 20
#ifndef DBL_MAX
#define DBL_MAX 1e99
#endif
#define BUFFER_SIZE 1024
Screen *g_screen;
Screen::Screen() : _filterScaler(nullptr), _currentMouseCursor(-1),
_gemLayout(nullptr), _tileAnims(nullptr), _charSetInfo(nullptr),
_gemTilesInfo(nullptr), _needPrompt(1), _currentCycle(0),
_cursorStatus(0), _cursorEnabled(1), _priorFrameTime(0)
/* , _continueScreenRefresh(true) */ {
g_screen = this;
Common::fill(&_mouseCursors[0], &_mouseCursors[5], (MouseCursorSurface *)nullptr);
Common::fill(&_los[0][0], &_los[0][0] + (VIEWPORT_W * VIEWPORT_H), 0);
_filterNames.clear();
_filterNames.push_back("point");
_filterNames.push_back("2xBi");
_filterNames.push_back("2xSaI");
_filterNames.push_back("Scale2x");
_lineOfSightStyles.clear();
_lineOfSightStyles.push_back("DOS");
_lineOfSightStyles.push_back("Enhanced");
}
Screen::~Screen() {
clear();
for (uint idx = 0; idx < _tileAnimSets.size(); ++idx)
delete _tileAnimSets[idx];
g_screen = nullptr;
}
void Screen::init() {
Common::Point size(SCREEN_WIDTH * settings._scale, SCREEN_HEIGHT * settings._scale);
initGraphics(size.x, size.y, nullptr);
create(size.x, size.y, g_system->getScreenFormat());
loadMouseCursors();
screenLoadGraphicsFromConf();
debug(1, "using %s scaler\n", settings._filter.c_str());
/* find the tile animations for our tileset */
_tileAnims = nullptr;
for (auto *set : _tileAnimSets) {
if (set->_name == settings._videoType)
_tileAnims = set;
}
if (!_tileAnims)
error("unable to find tile animations for \"%s\" video mode in graphics.xml", settings._videoType.c_str());
_dungeonTileChars.clear();
_dungeonTileChars["brick_floor"] = CHARSET_FLOOR;
_dungeonTileChars["up_ladder"] = CHARSET_LADDER_UP;
_dungeonTileChars["down_ladder"] = CHARSET_LADDER_DOWN;
_dungeonTileChars["up_down_ladder"] = CHARSET_LADDER_UPDOWN;
_dungeonTileChars["chest"] = '$';
_dungeonTileChars["ceiling_hole"] = CHARSET_FLOOR;
_dungeonTileChars["floor_hole"] = CHARSET_FLOOR;
_dungeonTileChars["magic_orb"] = CHARSET_ORB;
_dungeonTileChars["ceiling_hole"] = 'T';
_dungeonTileChars["floor_hole"] = 'T';
_dungeonTileChars["fountain"] = 'F';
_dungeonTileChars["secret_door"] = CHARSET_SDOOR;
_dungeonTileChars["brick_wall"] = CHARSET_WALL;
_dungeonTileChars["dungeon_door"] = CHARSET_ROOM;
_dungeonTileChars["avatar"] = CHARSET_REDDOT;
_dungeonTileChars["dungeon_room"] = CHARSET_ROOM;
_dungeonTileChars["dungeon_altar"] = CHARSET_ANKH;
_dungeonTileChars["energy_field"] = '^';
_dungeonTileChars["fire_field"] = '^';
_dungeonTileChars["poison_field"] = '^';
_dungeonTileChars["sleep_field"] = '^';
}
void Screen::clear() {
// Clear any pending updates for the current screen
update();
for (auto *layout : _layouts)
delete layout;
_layouts.clear();
ImageMgr::destroy();
_charSetInfo = nullptr;
// Delete cursors
for (int idx = 0; idx < 5; ++idx) {
delete _mouseCursors[idx];
_mouseCursors[idx] = nullptr;
}
}
void Screen::loadMouseCursors() {
// enable or disable the mouse cursor
if (settings._mouseOptions._enabled) {
Common::File cursorsFile;
if (!cursorsFile.open("data/graphics/cursors.txt"))
error("Could not load mouse cursors");
for (int idx = 0; idx < 5; ++idx)
_mouseCursors[idx] = loadMouseCursor(cursorsFile);
// Set the default initial cursor
const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80);
MouseCursorSurface *c = _mouseCursors[MC_DEFAULT];
CursorMan.pushCursor(*c, c->_hotspot.x, c->_hotspot.y, TRANSPARENT, false);
CursorMan.showMouse(true);
} else {
CursorMan.showMouse(false);
}
_filterScaler = scalerGet(settings._filter);
if (!_filterScaler)
error("%s is not a valid filter", settings._filter.c_str());
}
void Screen::setMouseCursor(MouseCursor cursor) {
const MouseCursorSurface *c = _mouseCursors[cursor];
if (c && cursor != _currentMouseCursor) {
_currentMouseCursor = cursor;
const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80);
CursorMan.replaceCursor(*c, c->_hotspot.x, c->_hotspot.y, TRANSPARENT, false);
}
}
MouseCursorSurface *Screen::loadMouseCursor(Common::File &src) {
uint row, col, endCol, pixel;
int hotX, hotY;
Common::String line;
byte *destP;
const uint WHITE = format.RGBToColor(0xff, 0xff, 0xff);
const uint BLACK = format.RGBToColor(0, 0, 0);
const uint TRANSPARENT = format.RGBToColor(0x80, 0x80, 0x80);
int bpp = format.bytesPerPixel;
assert(bpp >= 2);
MouseCursorSurface *c = new MouseCursorSurface();
c->create(MOUSE_CURSOR_SIZE, MOUSE_CURSOR_SIZE, format);
c->clear(TRANSPARENT);
for (row = 0; row < MOUSE_CURSOR_SIZE; row++) {
line = src.readLine();
destP = (byte *)c->getBasePtr(0, row);
endCol = MIN(line.size(), (uint)MOUSE_CURSOR_SIZE);
for (col = 0; col < endCol; ++col, destP += bpp) {
pixel = TRANSPARENT;
if (line[col] == 'X')
pixel = BLACK;
else if (line[col] == '.')
pixel = WHITE;
if (bpp == 2)
*((uint16 *)destP) = pixel;
else
*((uint32 *)destP) = pixel;
}
}
// Read in the hotspot position
line = src.readLine();
(void)sscanf(line.c_str(), "%d,%d", &hotX, &hotY);
c->_hotspot.x = hotX;
c->_hotspot.y = hotY;
return c;
}
void Screen::screenReInit() {
g_intro->deleteIntro(); // delete intro stuff
g_tileSets->unloadAllImages(); // unload tilesets, which will be reloaded lazily as needed
ImageMgr::destroy();
_tileAnims = nullptr;
clear();
init(); // Re-init screen stuff (loading new backgrounds, etc.)
g_intro->init(); // Re-fix the backgrounds loaded and scale images, etc.
}
void Screen::screenTextAt(int x, int y, const char *fmt, ...) {
char buffer[BUFFER_SIZE];
uint i;
va_list args;
va_start(args, fmt);
vsnprintf(buffer, BUFFER_SIZE, fmt, args);
va_end(args);
for (i = 0; i < strlen(buffer); i++)
screenShowChar(buffer[i], x + i, y);
}
void Screen::screenPrompt() {
if (_needPrompt && _cursorEnabled && g_context->_col == 0) {
screenMessage("%c", CHARSET_PROMPT);
_needPrompt = 0;
}
}
void Screen::screenMessage(const char *fmt, ...) {
#ifdef IOS_ULTIMA4
static bool recursed = false;
#endif
if (!g_context)
return; //Because some cases (like the intro) don't have the context initiated.
char buffer[BUFFER_SIZE];
uint i;
int wordlen;
va_list args;
va_start(args, fmt);
vsnprintf(buffer, BUFFER_SIZE, fmt, args);
va_end(args);
#ifdef IOS_ULTIMA4
if (recursed)
recursed = false;
else
U4IOS::drawMessageOnLabel(Common::String(buffer, 1024));
#endif
screenHideCursor();
/* scroll the message area, if necessary */
if (g_context->_line == 12) {
screenScrollMessageArea();
g_context->_line--;
}
for (i = 0; i < strlen(buffer); i++) {
// include whitespace and color-change codes
wordlen = strcspn(buffer + i, " \b\t\n\024\025\026\027\030\031");
/* backspace */
if (buffer[i] == '\b') {
g_context->_col--;
if (g_context->_col < 0) {
g_context->_col += 16;
g_context->_line--;
}
continue;
}
/* color-change codes */
switch (buffer[i]) {
case FG_GREY:
case FG_BLUE:
case FG_PURPLE:
case FG_GREEN:
case FG_RED:
case FG_YELLOW:
case FG_WHITE:
screenTextColor(buffer[i]);
continue;
}
/* check for word wrap */
if ((g_context->_col + wordlen > 16) || buffer[i] == '\n' || g_context->_col == 16) {
if (buffer[i] == '\n' || buffer[i] == ' ')
i++;
g_context->_line++;
g_context->_col = 0;
#ifdef IOS_ULTIMA4
recursed = true;
#endif
screenMessage("%s", buffer + i);
return;
}
/* code for move cursor right */
if (buffer[i] == 0x12) {
g_context->_col++;
continue;
}
/* don't show a space in column 1. Helps with Hawkwind. */
if (buffer[i] == ' ' && g_context->_col == 0)
continue;
screenShowChar(buffer[i], TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
g_context->_col++;
}
screenSetCursorPos(TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
screenShowCursor();
_needPrompt = 1;
}
void Screen::screenLoadGraphicsFromConf() {
const Config *config = Config::getInstance();
Std::vector graphicsConf = config->getElement("graphics").getChildren();
for (const auto &conf : graphicsConf) {
if (conf.getName() == "layout")
_layouts.push_back(screenLoadLayoutFromConf(conf));
else if (conf.getName() == "tileanimset")
_tileAnimSets.push_back(new TileAnimSet(conf));
}
_gemLayoutNames.clear();
for (const auto *layout : _layouts) {
if (layout->_type == LAYOUT_GEM) {
_gemLayoutNames.push_back(layout->_name);
}
}
/*
* Find gem layout to use.
*/
for (auto *layout : _layouts) {
if (layout->_type == LAYOUT_GEM && layout->_name == settings._gemLayout) {
_gemLayout = layout;
break;
}
}
if (!_gemLayout)
error("no gem layout named %s found!\n", settings._gemLayout.c_str());
}
Layout *Screen::screenLoadLayoutFromConf(const ConfigElement &conf) {
Layout *layout;
static const char *const typeEnumStrings[] = {"standard", "gem", "dungeon_gem", nullptr};
layout = new Layout();
layout->_name = conf.getString("name");
layout->_type = static_cast(conf.getEnum("type", typeEnumStrings));
Std::vector children = conf.getChildren();
for (const auto &i : children) {
if (i.getName() == "tileshape") {
layout->_tileShape.x = i.getInt("width");
layout->_tileShape.y = i.getInt("height");
} else if (i.getName() == "viewport") {
layout->_viewport.left = i.getInt("x");
layout->_viewport.top = i.getInt("y");
layout->_viewport.setWidth(i.getInt("width"));
layout->_viewport.setHeight(i.getInt("height"));
}
}
return layout;
}
Std::vector Screen::screenViewportTile(uint width, uint height, int x, int y, bool &focus) {
MapCoords center = g_context->_location->_coords;
static MapTile grass = g_context->_location->_map->_tileSet->getByName("grass")->getId();
if (g_context->_location->_map->_width <= width &&
g_context->_location->_map->_height <= height) {
center.x = g_context->_location->_map->_width / 2;
center.y = g_context->_location->_map->_height / 2;
}
MapCoords tc = center;
tc.x += x - (width / 2);
tc.y += y - (height / 2);
/* Wrap the location if we can */
tc.wrap(g_context->_location->_map);
/* off the edge of the map: pad with grass tiles */
if (MAP_IS_OOB(g_context->_location->_map, tc)) {
focus = false;
Std::vector result;
result.push_back(grass);
return result;
}
return g_context->_location->tilesAt(tc, focus);
}
bool Screen::screenTileUpdate(TileView *view, const Coords &coords, bool redraw) {
if (g_context->_location->_map->_flags & FIRST_PERSON)
return false;
// Get the tiles
bool focus;
MapCoords mc(coords);
mc.wrap(g_context->_location->_map);
Std::vector tiles = g_context->_location->tilesAt(mc, focus);
// Get the screen coordinates
int x = coords.x;
int y = coords.y;
if (g_context->_location->_map->_width > VIEWPORT_W || g_context->_location->_map->_height > VIEWPORT_H) {
//Center the coordinates to the viewport if you're on centered-view map.
x = x - g_context->_location->_coords.x + VIEWPORT_W / 2;
y = y - g_context->_location->_coords.y + VIEWPORT_H / 2;
}
// Draw if it is on screen
if (x >= 0 && y >= 0 && x < VIEWPORT_W && y < VIEWPORT_H && _los[x][y]) {
view->drawTile(tiles, focus, x, y);
if (redraw) {
//screenRedrawMapArea();
}
return true;
}
return false;
}
void Screen::screenUpdate(TileView *view, bool showmap, bool blackout) {
assertMsg(g_context != nullptr, "context has not yet been initialized");
if (blackout) {
screenEraseMapArea();
} else if (g_context->_location->_viewMode == VIEW_GEM) {
// No need to render view when cheat overhead map showing
} else if (g_context->_location->_map->_flags & FIRST_PERSON) {
DungeonViewer.display(g_context, view);
screenRedrawMapArea();
} else if (showmap) {
static MapTile black = g_context->_location->_map->_tileSet->getByName("black")->getId();
//static MapTile avatar = g_context->_location->_map->_tileset->getByName("avatar")->getId();
int x, y;
Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H];
bool viewportFocus[VIEWPORT_W][VIEWPORT_H];
for (y = 0; y < VIEWPORT_H; y++) {
for (x = 0; x < VIEWPORT_W; x++) {
viewportTiles[x][y] = screenViewportTile(VIEWPORT_W, VIEWPORT_H, x, y, viewportFocus[x][y]);
}
}
screenFindLineOfSight(viewportTiles);
for (y = 0; y < VIEWPORT_H; y++) {
for (x = 0; x < VIEWPORT_W; x++) {
if (_los[x][y]) {
view->drawTile(viewportTiles[x][y], viewportFocus[x][y], x, y);
} else
view->drawTile(black, false, x, y);
}
}
screenRedrawMapArea();
}
screenUpdateCursor();
screenUpdateMoons();
screenUpdateWind();
}
void Screen::screenDrawImage(const Common::String &name, int x, int y) {
ImageInfo *info = imageMgr->get(name);
if (info) {
info->_image->alphaOn();
info->_image->draw(x, y);
return;
}
SubImage *subimage = imageMgr->getSubImage(name);
if (subimage)
info = imageMgr->get(subimage->_srcImageName);
if (info) {
info->_image->alphaOn();
if (info) {
info->_image->drawSubRect(x, y,
subimage->left * (settings._scale / info->_prescale),
subimage->top * (settings._scale / info->_prescale),
subimage->width() * (settings._scale / info->_prescale),
subimage->height() * (settings._scale / info->_prescale));
return;
}
}
error("ERROR 1006: Unable to load the image \"%s\"", name.c_str());
}
void Screen::screenDrawImageInMapArea(const Common::String &name) {
ImageInfo *info;
info = imageMgr->get(name);
if (!info)
error("ERROR 1004: Unable to load data files");
info->_image->drawSubRect(BORDER_WIDTH * settings._scale, BORDER_HEIGHT * settings._scale,
BORDER_WIDTH * settings._scale, BORDER_HEIGHT * settings._scale,
VIEWPORT_W * TILE_WIDTH * settings._scale,
VIEWPORT_H * TILE_HEIGHT * settings._scale);
}
void Screen::screenTextColor(int color) {
if (_charSetInfo == nullptr) {
_charSetInfo = imageMgr->get(BKGD_CHARSET);
if (!_charSetInfo)
error("ERROR 1003: Unable to load the \"%s\" data file", BKGD_CHARSET);
}
if (!settings._enhancements || !settings._enhancementsOptions._textColorization) {
return;
}
switch (color) {
case FG_GREY:
case FG_BLUE:
case FG_PURPLE:
case FG_GREEN:
case FG_RED:
case FG_YELLOW:
case FG_WHITE:
_charSetInfo->_image->setFontColorFG((ColorFG)color);
}
}
void Screen::screenShowChar(int chr, int x, int y) {
if (_charSetInfo == nullptr) {
_charSetInfo = imageMgr->get(BKGD_CHARSET);
if (!_charSetInfo)
error("ERROR 1001: Unable to load the \"%s\" data file", BKGD_CHARSET);
}
_charSetInfo->_image->drawSubRect(x * _charSetInfo->_image->width(), y * (CHAR_HEIGHT * settings._scale),
0, chr * (CHAR_HEIGHT * settings._scale),
_charSetInfo->_image->width(), CHAR_HEIGHT * settings._scale);
}
void Screen::screenScrollMessageArea() {
assertMsg(_charSetInfo != nullptr && _charSetInfo->_image != nullptr, "charset not initialized!");
Image *screen = imageMgr->get("screen")->_image;
screen->drawSubRectOn(screen,
TEXT_AREA_X * _charSetInfo->_image->width(),
TEXT_AREA_Y * CHAR_HEIGHT * settings._scale,
TEXT_AREA_X * _charSetInfo->_image->width(),
(TEXT_AREA_Y + 1) * CHAR_HEIGHT * settings._scale,
TEXT_AREA_W * _charSetInfo->_image->width(),
(TEXT_AREA_H - 1) * CHAR_HEIGHT * settings._scale);
screen->fillRect(TEXT_AREA_X * _charSetInfo->_image->width(),
TEXT_AREA_Y * CHAR_HEIGHT * settings._scale + (TEXT_AREA_H - 1) * CHAR_HEIGHT * settings._scale,
TEXT_AREA_W * _charSetInfo->_image->width(),
CHAR_HEIGHT * settings._scale,
0, 0, 0);
update();
}
void Screen::screenFrame() {
uint32 time = g_system->getMillis();
if (time >= (_priorFrameTime + SCREEN_FRAME_TIME)) {
_priorFrameTime = time;
update();
}
}
void Screen::screenCycle() {
if (++_currentCycle >= SCR_CYCLE_MAX)
_currentCycle = 0;
}
void Screen::screenUpdateCursor() {
int phase = _currentCycle * SCR_CYCLE_PER_SECOND / SCR_CYCLE_MAX;
assertMsg(phase >= 0 && phase < 4, "derived an invalid cursor phase: %d", phase);
if (_cursorStatus) {
screenShowChar(31 - phase, _cursorPos.x, _cursorPos.y);
screenRedrawTextArea(_cursorPos.x, _cursorPos.y, 1, 1);
}
}
void Screen::screenUpdateMoons() {
int trammelChar, feluccaChar;
/* show "L?" for the dungeon level */
if (g_context->_location->_context == CTX_DUNGEON) {
screenShowChar('L', 11, 0);
screenShowChar('1' + g_context->_location->_coords.z, 12, 0);
}
/* show the current moons (non-combat) */
else if ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context) {
trammelChar = (g_ultima->_saveGame->_trammelPhase == 0) ? MOON_CHAR + 7 : MOON_CHAR + g_ultima->_saveGame->_trammelPhase - 1;
feluccaChar = (g_ultima->_saveGame->_feluccaPhase == 0) ? MOON_CHAR + 7 : MOON_CHAR + g_ultima->_saveGame->_feluccaPhase - 1;
screenShowChar(trammelChar, 11, 0);
screenShowChar(feluccaChar, 12, 0);
}
screenRedrawTextArea(11, 0, 2, 1);
}
void Screen::screenUpdateWind() {
/* show the direction we're facing in the dungeon */
if (g_context->_location->_context == CTX_DUNGEON) {
screenEraseTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H);
screenTextAt(WIND_AREA_X, WIND_AREA_Y, "Dir: %5s", getDirectionName((Direction)g_ultima->_saveGame->_orientation));
}
/* show the wind direction */
else if ((g_context->_location->_context & CTX_NON_COMBAT) == g_context->_location->_context) {
screenEraseTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H);
screenTextAt(WIND_AREA_X, WIND_AREA_Y, "Wind %5s", getDirectionName((Direction)g_context->_windDirection));
}
screenRedrawTextArea(WIND_AREA_X, WIND_AREA_Y, WIND_AREA_W, WIND_AREA_H);
}
void Screen::screenShowCursor() {
if (!_cursorStatus && _cursorEnabled) {
_cursorStatus = 1;
screenUpdateCursor();
}
}
void Screen::screenHideCursor() {
if (_cursorStatus) {
screenEraseTextArea(_cursorPos.x, _cursorPos.y, 1, 1);
screenRedrawTextArea(_cursorPos.x, _cursorPos.y, 1, 1);
}
_cursorStatus = 0;
}
void Screen::screenEnableCursor(void) {
_cursorEnabled = 1;
}
void Screen::screenDisableCursor(void) {
screenHideCursor();
_cursorEnabled = 0;
}
void Screen::screenSetCursorPos(int x, int y) {
_cursorPos.x = x;
_cursorPos.y = y;
}
void Screen::screenFindLineOfSight(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) {
int x, y;
if (!g_context)
return;
/*
* if the map has the no line of sight flag, all is visible
*/
if (g_context->_location->_map->_flags & NO_LINE_OF_SIGHT) {
for (y = 0; y < VIEWPORT_H; y++) {
for (x = 0; x < VIEWPORT_W; x++) {
_los[x][y] = 1;
}
}
return;
}
/*
* otherwise calculate it from the map data
*/
for (y = 0; y < VIEWPORT_H; y++) {
for (x = 0; x < VIEWPORT_W; x++) {
_los[x][y] = 0;
}
}
if (settings._lineOfSight == "DOS")
screenFindLineOfSightDOS(viewportTiles);
else if (settings._lineOfSight == "Enhanced")
screenFindLineOfSightEnhanced(viewportTiles);
else
error("unknown line of sight style %s!\n", settings._lineOfSight.c_str());
}
void Screen::screenFindLineOfSightDOS(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) {
int x, y;
_los[VIEWPORT_W / 2][VIEWPORT_H / 2] = 1;
for (x = VIEWPORT_W / 2 - 1; x >= 0; x--)
if (_los[x + 1][VIEWPORT_H / 2] &&
!viewportTiles[x + 1][VIEWPORT_H / 2].front().getTileType()->isOpaque())
_los[x][VIEWPORT_H / 2] = 1;
for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++)
if (_los[x - 1][VIEWPORT_H / 2] &&
!viewportTiles[x - 1][VIEWPORT_H / 2].front().getTileType()->isOpaque())
_los[x][VIEWPORT_H / 2] = 1;
for (y = VIEWPORT_H / 2 - 1; y >= 0; y--)
if (_los[VIEWPORT_W / 2][y + 1] &&
!viewportTiles[VIEWPORT_W / 2][y + 1].front().getTileType()->isOpaque())
_los[VIEWPORT_W / 2][y] = 1;
for (y = VIEWPORT_H / 2 + 1; y < VIEWPORT_H; y++)
if (_los[VIEWPORT_W / 2][y - 1] &&
!viewportTiles[VIEWPORT_W / 2][y - 1].front().getTileType()->isOpaque())
_los[VIEWPORT_W / 2][y] = 1;
for (y = VIEWPORT_H / 2 - 1; y >= 0; y--) {
for (x = VIEWPORT_W / 2 - 1; x >= 0; x--) {
if (_los[x][y + 1] &&
!viewportTiles[x][y + 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x + 1][y] &&
!viewportTiles[x + 1][y].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x + 1][y + 1] &&
!viewportTiles[x + 1][y + 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
}
for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++) {
if (_los[x][y + 1] &&
!viewportTiles[x][y + 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x - 1][y] &&
!viewportTiles[x - 1][y].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x - 1][y + 1] &&
!viewportTiles[x - 1][y + 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
}
}
for (y = VIEWPORT_H / 2 + 1; y < VIEWPORT_H; y++) {
for (x = VIEWPORT_W / 2 - 1; x >= 0; x--) {
if (_los[x][y - 1] &&
!viewportTiles[x][y - 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x + 1][y] &&
!viewportTiles[x + 1][y].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x + 1][y - 1] &&
!viewportTiles[x + 1][y - 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
}
for (x = VIEWPORT_W / 2 + 1; x < VIEWPORT_W; x++) {
if (_los[x][y - 1] &&
!viewportTiles[x][y - 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x - 1][y] &&
!viewportTiles[x - 1][y].front().getTileType()->isOpaque())
_los[x][y] = 1;
else if (_los[x - 1][y - 1] &&
!viewportTiles[x - 1][y - 1].front().getTileType()->isOpaque())
_los[x][y] = 1;
}
}
}
void Screen::screenFindLineOfSightEnhanced(Std::vector viewportTiles[VIEWPORT_W][VIEWPORT_H]) {
int x, y;
/*
* the shadow rasters for each viewport octant
*
* shadowRaster[0][0] // number of raster segments in this shadow
* shadowRaster[0][1] // #1 shadow bitmask value (low three bits) + "newline" flag (high bit)
* shadowRaster[0][2] // #1 length
* shadowRaster[0][3] // #2 shadow bitmask value
* shadowRaster[0][4] // #2 length
* shadowRaster[0][5] // #3 shadow bitmask value
* shadowRaster[0][6] // #3 length
* ...etc...
*/
const int shadowRaster[14][13] = {
{6, __VCH, 4, _N_CH, 1, __VCH, 3, _N___, 1, ___CH, 1, __VCH, 1}, // raster_1_0
{6, __VC_, 1, _NVCH, 2, __VC_, 1, _NVCH, 3, _NVCH, 2, _NVCH, 1}, // raster_1_1
//
{4, __VCH, 3, _N__H, 1, ___CH, 1, __VCH, 1, 0, 0, 0, 0}, // raster_2_0
{6, __VC_, 2, _N_CH, 1, __VCH, 2, _N_CH, 1, __VCH, 1, _N__H, 1}, // raster_2_1
{6, __V__, 1, _NVCH, 1, __VC_, 1, _NVCH, 1, __VC_, 1, _NVCH, 1}, // raster_2_2
//
{2, __VCH, 2, _N__H, 2, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_3_0
{3, __VC_, 2, _N_CH, 1, __VCH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_1
{3, __VC_, 1, _NVCH, 2, _N_CH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_2
{3, _NVCH, 1, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0}, // raster_3_3
//
{2, __VCH, 1, _N__H, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_0
{2, __VC_, 1, _N__H, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_1
{2, __VC_, 1, _N_CH, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_2
{2, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // raster_4_3
{2, __V__, 1, _NVCH, 1, 0, 0, 0, 0, 0, 0, 0, 0} // raster_4_4
};
/*
* As each viewport tile is processed, it will store the bitmask for the shadow it casts.
* Later, after processing all octants, the entire viewport will be marked visible except
* for those tiles that have the __VCH bitmask.
*/
const int _OCTANTS = 8;
const int _NUM_RASTERS_COLS = 4;
int octant;
int xOrigin, yOrigin, xSign = 0, ySign = 0, reflect = false, xTile, yTile, xTileOffset, yTileOffset;
for (octant = 0; octant < _OCTANTS; octant++) {
switch (octant) {
case 0:
xSign = 1;
ySign = 1;
reflect = false;
break; // lower-right
case 1:
xSign = 1;
ySign = 1;
reflect = true;
break;
case 2:
xSign = 1;
ySign = -1;
reflect = true;
break; // lower-left
case 3:
xSign = -1;
ySign = 1;
reflect = false;
break;
case 4:
xSign = -1;
ySign = -1;
reflect = false;
break; // upper-left
case 5:
xSign = -1;
ySign = -1;
reflect = true;
break;
case 6:
xSign = -1;
ySign = 1;
reflect = true;
break; // upper-right
case 7:
xSign = 1;
ySign = -1;
reflect = false;
break;
}
// determine the origin point for the current LOS octant
xOrigin = VIEWPORT_W / 2;
yOrigin = VIEWPORT_H / 2;
// make sure the segment doesn't reach out of bounds
int maxWidth = xOrigin;
int maxHeight = yOrigin;
int currentRaster = 0;
// just in case the viewport isn't square, swap the width and height
if (reflect) {
// swap height and width for later use
maxWidth ^= maxHeight;
maxHeight ^= maxWidth;
maxWidth ^= maxHeight;
}
// check the visibility of each tile
for (int currentCol = 1; currentCol <= _NUM_RASTERS_COLS; currentCol++) {
for (int currentRow = 0; currentRow <= currentCol; currentRow++) {
// swap X and Y to reflect the octant rasters
if (reflect) {
xTile = xOrigin + (currentRow * ySign);
yTile = yOrigin + (currentCol * xSign);
} else {
xTile = xOrigin + (currentCol * xSign);
yTile = yOrigin + (currentRow * ySign);
}
if (viewportTiles[xTile][yTile].front().getTileType()->isOpaque()) {
// a wall was detected, so go through the raster for this wall
// segment and mark everything behind it with the appropriate
// shadow bitmask.
//
// first, get the correct raster
//
if ((currentCol == 1) && (currentRow == 0)) {
currentRaster = 0;
} else if ((currentCol == 1) && (currentRow == 1)) {
currentRaster = 1;
} else if ((currentCol == 2) && (currentRow == 0)) {
currentRaster = 2;
} else if ((currentCol == 2) && (currentRow == 1)) {
currentRaster = 3;
} else if ((currentCol == 2) && (currentRow == 2)) {
currentRaster = 4;
} else if ((currentCol == 3) && (currentRow == 0)) {
currentRaster = 5;
} else if ((currentCol == 3) && (currentRow == 1)) {
currentRaster = 6;
} else if ((currentCol == 3) && (currentRow == 2)) {
currentRaster = 7;
} else if ((currentCol == 3) && (currentRow == 3)) {
currentRaster = 8;
} else if ((currentCol == 4) && (currentRow == 0)) {
currentRaster = 9;
} else if ((currentCol == 4) && (currentRow == 1)) {
currentRaster = 10;
} else if ((currentCol == 4) && (currentRow == 2)) {
currentRaster = 11;
} else if ((currentCol == 4) && (currentRow == 3)) {
currentRaster = 12;
} else {
currentRaster = 13; // currentCol and currentRow must equal 4
}
xTileOffset = 0;
yTileOffset = 0;
//========================================
for (int currentSegment = 0; currentSegment < shadowRaster[currentRaster][0]; currentSegment++) {
// each shadow segment is 2 bytes
int shadowType = shadowRaster[currentRaster][currentSegment * 2 + 1];
int shadowLength = shadowRaster[currentRaster][currentSegment * 2 + 2];
// update the raster length to make sure it fits in the viewport
shadowLength = (shadowLength + 1 + yTileOffset > maxWidth ? maxWidth : shadowLength);
// check to see if we should move up a row
if (shadowType & 0x80) {
// remove the flag from the shadowType
shadowType ^= _N___;
// if (currentRow + yTileOffset >= maxHeight) {
if (currentRow + yTileOffset > maxHeight) {
break;
}
xTileOffset = yTileOffset;
yTileOffset++;
}
/* it is seemingly unnecessary to swap the edges for
* shadow tiles, because we only care about shadow
* tiles that have all three parts (V, C, and H)
* flagged. if a tile has fewer than three, it is
* ignored during the draw phase, so vertical and
* horizontal shadow edge accuracy isn't important
*/
// if reflecting the octant, swap the edges
// if (reflect) {
// int shadowTemp = 0;
// // swap the vertical and horizontal shadow edges
// if (shadowType & __V__) { shadowTemp |= ____H; }
// if (shadowType & ___C_) { shadowTemp |= ___C_; }
// if (shadowType & ____H) { shadowTemp |= __V__; }
// shadowType = shadowTemp;
// }
for (int currentShadow = 1; currentShadow <= shadowLength; currentShadow++) {
// apply the shadow to the shadowMap
if (reflect) {
_los[xTile + ((yTileOffset)*ySign)][yTile + ((currentShadow + xTileOffset) * xSign)] |= shadowType;
} else {
_los[xTile + ((currentShadow + xTileOffset) * xSign)][yTile + ((yTileOffset)*ySign)] |= shadowType;
}
}
xTileOffset += shadowLength;
} // for (int currentSegment = 0; currentSegment < shadowRaster[currentRaster][0]; currentSegment++)
//========================================
} // if (viewportTiles[xTile][yTile].front().getTileType()->isOpaque())
} // for (int currentRow = 0; currentRow <= currentCol; currentRow++)
} // for (int currentCol = 1; currentCol <= _NUM_RASTERS_COLS; currentCol++)
} // for (octant = 0; octant < _OCTANTS; octant++)
// go through all tiles on the viewable area and set the appropriate visibility
for (y = 0; y < VIEWPORT_H; y++) {
for (x = 0; x < VIEWPORT_W; x++) {
// if the shadow flags equal __VCH, hide it, otherwise it's fully visible
//
if ((_los[x][y] & __VCH) == __VCH) {
_los[x][y] = 0;
} else {
_los[x][y] = 1;
}
}
}
}
void Screen::screenGetLineTerms(int x1, int y1, int x2, int y2, double *a, double *b) {
if (x2 - x1 == 0) {
*a = DBL_MAX;
*b = x1;
} else {
*a = ((double)(y2 - y1)) / ((double)(x2 - x1));
*b = y1 - ((*a) * x1);
}
}
int Screen::screenPointsOnSameSideOfLine(int x1, int y1, int x2, int y2, double a, double b) {
double p1, p2;
if (a == DBL_MAX) {
p1 = x1 - b;
p2 = x2 - b;
} else {
p1 = x1 * a + b - y1;
p2 = x2 * a + b - y2;
}
if ((p1 > 0.0 && p2 > 0.0) ||
(p1 < 0.0 && p2 < 0.0) ||
(p1 == 0.0 && p2 == 0.0))
return 1;
return 0;
}
int Screen::screenPointInTriangle(int x, int y, int tx1, int ty1, int tx2, int ty2, int tx3, int ty3) {
double a[3], b[3];
screenGetLineTerms(tx1, ty1, tx2, ty2, &(a[0]), &(b[0]));
screenGetLineTerms(tx2, ty2, tx3, ty3, &(a[1]), &(b[1]));
screenGetLineTerms(tx3, ty3, tx1, ty1, &(a[2]), &(b[2]));
if (!screenPointsOnSameSideOfLine(x, y, tx3, ty3, a[0], b[0]))
return 0;
if (!screenPointsOnSameSideOfLine(x, y, tx1, ty1, a[1], b[1]))
return 0;
if (!screenPointsOnSameSideOfLine(x, y, tx2, ty2, a[2], b[2]))
return 0;
return 1;
}
int Screen::screenPointInMouseArea(int x, int y, const MouseArea *area) {
assertMsg(area->_nPoints == 2 || area->_nPoints == 3, "unsupported number of points in area: %d", area->_nPoints);
/* two points define a rectangle */
if (area->_nPoints == 2) {
if (x >= (int)(area->_point[0].x * settings._scale) &&
y >= (int)(area->_point[0].y * settings._scale) &&
x < (int)(area->_point[1].x * settings._scale) &&
y < (int)(area->_point[1].y * settings._scale)) {
return 1;
}
}
/* three points define a triangle */
else if (area->_nPoints == 3) {
return screenPointInTriangle(x, y,
area->_point[0].x * settings._scale, area->_point[0].y * settings._scale,
area->_point[1].x * settings._scale, area->_point[1].y * settings._scale,
area->_point[2].x * settings._scale, area->_point[2].y * settings._scale);
}
return 0;
}
void Screen::screenRedrawMapArea() {
g_game->_mapArea.update();
}
void Screen::screenEraseMapArea() {
Image *screen = imageMgr->get("screen")->_image;
screen->fillRect(BORDER_WIDTH * settings._scale,
BORDER_WIDTH * settings._scale,
VIEWPORT_W * TILE_WIDTH * settings._scale,
VIEWPORT_H * TILE_HEIGHT * settings._scale,
0, 0, 0);
}
void Screen::screenEraseTextArea(int x, int y, int width, int height) {
Image *screen = imageMgr->get("screen")->_image;
screen->fillRect(x * CHAR_WIDTH * settings._scale,
y * CHAR_HEIGHT * settings._scale,
width * CHAR_WIDTH * settings._scale,
height * CHAR_HEIGHT * settings._scale,
0, 0, 0);
}
void Screen::screenShake(int iterations) {
if (settings._screenShakes) {
// specify the size of the shake
const int SHAKE_OFFSET = 1 * settings._scale;
for (int i = 0; i < iterations; ++i) {
// Shift the screen down
g_system->setShakePos(0, SHAKE_OFFSET);
g_system->updateScreen();
EventHandler::sleep(settings._shakeInterval);
// shift the screen back up
g_system->setShakePos(0, 0);
g_system->updateScreen();
EventHandler::sleep(settings._shakeInterval);
}
}
}
void Screen::screenShowGemTile(Layout *layout, Map *map, MapTile &t, bool focus, int x, int y) {
// Make sure we account for tiles that look like other tiles (dungeon tiles, mainly)
Common::String looks_like = t.getTileType()->getLooksLike();
if (!looks_like.empty())
t = map->_tileSet->getByName(looks_like)->getId();
uint tile = map->translateToRawTileIndex(t);
if (map->_type == Map::DUNGEON) {
assertMsg(_charSetInfo, "charset not initialized");
Common::HashMap::iterator charIndex = _dungeonTileChars.find(t.getTileType()->getName());
if (charIndex != _dungeonTileChars.end()) {
_charSetInfo->_image->drawSubRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale,
(layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale,
0,
charIndex->_value * layout->_tileShape.y * settings._scale,
layout->_tileShape.x * settings._scale,
layout->_tileShape.y * settings._scale);
}
} else {
if (_gemTilesInfo == nullptr) {
_gemTilesInfo = imageMgr->get(BKGD_GEMTILES);
if (!_gemTilesInfo)
error("ERROR 1002: Unable to load the \"%s\" data file", BKGD_GEMTILES);
}
if (tile < 128) {
_gemTilesInfo->_image->drawSubRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale,
(layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale,
0,
tile * layout->_tileShape.y * settings._scale,
layout->_tileShape.x * settings._scale,
layout->_tileShape.y * settings._scale);
} else {
Image *screen = imageMgr->get("screen")->_image;
screen->fillRect((layout->_viewport.left + (x * layout->_tileShape.x)) * settings._scale,
(layout->_viewport.top + (y * layout->_tileShape.y)) * settings._scale,
layout->_tileShape.x * settings._scale,
layout->_tileShape.y * settings._scale,
0, 0, 0);
}
}
}
Layout *Screen::screenGetGemLayout(const Map *map) {
if (map->_type == Map::DUNGEON) {
for (auto *layout : _layouts) {
if (layout->_type == LAYOUT_DUNGEONGEM)
return layout;
}
error("no dungeon gem layout found!\n");
return nullptr;
} else
return _gemLayout;
}
void Screen::screenGemUpdate() {
MapTile tile;
int x, y;
Image *screen = imageMgr->get("screen")->_image;
screen->fillRect(BORDER_WIDTH * settings._scale,
BORDER_HEIGHT * settings._scale,
VIEWPORT_W * TILE_WIDTH * settings._scale,
VIEWPORT_H * TILE_HEIGHT * settings._scale,
0, 0, 0);
Layout *layout = screenGetGemLayout(g_context->_location->_map);
// TODO: Move the code responsible for determining 'peer' visibility to a non SDL specific part of the code.
if (g_context->_location->_map->_type == Map::DUNGEON) {
//DO THE SPECIAL DUNGEON MAP TRAVERSAL
Std::vector > drawnTiles(layout->_viewport.width(), Std::vector(layout->_viewport.height(), 0));
Common::List > coordStack;
//Put the avatar's position on the stack
int center_x = layout->_viewport.width() / 2 - 1;
int center_y = layout->_viewport.height() / 2 - 1;
int avt_x = g_context->_location->_coords.x - 1;
int avt_y = g_context->_location->_coords.y - 1;
coordStack.push_back(Common::Pair(center_x, center_y));
bool weAreDrawingTheAvatarTile = true;
//And draw each tile on the growing stack until it is empty
while (coordStack.size() > 0) {
Common::Pair currentXY = coordStack.back();
coordStack.pop_back();
x = currentXY.first;
y = currentXY.second;
if (x < 0 || x >= layout->_viewport.width() ||
y < 0 || y >= layout->_viewport.height())
continue; //Skip out of range tiles
if (drawnTiles[x][y])
continue; //Skip already considered tiles
drawnTiles[x][y] = 1;
// DRAW THE ACTUAL TILE
bool focus;
Std::vector tiles = screenViewportTile(layout->_viewport.width(),
layout->_viewport.height(), x - center_x + avt_x, y - center_y + avt_y, focus);
tile = tiles.front();
TileId avatarTileId = g_context->_location->_map->_tileSet->getByName("avatar")->getId();
if (!weAreDrawingTheAvatarTile) {
//Hack to avoid showing the avatar tile multiple times in cycling dungeon maps
if (tile.getId() == avatarTileId)
tile = g_context->_location->_map->getTileFromData(g_context->_location->_coords)->getId();
}
screenShowGemTile(layout, g_context->_location->_map, tile, focus, x, y);
if (!tile.getTileType()->isOpaque() || tile.getTileType()->isWalkable() || weAreDrawingTheAvatarTile) {
//Continue the search so we can see through all walkable objects, non-opaque objects (like creatures)
//or the avatar position in those rare circumstances where he is stuck in a wall
//by adding all relative adjacency combinations to the stack for drawing
coordStack.push_back(Common::Pair(x + 1, y - 1));
coordStack.push_back(Common::Pair(x + 1, y));
coordStack.push_back(Common::Pair(x + 1, y + 1));
coordStack.push_back(Common::Pair(x, y - 1));
coordStack.push_back(Common::Pair(x, y + 1));
coordStack.push_back(Common::Pair(x - 1, y - 1));
coordStack.push_back(Common::Pair(x - 1, y));
coordStack.push_back(Common::Pair(x - 1, y + 1));
// We only draw the avatar tile once, it is the first tile drawn
weAreDrawingTheAvatarTile = false;
}
}
} else {
// DO THE REGULAR EVERYTHING-IS-VISIBLE MAP TRAVERSAL
for (x = 0; x < layout->_viewport.width(); x++) {
for (y = 0; y < layout->_viewport.height(); y++) {
bool focus;
tile = screenViewportTile(layout->_viewport.width(), layout->_viewport.height(),
x, y, focus)
.front();
screenShowGemTile(layout, g_context->_location->_map, tile, focus, x, y);
}
}
}
screenRedrawMapArea();
screenUpdateCursor();
screenUpdateMoons();
screenUpdateWind();
}
void Screen::screenRedrawTextArea(int x, int y, int width, int height) {
g_system->updateScreen();
}
void Screen::screenWait(int numberOfAnimationFrames) {
update();
g_system->delayMillis(numberOfAnimationFrames * SCREEN_FRAME_TIME);
}
Image *Screen::screenScale(Image *src, int scale, int n, int filter) {
Image *dest = nullptr;
bool isTransparent;
uint transparentIndex;
bool alpha = src->isAlphaOn();
if (n == 0)
n = 1;
isTransparent = src->getTransparentIndex(transparentIndex);
src->alphaOff();
while (filter && _filterScaler && (scale % 2 == 0)) {
dest = (*_filterScaler)(src, 2, n);
src = dest;
scale /= 2;
}
if (scale == 3 && scaler3x(settings._filter)) {
dest = (*_filterScaler)(src, 3, n);
src = dest;
scale /= 3;
}
if (scale != 1)
dest = (*scalerGet("point"))(src, scale, n);
if (!dest)
dest = Image::duplicate(src, src->format());
if (isTransparent)
dest->setTransparentIndex(transparentIndex);
if (alpha)
src->alphaOn();
return dest;
}
Image *Screen::screenScaleDown(Image *src, int scale) {
int x, y;
Image *dest;
bool isTransparent;
uint transparentIndex;
bool alpha = src->isAlphaOn();
isTransparent = src->getTransparentIndex(transparentIndex);
src->alphaOff();
dest = Image::create(src->width() / scale, src->height() / scale, src->format());
if (!dest)
return nullptr;
if (dest->isIndexed())
dest->setPaletteFromImage(src);
for (y = 0; y < src->height(); y += scale) {
for (x = 0; x < src->width(); x += scale) {
uint index;
src->getPixelIndex(x, y, index);
dest->putPixelIndex(x / scale, y / scale, index);
}
}
if (isTransparent)
dest->setTransparentIndex(transparentIndex);
if (alpha)
src->alphaOn();
return dest;
}
#ifdef IOS_ULTIMA4
//Unsure if implementation required in iOS.
void inline screenLock(){};
void inline screenUnlock(){};
void inline screenWait(int numberOfAnimationFrames){};
#endif
const Std::vector &screenGetFilterNames() {
return g_screen->_filterNames;
}
const Std::vector &screenGetGemLayoutNames() {
return g_screen->_gemLayoutNames;
}
const Std::vector &screenGetLineOfSightStyles() {
return g_screen->_lineOfSightStyles;
}
} // End of namespace Ultima4
} // End of namespace Ultima