diff options
-rwxr-xr-x | devtools/create_wage/create_wage.sh | 119 | ||||
-rw-r--r-- | engines/wage/combat.cpp | 914 | ||||
-rw-r--r-- | engines/wage/configure.engine | 3 | ||||
-rw-r--r-- | engines/wage/design.cpp | 538 | ||||
-rw-r--r-- | engines/wage/design.h | 115 | ||||
-rw-r--r-- | engines/wage/detection.cpp | 238 | ||||
-rw-r--r-- | engines/wage/dialog.cpp | 241 | ||||
-rw-r--r-- | engines/wage/dialog.h | 101 | ||||
-rw-r--r-- | engines/wage/entities.cpp | 500 | ||||
-rw-r--r-- | engines/wage/entities.h | 345 | ||||
-rw-r--r-- | engines/wage/gui-console.cpp | 398 | ||||
-rw-r--r-- | engines/wage/gui.cpp | 594 | ||||
-rw-r--r-- | engines/wage/gui.h | 178 | ||||
-rw-r--r-- | engines/wage/menu.cpp | 563 | ||||
-rw-r--r-- | engines/wage/menu.h | 138 | ||||
-rw-r--r-- | engines/wage/module.mk | 27 | ||||
-rw-r--r-- | engines/wage/randomhat.cpp | 83 | ||||
-rw-r--r-- | engines/wage/randomhat.h | 77 | ||||
-rw-r--r-- | engines/wage/script.cpp | 1146 | ||||
-rw-r--r-- | engines/wage/script.h | 153 | ||||
-rw-r--r-- | engines/wage/util.cpp | 123 | ||||
-rw-r--r-- | engines/wage/wage.cpp | 504 | ||||
-rw-r--r-- | engines/wage/wage.h | 234 | ||||
-rw-r--r-- | engines/wage/world.cpp | 510 | ||||
-rw-r--r-- | engines/wage/world.h | 141 | ||||
-rw-r--r-- | graphics/primitives.cpp | 349 | ||||
-rw-r--r-- | graphics/primitives.h | 11 |
27 files changed, 8343 insertions, 0 deletions
diff --git a/devtools/create_wage/create_wage.sh b/devtools/create_wage/create_wage.sh new file mode 100755 index 0000000000..5e8fe352a2 --- /dev/null +++ b/devtools/create_wage/create_wage.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# +# This script downloads System 7.0.1 image from Apple and extracts fonts +# from it. Mac only, unfortunately. +# +# On Windows you perhaps can perform the extraction manually with use of +# HFV Explorer: https://web.archive.org/web/20011202005455/http://gamma.nic.fi/~lpesonen/HFVExplorer/ +# +# More information could be found in the vMac documentation: http://www.gryphel.com/c/image/ +# +# Alternatively you may use vMac instructions for extracting these disk images: +# http://www.gryphel.com/c/minivmac/recipes/sys7inst/ +# +# Based on instructions posted at +# http://apple.stackexchange.com/questions/58243/can-i-get-the-original-mac-font-chicago-on-a-mountain-lion-mac + +echo_n() { + printf "$@" +} + +if test `uname` != "Darwin"; then + echo This script is Mac OS X-only + exit +fi + +echo_n "Downloading System 7.0.1 image..." +if test ! -f System_7.0.1.smi.bin; then + curl -s http://download.info.apple.com/Apple_Support_Area/Apple_Software_Updates/English-North_American/Macintosh/System/Older_System/System_7.0.x/System_7.0.1.smi.bin -o System_7.0.1.smi.bin +fi + +if test ! -f System_7.0.1.smi.bin; then + echo "Cannot download System_7.0.1.smi.bin" + exit +fi + +echo done + +echo_n "Mounting System 7.0.1 image..." + +macbinary decode System_7.0.1.smi.bin +hdiutil convert -quiet System\ 7.0.1.smi -format UDRO -o sys7.dmg +hdiutil attach -quiet sys7.dmg + +if test ! -f /Volumes/7.0.1\ \(1440k.images\)/Fonts.image; then + echo "Failed to attach sys7.dmg" + exit +fi + +echo done + +echo_n "Mounting Fonts disk image..." + +hdiutil convert -quiet /Volumes/7.0.1\ \(1440k.images\)/Fonts.image -format UDRO -o fonts.dmg +hdiutil detach -quiet `hdiutil info|grep "/Volumes/7.0.1 (1440k.images)"|cut -f 1` +hdiutil attach -quiet fonts.dmg + +if test ! -f /Volumes/Fonts/Chicago; then + echo "Failed to attach fonts.dmg" + exit +fi + +echo done + +echo_n "Copying fonts..." + +for i in Athens Cairo Chicago Courier Geneva Helvetica London "Los Angeles" Monaco "New York" Palatino "San Francisco" Symbol Times Venice +do + echo $i + macbinary encode "/Volumes/Fonts/$i" -o "$i.bin" -n +done + +echo ...Done + +hdiutil detach -quiet `hdiutil info|grep "/Volumes/Fonts"|cut -f 1` + +if test ! -f fondu_src-060102.tgz; then + echo_n "Getting fondu_src-060102.tgz..." + curl -s http://fondu.sourceforge.net/fondu_src-060102.tgz -o fondu_src-060102.tgz + tar xf fondu_src-060102.tgz +fi + +if test ! -d fondu-060102; then + echo "Failed to download fondu_src-060102.tgz" + exit +fi + +echo done + +if test ! -x fondu-060102/fondu; then + echo_n "Compiling fondu..." + cd fondu-060102 + ./configure >configure.log 2>&1 && make 2>&1 >make.log + cd .. +fi + +if test ! -x fondu-060102/fondu; then + echo "Failed to build fondu. See configure.log and make.log" + exit +else + rm -f configure.log make.log +fi + +echo done + +echo_n "Converting fonts..." +fondu-060102/fondu -force *.bin +echo done + +zip -9 wage *.bdf +mv wage.zip wage.dat + +echo_n "Cleaning up..." +rm *.bdf +rm *.ttf +rm *.bin +rm *.dmg +echo done + +ls -l wage.dat diff --git a/engines/wage/combat.cpp b/engines/wage/combat.cpp new file mode 100644 index 0000000000..aa8ec535d2 --- /dev/null +++ b/engines/wage/combat.cpp @@ -0,0 +1,914 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/randomhat.h" +#include "wage/world.h" + +namespace Wage { + +Obj *WageEngine::getOffer() { + if (_offer != NULL) { + Chr *owner = _offer->_currentOwner; + if (owner == NULL || owner->_playerCharacter || owner->_currentScene != _world->_player->_currentScene) { + _offer = NULL; + } + } + return _offer; +} + +Chr *WageEngine::getMonster() { + if (_monster != NULL && _monster->_currentScene != _world->_player->_currentScene) { + _monster = NULL; + } + return _monster; +} + +void WageEngine::encounter(Chr *player, Chr *chr) { + char buf[512]; + + snprintf(buf, 512, "You encounter %s%s.", chr->_nameProperNoun ? "" : getIndefiniteArticle(chr->_name), + chr->_name.c_str()); + appendText(buf); + + if (!chr->_initialComment.empty()) + appendText(chr->_initialComment.c_str()); + + if (chr->_armor[Chr::HEAD_ARMOR] != NULL) { + snprintf(buf, 512, "%s%s is wearing %s.", chr->getDefiniteArticle(true), chr->_name.c_str(), + getIndefiniteArticle(chr->_armor[Chr::HEAD_ARMOR]->_name)); + appendText(buf); + } + if (chr->_armor[Chr::BODY_ARMOR] != NULL) { + snprintf(buf, 512, "%s is protected by %s%s.", getGenderSpecificPronoun(chr->_gender, true), + prependGenderSpecificPronoun(chr->_gender), chr->_armor[Chr::BODY_ARMOR]->_name.c_str()); + appendText(buf); + } + if (chr->_armor[Chr::SHIELD_ARMOR] != NULL) { + Obj *obj = chr->_armor[Chr::SHIELD_ARMOR]; + + snprintf(buf, 512, "%s carries %s%s.", getGenderSpecificPronoun(chr->_gender, true), + obj->_namePlural ? "" : getIndefiniteArticle(obj->_name), obj->_name.c_str()); + appendText(buf); + } +} + +void WageEngine::performCombatAction(Chr *npc, Chr *player) { + if (npc->_context._frozen) + return; + + RandomHat hat(_rnd); + + bool winning = (npc->_context._statVariables[PHYS_HIT_CUR] > player->_context._statVariables[PHYS_HIT_CUR]); + int validMoves = getValidMoveDirections(npc); + ObjArray *weapons = npc->getWeapons(false); + ObjArray *magics = npc->getMagicalObjects(); + // TODO: Figure out under what circumstances we need to add +1 + // for the chance (e.g. only when all values were set to 0?). + if (winning) { + if (!_world->_weaponMenuDisabled) { + if (!weapons->empty()) + hat.addTokens(kTokWeapons, npc->_winningWeapons + 1); + if (!magics->empty()) + hat.addTokens(kTokMagic, npc->_winningMagic); + } + if (validMoves != 0) + hat.addTokens(kTokRun, npc->_winningRun + 1); + if (!npc->_inventory.empty()) + hat.addTokens(kTokOffer, npc->_winningOffer + 1); + } else { + if (!_world->_weaponMenuDisabled) { + if (!weapons->empty()) + hat.addTokens(kTokWeapons, npc->_losingWeapons + 1); + if (!magics->empty()) + hat.addTokens(kTokMagic, npc->_losingMagic); + } + if (validMoves != 0) + hat.addTokens(kTokRun, npc->_losingRun + 1); + if (!npc->_inventory.empty()) + hat.addTokens(kTokOffer, npc->_losingOffer + 1); + } + + ObjList *objs = &npc->_currentScene->_objs; + if (npc->_inventory.size() < npc->_maximumCarriedObjects) { + int cnt = 0; + for (ObjList::const_iterator it = objs->begin(); it != objs->end(); ++it, ++cnt) { + if ((*it)->_type != Obj::IMMOBILE_OBJECT) { + // TODO: I'm not sure what the chance should be here. + hat.addTokens(cnt, 123); + } + } + } + + int token = hat.drawToken(); + switch (token) { + case kTokWeapons: + // TODO: I think the monster should choose the "best" weapon. + performAttack(npc, player, weapons->operator[](_rnd->getRandomNumber(weapons->size() - 1))); + break; + case kTokMagic: + // TODO: I think the monster should choose the "best" magic. + performMagic(npc, player, magics->operator[](_rnd->getRandomNumber(magics->size() - 1))); + break; + case kTokRun: + performMove(npc, validMoves); + break; + case kTokOffer: + performOffer(npc, player); + break; + case kTokNone: + break; + default: + { + int cnt = 0; + for (ObjList::const_iterator it = objs->begin(); it != objs->end(); ++it, ++cnt) + if (cnt == token) + performTake(npc, *it); + break; + } + } + + delete weapons; + delete magics; +} + +static const char *const targets[] = { "head", "chest", "side" }; + +void WageEngine::performAttack(Chr *attacker, Chr *victim, Obj *weapon) { + if (_world->_weaponMenuDisabled) + return; + + // TODO: verify that a player not aiming will always target the chest?? + int targetIndex = -1; + char buf[256]; + + if (weapon->_type != Obj::MAGICAL_OBJECT) { + if (attacker->_playerCharacter) { + targetIndex = _aim; + } else { + targetIndex = _rnd->getRandomNumber(ARRAYSIZE(targets) - 1); + _opponentAim = targetIndex + 1; + } + + if (!attacker->_playerCharacter) { + snprintf(buf, 256, "%s%s %ss %s%s at %s%s's %s.", + attacker->getDefiniteArticle(true), attacker->_name.c_str(), + weapon->_operativeVerb.c_str(), + prependGenderSpecificPronoun(attacker->_gender), weapon->_name.c_str(), + victim->getDefiniteArticle(true), victim->_name.c_str(), + targets[targetIndex]); + appendText(buf); + } + } else if (!attacker->_playerCharacter) { + snprintf(buf, 256, "%s%s %ss %s%s at %s%s.", + attacker->getDefiniteArticle(true), attacker->_name.c_str(), + weapon->_operativeVerb.c_str(), + prependGenderSpecificPronoun(attacker->_gender), weapon->_name.c_str(), + victim->getDefiniteArticle(true), victim->_name.c_str()); + appendText(buf); + } + + playSound(weapon->_sound); + + bool usesDecremented = false; + int chance = _rnd->getRandomNumber(255); + // TODO: what about obj accuracy + if (chance < attacker->_physicalAccuracy) { + usesDecremented = attackHit(attacker, victim, weapon, targetIndex); + } else if (weapon->_type != Obj::MAGICAL_OBJECT) { + appendText("A miss!"); + } else if (attacker->_playerCharacter) { + appendText("The spell has no effect."); + } + + if (!usesDecremented) { + decrementUses(weapon); + } +} + +void WageEngine::decrementUses(Obj *obj) { + int numberOfUses = obj->_numberOfUses; + if (numberOfUses != -1) { + numberOfUses--; + if (numberOfUses > 0) { + obj->_numberOfUses = numberOfUses; + } else { + if (!obj->_failureMessage.empty()) { + appendText(obj->_failureMessage.c_str()); + } + if (obj->_returnToRandomScene) { + _world->move(obj, _world->getRandomScene()); + } else { + _world->move(obj, _world->_storageScene); + } + obj->resetState(obj->_currentOwner, obj->_currentScene); + } + } +} + +bool WageEngine::attackHit(Chr *attacker, Chr *victim, Obj *weapon, int targetIndex) { + bool receivedHitTextPrinted = false; + char buf[512]; + + if (targetIndex != -1) { + Obj *armor = victim->_armor[targetIndex]; + if (armor != NULL) { + // TODO: Absorb some damage. + snprintf(buf, 512, "%s%s's %s weakens the impact of %s%s's %s.", + victim->getDefiniteArticle(true), victim->_name.c_str(), + victim->_armor[targetIndex]->_name.c_str(), + attacker->getDefiniteArticle(false), attacker->_name.c_str(), + weapon->_name.c_str()); + appendText(buf); + decrementUses(armor); + } else { + snprintf(buf, 512, "A hit to the %s!", targets[targetIndex]); + appendText(buf); + } + playSound(attacker->_scoresHitSound); + appendText(attacker->_scoresHitComment.c_str()); + playSound(victim->_receivesHitSound); + appendText(victim->_receivesHitComment.c_str()); + receivedHitTextPrinted = true; + } else if (weapon->_type == Obj::MAGICAL_OBJECT) { + appendText(weapon->_useMessage.c_str()); + appendText("The spell is effective!"); + } + + bool causesPhysicalDamage = true; + bool causesSpiritualDamage = false; + bool freezesOpponent = false; + bool usesDecremented = false; + + if (weapon->_type == Obj::THROW_WEAPON) { + _world->move(weapon, victim->_currentScene); + } else if (weapon->_type == Obj::MAGICAL_OBJECT) { + int type = weapon->_attackType; + causesPhysicalDamage = (type == Obj::CAUSES_PHYSICAL_DAMAGE || type == Obj::CAUSES_PHYSICAL_AND_SPIRITUAL_DAMAGE); + causesSpiritualDamage = (type == Obj::CAUSES_SPIRITUAL_DAMAGE || type == Obj::CAUSES_PHYSICAL_AND_SPIRITUAL_DAMAGE); + freezesOpponent = (type == Obj::FREEZES_OPPONENT); + } + + if (causesPhysicalDamage) { + victim->_context._userVariables[PHYS_HIT_CUR] -= weapon->_damage; + + /* Do it here to get the right order of messages in case of death. */ + decrementUses(weapon); + usesDecremented = true; + + if (victim->_context._userVariables[PHYS_HIT_CUR] < 0) { + playSound(victim->_dyingSound); + appendText(victim->_dyingWords.c_str()); + snprintf(buf, 512, "%s%s is dead!", victim->getDefiniteArticle(true), victim->_name.c_str()); + appendText(buf); + + attacker->_context._kills++; + attacker->_context._experience += victim->_context._userVariables[SPIR_HIT_CUR] + victim->_context._userVariables[PHYS_HIT_CUR]; + + if (!victim->_playerCharacter && !victim->_inventory.empty()) { + Scene *currentScene = victim->_currentScene; + + for (int i = victim->_inventory.size() - 1; i >= 0; i--) { + _world->move(victim->_inventory[i], currentScene); + } + Common::String *s = getGroundItemsList(currentScene); + appendText(s->c_str()); + delete s; + } + _world->move(victim, _world->_storageScene); + } else if (attacker->_playerCharacter && !receivedHitTextPrinted) { + double physicalPercent = (double)victim->_context._userVariables[SPIR_HIT_CUR] / + victim->_context._userVariables[SPIR_HIT_BAS]; + snprintf(buf, 512, "%s%s's condition appears to be %s.", + victim->getDefiniteArticle(true), victim->_name.c_str(), + getPercentMessage(physicalPercent)); + appendText(buf); + } + } + + if (causesSpiritualDamage) { + /* TODO */ + warning("TODO: Spiritual damage"); + } + + if (freezesOpponent) { + victim->_context._frozen = true; + } + + return usesDecremented; +} + +void WageEngine::performMagic(Chr *attacker, Chr *victim, Obj *magicalObject) { + switch (magicalObject->_attackType) { + case Obj::HEALS_PHYSICAL_DAMAGE: + case Obj::HEALS_SPIRITUAL_DAMAGE: + case Obj::HEALS_PHYSICAL_AND_SPIRITUAL_DAMAGE: + performHealingMagic(attacker, magicalObject); + return; + } + + performAttack(attacker, victim, magicalObject); +} + +void WageEngine::performHealingMagic(Chr *chr, Obj *magicalObject) { + char buf[512]; + + if (!chr->_playerCharacter) { + snprintf(buf, 512, "%s%s %ss %s%s.", + chr->getDefiniteArticle(true), chr->_name.c_str(), + magicalObject->_operativeVerb.c_str(), + getIndefiniteArticle(magicalObject->_name), magicalObject->_name.c_str()); + appendText(buf); + } + + uint chance = _rnd->getRandomNumber(255); + if (chance < magicalObject->_accuracy) { + int type = magicalObject->_attackType; + + if (type == Obj::HEALS_PHYSICAL_DAMAGE || type == Obj::HEALS_PHYSICAL_AND_SPIRITUAL_DAMAGE) + chr->_context._statVariables[PHYS_HIT_CUR] += magicalObject->_damage; + + if (type == Obj::HEALS_SPIRITUAL_DAMAGE || type == Obj::HEALS_PHYSICAL_AND_SPIRITUAL_DAMAGE) + chr->_context._statVariables[SPIR_HIT_CUR] += magicalObject->_damage; + + playSound(magicalObject->_sound); + appendText(magicalObject->_useMessage.c_str()); + + // TODO: what if enemy heals himself? + if (chr->_playerCharacter) { + double physicalPercent = (double)chr->_context._statVariables[PHYS_HIT_CUR] / chr->_context._statVariables[PHYS_HIT_BAS]; + double spiritualPercent = (double)chr->_context._statVariables[SPIR_HIT_CUR] / chr->_context._statVariables[SPIR_HIT_BAS]; + snprintf(buf, 256, "Your physical condition is %s.", getPercentMessage(physicalPercent)); + appendText(buf); + + snprintf(buf, 256, "Your spiritual condition is %s.", getPercentMessage(spiritualPercent)); + appendText(buf); + } + } + + decrementUses(magicalObject); +} + +static const int directionsX[] = { 0, 0, 1, -1 }; +static const int directionsY[] = { -1, 1, 0, 0 }; +static const char *const directionsS[] = { "north", "south", "east", "west" }; + +void WageEngine::performMove(Chr *chr, int validMoves) { + // count how many valid moves we have + int numValidMoves = 0; + + for (int dir = 0; dir < 4; dir++) + if ((validMoves & (1 << dir)) != 0) + numValidMoves++; + + // Now pick random dir + int dir = _rnd->getRandomNumber(numValidMoves); + + // And get it + for (int i = 0; i < 4; i++, dir--) + if ((validMoves & (1 << i)) != 0) { + if (dir == 1) { + dir = i; + break; + } + } + + char buf[256]; + snprintf(buf, 256, "%s%s runs %s.", chr->getDefiniteArticle(true), chr->_name.c_str(), directionsS[dir]); + appendText(buf); + + _running = chr; + Scene *currentScene = chr->_currentScene; + int destX = currentScene->_worldX + directionsX[dir]; + int destY = currentScene->_worldY + directionsY[dir]; + + _world->move(chr, _world->getSceneAt(destX, destY)); +} + +void WageEngine::performOffer(Chr *attacker, Chr *victim) { + /* TODO: choose in a smarter way? */ + Obj *obj = attacker->_inventory[0]; + char buf[512]; + + snprintf(buf, 512, "%s%s offers %s%s.", attacker->getDefiniteArticle(true), attacker->_name.c_str(), + obj->_namePlural ? "some " : getIndefiniteArticle(obj->_name), obj->_name.c_str()); + + appendText(buf); + + _offer = obj; +} + +void WageEngine::performTake(Chr *npc, Obj *obj) { + char buf[512]; + + snprintf(buf, 512, "%s%s picks up the %s%s.", npc->getDefiniteArticle(true), npc->_name.c_str(), + getIndefiniteArticle(obj->_name), obj->_name.c_str()); + + appendText(buf); + + _world->move(obj, npc); +} + +int WageEngine::getValidMoveDirections(Chr *npc) { + int directions = 0; + Scene *currentScene = npc->_currentScene; + for (int dir = 0; dir < 4; dir++) { + if (!currentScene->_blocked[dir]) { + int destX = currentScene->_worldX + directionsX[dir]; + int destY = currentScene->_worldY + directionsY[dir]; + + Scene *scene = _world->getSceneAt(destX, destY); + + if (scene != NULL && scene->_chrs.empty()) { + directions |= (1 << dir); + } + } + } + + return directions; +} + +void WageEngine::regen() { + Chr *player = _world->_player; + int curHp = player->_context._statVariables[PHYS_HIT_CUR]; + int maxHp = player->_context._statVariables[PHYS_HIT_BAS]; + int delta = maxHp - curHp; + + if (delta > 0) { + int bonus = (int)(delta / (8 + _rnd->getRandomNumber(2))); + player->_context._statVariables[PHYS_HIT_CUR] += bonus; + } +} + +void WageEngine::takeObj(Obj *obj) { + if (_world->_player->_inventory.size() >= _world->_player->_maximumCarriedObjects) { + appendText("Your pack is full, you must drop something."); + } else { + char buf[256]; + + _world->move(obj, _world->_player); + int type = _world->_player->wearObjIfPossible(obj); + if (type == Chr::HEAD_ARMOR) { + snprintf(buf, 256, "You are now wearing the %s.", obj->_name.c_str()); + appendText(buf); + } else if (type == Chr::BODY_ARMOR) { + snprintf(buf, 256, "You are now wearing the %s.", obj->_name.c_str()); + appendText(buf); + } else if (type == Chr::SHIELD_ARMOR) { + snprintf(buf, 256, "You are now wearing the %s.", obj->_name.c_str()); + appendText(buf); + } else if (type == Chr::MAGIC_ARMOR) { + snprintf(buf, 256, "You are now wearing the %s.", obj->_name.c_str()); + appendText(buf); + } else { + snprintf(buf, 256, "You now have the %s.", obj->_name.c_str()); + appendText(buf); + } + appendText(obj->_clickMessage.c_str()); + } +} + +bool WageEngine::handleMoveCommand(Directions dir, const char *dirName) { + Scene *playerScene = _world->_player->_currentScene; + const char *msg = playerScene->_messages[dir].c_str(); + + if (!playerScene->_blocked[dir]) { + int destX = playerScene->_worldX + directionsX[dir]; + int destY = playerScene->_worldY + directionsY[dir]; + + Scene *scene = _world->getSceneAt(destX, destY); + + if (scene != NULL) { + if (strlen(msg) > 0) { + appendText(msg); + } + _world->move(_world->_player, scene); + return true; + } + } + if (strlen(msg) > 0) { + appendText(msg); + } else { + Common::String txt("You can't go "); + txt += dirName; + txt += "."; + appendText(txt.c_str()); + } + + return true; +} + +bool WageEngine::handleLookCommand() { + appendText(_world->_player->_currentScene->_text.c_str()); + + Common::String *items = getGroundItemsList(_world->_player->_currentScene); + if (items != NULL) { + appendText(items->c_str()); + + delete items; + } + + return true; +} + +Common::String *WageEngine::getGroundItemsList(Scene *scene) { + ObjArray objs; + + for (ObjList::const_iterator it = scene->_objs.begin(); it != scene->_objs.end(); ++it) + if ((*it)->_type != Obj::IMMOBILE_OBJECT) + objs.push_back(*it); + + if (!objs.empty()) { + Common::String *res = new Common::String("On the ground you see "); + appendObjNames(*res, objs); + return res; + } + return NULL; +} + +void WageEngine::appendObjNames(Common::String &str, const ObjArray &objs) { + for (uint i = 0; i < objs.size(); i++) { + Obj *obj = objs[i]; + + if (!obj->_namePlural) + str += getIndefiniteArticle(obj->_name); + else + str += "some "; + + str += obj->_name; + + if (i == objs.size() - 1) { + str += "."; + } else if (i == objs.size() - 2) { + if (objs.size() > 2) + str += ","; + str += " and "; + } else { + str += ", "; + } + } +} + +bool WageEngine::handleInventoryCommand() { + Chr *player = _world->_player; + ObjArray objs; + + for (ObjArray::const_iterator it = player->_inventory.begin(); it != player->_inventory.end(); ++it) + if (!player->isWearing(*it)) + objs.push_back(*it); + + if (objs.empty()) { + appendText("Your pack is empty."); + } else { + Common::String res("Your pack contains "); + appendObjNames(res, objs); + appendText(res.c_str()); + } + + return true; +} + +static const char *const armorMessages[] = { + "Head protection:", + "Chest protection:", + "Shield protection:", // TODO: check message + "Magical protection:" +}; + +bool WageEngine::handleStatusCommand() { + Chr *player = _world->_player; + char buf[512]; + + snprintf(buf, 512, "Character name: %s%s", player->getDefiniteArticle(false), player->_name.c_str()); + appendText(buf); + snprintf(buf, 512, "Experience: %d", player->_context._experience); + appendText(buf); + + int wealth = 0; + for (ObjArray::const_iterator it = player->_inventory.begin(); it != player->_inventory.end(); ++it) + wealth += (*it)->_value; + + snprintf(buf, 512, "Wealth: %d", wealth); + appendText(buf); + + for (int i = 0; i < Chr::NUMBER_OF_ARMOR_TYPES; i++) { + if (player->_armor[i] != NULL) { + snprintf(buf, 512, "%s %s", armorMessages[i], player->_armor[i]->_name.c_str()); + appendText(buf); + } + } + + for (ObjArray::const_iterator it = player->_inventory.begin(); it != player->_inventory.end(); ++it) { + int uses = (*it)->_numberOfUses; + + if (uses > 0) { + snprintf(buf, 512, "Your %s has %d uses left.", (*it)->_name.c_str(), uses); + appendText(buf); + } + } + + printPlayerCondition(player); + + _commandWasQuick = true; + + return true; +} + +bool WageEngine::handleRestCommand() { + if (getMonster() != NULL) { + appendText("This is no time to rest!"); + _commandWasQuick = true; + } else { + regen(); + printPlayerCondition(_world->_player); + } + + return true; +} + +bool WageEngine::handleAcceptCommand() { + Chr *chr = _offer->_currentOwner; + + char buf[512]; + snprintf(buf, 512, "%s%s lays the %s on the ground and departs peacefully.", + chr->getDefiniteArticle(true), chr->_name.c_str(), _offer->_name.c_str()); + appendText(buf); + + _world->move(_offer, chr->_currentScene); + _world->move(chr, _world->_storageScene); + + return true; +} + +bool WageEngine::handleTakeCommand(const char *target) { + Common::String t(target); + bool handled = false; + + for (ObjList::const_iterator it = _world->_player->_currentScene->_objs.begin(); it != _world->_player->_currentScene->_objs.end(); ++it) { + Common::String n((*it)->_name); + n.toLowercase(); + + if (t.contains(n)) { + if ((*it)->_type == Obj::IMMOBILE_OBJECT) { + appendText("You can't move it."); + } else { + takeObj(*it); + } + + handled = true; + break; + } + } + + return handled; +} + +bool WageEngine::handleDropCommand(const char *target) { + Common::String t(target); + bool handled = false; + + t.toLowercase(); + + for (ObjArray::const_iterator it = _world->_player->_inventory.begin(); it != _world->_player->_inventory.end(); ++it) { + Common::String n((*it)->_name); + n.toLowercase(); + + if (t.contains(n)) { + char buf[256]; + + snprintf(buf, 256, "You no longer have the %s.", (*it)->_name.c_str()); + appendText(buf); + _world->move(*it, _world->_player->_currentScene); + + handled = true; + break; + } + } + + return handled; +} + +bool WageEngine::handleAimCommand(const char *t) { + bool wasHandled = true; + Common::String target(t); + + target.toLowercase(); + + if (target.contains("head")) { + _aim = Chr::HEAD; + } else if (target.contains("chest")) { + _aim = Chr::CHEST; + } else if (target.contains("side")) { + _aim = Chr::SIDE; + } else { + wasHandled = false; + appendText("Please aim for the head, chest, or side."); + } + + _commandWasQuick = true; + + return wasHandled; +} + +bool WageEngine::handleWearCommand(const char *t) { + Chr *player = _world->_player; + char buf[512]; + Common::String target(t); + bool handled = false; + + target.toLowercase(); + + for (ObjArray::const_iterator it = _world->_player->_inventory.begin(); it != _world->_player->_inventory.end(); ++it) { + Common::String n((*it)->_name); + + if (target.contains(n)) { + if ((*it)->_type == Obj::HELMET) { + wearObj(*it, Chr::HEAD_ARMOR); + } else if ((*it)->_type == Obj::CHEST_ARMOR) { + wearObj(*it, Chr::BODY_ARMOR); + } else if ((*it)->_type == Obj::SHIELD) { + wearObj(*it, Chr::SHIELD_ARMOR); + } else if ((*it)->_type == Obj::SPIRITUAL_ARMOR) { + wearObj(*it, Chr::MAGIC_ARMOR); + } else { + appendText("You cannot wear that object."); + } + + handled = true; + break; + } + } + + for (ObjList::const_iterator it = player->_currentScene->_objs.begin(); it != player->_currentScene->_objs.end(); ++it) { + Common::String n((*it)->_name); + n.toLowercase(); + if (target.contains(n)) { + snprintf(buf, 512, "First you must get the %s.", (*it)->_name.c_str()); + appendText(buf); + + handled = true; + break; + } + } + + return handled; +} + +void WageEngine::wearObj(Obj *o, int pos) { + Chr *player = _world->_player; + char buf[512]; + + if (player->_armor[pos] == o) { + snprintf(buf, 512, "You are already wearing the %s.", o->_name.c_str()); + appendText(buf); + } else { + if (player->_armor[pos] != NULL) { + snprintf(buf, 512, "You are no longer wearing the %s.", player->_armor[pos]->_name.c_str()); + appendText(buf); + } + + player->_armor[pos] = o; + snprintf(buf, 512, "You are now wearing the %s.", o->_name.c_str()); + appendText(buf); + } +} + + +bool WageEngine::handleOfferCommand(const char *target) { + Chr *player = _world->_player; + Chr *enemy = getMonster(); + + if (enemy != NULL) { + Common::String t(target); + t.toLowercase(); + + for (ObjArray::const_iterator it = player->_inventory.begin(); it != player->_inventory.end(); ++it) { + Common::String n((*it)->_name); + n.toLowercase(); + + if (t.contains(n)) { + if ((*it)->_value < enemy->_rejectsOffers) { + appendText("Your offer is rejected."); + } else { + appendText("Your offer is accepted."); + appendText(enemy->_acceptsOfferComment.c_str()); + _world->move(*it, enemy); + _world->move(enemy, _world->_storageScene); + } + + return true; + } + } + } + + return false; +} + +bool WageEngine::tryAttack(const Obj *weapon, const Common::String &input) { + Common::String w(weapon->_name); + w.toLowercase(); + Common::String i(input); + i.toLowercase(); + Common::String v(weapon->_operativeVerb); + v.toLowercase(); + + return i.contains(w) && i.contains(v); +} + +bool WageEngine::handleAttack(Obj *weapon) { + Chr *player = _world->_player; + Chr *enemy = getMonster(); + + if (weapon->_type == Obj::MAGICAL_OBJECT) { + switch (weapon->_attackType) { + case Obj::HEALS_PHYSICAL_AND_SPIRITUAL_DAMAGE: + case Obj::HEALS_PHYSICAL_DAMAGE: + case Obj::HEALS_SPIRITUAL_DAMAGE: + performMagic(player, enemy, weapon); + return true; + } + } + if (enemy != NULL) + performAttack(player, enemy, weapon); + else if (weapon->_type == Obj::MAGICAL_OBJECT) + appendText("There is nobody to cast a spell at."); + else + appendText("There is no one to fight."); + + return true; +} + +const char *WageEngine::getPercentMessage(double percent) { + if (percent < 0.40) { + return "very bad"; + } else if (percent < 0.55) { + return "bad"; + } else if (percent < 0.70) { + return "average"; + } else if (percent < 0.85) { + return "good"; + } else if (percent <= 1.00) { + return "very good"; + } else { + return "enhanced"; + } +} + +void WageEngine::printPlayerCondition(Chr *player) { + double physicalPercent = (double)player->_context._statVariables[PHYS_HIT_CUR] / player->_context._statVariables[PHYS_HIT_BAS]; + double spiritualPercent = (double)player->_context._statVariables[SPIR_HIT_CUR] / player->_context._statVariables[SPIR_HIT_BAS]; + char buf[256]; + + snprintf(buf, 256, "Your physical condition is %s.", getPercentMessage(physicalPercent)); + appendText(buf); + + snprintf(buf, 256, "Your spiritual condition is %s.", getPercentMessage(spiritualPercent)); + appendText(buf); +} + +} // End of namespace Wage diff --git a/engines/wage/configure.engine b/engines/wage/configure.engine new file mode 100644 index 0000000000..6205211158 --- /dev/null +++ b/engines/wage/configure.engine @@ -0,0 +1,3 @@ +# This file is included from the main "configure" script +# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] +add_engine wage "WAGE" no diff --git a/engines/wage/design.cpp b/engines/wage/design.cpp new file mode 100644 index 0000000000..44891f1637 --- /dev/null +++ b/engines/wage/design.cpp @@ -0,0 +1,538 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "graphics/primitives.h" +#include "wage/wage.h" +#include "wage/design.h" + +namespace Wage { + +struct PlotData { + Graphics::Surface *surface; + Patterns *patterns; + uint fillType; + int thickness; + + PlotData(Graphics::Surface *s, Patterns *p, int f, int t) : + surface(s), patterns(p), fillType(f), thickness(t) {} +}; + +void drawPixel(int x, int y, int color, void *data); +void drawPixelPlain(int x, int y, int color, void *data); + +Design::Design(Common::SeekableReadStream *data) { + _len = data->readUint16BE() - 2; + _data = (byte *)malloc(_len); + data->read(_data, _len); + + _surface = NULL; +} + +Design::~Design() { + free(_data); + if (_surface) + _surface->free(); + delete _surface; +} + +void Design::paint(Graphics::Surface *surface, Patterns &patterns, int x, int y) { + Common::MemoryReadStream in(_data, _len); + Common::Rect r(0, 0, _bounds->width(), _bounds->height()); + bool needRender = false; + + if (_surface == NULL) { + _surface = new Graphics::Surface; + _surface->create(_bounds->width(), _bounds->height(), Graphics::PixelFormat::createFormatCLUT8()); + _surface->fillRect(r, kColorGreen); + + needRender = true; + } + +#if 0 + PlotData pd(_surface, &patterns, 8, 1); + int x1 = 50, y1 = 50, x2 = 200, y2 = 200, borderThickness = 30; + Common::Rect inn(x1-5, y1-5, x2+5, y2+5); + drawRoundRect(inn, 6, kColorGray, false, drawPixelPlain, &pd); + + drawThickLine(x1, y1, x2-borderThickness, y1, borderThickness, kColorBlack, drawPixel, &pd); + drawThickLine(x2-borderThickness, y1, x2-borderThickness, y2, borderThickness, kColorBlack, drawPixel, &pd); + drawThickLine(x2-borderThickness, y2-borderThickness, x1, y2-borderThickness, borderThickness, kColorBlack, drawPixel, &pd); + drawThickLine(x1, y2-borderThickness, x1, y1, borderThickness, kColorBlack, drawPixel, &pd); + drawThickLine(x2+10, y2+10, x2+100, y2+100, borderThickness, kColorBlack, drawPixel, &pd); + + g_system->copyRectToScreen(_surface->getPixels(), _surface->pitch, 0, 0, _surface->w, _surface->h); + + while (true) { + ((WageEngine *)g_engine)->processEvents(); + g_system->updateScreen(); + g_system->delayMillis(50); + } + return; +#endif + + while (needRender) { + byte fillType = in.readByte(); + byte borderThickness = in.readByte(); + byte borderFillType = in.readByte(); + int type = in.readByte(); + + if (in.eos()) + break; + + debug(8, "fill: %d borderFill: %d border: %d type: %d", fillType, borderFillType, borderThickness, type); + switch (type) { + case 4: + drawRect(_surface, in, patterns, fillType, borderThickness, borderFillType); + break; + case 8: + drawRoundRect(_surface, in, patterns, fillType, borderThickness, borderFillType); + break; + case 12: + drawOval(_surface, in, patterns, fillType, borderThickness, borderFillType); + break; + case 16: + case 20: + drawPolygon(_surface, in, patterns, fillType, borderThickness, borderFillType); + break; + case 24: + drawBitmap(_surface, in); + break; + default: + warning("Unknown type => %d", type); + break; + } + + //g_system->copyRectToScreen(_surface->getPixels(), _surface->pitch, 0, 0, _surface->w, _surface->h); + //((WageEngine *)g_engine)->processEvents(); + //g_system->updateScreen(); + //g_system->delayMillis(500); + } + + const int padding = 3; + for (int i = padding; i < _bounds->height() - 2 * padding; i++) { + const byte *src = (const byte *)_surface->getBasePtr(padding, i); + byte *dst = (byte *)surface->getBasePtr(x + padding, y+i); + for (int j = padding; j < _bounds->width() - 2 * padding; j++) { + if (*src != kColorGreen) + *dst = *src; + src++; + dst++; + } + } +} + +bool Design::isPointOpaque(int x, int y) { + if (_surface == NULL) + error("Surface is null"); + + byte pixel = ((byte *)_surface->getBasePtr(x, y))[0]; + + return pixel != kColorGreen; +} + +void drawPixel(int x, int y, int color, void *data) { + PlotData *p = (PlotData *)data; + + if (p->fillType > p->patterns->size()) + return; + + byte *pat = p->patterns->operator[](p->fillType - 1); + + if (p->thickness == 1) { + if (x >= 0 && x < p->surface->w && y >= 0 && y < p->surface->h) { + uint xu = (uint)x; // for letting compiler optimize it + uint yu = (uint)y; + *((byte *)p->surface->getBasePtr(xu, yu)) = + (pat[yu % 8] & (1 << (7 - xu % 8))) ? + color : kColorWhite; + } + } else { + int x1 = x - p->thickness / 2; + int x2 = x1 + p->thickness; + int y1 = y - p->thickness / 2; + int y2 = y1 + p->thickness; + + for (y = y1; y < y2; y++) + for (x = x1; x < x2; x++) + if (x >= 0 && x < p->surface->w && y >= 0 && y < p->surface->h) { + uint xu = (uint)x; // for letting compiler optimize it + uint yu = (uint)y; + *((byte *)p->surface->getBasePtr(xu, yu)) = + (pat[yu % 8] & (1 << (7 - xu % 8))) ? + color : kColorWhite; + } + } +} + +void drawPixelPlain(int x, int y, int color, void *data) { + PlotData *p = (PlotData *)data; + + if (x >= 0 && x < p->surface->w && y >= 0 && y < p->surface->h) + *((byte *)p->surface->getBasePtr(x, y)) = (byte)color; +} + +void Design::drawRect(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType) { + int16 y1 = in.readSint16BE(); + int16 x1 = in.readSint16BE(); + int16 y2 = in.readSint16BE(); + int16 x2 = in.readSint16BE(); + + if (x1 > x2) + SWAP(x1, x2); + if (y1 > y2) + SWAP(y1, y2); + + Common::Rect r(x1, y1, x2, y2); + PlotData pd(surface, &patterns, fillType, 1); + + if (fillType <= patterns.size()) + Graphics::drawFilledRect(r, kColorBlack, drawPixel, &pd); + + pd.fillType = borderFillType; + pd.thickness = borderThickness; + + if (borderThickness > 0 && borderFillType <= patterns.size()) { + Graphics::drawLine(x1, y1, x2, y1, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x2, y1, x2, y2, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x2, y2, x1, y2, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x1, y2, x1, y1, kColorBlack, drawPixel, &pd); + } +} + +void Design::drawRoundRect(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType) { + int16 y1 = in.readSint16BE(); + int16 x1 = in.readSint16BE(); + int16 y2 = in.readSint16BE(); + int16 x2 = in.readSint16BE(); + int16 arc = in.readSint16BE(); + + if (x1 > x2) + SWAP(x1, x2); + if (y1 > y2) + SWAP(y1, y2); + + Common::Rect r(x1, y1, x2, y2); + PlotData pd(surface, &patterns, fillType, 1); + + if (fillType <= patterns.size()) + Graphics::drawRoundRect(r, arc/2, kColorBlack, true, drawPixel, &pd); + + pd.fillType = borderFillType; + pd.thickness = borderThickness; + + if (borderThickness > 0 && borderFillType <= patterns.size()) + Graphics::drawRoundRect(r, arc/2, kColorBlack, false, drawPixel, &pd); +} + +void Design::drawPolygon(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType) { + + byte ignored = in.readSint16BE(); // ignored + assert(ignored == 0); + + int numBytes = in.readSint16BE(); // #bytes used by polygon data, including the numBytes + int16 by1 = in.readSint16BE(); + int16 bx1 = in.readSint16BE(); + int16 by2 = in.readSint16BE(); + int16 bx2 = in.readSint16BE(); + Common::Rect bbox(bx1, by1, bx2, by2); + + numBytes -= 8; + + int y1 = in.readSint16BE(); + int x1 = in.readSint16BE(); + + Common::Array<int> xcoords; + Common::Array<int> ycoords; + + numBytes -= 6; + + while (numBytes > 0) { + int y2 = y1; + int x2 = x1; + int b = in.readSByte(); + if (b == -128) { + y2 = in.readSint16BE(); + numBytes -= 3; + } else { + y2 += b; + numBytes -= 1; + } + b = in.readSByte(); + if (b == -128) { + x2 = in.readSint16BE(); + numBytes -= 3; + } else { + x2 += b; + numBytes -= 1; + } + xcoords.push_back(x1); + ycoords.push_back(y1); + x1 = x2; + y1 = y2; + } + xcoords.push_back(x1); + ycoords.push_back(y1); + + int npoints = xcoords.size(); + int *xpoints = (int *)calloc(npoints, sizeof(int)); + int *ypoints = (int *)calloc(npoints, sizeof(int)); + for (int i = 0; i < npoints; i++) { + xpoints[i] = xcoords[i]; + ypoints[i] = ycoords[i]; + } + + PlotData pd(surface, &patterns, fillType, 1); + + if (fillType <= patterns.size()) { + Graphics::drawPolygonScan(xpoints, ypoints, npoints, bbox, kColorBlack, drawPixel, &pd); + } + + pd.fillType = borderFillType; + pd.thickness = borderThickness; + if (borderThickness > 0 && borderFillType <= patterns.size()) { + for (int i = 1; i < npoints; i++) + Graphics::drawLine(xpoints[i-1], ypoints[i-1], xpoints[i], ypoints[i], kColorBlack, drawPixel, &pd); + } + + free(xpoints); + free(ypoints); +} + +void Design::drawOval(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType) { + int16 y1 = in.readSint16BE(); + int16 x1 = in.readSint16BE(); + int16 y2 = in.readSint16BE(); + int16 x2 = in.readSint16BE(); + PlotData pd(surface, &patterns, fillType, 1); + + if (fillType <= patterns.size()) + Graphics::drawEllipse(x1, y1, x2-1, y2-1, kColorBlack, true, drawPixel, &pd); + + pd.fillType = borderFillType; + pd.thickness = borderThickness; + + if (borderThickness > 0 && borderFillType <= patterns.size()) + Graphics::drawEllipse(x1, y1, x2-1, y2-1, kColorBlack, false, drawPixel, &pd); +} + +void Design::drawBitmap(Graphics::Surface *surface, Common::SeekableReadStream &in) { + int numBytes = in.readSint16BE(); + int y1 = in.readSint16BE(); + int x1 = in.readSint16BE(); + int y2 = in.readSint16BE(); + int x2 = in.readSint16BE(); + int w = x2 - x1; + int h = y2 - y1; + Graphics::Surface tmp; + + tmp.create(w, h, Graphics::PixelFormat::createFormatCLUT8()); + + numBytes -= 10; + + int x = 0, y = 0; + while (numBytes > 0 && y < h) { + int n = in.readSByte(); + int count; + int b; + int state = 0; + + numBytes--; + + if ((n >= 0) && (n <= 127)) { // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. + count = n + 1; + state = 1; + } else if ((n >= -127) && (n <= -1)) { // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. + b = in.readByte(); + numBytes--; + count = -n + 1; + state = 2; + } else { // Else if n is -128, noop. + count = 0; + } + + for (int i = 0; i < count && y < h; i++) { + byte color; + if (state == 1) { + color = in.readByte(); + numBytes--; + } else if (state == 2) + color = b; + + for (int c = 0; c < 8; c++) { + if (x1 + x >= 0 && x1 + x < surface->w && y1 + y >= 0 && y1 + y < surface->h) + *((byte *)tmp.getBasePtr(x, y)) = (color & (1 << (7 - c % 8))) ? kColorBlack : kColorWhite; + x++; + if (x == w) { + y++; + x = 0; + break; + } + } + } + } + + in.skip(numBytes); + + FloodFill ff(&tmp, kColorWhite, kColorGreen); + for (int yy = 0; yy < h; yy++) { + ff.addSeed(0, yy); + ff.addSeed(w - 1, yy); + } + for (int xx = 0; xx < w; xx++) { + ff.addSeed(xx, 0); + ff.addSeed(xx, h - 1); + } + ff.fill(); + + for (y = 0; y < h; y++) { + byte *src = (byte *)tmp.getBasePtr(0, y); + byte *dst = (byte *)surface->getBasePtr(x1, y1 + y); + for (x = 0; x < w; x++) { + if (*src != kColorGreen) + *dst = *src; + src++; + dst++; + } + } + + tmp.free(); +} + +void Design::drawRect(Graphics::Surface *surface, Common::Rect &rect, int thickness, int color, Patterns &patterns, byte fillType) { + drawRect(surface, rect.left, rect.top, rect.right, rect.bottom, thickness, color, patterns, fillType); +} + +void Design::drawRect(Graphics::Surface *surface, int x1, int y1, int x2, int y2, int thickness, int color, Patterns &patterns, byte fillType) { + PlotData pd(surface, &patterns, fillType, thickness); + + Graphics::drawLine(x1, y1, x2, y1, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x2, y1, x2, y2, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x2, y2, x1, y2, kColorBlack, drawPixel, &pd); + Graphics::drawLine(x1, y2, x1, y1, kColorBlack, drawPixel, &pd); +} + + +void Design::drawFilledRect(Graphics::Surface *surface, Common::Rect &rect, int color, Patterns &patterns, byte fillType) { + PlotData pd(surface, &patterns, fillType, 1); + + for (int y = rect.top; y <= rect.bottom; y++) + Graphics::drawHLine(rect.left, rect.right, y, color, drawPixel, &pd); +} + +void Design::drawFilledRoundRect(Graphics::Surface *surface, Common::Rect &rect, int arc, int color, Patterns &patterns, byte fillType) { + PlotData pd(surface, &patterns, fillType, 1); + + Graphics::drawRoundRect(rect, arc, color, true, drawPixel, &pd); +} + +void Design::drawHLine(Graphics::Surface *surface, int x1, int x2, int y, int thickness, int color, Patterns &patterns, byte fillType) { + PlotData pd(surface, &patterns, fillType, thickness); + + Graphics::drawHLine(x1, x2, y, color, drawPixel, &pd); +} + +void Design::drawVLine(Graphics::Surface *surface, int x, int y1, int y2, int thickness, int color, Patterns &patterns, byte fillType) { + PlotData pd(surface, &patterns, fillType, thickness); + + Graphics::drawVLine(x, y1, y2, color, drawPixel, &pd); +} + +FloodFill::FloodFill(Graphics::Surface *surface, byte color1, byte color2) { + _surface = surface; + _color1 = color1; + _color2 = color2; + _w = surface->w; + _h = surface->h; + + _visited = (byte *)calloc(_w * _h, 1); +} + +FloodFill::~FloodFill() { + while(!_queue.empty()) { + Common::Point *p = _queue.front(); + + delete p; + _queue.pop_front(); + } + + free(_visited); +} + +void FloodFill::addSeed(int x, int y) { + byte *p; + + if (x >= 0 && x < _w && y >= 0 && y < _h) { + if (!_visited[y * _w + x] && *(p = (byte *)_surface->getBasePtr(x, y)) == _color1) { + _visited[y * _w + x] = 1; + *p = _color2; + + Common::Point *pt = new Common::Point(x, y); + + _queue.push_back(pt); + } + } +} + +void FloodFill::fill() { + while (!_queue.empty()) { + Common::Point *p = _queue.front(); + _queue.pop_front(); + addSeed(p->x , p->y - 1); + addSeed(p->x - 1, p->y ); + addSeed(p->x , p->y + 1); + addSeed(p->x + 1, p->y ); + + delete p; + } +} + + +} // End of namespace Wage diff --git a/engines/wage/design.h b/engines/wage/design.h new file mode 100644 index 0000000000..baa99730fe --- /dev/null +++ b/engines/wage/design.h @@ -0,0 +1,115 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_DESIGN_H +#define WAGE_DESIGN_H + +#include "graphics/surface.h" +#include "common/memstream.h" +#include "common/rect.h" + +namespace Wage { + +class Design { +public: + Design(Common::SeekableReadStream *data); + ~Design(); + + void setBounds(Common::Rect *bounds) { + _bounds = bounds; + } + + Common::Rect *getBounds() { + return _bounds; + } + + void paint(Graphics::Surface *canvas, Patterns &patterns, int x, int y); + bool isPointOpaque(int x, int y); + static void drawRect(Graphics::Surface *surface, Common::Rect &rect, int thickness, int color, Patterns &patterns, byte fillType); + static void drawRect(Graphics::Surface *surface, int x1, int y1, int x2, int y2, int thickness, int color, Patterns &patterns, byte fillType); + static void drawFilledRect(Graphics::Surface *surface, Common::Rect &rect, int color, Patterns &patterns, byte fillType); + static void drawFilledRoundRect(Graphics::Surface *surface, Common::Rect &rect, int arc, int color, Patterns &patterns, byte fillType); + static void drawHLine(Graphics::Surface *surface, int x1, int x2, int y, int thickness, int color, Patterns &patterns, byte fillType); + static void drawVLine(Graphics::Surface *surface, int x, int y1, int y2, int thickness, int color, Patterns &patterns, byte fillType); + + +private: + byte *_data; + int _len; + Common::Rect *_bounds; + Graphics::Surface *_surface; + +private: + void drawRect(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType); + void drawRoundRect(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType); + void drawPolygon(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType); + void drawOval(Graphics::Surface *surface, Common::ReadStream &in, + Patterns &patterns, byte fillType, byte borderThickness, byte borderFillType); + void drawBitmap(Graphics::Surface *surface, Common::SeekableReadStream &in); +}; + +class FloodFill { +public: + FloodFill(Graphics::Surface *surface, byte color1, byte color2); + ~FloodFill(); + void addSeed(int x, int y); + void fill(); + +private: + Common::List<Common::Point *> _queue; + Graphics::Surface *_surface; + byte _color1, _color2; + byte *_visited; + int _w, _h; +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/detection.cpp b/engines/wage/detection.cpp new file mode 100644 index 0000000000..e03c447484 --- /dev/null +++ b/engines/wage/detection.cpp @@ -0,0 +1,238 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + + +#include "base/plugins.h" + +#include "engines/advancedDetector.h" +#include "common/system.h" +#include "common/savefile.h" + +#include "wage/wage.h" + +namespace Wage { + +const char *WageEngine::getGameFile() const { + return _gameDescription->filesDescriptions[0].fileName; +} + +} + +static const PlainGameDescriptor wageGames[] = { + {"afm", "Another Fine Mess"}, + {"amot", "A Mess O' Trouble"}, + {"cantitoe", "Camp Cantitoe"}, + {"scepters", "Enchanted Scepters"}, + {"wage", "WAGE"}, + {0, 0} +}; + +namespace Wage { + +#define ADGF_DEFAULT (ADGF_DROPLANGUAGE|ADGF_DROPPLATFORM) +#define ADGF_GENERIC (ADGF_DROPLANGUAGE|ADGF_DROPPLATFORM|ADGF_USEEXTRAASTITLE) + +static const ADGameDescription gameDescriptions[] = { + { + "afm", + "v1.8", + AD_ENTRY1s("Another Fine Mess 1.8", "8e5aa915f3253efb2aab52435647b25e", 1456000), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_DEFAULT, + GUIO0() + }, + { + "amot", + "v1.8", + AD_ENTRY1s("A Mess O' Trouble 1.8", "b3ef53afed282671b704e45df829350c", 1895552), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_DEFAULT, + GUIO0() + }, + { + "cantitoe", + "", + AD_ENTRY1s("Camp Cantitoe", "098aa5c11c58e1ef274a30a9e01b4755", 621440), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_DEFAULT, + GUIO0() + }, + { + "wage", + "Deep Angst", + AD_ENTRY1s("Deep Angst", "635f62bbc569e72b03cab9107927d03d", 335232), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_GENERIC, + GUIO0() + }, + { + "wage", + "Escape from School!", + AD_ENTRY1s("Escape from School!", "a854be48d4af20126d18a9cad93a969b", 51840), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_GENERIC, + GUIO0() + }, + { + "wage", + "Queen Quest", + AD_ENTRY1s("Queen Quest", "730605d312efedb5e3ff108522fcac18", 59776), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_GENERIC, + GUIO0() + }, + { + "scepters", + "", + AD_ENTRY1s("Scepters", "b80bff315897776dda7689cdf829fab4", 360832), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_DEFAULT, + GUIO0() + }, + { + "wage", + "ZikTuria", + AD_ENTRY1s("ZikTuria", "e793155bed1a70fa2074a3fcd696b751", 54784), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_GENERIC, + GUIO0() + }, + { + "wage", + "Zoony", + AD_ENTRY1s("Zoony", "e6cc8a914a4215dafbcce6315dd12cf5", 160256), + Common::EN_ANY, + Common::kPlatformMacintosh, + ADGF_GENERIC, + GUIO0() + }, + + AD_TABLE_END_MARKER +}; + +} // End of namespace Wage + +class WageMetaEngine : public AdvancedMetaEngine { +public: + WageMetaEngine() : AdvancedMetaEngine(Wage::gameDescriptions, sizeof(ADGameDescription), wageGames) { + _singleid = "wage"; + _guioptions = GUIO2(GUIO_NOSPEECH, GUIO_NOMIDI); + } + + virtual const char *getName() const { + return "World Adventure Game Engine"; + } + + virtual const char *getOriginalCopyright() const { + return "World Builder (C) Silicon Beach Software"; + } + + virtual bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const; + virtual bool hasFeature(MetaEngineFeature f) const; + virtual SaveStateList listSaves(const char *target) const; + virtual int getMaximumSaveSlot() const; + virtual void removeSaveState(const char *target, int slot) const; +}; + +bool WageMetaEngine::hasFeature(MetaEngineFeature f) const { + return + (f == kSupportsListSaves) || + (f == kSupportsLoadingDuringStartup) || + (f == kSupportsDeleteSave); +} + +bool Wage::WageEngine::hasFeature(EngineFeature f) const { + return + (f == kSupportsRTL) || + (f == kSupportsLoadingDuringRuntime) || + (f == kSupportsSavingDuringRuntime); +} + +bool WageMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const { + if (desc) { + *engine = new Wage::WageEngine(syst, desc); + } + return desc != 0; +} + +SaveStateList WageMetaEngine::listSaves(const char *target) const { + const uint32 WAGEflag = MKTAG('W','A','G','E'); + Common::SaveFileManager *saveFileMan = g_system->getSavefileManager(); + Common::StringArray filenames; + char saveDesc[31]; + Common::String pattern = target; + pattern += ".???"; + + filenames = saveFileMan->listSavefiles(pattern); + sort(filenames.begin(), filenames.end()); // Sort (hopefully ensuring we are sorted numerically..) + + SaveStateList saveList; + for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) { + // Obtain the last 3 digits of the filename, since they correspond to the save slot + int slotNum = atoi(file->c_str() + file->size() - 3); + + if (slotNum >= 0 && slotNum <= 999) { + Common::InSaveFile *in = saveFileMan->openForLoading(*file); + if (in) { + uint32 type = in->readUint32BE(); + if (type == WAGEflag) + in->read(saveDesc, 31); + saveList.push_back(SaveStateDescriptor(slotNum, saveDesc)); + delete in; + } + } + } + + return saveList; +} + +int WageMetaEngine::getMaximumSaveSlot() const { return 999; } + +void WageMetaEngine::removeSaveState(const char *target, int slot) const { + g_system->getSavefileManager()->removeSavefile(Common::String::format("%s.%03d", target, slot)); +} + +#if PLUGIN_ENABLED_DYNAMIC(WAGE) + REGISTER_PLUGIN_DYNAMIC(WAGE, PLUGIN_TYPE_ENGINE, WageMetaEngine); +#else + REGISTER_PLUGIN_STATIC(WAGE, PLUGIN_TYPE_ENGINE, WageMetaEngine); +#endif + +namespace Wage { + +bool WageEngine::canLoadGameStateCurrently() { + return false; +} + +bool WageEngine::canSaveGameStateCurrently() { + return false; +} + +} // End of namespace Wage diff --git a/engines/wage/dialog.cpp b/engines/wage/dialog.cpp new file mode 100644 index 0000000000..263570bddc --- /dev/null +++ b/engines/wage/dialog.cpp @@ -0,0 +1,241 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/system.h" +#include "common/events.h" + +#include "wage/wage.h" +#include "wage/design.h" +#include "wage/gui.h" +#include "wage/dialog.h" + +namespace Wage { + +enum { + kDialogHeight = 113 +}; + +Dialog::Dialog(Gui *gui, int width, const char *text, DialogButtonArray *buttons, uint defaultButton) : + _gui(gui), _text(text), _buttons(buttons), _defaultButton(defaultButton) { + assert(_gui->_engine); + assert(_gui->_engine->_world); + + _font = getDialogFont(); + + _tempSurface.create(width + 1, kDialogHeight + 1, Graphics::PixelFormat::createFormatCLUT8()); + + _bbox.left = (_gui->_screen.w - width) / 2; + _bbox.top = (_gui->_screen.h - kDialogHeight) / 2; + _bbox.right = (_gui->_screen.w + width) / 2; + _bbox.bottom = (_gui->_screen.h + kDialogHeight) / 2; + + _pressedButton = -1; + + _mouseOverPressedButton = false; + + // Adjust button positions + for (uint i = 0; i < _buttons->size(); i++) + _buttons->operator[](i)->bounds.translate(_bbox.left, _bbox.top); + + _needsRedraw = true; +} + +Dialog::~Dialog() { +} + +const Graphics::Font *Dialog::getDialogFont() { + return _gui->getFont("Chicago-12", Graphics::FontManager::kBigGUIFont); +} + +void Dialog::paint() { + Design::drawFilledRect(&_gui->_screen, _bbox, kColorWhite, _gui->_patterns, kPatternSolid); + _font->drawString(&_gui->_screen, _text, _bbox.left + 24, _bbox.top + 16, _bbox.width(), kColorBlack); + + static int boxOutline[] = { 1, 0, 0, 1, 1 }; + drawOutline(_bbox, boxOutline, ARRAYSIZE(boxOutline)); + + for (uint i = 0; i < _buttons->size(); i++) { + DialogButton *button = _buttons->operator[](i); + static int buttonOutline[] = { 0, 0, 0, 0, 1 }; + + if (i == _defaultButton) { + buttonOutline[0] = buttonOutline[1] = 1; + } else { + buttonOutline[0] = buttonOutline[1] = 0; + } + + int color = kColorBlack; + + if ((int)i == _pressedButton && _mouseOverPressedButton) { + Common::Rect bb(button->bounds.left + 5, button->bounds.top + 5, + button->bounds.right - 5, button->bounds.bottom - 5); + + Design::drawFilledRect(&_gui->_screen, bb, kColorBlack, _gui->_patterns, kPatternSolid); + + color = kColorWhite; + } + int w = _font->getStringWidth(button->text); + int x = button->bounds.left + (button->bounds.width() - w) / 2; + int y = button->bounds.top + 6; + + _font->drawString(&_gui->_screen, button->text, x, y, _bbox.width(), color); + + drawOutline(button->bounds, buttonOutline, ARRAYSIZE(buttonOutline)); + } + + g_system->copyRectToScreen(_gui->_screen.getBasePtr(_bbox.left, _bbox.top), _gui->_screen.pitch, + _bbox.left, _bbox.top, _bbox.width() + 1, _bbox.height() + 1); + + _needsRedraw = false; +} + +void Dialog::drawOutline(Common::Rect &bounds, int *spec, int speclen) { + for (int i = 0; i < speclen; i++) + if (spec[i] != 0) + Design::drawRect(&_gui->_screen, bounds.left + i, bounds.top + i, bounds.right - i, bounds.bottom - i, + 1, kColorBlack, _gui->_patterns, kPatternSolid); +} + +int Dialog::run() { + bool shouldQuit = false; + Common::Rect r(_bbox); + + _tempSurface.copyRectToSurface(_gui->_screen.getBasePtr(_bbox.left, _bbox.top), _gui->_screen.pitch, 0, 0, _bbox.width() + 1, _bbox.height() + 1); + _gui->pushArrowCursor(); + + while (!shouldQuit) { + Common::Event event; + + while (_gui->_engine->_eventMan->pollEvent(event)) { + switch (event.type) { + case Common::EVENT_QUIT: + _gui->_engine->_shouldQuit = true; + shouldQuit = true; + break; + case Common::EVENT_MOUSEMOVE: + mouseMove(event.mouse.x, event.mouse.y); + break; + case Common::EVENT_LBUTTONDOWN: + mouseClick(event.mouse.x, event.mouse.y); + break; + case Common::EVENT_LBUTTONUP: + shouldQuit = mouseRaise(event.mouse.x, event.mouse.y); + break; + case Common::EVENT_KEYDOWN: + switch (event.kbd.keycode) { + case Common::KEYCODE_ESCAPE: + _pressedButton = -1; + shouldQuit = true; + default: + break; + } + break; + default: + break; + } + } + + if (_needsRedraw) + paint(); + + g_system->updateScreen(); + g_system->delayMillis(50); + } + + _gui->_screen.copyRectToSurface(_tempSurface.getBasePtr(0, 0), _tempSurface.pitch, _bbox.left, _bbox.top, _bbox.width() + 1, _bbox.height() + 1); + g_system->copyRectToScreen(_gui->_screen.getBasePtr(r.left, r.top), _gui->_screen.pitch, r.left, r.top, r.width() + 1, r.height() + 1); + + _gui->popCursor(); + + return _pressedButton; +} + +int Dialog::matchButton(int x, int y) { + for (uint i = 0; i < _buttons->size(); i++) + if (_buttons->operator[](i)->bounds.contains(x, y)) + return i; + + return -1; +} + +void Dialog::mouseMove(int x, int y) { + if (_pressedButton != -1) { + int match = matchButton(x, y); + + if (_mouseOverPressedButton && match != _pressedButton) { + _mouseOverPressedButton = false; + _needsRedraw = true; + } else if (!_mouseOverPressedButton && match == _pressedButton) { + _mouseOverPressedButton = true; + _needsRedraw = true; + } + } +} + +void Dialog::mouseClick(int x, int y) { + int match = matchButton(x, y); + + if (match != -1) { + _pressedButton = match; + _mouseOverPressedButton = true; + + _needsRedraw = true; + } +} + +int Dialog::mouseRaise(int x, int y) { + bool res = false; + + if (_pressedButton != -1) { + if (matchButton(x, y) == _pressedButton) + res = true; + } + + return res; +} + +} // End of namespace Wage diff --git a/engines/wage/dialog.h b/engines/wage/dialog.h new file mode 100644 index 0000000000..c5878acc95 --- /dev/null +++ b/engines/wage/dialog.h @@ -0,0 +1,101 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_DIALOG_H +#define WAGE_DIALOG_H + +namespace Wage { + +struct DialogButton { + Common::String text; + Common::Rect bounds; + + DialogButton(const char *t, int x1, int y1, int w, int h) { + text = t; + bounds.left = x1; + bounds.top = y1; + bounds.right = x1 + w - 1; + bounds.bottom = y1 + h - 1; + } +}; + +typedef Common::Array<DialogButton *> DialogButtonArray; + +class Dialog { +public: + Dialog(Gui *gui, int width, const char *text, DialogButtonArray *buttons, uint defaultButton); + ~Dialog(); + + int run(); + +private: + Gui *_gui; + Graphics::Surface _tempSurface; + Common::Rect _bbox; + Common::String _text; + + const Graphics::Font *_font; + DialogButtonArray *_buttons; + int _pressedButton; + uint _defaultButton; + bool _mouseOverPressedButton; + + bool _needsRedraw; + +private: + const Graphics::Font *getDialogFont(); + void drawOutline(Common::Rect &bounds, int *spec, int speclen); + void paint(); + void mouseMove(int x, int y); + void mouseClick(int x, int y); + int mouseRaise(int x, int y); + int matchButton(int x, int y); +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/entities.cpp b/engines/wage/entities.cpp new file mode 100644 index 0000000000..f85a228fbd --- /dev/null +++ b/engines/wage/entities.cpp @@ -0,0 +1,500 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/design.h" +#include "wage/script.h" +#include "wage/world.h" + +#include "common/memstream.h" + +namespace Wage { + +void Designed::setDesignBounds(Common::Rect *bounds) { + _designBounds = bounds; + _design->setBounds(bounds); +} + +Designed::~Designed() { + delete _design; + delete _designBounds; +} + +Context::Context() { + _visits = 0; + _kills = 0; + _experience = 0; + _frozen = false; + + for (int i = 0; i < 26 * 9; i++) + _userVariables[i] = 0; + + for (int i = 0; i < 18; i++) + _statVariables[i] = 0; +} + +Scene::Scene() { + _script = NULL; + _design = NULL; + _textBounds = NULL; + _fontSize = 0; + _fontType = 0; + + for (int i = 0; i < 4; i++) + _blocked[i] = false; + + _soundFrequency = 0; + _soundType = 0; + _worldX = 0; + _worldY = 0; + + _visited = false; +} + +Scene::Scene(Common::String name, Common::SeekableReadStream *data) { + _name = name; + _classType = SCENE; + _design = new Design(data); + + _script = NULL; + _textBounds = NULL; + _fontSize = 0; + _fontType = 0; + + setDesignBounds(readRect(data)); + _worldY = data->readSint16BE(); + _worldX = data->readSint16BE(); + _blocked[NORTH] = (data->readByte() != 0); + _blocked[SOUTH] = (data->readByte() != 0); + _blocked[EAST] = (data->readByte() != 0); + _blocked[WEST] = (data->readByte() != 0); + _soundFrequency = data->readSint16BE(); + _soundType = data->readByte(); + data->readByte(); // unknown + _messages[NORTH] = readPascalString(data); + _messages[SOUTH] = readPascalString(data); + _messages[EAST] = readPascalString(data); + _messages[WEST] = readPascalString(data); + _soundName = readPascalString(data); + + _visited = false; + + delete data; +} + +Scene::~Scene() { + delete _script; + delete _textBounds; +} + +void Scene::paint(Graphics::Surface *surface, int x, int y) { + Common::Rect r(x + 5, y + 5, _design->getBounds()->width() + x - 10, _design->getBounds()->height() + y - 10); + surface->fillRect(r, kColorWhite); + + _design->paint(surface, ((WageEngine *)g_engine)->_world->_patterns, x, y); + + for (ObjList::const_iterator it = _objs.begin(); it != _objs.end(); ++it) { + debug(2, "paining Obj: %s", (*it)->_name.c_str()); + (*it)->_design->paint(surface, ((WageEngine *)g_engine)->_world->_patterns, x, y); + } + + for (ChrList::const_iterator it = _chrs.begin(); it != _chrs.end(); ++it) { + debug(2, "paining Chr: %s", (*it)->_name.c_str()); + (*it)->_design->paint(surface, ((WageEngine *)g_engine)->_world->_patterns, x, y); + } +} + +// Source: Apple IIGS Technical Note #41, "Font Family Numbers" +// http://apple2.boldt.ca/?page=til/tn.iigs.041 +static const char *const fontNames[] = { + "Chicago", // system font + "Geneva", // application font + "New York", + "Geneva", + + "Monaco", + "Venice", + "London", + "Athens", + + "San Francisco", + "Toronto", + NULL, + "Cairo", + "Los Angeles", // 12 + + "Zapf Dingbats", + "Bookman", + "Helvetica Narrow", + "Palatino", + NULL, + "Zapf Chancery", + NULL, + + "Times", // 20 + "Helvetica", + "Courier", + "Symbol", + "Taliesin", // mobile? + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, // 30 + NULL, + NULL, + "Avant Garde", + "New Century Schoolbook" +}; + +const char *Scene::getFontName() { + if (_fontType >= 0 && _fontType < ARRAYSIZE(fontNames) && fontNames[_fontType] != NULL) { + return fontNames[_fontType]; + } + return "Unknown"; +} + +Obj::Obj(Common::String name, Common::SeekableReadStream *data) { + _name = name; + _classType = OBJ; + _currentOwner = NULL; + _currentScene = NULL; + + _index = 0; + + _design = new Design(data); + + setDesignBounds(readRect(data)); + + int16 namePlural = data->readSint16BE(); + + if (namePlural == 256) + _namePlural = true; // TODO: other flags? + else if (namePlural == 0) + _namePlural = false; + else + error("Obj <%s> had weird namePlural set (%d)", name.c_str(), namePlural); + + if (data->readSint16BE() != 0) + error("Obj <%s> had short set", name.c_str()); + + if (data->readByte() != 0) + error("Obj <%s> had byte set", name.c_str()); + + _accuracy = data->readByte(); + _value = data->readByte(); + _type = data->readSByte(); + _damage = data->readByte(); + _attackType = data->readSByte(); + _numberOfUses = data->readSint16BE(); + int16 returnTo = data->readSint16BE(); + if (returnTo == 256) // TODO any other possibilities? + _returnToRandomScene = true; + else if (returnTo == 0) + _returnToRandomScene = false; + else + error("Obj <%s> had weird returnTo set", name.c_str()); + + _sceneOrOwner = readPascalString(data); + _clickMessage = readPascalString(data); + _operativeVerb = readPascalString(data); + _failureMessage = readPascalString(data); + _useMessage = readPascalString(data); + _sound = readPascalString(data); + + delete data; +} + +Obj::~Obj() { +} + +Chr *Obj::removeFromChr() { + if (_currentOwner != NULL) { + for (int i = (int)_currentOwner->_inventory.size() - 1; i >= 0; i--) + if (_currentOwner->_inventory[i] == this) + _currentOwner->_inventory.remove_at(i); + + for (int i = 0; i < Chr::NUMBER_OF_ARMOR_TYPES; i++) { + if (_currentOwner->_armor[i] == this) { + _currentOwner->_armor[i] = NULL; + } + } + } + + return _currentOwner; +} + +Designed *Obj::removeFromCharOrScene() { + Designed *from = removeFromChr(); + + if (_currentScene != NULL) { + _currentScene->_objs.remove(this); + from = _currentScene; + } + + return from; +} + +void Obj::resetState(Chr *owner, Scene *scene) { + warning("STUB: Obj::resetState()"); +} + +Chr::Chr(Common::String name, Common::SeekableReadStream *data) { + _name = name; + _classType = CHR; + _design = new Design(data); + + _index = 0; + _currentScene = NULL; + + setDesignBounds(readRect(data)); + + _physicalStrength = data->readByte(); + _physicalHp = data->readByte(); + _naturalArmor = data->readByte(); + _physicalAccuracy = data->readByte(); + + _spiritualStength = data->readByte(); + _spiritialHp = data->readByte(); + _resistanceToMagic = data->readByte(); + _spiritualAccuracy = data->readByte(); + + _runningSpeed = data->readByte(); + _rejectsOffers = data->readByte(); + _followsOpponent = data->readByte(); + + data->readSByte(); // TODO: ??? + data->readSint32BE(); // TODO: ??? + + _weaponDamage1 = data->readByte(); + _weaponDamage2 = data->readByte(); + + data->readSByte(); // TODO: ??? + + if (data->readSByte() == 1) + _playerCharacter = true; + else + _playerCharacter = false; + + _maximumCarriedObjects = data->readByte(); + _returnTo = data->readSByte(); + + _winningWeapons = data->readByte(); + _winningMagic = data->readByte(); + _winningRun = data->readByte(); + _winningOffer = data->readByte(); + _losingWeapons = data->readByte(); + _losingMagic = data->readByte(); + _losingRun = data->readByte(); + _losingOffer = data->readByte(); + + _gender = data->readSByte(); + if (data->readSByte() == 1) + _nameProperNoun = true; + else + _nameProperNoun = false; + + _initialScene = readPascalString(data); + _nativeWeapon1 = readPascalString(data); + _operativeVerb1 = readPascalString(data); + _nativeWeapon2 = readPascalString(data); + _operativeVerb2 = readPascalString(data); + + _initialComment = readPascalString(data); + _scoresHitComment = readPascalString(data); + _receivesHitComment = readPascalString(data); + _makesOfferComment = readPascalString(data); + _rejectsOfferComment = readPascalString(data); + _acceptsOfferComment = readPascalString(data); + _dyingWords = readPascalString(data); + + _initialSound = readPascalString(data); + _scoresHitSound = readPascalString(data); + _receivesHitSound = readPascalString(data); + _dyingSound = readPascalString(data); + + _weaponSound1 = readPascalString(data); + _weaponSound2 = readPascalString(data); + + for (int i = 0; i < NUMBER_OF_ARMOR_TYPES; i++) + _armor[i] = NULL; + + _weapon1 = NULL; + _weapon2 = NULL; + + // Create native weapons + if (!_nativeWeapon1.empty() && !_operativeVerb1.empty()) { + _weapon1 = new Obj; + + _weapon1->_name = _nativeWeapon1; + _weapon1->_operativeVerb = _operativeVerb1; + _weapon1->_type = Obj::REGULAR_WEAPON; + _weapon1->_accuracy = 0; + _weapon1->_damage = _weaponDamage1; + _weapon1->_sound = _weaponSound1; + } + + if (!_nativeWeapon2.empty() && !_operativeVerb2.empty()) { + _weapon2 = new Obj; + + _weapon2->_name = _nativeWeapon2; + _weapon2->_operativeVerb = _operativeVerb2; + _weapon2->_type = Obj::REGULAR_WEAPON; + _weapon2->_accuracy = 0; + _weapon2->_damage = _weaponDamage2; + _weapon2->_sound = _weaponSound2; + } + + delete data; +} + +void Chr::resetState() { + _context._statVariables[PHYS_STR_BAS] = _context._statVariables[PHYS_STR_CUR] = _physicalStrength; + _context._statVariables[PHYS_HIT_BAS] = _context._statVariables[PHYS_HIT_CUR] = _physicalHp; + _context._statVariables[PHYS_ARM_BAS] = _context._statVariables[PHYS_ARM_CUR] = _naturalArmor; + _context._statVariables[PHYS_ACC_BAS] = _context._statVariables[PHYS_ACC_CUR] = _physicalAccuracy; + + _context._statVariables[SPIR_STR_BAS] = _context._statVariables[SPIR_STR_CUR] = _spiritualStength; + _context._statVariables[SPIR_HIT_BAS] = _context._statVariables[SPIR_HIT_CUR] = _spiritialHp; + _context._statVariables[SPIR_ARM_BAS] = _context._statVariables[SPIR_ARM_CUR] = _naturalArmor; + _context._statVariables[SPIR_ACC_BAS] = _context._statVariables[SPIR_ACC_CUR] = _physicalAccuracy; + + _context._statVariables[PHYS_SPE_BAS] = _context._statVariables[PHYS_SPE_CUR] = _runningSpeed; +} + +ObjArray *Chr::getWeapons(bool includeMagic) { + ObjArray *list = new ObjArray; + + if (_weapon1) + list->push_back(_weapon1); + + if (_weapon2) + list->push_back(_weapon2); + + for (uint i = 0; i < _inventory.size(); i++) + switch (_inventory[i]->_type) { + case Obj::REGULAR_WEAPON: + case Obj::THROW_WEAPON: + list->push_back(_inventory[i]); + break; + case Obj::MAGICAL_OBJECT: + if (includeMagic) + list->push_back(_inventory[i]); + break; + default: + break; + } + + return list; +} + +ObjArray *Chr::getMagicalObjects() { + ObjArray *list = new ObjArray; + + for (uint i = 0; i < _inventory.size(); i++) + if (_inventory[i]->_type == Obj::MAGICAL_OBJECT) + list->push_back(_inventory[i]); + + return list; +} + +void Chr::wearObjs() { + for (uint i = 0; i < _inventory.size(); i++) + wearObjIfPossible(_inventory[i]); +} + +int Chr::wearObjIfPossible(Obj *obj) { + switch (obj->_type) { + case Obj::HELMET: + if (_armor[HEAD_ARMOR] == NULL) { + _armor[HEAD_ARMOR] = obj; + return Chr::HEAD_ARMOR; + } + break; + case Obj::CHEST_ARMOR: + if (_armor[BODY_ARMOR] == NULL) { + _armor[BODY_ARMOR] = obj; + return Chr::BODY_ARMOR; + } + break; + case Obj::SHIELD: + if (_armor[SHIELD_ARMOR] == NULL) { + _armor[SHIELD_ARMOR] = obj; + return Chr::SHIELD_ARMOR; + } + break; + case Obj::SPIRITUAL_ARMOR: + if (_armor[MAGIC_ARMOR] == NULL) { + _armor[MAGIC_ARMOR] = obj; + return Chr::MAGIC_ARMOR; + } + break; + default: + return -1; + } + + return -1; +} + +const char *Chr::getDefiniteArticle(bool capitalize) { + if (!_nameProperNoun) + return capitalize ? "The " : "the "; + + return ""; +} + +bool Chr::isWearing(Obj *obj) { + for (int i = 0; i < NUMBER_OF_ARMOR_TYPES; i++) + if (_armor[i] == obj) + return true; + + return false; +} + +} // End of namespace Wage diff --git a/engines/wage/entities.h b/engines/wage/entities.h new file mode 100644 index 0000000000..0f5016e8b1 --- /dev/null +++ b/engines/wage/entities.h @@ -0,0 +1,345 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_ENTITIES_H +#define WAGE_ENTITIES_H + +namespace Graphics { + struct Surface; +} + +namespace Wage { + +class Design; +class Script; + +enum StatVariable { +/** The base physical accuracy of the player. */ + PHYS_ACC_BAS = 0, +/** The current physical accuracy of the player. */ + PHYS_ACC_CUR = 1, +/** The base physical armor of the player. */ + PHYS_ARM_BAS = 2, +/** The current physical armor of the player. */ + PHYS_ARM_CUR = 3, +/** The base physical hit points of the player. */ + PHYS_HIT_BAS = 4, +/** The current physical hit points of the player. */ + PHYS_HIT_CUR = 5, +/** The base physical speed of the player. */ + PHYS_SPE_BAS = 6, +/** The current physical speed of the player. */ + PHYS_SPE_CUR = 7, +/** The base physical strength of the player. */ + PHYS_STR_BAS = 8, +/** The current physical strength of the player. */ + PHYS_STR_CUR = 9, +/** The base spiritual accuracy of the player. */ + SPIR_ACC_BAS = 10, +/** The current spiritual accuracy of the player. */ + SPIR_ACC_CUR = 11, +/** The base spiritual armor of the player. */ + SPIR_ARM_BAS = 12, +/** The current spiritual armor of the player. */ + SPIR_ARM_CUR = 13, +/** The base spiritual hit points of the player. */ + SPIR_HIT_BAS = 14, +/** The current spiritual hit points of the player. */ + SPIR_HIT_CUR = 15, +/** The base spiritual strength of the player. */ + SPIR_STR_BAS = 16, +/** The current spiritual strength of the player. */ + SPIR_STR_CUR = 17 +}; + +class Context { +public: + Context(); + + int16 _visits; // Number of scenes visited, including repeated visits + int16 _kills; // Number of characters killed + int16 _experience; + bool _frozen; + int16 _userVariables[26 * 9]; + int16 _statVariables[18]; +}; + +class Designed { +public: + Designed() : _design(NULL), _designBounds(NULL), _classType(UNKNOWN) {} + ~Designed(); + + Common::String _name; + Design *_design; + Common::Rect *_designBounds; + OperandType _classType; + + Common::Rect *getDesignBounds() { + return _designBounds == NULL ? NULL : new Common::Rect(*_designBounds); + } + + void setDesignBounds(Common::Rect *bounds); + + Common::String toString() { return _name; } +}; + +class Chr : public Designed { +public: + enum ChrDestination { + RETURN_TO_STORAGE = 0, + RETURN_TO_RANDOM_SCENE = 1, + RETURN_TO_INITIAL_SCENE = 2 + }; + + enum ChrPart { + HEAD = 0, + CHEST = 1, + SIDE = 2 + }; + + enum ChrArmorType { + HEAD_ARMOR = 0, + BODY_ARMOR = 1, + SHIELD_ARMOR = 2, + MAGIC_ARMOR = 3, + NUMBER_OF_ARMOR_TYPES = 4 + }; + + Chr(Common::String name, Common::SeekableReadStream *data); + + int _index; + Common::String _initialScene; + int _gender; + bool _nameProperNoun; + bool _playerCharacter; + uint _maximumCarriedObjects; + int _returnTo; + + int _physicalStrength; + int _physicalHp; + int _naturalArmor; + int _physicalAccuracy; + int _spiritualStength; + int _spiritialHp; + int _resistanceToMagic; + int _spiritualAccuracy; + int _runningSpeed; + uint _rejectsOffers; + int _followsOpponent; + + Common::String _initialSound; + Common::String _scoresHitSound; + Common::String _receivesHitSound; + Common::String _dyingSound; + + Common::String _nativeWeapon1; + Common::String _operativeVerb1; + int _weaponDamage1; + Common::String _weaponSound1; + + Common::String _nativeWeapon2; + Common::String _operativeVerb2; + int _weaponDamage2; + Common::String _weaponSound2; + + int _winningWeapons; + int _winningMagic; + int _winningRun; + int _winningOffer; + int _losingWeapons; + int _losingMagic; + int _losingRun; + int _losingOffer; + + Common::String _initialComment; + Common::String _scoresHitComment; + Common::String _receivesHitComment; + Common::String _makesOfferComment; + Common::String _rejectsOfferComment; + Common::String _acceptsOfferComment; + Common::String _dyingWords; + + Scene *_currentScene; + ObjArray _inventory; + + Obj *_armor[NUMBER_OF_ARMOR_TYPES]; + + Context _context; + + ObjArray *getWeapons(bool includeMagic); + ObjArray *getMagicalObjects(); + const char *getDefiniteArticle(bool capitalize); + + Obj *_weapon1; + Obj *_weapon2; + +public: + int wearObjIfPossible(Obj *obj); + void wearObjs(); + + void resetState(); + + bool isWearing(Obj *obj); +}; + +class Obj : public Designed { +public: + Obj() : _currentOwner(NULL), _currentScene(NULL) {} + Obj(Common::String name, Common::SeekableReadStream *data); + ~Obj(); + + enum ObjectType { + REGULAR_WEAPON = 1, + THROW_WEAPON = 2, + MAGICAL_OBJECT = 3, + HELMET = 4, + SHIELD = 5, + CHEST_ARMOR = 6, + SPIRITUAL_ARMOR = 7, + MOBILE_OBJECT = 8, + IMMOBILE_OBJECT = 9 + }; + + enum AttackType { + CAUSES_PHYSICAL_DAMAGE = 0, + CAUSES_SPIRITUAL_DAMAGE = 1, + CAUSES_PHYSICAL_AND_SPIRITUAL_DAMAGE = 2, + HEALS_PHYSICAL_DAMAGE = 3, + HEALS_SPIRITUAL_DAMAGE = 4, + HEALS_PHYSICAL_AND_SPIRITUAL_DAMAGE = 5, + FREEZES_OPPONENT = 6 + }; + +public: + int _index; + bool _namePlural; + uint _value; + int _attackType; + int _numberOfUses; + bool _returnToRandomScene; + Common::String _sceneOrOwner; + Common::String _clickMessage; + Common::String _failureMessage; + Common::String _useMessage; + + Scene *_currentScene; + Chr *_currentOwner; + + int _type; + uint _accuracy; + Common::String _operativeVerb; + int _damage; + Common::String _sound; + +public: + void setCurrentOwner(Chr *currentOwner) { + _currentOwner = currentOwner; + if (currentOwner != NULL) + _currentScene = NULL; + } + + void setCurrentScene(Scene *currentScene) { + _currentScene = currentScene; + if (currentScene != NULL) + _currentOwner = NULL; + } + + Chr *removeFromChr(); + Designed *removeFromCharOrScene(); + + void resetState(Chr *owner, Scene *scene); +}; + +class Scene : public Designed { +public: + enum SceneTypes { + PERIODIC = 0, + RANDOM = 1 + }; + + Script *_script; + Common::String _text; + Common::Rect *_textBounds; + int _fontSize; + int _fontType; // 3 => Geneva, 22 => Courier, param to TextFont() function + bool _blocked[4]; + Common::String _messages[4]; + int _soundFrequency; // times a minute, max 3600 + int _soundType; + Common::String _soundName; + int _worldX; + int _worldY; + bool _visited; + + ObjList _objs; + ChrList _chrs; + + Scene(); + Scene(Common::String name, Common::SeekableReadStream *data); + ~Scene(); + + Common::Rect *getTextBounds() { + return _textBounds == NULL ? NULL : new Common::Rect(*_textBounds); + } + + void paint(Graphics::Surface *screen, int x, int y); + + const char *getFontName(); +}; + +class Sound { +public: + Sound(Common::String name, Common::SeekableReadStream *data) : _name(name), _data(data) {} + ~Sound() { } + + Common::String _name; + Common::SeekableReadStream *_data; +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/gui-console.cpp b/engines/wage/gui-console.cpp new file mode 100644 index 0000000000..f1095de3ca --- /dev/null +++ b/engines/wage/gui-console.cpp @@ -0,0 +1,398 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/timer.h" +#include "common/unzip.h" +#include "graphics/cursorman.h" +#include "graphics/fonts/bdf.h" +#include "graphics/palette.h" + +#include "wage/wage.h" +#include "wage/design.h" +#include "wage/entities.h" +#include "wage/menu.h" +#include "wage/gui.h" +#include "wage/world.h" + +namespace Wage { + +const Graphics::Font *Gui::getConsoleFont() { + char fontName[128]; + Scene *scene = _engine->_world->_player->_currentScene; + + snprintf(fontName, 128, "%s-%d", scene->getFontName(), scene->_fontSize); + + return getFont(fontName, Graphics::FontManager::kConsoleFont); +} + +void Gui::clearOutput() { + _out.clear(); + _lines.clear(); + _consoleFullRedraw = true; +} + +void Gui::appendText(const char *s) { + Common::String str(s); + _consoleDirty = true; + + if (!str.contains('\n')) { + _out.push_back(str); + flowText(str); + return; + } + + // Okay, we got new lines, need to split it + // and push substrings individually + Common::String tmp; + + for (uint i = 0; i < str.size(); i++) { + if (str[i] == '\n') { + _out.push_back(tmp); + flowText(tmp); + tmp.clear(); + continue; + } + + tmp += str[i]; + } + + _out.push_back(tmp); + flowText(tmp); +} + +enum { + kConWOverlap = 20, + kConHOverlap = 20, + kConWPadding = 3, + kConHPadding = 4, + kConOverscan = 3 +}; + +void Gui::flowText(Common::String &str) { + Common::StringArray wrappedLines; + int textW = _consoleTextArea.width() - kConWPadding * 2; + const Graphics::Font *font = getConsoleFont(); + + font->wordWrapText(str, textW, wrappedLines); + + if (wrappedLines.empty()) // Sometimes we have empty lines + _lines.push_back(""); + + for (Common::StringArray::const_iterator j = wrappedLines.begin(); j != wrappedLines.end(); ++j) + _lines.push_back(*j); + + uint pos = _scrollPos; + _scrollPos = MAX<int>(0, (_lines.size() - 1 - _consoleNumLines) * _consoleLineHeight); + + _cursorX = kConWPadding; + + if (_scrollPos) + _cursorY = (_consoleNumLines) * _consoleLineHeight + kConHPadding; + else + _cursorY = (_lines.size() - 1) * _consoleLineHeight + kConHPadding; + + if (pos != _scrollPos) + _consoleFullRedraw = true; + + if (!_engine->_temporarilyHidden) + draw(); +} + +void Gui::renderConsole(Graphics::Surface *g, Common::Rect &r) { + bool fullRedraw = _consoleFullRedraw; + bool textReflow = false; + int surfW = r.width() + kConWOverlap * 2; + int surfH = r.height() + kConHOverlap * 2; + + Common::Rect boundsR(kConWOverlap - kConOverscan, kConHOverlap - kConOverscan, + r.width() + kConWOverlap + kConOverscan, r.height() + kConHOverlap + kConOverscan); + Common::Rect fullR(0, 0, surfW, surfH); + + if (_console.w != surfW || _console.h != surfH) { + if (_console.w != surfW) + textReflow = true; + + _console.free(); + + _console.create(surfW, surfH, Graphics::PixelFormat::createFormatCLUT8()); + fullRedraw = true; + } + + if (fullRedraw) + _console.fillRect(fullR, kColorWhite); + + const Graphics::Font *font = getConsoleFont(); + + _consoleLineHeight = font->getFontHeight(); + int textW = r.width() - kConWPadding * 2; + int textH = r.height() - kConHPadding * 2; + + if (textReflow) { + _lines.clear(); + + for (uint i = 0; i < _out.size(); i++) + flowText(_out[i]); + } + + const int firstLine = _scrollPos / _consoleLineHeight; + const int lastLine = MIN((_scrollPos + textH) / _consoleLineHeight + 1, _lines.size()); + const int xOff = kConWOverlap; + const int yOff = kConHOverlap; + int x1 = xOff + kConWPadding; + int y1 = yOff - (_scrollPos % _consoleLineHeight) + kConHPadding; + + if (fullRedraw) + _consoleNumLines = (r.height() - 2 * kConWPadding) / _consoleLineHeight - 2; + + for (int line = firstLine; line < lastLine; line++) { + const char *str = _lines[line].c_str(); + int color = kColorBlack; + + if ((line > _selectionStartY && line < _selectionEndY) || + (line > _selectionEndY && line < _selectionStartY)) { + color = kColorWhite; + Common::Rect trect(0, y1, _console.w, y1 + _consoleLineHeight); + + Design::drawFilledRect(&_console, trect, kColorBlack, _patterns, kPatternSolid); + } + + if (line == _selectionStartY || line == _selectionEndY) { + if (_selectionStartY != _selectionEndY) { + int color1 = kColorBlack; + int color2 = kColorWhite; + int midpoint = _selectionStartX; + + if (_selectionStartY > _selectionEndY) + SWAP(color1, color2); + + if (line == _selectionEndY) { + SWAP(color1, color2); + midpoint = _selectionEndX; + } + + Common::String beg(_lines[line].c_str(), &_lines[line].c_str()[midpoint]); + Common::String end(&_lines[line].c_str()[midpoint]); + + int rectW = font->getStringWidth(beg) + kConWPadding + kConWOverlap; + Common::Rect trect(0, y1, _console.w, y1 + _consoleLineHeight); + if (color1 == kColorWhite) + trect.right = rectW; + else + trect.left = rectW; + + Design::drawFilledRect(&_console, trect, kColorBlack, _patterns, kPatternSolid); + + font->drawString(&_console, beg, x1, y1, textW, color1); + font->drawString(&_console, end, x1 + rectW - kConWPadding - kConWOverlap, y1, textW, color2); + } else { + int startPos = _selectionStartX; + int endPos = _selectionEndX; + + if (startPos > endPos) + SWAP(startPos, endPos); + + Common::String beg(_lines[line].c_str(), &_lines[line].c_str()[startPos]); + Common::String mid(&_lines[line].c_str()[startPos], &_lines[line].c_str()[endPos]); + Common::String end(&_lines[line].c_str()[endPos]); + + int rectW1 = font->getStringWidth(beg) + kConWPadding + kConWOverlap; + int rectW2 = rectW1 + font->getStringWidth(mid); + Common::Rect trect(rectW1, y1, rectW2, y1 + _consoleLineHeight); + + Design::drawFilledRect(&_console, trect, kColorBlack, _patterns, kPatternSolid); + + font->drawString(&_console, beg, x1, y1, textW, kColorBlack); + font->drawString(&_console, mid, x1 + rectW1 - kConWPadding - kConWOverlap, y1, textW, kColorWhite); + font->drawString(&_console, end, x1 + rectW2 - kConWPadding - kConWOverlap, y1, textW, kColorBlack); + } + } else { + if (*str) + font->drawString(&_console, _lines[line], x1, y1, textW, color); + } + + y1 += _consoleLineHeight; + } + + g->copyRectToSurface(_console, r.left - kConOverscan, r.top - kConOverscan, boundsR); + g_system->copyRectToScreen(g->getBasePtr(r.left, r.top), g->pitch, r.left, r.top, r.width(), r.height()); +} + +void Gui::drawInput() { + if (!_screen.getPixels()) + return; + + if (_sceneIsActive) { + _sceneIsActive = false; + _bordersDirty = true; + } + + _out.pop_back(); + _lines.pop_back(); + appendText(_engine->_inputText.c_str()); + _inputTextLineNum = _out.size() - 1; + + const Graphics::Font *font = getConsoleFont(); + + if (_engine->_inputText.contains('\n')) { + _consoleDirty = true; + } else { + int x = kConWPadding + _consoleTextArea.left; + int y = _cursorY + _consoleTextArea.top; + + Common::Rect r(x, y, x + _consoleTextArea.width() - kConWPadding, y + font->getFontHeight()); + _screen.fillRect(r, kColorWhite); + + undrawCursor(); + + font->drawString(&_screen, _out[_inputTextLineNum], x, y, _screen.w, kColorBlack); + + g_system->copyRectToScreen(_screen.getBasePtr(x, y), _screen.pitch, x, y, _consoleTextArea.width(), font->getFontHeight()); + } + + _cursorX = font->getStringWidth(_out[_inputTextLineNum]) + kConHPadding; +} + +void Gui::actionCopy() { + if (_selectionStartX == -1) + return; + + int startX = _selectionStartX; + int startY = _selectionStartY; + int endX = _selectionEndX; + int endY = _selectionEndY; + + if (startY > endY) { + SWAP(startX, endX); + SWAP(endX, endY); + } + + _clipboard.clear(); + + for (int i = startY; i <= endY; i++) { + if (startY == endY) { + _clipboard = Common::String(&_lines[i].c_str()[startX], &_lines[i].c_str()[endX]); + break; + } + + if (i == startY) { + _clipboard += &_lines[i].c_str()[startX]; + _clipboard += '\n'; + } else if (i == endY) { + _clipboard += Common::String(_lines[i].c_str(), &_lines[i].c_str()[endX]); + } else { + _clipboard += _lines[i]; + _clipboard += '\n'; + } + } + + _menu->enableCommand(kMenuEdit, kMenuActionPaste, true); +} + +void Gui::actionPaste() { + _undobuffer = _engine->_inputText; + _engine->_inputText += _clipboard; + drawInput(); + _engine->_inputText = _out.back(); // Set last part of the multiline text + + _menu->enableCommand(kMenuEdit, kMenuActionUndo, true); +} + +void Gui::actionUndo() { + _engine->_inputText = _undobuffer; + drawInput(); + + _menu->enableCommand(kMenuEdit, kMenuActionUndo, false); +} + +void Gui::actionClear() { + int startPos = _selectionStartX; + int endPos = _selectionEndX; + + if (startPos > endPos) + SWAP(startPos, endPos); + + Common::String beg(_lines[_selectionStartY].c_str(), &_lines[_selectionStartY].c_str()[startPos]); + Common::String end(&_lines[_selectionStartY].c_str()[endPos]); + + _undobuffer = _engine->_inputText; + _engine->_inputText = beg + end; + drawInput(); + + _menu->enableCommand(kMenuEdit, kMenuActionUndo, true); + + _selectionStartY = -1; + _selectionEndY = -1; +} + +void Gui::actionCut() { + int startPos = _selectionStartX; + int endPos = _selectionEndX; + + if (startPos > endPos) + SWAP(startPos, endPos); + + Common::String beg(_lines[_selectionStartY].c_str(), &_lines[_selectionStartY].c_str()[startPos]); + Common::String mid(&_lines[_selectionStartY].c_str()[startPos], &_lines[_selectionStartY].c_str()[endPos]); + Common::String end(&_lines[_selectionStartY].c_str()[endPos]); + + _undobuffer = _engine->_inputText; + _engine->_inputText = beg + end; + _clipboard = mid; + drawInput(); + + _menu->enableCommand(kMenuEdit, kMenuActionUndo, true); + _menu->enableCommand(kMenuEdit, kMenuActionPaste, true); + + _selectionStartY = -1; + _selectionEndY = -1; +} + +void Gui::disableUndo() { + _menu->enableCommand(kMenuEdit, kMenuActionUndo, false); +} + +} // End of namespace Wage diff --git a/engines/wage/gui.cpp b/engines/wage/gui.cpp new file mode 100644 index 0000000000..f7196abf17 --- /dev/null +++ b/engines/wage/gui.cpp @@ -0,0 +1,594 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/timer.h" +#include "common/unzip.h" +#include "graphics/cursorman.h" +#include "graphics/fonts/bdf.h" +#include "graphics/palette.h" + +#include "wage/wage.h" +#include "wage/design.h" +#include "wage/entities.h" +#include "wage/menu.h" +#include "wage/gui.h" +#include "wage/world.h" + +namespace Wage { + +static const byte palette[] = { + 0, 0, 0, // Black + 0x80, 0x80, 0x80, // Gray + 0xff, 0xff, 0xff, // White + 0x00, 0xff, 0x00 // Green +}; + +static byte fillPatterns[][8] = { { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // kPatternSolid + { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }, // kPatternStripes + { 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55 }, // kPatternCheckers + { 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa } // kPatternCheckers2 +}; + +static const byte macCursorArrow[] = { + 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 0, 0, 2, 3, 3, 3, 3, 3, 3, 3, + 2, 0, 0, 0, 2, 3, 3, 3, 3, 3, 3, + 2, 0, 0, 0, 0, 2, 3, 3, 3, 3, 3, + 2, 0, 0, 0, 0, 0, 2, 3, 3, 3, 3, + 2, 0, 0, 0, 0, 0, 0, 2, 3, 3, 3, + 2, 0, 0, 0, 0, 0, 0, 0, 2, 3, 3, + 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, + 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, + 2, 0, 0, 2, 0, 0, 2, 3, 3, 3, 3, + 2, 0, 2, 3, 2, 0, 0, 2, 3, 3, 3, + 2, 2, 3, 3, 2, 0, 0, 2, 3, 3, 3, + 2, 3, 3, 3, 3, 2, 0, 0, 2, 3, 3, + 3, 3, 3, 3, 3, 2, 0, 0, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 2, 2, 2, 3, 3 +}; + +static const byte macCursorBeam[] = { + 0, 0, 3, 3, 3, 0, 0, 3, 3, 3, 3, + 3, 3, 0, 3, 0, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 0, 3, 0, 3, 3, 3, 3, 3, 3, + 0, 0, 3, 3, 3, 0, 0, 3, 3, 3, 3, +}; + +static void cursorTimerHandler(void *refCon) { + Gui *gui = (Gui *)refCon; + + int x = gui->_cursorX; + int y = gui->_cursorY; + + if (x == 0 && y == 0) + return; + + if (!gui->_screen.getPixels()) + return; + + x += gui->_consoleTextArea.left; + y += gui->_consoleTextArea.top; + + gui->_screen.vLine(x, y, y + kCursorHeight, gui->_cursorState ? kColorBlack : kColorWhite); + + if (!gui->_cursorOff) + gui->_cursorState = !gui->_cursorState; + + gui->_cursorRect.left = x; + gui->_cursorRect.right = x + 1; + gui->_cursorRect.top = y; + gui->_cursorRect.bottom = y + kCursorHeight; + + gui->_cursorDirty = true; +} + +Gui::Gui(WageEngine *engine) { + _engine = engine; + _scene = NULL; + _sceneDirty = true; + _consoleDirty = true; + _bordersDirty = true; + _menuDirty = true; + _cursorDirty = false; + _consoleFullRedraw = true; + _screen.create(g_system->getWidth(), g_system->getHeight(), Graphics::PixelFormat::createFormatCLUT8()); + + _scrollPos = 0; + _consoleLineHeight = 8; // Dummy value which makes sense + _consoleNumLines = 24; // Dummy value + _builtInFonts = false; + _sceneIsActive = false; + + _cursorX = 0; + _cursorY = 0; + _cursorState = false; + _cursorOff = false; + + _inTextSelection = false; + _selectionStartX = _selectionStartY = -1; + _selectionEndX = _selectionEndY = -1; + + _inputTextLineNum = 0; + + g_system->getPaletteManager()->setPalette(palette, 0, 4); + + CursorMan.replaceCursorPalette(palette, 0, 4); + CursorMan.replaceCursor(macCursorArrow, 11, 16, 1, 1, 3); + _cursorIsArrow = true; + CursorMan.showMouse(true); + + for (int i = 0; i < ARRAYSIZE(fillPatterns); i++) + _patterns.push_back(fillPatterns[i]); + + loadFonts(); + + g_system->getTimerManager()->installTimerProc(&cursorTimerHandler, 200000, this, "wageCursor"); + + _menu = new Menu(this); +} + +Gui::~Gui() { + _screen.free(); + _console.free(); + g_system->getTimerManager()->removeTimerProc(&cursorTimerHandler); + delete _menu; +} + +void Gui::undrawCursor() { + _cursorOff = true; + _cursorState = false; + cursorTimerHandler(this); + _cursorOff = false; +} + +const Graphics::Font *Gui::getFont(const char *name, Graphics::FontManager::FontUsage fallback) { + const Graphics::Font *font; + + if (!_builtInFonts) { + font = FontMan.getFontByName(name); + + if (!font) + warning("Cannot load font %s", name); + } + + if (_builtInFonts || !font) + font = FontMan.getFontByUsage(fallback); + + return font; +} + +const Graphics::Font *Gui::getTitleFont() { + return getFont("Chicago-12", Graphics::FontManager::kBigGUIFont); +} + +void Gui::draw() { + if (_scene != _engine->_world->_player->_currentScene || _sceneDirty) { + _scene = _engine->_world->_player->_currentScene; + + // Draw desktop + Common::Rect r(0, 0, _screen.w - 1, _screen.h - 1); + Design::drawFilledRoundRect(&_screen, r, kDesktopArc, kColorBlack, _patterns, kPatternCheckers); + g_system->copyRectToScreen(_screen.getPixels(), _screen.pitch, 0, 0, _screen.w, _screen.h); + + _sceneDirty = true; + _consoleDirty = true; + _menuDirty = true; + _consoleFullRedraw = true; + + _scene->paint(&_screen, _scene->_designBounds->left, _scene->_designBounds->top); + + _sceneArea.left = _scene->_designBounds->left + kBorderWidth - 2; + _sceneArea.top = _scene->_designBounds->top + kBorderWidth - 2; + _sceneArea.setWidth(_scene->_designBounds->width() - 2 * kBorderWidth); + _sceneArea.setHeight(_scene->_designBounds->height() - 2 * kBorderWidth); + + _consoleTextArea.left = _scene->_textBounds->left + kBorderWidth - 2; + _consoleTextArea.top = _scene->_textBounds->top + kBorderWidth - 2; + _consoleTextArea.setWidth(_scene->_textBounds->width() - 2 * kBorderWidth); + _consoleTextArea.setHeight(_scene->_textBounds->height() - 2 * kBorderWidth); + } + + if (_scene && (_bordersDirty || _sceneDirty)) + paintBorder(&_screen, _sceneArea, kWindowScene); + + // Render console + if (_consoleDirty || _consoleFullRedraw) + renderConsole(&_screen, _consoleTextArea); + + if (_bordersDirty || _consoleDirty || _consoleFullRedraw) + paintBorder(&_screen, _consoleTextArea, kWindowConsole); + + if (_menuDirty) + _menu->render(); + + if (_cursorDirty) { + g_system->copyRectToScreen(_screen.getBasePtr(_cursorRect.left, _cursorRect.top), _screen.pitch, + _cursorRect.left, _cursorRect.top, _cursorRect.width(), _cursorRect.height()); + + _cursorDirty = false; + } + + _sceneDirty = false; + _consoleDirty = false; + _bordersDirty = false; + _menuDirty = false; + _consoleFullRedraw = false; +} + +void Gui::drawBox(Graphics::Surface *g, int x, int y, int w, int h) { + Common::Rect r(x, y, x + w + 1, y + h + 1); + + g->fillRect(r, kColorWhite); + g->frameRect(r, kColorBlack); +} + +void Gui::fillRect(Graphics::Surface *g, int x, int y, int w, int h) { + Common::Rect r(x, y, x + w, y + h); + + g->fillRect(r, kColorBlack); +} + +#define ARROW_W 12 +#define ARROW_H 6 +const int arrowPixels[ARROW_H][ARROW_W] = { + {0,0,0,0,0,1,1,0,0,0,0,0}, + {0,0,0,0,1,1,1,1,0,0,0,0}, + {0,0,0,1,1,1,1,1,1,0,0,0}, + {0,0,1,1,1,1,1,1,1,1,0,0}, + {0,1,1,1,1,1,1,1,1,1,1,0}, + {1,1,1,1,1,1,1,1,1,1,1,1}}; + +void Gui::paintBorder(Graphics::Surface *g, Common::Rect &r, WindowType windowType) { + bool active, scrollable, closeable, closeBoxPressed, drawTitle; + const int size = kBorderWidth; + int x = r.left - size; + int y = r.top - size; + int width = r.width() + 2 * size; + int height = r.height() + 2 * size; + + switch (windowType) { + case kWindowScene: + active = _sceneIsActive; + scrollable = false; + closeable = _sceneIsActive; + closeBoxPressed = false; + drawTitle = true; + break; + case kWindowConsole: + active = !_sceneIsActive; + scrollable = true; + closeable = !_sceneIsActive; + closeBoxPressed = false; + drawTitle = false; + break; + } + + drawBox(g, x, y, size, size); + drawBox(g, x + width - size - 1, y, size, size); + drawBox(g, x + width - size - 1, y + height - size - 1, size, size); + drawBox(g, x, y + height - size - 1, size, size); + drawBox(g, x + size, y + 2, width - 2 * size - 1, size - 4); + drawBox(g, x + size, y + height - size + 1, width - 2 * size - 1, size - 4); + drawBox(g, x + 2, y + size, size - 4, height - 2 * size - 1); + drawBox(g, x + width - size + 1, y + size, size - 4, height - 2 * size - 1); + + if (active) { + fillRect(g, x + size, y + 5, width - 2 * size - 1, 8); + fillRect(g, x + size, y + height - 13, width - 2 * size - 1, 8); + fillRect(g, x + 5, y + size, 8, height - 2 * size - 1); + if (!scrollable) { + fillRect(g, x + width - 13, y + size, 8, height - 2 * size - 1); + } else { + int x1 = x + width - 15; + int y1 = y + size + 1; + for (int yy = 0; yy < ARROW_H; yy++) { + for (int xx = 0; xx < ARROW_W; xx++) { + if (arrowPixels[yy][xx] != 0) { + g->hLine(x1 + xx, y1 + yy, x1 + xx, kColorBlack); + } else { + g->hLine(x1 + xx, y1 + yy, x1 + xx, kColorWhite); + } + } + } + fillRect(g, x + width - 13, y + size + ARROW_H, 8, height - 2 * size - 1 - ARROW_H * 2); + y1 += height - 2 * size - ARROW_H - 2; + for (int yy = 0; yy < ARROW_H; yy++) { + for (int xx = 0; xx < ARROW_W; xx++) { + if (arrowPixels[ARROW_H - yy - 1][xx] != 0) { + g->hLine(x1 + xx, y1 + yy, x1 + xx, kColorBlack); + } else { + g->hLine(x1 + xx, y1 + yy, x1 + xx, kColorWhite); + } + } + } + } + if (closeable) { + if (closeBoxPressed) { + fillRect(g, x + 6, y + 6, 6, 6); + } else { + drawBox(g, x + 5, y + 5, 7, 7); + } + } + } + + if (drawTitle) { + const Graphics::Font *font = getTitleFont(); + int yOff = _builtInFonts ? 3 : 1; + + int w = font->getStringWidth(_scene->_name) + 10; + int maxWidth = width - size * 2 - 7; + if (w > maxWidth) + w = maxWidth; + drawBox(g, x + (width - w) / 2, y, w, size); + font->drawString(g, _scene->_name, x + (width - w) / 2 + 5, y + yOff, w, kColorBlack); + } + + if (x + width > _screen.w) + width = _screen.w - x; + if (y + height > _screen.h) + height = _screen.h - y; + + g_system->copyRectToScreen(g->getBasePtr(x, y), g->pitch, x, y, width, height); +} + +void Gui::loadFonts() { + Common::Archive *dat; + + dat = Common::makeZipArchive("wage.dat"); + + if (!dat) { + warning("Could not find wage.dat. Falling back to built-in fonts"); + _builtInFonts = true; + + return; + } + + Common::ArchiveMemberList list; + dat->listMembers(list); + + for (Common::ArchiveMemberList::iterator it = list.begin(); it != list.end(); ++it) { + Common::SeekableReadStream *stream = dat->createReadStreamForMember((*it)->getName()); + + Graphics::BdfFont *font = Graphics::BdfFont::loadFont(*stream); + + delete stream; + + Common::String fontName = (*it)->getName(); + + // Trim the .bdf extension + for (int i = fontName.size() - 1; i >= 0; --i) { + if (fontName[i] == '.') { + while ((uint)i < fontName.size()) { + fontName.deleteLastChar(); + } + break; + } + } + + FontMan.assignFontToName(fontName, font); + + debug(2, " %s", fontName.c_str()); + } + + _builtInFonts = false; + + delete dat; +} + +void Gui::regenCommandsMenu() { + _menu->regenCommandsMenu(); +} + +void Gui::regenWeaponsMenu() { + _menu->regenWeaponsMenu(); +} + +void Gui::processMenuShortCut(byte flags, uint16 ascii) { + _menu->processMenuShortCut(flags, ascii); +} + +void Gui::mouseMove(int x, int y) { + if (_menu->_menuActivated) { + if (_menu->mouseMove(x, y)) + _menuDirty = true; + + return; + } + + if (_inTextSelection) { + updateTextSelection(x, y); + return; + } + + if (_consoleTextArea.contains(x, y)) { + if (_cursorIsArrow) { + CursorMan.replaceCursor(macCursorBeam, 11, 16, 3, 8, 3); + _cursorIsArrow = false; + } + } else if (_cursorIsArrow == false) { + CursorMan.replaceCursor(macCursorArrow, 11, 16, 1, 1, 3); + _cursorIsArrow = true; + } +} + +void Gui::pushArrowCursor() { + CursorMan.pushCursor(macCursorArrow, 11, 16, 1, 1, 3); +} + +void Gui::popCursor() { + CursorMan.popCursor(); +} + +Designed *Gui::mouseUp(int x, int y) { + if (_menu->_menuActivated) { + if (_menu->mouseRelease(x, y)) { + _sceneDirty = true; + _consoleDirty = true; + _bordersDirty = true; + _menuDirty = true; + } + + return NULL; + } + + if (_inTextSelection) { + _inTextSelection = false; + + if (_selectionEndY == -1 || + (_selectionEndX == _selectionStartX && _selectionEndY == _selectionStartY)) { + _selectionStartY = _selectionEndY = -1; + _consoleFullRedraw = true; + _menu->enableCommand(kMenuEdit, kMenuActionCopy, false); + } else { + _menu->enableCommand(kMenuEdit, kMenuActionCopy, true); + + bool cutAllowed = false; + + if (_selectionStartY == _selectionEndY && _selectionStartY == (int)_lines.size() - 1) + cutAllowed = true; + + _menu->enableCommand(kMenuEdit, kMenuActionCut, cutAllowed); + _menu->enableCommand(kMenuEdit, kMenuActionClear, cutAllowed); + } + } + + if (_sceneArea.contains(x, y)) { + if (!_sceneIsActive) { + _sceneIsActive = true; + _bordersDirty = true; + } + + for (ObjList::const_iterator it = _scene->_objs.begin(); it != _scene->_objs.end(); ++it) { + if ((*it)->_design->isPointOpaque(x - _sceneArea.left + kBorderWidth, y - _sceneArea.top + kBorderWidth)) + return *it; + } + + for (ChrList::const_iterator it = _scene->_chrs.begin(); it != _scene->_chrs.end(); ++it) { + if ((*it)->_design->isPointOpaque(x - _sceneArea.left + kBorderWidth, y - _sceneArea.top + kBorderWidth)) + return *it; + } + } else if (_consoleTextArea.contains(x, y)) { + if (_sceneIsActive) { + _sceneIsActive = false; + _bordersDirty = true; + } + } + + return NULL; +} + +void Gui::mouseDown(int x, int y) { + if (_menu->mouseClick(x, y)) { + _menuDirty = true; + } else if (_consoleTextArea.contains(x, y)) { + startMarking(x, y); + } +} + +int Gui::calcTextX(int x, int textLine) { + const Graphics::Font *font = getConsoleFont(); + + if ((uint)textLine >= _lines.size()) + return 0; + + Common::String str = _lines[textLine]; + + x -= _consoleTextArea.left; + + for (int i = str.size(); i >= 0; i--) { + if (font->getStringWidth(str) < x) { + return i; + } + + str.deleteLastChar(); + } + + return 0; +} + +int Gui::calcTextY(int y) { + y -= _consoleTextArea.top; + + if (y < 0) + y = 0; + + const int firstLine = _scrollPos / _consoleLineHeight; + int textLine = (y - _scrollPos % _consoleLineHeight) / _consoleLineHeight + firstLine; + + return textLine; +} + +void Gui::startMarking(int x, int y) { + _selectionStartY = calcTextY(y); + _selectionStartX = calcTextX(x, _selectionStartY); + + _selectionEndY = -1; + + _inTextSelection = true; +} + +void Gui::updateTextSelection(int x, int y) { + _selectionEndY = calcTextY(y); + _selectionEndX = calcTextX(x, _selectionEndY); + + _consoleFullRedraw = true; +} + +} // End of namespace Wage diff --git a/engines/wage/gui.h b/engines/wage/gui.h new file mode 100644 index 0000000000..8cdc827bf0 --- /dev/null +++ b/engines/wage/gui.h @@ -0,0 +1,178 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_GUI_H +#define WAGE_GUI_H + +#include "common/str-array.h" +#include "graphics/font.h" +#include "graphics/fontman.h" +#include "graphics/surface.h" +#include "common/rect.h" + +namespace Wage { + +class Menu; + +enum WindowType { + kWindowScene, + kWindowConsole +}; + +enum { + kMenuHeight = 20, + kMenuLeftMargin = 7, + kMenuSpacing = 13, + kMenuPadding = 16, + kMenuDropdownPadding = 14, + kMenuDropdownItemHeight = 16, + kMenuItemHeight = 20, + kBorderWidth = 17, + kDesktopArc = 7, + kComponentsPadding = 10, + kCursorHeight = 12 +}; + +enum { + kPatternSolid = 1, + kPatternStripes = 2, + kPatternCheckers = 3, + kPatternCheckers2 = 4 +}; + +class Gui { +public: + Gui(WageEngine *engine); + ~Gui(); + + void draw(); + void appendText(const char *str); + void clearOutput(); + void mouseMove(int x, int y); + void mouseDown(int x, int y); + Designed *mouseUp(int x, int y); + void drawInput(); + void setSceneDirty() { _sceneDirty = true; } + const Graphics::Font *getFont(const char *name, Graphics::FontManager::FontUsage fallback); + void regenCommandsMenu(); + void regenWeaponsMenu(); + void processMenuShortCut(byte flags, uint16 ascii); + void pushArrowCursor(); + void popCursor(); + + void actionCopy(); + void actionPaste(); + void actionUndo(); + void actionClear(); + void actionCut(); + void disableUndo(); + +private: + void undrawCursor(); + void paintBorder(Graphics::Surface *g, Common::Rect &r, WindowType windowType); + void renderConsole(Graphics::Surface *g, Common::Rect &r); + void drawBox(Graphics::Surface *g, int x, int y, int w, int h); + void fillRect(Graphics::Surface *g, int x, int y, int w, int h); + void loadFonts(); + void flowText(Common::String &str); + const Graphics::Font *getConsoleFont(); + const Graphics::Font *getTitleFont(); + void startMarking(int x, int y); + int calcTextX(int x, int textLine); + int calcTextY(int y); + void updateTextSelection(int x, int y); + +public: + Graphics::Surface _screen; + int _cursorX, _cursorY; + bool _cursorState; + Common::Rect _consoleTextArea; + bool _cursorOff; + + bool _builtInFonts; + WageEngine *_engine; + + Patterns _patterns; + + bool _cursorDirty; + Common::Rect _cursorRect; + +private: + Graphics::Surface _console; + Menu *_menu; + Scene *_scene; + bool _sceneDirty; + bool _consoleDirty; + bool _bordersDirty; + bool _menuDirty; + + Common::StringArray _out; + Common::StringArray _lines; + uint _scrollPos; + int _consoleLineHeight; + uint _consoleNumLines; + bool _consoleFullRedraw; + + Common::Rect _sceneArea; + bool _sceneIsActive; + bool _cursorIsArrow; + + bool _inTextSelection; + int _selectionStartX; + int _selectionStartY; + int _selectionEndX; + int _selectionEndY; + + Common::String _clipboard; + Common::String _undobuffer; + + int _inputTextLineNum; +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/menu.cpp b/engines/wage/menu.cpp new file mode 100644 index 0000000000..b859867e9a --- /dev/null +++ b/engines/wage/menu.cpp @@ -0,0 +1,563 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/system.h" +#include "common/keyboard.h" + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/design.h" +#include "wage/gui.h" +#include "wage/menu.h" +#include "wage/world.h" + +namespace Wage { + +struct MenuSubItem { + Common::String text; + int action; + int style; + char shortcut; + bool enabled; + Common::Rect bbox; + + MenuSubItem(const char *t, int a, int s = 0, char sh = 0, bool e = true) : text(t), action(a), style(s), shortcut(sh), enabled(e) {} +}; + +typedef Common::Array<MenuSubItem *> SubItemArray; + +struct MenuItem { + Common::String name; + SubItemArray subitems; + Common::Rect bbox; + Common::Rect subbbox; + + MenuItem(const char *n) : name(n) {} +}; + +struct MenuData { + int menunum; + const char *title; + int action; + byte shortcut; + bool enabled; +} static const menuSubItems[] = { + { kMenuFile, "New", kMenuActionNew, 0, false }, + { kMenuFile, "Open...", kMenuActionOpen, 0, false }, + { kMenuFile, "Close", kMenuActionClose, 0, true }, + { kMenuFile, "Save", kMenuActionSave, 0, false }, + { kMenuFile, "Save as...", kMenuActionSaveAs, 0, true }, + { kMenuFile, "Revert", kMenuActionRevert, 0, false }, + { kMenuFile, "Quit", kMenuActionQuit, 0, true }, + + { kMenuEdit, "Undo", kMenuActionUndo, 'Z', false }, + { kMenuEdit, NULL, 0, 0, false }, + { kMenuEdit, "Cut", kMenuActionCut, 'K', false }, + { kMenuEdit, "Copy", kMenuActionCopy, 'C', false }, + { kMenuEdit, "Paste", kMenuActionPaste, 'V', false }, + { kMenuEdit, "Clear", kMenuActionClear, 'B', false }, + + { 0, NULL, 0, 0, false } +}; + +Menu::Menu(Gui *gui) : _gui(gui) { + assert(_gui->_engine); + assert(_gui->_engine->_world); + + _font = getMenuFont(); + + MenuItem *about = new MenuItem(_gui->_builtInFonts ? "\xa9" : "\xf0"); // (c) Symbol as the most resembling apple + _items.push_back(about); + _items[0]->subitems.push_back(new MenuSubItem(_gui->_engine->_world->getAboutMenuItemName(), kMenuActionAbout)); + + MenuItem *file = new MenuItem("File"); + _items.push_back(file); + + MenuItem *edit = new MenuItem("Edit"); + _items.push_back(edit); + + for (int i = 0; menuSubItems[i].menunum; i++) { + const MenuData *m = &menuSubItems[i]; + + _items[m->menunum]->subitems.push_back(new MenuSubItem(m->title, m->action, 0, m->shortcut, m->enabled)); + } + + _commands = new MenuItem(_gui->_engine->_world->_commandsMenuName.c_str()); + _items.push_back(_commands); + regenCommandsMenu(); + + if (!_gui->_engine->_world->_weaponMenuDisabled) { + _weapons = new MenuItem(_gui->_engine->_world->_weaponsMenuName.c_str()); + _items.push_back(_weapons); + + regenWeaponsMenu(); + } + + // Calculate menu dimensions + int y = 1; + int x = 18; + + for (uint i = 0; i < _items.size(); i++) { + int w = _font->getStringWidth(_items[i]->name); + + if (_items[i]->bbox.bottom == 0) { + _items[i]->bbox.left = x - kMenuLeftMargin; + _items[i]->bbox.top = y; + _items[i]->bbox.right = x + w + kMenuSpacing - kMenuLeftMargin; + _items[i]->bbox.bottom = y + _font->getFontHeight() + (_gui->_builtInFonts ? 3 : 2); + } + + calcMenuBounds(_items[i]); + + x += w + kMenuSpacing; + } + + _bbox.left = 0; + _bbox.top = 0; + _bbox.right = _gui->_screen.w - 1; + _bbox.bottom = kMenuHeight - 1; + + _menuActivated = false; + _activeItem = -1; + _activeSubItem = -1; + + _screenCopy.create(_gui->_screen.w, _gui->_screen.h, Graphics::PixelFormat::createFormatCLUT8()); + _tempSurface.create(_gui->_screen.w, _font->getFontHeight(), Graphics::PixelFormat::createFormatCLUT8()); +} + +Menu::~Menu() { + for (uint i = 0; i < _items.size(); i++) { + for (uint j = 0; j < _items[i]->subitems.size(); j++) + delete _items[i]->subitems[j]; + delete _items[i]; + } +} + +void Menu::regenCommandsMenu() { + for (uint j = 0; j < _commands->subitems.size(); j++) + delete _commands->subitems[j]; + + _commands->subitems.clear(); + + createCommandsMenu(_commands); + calcMenuBounds(_commands); +} + +void Menu::createCommandsMenu(MenuItem *menu) { + Common::String string(_gui->_engine->_world->_commandsMenu); + + Common::String item; + + for (uint i = 0; i < string.size(); i++) { + while(i < string.size() && string[i] != ';') // Read token + item += string[i++]; + + if (item == "(-") { + menu->subitems.push_back(new MenuSubItem(NULL, 0)); + } else { + bool enabled = true; + int style = 0; + char shortcut = 0; + const char *shortPtr = strrchr(item.c_str(), '/'); + if (shortPtr != NULL) { + if (strlen(shortPtr) == 2) { + shortcut = shortPtr[1]; + item.deleteLastChar(); + item.deleteLastChar(); + } else { + error("Unexpected shortcut: '%s', item '%s' in menu '%s'", shortPtr, item.c_str(), string.c_str()); + } + } + + while (item.size() >= 2 && item[item.size() - 2] == '<') { + char c = item.lastChar(); + if (c == 'B') { + style |= kFontStyleBold; + } else if (c == 'I') { + style |= kFontStyleItalic; + } else if (c == 'U') { + style |= kFontStyleUnderline; + } else if (c == 'O') { + style |= kFontStyleOutline; + } else if (c == 'S') { + style |= kFontStyleShadow; + } else if (c == 'C') { + style |= kFontStyleCondensed; + } else if (c == 'E') { + style |= kFontStyleExtended; + } + item.deleteLastChar(); + item.deleteLastChar(); + } + + Common::String tmpitem(item); + tmpitem.trim(); + if (tmpitem[0] == '(') { + enabled = false; + + for (uint j = 0; j < item.size(); j++) + if (item[j] == '(') { + item.deleteChar(j); + break; + } + } + + menu->subitems.push_back(new MenuSubItem(item.c_str(), kMenuActionCommand, style, shortcut, enabled)); + } + + item.clear(); + } +} + +void Menu::regenWeaponsMenu() { + if (_gui->_engine->_world->_weaponMenuDisabled) + return; + + for (uint j = 0; j < _weapons->subitems.size(); j++) + delete _weapons->subitems[j]; + + _weapons->subitems.clear(); + + createWeaponsMenu(_weapons); + calcMenuBounds(_weapons); +} + +void Menu::createWeaponsMenu(MenuItem *menu) { + Chr *player = _gui->_engine->_world->_player; + ObjArray *weapons = player->getWeapons(true); + + for (uint i = 0; i < weapons->size(); i++) { + Obj *obj = (*weapons)[i]; + if (obj->_type == Obj::REGULAR_WEAPON || + obj->_type == Obj::THROW_WEAPON || + obj->_type == Obj::MAGICAL_OBJECT) { + Common::String command(obj->_operativeVerb); + command += " "; + command += obj->_name; + + menu->subitems.push_back(new MenuSubItem(command.c_str(), kMenuActionCommand, 0, 0, true)); + } + } + delete weapons; + + if (menu->subitems.empty()) + menu->subitems.push_back(new MenuSubItem("You have no weapons", 0, 0, 0, false)); +} + +const Graphics::Font *Menu::getMenuFont() { + return _gui->getFont("Chicago-12", Graphics::FontManager::kBigGUIFont); +} + +const char *Menu::getAcceleratorString(MenuSubItem *item, const char *prefix) { + static char res[20]; + *res = 0; + + if (item->shortcut != 0) + sprintf(res, "%s%c%c", prefix, (_gui->_builtInFonts ? '^' : '\x11'), item->shortcut); + + return res; +} + +int Menu::calculateMenuWidth(MenuItem *menu) { + int maxWidth = 0; + for (uint i = 0; i < menu->subitems.size(); i++) { + MenuSubItem *item = menu->subitems[i]; + if (!item->text.empty()) { + Common::String text(item->text); + Common::String acceleratorText(getAcceleratorString(item, " ")); + if (!acceleratorText.empty()) { + text += acceleratorText; + } + + int width = _font->getStringWidth(text); + if (width > maxWidth) { + maxWidth = width; + } + } + } + return maxWidth; +} + +void Menu::calcMenuBounds(MenuItem *menu) { + // TODO: cache maxWidth + int maxWidth = calculateMenuWidth(menu); + int x1 = menu->bbox.left - 1; + int y1 = menu->bbox.bottom + 1; + int x2 = x1 + maxWidth + kMenuDropdownPadding * 2 - 4; + int y2 = y1 + menu->subitems.size() * kMenuDropdownItemHeight + 2; + + menu->subbbox.left = x1; + menu->subbbox.top = y1; + menu->subbbox.right = x2; + menu->subbbox.bottom = y2; +} + +void Menu::render() { + Common::Rect r(_bbox); + + Design::drawFilledRoundRect(&_gui->_screen, r, kDesktopArc, kColorWhite, _gui->_patterns, kPatternSolid); + r.top = 7; + Design::drawFilledRect(&_gui->_screen, r, kColorWhite, _gui->_patterns, kPatternSolid); + r.top = kMenuHeight - 1; + Design::drawFilledRect(&_gui->_screen, r, kColorBlack, _gui->_patterns, kPatternSolid); + + for (uint i = 0; i < _items.size(); i++) { + int color = kColorBlack; + MenuItem *it = _items[i]; + + if ((uint)_activeItem == i) { + Common::Rect hbox = it->bbox; + + hbox.left -= 1; + hbox.right += 2; + + Design::drawFilledRect(&_gui->_screen, hbox, kColorBlack, _gui->_patterns, kPatternSolid); + color = kColorWhite; + + if (!it->subitems.empty()) + renderSubmenu(it); + } + + _font->drawString(&_gui->_screen, it->name, it->bbox.left + kMenuLeftMargin, it->bbox.top + (_gui->_builtInFonts ? 2 : 1), it->bbox.width(), color); + } + + g_system->copyRectToScreen(_gui->_screen.getPixels(), _gui->_screen.pitch, 0, 0, _gui->_screen.w, kMenuHeight); +} + +void Menu::renderSubmenu(MenuItem *menu) { + Common::Rect *r = &menu->subbbox; + + if (r->width() == 0 || r->height() == 0) + return; + + Design::drawFilledRect(&_gui->_screen, *r, kColorWhite, _gui->_patterns, kPatternSolid); + Design::drawRect(&_gui->_screen, *r, 1, kColorBlack, _gui->_patterns, kPatternSolid); + Design::drawVLine(&_gui->_screen, r->right + 1, r->top + 3, r->bottom + 1, 1, kColorBlack, _gui->_patterns, kPatternSolid); + Design::drawHLine(&_gui->_screen, r->left + 3, r->right + 1, r->bottom + 1, 1, kColorBlack, _gui->_patterns, kPatternSolid); + + int x = r->left + kMenuDropdownPadding; + int y = r->top + 1; + for (uint i = 0; i < menu->subitems.size(); i++) { + Common::String text(menu->subitems[i]->text); + Common::String acceleratorText(getAcceleratorString(menu->subitems[i], "")); + int accelX = r->right - 25; + + int color = kColorBlack; + if (i == (uint)_activeSubItem && !text.empty() && menu->subitems[i]->enabled) { + color = kColorWhite; + Common::Rect trect(r->left, y - (_gui->_builtInFonts ? 1 : 0), r->right, y + _font->getFontHeight()); + + Design::drawFilledRect(&_gui->_screen, trect, kColorBlack, _gui->_patterns, kPatternSolid); + } + + if (!text.empty()) { + Graphics::Surface *s = &_gui->_screen; + int tx = x, ty = y; + + if (!menu->subitems[i]->enabled) { + s = &_tempSurface; + tx = 0; + ty = 0; + accelX -= x; + + _tempSurface.fillRect(Common::Rect(0, 0, _tempSurface.w, _tempSurface.h), kColorGreen); + } + + _font->drawString(s, text, tx, ty, r->width(), color); + + if (!acceleratorText.empty()) + _font->drawString(s, acceleratorText, accelX, ty, r->width(), color); + + if (!menu->subitems[i]->enabled) { + // I am lazy to extend drawString() with plotProc as a parameter, so + // fake it here + for (int ii = 0; ii < _tempSurface.h; ii++) { + const byte *src = (const byte *)_tempSurface.getBasePtr(0, ii); + byte *dst = (byte *)_gui->_screen.getBasePtr(x, y+ii); + byte pat = _gui->_patterns[kPatternCheckers2 - 1][ii % 8]; + for (int j = 0; j < r->width(); j++) { + if (*src != kColorGreen && (pat & (1 << (7 - (x + j) % 8)))) + *dst = *src; + src++; + dst++; + } + } + } + } else { // Delimiter + Design::drawHLine(&_gui->_screen, r->left + 1, r->right - 1, y + kMenuDropdownItemHeight / 2, 1, kColorBlack, _gui->_patterns, kPatternStripes); + } + + y += kMenuDropdownItemHeight; + } + + g_system->copyRectToScreen(_gui->_screen.getBasePtr(r->left, r->top), _gui->_screen.pitch, r->left, r->top, r->width() + 3, r->height() + 3); +} + +bool Menu::mouseClick(int x, int y) { + if (_bbox.contains(x, y)) { + if (!_menuActivated) + _screenCopy.copyFrom(_gui->_screen); + + for (uint i = 0; i < _items.size(); i++) + if (_items[i]->bbox.contains(x, y)) { + if ((uint)_activeItem == i) + return false; + + if (_activeItem != -1) { // Restore background + Common::Rect r(_items[_activeItem]->subbbox); + r.right += 3; + r.bottom += 3; + + _gui->_screen.copyRectToSurface(_screenCopy, r.left, r.top, r); + g_system->copyRectToScreen(_gui->_screen.getBasePtr(r.left, r.top), _gui->_screen.pitch, r.left, r.top, r.width() + 1, r.height() + 1); + } + + _activeItem = i; + _activeSubItem = -1; + _menuActivated = true; + + return true; + } + } else if (_menuActivated && _items[_activeItem]->subbbox.contains(x, y)) { + MenuItem *it = _items[_activeItem]; + int numSubItem = (y - it->subbbox.top) / kMenuDropdownItemHeight; + + if (numSubItem != _activeSubItem) { + _activeSubItem = numSubItem; + + renderSubmenu(_items[_activeItem]); + } + } else if (_menuActivated && _activeItem != -1) { + _activeSubItem = -1; + + renderSubmenu(_items[_activeItem]); + } + + return false; +} + +bool Menu::mouseMove(int x, int y) { + if (_menuActivated) + if (mouseClick(x, y)) + return true; + + return false; +} + +bool Menu::mouseRelease(int x, int y) { + if (_menuActivated) { + _menuActivated = false; + + if (_activeItem != -1 && _activeSubItem != -1 && _items[_activeItem]->subitems[_activeSubItem]->enabled) + executeCommand(_items[_activeItem]->subitems[_activeSubItem]); + + _activeItem = -1; + _activeSubItem = -1; + + return true; + } + + return false; +} + +void Menu::executeCommand(MenuSubItem *subitem) { + switch(subitem->action) { + case kMenuActionAbout: + case kMenuActionNew: + case kMenuActionOpen: + case kMenuActionClose: + case kMenuActionSave: + case kMenuActionSaveAs: + case kMenuActionRevert: + case kMenuActionQuit: + + case kMenuActionUndo: + _gui->actionUndo(); + break; + case kMenuActionCut: + _gui->actionCut(); + break; + case kMenuActionCopy: + _gui->actionCopy(); + break; + case kMenuActionPaste: + _gui->actionPaste(); + break; + case kMenuActionClear: + _gui->actionClear(); + break; + + case kMenuActionCommand: + _gui->_engine->processTurn(&subitem->text, NULL); + break; + + default: + warning("Unknown action: %d", subitem->action); + + } +} + +void Menu::processMenuShortCut(byte flags, uint16 ascii) { + ascii = tolower(ascii); + + if (flags & (Common::KBD_CTRL | Common::KBD_META)) { + for (uint i = 0; i < _items.size(); i++) + for (uint j = 0; j < _items[i]->subitems.size(); j++) + if (_items[i]->subitems[j]->enabled && tolower(_items[i]->subitems[j]->shortcut) == ascii) { + executeCommand(_items[i]->subitems[j]); + break; + } + } +} + +void Menu::enableCommand(int menunum, int action, bool state) { + for (uint i = 0; i < _items[menunum]->subitems.size(); i++) + if (_items[menunum]->subitems[i]->action == action) + _items[menunum]->subitems[i]->enabled = state; +} + +} // End of namespace Wage diff --git a/engines/wage/menu.h b/engines/wage/menu.h new file mode 100644 index 0000000000..cee7611664 --- /dev/null +++ b/engines/wage/menu.h @@ -0,0 +1,138 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_MENU_H +#define WAGE_MENU_H + +namespace Wage { + +struct MenuItem; +struct MenuSubItem; + +enum { + kFontStyleBold = 1, + kFontStyleItalic = 2, + kFontStyleUnderline = 4, + kFontStyleOutline = 8, + kFontStyleShadow = 16, + kFontStyleCondensed = 32, + kFontStyleExtended = 64 +}; + +enum { + kMenuAbout = 0, + kMenuFile = 1, + kMenuEdit = 2, + kMenuCommands = 3, + kMenuWeapons = 4 +}; + +enum { + kMenuActionAbout, + kMenuActionNew, + kMenuActionOpen, + kMenuActionClose, + kMenuActionSave, + kMenuActionSaveAs, + kMenuActionRevert, + kMenuActionQuit, + + kMenuActionUndo, + kMenuActionCut, + kMenuActionCopy, + kMenuActionPaste, + kMenuActionClear, + + kMenuActionCommand +}; + +class Menu { +public: + Menu(Gui *gui); + ~Menu(); + + void render(); + bool mouseClick(int x, int y); + bool mouseRelease(int x, int y); + bool mouseMove(int x, int y); + + void regenCommandsMenu(); + void regenWeaponsMenu(); + void processMenuShortCut(byte flags, uint16 ascii); + void enableCommand(int menunum, int action, bool state); + + bool _menuActivated; + Common::Rect _bbox; + +private: + Gui *_gui; + Graphics::Surface _screenCopy; + Graphics::Surface _tempSurface; + +private: + const Graphics::Font *getMenuFont(); + const char *getAcceleratorString(MenuSubItem *item, const char *prefix); + int calculateMenuWidth(MenuItem *menu); + void calcMenuBounds(MenuItem *menu); + void renderSubmenu(MenuItem *menu); + void createCommandsMenu(MenuItem *menu); + void createWeaponsMenu(MenuItem *menu); + void executeCommand(MenuSubItem *subitem); + + Common::Array<MenuItem *> _items; + MenuItem *_weapons; + MenuItem *_commands; + + const Graphics::Font *_font; + + int _activeItem; + int _activeSubItem; +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/module.mk b/engines/wage/module.mk new file mode 100644 index 0000000000..f91e84128d --- /dev/null +++ b/engines/wage/module.mk @@ -0,0 +1,27 @@ +MODULE := engines/wage + +MODULE_OBJS := \ + combat.o \ + design.o \ + detection.o \ + dialog.o \ + entities.o \ + gui.o \ + gui-console.o \ + menu.o \ + randomhat.o \ + script.o \ + util.o \ + wage.o \ + world.o + +MODULE_DIRS += \ + engines/wage + +# This module can be built as a plugin +ifeq ($(ENABLE_WAGE), DYNAMIC_PLUGIN) +PLUGIN := 1 +endif + +# Include common rules +include $(srcdir)/rules.mk diff --git a/engines/wage/randomhat.cpp b/engines/wage/randomhat.cpp new file mode 100644 index 0000000000..9371140398 --- /dev/null +++ b/engines/wage/randomhat.cpp @@ -0,0 +1,83 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/random.h" + +#include "common/hashmap.h" +#include "wage/randomhat.h" + +namespace Wage { + +void RandomHat::addTokens(int type, int count) { + _tokens.setVal(type, _tokens.getVal(type, 0) + count); +} + +int RandomHat::countTokens() { + int count = 0; + for (Common::HashMap<int, int>::const_iterator it = _tokens.begin(); it != _tokens.end(); ++it) + count += it->_value; + + return count; +} + +int RandomHat::drawToken() { + int total = countTokens(); + if (total > 0) { + int random = _rnd->getRandomNumber(total - 1); + int count = 0; + for (Common::HashMap<int, int>::iterator it = _tokens.begin(); it != _tokens.end(); ++it) { + if (random >= count && random < count + it->_value) { + it->_value--; + return it->_key; + } + count += it->_value; + } + } + return kTokNone; +} + +} // End of namespace Wage diff --git a/engines/wage/randomhat.h b/engines/wage/randomhat.h new file mode 100644 index 0000000000..254cd2ae8d --- /dev/null +++ b/engines/wage/randomhat.h @@ -0,0 +1,77 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_RANDOMHAT_H +#define WAGE_RANDOMHAT_H + +namespace Wage { + +enum { + kTokWeapons = -400, + kTokMagic = -300, + kTokRun = -200, + kTokOffer = -100, + kTokNone = -100000 +}; + +class RandomHat { +public: + RandomHat(Common::RandomSource *rnd) : _rnd(rnd) {} + + void addTokens(int type, int count); + int drawToken(); + +private: + Common::RandomSource *_rnd; + Common::HashMap<int, int> _tokens; + + int countTokens(); +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/script.cpp b/engines/wage/script.cpp new file mode 100644 index 0000000000..523f1e1a1b --- /dev/null +++ b/engines/wage/script.cpp @@ -0,0 +1,1146 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/script.h" +#include "wage/world.h" + +#include "common/stream.h" + +namespace Wage { + +Common::String Script::Operand::toString() { + switch(_type) { + case NUMBER: + return Common::String::format("%d", _value.number); + case STRING: + case TEXT_INPUT: + return *_value.string; + case OBJ: + return _value.obj->toString(); + case CHR: + return _value.chr->toString(); + case SCENE: + return _value.scene->toString(); + case CLICK_INPUT: + return _value.inputClick->toString(); + default: + error("Unhandled operand type: _type"); + } +} + +Script::Script(Common::SeekableReadStream *data) : _data(data) { + convertToText(); +} + +Script::~Script() { + for (uint i = 0; i < _scriptText.size(); i++) { + delete _scriptText[i]; + } + + delete _data; +} + +void Script::print() { + for (uint i = 0; i < _scriptText.size(); i++) { + debug(4, "%d [%04x]: %s", i, _scriptText[i]->offset, _scriptText[i]->line.c_str()); + } +} + +void Script::printLine(int offset) { + for (uint i = 0; i < _scriptText.size(); i++) + if (_scriptText[i]->offset >= offset) { + debug(4, "%d [%04x]: %s", i, _scriptText[i]->offset, _scriptText[i]->line.c_str()); + break; + } +} + +bool Script::execute(World *world, int loopCount, Common::String *inputText, Designed *inputClick, WageEngine *engine) { + _world = world; + _loopCount = loopCount; + _inputText = inputText; + _inputClick = inputClick; + _engine = engine; + _handled = false; + Common::String input; + + if (inputText) + input = *inputText; + + _data->seek(12); + while (_data->pos() < _data->size()) { + printLine(_data->pos()); + + byte command = _data->readByte(); + + switch(command) { + case 0x80: // IF + processIf(); + break; + case 0x87: // EXIT + debug(6, "exit at offset %d", _data->pos() - 1); + + return true; + case 0x89: // MOVE + { + Scene *currentScene = _world->_player->_currentScene; + processMove(); + if (_world->_player->_currentScene != currentScene) + return true; + break; + } + case 0x8B: // PRINT + { + Operand *op = readOperand(); + // TODO check op type is string or number, or something good... + _handled = true; + _engine->appendText(op->toString().c_str()); + delete op; + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + break; + } + case 0x8C: // SOUND + { + Operand *op = readOperand(); + // TODO check op type is string. + _handled = true; + _engine->playSound(op->toString()); + delete op; + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + break; + } + case 0x8E: // LET + processLet(); + break; + case 0x95: // MENU + { + Operand *op = readStringOperand(); // allows empty menu + // TODO check op type is string. + _engine->setMenu(op->toString()); + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + } + case 0x88: // END + break; + default: + debug(0, "Unknown opcode: %d", _data->pos()); + } + } + + if (_world->_globalScript != this) { + debug(1, "Executing global script..."); + bool globalHandled = _world->_globalScript->execute(_world, _loopCount, &input, _inputClick, _engine); + if (globalHandled) + _handled = true; + } else if (!input.empty()) { + if (input.contains("north")) { + _handled = _engine->handleMoveCommand(NORTH, "north"); + } else if (input.contains("east")) { + _handled = _engine->handleMoveCommand(EAST, "east"); + } else if (input.contains("south")) { + _handled = _engine->handleMoveCommand(SOUTH, "south"); + } else if (input.contains("west")) { + _handled = _engine->handleMoveCommand(WEST, "west"); + } else if (input.hasPrefix("take ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[5]); + } else if (input.hasPrefix("get ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[4]); + } else if (input.hasPrefix("pick up ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[8]); + } else if (input.hasPrefix("drop ")) { + _handled = _engine->handleDropCommand(&input.c_str()[5]); + } else if (input.hasPrefix("aim ")) { + _handled = _engine->handleAimCommand(&input.c_str()[4]); + } else if (input.hasPrefix("wear ")) { + _handled = _engine->handleWearCommand(&input.c_str()[5]); + } else if (input.hasPrefix("put on ")) { + _handled = _engine->handleWearCommand(&input.c_str()[7]); + } else if (input.hasPrefix("offer ")) { + _handled = _engine->handleOfferCommand(&input.c_str()[6]); + } else if (input.contains("look")) { + _handled = _engine->handleLookCommand(); + } else if (input.contains("inventory")) { + _handled = _engine->handleInventoryCommand(); + } else if (input.contains("status")) { + _handled = _engine->handleStatusCommand(); + } else if (input.contains("rest") || input.equals("wait")) { + _handled = _engine->handleRestCommand(); + } else if (_engine->getOffer() != NULL && input.contains("accept")) { + _handled = _engine->handleAcceptCommand(); + } else { + Chr *player = _world->_player; + ObjArray *weapons = player->getWeapons(true); + for (ObjArray::const_iterator weapon = weapons->begin(); weapon != weapons->end(); ++weapon) { + if (_engine->tryAttack(*weapon, input)) { + _handled = _engine->handleAttack(*weapon); + break; + } + } + + delete weapons; + } + // TODO: weapons, offer, etc... + } else if (_inputClick->_classType == OBJ) { + Obj *obj = (Obj *)_inputClick; + if (obj->_type != Obj::IMMOBILE_OBJECT) { + _engine->takeObj(obj); + } else { + _engine->appendText(obj->_clickMessage.c_str()); + } + + _handled = true; + } + + return _handled; +} + +Script::Operand *Script::readOperand() { + byte operandType = _data->readByte(); + + debug(7, "%x: readOperand: 0x%x", _data->pos(), operandType); + + Context *cont = &_world->_player->_context; + switch (operandType) { + case 0xA0: // TEXT$ + return new Operand(_inputText, TEXT_INPUT); + case 0xA1: + return new Operand(_inputClick, CLICK_INPUT); + case 0xC0: // STORAGE@ + return new Operand(_world->_storageScene, SCENE); + case 0xC1: // SCENE@ + return new Operand(_world->_player->_currentScene, SCENE); + case 0xC2: // PLAYER@ + return new Operand(_world->_player, CHR); + case 0xC3: // MONSTER@ + return new Operand(_engine->getMonster(), CHR); + case 0xC4: // RANDOMSCN@ + return new Operand(_world->_orderedScenes[_engine->_rnd->getRandomNumber(_world->_orderedScenes.size())], SCENE); + case 0xC5: // RANDOMCHR@ + return new Operand(_world->_orderedChrs[_engine->_rnd->getRandomNumber(_world->_orderedChrs.size())], CHR); + case 0xC6: // RANDOMOBJ@ + return new Operand(_world->_orderedObjs[_engine->_rnd->getRandomNumber(_world->_orderedObjs.size())], OBJ); + case 0xB0: // VISITS# + return new Operand(cont->_visits, NUMBER); + case 0xB1: // RANDOM# for Star Trek, but VISITS# for some other games? + return new Operand(1 + _engine->_rnd->getRandomNumber(100), NUMBER); + case 0xB5: // RANDOM# // A random number between 1 and 100. + return new Operand(1 + _engine->_rnd->getRandomNumber(100), NUMBER); + case 0xB2: // LOOP# + return new Operand(_loopCount, NUMBER); + case 0xB3: // VICTORY# + return new Operand(cont->_kills, NUMBER); + case 0xB4: // BADCOPY# + return new Operand(0, NUMBER); // \?\?\?? + case 0xFF: + { + // user variable + int value = _data->readByte(); + + // TODO: Verify that we're using the right index. + return new Operand(cont->_userVariables[value - 1], NUMBER); + } + case 0xD0: + return new Operand(cont->_statVariables[PHYS_STR_BAS], NUMBER); + case 0xD1: + return new Operand(cont->_statVariables[PHYS_HIT_BAS], NUMBER); + case 0xD2: + return new Operand(cont->_statVariables[PHYS_ARM_BAS], NUMBER); + case 0xD3: + return new Operand(cont->_statVariables[PHYS_ACC_BAS], NUMBER); + case 0xD4: + return new Operand(cont->_statVariables[SPIR_STR_BAS], NUMBER); + case 0xD5: + return new Operand(cont->_statVariables[SPIR_HIT_BAS], NUMBER); + case 0xD6: + return new Operand(cont->_statVariables[SPIR_ARM_BAS], NUMBER); + case 0xD7: + return new Operand(cont->_statVariables[SPIR_ACC_BAS], NUMBER); + case 0xD8: + return new Operand(cont->_statVariables[PHYS_SPE_BAS], NUMBER); + case 0xE0: + return new Operand(cont->_statVariables[PHYS_STR_CUR], NUMBER); + case 0xE1: + return new Operand(cont->_statVariables[PHYS_HIT_CUR], NUMBER); + case 0xE2: + return new Operand(cont->_statVariables[PHYS_ARM_CUR], NUMBER); + case 0xE3: + return new Operand(cont->_statVariables[PHYS_ACC_CUR], NUMBER); + case 0xE4: + return new Operand(cont->_statVariables[SPIR_STR_CUR], NUMBER); + case 0xE5: + return new Operand(cont->_statVariables[SPIR_HIT_CUR], NUMBER); + case 0xE6: + return new Operand(cont->_statVariables[SPIR_ARM_CUR], NUMBER); + case 0xE7: + return new Operand(cont->_statVariables[SPIR_ACC_CUR], NUMBER); + case 0xE8: + return new Operand(cont->_statVariables[PHYS_SPE_CUR], NUMBER); + default: + if (operandType >= 0x20 && operandType < 0x80) { + _data->seek(-1, SEEK_CUR); + return readStringOperand(); + } else { + debug("Dunno what %x is (index=%d)!\n", operandType, _data->pos()-1); + } + return NULL; + } +} + +void Script::assign(byte operandType, int uservar, uint16 value) { + Context *cont = &_world->_player->_context; + + switch (operandType) { + case 0xFF: + cont->_userVariables[uservar - 1] = value; + break; + case 0xD0: + cont->_statVariables[PHYS_STR_BAS] = value; + break; + case 0xD1: + cont->_statVariables[PHYS_HIT_BAS] = value; + break; + case 0xD2: + cont->_statVariables[PHYS_ARM_BAS] = value; + break; + case 0xD3: + cont->_statVariables[PHYS_ACC_BAS] = value; + break; + case 0xD4: + cont->_statVariables[SPIR_STR_BAS] = value; + break; + case 0xD5: + cont->_statVariables[SPIR_HIT_BAS] = value; + break; + case 0xD6: + cont->_statVariables[SPIR_ARM_BAS] = value; + break; + case 0xD7: + cont->_statVariables[SPIR_ACC_BAS] = value; + break; + case 0xD8: + cont->_statVariables[PHYS_SPE_BAS] = value; + break; + case 0xE0: + cont->_statVariables[PHYS_STR_CUR] = value; + break; + case 0xE1: + cont->_statVariables[PHYS_HIT_CUR] = value; + break; + case 0xE2: + cont->_statVariables[PHYS_ARM_CUR] = value; + break; + case 0xE3: + cont->_statVariables[PHYS_ACC_CUR] = value; + break; + case 0xE4: + cont->_statVariables[SPIR_STR_CUR] = value; + break; + case 0xE5: + cont->_statVariables[SPIR_HIT_CUR] = value; + break; + case 0xE6: + cont->_statVariables[SPIR_ARM_CUR] = value; + break; + case 0xE7: + cont->_statVariables[SPIR_ACC_CUR] = value; + break; + case 0xE8: + cont->_statVariables[PHYS_SPE_CUR] = value; + break; + default: + debug("No idea what I'm supposed to assign! (%x at %d)!\n", operandType, _data->pos()-1); + } +} + +Script::Operand *Script::readStringOperand() { + Common::String *str; + bool allDigits = true; + + str = new Common::String(); + + while (true) { + byte c = _data->readByte(); + if (c >= 0x20 && c < 0x80) + *str += c; + else + break; + if (c < '0' || c > '9') + allDigits = false; + } + _data->seek(-1, SEEK_CUR); + + if (allDigits && !str->empty()) { + int r = atol(str->c_str()); + delete str; + + return new Operand(r, NUMBER); + } else { + // TODO: This string could be a room name or something like that. + return new Operand(str, STRING); + } +} + +const char *Script::readOperator() { + byte cmd = _data->readByte(); + + debug(7, "readOperator: 0x%x", cmd); + switch (cmd) { + case 0x81: + return "="; + case 0x82: + return "<"; + case 0x83: + return ">"; + case 0x8f: + return "+"; + case 0x90: + return "-"; + case 0x91: + return "*"; + case 0x92: + return "/"; + case 0x93: + return "=="; + case 0x94: + return ">>"; + case 0xfd: + return ";"; + default: + warning("UNKNOWN OP %x", cmd); + } + return NULL; +} + +void Script::processIf() { + int logicalOp = 0; // 0 => initial, 1 => and, 2 => or + bool result = true; + bool done = false; + + do { + Operand *lhs = readOperand(); + const char *op = readOperator(); + Operand *rhs = readOperand(); + + bool condResult = eval(lhs, op, rhs); + + delete lhs; + delete rhs; + + if (logicalOp == 1) { + result = (result && condResult); + } else if (logicalOp == 2) { + result = (result || condResult); + } else { // logicalOp == 0 + result = condResult; + } + + byte logical = _data->readByte(); + + if (logical == 0x84) { + logicalOp = 1; // and + } else if (logical == 0x85) { + logicalOp = 2; // or + } else if (logical == 0xFE) { + done = true; // then + } + } while (!done); + + if (result == false) { + skipBlock(); + } +} + +void Script::skipIf() { + do { + Operand *lhs = readOperand(); + readOperator(); + Operand *rhs = readOperand(); + + delete lhs; + delete rhs; + } while (_data->readByte() != 0xFE); +} + +void Script::skipBlock() { + int nesting = 1; + + while (true) { + byte op = _data->readByte(); + + if (_data->eos()) + return; + + if (op == 0x80) { // IF + nesting++; + skipIf(); + } else if (op == 0x88 || op == 0x87) { // END or EXIT + nesting--; + if (nesting == 0) { + return; + } + } else switch (op) { + case 0x8B: // PRINT + case 0x8C: // SOUND + case 0x8E: // LET + case 0x95: // MENU + while (_data->readByte() != 0xFD) + ; + } + } +} + +enum { + kCompEqNumNum, + kCompEqObjScene, + kCompEqChrScene, + kCompEqObjChr, + kCompEqChrChr, + kCompEqSceneScene, + kCompEqStringTextInput, + kCompEqTextInputString, + kCompEqNumberTextInput, + kCompEqTextInputNumber, + kCompLtNumNum, + kCompLtStringTextInput, + kCompLtTextInputString, + kCompLtObjChr, + kCompLtChrObj, + kCompLtObjScene, + kCompGtNumNum, + kCompGtStringString, + kCompGtChrScene, + kMoveObjChr, + kMoveObjScene, + kMoveChrScene +}; + +struct Comparator { + char op; + OperandType o1; + OperandType o2; + int cmp; +} static comparators[] = { + { '=', NUMBER, NUMBER, kCompEqNumNum }, + { '=', OBJ, SCENE, kCompEqObjScene }, + { '=', CHR, SCENE, kCompEqChrScene }, + { '=', OBJ, CHR, kCompEqObjChr }, + { '=', CHR, CHR, kCompEqChrChr }, + { '=', SCENE, SCENE, kCompEqSceneScene }, + { '=', STRING, TEXT_INPUT, kCompEqStringTextInput }, + { '=', TEXT_INPUT, STRING, kCompEqTextInputString }, + { '=', NUMBER, TEXT_INPUT, kCompEqNumberTextInput }, + { '=', TEXT_INPUT, NUMBER, kCompEqTextInputNumber }, + + { '<', NUMBER, NUMBER, kCompLtNumNum }, + { '<', STRING, TEXT_INPUT, kCompLtStringTextInput }, + { '<', TEXT_INPUT, STRING, kCompLtTextInputString }, + { '<', OBJ, CHR, kCompLtObjChr }, + { '<', CHR, OBJ, kCompLtChrObj }, + { '<', OBJ, SCENE, kCompLtObjScene }, + { '<', CHR, CHR, kCompEqChrChr }, // Same logic as = + { '<', SCENE, SCENE, kCompEqSceneScene }, + + { '>', NUMBER, NUMBER, kCompGtNumNum }, + { '>', TEXT_INPUT, STRING, kCompLtTextInputString }, // Same logic as < + //FIXME: this prevents the below cases from working due to exact + //matches taking precedence over conversions... + //{ '>', STRING, STRING, kCompGtStringString }, // Same logic as < + { '>', OBJ, CHR, kCompLtObjChr }, // Same logic as < + { '>', OBJ, SCENE, kCompLtObjScene }, // Same logic as < + { '>', CHR, SCENE, kCompGtChrScene }, + + { 'M', OBJ, CHR, kMoveObjChr }, + { 'M', OBJ, SCENE, kMoveObjScene }, + { 'M', CHR, SCENE, kMoveChrScene }, + { 0, OBJ, OBJ, 0 } +}; + +bool Script::compare(Operand *o1, Operand *o2, int comparator) { + switch(comparator) { + case kCompEqNumNum: + return o1->_value.number == o2->_value.number; + case kCompEqObjScene: + for (ObjList::const_iterator it = o2->_value.scene->_objs.begin(); it != o2->_value.scene->_objs.end(); ++it) + if (*it == o1->_value.obj) + return true; + return false; + case kCompEqChrScene: + for (ChrList::const_iterator it = o2->_value.scene->_chrs.begin(); it != o2->_value.scene->_chrs.end(); ++it) + if (*it == o1->_value.chr) + return true; + return false; + case kCompEqObjChr: + for (ObjArray::const_iterator it = o2->_value.chr->_inventory.begin(); it != o2->_value.chr->_inventory.end(); ++it) + if (*it == o1->_value.obj) + return true; + return false; + case kCompEqChrChr: + return o1->_value.chr == o2->_value.chr; + case kCompEqSceneScene: + return o1->_value.scene == o2->_value.scene; + case kCompEqStringTextInput: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(*o1->_value.string); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompEqTextInputString: + return compare(o2, o1, kCompEqStringTextInput); + case kCompEqNumberTextInput: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(o1->toString()); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompEqTextInputNumber: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(o2->toString()); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompLtNumNum: + return o1->_value.number < o2->_value.number; + case kCompLtStringTextInput: + return !compare(o1, o2, kCompEqStringTextInput); + case kCompLtTextInputString: + return !compare(o2, o1, kCompEqStringTextInput); + case kCompLtObjChr: + return o1->_value.obj->_currentOwner != o2->_value.chr; + case kCompLtChrObj: + return compare(o2, o1, kCompLtObjChr); + case kCompLtObjScene: + return o1->_value.obj->_currentScene != o2->_value.scene; + case kCompGtNumNum: + return o1->_value.number > o2->_value.number; + case kCompGtStringString: + return o1->_value.string == o2->_value.string; + case kCompGtChrScene: + return (o1->_value.chr != NULL && o1->_value.chr->_currentScene != o2->_value.scene); + case kMoveObjChr: + if (o1->_value.obj->_currentOwner != o2->_value.chr) { + _world->move(o1->_value.obj, o2->_value.chr); + _handled = true; // TODO: Is this correct? + } + break; + case kMoveObjScene: + if (o1->_value.obj->_currentScene != o2->_value.scene) { + _world->move(o1->_value.obj, o2->_value.scene); + // Note: This shouldn't call setHandled() - see + // Sultan's Palace 'Food and Drink' scene. + } + break; + case kMoveChrScene: + _world->move(o1->_value.chr, o2->_value.scene); + _handled = true; // TODO: Is this correct? + break; + } + + return false; +} + +bool Script::evaluatePair(Operand *lhs, const char *op, Operand *rhs) { + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type && comparators[cmp].o2 == rhs->_type) + return compare(lhs, rhs, comparators[cmp].cmp); + } + + // Now, try partial matches. + Operand *c1, *c2; + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type && + (c2 = convertOperand(rhs, comparators[cmp].o2)) != NULL) { + bool res = compare(lhs, c2, comparators[cmp].cmp); + delete c2; + return res; + } else if (comparators[cmp].o2 == rhs->_type && + (c1 = convertOperand(lhs, comparators[cmp].o1)) != NULL) { + bool res = compare(c1, rhs, comparators[cmp].cmp); + delete c1; + return res; + } + } + + // Now, try double conversion. + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type || comparators[cmp].o2 == rhs->_type) + continue; + + if ((c1 = convertOperand(lhs, comparators[cmp].o1)) != NULL) { + if ((c2 = convertOperand(rhs, comparators[cmp].o2)) != NULL) { + bool res = compare(c1, c2, comparators[cmp].cmp); + delete c1; + delete c2; + return res; + } + delete c1; + } + } + + warning("UNHANDLED CASE: [lhs=%d/%s, op=%s rhs=%d/%s]", + lhs->_type, lhs->toString().c_str(), op, rhs->_type, rhs->toString().c_str()); + + + return false; +} + +bool Script::eval(Operand *lhs, const char *op, Operand *rhs) { + bool result = false; + + if (lhs->_type == CLICK_INPUT || rhs->_type == CLICK_INPUT) { + return evalClickCondition(lhs, op, rhs); + } else if (!strcmp(op, "==") || !strcmp(op, ">>")) { + // TODO: check if >> can be used for click inputs and if == can be used for other things + // exact string match + if (lhs->_type == TEXT_INPUT) { + if ((rhs->_type != STRING && rhs->_type != NUMBER) || _inputText == NULL) { + result = false; + } else { + result = _inputText->equalsIgnoreCase(rhs->toString()); + } + } else if (rhs->_type == TEXT_INPUT) { + if ((lhs->_type != STRING && lhs->_type != NUMBER) || _inputText == NULL) { + result = false; + } else { + result = _inputText->equalsIgnoreCase(lhs->toString()); + } + } else { + error("UNHANDLED CASE: [lhs=%d/%s, rhs=%d/%s]", + lhs->_type, lhs->toString().c_str(), rhs->_type, rhs->toString().c_str()); + } + if (!strcmp(op, ">>")) { + result = !result; + } + + return result; + } else { + return evaluatePair(lhs, op, rhs); + } + + return false; +} + +Script::Operand *Script::convertOperand(Operand *operand, int type) { + if (operand->_type == type) + error("Incorrect conversion to type %d", type); + + if (type == SCENE) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key(operand->toString()); + key.toLowercase(); + if (_world->_scenes.contains(key)) + return new Operand(_world->_scenes[key], SCENE); + } + } else if (type == OBJ) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key = operand->toString(); + key.toLowercase(); + if (_world->_objs.contains(key)) + return new Operand(_world->_objs[key], OBJ); + } else if (operand->_type == CLICK_INPUT) { + if (_inputClick->_classType == OBJ) + return new Operand(_inputClick, OBJ); + } + } else if (type == CHR) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key = operand->toString(); + key.toLowercase(); + if (_world->_chrs.contains(key)) + return new Operand(_world->_chrs[key], CHR); + } else if (operand->_type == CLICK_INPUT) { + if (_inputClick->_classType == CHR) + return new Operand(_inputClick, CHR); + } + } + + return NULL; +} + +bool Script::evalClickEquality(Operand *lhs, Operand *rhs, bool partialMatch) { + bool result = false; + if (lhs->_value.obj == NULL || rhs->_value.obj == NULL) { + result = false; + } else if (lhs->_value.obj == rhs->_value.obj) { + result = true; + } else if (rhs->_type == STRING) { + Common::String str = rhs->toString(); + str.toLowercase(); + + debug(9, "evalClickEquality(%s, %s, %d)", lhs->_value.designed->_name.c_str(), rhs->_value.designed->_name.c_str(), partialMatch); + debug(9, "l: %d r: %d (ch: %d ob: %d)", lhs->_type, rhs->_type, CHR, OBJ); + debug(9, "class: %d", lhs->_value.inputClick->_classType); + + if (lhs->_value.inputClick->_classType == CHR || lhs->_value.inputClick->_classType == OBJ) { + Common::String name = lhs->_value.designed->_name; + name.toLowercase(); + + if (partialMatch) + result = name.contains(str); + else + result = name.equals(str); + } + + debug(9, "result: %d", result); + } + return result; +} + +bool Script::evalClickCondition(Operand *lhs, const char *op, Operand *rhs) { + // TODO: check if >> can be used for click inputs + if (strcmp(op, "==") && strcmp(op, "=") && strcmp(op, "<") && strcmp(op, ">")) { + error("Unknown operation '%s' for Script::evalClickCondition", op); + } + + bool partialMatch = strcmp(op, "=="); + bool result; + if (lhs->_type == CLICK_INPUT) { + result = evalClickEquality(lhs, rhs, partialMatch); + } else { + result = evalClickEquality(rhs, lhs, partialMatch); + } + if (!strcmp(op, "<") || !strcmp(op, ">")) { + // CLICK$<FOO only matches if there was a click + if (_inputClick == NULL) { + result = false; + } else { + result = !result; + } + } + return result; +} + +void Script::processMove() { + Operand *what = readOperand(); + byte skip = _data->readByte(); + if (skip != 0x8a) + error("Incorrect operator for MOVE: %02x", skip); + + Operand *to = readOperand(); + + skip = _data->readByte(); + if (skip != 0xfd) + error("No end for MOVE: %02x", skip); + + evaluatePair(what, "M", to); + + delete what; + delete to; +} + +void Script::processLet() { + const char *lastOp = NULL; + int16 result = 0; + int operandType = _data->readByte(); + int uservar = 0; + + if (operandType == 0xff) { + uservar = _data->readByte(); + } + + byte eq = _data->readByte(); // skip "=" operator + + debug(7, "processLet: 0x%x, uservar: 0x%x, eq: 0x%x", operandType, uservar, eq); + + do { + Operand *operand = readOperand(); + // TODO assert that value is NUMBER + int16 value = operand->_value.number; + delete operand; + if (lastOp != NULL) { + if (lastOp[0] == '+') + result += value; + else if (lastOp[0] == '-') + result -= value; + else if (lastOp[0] == '/') + result = (int16)(value == 0 ? 0 : result / value); + else if (lastOp[0] == '*') + result *= value; + } else { + result = value; + } + lastOp = readOperator(); + + if (lastOp[0] == ';') + break; + } while (true); + //System.out.println("processLet " + buildStringFromOffset(oldIndex - 1, index - oldIndex + 1) + "}"); + + assign(operandType, uservar, result); +} + +enum { + BLOCK_START, + BLOCK_END, + STATEMENT, + OPERATOR, + OPCODE +}; + +struct Mapping { + const char *cmd; + int type; +} static const mapping[] = { + { "IF{", STATEMENT }, // 0x80 + { "=", OPERATOR }, + { "<", OPERATOR }, + { ">", OPERATOR }, + { "}AND{", OPCODE }, + { "}OR{", OPCODE }, + { "\?\?\?(0x86)", OPCODE }, + { "EXIT\n", BLOCK_END }, + { "END\n", BLOCK_END }, // 0x88 + { "MOVE{", STATEMENT }, + { "}TO{", OPCODE }, + { "PRINT{", STATEMENT }, + { "SOUND{", STATEMENT }, + { "\?\?\?(0x8d)", OPCODE }, + { "LET{", STATEMENT }, + { "+", OPERATOR }, + { "-", OPERATOR }, // 0x90 + { "*", OPERATOR }, + { "/", OPERATOR }, + { "==", OPERATOR }, + { ">>", OPERATOR }, + { "MENU{", STATEMENT }, + { "\?\?\?(0x96)", OPCODE }, + { "\?\?\?(0x97)", OPCODE }, + { "\?\?\?(0x98)", OPCODE }, // 0x98 + { "\?\?\?(0x99)", OPCODE }, + { "\?\?\?(0x9a)", OPCODE }, + { "\?\?\?(0x9b)", OPCODE }, + { "\?\?\?(0x9c)", OPCODE }, + { "\?\?\?(0x9d)", OPCODE }, + { "\?\?\?(0x9e)", OPCODE }, + { "\?\?\?(0x9f)", OPCODE }, + { "TEXT$", OPCODE }, // 0xa0 + { "CLICK$", OPCODE }, + { "\?\?\?(0xa2)", OPCODE }, + { "\?\?\?(0xa3)", OPCODE }, + { "\?\?\?(0xa4)", OPCODE }, + { "\?\?\?(0xa5)", OPCODE }, + { "\?\?\?(0xa6)", OPCODE }, + { "\?\?\?(0xa7)", OPCODE }, + { "\?\?\?(0xa8)", OPCODE }, // 0xa8 + { "\?\?\?(0xa9)", OPCODE }, + { "\?\?\?(0xaa)", OPCODE }, + { "\?\?\?(0xab)", OPCODE }, + { "\?\?\?(0xac)", OPCODE }, + { "\?\?\?(0xad)", OPCODE }, + { "\?\?\?(0xae)", OPCODE }, + { "\?\?\?(0xaf)", OPCODE }, + { "VISITS#", OPCODE }, // 0xb0 // The number of scenes the player has visited, including repeated visits. + { "RANDOM#", OPCODE }, // RANDOM# for Star Trek, but VISITS# for some other games? + { "LOOP#", OPCODE }, // The number of commands the player has given in the current scene. + { "VICTORY#", OPCODE }, // The number of characters killed. + { "BADCOPY#", OPCODE }, + { "RANDOM#", OPCODE }, // A random number between 1 and 100. + { "\?\?\?(0xb6)", OPCODE }, + { "\?\?\?(0xb7)", OPCODE }, + { "\?\?\?(0xb8)", OPCODE }, // 0xb8 + { "\?\?\?(0xb9)", OPCODE }, + { "\?\?\?(0xba)", OPCODE }, + { "\?\?\?(0xbb)", OPCODE }, + { "\?\?\?(0xbc)", OPCODE }, + { "\?\?\?(0xbd)", OPCODE }, + { "\?\?\?(0xbe)", OPCODE }, + { "\?\?\?(0xbf)", OPCODE }, + { "STORAGE@", OPCODE }, // 0xc0 + { "SCENE@", OPCODE }, + { "PLAYER@", OPCODE }, + { "MONSTER@", OPCODE }, + { "RANDOMSCN@", OPCODE }, + { "RANDOMCHR@", OPCODE }, + { "RANDOMOBJ@", OPCODE }, + { "\?\?\?(0xc7)", OPCODE }, + { "\?\?\?(0xc8)", OPCODE }, // 0xc8 + { "\?\?\?(0xc9)", OPCODE }, + { "\?\?\?(0xca)", OPCODE }, + { "\?\?\?(0xcb)", OPCODE }, + { "\?\?\?(0xcc)", OPCODE }, + { "\?\?\?(0xcd)", OPCODE }, + { "\?\?\?(0xce)", OPCODE }, + { "\?\?\?(0xcf)", OPCODE }, + { "PHYS.STR.BAS#", OPCODE }, // 0xd0 + { "PHYS.HIT.BAS#", OPCODE }, + { "PHYS.ARM.BAS#", OPCODE }, + { "PHYS.ACC.BAS#", OPCODE }, + { "SPIR.STR.BAS#", OPCODE }, + { "SPIR.HIT.BAS#", OPCODE }, + { "SPIR.ARM.BAS#", OPCODE }, + { "SPIR.ACC.BAS#", OPCODE }, + { "PHYS.SPE.BAS#", OPCODE }, // 0xd8 + { "\?\?\?(0xd9)", OPCODE }, + { "\?\?\?(0xda)", OPCODE }, + { "\?\?\?(0xdb)", OPCODE }, + { "\?\?\?(0xdc)", OPCODE }, + { "\?\?\?(0xdd)", OPCODE }, + { "\?\?\?(0xde)", OPCODE }, + { "\?\?\?(0xdf)", OPCODE }, + { "PHYS.STR.CUR#", OPCODE }, // 0xe0 + { "PHYS.HIT.CUR#", OPCODE }, + { "PHYS.ARM.CUR#", OPCODE }, + { "PHYS.ACC.CUR#", OPCODE }, + { "SPIR.STR.CUR#", OPCODE }, + { "SPIR.HIT.CUR#", OPCODE }, + { "SPIR.ARM.CUR#", OPCODE }, + { "SPIR.ACC.CUR#", OPCODE }, + { "PHYS.SPE.CUR#", OPCODE }, // 0xe8 + { "\?\?\?(0xe9)", OPCODE }, + { "\?\?\?(0xea)", OPCODE }, + { "\?\?\?(0xeb)", OPCODE }, + { "\?\?\?(0xec)", OPCODE }, + { "\?\?\?(0xed)", OPCODE }, + { "\?\?\?(0xee)", OPCODE }, + { "\?\?\?(0xef)", OPCODE }, + { "\?\?\?(0xf0)", OPCODE }, + { "\?\?\?(0xf1)", OPCODE }, + { "\?\?\?(0xf2)", OPCODE }, + { "\?\?\?(0xf3)", OPCODE }, + { "\?\?\?(0xf4)", OPCODE }, + { "\?\?\?(0xf5)", OPCODE }, + { "\?\?\?(0xf6)", OPCODE }, + { "\?\?\?(0xf7)", OPCODE }, + { "\?\?\?(0xf8)", OPCODE }, // 0xa8 + { "\?\?\?(0xf9)", OPCODE }, + { "\?\?\?(0xfa)", OPCODE }, + { "\?\?\?(0xfb)", OPCODE }, + { "\?\?\?(0xfc)", OPCODE }, + { "}\n", OPCODE }, + { "}THEN\n", BLOCK_START }, + { "\?\?\?(0xff)", OPCODE } // Uservar +}; + +void Script::convertToText() { + _data->seek(12); + + int indentLevel = 0; + ScriptText *scr = new ScriptText; + scr->offset = _data->pos(); + + while(true) { + int c = _data->readByte(); + + if (_data->eos()) + break; + + if (c < 0x80) { + if (c < 0x20) + error("Unknown code 0x%02x at %d", c, _data->pos()); + + do { + scr->line += c; + c = _data->readByte(); + } while (c < 0x80); + + _data->seek(-1, SEEK_CUR); + } else if (c == 0xff) { + int value = _data->readByte(); + value -= 1; + scr->line += (char)('A' + (value / 9)); + scr->line += (char)('0' + (value % 9) + 1); + scr->line += '#'; + } else { + const char *cmd = mapping[c - 0x80].cmd; + int type = mapping[c - 0x80].type; + + if (type == STATEMENT) { + for (int i = 0; i < indentLevel; i++) + scr->line += ' '; + } else if (type == BLOCK_START) { + indentLevel += 2; + } else if (type == BLOCK_END) { + indentLevel -= 2; + for (int i = 0; i < indentLevel; i++) + scr->line += ' '; + } + + scr->line += cmd; + + if (strchr(cmd, '\n')) { + scr->line.deleteLastChar(); + + _scriptText.push_back(scr); + + scr = new ScriptText; + scr->offset = _data->pos(); + } + } + } + + if (!scr->line.empty()) + _scriptText.push_back(scr); + else + delete scr; +} + +} // End of namespace Wage diff --git a/engines/wage/script.h b/engines/wage/script.h new file mode 100644 index 0000000000..1a7d07d29a --- /dev/null +++ b/engines/wage/script.h @@ -0,0 +1,153 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_SCRIPT_H +#define WAGE_SCRIPT_H + +namespace Wage { + +class Script { +public: + Script(Common::SeekableReadStream *data); + ~Script(); + +private: + Common::SeekableReadStream *_data; + + WageEngine *_engine; + World *_world; + int _loopCount; + Common::String *_inputText; + Designed *_inputClick; + bool _handled; + + class Operand { + public: + union { + Obj *obj; + Chr *chr; + Designed *designed; + Scene *scene; + int16 number; + Common::String *string; + Designed *inputClick; + } _value; + OperandType _type; + + Operand(Obj *value, OperandType type) { + _value.obj = value; + _type = type; + } + + Operand(Chr *value, OperandType type) { + _value.chr = value; + _type = type; + } + + Operand(Scene *value, OperandType type) { + _value.scene = value; + _type = type; + } + + Operand(int value, OperandType type) { + _value.number = value; + _type = type; + } + + Operand(Common::String *value, OperandType type) { + _value.string = value; + _type = type; + } + + Operand(Designed *value, OperandType type) { + _value.inputClick = value; + _type = type; + } + + ~Operand() { + if (_type == STRING) + delete _value.string; + } + + Common::String toString(); + }; + + struct ScriptText { + int offset; + Common::String line; + }; + +public: + void print(); + void printLine(int offset); + bool execute(World *world, int loopCount, Common::String *inputText, Designed *inputClick, WageEngine *engine); + +private: + Operand *readOperand(); + Operand *readStringOperand(); + const char *readOperator(); + void processIf(); + void skipBlock(); + void skipIf(); + bool compare(Operand *o1, Operand *o2, int comparator); + bool eval(Operand *lhs, const char *op, Operand *rhs); + bool evaluatePair(Operand *lhs, const char *op, Operand *rhs); + Operand *convertOperand(Operand *operand, int type); + bool evalClickCondition(Operand *lhs, const char *op, Operand *rhs); + bool evalClickEquality(Operand *lhs, Operand *rhs, bool partialMatch); + void processMove(); + void processLet(); + + void assign(byte operandType, int uservar, uint16 value); + + Common::Array<ScriptText *> _scriptText; + void convertToText(); +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/util.cpp b/engines/wage/util.cpp new file mode 100644 index 0000000000..1b3dfc9452 --- /dev/null +++ b/engines/wage/util.cpp @@ -0,0 +1,123 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/stream.h" + +#include "wage/wage.h" + +namespace Wage { + +Common::String readPascalString(Common::SeekableReadStream *in) { + Common::String s; + char *buf; + int len; + int i; + + len = in->readByte(); + buf = (char *)malloc(len + 1); + for (i = 0; i < len; i++) { + buf[i] = in->readByte(); + if (buf[i] == 0x0d) + buf[i] = '\n'; + } + + buf[i] = 0; + + s = buf; + free(buf); + + return s; +} + +Common::Rect *readRect(Common::SeekableReadStream *in) { + int x1, y1, x2, y2; + + y1 = in->readUint16BE(); + x1 = in->readUint16BE(); + y2 = in->readUint16BE() + 4; + x2 = in->readUint16BE() + 4; + + return new Common::Rect(x1, y1, x2, y2); +} + +const char *getIndefiniteArticle(const Common::String &word) { + switch (word[0]) { + case 'a': case 'A': + case 'e': case 'E': + case 'i': case 'I': + case 'o': case 'O': + case 'u': case 'U': + return "an "; + } + return "a "; +} + +enum { + GENDER_MALE = 0, + GENDER_FEMALE = 1, + GENDER_NEUTRAL = 2 +}; + +const char *prependGenderSpecificPronoun(int gender) { + if (gender == GENDER_MALE) + return "his "; + else if (gender == GENDER_FEMALE) + return "her "; + else + return "its "; +} + +const char *getGenderSpecificPronoun(int gender, bool capitalize) { + if (gender == GENDER_MALE) + return capitalize ? "He" : "he"; + else if (gender == GENDER_FEMALE) + return capitalize ? "She" : "she"; + else + return capitalize ? "It" : "it"; +} + +} // End of namespace Wage diff --git a/engines/wage/wage.cpp b/engines/wage/wage.cpp new file mode 100644 index 0000000000..1c13a31f66 --- /dev/null +++ b/engines/wage/wage.cpp @@ -0,0 +1,504 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/debug-channels.h" + +#include "engines/engine.h" +#include "engines/util.h" +#include "gui/EventRecorder.h" + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/gui.h" +#include "wage/dialog.h" +#include "wage/script.h" +#include "wage/world.h" + +namespace Wage { + +WageEngine::WageEngine(OSystem *syst, const ADGameDescription *desc) : Engine(syst), _gameDescription(desc) { + _rnd = new Common::RandomSource("wage"); + + _aim = -1; + _opponentAim = -1; + _temporarilyHidden = false; + _isGameOver = false; + _monster = NULL; + _running = NULL; + _lastScene = NULL; + + _commandWasQuick = false; + + _shouldQuit = false; + + debug("WageEngine::WageEngine()"); +} + +WageEngine::~WageEngine() { + debug("WageEngine::~WageEngine()"); + + DebugMan.clearAllDebugChannels(); + delete _world; + delete _resManager; + delete _gui; + delete _rnd; + delete _console; +} + +Common::Error WageEngine::run() { + initGraphics(512, 342, true); + + // Create debugger console. It requires GFX to be initialized + _console = new Console(this); + + debug("WageEngine::init"); + + // Your main event loop should be (invoked from) here. + _resManager = new Common::MacResManager(); + _resManager->open(getGameFile()); + + _world = new World(this); + + if (!_world->loadWorld(_resManager)) + return Common::kNoGameDataFoundError; + + _gui = new Gui(this); + + _temporarilyHidden = true; + performInitialSetup(); + Common::String input("look"); + processTurn(&input, NULL); + _temporarilyHidden = false; + + _shouldQuit = false; + + while (!_shouldQuit) { + processEvents(); + + _gui->draw(); + g_system->updateScreen(); + g_system->delayMillis(50); + } + + //_world->_orderedScenes[1]->_design->paint(&screen, _world->_patterns, false); + //_world->_objs["frank.1"]->_design->setBounds(&r); + //_world->_objs["frank.1"]->_design->paint(&screen, _world->_patterns, false); + //_world->_scenes["temple of the holy mackeral"]->_design->setBounds(&r); + //_world->_scenes["temple of the holy mackeral"]->_design->paint(&screen, _world->_patterns, false); + //_world->_scenes["tower level 3"]->_design->setBounds(&r); + //_world->_scenes["tower level 3"]->_design->paint(&screen, _world->_patterns, false); + + return Common::kNoError; +} + +void WageEngine::processEvents() { + Common::Event event; + + while (_eventMan->pollEvent(event)) { + switch (event.type) { + case Common::EVENT_QUIT: + if (saveDialog()) + _shouldQuit = true; + break; + case Common::EVENT_MOUSEMOVE: + _gui->mouseMove(event.mouse.x, event.mouse.y); + break; + case Common::EVENT_LBUTTONDOWN: + _gui->mouseDown(event.mouse.x, event.mouse.y); + break; + case Common::EVENT_LBUTTONUP: + { + Designed *obj = _gui->mouseUp(event.mouse.x, event.mouse.y); + if (obj != NULL) + processTurn(NULL, obj); + } + break; + case Common::EVENT_KEYDOWN: + switch (event.kbd.keycode) { + case Common::KEYCODE_BACKSPACE: + if (!_inputText.empty()) { + _inputText.deleteLastChar(); + _gui->drawInput(); + } + break; + + case Common::KEYCODE_RETURN: + if (_inputText.empty()) + break; + + processTurn(&_inputText, NULL); + _inputText.clear(); + _gui->appendText(""); + _gui->disableUndo(); + break; + + default: + if (event.kbd.flags & (Common::KBD_ALT | Common::KBD_CTRL | Common::KBD_META)) { + if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7f) { + _gui->processMenuShortCut(event.kbd.flags, event.kbd.ascii); + } + break; + } + + if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7f) { + _inputText += (char)event.kbd.ascii; + _gui->drawInput(); + } + + break; + } + break; + + default: + break; + } + } +} + +void WageEngine::playSound(Common::String soundName) { + warning("STUB: WageEngine::playSound(%s)", soundName.c_str()); +} + +void WageEngine::setMenu(Common::String menu) { + _world->_commandsMenu = menu; + + _gui->regenCommandsMenu(); +} + +void WageEngine::appendText(const char *str) { + _gui->appendText(str); + + _inputText.clear(); +} + +void WageEngine::gameOver() { + DialogButtonArray buttons; + + buttons.push_back(new DialogButton("OK", 66, 67, 68, 28)); + + Dialog gameOver(_gui, 199, _world->_gameOverMessage->c_str(), &buttons, 0); + + gameOver.run(); + + doClose(); +} + +bool WageEngine::saveDialog() { + DialogButtonArray buttons; + + buttons.push_back(new DialogButton("No", 19, 67, 68, 28)); + buttons.push_back(new DialogButton("Yes", 112, 67, 68, 28)); + buttons.push_back(new DialogButton("Cancel", 205, 67, 68, 28)); + + Dialog save(_gui, 291, "Save changes before closing?", &buttons, 1); + + int button = save.run(); + + if (button == 2) // Cancel + return false; + + if (button == 1) + saveGame(); + + doClose(); + + return true; +} + +void WageEngine::saveGame() { + warning("STUB: saveGame()"); +} + +void WageEngine::performInitialSetup() { + debug(5, "Resetting Objs: %d", _world->_orderedObjs.size()); + for (uint i = 0; i < _world->_orderedObjs.size() - 1; i++) + _world->move(_world->_orderedObjs[i], _world->_storageScene, true); + + _world->move(_world->_orderedObjs[_world->_orderedObjs.size() - 1], _world->_storageScene); + + debug(5, "Resetting Chrs: %d", _world->_orderedChrs.size()); + for (uint i = 0; i < _world->_orderedChrs.size() - 1; i++) + _world->move(_world->_orderedChrs[i], _world->_storageScene, true); + + _world->move(_world->_orderedChrs[_world->_orderedChrs.size() - 1], _world->_storageScene); + + debug(5, "Resetting Owners: %d", _world->_orderedObjs.size()); + for (uint i = 0; i < _world->_orderedObjs.size(); i++) { + Obj *obj = _world->_orderedObjs[i]; + if (!obj->_sceneOrOwner.equalsIgnoreCase(STORAGESCENE)) { + Common::String location = obj->_sceneOrOwner; + location.toLowercase(); + if (_world->_scenes.contains(location)) { + _world->move(obj, _world->_scenes[location]); + } else { + if (!_world->_chrs.contains(location)) { + // Note: PLAYER@ is not a valid target here. + warning("Couldn't move %s to %s", obj->_name.c_str(), obj->_sceneOrOwner.c_str()); + } else { + // TODO: Add check for max items. + _world->move(obj, _world->_chrs[location]); + } + } + } + } + + bool playerPlaced = false; + for (uint i = 0; i < _world->_orderedChrs.size(); i++) { + Chr *chr = _world->_orderedChrs[i]; + if (!chr->_initialScene.equalsIgnoreCase(STORAGESCENE)) { + Common::String key = chr->_initialScene; + key.toLowercase(); + if (_world->_scenes.contains(key) && _world->_scenes[key] != NULL) { + _world->move(chr, _world->_scenes[key]); + + if (chr->_playerCharacter) + debug(0, "Initial scene: %s", key.c_str()); + } else { + _world->move(chr, _world->getRandomScene()); + } + if (chr->_playerCharacter) { + playerPlaced = true; + } + } + chr->wearObjs(); + } + if (!playerPlaced) { + _world->move(_world->_player, _world->getRandomScene()); + } +} + +void WageEngine::doClose() { + warning("STUB: doClose()"); +} + +Scene *WageEngine::getSceneByName(Common::String &location) { + Scene *scene; + if (location.equals("random@")) { + scene = _world->getRandomScene(); + } else { + scene = _world->_scenes[location]; + } + return scene; +} + +void WageEngine::onMove(Designed *what, Designed *from, Designed *to) { + Chr *player = _world->_player; + Scene *currentScene = player->_currentScene; + if (currentScene == _world->_storageScene && !_temporarilyHidden) { + if (!_isGameOver) { + _isGameOver = true; + gameOver(); + } + return; + } + + if (from == currentScene || to == currentScene || + (what->_classType == CHR && ((Chr *)what)->_currentScene == currentScene) || + (what->_classType == OBJ && ((Obj *)what)->_currentScene == currentScene)) + _gui->setSceneDirty(); + + if ((from == player || to == player) && !_temporarilyHidden) + _gui->regenWeaponsMenu(); + + if (what != player && what->_classType == CHR) { + Chr *chr = (Chr *)what; + if (to == _world->_storageScene) { + int returnTo = chr->_returnTo; + if (returnTo != Chr::RETURN_TO_STORAGE) { + Common::String returnToSceneName; + if (returnTo == Chr::RETURN_TO_INITIAL_SCENE) { + returnToSceneName = chr->_initialScene; + returnToSceneName.toLowercase(); + } else { + returnToSceneName = "random@"; + } + Scene *scene = getSceneByName(returnToSceneName); + if (scene != NULL && scene != _world->_storageScene) { + _world->move(chr, scene); + // To avoid sleeping twice, return if the above move command would cause a sleep. + if (scene == currentScene) + return; + } + } + } else if (to == player->_currentScene) { + if (getMonster() == NULL) { + _monster = chr; + encounter(player, chr); + } + } + } + if (!_temporarilyHidden) { + if (to == currentScene || from == currentScene) { + redrawScene(); + g_system->delayMillis(100); + } + } +} + +void WageEngine::redrawScene() { + Scene *currentScene = _world->_player->_currentScene; + + if (currentScene != NULL) { + bool firstTime = (_lastScene != currentScene); + + updateSoundTimerForScene(currentScene, firstTime); + } +} + +void WageEngine::updateSoundTimerForScene(Scene *scene, bool firstTime) { + //warning("STUB: WageEngine::updateSoundTimerForScene()"); +} + +void WageEngine::processTurnInternal(Common::String *textInput, Designed *clickInput) { + Scene *playerScene = _world->_player->_currentScene; + if (playerScene == _world->_storageScene) + return; + + bool shouldEncounter = false; + + if (playerScene != _lastScene) { + _loopCount = 0; + _lastScene = playerScene; + _monster = NULL; + _running = NULL; + _offer = NULL; + + for (ChrList::const_iterator it = playerScene->_chrs.begin(); it != playerScene->_chrs.end(); ++it) { + if (!(*it)->_playerCharacter) { + _monster = *it; + shouldEncounter = true; + break; + } + } + } + + bool monsterWasNull = (_monster == NULL); + Script *script = playerScene->_script != NULL ? playerScene->_script : _world->_globalScript; + bool handled = script->execute(_world, _loopCount++, textInput, clickInput, this); + + playerScene = _world->_player->_currentScene; + + if (playerScene == _world->_storageScene) + return; + + if (playerScene != _lastScene) { + _temporarilyHidden = true; + _gui->clearOutput(); + regen(); + Common::String input("look"); + processTurnInternal(&input, NULL); + redrawScene(); + _temporarilyHidden = false; + } else if (_loopCount == 1) { + redrawScene(); + if (shouldEncounter && getMonster() != NULL) { + encounter(_world->_player, _monster); + } + } else if (textInput != NULL && !handled) { + if (monsterWasNull && getMonster() != NULL) + return; + + const char *rant = _rnd->getRandomNumber(1) ? "What?" : "Huh?"; + + appendText(rant); + _commandWasQuick = true; + } +} + +void WageEngine::processTurn(Common::String *textInput, Designed *clickInput) { + _commandWasQuick = false; + Scene *prevScene = _world->_player->_currentScene; + Chr *prevMonster = getMonster(); + Common::String input; + + if (textInput) + input = *textInput; + + input.toLowercase(); + if (input.equals("e")) + input = "east"; + else if (input.equals("w")) + input = "west"; + else if (input.equals("n")) + input = "north"; + else if (input.equals("s")) + input = "south"; + + processTurnInternal(&input, clickInput); + Scene *playerScene = _world->_player->_currentScene; + + if (prevScene != playerScene && playerScene != _world->_storageScene) { + if (prevMonster != NULL) { + bool followed = false; + if (getMonster() == NULL) { + // TODO: adjacent scenes doesn't contain up/down etc... verify that monsters can't follow these... + if (_world->scenesAreConnected(playerScene, prevMonster->_currentScene)) { + int chance = _rnd->getRandomNumber(255); + followed = (chance < prevMonster->_followsOpponent); + } + } + + char buf[512]; + + if (followed) { + snprintf(buf, 512, "%s%s follows you.", prevMonster->getDefiniteArticle(true), prevMonster->_name.c_str()); + appendText(buf); + + _world->move(prevMonster, playerScene); + } else { + snprintf(buf, 512, "You escape %s%s.", prevMonster->getDefiniteArticle(false), prevMonster->_name.c_str()); + appendText(buf); + } + } + } + if (!_commandWasQuick && getMonster() != NULL) { + performCombatAction(getMonster(), _world->_player); + } +} + + +} // End of namespace Wage diff --git a/engines/wage/wage.h b/engines/wage/wage.h new file mode 100644 index 0000000000..6905fdc530 --- /dev/null +++ b/engines/wage/wage.h @@ -0,0 +1,234 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_WAGE_H +#define WAGE_WAGE_H + +#include "engines/engine.h" +#include "common/debug.h" +#include "gui/debugger.h" +#include "common/endian.h" +#include "common/rect.h" +#include "common/macresman.h" +#include "common/random.h" + +struct ADGameDescription; + +namespace Wage { + +class Console; +class Chr; +class Designed; +class Dialog; +class Gui; +class Obj; +class Scene; +class World; + +typedef Common::Array<Obj *> ObjArray; +typedef Common::Array<Chr *> ChrArray; +typedef Common::List<Obj *> ObjList; +typedef Common::List<Chr *> ChrList; + +enum OperandType { + OBJ = 0, + CHR = 1, + SCENE = 2, + NUMBER = 3, + STRING = 4, + CLICK_INPUT = 5, + TEXT_INPUT = 6, + UNKNOWN = 100 +}; + +enum Directions { + NORTH = 0, + SOUTH = 1, + EAST = 2, + WEST = 3 +}; + +// our engine debug levels +enum { + kWageDebugExample = 1 << 0, + kWageDebugExample2 = 1 << 1 + // next new level must be 1 << 2 (4) + // the current limitation is 32 debug levels (1 << 31 is the last one) +}; + +enum { + kColorBlack = 0, + kColorGray = 1, + kColorWhite = 2, + kColorGreen = 3 +}; + +Common::String readPascalString(Common::SeekableReadStream *in); +Common::Rect *readRect(Common::SeekableReadStream *in); +const char *getIndefiniteArticle(const Common::String &word); +const char *prependGenderSpecificPronoun(int gender); +const char *getGenderSpecificPronoun(int gender, bool capitalize); + + +typedef Common::Array<byte *> Patterns; + +class WageEngine : public Engine { + friend class Dialog; +public: + WageEngine(OSystem *syst, const ADGameDescription *gameDesc); + ~WageEngine(); + + virtual bool hasFeature(EngineFeature f) const; + + virtual Common::Error run(); + + bool canLoadGameStateCurrently(); + bool canSaveGameStateCurrently(); + + const char *getGameFile() const; + void processTurn(Common::String *textInput, Designed *clickInput); + void regen(); + +private: + bool loadWorld(Common::MacResManager *resMan); + void performInitialSetup(); + void wearObjs(Chr *chr); + void processTurnInternal(Common::String *textInput, Designed *clickInput); + void performCombatAction(Chr *npc, Chr *player); + int getValidMoveDirections(Chr *npc); + void performAttack(Chr *attacker, Chr *victim, Obj *weapon); + void performMagic(Chr *attacker, Chr *victim, Obj *magicalObject); + void performMove(Chr *chr, int validMoves); + void performOffer(Chr *attacker, Chr *victim); + void performTake(Chr *npc, Obj *obj); + void decrementUses(Obj *obj); + bool attackHit(Chr *attacker, Chr *victim, Obj *weapon, int targetIndex); + void performHealingMagic(Chr *chr, Obj *magicalObject); + + void doClose(); + void updateSoundTimerForScene(Scene *scene, bool firstTime); + +public: + void takeObj(Obj *obj); + + bool handleMoveCommand(Directions dir, const char *dirName); + bool handleLookCommand(); + Common::String *getGroundItemsList(Scene *scene); + void appendObjNames(Common::String &str, const ObjArray &objs); + bool handleInventoryCommand(); + bool handleStatusCommand(); + bool handleRestCommand(); + bool handleAcceptCommand(); + + bool handleTakeCommand(const char *target); + bool handleDropCommand(const char *target); + bool handleAimCommand(const char *target); + bool handleWearCommand(const char *target); + bool handleOfferCommand(const char *target); + + void wearObj(Obj *o, int pos); + + bool tryAttack(const Obj *weapon, const Common::String &input); + bool handleAttack(Obj *weapon); + + void printPlayerCondition(Chr *player); + const char *getPercentMessage(double percent); + +public: + Common::RandomSource *_rnd; + + Gui *_gui; + World *_world; + + Scene *_lastScene; + int _loopCount; + int _turn; + Chr *_monster; + Chr *_running; + Obj *_offer; + int _aim; + int _opponentAim; + bool _temporarilyHidden; + bool _isGameOver; + bool _commandWasQuick; + + Common::String _inputText; + + void playSound(Common::String soundName); + void setMenu(Common::String soundName); + void appendText(const char *str); + void gameOver(); + bool saveDialog(); + Obj *getOffer(); + Chr *getMonster(); + void processEvents(); + Scene *getSceneByName(Common::String &location); + void onMove(Designed *what, Designed *from, Designed *to); + void encounter(Chr *player, Chr *chr); + void redrawScene(); + void saveGame(); + +private: + Console *_console; + + const ADGameDescription *_gameDescription; + + Common::MacResManager *_resManager; + + bool _shouldQuit; +}; + +// Example console class +class Console : public GUI::Debugger { +public: + Console(WageEngine *vm) {} + virtual ~Console(void) {} +}; + +} // End of namespace Wage + +#endif diff --git a/engines/wage/world.cpp b/engines/wage/world.cpp new file mode 100644 index 0000000000..a387d95c4a --- /dev/null +++ b/engines/wage/world.cpp @@ -0,0 +1,510 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "common/file.h" + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/script.h" +#include "wage/world.h" + +namespace Wage { + +World::World(WageEngine *engine) { + _storageScene = new Scene; + _storageScene->_name = STORAGESCENE; + + _orderedScenes.push_back(_storageScene); + _scenes[STORAGESCENE] = _storageScene; + + _gameOverMessage = nullptr; + _saveBeforeQuitMessage = nullptr; + _saveBeforeCloseMessage = nullptr; + _revertMessage = nullptr; + + _engine = engine; +} + +World::~World() { + for (uint i = 0; i < _orderedObjs.size(); i++) + delete _orderedObjs[i]; + + for (uint i = 0; i < _orderedChrs.size(); i++) + delete _orderedChrs[i]; + + for (uint i = 0; i < _orderedSounds.size(); i++) + delete _orderedSounds[i]; + + for (uint i = 0; i < _orderedScenes.size(); i++) + delete _orderedScenes[i]; + + for (uint i = 0; i < _patterns.size(); i++) + free(_patterns[i]); + + delete _globalScript; + + delete _gameOverMessage; + delete _saveBeforeQuitMessage; + delete _saveBeforeCloseMessage; + delete _revertMessage; + +} + +bool World::loadWorld(Common::MacResManager *resMan) { + Common::MacResIDArray resArray; + Common::SeekableReadStream *res; + Common::MacResIDArray::const_iterator iter; + + if ((resArray = resMan->getResIDArray(MKTAG('G','C','O','D'))).size() == 0) + return false; + + // Load global script + res = resMan->getResource(MKTAG('G','C','O','D'), resArray[0]); + _globalScript = new Script(res); + + // TODO: read creator + + // Load main configuration + if ((resArray = resMan->getResIDArray(MKTAG('V','E','R','S'))).size() == 0) + return false; + + _name = resMan->getBaseFileName(); + + if (resArray.size() > 1) + warning("Too many VERS resources"); + + if (!resArray.empty()) { + debug(3, "Loading version info"); + + res = resMan->getResource(MKTAG('V','E','R','S'), resArray[0]); + + res->skip(10); + byte b = res->readByte(); + _weaponMenuDisabled = (b != 0); + if (b != 0 && b != 1) + error("Unexpected value for weapons menu"); + + res->skip(3); + _aboutMessage = readPascalString(res); + + if (!scumm_stricmp(resMan->getBaseFileName().c_str(), "Scepters")) + res->skip(1); // ???? + + _soundLibrary1 = readPascalString(res); + _soundLibrary2 = readPascalString(res); + + delete res; + } + + Common::String *message; + if ((message = loadStringFromDITL(resMan, 2910, 1)) != NULL) { + message->trim(); + debug(2, "_gameOverMessage: %s", message->c_str()); + _gameOverMessage = message; + } + if ((message = loadStringFromDITL(resMan, 2480, 3)) != NULL) { + message->trim(); + debug(2, "_saveBeforeQuitMessage: %s", message->c_str()); + _saveBeforeQuitMessage = message; + } + if ((message = loadStringFromDITL(resMan, 2490, 3)) != NULL) { + message->trim(); + debug(2, "_saveBeforeCloseMessage: %s", message->c_str()); + _saveBeforeCloseMessage = message; + } + if ((message = loadStringFromDITL(resMan, 2940, 2)) != NULL) { + message->trim(); + debug(2, "_revertMessage: %s", message->c_str()); + _revertMessage = message; + } + + // Load scenes + resArray = resMan->getResIDArray(MKTAG('A','S','C','N')); + debug(3, "Loading %d scenes", resArray.size()); + + for (iter = resArray.begin(); iter != resArray.end(); ++iter) { + res = resMan->getResource(MKTAG('A','S','C','N'), *iter); + Scene *scene = new Scene(resMan->getResName(MKTAG('A','S','C','N'), *iter), res); + + res = resMan->getResource(MKTAG('A','C','O','D'), *iter); + if (res != NULL) + scene->_script = new Script(res); + + res = resMan->getResource(MKTAG('A','T','X','T'), *iter); + if (res != NULL) { + scene->_textBounds = readRect(res); + scene->_fontType = res->readUint16BE(); + scene->_fontSize = res->readUint16BE(); + + Common::String text; + while (res->pos() < res->size()) { + char c = res->readByte(); + if (c == 0x0d) + c = '\n'; + text += c; + } + scene->_text = text; + + delete res; + } + addScene(scene); + } + + // Load Objects + resArray = resMan->getResIDArray(MKTAG('A','O','B','J')); + debug(3, "Loading %d objects", resArray.size()); + + for (iter = resArray.begin(); iter != resArray.end(); ++iter) { + res = resMan->getResource(MKTAG('A','O','B','J'), *iter); + addObj(new Obj(resMan->getResName(MKTAG('A','O','B','J'), *iter), res)); + } + + // Load Characters + resArray = resMan->getResIDArray(MKTAG('A','C','H','R')); + debug(3, "Loading %d characters", resArray.size()); + + for (iter = resArray.begin(); iter != resArray.end(); ++iter) { + res = resMan->getResource(MKTAG('A','C','H','R'), *iter); + Chr *chr = new Chr(resMan->getResName(MKTAG('A','C','H','R'), *iter), res); + + addChr(chr); + // TODO: What if there's more than one player character? + if (chr->_playerCharacter) + _player = chr; + } + + // Load Sounds + resArray = resMan->getResIDArray(MKTAG('A','S','N','D')); + debug(3, "Loading %d sounds", resArray.size()); + + for (iter = resArray.begin(); iter != resArray.end(); ++iter) { + res = resMan->getResource(MKTAG('A','S','N','D'), *iter); + addSound(new Sound(resMan->getResName(MKTAG('A','S','N','D'), *iter), res)); + } + + if (!_soundLibrary1.empty()) { + loadExternalSounds(_soundLibrary1); + } + if (!_soundLibrary2.empty()) { + loadExternalSounds(_soundLibrary2); + } + + // Load Patterns + res = resMan->getResource(MKTAG('P','A','T','#'), 900); + if (res != NULL) { + int count = res->readUint16BE(); + debug(3, "Loading %d patterns", count); + + for (int i = 0; i < count; i++) { + byte *pattern = (byte *)malloc(8); + + res->read(pattern, 8); + _patterns.push_back(pattern); + } + + delete res; + } else { + /* Enchanted Scepters did not use the PAT# resource for the textures. */ + res = resMan->getResource(MKTAG('C','O','D','E'), 1); + if (res != NULL) { + res->skip(0x55ac); + for (int i = 0; i < 29; i++) { + byte *pattern = (byte *)malloc(8); + + res->read(pattern, 8); + _patterns.push_back(pattern); + } + } + delete res; + } + + res = resMan->getResource(MKTAG('M','E','N','U'), 2001); + if (res != NULL) { + Common::StringArray *menu = readMenu(res); + _aboutMenuItemName.clear(); + Common::String string = menu->operator[](1); + + for (uint i = 0; i < string.size() && string[i] != ';'; i++) // Read token + _aboutMenuItemName += string[i]; + + delete res; + } + res = resMan->getResource(MKTAG('M','E','N','U'), 2004); + if (res != NULL) { + Common::StringArray *menu = readMenu(res); + _commandsMenuName = menu->operator[](0); + _commandsMenu = menu->operator[](1); + delete menu; + delete res; + } + res = resMan->getResource(MKTAG('M','E','N','U'), 2005); + if (res != NULL) { + Common::StringArray *menu = readMenu(res); + _weaponsMenuName = menu->operator[](0); + delete menu; + delete res; + } + // TODO: Read Apple menu and get the name of that menu item.. + + // store global info in state object for use with save/load actions + //world.setCurrentState(initialState); // pass off the state object to the world + + return true; +} + +Common::StringArray *World::readMenu(Common::SeekableReadStream *res) { + res->skip(10); + int enableFlags = res->readUint32BE(); + Common::String menuName = readPascalString(res); + Common::String menuItem = readPascalString(res); + int menuItemNumber = 1; + Common::String menu; + byte itemData[4]; + + while (!menuItem.empty()) { + if (!menu.empty()) { + menu += ';'; + } + if ((enableFlags & (1 << menuItemNumber)) == 0) { + menu += '('; + } + menu += menuItem; + res->read(itemData, 4); + static const char styles[] = {'B', 'I', 'U', 'O', 'S', 'C', 'E', 0}; + for (int i = 0; styles[i] != 0; i++) { + if ((itemData[3] & (1 << i)) != 0) { + menu += '<'; + menu += styles[i]; + } + } + if (itemData[1] != 0) { + menu += '/'; + menu += (char)itemData[1]; + } + menuItem = readPascalString(res); + menuItemNumber++; + } + + Common::StringArray *result = new Common::StringArray; + result->push_back(menuName); + result->push_back(menu); + + debug(4, "menuName: %s", menuName.c_str()); + debug(4, "menu: %s", menu.c_str()); + + return result; +} + +void World::loadExternalSounds(Common::String fname) { + Common::File in; + + in.open(fname); + if (!in.isOpen()) { + warning("Cannot load sound file <%s>", fname.c_str()); + return; + } + in.close(); + + Common::MacResManager *resMan; + resMan = new Common::MacResManager(); + resMan->open(fname); + + Common::MacResIDArray resArray; + Common::SeekableReadStream *res; + Common::MacResIDArray::const_iterator iter; + + resArray = resMan->getResIDArray(MKTAG('A','S','N','D')); + for (iter = resArray.begin(); iter != resArray.end(); ++iter) { + res = resMan->getResource(MKTAG('A','S','N','D'), *iter); + addSound(new Sound(resMan->getResName(MKTAG('A','S','N','D'), *iter), res)); + } +} + +Common::String *World::loadStringFromDITL(Common::MacResManager *resMan, int resourceId, int itemIndex) { + Common::SeekableReadStream *res = resMan->getResource(MKTAG('D','I','T','L'), resourceId); + if (res) { + int itemCount = res->readSint16BE(); + for (int i = 0; i <= itemCount; i++) { + // int placeholder; short rect[4]; byte flags; pstring str; + res->skip(13); + Common::String message = readPascalString(res); + if (i == itemIndex) { + Common::String *msg = new Common::String(message); + delete res; + return msg; + } + } + + delete res; + } + + return NULL; +} + +static bool invComparator(const Obj *l, const Obj *r) { + return l->_index < r->_index; +} + +void World::move(Obj *obj, Chr *chr) { + if (obj == NULL) + return; + + Designed *from = obj->removeFromCharOrScene(); + obj->_currentOwner = chr; + chr->_inventory.push_back(obj); + + Common::sort(chr->_inventory.begin(), chr->_inventory.end(), invComparator); + + _engine->onMove(obj, from, chr); +} + +static bool objComparator(const Obj *o1, const Obj *o2) { + bool o1Immobile = (o1->_type == Obj::IMMOBILE_OBJECT); + bool o2Immobile = (o2->_type == Obj::IMMOBILE_OBJECT); + if (o1Immobile == o2Immobile) { + return o1->_index - o2->_index; + } + return o1Immobile; +} + +void World::move(Obj *obj, Scene *scene, bool skipSort) { + if (obj == NULL) + return; + + Designed *from = obj->removeFromCharOrScene(); + obj->_currentScene = scene; + scene->_objs.push_back(obj); + + if (!skipSort) + Common::sort(scene->_objs.begin(), scene->_objs.end(), objComparator); + + _engine->onMove(obj, from, scene); +} + +static bool chrComparator(const Chr *l, const Chr *r) { + return l->_index < r->_index; +} + +void World::move(Chr *chr, Scene *scene, bool skipSort) { + if (chr == NULL) + return; + Scene *from = chr->_currentScene; + if (from == scene) + return; + if (from != NULL) + from->_chrs.remove(chr); + scene->_chrs.push_back(chr); + + if (!skipSort) + Common::sort(scene->_chrs.begin(), scene->_chrs.end(), chrComparator); + + if (scene == _storageScene) { + chr->resetState(); + } else if (chr->_playerCharacter) { + scene->_visited = true; + _player->_context._visits++; + } + chr->_currentScene = scene; + + _engine->onMove(chr, from, scene); +} + +Scene *World::getRandomScene() { + // Not including storage: + return _orderedScenes[1 + _engine->_rnd->getRandomNumber(_orderedScenes.size() - 2)]; +} + +Scene *World::getSceneAt(int x, int y) { + for (uint i = 0; i < _orderedScenes.size(); i++) { + Scene *scene = _orderedScenes[i]; + + if (scene != _storageScene && scene->_worldX == x && scene->_worldY == y) { + return scene; + } + } + return NULL; +} + +static const int directionsX[] = { 0, 0, 1, -1 }; +static const int directionsY[] = { -1, 1, 0, 0 }; + +bool World::scenesAreConnected(Scene *scene1, Scene *scene2) { + if (!scene1 || !scene2) + return false; + + int x = scene2->_worldX; + int y = scene2->_worldY; + + for (int dir = 0; dir < 4; dir++) + if (!scene2->_blocked[dir]) + if (getSceneAt(x + directionsX[dir], y + directionsY[dir]) == scene1) + return true; + + return false; +} + +const char *World::getAboutMenuItemName() { + static char menu[256]; + + *menu = '\0'; + + if (_aboutMenuItemName.empty()) { + sprintf(menu, "About %s...", _name.c_str()); + } else { // Replace '@' with name + const char *str = _aboutMenuItemName.c_str(); + const char *pos = strchr(str, '@'); + if (pos) { + strncat(menu, str, (pos - str)); + strcat(menu, _name.c_str()); + strcat(menu, pos + 1); + } + } + + return menu; +} + +} // End of namespace Wage diff --git a/engines/wage/world.h b/engines/wage/world.h new file mode 100644 index 0000000000..1416fc39e6 --- /dev/null +++ b/engines/wage/world.h @@ -0,0 +1,141 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef WAGE_WORLD_H +#define WAGE_WORLD_H + +namespace Wage { + +#define STORAGESCENE "STORAGE@" + +class World { +public: + World(WageEngine *engine); + ~World(); + + bool loadWorld(Common::MacResManager *resMan); + void loadExternalSounds(Common::String fname); + Common::String *loadStringFromDITL(Common::MacResManager *resMan, int resourceId, int itemIndex); + void move(Obj *obj, Chr *chr); + void move(Obj *obj, Scene *scene, bool skipSort = false); + void move(Chr *chr, Scene *scene, bool skipSort = false); + Scene *getRandomScene(); + Scene *getSceneAt(int x, int y); + bool scenesAreConnected(Scene *scene1, Scene *scene2); + const char *getAboutMenuItemName(); + + WageEngine *_engine; + + Common::String _name; + Common::String _aboutMessage; + Common::String _soundLibrary1; + Common::String _soundLibrary2; + + bool _weaponMenuDisabled; + Script *_globalScript; + Common::HashMap<Common::String, Scene *> _scenes; + Common::HashMap<Common::String, Obj *> _objs; + Common::HashMap<Common::String, Chr *> _chrs; + Common::HashMap<Common::String, Sound *> _sounds; + Common::Array<Scene *> _orderedScenes; + ObjArray _orderedObjs; + ChrArray _orderedChrs; + Common::Array<Sound *> _orderedSounds; + Patterns _patterns; + Scene *_storageScene; + Chr *_player; + //List<MoveListener> moveListeners; + + Common::String *_gameOverMessage; + Common::String *_saveBeforeQuitMessage; + Common::String *_saveBeforeCloseMessage; + Common::String *_revertMessage; + + Common::String _aboutMenuItemName; + Common::String _commandsMenuName; + Common::String _commandsMenu; + Common::String _weaponsMenuName; + + void addScene(Scene *room) { + if (!room->_name.empty()) { + Common::String s = room->_name; + s.toLowercase(); + _scenes[s] = room; + } + _orderedScenes.push_back(room); + } + + void addObj(Obj *obj) { + Common::String s = obj->_name; + s.toLowercase(); + _objs[s] = obj; + obj->_index = _orderedObjs.size(); + _orderedObjs.push_back(obj); + } + + void addChr(Chr *chr) { + Common::String s = chr->_name; + s.toLowercase(); + _chrs[s] = chr; + chr->_index = _orderedChrs.size(); + _orderedChrs.push_back(chr); + } + + void addSound(Sound *sound) { + Common::String s = sound->_name; + s.toLowercase(); + _sounds[s] = sound; + _orderedSounds.push_back(sound); + } + +private: + Common::StringArray *readMenu(Common::SeekableReadStream *res); +}; + +} // End of namespace Wage + +#endif diff --git a/graphics/primitives.cpp b/graphics/primitives.cpp index 564bdb9673..90009b3332 100644 --- a/graphics/primitives.cpp +++ b/graphics/primitives.cpp @@ -20,7 +20,9 @@ * */ +#include "common/algorithm.h" #include "common/util.h" +#include "graphics/primitives.h" namespace Graphics { @@ -62,6 +64,22 @@ void drawLine(int x0, int y0, int x1, int y1, int color, void (*plotProc)(int, i } } +void drawHLine(int x1, int x2, int y, int color, void (*plotProc)(int, int, int, void *), void *data) { + if (x1 > x2) + SWAP(x1, x2); + + for (int x = x1; x <= x2; x++) + (*plotProc)(x, y, color, data); +} + +void drawVLine(int x, int y1, int y2, int color, void (*plotProc)(int, int, int, void *), void *data) { + if (y1 > y2) + SWAP(y1, y2); + + for (int y = y1; y <= y2; y++) + (*plotProc)(x, y, color, data); +} + void drawThickLine(int x0, int y0, int x1, int y1, int penX, int penY, int color, void (*plotProc)(int, int, int, void *), void *data) { assert(penX > 0 && penY > 0); @@ -79,4 +97,335 @@ void drawThickLine(int x0, int y0, int x1, int y1, int penX, int penY, int color drawLine(x0 + x, y0 + y, x1 + x, y1 + y, color, plotProc, data); } +/* Bresenham as presented in Foley & Van Dam */ +/* Code is based on GD lib http://libgd.github.io/ */ +void drawThickLine2(int x1, int y1, int x2, int y2, int thick, int color, void (*plotProc)(int, int, int, void *), void *data) { + int incr1, incr2, d, x, y, xend, yend, xdirflag, ydirflag; + int wid; + int w, wstart; + + int dx = abs(x2 - x1); + int dy = abs(y2 - y1); + + if (dx == 0) { + if (y1 > y2) + SWAP(y1, y2); + Common::Rect r(x1, y1, x1 + thick - 1, y2); + drawFilledRect(r, color, plotProc, data); + return; + } else if (dy == 0) { + if (x1 > x2) + SWAP(x1, x2); + Common::Rect r(x1, y1, x2, y1 + thick - 1); + drawFilledRect(r, color, plotProc, data); + return; + } + + if (dy <= dx) { + /* More-or-less horizontal. use wid for vertical stroke */ + /* Doug Claar: watch out for NaN in atan2 (2.0.5) */ + + /* 2.0.12: Michael Schwartz: divide rather than multiply; + TBB: but watch out for /0! */ + double ac = cos(atan2 (dy, dx)); + if (ac != 0) { + wid = thick / ac; + } else { + wid = 1; + } + if (wid == 0) { + wid = 1; + } + d = 2 * dy - dx; + incr1 = 2 * dy; + incr2 = 2 * (dy - dx); + if (x1 > x2) { + x = x2; + y = y2; + ydirflag = (-1); + xend = x1; + } else { + x = x1; + y = y1; + ydirflag = 1; + xend = x2; + } + + /* Set up line thickness */ + wstart = y - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(x, y, color, data); + + if (((y2 - y1) * ydirflag) > 0) { + while (x < xend) { + x++; + if (d < 0) { + d += incr1; + } else { + y++; + d += incr2; + } + wstart = y - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(x, w, color, data); + } + } else { + while (x < xend) { + x++; + if (d < 0) { + d += incr1; + } else { + y--; + d += incr2; + } + wstart = y - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(x, w, color, data); + } + } + } else { + /* More-or-less vertical. use wid for horizontal stroke */ + /* 2.0.12: Michael Schwartz: divide rather than multiply; + TBB: but watch out for /0! */ + double as = sin(atan2(dy, dx)); + if (as != 0) { + wid = thick / as; + } else { + wid = 1; + } + if (wid == 0) + wid = 1; + + d = 2 * dx - dy; + incr1 = 2 * dx; + incr2 = 2 * (dx - dy); + if (y1 > y2) { + y = y2; + x = x2; + yend = y1; + xdirflag = (-1); + } else { + y = y1; + x = x1; + yend = y2; + xdirflag = 1; + } + + /* Set up line thickness */ + wstart = x - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(w, y, color, data); + + if (((x2 - x1) * xdirflag) > 0) { + while (y < yend) { + y++; + if (d < 0) { + d += incr1; + } else { + x++; + d += incr2; + } + wstart = x - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(w, y, color, data); + } + } else { + while (y < yend) { + y++; + if (d < 0) { + d += incr1; + } else { + x--; + d += incr2; + } + wstart = x - wid / 2; + for (w = wstart; w < wstart + wid; w++) + (*plotProc)(w, y, color, data); + } + } + } +} + +void drawFilledRect(Common::Rect &rect, int color, void (*plotProc)(int, int, int, void *), void *data) { + for (int y = rect.top; y <= rect.bottom; y++) + drawHLine(rect.left, rect.right, y, color, plotProc, data); +} + +// http://members.chello.at/easyfilter/bresenham.html +void drawRoundRect(Common::Rect &rect, int arc, int color, bool filled, void (*plotProc)(int, int, int, void *), void *data) { + if (rect.height() < rect.width()) { + int x = -arc, y = 0, err = 2-2*arc; /* II. Quadrant */ + int dy = rect.height() - arc * 2; + int r = arc; + int stop = 0; + int lastx, lasty; + if (dy < 0) + stop = -dy / 2; + + do { + if (filled) { + drawHLine(rect.left+x+r, rect.right-x-r, rect.top-y+r-stop, color, plotProc, data); + drawHLine(rect.left+x+r, rect.right-x-r, rect.bottom+y-r+stop, color, plotProc, data); + } else { + (*plotProc)(rect.left+x+r, rect.top-y+r-stop, color, data); + (*plotProc)(rect.right-x-r, rect.top-y+r-stop, color, data); + (*plotProc)(rect.left+x+r, rect.bottom+y-r+stop, color, data); + (*plotProc)(rect.right-x-r, rect.bottom+y-r+stop, color, data); + + lastx = x; + lasty = y; + } + arc = err; + if (arc <= y) err += ++y*2+1; /* e_xy+e_y < 0 */ + if (arc > x || err > y) err += ++x*2+1; /* e_xy+e_x > 0 or no 2nd y-step */ + if (stop && y > stop) + break; + } while (x < 0); + + if (!filled) { + x = lastx; + y = lasty; + + drawHLine(rect.left+x+r, rect.right-x-r, rect.top-y+r-stop, color, plotProc, data); + drawHLine(rect.left+x+r, rect.right-x-r, rect.bottom+y-r+stop, color, plotProc, data); + } + + for (int i = 0; i < dy; i++) { + if (filled) { + drawHLine(rect.left, rect.right, rect.top + r + i, color, plotProc, data); + } else { + (*plotProc)(rect.left, rect.top + r + i, color, data); + (*plotProc)(rect.right, rect.top + r + i, color, data); + } + } + } else { + int y = -arc, x = 0, err = 2-2*arc; /* II. Quadrant */ + int dx = rect.width() - arc * 2; + int r = arc; + int stop = 0; + int lastx, lasty; + if (dx < 0) + stop = -dx / 2; + + do { + if (filled) { + drawVLine(rect.left-x+r-stop, rect.top+y+r, rect.bottom-y-r, color, plotProc, data); + drawVLine(rect.right+x-r+stop, rect.top+y+r, rect.bottom-y-r, color, plotProc, data); + } else { + (*plotProc)(rect.left-x+r-stop, rect.top+y+r, color, data); + (*plotProc)(rect.left-x+r-stop, rect.bottom-y-r, color, data); + (*plotProc)(rect.right+x-r+stop, rect.top+y+r, color, data); + (*plotProc)(rect.right+x-r+stop, rect.bottom-y-r, color, data); + + lastx = x; + lasty = y; + } + + arc = err; + if (arc <= x) err += ++x*2+1; /* e_xy+e_y < 0 */ + if (arc > y || err > x) err += ++y*2+1; /* e_xy+e_x > 0 or no 2nd y-step */ + if (stop && x > stop) + break; + } while (y < 0); + + if (!filled) { + x = lastx; + y = lasty; + drawVLine(rect.left-x+r-stop, rect.top+y+r, rect.bottom-y-r, color, plotProc, data); + drawVLine(rect.right+x-r+stop, rect.top+y+r, rect.bottom-y-r, color, plotProc, data); + } + + for (int i = 0; i < dx; i++) { + if (filled) { + drawVLine(rect.left + r + i, rect.top, rect.bottom, color, plotProc, data); + } else { + (*plotProc)(rect.left + r + i, rect.top, color, data); + (*plotProc)(rect.left + r + i, rect.bottom, color, data); + } + } + } +} + +// Based on public-domain code by Darel Rex Finley, 2007 +// http://alienryderflex.com/polygon_fill/ +void drawPolygonScan(int *polyX, int *polyY, int npoints, Common::Rect &bbox, int color, void (*plotProc)(int, int, int, void *), void *data) { + int *nodeX = (int *)calloc(npoints, sizeof(int)); + int i, j; + + // Loop through the rows of the image. + for (int pixelY = bbox.top; pixelY < bbox.bottom; pixelY++) { + // Build a list of nodes. + int nodes = 0; + j = npoints - 1; + + for (i = 0; i < npoints; i++) { + if ((polyY[i] < pixelY && polyY[j] >= pixelY) || (polyY[j] < pixelY && polyY[i] >= pixelY)) { + nodeX[nodes++] = (int)(polyX[i] + (double)(pixelY - polyY[i]) / (double)(polyY[j]-polyY[i]) * + (double)(polyX[j] - polyX[i]) + 0.5); + } + j = i; + } + + // Sort the nodes + Common::sort(nodeX, &nodeX[nodes]); + + // Fill the pixels between node pairs. + for (i = 0; i < nodes; i += 2) { + if (nodeX[i ] >= bbox.right) + break; + if (nodeX[i + 1] > bbox.left) { + nodeX[i] = MAX<int16>(nodeX[i], bbox.left); + nodeX[i + 1] = MIN<int16>(nodeX[i + 1], bbox.right); + + drawHLine(nodeX[i], nodeX[i + 1], pixelY, color, plotProc, data); + } + } + } + + free(nodeX); +} + +// http://members.chello.at/easyfilter/bresenham.html +void drawEllipse(int x0, int y0, int x1, int y1, int color, bool filled, void (*plotProc)(int, int, int, void *), void *data) { + int a = abs(x1-x0), b = abs(y1-y0), b1 = b&1; /* values of diameter */ + long dx = 4*(1-a)*b*b, dy = 4*(b1+1)*a*a; /* error increment */ + long err = dx+dy+b1*a*a, e2; /* error of 1.step */ + + if (x0 > x1) { x0 = x1; x1 += a; } /* if called with swapped points */ + if (y0 > y1) y0 = y1; /* .. exchange them */ + y0 += (b+1)/2; y1 = y0-b1; /* starting pixel */ + a *= 8*a; b1 = 8*b*b; + + do { + if (filled) { + drawHLine(x0, x1, y0, color, plotProc, data); + drawHLine(x0, x1, y1, color, plotProc, data); + } else { + (*plotProc)(x1, y0, color, data); /* I. Quadrant */ + (*plotProc)(x0, y0, color, data); /* II. Quadrant */ + (*plotProc)(x0, y1, color, data); /* III. Quadrant */ + (*plotProc)(x1, y1, color, data); /* IV. Quadrant */ + } + e2 = 2*err; + if (e2 <= dy) { y0++; y1--; err += dy += a; } /* y step */ + if (e2 >= dx || 2*err > dy) { x0++; x1--; err += dx += b1; } /* x step */ + } while (x0 <= x1); + + while (y0-y1 < b) { /* too early stop of flat ellipses a=1 */ + if (filled) { + drawHLine(x0-1, x0-1, y0, color, plotProc, data); /* -> finish tip of ellipse */ + drawHLine(x1+1, x1+1, y0, color, plotProc, data); + drawHLine(x0-1, x0-1, y1, color, plotProc, data); + drawHLine(x1+1, x1+1, y1, color, plotProc, data); + } else { + (*plotProc)(x0-1, y0, color, data); /* -> finish tip of ellipse */ + (*plotProc)(x1+1, y0, color, data); + (*plotProc)(x0-1, y1, color, data); + (*plotProc)(x1+1, y1, color, data); + } + y0++; + y1--; + } +} + } // End of namespace Graphics diff --git a/graphics/primitives.h b/graphics/primitives.h index a3e8ab1565..62dc10bfdf 100644 --- a/graphics/primitives.h +++ b/graphics/primitives.h @@ -23,10 +23,21 @@ #ifndef GRAPHICS_PRIMITIVES_H #define GRAPHICS_PRIMITIVES_H +#include "common/rect.h" + namespace Graphics { void drawLine(int x0, int y0, int x1, int y1, int color, void (*plotProc)(int, int, int, void *), void *data); +void drawHLine(int x1, int x2, int y, int color, void (*plotProc)(int, int, int, void *), void *data); +void drawVLine(int x, int y1, int y2, int color, void (*plotProc)(int, int, int, void *), void *data); void drawThickLine(int x0, int y0, int x1, int y1, int penX, int penY, int color, void (*plotProc)(int, int, int, void *), void *data); +void drawThickLine2(int x1, int y1, int x2, int y2, int thick, int color, + void (*plotProc)(int, int, int, void *), void *data); +void drawFilledRect(Common::Rect &rect, int color, void (*plotProc)(int, int, int, void *), void *data); +void drawRoundRect(Common::Rect &rect, int arc, int color, bool filled, void (*plotProc)(int, int, int, void *), void *data); +void drawPolygonScan(int *polyX, int *polyY, int npoints, Common::Rect &bbox, int color, + void (*plotProc)(int, int, int, void *), void *data); +void drawEllipse(int x0, int y0, int x1, int y1, int color, bool filled, void (*plotProc)(int, int, int, void *), void *data); } // End of namespace Graphics |