diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index c550b69f66..710be77781 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -7,16 +7,56 @@ on: pull_request: paths: - 'app/android/**' + workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - id: job_info + uses: daleyjem/gh-job-id@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + job_name: 'build' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + - name: Checkout Repository + uses: actions/checkout@v3 - - name: Build Android Packages - run: cd app/android/client && gradle assemble + - name: Set Up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + - name: Validate Gradle Wrapper .jar + run: | + cd app/android/gradle/wrapper + curl --location --output gradle-wrapper.jar.sha256 \ + https://services.gradle.org/distributions/gradle-7.5.1-wrapper.jar.sha256 + echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256 + sha256sum --check gradle-wrapper.jar.sha256 + + - name: Build APKs with Ant + uses: cedx/setup-ant@v6 + with: + version: '1.10' + - run: ant -noinput -buildfile build.xml android -Dgradle.params="assembleDebug assembleRelease" + + - name: Failure Report + uses: sarisia/actions-status-discord@v1 + if: failure() + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVELOPMENT }} + with: + title: 'Android Build Failed' + description: '${{ steps.job_info.outputs.html_url }}' + nodetail: true + - uses: sarisia/actions-status-discord@v1 + if: failure() + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVLOG }} + with: + title: 'Android Build Failed' + description: '${{ steps.job_info.outputs.html_url }}' + nodetail: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1aaaeb4e51..3c5094235c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,15 +29,15 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + #- name: Autobuild + # uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -46,9 +46,7 @@ jobs: # and modify them (or add more) to build your code if your project # uses a compiled language - #- run: | - # make bootstrap - # make release + - run: ant -noinput -f build.xml clean compile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.project b/.project index 8aa95068ea..a3d0298ad9 100644 --- a/.project +++ b/.project @@ -20,4 +20,15 @@ org.eclipse.jdt.core.javanature org.eclipse.wst.common.project.facet.core.nature + + + 1771748271774 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/README.md b/README.md index 1f1a30a95d..8b25a90318 100644 --- a/README.md +++ b/README.md @@ -30,29 +30,28 @@ button to create or log in to your account. ### Mobile Play using the [progressive web app](https://en.wikipedia.org/wiki/Progressive_web_app). Just visit -https://stendhalgame.org/ in your mobile browser and click "install" when prompted. +[stendhalgame.org](https://stendhalgame.org/) in your mobile browser and click "install" when prompted. [](https://f-droid.org/packages/org.stendhalgame.client/) Play on your Android device using the dedicated app. You can get it from the -[F-Droid App Store](https://f-droid.org/) or download it from https://stendhalgame.org/download.html. +[F-Droid App Store](https://f-droid.org/) or download it from [stendhalgame.org](https://stendhalgame.org/download.html). ### Classic Desktop [![Download](https://stendhalgame.org/images/downloadbutton.png)](https://arianne-project.org/download/stendhal.zip) Play using the classic desktop client. You will need Java which you can download from -[https://www.java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in +[java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in most Linux distributions. Both Oracle Java and OpenJDK are supported. -Please download Stendhal from [https://stendhalgame.org](https://stendhalgame.org)
-You can double click on stendhal-starter.jar and the client will run.
-If you prefer to run from command line, then just execute this command -in the stendhal folder: +Please download Stendhal from [stendhalgame.org](https://stendhalgame.org/download.html).
+You can double click on `stendhal-starter.jar` and the client will run.
+If you prefer to run from command line, then just execute this command in the stendhal folder: `java -jar stendhal-starter.jar` -There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen. +There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen. ## Development environment @@ -67,7 +66,7 @@ There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there i Stendhal is free software. You can redistribute it and/or modify under the following terms: -- The **server**, **java client**, and **Android client**: *GNU General Public License* 2 or later as published by the Free +- The **server**, **java client**, and **Android client**: *GNU General Public License* 2 or later as published by the Free Software Foundation - The **web client** and **native client**: *GNU Affero General Public License* 3 or later as published by the Free Software Foundation with the definition of "System libraries" being extended to include authentication providers and software distribution platforms. - The **nativehelper** extensions: *GNU Lesser General Public License* 3 or later as published by the Free Software Foundation @@ -76,6 +75,6 @@ even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE [LICENSE.txt](LICENSE.txt) and [src/js/LICENSE.html](src/js/LICENSE.html) file for more details. Stendhal(c) is copyright of Miguel Angel Blanch Lardin, 2005-2008, arianne_rpg at users dot sourceforge dot net and others
-Stendhal(c) is copyright of the Arianne Project, 2006-2024, arianne-general at lists dot sourceforge dot net +Stendhal(c) is copyright of the Arianne Project, 2006-2026, arianne-general at lists dot sourceforge dot net Please have a look at the list of [contributors](doc/contributors.md#contributors). diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000000..a82f7921f3 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/godot/ diff --git a/app/android/client/build.gradle b/app/android/client/build.gradle index a3c3c643c4..d5f56cb075 100644 --- a/app/android/client/build.gradle +++ b/app/android/client/build.gradle @@ -10,15 +10,23 @@ android { applicationId namespace minSdk 21 targetSdk 32 - versionCode 1046005 - versionName "1.46.5" + versionCode 1049005 + versionName "1.49.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" targetSdkVersion 32 minSdkVersion 21 } + // F-Droid doesn't allow encrypted DependencyInfoBlock in APK + dependenciesInfo { + // disables dependency metadata when building APKs + includeInApk = false + // disables dependency metadata when building Android App Bundles + includeInBundle = false + } + // build output - project.setProperty("archivesBaseName", "${namespace}-${defaultConfig.versionCode}") + project.setProperty("archivesBaseName", "${namespace}-${defaultConfig.versionName}") project.setProperty("buildDir", "${projectDir}/../../../build/build_android_client") /* A keystore.properties file can be placed in the root Android project directory and must diff --git a/app/android/client/src/main/java/org/stendhalgame/client/ClientView.java b/app/android/client/src/main/java/org/stendhalgame/client/ClientView.java index d7205ee53a..d3bb03015a 100644 --- a/app/android/client/src/main/java/org/stendhalgame/client/ClientView.java +++ b/app/android/client/src/main/java/org/stendhalgame/client/ClientView.java @@ -14,6 +14,7 @@ package org.stendhalgame.client; import java.util.Random; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -189,7 +190,11 @@ public class ClientView extends WebView { if (!UrlHelper.isInternalUri(uri)) { // open external links in default browser/app // FIXME: should we ask for confirmation? - MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, uri)); + try { + MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } catch (final ActivityNotFoundException e) { + Notifier.toast("No app found to open this link"); + } return true; } view.loadUrl(UrlHelper.checkClientUrl(uri.toString())); @@ -263,6 +268,9 @@ public class ClientView extends WebView { /** * Handles downloading screenshot created by web client. * + * TODO: get filename to pass to `DownloadHandler.download` from "download" property of + * anchor + * * @param url * Content URL to the screenshot image. * @param userAgent diff --git a/app/android/client/src/main/java/org/stendhalgame/client/DownloadHandler.java b/app/android/client/src/main/java/org/stendhalgame/client/DownloadHandler.java index 4144ed8515..29a423d24d 100644 --- a/app/android/client/src/main/java/org/stendhalgame/client/DownloadHandler.java +++ b/app/android/client/src/main/java/org/stendhalgame/client/DownloadHandler.java @@ -13,6 +13,8 @@ package org.stendhalgame.client; import java.io.File; import java.io.FileOutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; @@ -26,61 +28,175 @@ import android.util.Base64; * created by the web client page. */ public class DownloadHandler { - private boolean result = false; - private String message = null;; - public void download(final String url, final String mimetype) { + private static enum DownloadType { + IMAGE_PNG("image/png"), + TEXT_PLAIN("text/plain"), + UNSUPPORTED(null); + + private String value; + + private DownloadType(final String value) { + this.value = value; + } + + public static DownloadType fromString(final String value) { + for (final DownloadType v: DownloadType.values()) { + if (v.value != null && v.value.equals(value)) { + return v; + } + } + return DownloadType.UNSUPPORTED; + } + } + + private boolean result = false; + private String message = null; + + + /** + * Checks for a supported MIME type. + * + * @param url + * File or data URL. + * @param mimetype + * Detected MIME type. + * @return + * Download type or unsupported. + */ + private DownloadType checkMimeType(final String url, final String mimetype) { + final DownloadType dtype = DownloadType.fromString(mimetype); + if (DownloadType.UNSUPPORTED.equals(dtype) && url.startsWith("data:image/png;base64,")) { + return DownloadType.IMAGE_PNG; + } + return dtype; + } + + /** + * Writes file data to device storage. + * + * @param dir + * Directory where new file is to be created. + * @param basename + * Filename of new file. + * @param data + * File contents to be written to storage. + */ + private void downloadInternal(final File dir, final String basename, final byte[] data) { + String stacktrace = null; + try { + if (!dir.exists()) { + dir.mkdirs(); + } + final FileOutputStream fos = new FileOutputStream(new File(dir, basename)); + fos.write(data); + fos.close(); + this.result = true; + } catch (final java.lang.NoClassDefFoundError e) { + this.message = "an error occurred while decoding data (see debug log for more info)"; + stacktrace = stackTraceToString(e); + } catch (java.io.IOException e) { + this.message = "an error occurred while attempting to write file (see debug log for more info)"; + stacktrace = stackTraceToString(e); + } + + if (stacktrace != null) { + Logger.error(stacktrace.toString()); + } + } + + /** + * Writes file data to device storage. + * + * @param url + * A data URL. + * @param mimetype + * Detected MIME type. + * @param targetName + * Filename for new file. + */ + public void download(final String url, final String mimetype, String targetName) { final Uri uri = Uri.parse(url); final String scheme = uri.getScheme(); final String storageState = Environment.getExternalStorageState(); + final DownloadType dtype = checkMimeType(url, mimetype); if (!MainActivity.get().getActiveClientView().isGameActive()) { this.message = "downloading from this page not supported"; } else if (!"data".equals(scheme)) { + // only data URL supported this.message = "download type \"" + scheme + "\" not supported"; - } else if (!"image/png".equals(mimetype) || !url.startsWith("data:image/png;base64,")) { + } else if (DownloadType.UNSUPPORTED.equals(dtype)) { this.message = "mimetype not supported: " + mimetype; } else if (!Environment.MEDIA_MOUNTED.equals(storageState)) { this.message = "storage not available for writing (state: " + storageState + ")"; - } else { + } else if (DownloadType.IMAGE_PNG.equals(dtype)) { final File targetDir = new File(Environment.getExternalStorageDirectory() + "/Pictures/Screenshots"); - final String targetName = "stendhal_" - + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date()) + ".png"; + if (targetName == null) { + // default filename + targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date()) + + ".png"; + } Logger.debug("Saving screenshot: " + targetDir.getPath() + "/" + targetName + " (" + mimetype + ")"); - String msg; - String stacktrace = null; - try { - if (!targetDir.exists()) { - targetDir.mkdirs(); + final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT); + downloadInternal(targetDir, targetName, data); + if (this.result) { + this.message = "saved screenshot to " + targetDir.getPath() + "/" + targetName; + } + } else if (DownloadType.TEXT_PLAIN.equals(dtype)) { + // `Environment.DIRECTORY_DOWNLOADS` fails on newer Android versions + //final File targetDir = new File(Environment.DIRECTORY_DOWNLOADS); + final File targetDir = new File(Environment.getExternalStorageDirectory() + "/Download"); + if (targetName == null) { + // default filename + targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date()) + + ".txt"; + } + + Logger.debug("Saving file: " + targetDir.getPath() + + "/" + targetName + " (" + mimetype + ")"); + + final String[] parts = url.split(","); + byte[] data; + if (parts[0].endsWith(";base64")) { + data = Base64.decode(parts[1], Base64.DEFAULT); + } else { + try { + data = URLDecoder.decode(parts[1], StandardCharsets.UTF_8.name()) + .getBytes(StandardCharsets.UTF_8); + } catch (final java.io.UnsupportedEncodingException e) { + this.message = "an error occurred while attempting to decode data URL (see debug log for more info)"; + Logger.error(stackTraceToString(e)); + return; } - - final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT); - final FileOutputStream fos = new FileOutputStream(new File(targetDir, targetName)); - fos.write(data); - fos.close(); - - this.result = true; - msg = "saved screenshot to " + targetDir.getPath() + "/" + targetName; - } catch (final java.lang.NoClassDefFoundError e) { - msg = "an error occurred while decoding data (see debug log for more info)"; - stacktrace = stackTraceToString(e); - } catch (java.io.IOException e) { - msg = "an error occurred while attempting to write file (see debug log for more info)"; - stacktrace = stackTraceToString(e); } - this.message = msg; - if (stacktrace != null) { - Logger.error(stacktrace.toString()); + downloadInternal(targetDir, targetName, data); + if (this.result) { + this.message = "file saved to " + targetDir.getPath() + "/" + targetName; } + } else { + this.message = "an unknown error occurred"; } } + /** + * Writes file data to device storage. + * + * @param url + * A data URL. + * @param mimetype + * Detected MIME type. + */ + public void download(final String url, final String mimetype) { + download(url, mimetype, null); + } + public boolean getResult() { return result; } diff --git a/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/01-start.jpg b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/01-start.jpg new file mode 100644 index 0000000000..3dbef90e03 Binary files /dev/null and b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/01-start.jpg differ diff --git a/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/02-npc_interaction.jpg b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/02-npc_interaction.jpg new file mode 100644 index 0000000000..5cb65ec5e9 Binary files /dev/null and b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/02-npc_interaction.jpg differ diff --git a/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/03-melee_combat.jpg b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/03-melee_combat.jpg new file mode 100644 index 0000000000..86fe43ded9 Binary files /dev/null and b/app/android/client/src/main/play/listings/en-US/graphics/phone-screenshots/03-melee_combat.jpg differ diff --git a/app/android/client/src/main/play/listings/pt/full-description.txt b/app/android/client/src/main/play/listings/pt/full-description.temp.txt similarity index 100% rename from app/android/client/src/main/play/listings/pt/full-description.txt rename to app/android/client/src/main/play/listings/pt/full-description.temp.txt diff --git a/app/android/client/src/main/play/release-notes/en-US/default.txt b/app/android/client/src/main/play/release-notes/en-US/default.txt index e0547dac4e..70b8142595 100644 --- a/app/android/client/src/main/play/release-notes/en-US/default.txt +++ b/app/android/client/src/main/play/release-notes/en-US/default.txt @@ -1,65 +1,3 @@ +PUT RELEASE ANNOUNCEMENT LINES HERE -1.46 - -*world* -- renamed items: - - "courgette" to "zucchini" - - "iron" to "iron bar" - - "salad" to "lettuce" -- fixed many NPCs not responding to certain chat options -- normalized spelling of some words to American English in most NPC dialogues - - "colour" -> "color" - - "defence" -> "defense" - - "favour" -> "favor" - - "favourite" -> "favorite" - - "fulfil" -> "fulfill" - - "organise" -> "organize" - - "recognise" -> "rocognize" -- Orchiwald will stop after entity collision instead of reversing path -- foghorn sounds plays when ferry arrives at mainland & island -- added commerce sound to more NPCs when exchanging money for items/services -- seeds can be used from inventory - -*graphics* -- redrawn placeholder tiles in Oni Palace -- fixed some outfit errors -- disabled unfinished weapon-style attack sprites - -*web client* -- improved movement handling with on-screen joystick & direction pad -- alternate "floating" menu style -- alternate "floating" chat panel style -- software keyboard is automatically hidden after sending chat message -- optional activity indicator for corpses & some signs -- immediate configuration changes affect only the active client/browser tab -- application menu dialog doesn't close when clicking on background -- correct NPC title is displayed in chat options dialog -- disabled highlighting joystick/d-pad button on mouse double-click & drag -- joystick/d-pad isn't shown until user interface is ready -- character name in stats panel uses default font size -- fixed chest default open/close behavior (clicking on open chest opens inventory window instead of closing) -- doesn't attempt to open corpse/chest inventory windows when character not in range -- fixed chat history duplicates -- chat options dialog remains open until NPC is no longer attending or player closes -- fixed continuous movement state not always updated at startup -- supports displaying members' HP status in group panel -- fixed group panel not always showing membership after login -- added support for touch drag-and-drop -- fixed weather drawing to not appear to move with player -- fixed subsequent single clicks on items to not count as double after double click -- fixed deathmatch scrying orbs -- fixed portal default action when on a collision tile -- added autocomplete support -- fixed positioning of door sprites (-2_orril_lich_palace 84 108) -- optional click/touch indicator -- fixed difficult to move small floating windows -- supports chat commands tab completion -- supports text highlighting in speech bubbles -- fixed opening chat log context menu with touch -- supports continued zone music when changed zone matches - -*Android* -- added option to clear WebView cache -- added option to lock orientation -- fixes for using the correct software keyboard -- app ID changed to conform with F-Droid standards +Please see the complete change log for further details: https://arianne-project.org/game/stendhal.html#changes diff --git a/app/android/client/src/main/res/values/strings.xml b/app/android/client/src/main/res/values/strings.xml index a12f4b2d12..f60bf90f42 100644 --- a/app/android/client/src/main/res/values/strings.xml +++ b/app/android/client/src/main/res/values/strings.xml @@ -5,6 +5,6 @@ Stendhal stendhalprod8gps5y99pu prod - 1.46.5 + 1.49.5 diff --git a/app/android/client/src/shared/java/org/stendhalgame/client/Logger.java b/app/android/client/src/shared/java/org/stendhalgame/client/Logger.java index c4aab87415..cb14799ca6 100644 --- a/app/android/client/src/shared/java/org/stendhalgame/client/Logger.java +++ b/app/android/client/src/shared/java/org/stendhalgame/client/Logger.java @@ -19,7 +19,7 @@ import android.content.DialogInterface; /** * Semi-functional logger for release builds. - * + * * Displays notification dialogs but does not write to logs directory. */ public class Logger { diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index 0176d15822..b916c04dbb 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=91a239400bb638f36a1795d8fdf7939d532cdc7d794d1119b7261aac158b1e60 +distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/app/nativehelper/Cargo.toml b/app/nativehelper/Cargo.toml index bdf01b24a2..4df05921bc 100644 --- a/app/nativehelper/Cargo.toml +++ b/app/nativehelper/Cargo.toml @@ -18,6 +18,7 @@ panic = 'abort' strip = true [dependencies] +lazy_static = "1.5.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" steamworks = "0.10.0" diff --git a/app/nativehelper/src/main.rs b/app/nativehelper/src/main.rs index 7f3ae7912e..75186ee688 100644 --- a/app/nativehelper/src/main.rs +++ b/app/nativehelper/src/main.rs @@ -1,9 +1,12 @@ +use lazy_static::lazy_static; use serde_json::Value; use std::io; use std::io::Read; +use std::process; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; +use std::sync::Mutex; use std::sync::mpsc::Sender; use std::sync::mpsc::channel; use std::thread; @@ -16,6 +19,10 @@ use websocket::client::ClientBuilder; use websocket::{Message, OwnedMessage}; +lazy_static! { + static ref AUTHENTICATED: Mutex = Mutex::new(false); +} + fn connect() -> (Sender, JoinHandle<()>) { println!("Read connection parameters"); let mut json = String::new(); @@ -112,58 +119,70 @@ fn connect() -> (Sender, JoinHandle<()>) { } } }); - - return (tx, receive_loop); + + return (tx, receive_loop); } fn handle_message(nl_token: &String, tx: Sender, message: String) { if message.contains("event\":\"request_authentication\"") { authenticate(tx, nl_token.clone()); + } else if message.contains("event\":\"clientDisconnect\"") { + let authenticated = AUTHENTICATED.lock().unwrap(); + if *authenticated { + println!("Authenticated, exiting extension ..."); + } else { + println!("Unauthenticated, exiting extension ..."); + } + process::exit(0); } } fn authenticate(tx: Sender, nl_token: String) { - match Client::init() { - Ok((client, single)) => { + match Client::init() { + Ok((client, single)) => { let called = Arc::new(AtomicBool::new(false)); let callback_called = called.clone(); - let (_ticket_handle, session_ticket): (steamworks::AuthTicket, Vec) = client.user().authentication_session_ticket(); - let _callback = - client.register_callback(move |session_ticket_response: AuthSessionTicketResponse| { - //if session_ticket_response.ticket == ticket_handle { - println!("Ticket Response Result: {:?}", session_ticket_response.result); - callback_called.store(true, Ordering::Relaxed); - // println!("Ticket : {:?}", session_ticket_response.ticket); - // println!("Ticket Response: {:?}", session_ticket); + let (_ticket_handle, session_ticket): (steamworks::AuthTicket, Vec) = client.user().authentication_session_ticket(); + let _callback = + client.register_callback(move |session_ticket_response: AuthSessionTicketResponse| { + //if session_ticket_response.ticket == ticket_handle { + if session_ticket_response.result.is_ok() { + let mut authenticated = AUTHENTICATED.lock().unwrap(); + *authenticated = true; + } + println!("Ticket Response Result: {:?}", session_ticket_response.result); + callback_called.store(true, Ordering::Relaxed); + // println!("Ticket : {:?}", session_ticket_response.ticket); + // println!("Ticket Response: {:?}", session_ticket); - let message = OwnedMessage::Text(format!("\ - {{\ - \"id\": \"15fb0ffb-7b18-48d2-89a3-39b1ce4b2645\",\ - \"method\": \"app.broadcast\",\ - \"accessToken\": {:?},\ - \"data\": {{\ - \"event\": \"steamAuthToken\",\ - \"data\": {:?}\ - }}\ - }}", nl_token, &session_ticket)); - match tx.send(message) { - Ok(()) => (), - Err(e) => { - println!("Websocket send error: {:?}", e); - } - } + let message = OwnedMessage::Text(format!("\ + {{\ + \"id\": \"15fb0ffb-7b18-48d2-89a3-39b1ce4b2645\",\ + \"method\": \"app.broadcast\",\ + \"accessToken\": {:?},\ + \"data\": {{\ + \"event\": \"steamAuthToken\",\ + \"data\": {:?}\ + }}\ + }}", nl_token, &session_ticket)); + match tx.send(message) { + Ok(()) => (), + Err(e) => { + println!("Websocket send error: {:?}", e); + } + } - }); - for _ in 0..100 { - single.run_callbacks(); - if called.load(Ordering::Relaxed) { - return; - } - ::std::thread::sleep(::std::time::Duration::from_millis(50)); - } - }, - Err(_) => { + }); + for _ in 0..100 { + single.run_callbacks(); + if called.load(Ordering::Relaxed) { + return; + } + ::std::thread::sleep(::std::time::Duration::from_millis(50)); + } + }, + Err(_) => { println!("Steam not available"); let message = OwnedMessage::Text(format!("\ {{\ @@ -181,10 +200,10 @@ fn authenticate(tx: Sender, nl_token: String) { } } } - } + } } fn main() { - let (_tx, receive_loop) = connect(); + let (_tx, receive_loop) = connect(); let _ = receive_loop.join(); } diff --git a/app/neutralinojs/.gitignore b/app/neutralinojs/.gitignore index 87d1a9794d..3bdc203be2 100644 --- a/app/neutralinojs/.gitignore +++ b/app/neutralinojs/.gitignore @@ -11,6 +11,7 @@ neutralino.js # Neutralinojs related files .storage *.log +extensions/binary/steam_appid.txt extensions/binary/*.so extensions/binary/*.dll extensions/binary/nativehelper* diff --git a/app/neutralinojs/extensions/binary/steam_appid.txt b/app/neutralinojs/extensions/binary/steam_appid.txt deleted file mode 100644 index d689143772..0000000000 --- a/app/neutralinojs/extensions/binary/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -2267890 diff --git a/app/neutralinojs/neutralino.config.json b/app/neutralinojs/neutralino.config.json index e351797b8e..7d4ac4d423 100644 --- a/app/neutralinojs/neutralino.config.json +++ b/app/neutralinojs/neutralino.config.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", "applicationId": "org.stendhalgame.client", - "version": "1.46.5", + "version": "1.49.5", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", @@ -11,9 +11,9 @@ "enableExtensions": true, "extensions": [ { - "id": "nativehelper", - "commandLinux": "${NL_PATH}/extensions/binary/nativehelper", - "commandWindows": "${NL_PATH}/extensions/binary/nativehelper.exe" + "id": "nativehelper", + "commandLinux": "${NL_PATH}/extensions/binary/nativehelper", + "commandWindows": "${NL_PATH}/extensions/binary/nativehelper.exe" } ], "tokenSecurity": "one-time", @@ -55,7 +55,7 @@ "resourcesPath": "/resources/", "extensionsPath": "/extensions/", "clientLibrary": "/resources/js/neutralino.js", - "binaryVersion": "5.0.0", - "clientVersion": "5.0.1" + "binaryVersion": "6.2.0", + "clientVersion": "6.2.0" } } diff --git a/bin/check-file-size.py b/bin/check-file-size.py new file mode 100755 index 0000000000..8ea91e307e --- /dev/null +++ b/bin/check-file-size.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +''' +*************************************************************************** +* Copyright © 2025 - Faiumoni e. V. * +*************************************************************************** +*************************************************************************** +* * +* 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. * +* * +*************************************************************************** +''' + +import errno +import os +import sys + +from pathlib import Path + + +dir_root = Path(__file__).parent +if "bin" == dir_root.name: + dir_root = Path(dir_root.parent) +dir_music = Path(dir_root, "data/music") +dir_sounds = Path(dir_root, "data/sounds") + + +## Checks if a file is larger than 2MB. +# +# On systems with low resources, the Java client struggles to load large audio files. +# +# @param path +# File path. +# @return +# `EFBIG` if file is larger than 2MB, 0 otherwise. +def checkAudioFileSize(path): + if not path.is_file(): + # ignore if audio file doesn't exist + print("WARNING: Ignoring file not found: {}".format(path)) + return 0 + + if path.stat().st_size > 1048576 * 2: + # warn if file size > 2MB + return errno.EFBIG + return 0 + +## Checks directory for large audio files. +# +# @param path +# Directory path to be walked. +# @return +# `EFBIG` if any file larger than 2MB found, 0 otherwise. +def checkAudioDir(path): + if not path.is_dir(): + # ignore if audio directory doesn't exist + print("WARNING: Ignoring directory not found: {}".format(path)) + return 0 + + res = 0 + for ROOT, DIRS, FILES in path.walk(): + for filename in FILES: + filepath = Path(ROOT, filename) + if filepath.suffix.lower().lstrip(".") not in ("ogg", "oga", "mid", "midi"): + # only check supported file types + continue + + code = checkAudioFileSize(filepath) + if code == errno.EFBIG: + res = code + sys.stderr.write("WARNING: Audio file size larger than 2MB: {}\n".format(str(filepath)[len(str(dir_root)):].lstrip("/\\"))) + + return res + +def main(): + res = 0 + for audio_dir in dir_music, dir_sounds: + code = checkAudioDir(audio_dir) + if code != 0: + res = code + if res == 0: + print("No large audio data files detected") + return res + +if __name__ == "__main__": + # work from source tree root + os.chdir(dir_root) + sys.exit(main()) diff --git a/bin/runserver.bat b/bin/runserver.bat index 629f6ec30d..91d06c7e74 100644 --- a/bin/runserver.bat +++ b/bin/runserver.bat @@ -1,6 +1,6 @@ @echo off -set STENDHAL_VERSION=1.46.5 +set STENDHAL_VERSION=1.49.5 set SERVER_JAR=stendhal-server-%STENDHAL_VERSION%.jar :: change to server directory diff --git a/bin/runserver.sh b/bin/runserver.sh index 8b299080bb..6dbe08452d 100755 --- a/bin/runserver.sh +++ b/bin/runserver.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -STENDHAL_VERSION="1.46.5" +STENDHAL_VERSION="1.49.5" SERVER_JAR="stendhal-server-${STENDHAL_VERSION}.jar" # change to server directory diff --git a/data/maps/vampire_maplabel.py b/bin/vampire_maplabel.py similarity index 52% rename from data/maps/vampire_maplabel.py rename to bin/vampire_maplabel.py index d970b825b4..80ebe83c39 100644 --- a/data/maps/vampire_maplabel.py +++ b/bin/vampire_maplabel.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -#Tue Sep 10 23:32:58 CEST 2019, slightly tweaked -- omero """ This script is used to create the world map with labeled zone names. @@ -10,20 +9,19 @@ To create the labeled world map: * Open a command line * Change to your stendhal directory * Run - ant rendermaps + ant rendermaps * Change to the tiled directory * Run - python vampire_maplabel.py -world + python vampire_maplabel.py -world * Open tiled/world/world.tmx in tiled * Set the floor layer to opaque (no transparency) * Set the zoom to 1:1 (CTRL-0) * From the file menu, select "Save as Image" * Crop the black border (e.g. use "Crop Automatically" in The GIMP) * Upload the image, e.g. at http://imageshack.us - * Replace your labeled image files from the tiled/world directory with - the clean (unlabeled) ones from CVS. - * Edit http://stendhalgame.org/wiki/Template:SmallWorldMap - and add a link to your uploaded image. + * Replace your labeled image files from the tiled/world directory with the clean (unlabeled) ones + from CVS. + * Edit http://stendhalgame.org/wiki/Template:SmallWorldMap and add a link to your uploaded image. """ import sys @@ -33,29 +31,38 @@ import os from PIL import Image, ImageFont, ImageDraw -# get absolute path where script is located -dir_root = os.path.dirname(__file__) -if dir_root: - os.chdir(dir_root) -dir_root = os.getcwd() -dir_world = os.path.join(dir_root, 'world') + script_name = os.path.basename(__file__) +# directory from where script was executed +dir_start = os.getcwd() +# path to root directory +os.chdir(os.path.join(os.path.dirname(__file__), '../')) +dir_root = os.getcwd() +# directory relative to root where images are located +subdir_world = os.path.normpath('data/maps/world') +# absolute path to directory where images are located +dir_world = os.path.join(dir_root, subdir_world) + protected = ('world', 'logo', 'reserved', 'empty', 'empty-white') def showUsage(): - print ('Usage: {} [image-filenames...]'.format(script_name)) - print (' This will label each PNG file passed as argument.') - print (' E.g: ./world/int_*.png will label all interiors, etc. etc.') - print - print ('Alternatively, you can run: {} -world'.format(script_name)) - print (' This will label each PNG file in the world subdirectory.') + print('Usage: {} [image-filenames...]'.format(script_name)) + print(' This will label each PNG file passed as argument.') + print(' E.g: data/maps/world/int_*.png will label all\n interiors, etc.') + print(' Note: If filename parameters aren\'t found they\n are assumed to be nested within the\n {} directory.'.format(subdir_world)) + print() + print('Alternatively, you can run: {} -world'.format(script_name)) + print(' This will label each PNG file in the world subdirectory.') +processed = 0 def do_label(fname): - print ('Processing {}'.format(fname)) + global processed + + print('Processing {}'.format(fname)) # remove directory and file extension label = os.path.basename(fname).split('/')[-1].replace('.png', '') - print ('Processing LABEL: {}'.format(label)) + print('Processing LABEL: {}'.format(label)) img = Image.open(fname) img = img.convert('RGBA') @@ -75,12 +82,43 @@ def do_label(fname): img.paste(img2, img2) img.save(fname) + processed += 1 + + +## Checks file path & converts to absolute. +# +# @param imagepath +# Path to image file. +def checkAbsPath(imagepath): + if not imagepath.lower().endswith('.png'): + # only PNG images supported + imagepath += '.png' + imagepath = os.path.normpath(imagepath) + + # check from root directory + if os.path.isfile(imagepath): + return os.path.abspath(imagepath) + + # check from directory where script was executed + os.chdir(dir_start) + if os.path.isfile(imagepath): + imagepath = os.path.abspath(imagepath) + else: + # assume file is nested in `subdir_world` + imagepath = os.path.join(dir_world, imagepath) + # change back to root dir remaining execution + os.chdir(dir_root) + + return imagepath + if len(sys.argv) < 2: print('\nERROR: Not enough arguments\n') showUsage() sys.exit(1) -elif len(sys.argv) == 2 and sys.argv[1] == '-world': + + +if len(sys.argv) == 2 and sys.argv[1] == '-world': for fname in os.listdir(dir_world): if fname.endswith('.png'): absolute_fname = os.path.join(dir_world, fname) @@ -89,14 +127,8 @@ elif len(sys.argv) == 2 and sys.argv[1] == '-world': continue do_label(absolute_fname) else: - # This doesn't work for me. --mort - #for fname in reduce(operator.add, map(glob.glob, sys.argv[1:])): - - # This works for me. --omero for fname in list(sys.argv[1:]): - if not fname.endswith('.png'): - fname = '{}.png'.format(fname) - absolute_fname = os.path.join(dir_world, fname) + absolute_fname = checkAbsPath(fname) if fname.rstrip('.png') in protected: print('NOTE: skipping protected file: {}'.format(absolute_fname)) continue @@ -104,7 +136,10 @@ else: if not os.path.isfile(absolute_fname): print('\nERROR: Cannot process {}\n File does not exist.'.format(absolute_fname)) sys.exit(1) - print ( 'Labeling {}...'.format(fname )) + print('Labeling {}...'.format(fname )) do_label(absolute_fname) -print ('All Done.') +if processed == 0: + print('\nWARNING: no images were processed') +else: + print('\nprocessed {} {}'.format(processed, 'image' if processed == 1 else 'images')) diff --git a/build.ant.properties b/build.ant.properties index 8e9d19467f..fb5e66018b 100644 --- a/build.ant.properties +++ b/build.ant.properties @@ -91,10 +91,10 @@ updates_server_fallback = http://arianne.sourceforge.net/stendhal/updates version_server = http://arianne.sourceforge.net/stendhal.version # current version of stendhal -version.old = 1.46 -version = 1.46.5 +version.old = 1.49 +version = 1.49.5 # FIXME: dynamic method to set this property? -version.android = 1046005 +version.android = 1049005 # javac options javac.deprecation = true @@ -102,17 +102,17 @@ javac.debug = true javac.debuglevel = source,lines # change these only if you know what you are doing -buildroot = build +build = build +buildroot = ${build} build-archive = build-archive src = src testsrc = tests srcjs = ${src}/js -docu = data/docu -maps = data/maps -data = data/gui -sprites = data/sprites -layers = data/layers -build = build +docs = doc +data = data +gui = ${data}/gui +maps = ${data}/maps +sprites = ${data}/sprites lib = ${buildroot}/lib marauroa_root = ../marauroa marauroa_src = ${marauroa_root}/src diff --git a/build.xml b/build.xml index b596c5f772..f438a3c6ea 100644 --- a/build.xml +++ b/build.xml @@ -1,103 +1,33 @@ - - - + + +Project: + Name: .......... ${game_name} + Version: ....... ${version} + Root: .......... ${basedir} - - - +Build system: + Ant version: ... ${ant.version.number} + Ant home: ...... ${ant.location} + Java version: .. ${java.version} + Java home: ..... ${java.home} - - - - - - +Execute `ant help` for more information. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/buildtools/ant_modules/apps.xml b/buildtools/ant_modules/apps.xml index d78abac190..632566cf60 100644 --- a/buildtools/ant_modules/apps.xml +++ b/buildtools/ant_modules/apps.xml @@ -37,6 +37,7 @@ + @@ -62,12 +63,12 @@ --> - + - + @@ -77,23 +78,33 @@ + + - + - - + ${steam-appid} + + + + + + + + + + + @@ -150,7 +161,8 @@ - + + diff --git a/buildtools/ant_modules/clean.xml b/buildtools/ant_modules/clean.xml index f4d61adb72..eb73fe81aa 100644 --- a/buildtools/ant_modules/clean.xml +++ b/buildtools/ant_modules/clean.xml @@ -2,7 +2,8 @@ - + + @@ -18,14 +19,16 @@ - + + - + + diff --git a/buildtools/ant_modules/dist.xml b/buildtools/ant_modules/dist.xml index 5e65c516b2..69e17d1822 100644 --- a/buildtools/ant_modules/dist.xml +++ b/buildtools/ant_modules/dist.xml @@ -19,13 +19,15 @@ - + + Built client and server for NetBeans. - + + + - + - + + - + + - + + @@ -85,8 +90,8 @@ - - + + @@ -104,7 +109,8 @@ - + + @@ -132,11 +138,15 @@ + + + + - + @@ -175,7 +185,8 @@ - + + @@ -336,7 +347,8 @@ - + + @@ -362,8 +374,8 @@ - - + + @@ -388,10 +400,12 @@ - + + - + + @@ -400,7 +414,8 @@ - + + @@ -410,28 +425,57 @@ - + + - - + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + @@ -442,7 +486,8 @@ - + + @@ -490,7 +535,7 @@ - + @@ -535,7 +580,8 @@ - + + @@ -556,8 +602,12 @@ + + - + + + @@ -577,8 +627,8 @@ - - + + @@ -593,8 +643,8 @@ - - + + @@ -603,10 +653,8 @@ - - - - + + @@ -618,7 +666,8 @@ - + + @@ -654,8 +703,9 @@ + - + @@ -663,34 +713,46 @@ - - + + + + - - var stendhal = window.stendhal = window.stendhal || {}; - stendhal.data = stendhal.data || {}; + + +export function version(marauroa, stendhal) { + stendhal.data = stendhal.data || {}; - stendhal.data.build = { - version: "${version}", - build: "${datetime}", - dist: "webclient" - } + stendhal.data.build = { + version: "${version}", + build: "${datetime}", + dist: "webclient" + } + + //CI +} + + + + + + - + - + @@ -698,11 +760,11 @@ - + - + @@ -711,72 +773,19 @@ - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -//# sourceMappingURL=stendhal-compiled.js.map - + + + + + + + + @@ -793,7 +802,8 @@ - + + @@ -802,7 +812,8 @@ - + + @@ -824,17 +835,16 @@ - - - - + + - + + diff --git a/buildtools/ant_modules/docs.xml b/buildtools/ant_modules/docs.xml index 7b23177318..4e6cd05dfb 100644 --- a/buildtools/ant_modules/docs.xml +++ b/buildtools/ant_modules/docs.xml @@ -12,7 +12,8 @@ - + + @@ -88,7 +89,7 @@ - + @@ -108,7 +109,7 @@ message="Python unavailable, cannot run mkdocs"/> - diff --git a/buildtools/ant_modules/info.xml b/buildtools/ant_modules/info.xml new file mode 100644 index 0000000000..68c7ef2400 --- /dev/null +++ b/buildtools/ant_modules/info.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildtools/ant_modules/locale.xml b/buildtools/ant_modules/locale.xml index 18cf719959..66f25bb768 100644 --- a/buildtools/ant_modules/locale.xml +++ b/buildtools/ant_modules/locale.xml @@ -2,21 +2,23 @@ - + + - + - + - + + - + - + diff --git a/buildtools/ant_modules/main.xml b/buildtools/ant_modules/main.xml index 2902dede0a..9891b0664c 100644 --- a/buildtools/ant_modules/main.xml +++ b/buildtools/ant_modules/main.xml @@ -2,7 +2,8 @@ - + + @@ -42,6 +43,10 @@ match=""version": ".*"" replace=""version": "${version}"" byline="true"/> + - + + @@ -163,7 +169,8 @@ - + + diff --git a/buildtools/ant_modules/maps.xml b/buildtools/ant_modules/maps.xml index 788ba05a04..1f12acb6fa 100644 --- a/buildtools/ant_modules/maps.xml +++ b/buildtools/ant_modules/maps.xml @@ -4,7 +4,8 @@ - + + @@ -12,9 +13,9 @@ - + - + @@ -23,7 +24,8 @@ - + + @@ -41,16 +43,16 @@ 0 128 0.0078125 --> - + - + - + - + @@ -58,15 +60,13 @@ - - - - + + - - + + @@ -79,7 +79,7 @@ - + @@ -88,7 +88,8 @@ - + + @@ -97,7 +98,7 @@ - + diff --git a/buildtools/ant_modules/package.xml b/buildtools/ant_modules/package.xml index 7b0356c940..1d260be037 100644 --- a/buildtools/ant_modules/package.xml +++ b/buildtools/ant_modules/package.xml @@ -16,7 +16,8 @@ - + + @@ -46,7 +47,8 @@ - + + @@ -74,7 +76,8 @@ - + + @@ -107,7 +110,8 @@ - + + diff --git a/buildtools/ant_modules/properties.xml b/buildtools/ant_modules/properties.xml new file mode 100644 index 0000000000..76fdfc3ae4 --- /dev/null +++ b/buildtools/ant_modules/properties.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildtools/ant_modules/system.xml b/buildtools/ant_modules/system.xml new file mode 100644 index 0000000000..dfef2434cd --- /dev/null +++ b/buildtools/ant_modules/system.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildtools/ant_modules/testing.xml b/buildtools/ant_modules/testing.xml index bcb48f75ca..a0bfbea6cd 100644 --- a/buildtools/ant_modules/testing.xml +++ b/buildtools/ant_modules/testing.xml @@ -7,7 +7,8 @@ - + + @@ -21,7 +22,8 @@ - + + @@ -63,7 +65,8 @@ - + + database_implementation=games.stendhal.server.core.engine.StendhalPlayerDatabase factory_implementation=games.stendhal.server.core.engine.StendhalRPObjectFactory @@ -85,7 +88,8 @@ d = 9017093486007323270605414717455737376618092805104733236083857811315606869372 - + + @@ -147,7 +151,8 @@ d = 9017093486007323270605414717455737376618092805104733236083857811315606869372 - + + diff --git a/buildtools/ant_modules/tools.xml b/buildtools/ant_modules/tools.xml index b6dc403527..67bf2c0136 100644 --- a/buildtools/ant_modules/tools.xml +++ b/buildtools/ant_modules/tools.xml @@ -4,7 +4,8 @@ - + + @@ -28,7 +29,11 @@ - + + + + + @@ -36,7 +41,7 @@ - + @@ -55,16 +60,22 @@ - + + + + + + + + + - - - - + + @@ -100,7 +111,8 @@ - + + @@ -116,7 +128,8 @@ - + + Creating update from ${version.old} to ${version} @@ -160,8 +173,9 @@ + - + diff --git a/buildtools/mkdocs/mkdocs.yml b/buildtools/mkdocs/mkdocs.yml index af0e0e0f3a..b73584a76f 100644 --- a/buildtools/mkdocs/mkdocs.yml +++ b/buildtools/mkdocs/mkdocs.yml @@ -1,6 +1,6 @@ -site_name: Stendhal 1.46.5 Reference Documentation +site_name: Stendhal 1.49.5 Reference Documentation site_description: A fun, friendly, & free multiplayer online adventure game. -copyright: Copyright © 2003-2024 Stendhal +copyright: Copyright © 2003-2026 Stendhal site_url: https://stendhalgame.org/ nav: - Index: /reference/ diff --git a/data/conf/creatures/animal.xml b/data/conf/creatures/animal.xml index 97ffade6ef..ece2b108d4 100644 --- a/data/conf/creatures/animal.xml +++ b/data/conf/creatures/animal.xml @@ -33,6 +33,7 @@ + @@ -205,7 +206,7 @@ - + You see a giant killer bat. As if small wasn't pesky enough, these giant ones are capable of taking down anyone who dares come before them. @@ -982,7 +983,7 @@ - + You see a polar bear which lives in the icy desert. Beware! It can take you down with its powerful claws! diff --git a/data/conf/creatures/human_cannibal.xml b/data/conf/creatures/human_cannibal.xml index 0faf7f73b0..a8bca6a7e2 100644 --- a/data/conf/creatures/human_cannibal.xml +++ b/data/conf/creatures/human_cannibal.xml @@ -70,7 +70,7 @@ - + You see an elder cannibal. For years it has eaten humans. It will not stop until its hunger is satisfied. diff --git a/data/conf/creatures/mythical_being.xml b/data/conf/creatures/mythical_being.xml index 3a01f969d9..66c5bfd4bf 100644 --- a/data/conf/creatures/mythical_being.xml +++ b/data/conf/creatures/mythical_being.xml @@ -234,6 +234,7 @@ + diff --git a/data/conf/creatures/orc.xml b/data/conf/creatures/orc.xml index 39e879b168..165778f00b 100644 --- a/data/conf/creatures/orc.xml +++ b/data/conf/creatures/orc.xml @@ -357,6 +357,7 @@ + diff --git a/data/conf/creatures/reptile.xml b/data/conf/creatures/reptile.xml index b530352c67..cd1a637e62 100644 --- a/data/conf/creatures/reptile.xml +++ b/data/conf/creatures/reptile.xml @@ -236,7 +236,7 @@ - + diff --git a/data/conf/equipment.xsd b/data/conf/equipment.xsd index 78717c4260..08a04233e7 100644 --- a/data/conf/equipment.xsd +++ b/data/conf/equipment.xsd @@ -11,6 +11,7 @@ + diff --git a/data/conf/items/miscs.xml b/data/conf/items/miscs.xml index 3aa7d2016b..9fcbf59251 100644 --- a/data/conf/items/miscs.xml +++ b/data/conf/items/miscs.xml @@ -328,7 +328,7 @@ - + You see a pelt of animal skin. It's used to make clothes and leather armor. diff --git a/data/conf/items/rings.xml b/data/conf/items/rings.xml index e6d7051c57..1e1d50bc1b 100644 --- a/data/conf/items/rings.xml +++ b/data/conf/items/rings.xml @@ -230,4 +230,24 @@ + + + You see a ring of clarity. You feel more aware while wearing it. + + + + + + + + + + + + + + + + + diff --git a/data/conf/items/special.xml b/data/conf/items/special.xml index efa7c7b27c..28f4d8cdc6 100644 --- a/data/conf/items/special.xml +++ b/data/conf/items/special.xml @@ -147,4 +147,25 @@ + + + You see a lucky four-leaf clover. What are the chances of finding one!? + + + + + + + + + + + + + + + + + + diff --git a/data/conf/shops/sell_outfits.xml b/data/conf/shops/sell_outfits.xml index 14e7e77b71..ca2e0b0f40 100644 --- a/data/conf/shops/sell_outfits.xml +++ b/data/conf/shops/sell_outfits.xml @@ -16,18 +16,18 @@ --> - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/data/conf/zones/ados.xml b/data/conf/zones/ados.xml index 945ab0c438..f99c5f7c41 100644 --- a/data/conf/zones/ados.xml +++ b/data/conf/zones/ados.xml @@ -89,6 +89,70 @@ + + + + + + + + + + 100 + The entwives are NOT babysitters. Come back when you have some age on you. + + + + + + + + + + + + + + + 100 + The entwives are NOT babysitters. Come back when you have some age on you. + + + + + + + + + + + + + + + 100 + The entwives are NOT babysitters. Come back when you have some age on you. + + + + + + + + + + + + + + + 100 + The entwives are NOT babysitters. Come back when you have some age on you. + + + + + diff --git a/data/conf/zones/athor.xml b/data/conf/zones/athor.xml index 19c9a18f67..5f5d6a72a0 100644 --- a/data/conf/zones/athor.xml +++ b/data/conf/zones/athor.xml @@ -115,6 +115,7 @@ time varying(rain=-2, temperature=+5) + jump_and_run_tropics @@ -160,6 +161,7 @@ time varying(rain=-2, temperature=+5) + jump_and_run_tropics @@ -1082,6 +1084,7 @@ time varying(rain=-2, temperature=+5) + diff --git a/data/conf/zones/deniran.xml b/data/conf/zones/deniran.xml index 043d84d280..c06bef3c0b 100644 --- a/data/conf/zones/deniran.xml +++ b/data/conf/zones/deniran.xml @@ -277,14 +277,13 @@ - - + time varying + diff --git a/data/conf/zones/kikareukin.xml b/data/conf/zones/kikareukin.xml index 95df24700c..fa5efe64f5 100644 --- a/data/conf/zones/kikareukin.xml +++ b/data/conf/zones/kikareukin.xml @@ -12,6 +12,10 @@ file="Level 7/kikareukin/clouds.tmx"> time + clouds + semos_surrounds + + sky/cloud/clouds + + stars + sky/stars + + @@ -669,8 +674,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit class-name="games.stendhal.server.entity.mapstuff.portal.OneWayPortalDestination"/> + + + stars + sky/stars + + @@ -755,8 +766,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -828,8 +845,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -901,8 +924,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -974,8 +1003,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -1047,8 +1082,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -1120,8 +1161,14 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + + stars + sky/stars + + @@ -1146,6 +1193,11 @@ Unfortunately I will not be able to visit Athor this year. I have run into a bit + + view_wizards_tower + sky/view_wizards_tower + + + + view_wizards_tower + sky/view_wizards_tower + + + + + 60 44 diff --git a/data/conf/zones/sikhw.xml b/data/conf/zones/sikhw.xml index 2e92ba9623..d6c289eb2a 100644 --- a/data/conf/zones/sikhw.xml +++ b/data/conf/zones/sikhw.xml @@ -9,7 +9,7 @@ + file="Level 0/sikhw/desert_n_w2.tmx" accessible="false"> time @@ -20,7 +20,7 @@ + file="Level 0/sikhw/desert_w2.tmx" accessible="false"> time @@ -31,7 +31,7 @@ + file="Level 0/sikhw/desert_s_w2.tmx" accessible="false"> time @@ -42,7 +42,7 @@ + file="Level 0/sikhw/desert_s2_w2.tmx" accessible="false"> time @@ -55,7 +55,7 @@ + file="Level 0/sikhw/desert_nw.tmx" accessible="false"> time @@ -66,7 +66,7 @@ + file="Level 0/sikhw/desert_w.tmx" accessible="false"> time @@ -77,7 +77,7 @@ + file="Level 0/sikhw/desert_sw.tmx" accessible="false"> time @@ -88,7 +88,7 @@ + file="Level 0/sikhw/desert_s2_w.tmx" accessible="false"> time @@ -101,7 +101,7 @@ + file="Level 0/sikhw/desert_n.tmx" accessible="false"> time @@ -113,7 +113,7 @@ + file="Level 0/sikhw/city.tmx" accessible="false"> time @@ -124,7 +124,7 @@ + file="Level 0/sikhw/desert_s.tmx" accessible="false"> time @@ -135,7 +135,7 @@ + file="Level 0/sikhw/desert_s2.tmx" accessible="false"> time @@ -148,7 +148,7 @@ + file="Level 0/sikhw/desert_ne.tmx" accessible="false"> time @@ -159,7 +159,7 @@ + file="Level 0/sikhw/desert_e.tmx" accessible="false"> time @@ -170,7 +170,7 @@ + file="Level 0/sikhw/desert_se.tmx" accessible="false"> time @@ -180,7 +180,7 @@ + file="Level 0/sikhw/desert_s2_e.tmx" accessible="false"> time diff --git a/data/gui/quickmenu/pathfinding-disabled.png b/data/gui/quickmenu/pathfinding-disabled.png new file mode 100644 index 0000000000..fa2e3aaf55 Binary files /dev/null and b/data/gui/quickmenu/pathfinding-disabled.png differ diff --git a/data/gui/quickmenu/pathfinding-disabled.svg b/data/gui/quickmenu/pathfinding-disabled.svg new file mode 100644 index 0000000000..5d7d83a4b2 --- /dev/null +++ b/data/gui/quickmenu/pathfinding-disabled.svg @@ -0,0 +1,127 @@ + + + Path Finding Enabled + + + + + + + + + + + + + + + 2024-06-23 + + + Jordan Irwin (AntumDeluge) + + + Path Finding Enabled + + + Stendhal Game + + + + + icon + menu + button + cursor + boot + shoe + foot + walk + walking + move + moving + path + pathfinding + path finding + disable + disabled + + + + + + + + + + + + diff --git a/data/gui/quickmenu/pathfinding.png b/data/gui/quickmenu/pathfinding.png new file mode 100644 index 0000000000..252f985d33 Binary files /dev/null and b/data/gui/quickmenu/pathfinding.png differ diff --git a/data/gui/quickmenu/pathfinding.svg b/data/gui/quickmenu/pathfinding.svg new file mode 100644 index 0000000000..7e46e618a4 --- /dev/null +++ b/data/gui/quickmenu/pathfinding.svg @@ -0,0 +1,121 @@ + + + Path Finding Enabled + + + + + + + + + + + + + + 2024-06-23 + + + Jordan Irwin (AntumDeluge) + + + Path Finding Enabled + + + Stendhal Game + + + + + icon + menu + button + cursor + boot + shoe + foot + walk + walking + move + moving + path + pathfinding + path finding + enable + enabled + + + + + + + + + + + + diff --git a/data/languages/en.txt b/data/languages/en.txt deleted file mode 100644 index 2368fd02bd..0000000000 --- a/data/languages/en.txt +++ /dev/null @@ -1,1779 +0,0 @@ -wooden arrow=wooden arrow -You see a wooden arrow. With a supply of arrows in one hand and a bow in the other you can attack from a distance.=You see a wooden arrow. With a supply of arrows in one hand and a bow in the other you can attack from a distance. -steel arrow=steel arrow -You see a shining steel arrow. With plenty of these, and a good bow, you'll be a formidable archer.=You see a shining steel arrow. With plenty of these, and a good bow, you'll be a formidable archer. -golden arrow=golden arrow -You see a golden arrow. With a fist full of these and a good bow, you will surely take down your enemies quicker.=You see a golden arrow. With a fist full of these and a good bow, you will surely take down your enemies quicker. -power arrow=power arrow -You see a power arrow. The sting of this arrow will surely defeat your enemies, when equipped with a bow.=You see a power arrow. The sting of this arrow will surely defeat your enemies, when equipped with a bow. -barbarian armor=barbarian armor -You see a suit of barbarian armor. It's dirty and smelly, but still, rather strong.=You see a suit of barbarian armor. It's dirty and smelly, but still, rather strong. -black armor=black armor -You see a suit of black armor. Does it put you in a dark mood?=You see a suit of black armor. Does it put you in a dark mood? -blue armor=blue armor -You see a suit of blue armor. The color is stunning azure.=You see a suit of blue armor. The color is stunning azure. -chain armor=chain armor -You see a suit of chain armor. Perhaps even if you don't need it, you can sell it.=You see a suit of chain armor. Perhaps even if you don't need it, you can sell it. -chaos armor=chaos armor -You see a suit of chaos armor. The reflecting contrasting colors make no sense to you.=You see a suit of chaos armor. The reflecting contrasting colors make no sense to you. -dress=dress -You see a dress, which provides practically no defense. It is barely worth picking up.=You see a dress, which provides practically no defense. It is barely worth picking up. -dwarvish armor=dwarvish armor -You see a dwarvish armor. It's made with ingenuity and skill.=You see a dwarvish armor. It's made with ingenuity and skill. -elvish armor=elvish armor -You see a suit of elvish armor. It's made from tree leaves! But somehow constructed to give strong defense.=You see a suit of elvish armor. It's made from tree leaves! But somehow constructed to give strong defense. -enhanced chainmail=enhanced chainmail -You see a robust suit of enhanced chainmail. The links are closely spaced so even the sharpest point should not pierce it.=You see a robust suit of enhanced chainmail. The links are closely spaced so even the sharpest point should not pierce it. -golden armor=golden armor -You see a suit of golden armor. It's made from the best materials with great attention to detail.=You see a suit of golden armor. It's made from the best materials with great attention to detail. -golden chainmail=golden chainmail -You see a valuable suit of golden chainmail. It is crafted from fine links of expensive metal.=You see a valuable suit of golden chainmail. It is crafted from fine links of expensive metal. -ice armor=ice armor -You see a suit of ice armor, to protect even the coldest of hearts.=You see a suit of ice armor, to protect even the coldest of hearts. -iron scale armor=iron scale armor -You see a suit of iron scale armor, whose rounded scales will protect you from many blows.=You see a suit of iron scale armor, whose rounded scales will protect you from many blows. -leather armor=leather armor -You see a worn suit of leather armor. It will provide only minimal protection.=You see a worn suit of leather armor. It will provide only minimal protection. -leather cuirass=leather cuirass -You see a leather cuirass, well made but still able to be pierced by sharp swords.=You see a leather cuirass, well made but still able to be pierced by sharp swords. -leather scale armor=leather scale armor -You see a suit of leather scale armor. The leather is toughened somehow for better protection.=You see a suit of leather scale armor. The leather is toughened somehow for better protection. -magic plate armor=magic plate armor -You see a suit of magic plate armor. The steel is well tempered and the suit is very strong.=You see a suit of magic plate armor. The steel is well tempered and the suit is very strong. -mainio armor=mainio armor -You see a strange strange suit of mainio armor. What it lacks in subtlety it makes up for with strength.=You see a strange strange suit of mainio armor. What it lacks in subtlety it makes up for with strength. -mithril armor=mithril armor -You see a suit of mithril armor. Its tightly woven links are truly amazing.=You see a suit of mithril armor. Its tightly woven links are truly amazing. -pauldroned iron cuirass=pauldroned iron cuirass -You see a formidable pauldroned iron cuirass. The pauldrons (shoulder pieces) will protect your upper half.=You see a formidable pauldroned iron cuirass. The pauldrons (shoulder pieces) will protect your upper half. -pauldroned leather cuirass=pauldroned leather cuirass -You see a pauldroned leather cuirass. It was crafted to withstand minor attacks.=You see a pauldroned leather cuirass. It was crafted to withstand minor attacks. -plate armor=plate armor -You see a suit of plate armor. You imagine it would be heavy but the steel is well tempered and the suit is very strong.=You see a suit of plate armor. You imagine it would be heavy but the steel is well tempered and the suit is very strong. -red armor=red armor -You see a suit of red armor. The color is a striking crimson.=You see a suit of red armor. The color is a striking crimson. -scale armor=scale armor -You see a suit of scale armor. It is not flexible like chain, but will be a better defense.=You see a suit of scale armor. It is not flexible like chain, but will be a better defense. -shadow armor=shadow armor -You see a suit of shadow armor. It's hard to decide with what it is made, it is so dark.=You see a suit of shadow armor. It's hard to decide with what it is made, it is so dark. -stone armor=stone armor -You see a suit of stone armor. Swords will crumble if they try to pierce it.=You see a suit of stone armor. Swords will crumble if they try to pierce it. -studded armor=studded armor -You see a suit of studded armor. Some studs are loose or rusty but it is still fit for purpose.=You see a suit of studded armor. Some studs are loose or rusty but it is still fit for purpose. -xeno armor=xeno armor -You see a suit of xeno armor. The colors remind you of the xenocium hybrid creature.=You see a suit of xeno armor. The colors remind you of the xenocium hybrid creature. -axe=axe -You see a rusty but heavy axe. What it lacks in sharpness it makes up for in weight.=You see a rusty but heavy axe. What it lacks in sharpness it makes up for in weight. -bardiche=bardiche -You see a rare and powerful bardiche.=You see a rare and powerful bardiche. -battle axe=battle axe -You see a battle axe. It is well crafted and sharp despite its age.=You see a battle axe. It is well crafted and sharp despite its age. -black halberd=black halberd -You see a black halberd. It's immensely heavy so will make huge blows but you can only swing it at a very slow rate.=You see a black halberd. It's immensely heavy so will make huge blows but you can only swing it at a very slow rate. -black scythe=black scythe -You see a black scythe. Its attack is awesome for a farming tool.=You see a black scythe. Its attack is awesome for a farming tool. -chaos axe=chaos axe -You see a chaos axe. The handle is some alloy you can't recognize and the blade is golden colored, but thinking too long about it makes you confused.=You see a chaos axe. The handle is some alloy you can't recognize and the blade is golden colored, but thinking too long about it makes you confused. -durin axe=durin axe -You see a durin axe, a great prize from the dwarves.=You see a durin axe, a great prize from the dwarves. -golden twoside axe=golden twoside axe -You see a golden twoside axe. This axe is a rare find.=You see a golden twoside axe. This axe is a rare find. -halberd=halberd -You see a rare halberd, with an axe's blade fused onto a shaft. It is both heavy and keen-edged.=You see a rare halberd, with an axe's blade fused onto a shaft. It is both heavy and keen-edged. -hand axe=hand axe -You see a hand axe, well worn from years of use but may prove useful.=You see a hand axe, well worn from years of use but may prove useful. -magic twoside axe=magic twoside axe -You see a magic twoside axe. The nature of the magic isn't clear to you.=You see a magic twoside axe. The nature of the magic isn't clear to you. -old scythe=old scythe -You see an old scythe, it used to be weapon of death. But now it is only a useful tool for farming.=You see an old scythe, it used to be weapon of death. But now it is only a useful tool for farming. -scythe=scythe -You see the weapon of death - a scythe. It looks dangerous but perhaps also a useful tool for farming.=You see the weapon of death - a scythe. It looks dangerous but perhaps also a useful tool for farming. -sickle=sickle -You see a sickle, it is a useful tool for farming.=You see a sickle, it is a useful tool for farming. -small axe=small axe -You see a small axe. It appears well made but you suspect it is too light to do much damage.=You see a small axe. It appears well made but you suspect it is too light to do much damage. -twoside axe=twoside axe -You see a well crafted twoside axe.=You see a well crafted twoside axe. -black book=black book -You see the black book that Jynath kept out for so long.=You see the black book that Jynath kept out for so long. -blue book=blue book -You see a book with the words GEMS AND MINERALS on the front cover.=You see a book with the words GEMS AND MINERALS on the front cover. -black boots=black boots -You stub your toe on this pair of black boots. Perhaps they bring you bad luck?=You stub your toe on this pair of black boots. Perhaps they bring you bad luck? -blue boots=blue boots -You see a pair of blue boots.=You see a pair of blue boots. -chain boots=chain boots -You see a pair of chain boots, quite rusty but still strong.=You see a pair of chain boots, quite rusty but still strong. -chaos boots=chaos boots -You see a pair of chaos boots. Your steps are unsteady in them, but the extra defense they give is worth it.=You see a pair of chaos boots. Your steps are unsteady in them, but the extra defense they give is worth it. -elvish boots=elvish boots -You see a pair of elvish boots.=You see a pair of elvish boots. -golden boots=golden boots -You see a pair of golden boots. They will be heavy to wear but well worth their weight.=You see a pair of golden boots. They will be heavy to wear but well worth their weight. -green thing boots=green thing boots -You see a pair of green thing boots. They're slimy inside, ewwww.=You see a pair of green thing boots. They're slimy inside, ewwww. -killer boots=killer boots -You see a high heeled pair of killer boots. Are you brave enough to wear them?=You see a high heeled pair of killer boots. Are you brave enough to wear them? -leather boots=leather boots -You see a simple pair of leather boots to give your feet some protection.=You see a simple pair of leather boots to give your feet some protection. -mainio boots=mainio boots -You see a pair of mainio boots. It's hard to believe that boots looking like this would protect your feet so well as they do.=You see a pair of mainio boots. It's hard to believe that boots looking like this would protect your feet so well as they do. -mithril boots=mithril boots -You see a sparkling set of mithril boots.=You see a sparkling set of mithril boots. -red boots=red boots -You see a pair of red boots, the color reminds you of fresh blood.=You see a pair of red boots, the color reminds you of fresh blood. -shadow boots=shadow boots -You see a pair of shadow boots. You can't fathom what they are made of.=You see a pair of shadow boots. You can't fathom what they are made of. -steel boots=steel boots -You see a pair of steel boots, fit for any soldier.=You see a pair of steel boots, fit for any soldier. -stone boots=stone boots -You see a pair of stone boots. They may be heavy, but they protect your feet well.=You see a pair of stone boots. They may be heavy, but they protect your feet well. -studded boots=studded boots -You see a pair of worn studded boots with moderate protection for your feet.=You see a pair of worn studded boots with moderate protection for your feet. -xeno boots=xeno boots -You see a fine specimen of craftmanship, a pair of xeno boots.=You see a fine specimen of craftmanship, a pair of xeno boots. -present=present -You see a present. Open it.=You see a present. Open it. -basket=basket -You see an easter basket. Look inside.=You see an easter basket. Look inside. -stocking=stocking -You see a christmas stocking. Look inside.=You see a christmas stocking. Look inside. -black cloak=black cloak -You see a black cloak. The last owner had black thoughts. Will you?=You see a black cloak. The last owner had black thoughts. Will you? -black dragon cloak=black dragon cloak -You see a torn section from an enormous black dragon cloak. You're very lucky: the piece is just the right size for a human to wear.=You see a torn section from an enormous black dragon cloak. You're very lucky: the piece is just the right size for a human to wear. -blue dragon cloak=blue dragon cloak -You see a blue dragon cloak. The weave is tight and strong, though you wish the color drew less attention.=You see a blue dragon cloak. The weave is tight and strong, though you wish the color drew less attention. -blue elf cloak=blue elf cloak -You see a blue elf cloak. The material is thick and warm.=You see a blue elf cloak. The material is thick and warm. -blue striped cloak=blue striped cloak -You see a fashionable blue striped cloak.=You see a fashionable blue striped cloak. -bone dragon cloak=bone dragon cloak -You see a bone dragon cloak. It's incredibly old, like its last owner.=You see a bone dragon cloak. It's incredibly old, like its last owner. -chaos cloak=chaos cloak -You see a chaos cloak. The colors look like oil shimmering on a puddle.=You see a chaos cloak. The colors look like oil shimmering on a puddle. -cloak=cloak -You see a simple cloth cloak.=You see a simple cloth cloak. -dwarf cloak=dwarf cloak -You see a dwarf cloak. Apparently this passes for fashion in some parts, but to you it looks more functional than fashionable.=You see a dwarf cloak. Apparently this passes for fashion in some parts, but to you it looks more functional than fashionable. -elf cloak=elf cloak -You see a white elf cloak. It doesn't look very warm.=You see a white elf cloak. It doesn't look very warm. -elvish cloak=elvish cloak -You see an elvish cloak. The material is dark, but thick and warm.=You see an elvish cloak. The material is dark, but thick and warm. -golden cloak=golden cloak -You see a golden cloak. See how the light glistens off the gold, it is a prized possession.=You see a golden cloak. See how the light glistens off the gold, it is a prized possession. -green dragon cloak=green dragon cloak -You see a huge green dragon cloak. It's probably too big for you but, even dragging on the floor it will provide excellent protection.=You see a huge green dragon cloak. It's probably too big for you but, even dragging on the floor it will provide excellent protection. -lich cloak=lich cloak -You see a purple lich cloak. It's stained with blood - yours, from the previous undead owner, or perhaps the blood of his enemies...=You see a purple lich cloak. It's stained with blood - yours, from the previous undead owner, or perhaps the blood of his enemies... -mainio cloak=mainio cloak -You see a mainio cloak. Its flashing colors are absurd! But you wear it because it's very well made.=You see a mainio cloak. Its flashing colors are absurd! But you wear it because it's very well made. -mithril cloak=mithril cloak -You see a mithril cloak. Its weave is so fine and its looks are breathtaking.=You see a mithril cloak. Its weave is so fine and its looks are breathtaking. -red cloak=red cloak -You see a red cloak covered in polka dots. It's terribly snazzy.=You see a red cloak covered in polka dots. It's terribly snazzy. -red dragon cloak=red dragon cloak -You see a red dragon cloak. He didn't have one for a long time but finally caved in to peer pressure and got one.=You see a red dragon cloak. He didn't have one for a long time but finally caved in to peer pressure and got one. -rift cloak=rift cloak -You see a rift cloak. Almost useless in the battle, but saves your life when you enter a magical rift.=You see a rift cloak. Almost useless in the battle, but saves your life when you enter a magical rift. -shadow cloak=shadow cloak -You see a shadow cloak, in colors of the shadow armor.=You see a shadow cloak, in colors of the shadow armor. -stone cloak=stone cloak -You see a stone cloak. It's hard to tell how it was made, as the fibres look like narrow threads of mineral.=You see a stone cloak. It's hard to tell how it was made, as the fibres look like narrow threads of mineral. -vampire cloak=vampire cloak -You see a vibrant red vampire cloak. Wearing it sends a shiver down your spine.=You see a vibrant red vampire cloak. Wearing it sends a shiver down your spine. -xeno cloak=xeno cloak -You see a xeno cloak. It's heavy and thick, giving you a good protection.=You see a xeno cloak. It's heavy and thick, giving you a good protection. -chaos hammer=chaos hammer -You see a chaos hammer. It is a fearsome weapon.=You see a chaos hammer. It is a fearsome weapon. -club=club -You see a wooden club. It won't kill much but is better than no weapon.=You see a wooden club. It won't kill much but is better than no weapon. -club of thorns=club of thorns -You see the club of thorns. The thorns are full with blood of tortured humans. It is hugely powerful but will drain your life to use it.=You see the club of thorns. The thorns are full with blood of tortured humans. It is hugely powerful but will drain your life to use it. -enhanced mace=enhanced mace -You see an enhanced mace. The heavy attacking end makes it hard to balance.=You see an enhanced mace. The heavy attacking end makes it hard to balance. -flail=flail -You see a flail. Swinging it in circles will tire you but it will be an effective weapon.=You see a flail. Swinging it in circles will tire you but it will be an effective weapon. -golden hammer=golden hammer -You see a valuable golden hammer. This golden weapon may be worth a lot to someone. It has a slow rate of attack.=You see a valuable golden hammer. This golden weapon may be worth a lot to someone. It has a slow rate of attack. -golden mace=golden mace -You see a shining golden mace. Despite looking ornamental, it is sharp enough to do some damage.=You see a shining golden mace. Despite looking ornamental, it is sharp enough to do some damage. -hammer=hammer -You see a common hammer. It is heavy enough that you should be able to bludgeon your enemies with it, but only slowly=You see a common hammer. It is heavy enough that you should be able to bludgeon your enemies with it, but only slowly -ice war hammer=ice war hammer -You see a ice war hammer. The last owner must have been huge, to wield it.=You see a ice war hammer. The last owner must have been huge, to wield it. -mace=mace -You see a mace, much neglected in years of use.=You see a mace, much neglected in years of use. -morning star=morning star -You see a morning star. This is a very rare item indeed.=You see a morning star. This is a very rare item indeed. -necromancers staff=necromancers staff -You see a cunningly beautiful staff. If you listen carefully you can hear its previous owners' souls screaming from within it.=You see a cunningly beautiful staff. If you listen carefully you can hear its previous owners' souls screaming from within it. -rod of the gm=rod of the gm -You see a rod of the GM. This rod is reserved for those with special powers, be careful not to let it fall into the wrong hands.=You see a rod of the GM. This rod is reserved for those with special powers, be careful not to let it fall into the wrong hands. -skull staff=skull staff -You see a rare skull staff. It appears mystical but will work like any other staff.=You see a rare skull staff. It appears mystical but will work like any other staff. -staff=staff -You see a staff. This is a seldom seen or used weapon.=You see a staff. This is a seldom seen or used weapon. -ugmash=ugmash -You see a huge ugmash. It can attack with a faster rate than many other clubs.=You see a huge ugmash. It can attack with a faster rate than many other clubs. -vulcano hammer=vulcano hammer -You see a vulcano hammer. It's said to be a weapon of a God, but attacks with a snail like pace.=You see a vulcano hammer. It's said to be a weapon of a God, but attacks with a snail like pace. -war hammer=war hammer -You see a huge war hammer. It seems strong but is so heavy that you can only lift it to attack every so often..=You see a huge war hammer. It seems strong but is so heavy that you can only lift it to attack every so often.. -empty sack=empty sack -You see an empty sack. It will not stand up, unless you fill it first.=You see an empty sack. It will not stand up, unless you fill it first. -flask=flask -You see a flask. This flask is good for storing medicine.=You see a flask. This flask is good for storing medicine. -eared bottle=eared bottle -You see an eared bottle. It is empty, someone drank the contents.=You see an eared bottle. It is empty, someone drank the contents. -fat bottle=fat bottle -You see a fat bottle. It is empty, someone drank the contents.=You see a fat bottle. It is empty, someone drank the contents. -slim bottle=slim bottle -You see a slim bottle. It is empty, someone drank the contents.=You see a slim bottle. It is empty, someone drank the contents. -empty goblet=empty goblet -You see an empty goblet. You wonder what could it hold.=You see an empty goblet. You wonder what could it hold. -goblet=goblet -You see a goblet. You look at its contents, and shiver at the blood inside.=You see a goblet. You look at its contents, and shiver at the blood inside. -keyring=keyring -You see a keyring.=You see a keyring. -red emotion crystal=red emotion crystal -You see a red emotion crystal. It feels hot to the touch.=You see a red emotion crystal. It feels hot to the touch. -purple emotion crystal=purple emotion crystal -You see a purple emotion crystal. It feels cold to the touch.=You see a purple emotion crystal. It feels cold to the touch. -yellow emotion crystal=yellow emotion crystal -You see a yellow emotion crystal. It energizes you.=You see a yellow emotion crystal. It energizes you. -pink emotion crystal=pink emotion crystal -You see a pink emotion crystal. It almost feels soft.=You see a pink emotion crystal. It almost feels soft. -blue emotion crystal=blue emotion crystal -You see a blue emotion crystal. While holding it your worries seem to disappear.=You see a blue emotion crystal. While holding it your worries seem to disappear. -note=note -You see a note.=You see a note. -coupon=coupon -You see a coupon.=You see a coupon. -closed envelope=closed envelope -You see a closed envelope.=You see a closed envelope. -opened envelope=opened envelope -You see an opened envelope.=You see an opened envelope. -sealed envelope=sealed envelope -You see a sealed envelope.=You see a sealed envelope. -unsealed envelope=unsealed envelope -You see an unsealed envelope.=You see an unsealed envelope. -assassins id=assassins id -You see an assassin's ID. Perhaps this can be put to good use.=You see an assassin's ID. Perhaps this can be put to good use. -map=map -You see a map.=You see a map. -blank scroll=blank scroll -You see a rolled up blank scroll.=You see a rolled up blank scroll. -ice scroll=ice scroll -You see a scroll, circled by ice crystals. You can feel its cold aura.=You see a scroll, circled by ice crystals. You can feel its cold aura. -fierywater=fierywater -You see a bottle of fierywater. Do not drink it pure, you would evaporate!=You see a bottle of fierywater. Do not drink it pure, you would evaporate! -mild koboldish torcibud=mild koboldish torcibud -You see a bottle of mild koboldish torcibud. It will help you recover when you're all bruised and battered.=You see a bottle of mild koboldish torcibud. It will help you recover when you're all bruised and battered. -strong koboldish torcibud=strong koboldish torcibud -You see a bottle of strong koboldish torcibud. It will help you recover when you're all bruised and battered.=You see a bottle of strong koboldish torcibud. It will help you recover when you're all bruised and battered. -vsop koboldish torcibud=vsop koboldish torcibud -You see a bottle of VSOP koboldish torcibud, specially aged in oak casks wrapped by the darkness of a dungeon cellar. It will help you recover when you're all bruised and battered.=You see a bottle of VSOP koboldish torcibud, specially aged in oak casks wrapped by the darkness of a dungeon cellar. It will help you recover when you're all bruised and battered. -apple juice=apple juice -You see apple juice. On a hot summer day, it would taste great.=You see apple juice. On a hot summer day, it would taste great. -beer=beer -You see a beer. On a hot summer day, it would taste great.=You see a beer. On a hot summer day, it would taste great. -wine=wine -You see a glass of red wine. It will quench your thirst.=You see a glass of red wine. It will quench your thirst. -water=water -You see a bottle of fresh spring water. It's really tasty and fresh.=You see a bottle of fresh spring water. It's really tasty and fresh. -pina colada=pina colada -You see a pina colada. It looks delicious but tastes dangerously strong!=You see a pina colada. It looks delicious but tastes dangerously strong! -tea=tea -You see a lovely cup of tea.=You see a lovely cup of tea. -milk=milk -You see a small bottle of fresh milk.=You see a small bottle of fresh milk. -antidote=antidote -You see an antidote, to cure you of poison and protect you for a while.=You see an antidote, to cure you of poison and protect you for a while. -greater antidote=greater antidote -You see a greater antidote, to cure you of poison and protect you for a long while.=You see a greater antidote, to cure you of poison and protect you for a long while. -mana=mana -You see an bottle of mana, to increase your mana rapidly.=You see an bottle of mana, to increase your mana rapidly. -poison=poison -You see a bottle of poison. Don't drink it!=You see a bottle of poison. Don't drink it! -greater poison=greater poison -You see a greater poison. Don't drink it!=You see a greater poison. Don't drink it! -deadly poison=deadly poison -You see a deadly poison. Don't drink it!=You see a deadly poison. Don't drink it! -disease poison=disease poison -You see a disease poison. Be careful, it's wicked.=You see a disease poison. Be careful, it's wicked. -mega poison=mega poison -You see a mega poison. This is some wicked poison.=You see a mega poison. This is some wicked poison. -twilight elixir=twilight elixir -You see a bottle of medicinal elixir.=You see a bottle of medicinal elixir. -minor potion=minor potion -You see a minor potion. It's not much but may save you in a tight spot.=You see a minor potion. It's not much but may save you in a tight spot. -soup=soup -You see a bowl of soup. Its contents fill you up.=You see a bowl of soup. Its contents fill you up. -fish soup=fish soup -You see a bowl of fish soup. Its contents fill you up.=You see a bowl of fish soup. Its contents fill you up. -potion=potion -You see a potion. Take it to battle and you may survive longer.=You see a potion. Take it to battle and you may survive longer. -greater potion=greater potion -You see a greater potion, invaluable in battle.=You see a greater potion, invaluable in battle. -mega potion=mega potion -You see a mega potion. It strengthens greatly.=You see a mega potion. It strengthens greatly. -love potion=love potion -You see a love potion. It looks powerful so take care to use it carefully!=You see a love potion. It looks powerful so take care to use it carefully! -rhosyd=rhosyd -You see a beautiful and rare rhosyd flower.=You see a beautiful and rare rhosyd flower. -rose=rose -You see a single red rose. It smells heavenly.=You see a single red rose. It smells heavenly. -lilia=lilia -You see a lilia, a single precious bloom.=You see a lilia, a single precious bloom. -daisies=daisies -You see a bunch of daisies. They are cheap and cheerful!=You see a bunch of daisies. They are cheap and cheerful! -pansy=pansy -You see a pansy, so colorful and beautiful.=You see a pansy, so colorful and beautiful. -zantedeschia=zantedeschia -You see a zantedeschia, a delicate bloom with the sweetest fragrance.=You see a zantedeschia, a delicate bloom with the sweetest fragrance. -apple=apple -You see a nice shiny apple.=You see a nice shiny apple. -black apple=black apple -You see a black apple. It must be a gift from lil Johnnnny. He's the guy with black items.=You see a black apple. It must be a gift from lil Johnnnny. He's the guy with black items. -purple apple=purple apple -You see a purple apple. It is loaded with memories= -banana=banana -You see a banana. Take care of monkeys when you have them in your bag!=You see a banana. Take care of monkeys when you have them in your bag! -cherry=cherry -You see a cherry. Its tangy taste revives you a little.=You see a cherry. Its tangy taste revives you a little. -coconut=coconut -You see a coconut. Luckily it's already cracked and you can drink from it.=You see a coconut. Luckily it's already cracked and you can drink from it. -grapes=grapes -You see a bunch of grapes. They look quite juicy and tasty.=You see a bunch of grapes. They look quite juicy and tasty. -olive=olive -You see a small ripe olive. It can be eaten or used to create olive oil.=You see a small ripe olive. It can be eaten or used to create olive oil. -pear=pear -You see a pear. Its lovely smell convinces you of taking it.=You see a pear. Its lovely smell convinces you of taking it. -pineapple=pineapple -You see a tropical pineapple.=You see a tropical pineapple. -pomegranate=pomegranate -You see a pomegranate. If you eat it, you will be in a great shape soon.=You see a pomegranate. If you eat it, you will be in a great shape soon. -watermelon=watermelon -You see a watermelon. It freshes your throat during hot summer days.=You see a watermelon. It freshes your throat during hot summer days. -artichoke=artichoke -You see an artichoke. It can be an ingredient, or you can eat it.=You see an artichoke. It can be an ingredient, or you can eat it. -broccoli=broccoli -You see a bunch of broccoli.=You see a bunch of broccoli. -carrot=carrot -You see a carrot. It can be an ingredient, or you can eat it.=You see a carrot. It can be an ingredient, or you can eat it. -cauliflower=cauliflower -You see a fresh head of cauliflower.=You see a fresh head of cauliflower. -courgette=courgette -You see a courgette. It can be an ingredient, or you can eat it.=You see a courgette. It can be an ingredient, or you can eat it. -collard=collard -You see a collard. It's rare and strangely magical.=You see a collard. It's rare and strangely magical. -garlic=garlic -You see some fresh garlic.=You see some fresh garlic. -leek=leek -You see a fresh leek.=You see a fresh leek. -onion=onion -You see an onion.=You see an onion. -salad=salad -You see a salad.=You see a salad. -spinach=spinach -You see spinach. It is enriched with vitamins.=You see spinach. It is enriched with vitamins. -tomato=tomato -You see a tomato, put it in a salad or eat it.=You see a tomato, put it in a salad or eat it. -egg=egg -You see an egg.=You see an egg. -button mushroom=button mushroom -You see a button mushroom. It can be an ingredient, or you can eat it.=You see a button mushroom. It can be an ingredient, or you can eat it. -porcini=porcini -You see a porcini. It can be an ingredient, or you can eat it.=You see a porcini. It can be an ingredient, or you can eat it. -toadstool=toadstool -You see a toadstool. Beware!=You see a toadstool. Beware! -char=char -You see a char, with a fleshy pink belly.=You see a char, with a fleshy pink belly. -cod=cod -You see a cod.=You see a cod. -mackerel=mackerel -You see a mackerel.=You see a mackerel. -perch=perch -You see a perch.=You see a perch. -roach=roach -You see a roach.=You see a roach. -clownfish=clownfish -You see a striped clownfish.=You see a striped clownfish. -red lionfish=red lionfish -You see a venomous coral reef red lionfish.=You see a venomous coral reef red lionfish. -surgeonfish=surgeonfish -You see a bright blue surgeonfish.=You see a bright blue surgeonfish. -trout=trout -You see a trout.=You see a trout. -bread=bread -You see a loaf of bread. It is freshly baked and ready to eat.=You see a loaf of bread. It is freshly baked and ready to eat. -crepes suzette=crepes suzette -You see a crepes suzette topped with hot chocolate sauce.=You see a crepes suzette topped with hot chocolate sauce. -apple pie=apple pie -You see a delicious apple pie, made with only genuine ingredients.=You see a delicious apple pie, made with only genuine ingredients. -cherry pie=cherry pie -You see a delicious cherry pie, made with only genuine ingredients.=You see a delicious cherry pie, made with only genuine ingredients. -pie=pie -You see a meat and vegetable pie.=You see a meat and vegetable pie. -fish pie=fish pie -You see a fish and leek pie. It's not too stodgy so you can eat it faster than a meat pie.=You see a fish and leek pie. It's not too stodgy so you can eat it faster than a meat pie. -sandwich=sandwich -You see a ham and cheese sandwich.=You see a ham and cheese sandwich. -smoked ham=smoked ham -You see a smoked ham.=You see a smoked ham. -smoked meat=smoked meat -You see a piece of smoked meat.=You see a piece of smoked meat. -smoked cod=smoked cod -You see a smoked cod.=You see a smoked cod. -smoked trout=smoked trout -You see a smoked trout.=You see a smoked trout. -sugar=sugar -You see a sack of raw sugar.=You see a sack of raw sugar. -butter=butter -You see some butter. It is an ingredient or can be eaten if you like.=You see some butter. It is an ingredient or can be eaten if you like. -honey=honey -You see a jar of sticky sweet honey. It's the clear runny kind.=You see a jar of sticky sweet honey. It's the clear runny kind. -icecream=icecream -You see a wafer cone with vanilla ice cream and a chocolate flake.=You see a wafer cone with vanilla ice cream and a chocolate flake. -licorice=licorice -You see some delicious licorice. Your mouth waters.=You see some delicious licorice. Your mouth waters. -hotdog=hotdog -You see what looks like a tasty hotdog on a bun.=You see what looks like a tasty hotdog on a bun. -cheeseydog=cheeseydog -You see what looks like a tasty cheeseydog on a bun.=You see what looks like a tasty cheeseydog on a bun. -cheese sausage=cheese sausage -You see what looks like a tasty cheese sausage.=You see what looks like a tasty cheese sausage. -sausage=sausage -You see what looks like a tasty sausage.=You see what looks like a tasty sausage. -tuna sandwich=tuna sandwich -You see what looks like tuna sandwich.=You see what looks like tuna sandwich. -chocolate shake=chocolate shake -You see a scrumptious chocolate shake. Absolutely NO calories.=You see a scrumptious chocolate shake. Absolutely NO calories. -vanilla shake=vanilla shake -You see a yummy vanilla shake. Absolutely NO calories.=You see a yummy vanilla shake. Absolutely NO calories. -cheese=cheese -You see some cheese. Eat it and see what happens.=You see some cheese. Eat it and see what happens. -chicken=chicken -You see a tasty leg of chicken.=You see a tasty leg of chicken. -chocolate bar=chocolate bar -You see an absolutely yummy chocolate bar.=You see an absolutely yummy chocolate bar. -fairy cake=fairy cake -You see a luscious fairy cake with icing and sprinkles on top.=You see a luscious fairy cake with icing and sprinkles on top. -ham=ham -You see a juicy ham.=You see a juicy ham. -meat=meat -You see a piece of meat.=You see a piece of meat. -easter egg=easter egg -You see a lovely chocolate easter egg. It's huge!=You see a lovely chocolate easter egg. It's huge! -small easter egg=small easter egg -You see a small chocolate easter egg. Yummy!=You see a small chocolate easter egg. Yummy! -grilled steak=grilled steak -You see a fresh grilled steak. It smells awesome and is really juicy.=You see a fresh grilled steak. It smells awesome and is really juicy. -pizza=pizza -You see a tasty pizza, and hope they put extra cheese on it.=You see a tasty pizza, and hope they put extra cheese on it. -leather helmet=leather helmet -You see a leather helmet, it seems more like a hat than a defensive item.=You see a leather helmet, it seems more like a hat than a defensive item. -trophy helmet=trophy helmet -You see a Trophy Helmet. Those who were brave and finished the death match, win this helmet.=You see a Trophy Helmet. Those who were brave and finished the death match, win this helmet. -robins hat=robins hat -You see a robins hat. It's cute but that's about it.=You see a robins hat. It's cute but that's about it. -studded helmet=studded helmet -You see a studded helmet, it should protect your head against cuts but perhaps not hard blows.=You see a studded helmet, it should protect your head against cuts but perhaps not hard blows. -elvish hat=elvish hat -You see an elvish hat. The elf craftmanship has made the leafy material stronger than one would think.=You see an elvish hat. The elf craftmanship has made the leafy material stronger than one would think. -chain helmet=chain helmet -You see a solid chain helmet, it will be excellent protection for your head.=You see a solid chain helmet, it will be excellent protection for your head. -viking helmet=viking helmet -You see a viking helmet. The previous owner wore it to many battles.=You see a viking helmet. The previous owner wore it to many battles. -aventail=aventail -You see a aventail. It's a clever design.=You see a aventail. It's a clever design. -blue helmet=blue helmet -You see a brand new blue helmet.=You see a brand new blue helmet. -red helmet=red helmet -You see a red helmet, it reminds you of a jester's hat but you can be sure it has better protection than that.=You see a red helmet, it reminds you of a jester's hat but you can be sure it has better protection than that. -golden helmet=golden helmet -You see a heavy golden helmet. The thick metal should protect you against almost anything.=You see a heavy golden helmet. The thick metal should protect you against almost anything. -horned golden helmet=horned golden helmet -You see a horned golden helmet.=You see a horned golden helmet. -shadow helmet=shadow helmet -You see a heavy shadow helmet. Instead of shining, it darkly gleams.=You see a heavy shadow helmet. Instead of shining, it darkly gleams. -xeno helmet=xeno helmet -You see a xeno helmet.=You see a xeno helmet. -chaos helmet=chaos helmet -You see a chaos helmet.=You see a chaos helmet. -mainio helmet=mainio helmet -You see a strange mainio helmet. You can barely see when wearing it, but it protects your head very well.=You see a strange mainio helmet. You can barely see when wearing it, but it protects your head very well. -magic chain helmet=magic chain helmet -You see a magic chain helmet.=You see a magic chain helmet. -liberty helmet=liberty helmet -You see the liberty helmet. It's light and strong, you feel free wearing it.=You see the liberty helmet. It's light and strong, you feel free wearing it. -black helmet=black helmet -You see a black helmet.=You see a black helmet. -mithril helmet=mithril helmet -You see a mithril helmet.=You see a mithril helmet. -arandula=arandula -You see a arandula. It has medicinal properties, if the right person uses it.=You see a arandula. It has medicinal properties, if the right person uses it. -kokuda=kokuda -You see a kokuda. It has medicinal properties, if the right person uses it.=You see a kokuda. It has medicinal properties, if the right person uses it. -kekik=kekik -You see a kekik. It is a herb which can have healing properties.=You see a kekik. It is a herb which can have healing properties. -mandragora=mandragora -You see a root of mandragora. It has medicinal properties, if the right person uses it.=You see a root of mandragora. It has medicinal properties, if the right person uses it. -sclaria=sclaria -You see the herb sclaria. The smell is calming.=You see the herb sclaria. The smell is calming. -reindeer moss=reindeer moss -You see reindeer moss, something that grows only around Semos. It is quite a delicacy to reindeer, but extremely bitter to humans.=You see reindeer moss, something that grows only around Semos. It is quite a delicacy to reindeer, but extremely bitter to humans. -emerald=emerald -You see an emerald, a precious gemstone.=You see an emerald, a precious gemstone. -sapphire=sapphire -You see a sapphire, a precious gemstone.=You see a sapphire, a precious gemstone. -carbuncle=carbuncle -You see a carbuncle, a precious gemstone related to the garnet.=You see a carbuncle, a precious gemstone related to the garnet. -obsidian=obsidian -You see a piece of obsidian, it's known as volcanic glass because it forms when lava cools rapidly. When used as a gemstone it may look jet black or silvery grey, depending on how it is cut.=You see a piece of obsidian, it's known as volcanic glass because it forms when lava cools rapidly. When used as a gemstone it may look jet black or silvery grey, depending on how it is cut. -diamond=diamond -You see a diamond, not just a beautiful gem but also the hardest naturally occurring material .=You see a diamond, not just a beautiful gem but also the hardest naturally occurring material . -black pearl=black pearl -You see a black pearl, so dark and shiny.=You see a black pearl, so dark and shiny. -dungeon silver key=dungeon silver key -You see a silver dungeon key. Use it only if you are brave.=You see a silver dungeon key. Use it only if you are brave. -lich gold key=lich gold key -You see a golden lich key. It will open the door to terrible things.=You see a golden lich key. It will open the door to terrible things. -minotaur key=minotaur key -You see a fancy bronze key. It smells faintly of minotaur, perhaps one dropped it?=You see a fancy bronze key. It smells faintly of minotaur, perhaps one dropped it? -nalwor bank key=nalwor bank key -You see a key to the customer room of Nalwor Bank.=You see a key to the customer room of Nalwor Bank. -kotoch prison key=kotoch prison key -You see an ancient copper key, enscribed with something in Orcish.=You see an ancient copper key, enscribed with something in Orcish. -kanmararn prison key=kanmararn prison key -You see a key which matches the locks in the Kanmararn prison.=You see a key which matches the locks in the Kanmararn prison. -sedah gate key=sedah gate key -You see a key for the gate to the imperial city of Sedah.=You see a key for the gate to the imperial city of Sedah. -pet sanctuary key=pet sanctuary key -You see a key for the pet sanctuary in Ados.=You see a key for the pet sanctuary in Ados. -house key=house key -You should not see this description=You should not see this description -gate key=gate key -You should not see this description=You should not see this description -master key=master key -You see a master key.=You see a master key. -small key=small key -You see the key for Zara's row house in Ados.=You see the key for Zara's row house in Ados. -black legs=black legs -You see a pair of black legs.=You see a pair of black legs. -blue legs=blue legs -You see a pair of shiny new blue legs.=You see a pair of shiny new blue legs. -chain legs=chain legs -You see a pair of well made chain legs. They should protect you against any attacks aimed low.=You see a pair of well made chain legs. They should protect you against any attacks aimed low. -chaos legs=chaos legs -You see a pair of chaos legs. The strange colors confuse you.=You see a pair of chaos legs. The strange colors confuse you. -dwarvish legs=dwarvish legs -You see a pair of dwarvish legs, designed by master craftsmen.=You see a pair of dwarvish legs, designed by master craftsmen. -elvish legs=elvish legs -You see a pair of elvish legs.=You see a pair of elvish legs. -golden legs=golden legs -You see a pair of shining, strong golden legs.=You see a pair of shining, strong golden legs. -jewelled legs=jewelled legs -You see a beautiful pair of jewelled legs. A large red gem glints at you and they're encrusted all over with sapphires.=You see a beautiful pair of jewelled legs. A large red gem glints at you and they're encrusted all over with sapphires. -leather legs=leather legs -You see a pair of leather legs. They look new but they're poor quality.=You see a pair of leather legs. They look new but they're poor quality. -mainio legs=mainio legs -You see a pair of odd looking mainio legs. They are somewhat startling but they give you a good protection.=You see a pair of odd looking mainio legs. They are somewhat startling but they give you a good protection. -mithril legs=mithril legs -You see a pair of mithril legs. They will serve you well.=You see a pair of mithril legs. They will serve you well. -red legs=red legs -A pair of red legs are in your grasp.=A pair of red legs are in your grasp. -shadow legs=shadow legs -You see a pair of shadow legs. The weight of them is incredible.=You see a pair of shadow legs. The weight of them is incredible. -stone legs=stone legs -You see a pair of stone legs, heavy and durable.=You see a pair of stone legs, heavy and durable. -studded legs=studded legs -You see a pair of blood stained studded legs. You're not sure if the blood came from their last owner ... or his enemies.=You see a pair of blood stained studded legs. You're not sure if the blood came from their last owner ... or his enemies. -xeno legs=xeno legs -You see a pair of xeno legs. They're very strong and look brand new.=You see a pair of xeno legs. They're very strong and look brand new. -bulb=bulb -You see a bulb. It can be planted anywhere, but it will only thrive on fertile ground.=You see a bulb. It can be planted anywhere, but it will only thrive on fertile ground. -seed=seed -You see a seed. It can be planted anywhere, but it will only thrive on fertile ground.=You see a seed. It can be planted anywhere, but it will only thrive on fertile ground. -giant heart=giant heart -You see a giant heart. This proves that a giant was defeated, and has magical properties that you are not sure of.=You see a giant heart. This proves that a giant was defeated, and has magical properties that you are not sure of. -dice=dice -You see a set of dice. May lady luck be with you.=You see a set of dice. May lady luck be with you. -cards=cards -You see a deck of cards. Shuffle them well, to get a winning hand.=You see a deck of cards. Shuffle them well, to get a winning hand. -teddy=teddy -You see a teddy bear. Its soft and cuddly, and children love them.=You see a teddy bear. Its soft and cuddly, and children love them. -rodent trap=rodent trap -You see a rodent trap.=You see a rodent trap. -vampirette entrails=vampirette entrails -You see some vampirette entrails. These guts proves that a vampirette was slain.=You see some vampirette entrails. These guts proves that a vampirette was slain. -bat entrails=bat entrails -You see some bat entrails, full of the blood of a living creature.=You see some bat entrails, full of the blood of a living creature. -suntan cream=suntan cream -You see a suntan cream.=You see a suntan cream. -marbles=marbles -You see some marbles. Boys will be boys.=You see some marbles. Boys will be boys. -canned tuna=canned tuna -You see what appears to be canned tuna. Too bad you don't have a can opener.=You see what appears to be canned tuna. Too bad you don't have a can opener. -snowglobe=snowglobe -You see a snowglobe. It's so festive, it reminds you of Christmas!=You see a snowglobe. It's so festive, it reminds you of Christmas! -bobbin=bobbin -You see a bobbin for a sewing machine.=You see a bobbin for a sewing machine. -venom gland=venom gland -You see the venom gland of a giant cobra. It is filled with deadly toxin.=You see the venom gland of a giant cobra. It is filled with deadly toxin. -snowball=snowball -You see snowballs. You can throw them at your friends or put it in your bag to keep your fish fresh. But be careful, there could be little stones in it.=You see snowballs. You can throw them at your friends or put it in your bag to keep your fish fresh. But be careful, there could be little stones in it. -vomit=vomit -You see a pile of vomit. Gross!=You see a pile of vomit. Gross! -shuriken=shuriken -You see a shuriken. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results.=You see a shuriken. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results. -fire shuriken=fire shuriken -You see a fire shuriken. Throwing this flaming missile unleashes fiendish power.=You see a fire shuriken. Throwing this flaming missile unleashes fiendish power. -wooden spear=wooden spear -You see a wooden spear. You can attack at a distance with this primitive weapon. Stand square on, not diagonally, for the best results.=You see a wooden spear. You can attack at a distance with this primitive weapon. Stand square on, not diagonally, for the best results. -money=money -You see a money.=You see a money. -wooden bow=wooden bow -You see a wooden bow. With this in one hand, and wooden arrows in the other, you may be able to attack from a distance.=You see a wooden bow. With this in one hand, and wooden arrows in the other, you may be able to attack from a distance. -longbow=longbow -You see a longbow. Equipped with this, and a supply of arrows, you will have an effective ranged attack.=You see a longbow. Equipped with this, and a supply of arrows, you will have an effective ranged attack. -composite bow=composite bow -You see a composite bow. It's better strung than most bows, for excellent ranged attack.=You see a composite bow. It's better strung than most bows, for excellent ranged attack. -crossbow=crossbow -You see a crossbow. The attack is high but it has a very short range, what do you prefer?=You see a crossbow. The attack is high but it has a very short range, what do you prefer? -hunter crossbow=hunter crossbow -You see a hunter crossbow. You must decide if the high attack is worth the short range it has.=You see a hunter crossbow. You must decide if the high attack is worth the short range it has. -mithril bow=mithril bow -You see a light and strong mithril bow. Its impeccable workmanship affords incredible accuracy.=You see a light and strong mithril bow. Its impeccable workmanship affords incredible accuracy. -wood=wood -You see some wood, fallen from a nearby tree.=You see some wood, fallen from a nearby tree. -sugar cane=sugar cane -You see a sugar cane. You can grind it for raw sugar.=You see a sugar cane. You can grind it for raw sugar. -grain=grain -You see a sheaf of grain.=You see a sheaf of grain. -flour=flour -You see a sack of flour.=You see a sack of flour. -iron ore=iron ore -You see a nugget of iron ore.=You see a nugget of iron ore. -coal=coal -You see a black and shiny piece of stone coal. It stayed in the ground for million of years.=You see a black and shiny piece of stone coal. It stayed in the ground for million of years. -wool=wool -You see wool. It was recently shorn.=You see wool. It was recently shorn. -yarn=yarn -You see a ball of yarn. It was spun with care.=You see a ball of yarn. It was spun with care. -cloth=cloth -You see a roll of cloth. It is of first-class quality.=You see a roll of cloth. It is of first-class quality. -gold nugget=gold nugget -You see a gold nugget. Its rich golden color shines, and it may be useful.=You see a gold nugget. Its rich golden color shines, and it may be useful. -mithril nugget=mithril nugget -You see a mithril nugget. Its pale silvery color shines, and it may be useful.=You see a mithril nugget. Its pale silvery color shines, and it may be useful. -iron=iron -You see a piece of iron.=You see a piece of iron. -gold bar=gold bar -You see a gold bar. It will make beautiful jewelry, or fetch a fine price.=You see a gold bar. It will make beautiful jewelry, or fetch a fine price. -mithril bar=mithril bar -You see a mithril bar. It can be made into excellent armor, or fetch a fine price.=You see a mithril bar. It can be made into excellent armor, or fetch a fine price. -golden egg=golden egg -You see a valuable golden egg.=You see a valuable golden egg. -spotted egg=spotted egg -You see a spotted egg. They are said to be delicious.=You see a spotted egg. They are said to be delicious. -reptile egg=reptile egg -You see a valuable reptile egg.=You see a valuable reptile egg. -silk gland=silk gland -You see a silk gland from a giant spider. Its contents are extremely valuable.=You see a silk gland from a giant spider. Its contents are extremely valuable. -silk thread=silk thread -You see a spool of silk thread, spun from the glands of a giant spider..=You see a spool of silk thread, spun from the glands of a giant spider.. -oil=oil -You see a small can of oil.=You see a small can of oil. -mithril thread=mithril thread -You see a spool of mithril thread, made from silk fused with mithril.=You see a spool of mithril thread, made from silk fused with mithril. -mithril fabric=mithril fabric -You see a couple yards of mithril fabric. It's very sparkly!=You see a couple yards of mithril fabric. It's very sparkly! -horse hair=horse hair -You see some horse hairs.=You see some horse hairs. -beeswax=beeswax -You see a piece of beeswax.=You see a piece of beeswax. -candle=candle -You see a candle in a candle stick.=You see a candle in a candle stick. -angel figurine=angel figurine -You see a sparkling angel figurine.=You see a sparkling angel figurine. -skull ring=skull ring -You see a skull ring.=You see a skull ring. -amulet=amulet -You see an amulet.=You see an amulet. -ankh=ankh -You see a mysterious ankh. You don't know what it is for.=You see a mysterious ankh. You don't know what it is for. -wedding ring=wedding ring -You see a golden wedding ring, a symbol of love.=You see a golden wedding ring, a symbol of love. -engagement ring=engagement ring -You see a diamond engagement ring, a symbol of love (or a symbol of wealth?)=You see a diamond engagement ring, a symbol of love (or a symbol of wealth?) -emerald ring=emerald ring -You see an emerald ring, known as ring of life. Wear it, and you risk less from death.=You see an emerald ring, known as ring of life. Wear it, and you risk less from death. -summon scroll=summon scroll -You see a summon scroll.=You see a summon scroll. -nalwor city scroll=nalwor city scroll -You see a nalwor city scroll, it will take you back to nalwor city.=You see a nalwor city scroll, it will take you back to nalwor city. -ados city scroll=ados city scroll -You see a ados city scroll, it will take you back to ados city.=You see a ados city scroll, it will take you back to ados city. -fado city scroll=fado city scroll -You see a fado city scroll, it will take you back to fado city.=You see a fado city scroll, it will take you back to fado city. -kirdneh city scroll=kirdneh city scroll -You see a kirdneh city scroll, it will take you back to kirdneh city.=You see a kirdneh city scroll, it will take you back to kirdneh city. -kalavan city scroll=kalavan city scroll -You see a kalavan city scroll, it will take you back to kalavan city.=You see a kalavan city scroll, it will take you back to kalavan city. -marked scroll=marked scroll -You see a marked scroll.=You see a marked scroll. -invitation scroll=invitation scroll -You see an invitation scroll.=You see an invitation scroll. -home scroll=home scroll -You see a home scroll, it will take you back to semos city.=You see a home scroll, it will take you back to semos city. -empty scroll=empty scroll -You see an empty scroll. If used correctly it will return you to where you marked it at.=You see an empty scroll. If used correctly it will return you to where you marked it at. -rainbow beans=rainbow beans -You see a small pile of rainbow beans. One says something odd.=You see a small pile of rainbow beans. One says something odd. -balloon=balloon -You see a bright red balloon. You can use it to float high up into the clouds.=You see a bright red balloon. You can use it to float high up into the clouds. -twilight moss=twilight moss -You see a strange looking piece of moss. It looks like it could have hallucinogenic properties.=You see a strange looking piece of moss. It looks like it could have hallucinogenic properties. -lucky charm=lucky charm -You see a lucky charm. Some believe that it helps their karma in some way, but others think that is superstition.=You see a lucky charm. Some believe that it helps their karma in some way, but others think that is superstition. -mithril clasp=mithril clasp -You see a clasp made from mithril, it would provide the fastening for a cloak.=You see a clasp made from mithril, it would provide the fastening for a cloak. -mythical egg=mythical egg -You see a mythical egg, but you do not know, what it is for.=You see a mythical egg, but you do not know, what it is for. -magical eggshells=magical eggshells -You see shards from a mythical egg. They have magical properties.=You see shards from a mythical egg. They have magical properties. -golden gm token=golden gm token -You see a golden gm token.=You see a golden gm token. -silvery gm token=silvery gm token -You see a silvery gm token.=You see a silvery gm token. -bronze gm token=bronze gm token -You see a bronze gm token.=You see a bronze gm token. -black shield=black shield -You see a black shield. Its darkness will surely protect you.=You see a black shield. Its darkness will surely protect you. -blue shield=blue shield -You see a blue shield. It feels light in your hand but is strong enough for excellent protection.=You see a blue shield. It feels light in your hand but is strong enough for excellent protection. -buckler=buckler -You see a small buckler. It may protect you against swords and maces but against stronger weapons you do not trust it.=You see a small buckler. It may protect you against swords and maces but against stronger weapons you do not trust it. -chaos shield=chaos shield -You see a chaos shield. Its charming appearance will fool your enemies.=You see a chaos shield. Its charming appearance will fool your enemies. -crown shield=crown shield -You see a mighty crown shield. It is in perfect condition.=You see a mighty crown shield. It is in perfect condition. -elvish shield=elvish shield -You see an elvish shield. It looks like one big tree leaf!=You see an elvish shield. It looks like one big tree leaf! -enhanced lion shield=enhanced lion shield -An enhanced lion shield glints at you. It will defend you well.=An enhanced lion shield glints at you. It will defend you well. -golden shield=golden shield -You see a golden shield, could it be the ultimate in defense?=You see a golden shield, could it be the ultimate in defense? -green dragon shield=green dragon shield -You see an green dragon shield, scaly and strong.=You see an green dragon shield, scaly and strong. -lion shield=lion shield -You see a valuable lion shield. It is heavy enough to defend you against strong attacks.=You see a valuable lion shield. It is heavy enough to defend you against strong attacks. -magic plate shield=magic plate shield -You see a magic plate shield. It has a magical quality surrounding it.=You see a magic plate shield. It has a magical quality surrounding it. -mainio shield=mainio shield -You see a strange looking mainio shield. You have no idea what it's made of, but it seems to protect you well by distracting your enemies.=You see a strange looking mainio shield. You have no idea what it's made of, but it seems to protect you well by distracting your enemies. -mithril shield=mithril shield -You see a mithril shield. You will most surely be protected.=You see a mithril shield. You will most surely be protected. -plate shield=plate shield -You see a solid plate shield. It is in good condition, and might be worth a little.=You see a solid plate shield. It is in good condition, and might be worth a little. -red shield=red shield -You see a red shield. It is a stunning shade of scarlet.=You see a red shield. It is a stunning shade of scarlet. -shadow shield=shadow shield -You see a shadow shield. Rather than reflecting light, it seems to absorb it.=You see a shadow shield. Rather than reflecting light, it seems to absorb it. -skull shield=skull shield -The face of a skull scowls at you from this fearsome skull shield.=The face of a skull scowls at you from this fearsome skull shield. -studded shield=studded shield -You see a studded shield. It's a little dented but seems robust enough.=You see a studded shield. It's a little dented but seems robust enough. -unicorn shield=unicorn shield -You see a unicorn shield. It's beautifully crafted and will give you excellent protection.=You see a unicorn shield. It's beautifully crafted and will give you excellent protection. -wooden shield=wooden shield -You see a wooden shield. It is made of cheap wood and heavily used. However, it still seems to offer you some kind of basic protection.=You see a wooden shield. It is made of cheap wood and heavily used. However, it still seems to offer you some kind of basic protection. -xeno shield=xeno shield -You see a lightweight but strong xeno shield, in the colors of the creature it was looted from.=You see a lightweight but strong xeno shield, in the colors of the creature it was looted from. -assassin dagger=assassin dagger -You see an assassin dagger, easy to pull out and use when needed.=You see an assassin dagger, easy to pull out and use when needed. -biting sword=biting sword -The point of this biting sword is chiseled to a lethal point. It will serve you well.=The point of this biting sword is chiseled to a lethal point. It will serve you well. -black sword=black sword -You see a black sword. Its darkness will surely protect you.=You see a black sword. Its darkness will surely protect you. -broadsword=broadsword -You see a mighty broadsword. Enemies will see their death.=You see a mighty broadsword. Enemies will see their death. -buster=buster -You see a buster, with its distinctive, oversized blade.=You see a buster, with its distinctive, oversized blade. -chaos dagger=chaos dagger -You see a chaos dagger, it is very lightweight and sharp. It will heal you while hurting them.=You see a chaos dagger, it is very lightweight and sharp. It will heal you while hurting them. -chaos sword=chaos sword -The chaos sword. It hurts them but it heals you.=The chaos sword. It hurts them but it heals you. -claymore=claymore -This claymore is very hard to lift as it is so heavy. It will be a good weapon if you can swing it.=This claymore is very hard to lift as it is so heavy. It will be a good weapon if you can swing it. -dagger=dagger -You see a dagger, it is little more than decorative but you can jab pretty fast with it.=You see a dagger, it is little more than decorative but you can jab pretty fast with it. -dark dagger=dark dagger -You see a dark dagger. It's designed to be used with a fast motion.=You see a dark dagger. It's designed to be used with a fast motion. -demon fire sword=demon fire sword -The demon fire sword, a horrible sword taken from hell's deepest dungeons.=The demon fire sword, a horrible sword taken from hell's deepest dungeons. -demon sword=demon sword -Demon skeletons return from hell with this demon sword.=Demon skeletons return from hell with this demon sword. -drow sword=drow sword -This is a genuine drow sword.=This is a genuine drow sword. -elvish sword=elvish sword -This is an elvish sword. It looks like it is made from tree leaves!=This is an elvish sword. It looks like it is made from tree leaves! -fire sword=fire sword -Flames race along this fire sword as you wield it.=Flames race along this fire sword as you wield it. -golden blade=golden blade -You see the rare and precious golden blade. It can steal life from those you stab with it.=You see the rare and precious golden blade. It can steal life from those you stab with it. -golden orc sword=golden orc sword -You see a golden orc sword, with its heavy and deadly appearance it seems also valuable.=You see a golden orc sword, with its heavy and deadly appearance it seems also valuable. -great sword=great sword -This rare great sword is heavy and of good craftsmanship. It can only attack incredibly slowly.=This rare great sword is heavy and of good craftsmanship. It can only attack incredibly slowly. -hell dagger=hell dagger -You see a hell dagger. Is the color that of the burning fires of Hell?=You see a hell dagger. Is the color that of the burning fires of Hell? -ice sword=ice sword -You see an ice sword, the blade is lethally cold to the touch, and you know it is one of the best weapons you'll find.=You see an ice sword, the blade is lethally cold to the touch, and you know it is one of the best weapons you'll find. -imperator sword=imperator sword -You see a an imperator sword, crafted from you know not what.=You see a an imperator sword, crafted from you know not what. -immortal sword=immortal sword -This immortal sword is a gift of gods to the chosen one. It is heavy and of exquisite craftsmanship.=This immortal sword is a gift of gods to the chosen one. It is heavy and of exquisite craftsmanship. -katana=katana -You see a valuable katana, with a lethal looking blade.=You see a valuable katana, with a lethal looking blade. -knife=knife -You see a blunt knife which looks capable of little damage.=You see a blunt knife which looks capable of little damage. -l hand sword=l hand sword -You see l hand sword. It must be used with right hand sword.=You see l hand sword. It must be used with right hand sword. -night dagger=night dagger -You see a night dagger, to be used with stealth.=You see a night dagger, to be used with stealth. -nihonto=nihonto -You see a samurai's nihonto sword with a razor sharp blade.=You see a samurai's nihonto sword with a razor sharp blade. -obsidian knife=obsidian knife -You see a knife whose blade is crafted to a lethal edge from finest obsidian.=You see a knife whose blade is crafted to a lethal edge from finest obsidian. -orc sword=orc sword -You see an orc sword which has a heavy and deadly appearance.=You see an orc sword which has a heavy and deadly appearance. -orcish sword=orcish sword -This orcish sword is badly done and weighs a lot.=This orcish sword is badly done and weighs a lot. -r hand sword=r hand sword -You see r hand sword. It must be used with a left hand sword.=You see r hand sword. It must be used with a left hand sword. -scimitar=scimitar -This scimitar catches the light as you look at it, it will be a powerful weapon.=This scimitar catches the light as you look at it, it will be a powerful weapon. -short sword=short sword -You see a well kept short sword which will be easy to carry as you travel.=You see a well kept short sword which will be easy to carry as you travel. -soul dagger=soul dagger -You see a soul dagger. It's so light and sharp that you can stab at a very fast rate.=You see a soul dagger. It's so light and sharp that you can stab at a very fast rate. -sword=sword -You see a sharp sword which might even be worth a little.=You see a sharp sword which might even be worth a little. -vampire sword=vampire sword -You see the vampire sword, which will suck life from those you attack with it.=You see the vampire sword, which will suck life from those you attack with it. -xeno sword=xeno sword -You wield a xeno sword. Despite the great strength it is strangely light.=You wield a xeno sword. Despite the great strength it is strangely light. -arrow game token=arrow game token -You see a token, that when placed in the right spot could point you in the right direction.=You see a token, that when placed in the right spot could point you in the right direction. -o board token=o board token -You see an O token.=You see an O token. -x board token=x board token -You see an X token.=You see an X token. -food mill=food mill -You see a food mill. It is the traditional tool to mill food.=You see a food mill. It is the traditional tool to mill food. -sugar mill=sugar mill -You see a sugar mill. You can obtain sugar if you grind canes with it.=You see a sugar mill. You can obtain sugar if you grind canes with it. -scroll eraser=scroll eraser -You see a scroll eraser. Put a marked scroll in the opposite hand to clean the scroll.=You see a scroll eraser. Put a marked scroll in the opposite hand to clean the scroll. -pestle and mortar=pestle and mortar -You see a pestle and mortar. This is the traditional tool to pound herbs, food and other substances.=You see a pestle and mortar. This is the traditional tool to pound herbs, food and other substances. -gold pan=gold pan -You see a gold pan. It is the traditional tool to prospect for gold.=You see a gold pan. It is the traditional tool to prospect for gold. -fishing rod=fishing rod -You see a fishing rod. If you find a fishing spot you can catch fish with it.=You see a fishing rod. If you find a fishing spot you can catch fish with it. -magical scissors=magical scissors -You see a sparkling pair of magical scissors. Just think what they could cut!=You see a sparkling pair of magical scissors. Just think what they could cut! -magical needle=magical needle -You see a magical needle with a very sharp point.=You see a magical needle with a very sharp point. -pick=pick -You see a pick. It is a tool which helps you to get some coal.=You see a pick. It is a tool which helps you to get some coal. -shovel=shovel -You see a shovel, a tool for digging.=You see a shovel, a tool for digging. -dark angel=dark angel -You see a dark angel. He is an strong evil angel, and for now he is under cover of darkness.=You see a dark angel. He is an strong evil angel, and for now he is under cover of darkness. -My will be done over land and sky!=My will be done over land and sky! -Mine is the Power and the Glory!=Mine is the Power and the Glory! -dark archangel=dark archangel -You see a dark archangel. After she fell from heaven her soul became as dark as she is now.=You see a dark archangel. After she fell from heaven her soul became as dark as she is now. -I will strike you down with more power than you can imagine!!=I will strike you down with more power than you can imagine!! -My will be done!=My will be done! -fallen angel=fallen angel -You see a fallen angel. When he turned away from good, he fell down from heaven.=You see a fallen angel. When he turned away from good, he fell down from heaven. -You are DOOMED.=You are DOOMED. -I am here to destroy all people.=I am here to destroy all people. -baby angel=baby angel -You see a baby angel. Will it ever get a chance to grow up?=You see a baby angel. Will it ever get a chance to grow up? -I want to grow to be mighty!=I want to grow to be mighty! -angel=angel -You see an angel. With his flame sword he guards the way and doesn't let you pass.=You see an angel. With his flame sword he guards the way and doesn't let you pass. -Meet thy maker!=Meet thy maker! -Mere mortals cannot challenge me!=Mere mortals cannot challenge me! -archangel=archangel -You see an archangel. She wants to kill you because she doesn't want you in the clouds.=You see an archangel. She wants to kill you because she doesn't want you in the clouds. -Neither the earth nor the sky has a place for you...=Neither the earth nor the sky has a place for you... -Leave now, before it is too late for you.=Leave now, before it is too late for you. -Your destiny is not here.=Your destiny is not here. -spider=spider -You see a spider. It makes a giant spider web to trap you!=You see a spider. It makes a giant spider web to trap you! -poisonous spider=poisonous spider -You see a poisonous spider. Be careful! Its poison is very strong! It will make you very weak and can probably kill you!=You see a poisonous spider. Be careful! Its poison is very strong! It will make you very weak and can probably kill you! -giant spider=giant spider -You see a giant spider. It has very long legs which are quite dangerous.=You see a giant spider. It has very long legs which are quite dangerous. -crab=crab -You see a little crab that comes from the beach, be careful of its pincers.=You see a little crab that comes from the beach, be careful of its pincers. -click-clack-click=click-clack-click -chick=chick -You see a little chick, it doesn't like to be away from its mother.=You see a little chick, it doesn't like to be away from its mother. -peep peep=peep peep -peep peep peep=peep peep peep -chicken=chicken -You see a chicken, isn't it nice?=You see a chicken, isn't it nice? -cluck cluck=cluck cluck -cluck cluck cluck=cluck cluck cluck -mother hen=mother hen -You see a mother hen, she will probably hurt you if you touch her sons.=You see a mother hen, she will probably hurt you if you touch her sons. -peep peep=peep peep -peep peep peep=peep peep peep -penguin=penguin -You see a penguin, smart and silly.=You see a penguin, smart and silly. -Oou! Oou!=Oou! Oou! -*flap* *flap*=*flap* *flap* -pigeon=pigeon -You see a pigeon, it loves the little seeds.=You see a pigeon, it loves the little seeds. -killer bee=killer bee -You see a killer bee which will sting you to death!=You see a killer bee which will sting you to death! -BbzzZZzzz!=BbzzZZzzz! -deer=deer -You see a defenseless deer, do not have fear of it.=You see a defenseless deer, do not have fear of it. -mouse=mouse -You see a small mouse that seems to be very hungry.=You see a small mouse that seems to be very hungry. -squeak ... squeak!=squeak ... squeak! -rat=rat -You see a small rat that seems to be extremely hungry.=You see a small rat that seems to be extremely hungry. -Squeak! Squeak!=Squeak! Squeak! -bat=bat -You see a bat, it will seek your blood, don't be distracted...=You see a bat, it will seek your blood, don't be distracted... -caverat=caverat -You see a caverat which is dirty and loves the dungeons.=You see a caverat which is dirty and loves the dungeons. -piglet=piglet -You see a piglet, a bit dirty.=You see a piglet, a bit dirty. -shnuffle=shnuffle -oink oink=oink oink -oink=oink -babybear=babybear -You see a babybear, isn't it sweet?=You see a babybear, isn't it sweet? -Roar!=Roar! -beaver=beaver -You see a beaver. It likes to build dams in the river.=You see a beaver. It likes to build dams in the river. -big bad wolf=big bad wolf -You see a big bad wolf. It is really wild, hungry and really dangerous!=You see a big bad wolf. It is really wild, hungry and really dangerous! -Aooooooouuhhh!!=Aooooooouuhhh!! -boar=boar -You see a boar, they are pretty wild!=You see a boar, they are pretty wild! -Oink!!=Oink!! -bull=bull -You see a bull, run away!!!=You see a bull, run away!!! -MOOOOO!!!=MOOOOO!!! -cow=cow -You see a cow, taste her delicious milk.=You see a cow, taste her delicious milk. -mooooo=mooooo -Moooo moooo mooooooo=Moooo moooo mooooooo -fox=fox -You see a fox, it becomes extinct!=You see a fox, it becomes extinct! -goat=goat -You see a goat, which is wild and lives in the mountain.=You see a goat, which is wild and lives in the mountain. -horse=horse -You see a horse, don't slap it or it will run away!=You see a horse, don't slap it or it will run away! -neigghhh neigghhh=neigghhh neigghhh -monkey=monkey -You see a monkey, would you like to offer it a banana?=You see a monkey, would you like to offer it a banana? -Aooooooouuhhh!!=Aooooooouuhhh!! -panda=panda -You see a panda which comes from an exotic land.=You see a panda which comes from an exotic land. -Roar!=Roar! -ram=ram -You see a ram, maybe you are able to shear it?=You see a ram, maybe you are able to shear it? -Baaaaaa=Baaaaaa -white horse=white horse -You see a white horse, they love to stay with their brown friends.=You see a white horse, they love to stay with their brown friends. -naayyy=naayyy -wolf=wolf -You see a wolf. It is wild and very hungry!=You see a wolf. It is wild and very hungry! -Aooooooouuhhh!!=Aooooooouuhhh!! -bear=bear -You see a bear. You are lucky and found it awake.=You see a bear. You are lucky and found it awake. -Roar!=Roar! -lion=lion -You see a lion, dangerous and hungry...=You see a lion, dangerous and hungry... -Grooooaaaarrrr!=Grooooaaaarrrr! -elephant=elephant -You see an elephant, a huge animal with lots of ham and meat. Are you sure that you want to attack it?=You see an elephant, a huge animal with lots of ham and meat. Are you sure that you want to attack it? -WHOOOOEOEOEZZZZZZ!!!=WHOOOOEOEOEZZZZZZ!!! -tiger=tiger -You see a tiger, which likes deer and human meat.=You see a tiger, which likes deer and human meat. -Miiiiaaauuu!=Miiiiaaauuu! -venomrat=venomrat -You see a venomrat, extremely poisonous.=You see a venomrat, extremely poisonous. -killer bat=killer bat -You see a killer bat which will suck your blood!=You see a killer bat which will suck your blood! -black bear=black bear -You see a strong black bear which hibernates in winter.=You see a strong black bear which hibernates in winter. -ROOOOAAAAR!=ROOOOAAAAR! -razorrat=razorrat -You see a razorrat, its noise will make you crazy!=You see a razorrat, its noise will make you crazy! -giantrat=giantrat -You see a giant rat. It seems like it survived from an experiment which went wrong.=You see a giant rat. It seems like it survived from an experiment which went wrong. -unicorn=unicorn -You see a rare unicorn. It is widely believed to be only a myth.=You see a rare unicorn. It is widely believed to be only a myth. -centaur=centaur -You see a noble centaur, half man, half horse. He looks capable of great damage but thankfully seems to mean you no harm.=You see a noble centaur, half man, half horse. He looks capable of great damage but thankfully seems to mean you no harm. -pegasus=pegasus -You see Pegasus, the winged horse. His name means Lightning and he carries bolts of Thunder.=You see Pegasus, the winged horse. His name means Lightning and he carries bolts of Thunder. -caiman=caiman -You see a caiman, which is hidden in the water.=You see a caiman, which is hidden in the water. -Grrr Grrr!=Grrr Grrr! -grass snake=grass snake -You see a grass snake which is quite dangerous.=You see a grass snake which is quite dangerous. -sssSSSsssss...=sssSSSsssss... -snake=snake -You see a snake, it came from the forest to poison you!=You see a snake, it came from the forest to poison you! -SsSssSsssssss!=SsSssSsssssss! -crocodile=crocodile -You see a crocodile, it arises from the deep waters in the rivers.=You see a crocodile, it arises from the deep waters in the rivers. -Rrrrrrrrr=Rrrrrrrrr -cobra=cobra -You see a cobra.=You see a cobra. -king cobra=king cobra -You see a king cobra. It is extremely venomous.=You see a king cobra. It is extremely venomous. -young beholder=young beholder -You see a young beholder. His huge eye will find you everywhere!=You see a young beholder. His huge eye will find you everywhere! -Defend yourself! I can see you!=Defend yourself! I can see you! -beholder=beholder -You see a beholder which can see you very well with his huge eye...=You see a beholder which can see you very well with his huge eye... -elder beholder=elder beholder -You see an elder beholder, his nasty eye will scare you.=You see an elder beholder, his nasty eye will scare you. -gargoyle=gargoyle -You see a gargoyle, You should think that it is a statue, but no, it is alive!=You see a gargoyle, You should think that it is a statue, but no, it is alive! -elder gargoyle=elder gargoyle -You see an elder gargoyle, it is wise...=You see an elder gargoyle, it is wise... -dark gargoyle=dark gargoyle -You see a dark gargoyle, do you feel the fear?=You see a dark gargoyle, do you feel the fear? -venom gargoyle=venom gargoyle -You see a venom gargoyle, a hideous and poisonous creature.=You see a venom gargoyle, a hideous and poisonous creature. -mimic=mimic -You see a mimic. Its content might be different from what you expect.=You see a mimic. Its content might be different from what you expect. -snarfkin=snarfkin -You see a snarfkin, the favorite pet of wizards. Be careful, its claws are poisonous!=You see a snarfkin, the favorite pet of wizards. Be careful, its claws are poisonous! -glow monster=glow monster -You see a glow monster.=You see a glow monster. -dark mutant=dark mutant -You see a dark mutant.=You see a dark mutant. -googon=googon -You see a googon that comes from the worst place in this world...Be careful this creature is a deadly killer.=You see a googon that comes from the worst place in this world...Be careful this creature is a deadly killer. -grobble monster=grobble monster -You see a grobble monster.=You see a grobble monster. -grobble grubble grobble=grobble grubble grobble -gribble grobble grubble=gribble grobble grubble -winged blurble=winged blurble -You see a winged blurble.=You see a winged blurble. -imperial experiment=imperial experiment -You see an imperial experiment which mirrors the fate of many of science departments test subjects. It's skin glows and standing before it taxes your body.=You see an imperial experiment which mirrors the fate of many of science departments test subjects. It's skin glows and standing before it taxes your body. -imperial mutant=imperial mutant -You see an imperial mutant.=You see an imperial mutant. -Ouurgghh!=Ouurgghh! -thing=thing -You see a thing. It looks like a very mutated human and is so scary that you think it has to come from another world.=You see a thing. It looks like a very mutated human and is so scary that you think it has to come from another world. -green slime=green slime -You see a green slime, sticky and shiny.=You see a green slime, sticky and shiny. -brown slime=brown slime -You see a brown slime, sticky and hungry.=You see a brown slime, sticky and hungry. -black slime=black slime -You see a black slime, with a sticky trail behind it.=You see a black slime, with a sticky trail behind it. -twilight slime=twilight slime -You see a twilight slime, so strange and beautiful. Why is it coming towards you? Runnnnnnnnn!=You see a twilight slime, so strange and beautiful. Why is it coming towards you? Runnnnnnnnn! -violet avenger=violet avenger -You see a violet avenger.=You see a violet avenger. -devil queen=devil queen -You see a devil queen.=You see a devil queen. -Who wakes me from my meditation?=Who wakes me from my meditation? -You will regret this!=You will regret this! -demon=demon -You see a demon. He personifies evil.=You see a demon. He personifies evil. -imperial demon servant=imperial demon servant -You see an imperial demon servant who carries out whims of his demon lords.=You see an imperial demon servant who carries out whims of his demon lords. -imperial demon lord=imperial demon lord -You see an imperial demon lord. He holds the rank of General among demons, and very few out rank it in hell.=You see an imperial demon lord. He holds the rank of General among demons, and very few out rank it in hell. -balrog=balrog -You see a huge balrog. He is slow hitting, but terribly strong as he stands towering above you.=You see a huge balrog. He is slow hitting, but terribly strong as he stands towering above you. -Arrrrllllggggghhhh=Arrrrllllggggghhhh -Growwwwwwwrrrrrrr!=Growwwwwwwrrrrrrr! -imp=imp -You see an imp. He may look a little bit cute, but don't forget he is one of the devil's servants!=You see an imp. He may look a little bit cute, but don't forget he is one of the devil's servants! -winged blue imp=winged blue imp -You see a winged blue imp. It's endowed with the power of evil. His trident is able to poison even the bravest warriors.=You see a winged blue imp. It's endowed with the power of evil. His trident is able to poison even the bravest warriors. -chaos green dragonrider=chaos green dragonrider -You see a chaos green dragonrider. Don't misjudge him as to believe it's a normal green dragon, as it is controlled by a mysterious sorcerer seated on his back.=You see a chaos green dragonrider. Don't misjudge him as to believe it's a normal green dragon, as it is controlled by a mysterious sorcerer seated on his back. -Wheee!!=Wheee!! -It's fun up here!=It's fun up here! -chaos red dragonrider=chaos red dragonrider -You see a chaos red dragonrider. He might look like a red dragon, but the sorcerer seated on his back guides him to give even more strength out.=You see a chaos red dragonrider. He might look like a red dragon, but the sorcerer seated on his back guides him to give even more strength out. -Charrrrrrrrge!=Charrrrrrrrge! -green dragon=green dragon -You see a green dragon. His wings may be too small for him to fly with them but nevertheless he looks very strong and his thick green skin seems to protect him quite well.=You see a green dragon. His wings may be too small for him to fly with them but nevertheless he looks very strong and his thick green skin seems to protect him quite well. -bone dragon=bone dragon -You see a bone dragon. It's been said that there are creatures with enough will to live that their souls still cling to life even as their flesh and blood has failed them. This is living proof.=You see a bone dragon. It's been said that there are creatures with enough will to live that their souls still cling to life even as their flesh and blood has failed them. This is living proof. -blue dragon=blue dragon -You see a blue dragon. It has a very beautiful blue skin which shimmers mysteriously in the light. But don't forget that it also has claws and teeth!=You see a blue dragon. It has a very beautiful blue skin which shimmers mysteriously in the light. But don't forget that it also has claws and teeth! -red dragon=red dragon -You see into the emerald eyes of a red dragon. With these it can spot prey in the darkness. It's red scales serve it well, reflecting any light that shines on them blinding enemies. The scales are prized for their beauty, and difficult to obtain from such a ferocious beast.=You see into the emerald eyes of a red dragon. With these it can spot prey in the darkness. It's red scales serve it well, reflecting any light that shines on them blinding enemies. The scales are prized for their beauty, and difficult to obtain from such a ferocious beast. -black dragon=black dragon -You see a black dragon. As you see this huge black creature you almost faint! If you concentrate you can feel the deep growling of it in your bones.=You see a black dragon. As you see this huge black creature you almost faint! If you concentrate you can feel the deep growling of it in your bones. -flying golden dragon=flying golden dragon -You see a flying golden dragon. As it spreads its mighty wings you shiver... this mystical creature seems to have much power in it!=You see a flying golden dragon. As it spreads its mighty wings you shiver... this mystical creature seems to have much power in it! -twin headed dragon=twin headed dragon -You see a twin headed dragon. The two heads are staring at you with their four eyes!=You see a twin headed dragon. The two heads are staring at you with their four eyes! -duergar=duergar -You see a duergar. It is a close relative to dwarves but with a diabolically tainted blood.=You see a duergar. It is a close relative to dwarves but with a diabolically tainted blood. -duergar axeman=duergar axeman -You see a duergar axeman.=You see a duergar axeman. -elder duergar=elder duergar -You see an elder duergar.=You see an elder duergar. -duergar elite=duergar elite -You see a duergar elite.=You see a duergar elite. -duergar hero=duergar hero -You see a duergar hero.=You see a duergar hero. -We mighty duergars will crush the dwarves!=We mighty duergars will crush the dwarves! -duergar wizard=duergar wizard -You see a duergar wizard.=You see a duergar wizard. -black duergar=black duergar -You see a black duergar.=You see a black duergar. -Dwarves are the enemy ... but we kill all who cross our path!=Dwarves are the enemy ... but we kill all who cross our path! -duergar king=duergar king -You see a duergar king.=You see a duergar king. -dwarf=dwarf -You see a dwarf who works in the mine to find some iron for us.=You see a dwarf who works in the mine to find some iron for us. -dwarf guardian=dwarf guardian -You see a dwarf guardian who fights against the humans.=You see a dwarf guardian who fights against the humans. -elder dwarf=elder dwarf -You see an elder dwarf, a mighty warrior tired of fighting.=You see an elder dwarf, a mighty warrior tired of fighting. -hero dwarf=hero dwarf -You see a hero dwarf. He is an old dwarf warrior who became a hero for fighting in many dwarf wars and he is very famous for his strength and brutality.=You see a hero dwarf. He is an old dwarf warrior who became a hero for fighting in many dwarf wars and he is very famous for his strength and brutality. -leader dwarf=leader dwarf -You see a leader dwarf. He is the most mighty dwarf of his tribe so he enjoys much popularity there.=You see a leader dwarf. He is the most mighty dwarf of his tribe so he enjoys much popularity there. -giant dwarf=giant dwarf -You see a giant dwarf. It is very hard for him to be a giant dwarf because some of the other dwarves don't really see him as one of them because of his height... But in his big heart he is a dwarf and in a battle he will fight against each enemy!=You see a giant dwarf. It is very hard for him to be a giant dwarf because some of the other dwarves don't really see him as one of them because of his height... But in his big heart he is a dwarf and in a battle he will fight against each enemy! -Dhohr Nuggetcutter=Dhohr Nuggetcutter -You see a dhohr nuggetcutter.=You see a dhohr nuggetcutter. -Lord Durin=Lord Durin -You see the mighty Lord Durin, an ancient dwarf who has led his race through countless battles.=You see the mighty Lord Durin, an ancient dwarf who has led his race through countless battles. -Don't call me Friend.=Don't call me Friend. -mountain dwarf=mountain dwarf -You see a mountain dwarf. He likes earth, stones and rocks. Deep within the mountain, where no animal and no human has ever been, you can hear the sound of his pickaxe as he searches for gold and ore.=You see a mountain dwarf. He likes earth, stones and rocks. Deep within the mountain, where no animal and no human has ever been, you can hear the sound of his pickaxe as he searches for gold and ore. -mountain dwarf guardian=mountain dwarf guardian -You see a mountain dwarf guardian. He has to watch out for enemies and guards the mountain dwarfs while they are doing their work.=You see a mountain dwarf guardian. He has to watch out for enemies and guards the mountain dwarfs while they are doing their work. -mountain elder dwarf=mountain elder dwarf -You see a mountain elder dwarf. For many years he worked deep in the mountain and found much gold and ore in his life. Soon he will be too old to do this work but at the moment he is still a strong dwarf.=You see a mountain elder dwarf. For many years he worked deep in the mountain and found much gold and ore in his life. Soon he will be too old to do this work but at the moment he is still a strong dwarf. -mountain hero dwarf=mountain hero dwarf -You see a mountain hero dwarf. Many, many dwarves admire him! He was a member of a dwarvish group which discovered a very big gold deposit!=You see a mountain hero dwarf. Many, many dwarves admire him! He was a member of a dwarvish group which discovered a very big gold deposit! -mountain leader dwarf=mountain leader dwarf -You see a mountain leader dwarf. He is responsible for the mountain dwarves and their work, and also for the organization of the guardian troops. But in serious cases he will also fight on his own!=You see a mountain leader dwarf. He is responsible for the mountain dwarves and their work, and also for the organization of the guardian troops. But in serious cases he will also fight on his own! -air elemental=air elemental -You see an air elemental and feel a fresh breeze past you.=You see an air elemental and feel a fresh breeze past you. -djinn=djinn -You see a djinn. He isn't caught in a bottle or lamp so someone must have let him free and gave him the order to kill you.=You see a djinn. He isn't caught in a bottle or lamp so someone must have let him free and gave him the order to kill you. -nymph=nymph -You see a nymph. She loves nature very much! As you take a look at her she feels disturbed in sun-bathing and talking to the birds, perhaps she was best left alone.=You see a nymph. She loves nature very much! As you take a look at her she feels disturbed in sun-bathing and talking to the birds, perhaps she was best left alone. -earth elemental=earth elemental -You see an earth elemental. The earth is quaking as it moves over the surface.=You see an earth elemental. The earth is quaking as it moves over the surface. -fire elemental=fire elemental -You see a fire elemental and hardly can stand the heat.=You see a fire elemental and hardly can stand the heat. -ice elemental=ice elemental -You see an ice elemental and sense a shiver on your spine.=You see an ice elemental and sense a shiver on your spine. -ice golem=ice golem -You see an Ice golem. Ice Golems are elementals made out of ice. You should watch out for its icy fists or you might get hurt.=You see an Ice golem. Ice Golems are elementals made out of ice. You should watch out for its icy fists or you might get hurt. -ice giant=ice giant -You see an ice giant. His skin is covered with frozen hoarfrost and his breath feels like the cold north wind.=You see an ice giant. His skin is covered with frozen hoarfrost and his breath feels like the cold north wind. -stone golem=stone golem -You see a stone golem, it only exists to protect. What was it crafted for to protect, and what would one do if it lost its purpose?=You see a stone golem, it only exists to protect. What was it crafted for to protect, and what would one do if it lost its purpose? -dwarf golem=dwarf golem -You see a dwarf golem. A long time ago a dwarf got turned into a big rock by some mystical forces. To save their friend the other dwarves created a golem out of this rock! From this day the dwarf golem lives together with them.=You see a dwarf golem. A long time ago a dwarf got turned into a big rock by some mystical forces. To save their friend the other dwarves created a golem out of this rock! From this day the dwarf golem lives together with them. -water elemental=water elemental -You see a Water elemental which is made of water and various other liquids.=You see a Water elemental which is made of water and various other liquids. -wooden golem=wooden golem -You see a wooden golem. The scroll with ancient words in his head keeps it walking.=You see a wooden golem. The scroll with ancient words in his head keeps it walking. -ent=ent -You see an ent. First you think it is a tree but you will get a surprise when it starts to walk!=You see an ent. First you think it is a tree but you will get a surprise when it starts to walk! -old ent=old ent -You see an old ent. It has seen many years passing by and survived a lot of storms. You can see that it is very old because it has already lost all of its leaves and most of its bark is broken...=You see an old ent. It has seen many years passing by and survived a lot of storms. You can see that it is very old because it has already lost all of its leaves and most of its bark is broken... -entwife=entwife -You see an entwife. It is stronger than an ent and lives in a group in an ancient forest.=You see an entwife. It is stronger than an ent and lives in a group in an ancient forest. -albino elf swordsman=albino elf swordsman -You see an albino elf swordsman who is a professional in using swords. He can attack you from behind in a blink of an eye.=You see an albino elf swordsman who is a professional in using swords. He can attack you from behind in a blink of an eye. -albino elf archer=albino elf archer -You see an albino elf archer. He is so sharp-eyed that he could hit a bee from a distance of 100 meters.=You see an albino elf archer. He is so sharp-eyed that he could hit a bee from a distance of 100 meters. -albino elf magician=albino elf magician -You see an albino elf magician. He knows all the secrets of magic. You'd be better to not let him unleash his full power!=You see an albino elf magician. He knows all the secrets of magic. You'd be better to not let him unleash his full power! -albino elf queen=albino elf queen -You see an albino elf queen. She is the matriarch of the albino elf race and she will do everything to protect it.=You see an albino elf queen. She is the matriarch of the albino elf race and she will do everything to protect it. -albino elf king=albino elf king -You see an albino elf king. He is the patriarch of albino elves and won countless of battles with his powerful albino elf army.=You see an albino elf king. He is the patriarch of albino elves and won countless of battles with his powerful albino elf army. -child dark elf=child dark elf -You see a child dark elf. You should know that dark elves are already quite strong and intelligent when they are children. Don't think you can fight him so easily!=You see a child dark elf. You should know that dark elves are already quite strong and intelligent when they are children. Don't think you can fight him so easily! -dark elf archer=dark elf archer -You see a dark elf archer. Dark elf archer's flawless aim can be fatal for those who dare to oppose them.=You see a dark elf archer. Dark elf archer's flawless aim can be fatal for those who dare to oppose them. -dark elf=dark elf -You see a dark elf.=You see a dark elf. -dark elf elite archer=dark elf elite archer -You see a dark elf elite archer.=You see a dark elf elite archer. -dark elf captain=dark elf captain -You see a dark elf captain.=You see a dark elf captain. -Think not to defy the dark elves!=Think not to defy the dark elves! -dark elf knight=dark elf knight -You see a dark elf knight.=You see a dark elf knight. -dark elf general=dark elf general -You see a Dark Elf General. It is commanding the evil troops of Nalwor Caves against intruders.=You see a Dark Elf General. It is commanding the evil troops of Nalwor Caves against intruders. -We will never return to the light!=We will never return to the light! -dark elf wizard=dark elf wizard -Dark elf wizards are very powerful and dangerous. They are specialised in dark magic.=Dark elf wizards are very powerful and dangerous. They are specialised in dark magic. -The Spider Queen will avenge me!=The Spider Queen will avenge me! -dark elf viceroy=dark elf viceroy -You see a dark elf viceroy.=You see a dark elf viceroy. -Keep away from me, sunlight dweller!=Keep away from me, sunlight dweller! -dark elf ranger=dark elf ranger -You see a dark elf ranger.=You see a dark elf ranger. -Keep away from me, sunlight dweller!=Keep away from me, sunlight dweller! -dark elf sacerdotist=dark elf sacerdotist -You see a dark elf sacerdotist.=You see a dark elf sacerdotist. -The Queen of Spiders will take her revenge on you!=The Queen of Spiders will take her revenge on you! -dark elf admiral=dark elf admiral -You see a dark elf admiral.=You see a dark elf admiral. -We are the darkness you fear!=We are the darkness you fear! -Fear the dark elf armies!=Fear the dark elf armies! -dark elf master=dark elf master -You see a dark elf master. Beware of his high skills on magic and swordsmanship!=You see a dark elf master. Beware of his high skills on magic and swordsmanship! -Darkness will fall upon you!=Darkness will fall upon you! -You will never see the light again!=You will never see the light again! -dark elf matronmother=dark elf matronmother -You see a dark elf matronmother.=You see a dark elf matronmother. -Now, you will pay.=Now, you will pay. -You disturbed my rest!=You disturbed my rest! -elf=elf -You see an elf which loves the forest and lives in peace.=You see an elf which loves the forest and lives in peace. -militia elf=militia elf -You see a militia elf which trains hard to beat you!=You see a militia elf which trains hard to beat you! -archer elf=archer elf -You see an archer elf which can do much damage with its wooden arrows.=You see an archer elf which can do much damage with its wooden arrows. -soldier elf=soldier elf -You see a soldier elf, a good partner of the militia elves.=You see a soldier elf, a good partner of the militia elves. -mage elf=mage elf -You see a mage elf. She can feel pure magic when she stands outside in the forest and just frees her mind.=You see a mage elf. She can feel pure magic when she stands outside in the forest and just frees her mind. -commander elf=commander elf -You see a busy commander elf. He has control over the army of elves.=You see a busy commander elf. He has control over the army of elves. -archmage elf=archmage elf -You see an archmage elf. She is pretty.=You see an archmage elf. She is pretty. -elf sacerdotist=elf sacerdotist -You see an elf sacerdotist.=You see an elf sacerdotist. -elf wizard=elf wizard -You see an elf wizard. He is a very wise elf who knows almost every kind of magic. He likes to use nature spells to make every plant and creature fertile, but he is also able to defend himself when he has to do.=You see an elf wizard. He is a very wise elf who knows almost every kind of magic. He likes to use nature spells to make every plant and creature fertile, but he is also able to defend himself when he has to do. -elf bodyguard=elf bodyguard -You see an elf bodyguard.=You see an elf bodyguard. -amazoness giant=amazoness giant -You see an amazoness giant. She looks very beautiful but don't be deceived by that!=You see an amazoness giant. She looks very beautiful but don't be deceived by that! -giant=giant -You look up into the face of a huge giant. His physical proportions and extraordinary strength should give you goosebumps.=You look up into the face of a huge giant. His physical proportions and extraordinary strength should give you goosebumps. -I seek the blood of a Semos Man!=I seek the blood of a Semos Man! -Fee Fie Foe Fum!!=Fee Fie Foe Fum!! -elder giant=elder giant -You see an elder giant. For many years he lived alone and had to hide because he was hunted by humans! Now he is old and has decided not to hide any longer because he wants to see something else in his life.=You see an elder giant. For many years he lived alone and had to hide because he was hunted by humans! Now he is old and has decided not to hide any longer because he wants to see something else in his life. -Gnomes! Run away!=Gnomes! Run away! -You may steal my money but you'll never take my heart!=You may steal my money but you'll never take my heart! -master giant=master giant -You see a master giant with dark black eyes. His blood pumps loudly into his heart.=You see a master giant with dark black eyes. His blood pumps loudly into his heart. -Thou shalt die!=Thou shalt die! -Worms will feast on your corpse!=Worms will feast on your corpse! -black giant=black giant -You see a tall black giant. His armor and sword will hit you hard. Be prepared!=You see a tall black giant. His armor and sword will hit you hard. Be prepared! -Leave me now, before it is too late!=Leave me now, before it is too late! -Who dare challenge me?=Who dare challenge me? -imperial general giant=imperial general giant -You see an imperial general giant.=You see an imperial general giant. -giant kobold=giant kobold -You see a giant kobold. Though generally playful it doesn't quite know it's own strength. When it asks for a hug, it's best to decline that offer.=You see a giant kobold. Though generally playful it doesn't quite know it's own strength. When it asks for a hug, it's best to decline that offer. -Hug?=Hug? -kasarkutominubat=kasarkutominubat -You see a kasarkutominubat.=You see a kasarkutominubat. -gnome=gnome -You see a gnome who lives in the mountains.=You see a gnome who lives in the mountains. -Giiiiiaaants! Run away!=Giiiiiaaants! Run away! -mage gnome=mage gnome -You see a mage gnome, she cleans the dirty mountain.=You see a mage gnome, she cleans the dirty mountain. -Giiiiiaaants! Run away!=Giiiiiaaants! Run away! -infantry gnome=infantry gnome -You see an infantry gnome who will protect his little town against the humans!=You see an infantry gnome who will protect his little town against the humans! -Ready! Aim! Runnnnnnnnnnn=Ready! Aim! Runnnnnnnnnnn -cavalryman gnome=cavalryman gnome -You see a cavalryman gnome who does long travels to search for gold.=You see a cavalryman gnome who does long travels to search for gold. -Ready! Aim! Runnnnnnnnnnn=Ready! Aim! Runnnnnnnnnnn -goblin=goblin -You see a goblin, it is an evil and mischievous creature.=You see a goblin, it is an evil and mischievous creature. -Umm! Tasty meat.=Umm! Tasty meat. -veteran goblin=veteran goblin -You see a veteran goblin, an old warrior...=You see a veteran goblin, an old warrior... -Breakfast time!=Breakfast time! -soldier goblin=soldier goblin -You see a soldier goblin, a young warrior.=You see a soldier goblin, a young warrior. -amazoness archer=amazoness archer -You see an amazoness archer. She handles her bow quite well and her arrows are very dangerous when she shoots them at you out of the forest!=You see an amazoness archer. She handles her bow quite well and her arrows are very dangerous when she shoots them at you out of the forest! -amazoness archer commander=amazoness archer commander -You see an amazoness archer commander. She is the leader of amazoness archers. Her bow skills are incredible and her arrows pierce everything.=You see an amazoness archer commander. She is the leader of amazoness archers. Her bow skills are incredible and her arrows pierce everything. -amazoness bodyguard=amazoness bodyguard -You see an amazoness bodyguard who will guarantee the safety to the people of Amazon island.=You see an amazoness bodyguard who will guarantee the safety to the people of Amazon island. -amazoness coastguard=amazoness coastguard -You see an amazoness coastguard. She protects the coasts of Amazon Island from intruders. She is not too happy to see you running in her island.=You see an amazoness coastguard. She protects the coasts of Amazon Island from intruders. She is not too happy to see you running in her island. -amazoness coastguard mistress=amazoness coastguard mistress -You see an amazoness coastguard mistress. Even if they are women, these blue haired mistresses should not be underestimated.=You see an amazoness coastguard mistress. Even if they are women, these blue haired mistresses should not be underestimated. -amazoness commander=amazoness commander -You see an amazoness commander who is specialised in eliminating any strangers who dare to take a step to her island.=You see an amazoness commander who is specialised in eliminating any strangers who dare to take a step to her island. -amazoness elite coastguard=amazoness elite coastguard -You see an amazoness elite coastguard. When it is her shift to protect the island, you would just wish to have stayed at home.=You see an amazoness elite coastguard. When it is her shift to protect the island, you would just wish to have stayed at home. -amazoness hunter=amazoness hunter -You see an amazoness hunter. She creeps through the forest to hunt animals. But she doesn't like that you are in her hunting area!=You see an amazoness hunter. She creeps through the forest to hunt animals. But she doesn't like that you are in her hunting area! -amazoness vigilance=amazoness vigilance -You see an amazoness vigilance. Her senses are well tuned, so you have to be very silent when you don't want her to hear you!=You see an amazoness vigilance. Her senses are well tuned, so you have to be very silent when you don't want her to hear you! -amazoness imperator=amazoness imperator -You see an amazoness imperator. She is the emperor of Amazon island and has ruled it for decades and won't let you get past her very easily.=You see an amazoness imperator. She is the emperor of Amazon island and has ruled it for decades and won't let you get past her very easily. -bandit=bandit -You see a lean and mean bandit. He's out to get your life and purse!=You see a lean and mean bandit. He's out to get your life and purse! -disciple assassin=disciple assassin -You see a disciple assassin. He is quite young but he has learned to fight and kill already.=You see a disciple assassin. He is quite young but he has learned to fight and kill already. -My dad can beat your dad.=My dad can beat your dad. -I'm gonna tell my mommy.=I'm gonna tell my mommy. -Teacher, make him stop.=Teacher, make him stop. -apprentice assassin=apprentice assassin -You see an apprentice assassin. He has been taught some very awesome moves and methods, so he is already able to kill an enemy almost perfectly!=You see an apprentice assassin. He has been taught some very awesome moves and methods, so he is already able to kill an enemy almost perfectly! -Bring it on!=Bring it on! -Any time you're ready.=Any time you're ready. -You want a piece of me?=You want a piece of me? -Come on, i dare you.=Come on, i dare you. -hunter=hunter -You see a hunter.=You see a hunter. -I always get my prey!=I always get my prey! -assassin=assassin -You see an assassin. He is very good at killing his victims silently without being seen by anyone.=You see an assassin. He is very good at killing his victims silently without being seen by anyone. -Time to die.=Time to die. -ninja=ninja -Your keen eyes made you able to see this fast ninja. Prepare for a fight!=Your keen eyes made you able to see this fast ninja. Prepare for a fight! -samurai=samurai -You see a samurai.=You see a samurai. -shadows assassin=shadows assassin -You see a shadows assassin. He knows how to become one with the shadows and to move in the dark without being seen or making any sound.=You see a shadows assassin. He knows how to become one with the shadows and to move in the dark without being seen or making any sound. -master assassin=master assassin -You see a master assassin. He is a master of assassination! He is able to kill an enemy without letting him know that he is already there!=You see a master assassin. He is a master of assassination! He is able to kill an enemy without letting him know that he is already there! -Why do you return?=Why do you return? -You've been warned before.=You've been warned before. -Have you lost your marbles?=Have you lost your marbles? -barbarian=barbarian -You see a barbarian.=You see a barbarian. -barbarian wolf=barbarian wolf -You see a barbarian wolf.=You see a barbarian wolf. -barbarian elite=barbarian elite -You see a barbarian elite.=You see a barbarian elite. -barbarian priest=barbarian priest -You see a barbarian priest.=You see a barbarian priest. -barbarian chaman=barbarian chaman -You see a barbarian chaman.=You see a barbarian chaman. -King he say kill!=King he say kill! -barbarian leader=barbarian leader -You see a barbarian leader.=You see a barbarian leader. -Me destroy you!=Me destroy you! -barbarian king=barbarian king -You see a barbarian king.=You see a barbarian king. -blordrough quartermaster=blordrough quartermaster -You see a man who gave up his life as a puppet of the Blordrough army, all for the love of dragons. He is wonderfully happy.=You see a man who gave up his life as a puppet of the Blordrough army, all for the love of dragons. He is wonderfully happy. -We might be in hiding, but we are STILL mighty.=We might be in hiding, but we are STILL mighty. -blordrough corporal=blordrough corporal -You see a man who feels his past indicates how he should be treated now but desparately wishes to exercise his intelect in a game of chess.=You see a man who feels his past indicates how he should be treated now but desparately wishes to exercise his intelect in a game of chess. -You WILL show us the respect we deserve.=You WILL show us the respect we deserve. -Care for a game of chess?=Care for a game of chess? -blordrough storm trooper=blordrough storm trooper -You see a man who is but a fraction of his former glory as one of the most feared foes of Faiumoni. Now, his main concern is keeping his house clean and tidy.=You see a man who is but a fraction of his former glory as one of the most feared foes of Faiumoni. Now, his main concern is keeping his house clean and tidy. -Wipe your feet before entering our domain.=Wipe your feet before entering our domain. -I'll teach you to defile our sanctuary.=I'll teach you to defile our sanctuary. -cannibal woman=cannibal woman -You see a cannibal woman. She wants to make a soup out of your bones!=You see a cannibal woman. She wants to make a soup out of your bones! -Zag-Zag!=Zag-Zag! -cannibal=cannibal -You see a cannibal. He hunts you for cooking and eating your fresh meat! It's useless to tell him that your flesh is not good, better fight him or run away!=You see a cannibal. He hunts you for cooking and eating your fresh meat! It's useless to tell him that your flesh is not good, better fight him or run away! -Ou-Garrrr!=Ou-Garrrr! -elder cannibal=elder cannibal -You see an elder cannibal.=You see an elder cannibal. -Yummie!=Yummie! -chaos soldier=chaos soldier -You see a chaos soldier. Chaos soldiers are servants of the evil. Killing you is one of their evil plans.=You see a chaos soldier. Chaos soldiers are servants of the evil. Killing you is one of their evil plans. -chaos warrior=chaos warrior -You see a chaos warrior. This chaotic creature is fighting with a big blade to slay his enemies.=You see a chaos warrior. This chaotic creature is fighting with a big blade to slay his enemies. -Defy order! Seek chaos!=Defy order! Seek chaos! -chaos commander=chaos commander -You see a chaos commander who is working on an evil plan. You should take care of him before he fulfills his duty.=You see a chaos commander who is working on an evil plan. You should take care of him before he fulfills his duty. -Chaos in the wrath!=Chaos in the wrath! -chaos sorcerer=chaos sorcerer -You see a chaos sorcerer. Chaos sorcerers will cast some evil spells on you if you don't obey them.=You see a chaos sorcerer. Chaos sorcerers will cast some evil spells on you if you don't obey them. -I will summon chaos and fury upon you!=I will summon chaos and fury upon you! -chaos dragonrider=chaos dragonrider -You see a chaos dragonrider. He is very well trained in dragon riding, but he can also fight well on the ground.=You see a chaos dragonrider. He is very well trained in dragon riding, but he can also fight well on the ground. -chaos lord=chaos lord -You see a chaos lord who is one of the strongest chaos creatures in Faiumoni. You should prepare well before attacking him.=You see a chaos lord who is one of the strongest chaos creatures in Faiumoni. You should prepare well before attacking him. -Chaos will rule!=Chaos will rule! -chaos overlord=chaos overlord -You see a chaos overlord. It is him who directs the creation of evil plans and gives orders to all chaotic creatures which exist.=You see a chaos overlord. It is him who directs the creation of evil plans and gives orders to all chaotic creatures which exist. -I am become death, destroyer of worlds.=I am become death, destroyer of worlds. -chaos pawn=chaos pawn -You see a pawn for a game of chess. He will drop nothing if you kill him.=You see a pawn for a game of chess. He will drop nothing if you kill him. -chaos rook=chaos rook -You see a rook for a game of chess. He will drop nothing if you kill him.=You see a rook for a game of chess. He will drop nothing if you kill him. -chaos knight=chaos knight -You see a knight for a game of chess. He will drop nothing if you kill him.=You see a knight for a game of chess. He will drop nothing if you kill him. -chaos bishop=chaos bishop -You see a bishop for a game of chess. He will not reward you if you kill him.=You see a bishop for a game of chess. He will not reward you if you kill him. -chaos king=chaos king -You see a king of a game of chess. He will reward nothing if you kill him.=You see a king of a game of chess. He will reward nothing if you kill him. -chaos queen=chaos queen -You see a queen piece for a game of chess. She will reward nothing if you kill her.=You see a queen piece for a game of chess. She will reward nothing if you kill her. -imperial defender=imperial defender -You see an imperial defender.=You see an imperial defender. -imperial veteran=imperial veteran -You see an imperial veteran. He fought in many battles and wars. Now he is old, but over the years he always looked for a new enemy to fight against.=You see an imperial veteran. He fought in many battles and wars. Now he is old, but over the years he always looked for a new enemy to fight against. -imperial archer=imperial archer -You see an imperial archer.=You see an imperial archer. -imperial priest=imperial priest -You see an imperial priest. The church has fallen out of favor with the citizens as of late, but this imperial priest still clings to the faith.=You see an imperial priest. The church has fallen out of favor with the citizens as of late, but this imperial priest still clings to the faith. -imperial elite guardian=imperial elite guardian -You see an Imperial Elite Guardian. He is a very well trained guardian and he will protect his environment until the end.=You see an Imperial Elite Guardian. He is a very well trained guardian and he will protect his environment until the end. -imperial scientist=imperial scientist -You see an imperial scientist. He is quite clever but also a little bit crazy and unfortunately he uses his intelligence for evil experiments.=You see an imperial scientist. He is quite clever but also a little bit crazy and unfortunately he uses his intelligence for evil experiments. -imperial high priest=imperial high priest -You see an imperial high priest who's troubled by the actions of the imperial science department. Be careful, he might take those frustrations out on you!=You see an imperial high priest who's troubled by the actions of the imperial science department. Be careful, he might take those frustrations out on you! -imperial archer leader=imperial archer leader -You see an imperial archer leader.=You see an imperial archer leader. -imperial elite archer=imperial elite archer -You see an imperial elite archer.=You see an imperial elite archer. -imperial leader=imperial leader -You see a well dressed imperial leader. It's troops will try to dash you.=You see a well dressed imperial leader. It's troops will try to dash you. -imperial knight=imperial knight -You see an imperial knight.=You see an imperial knight. -imperial chief=imperial chief -You see an imperial chief.=You see an imperial chief. -imperial commander=imperial commander -You see an imperial commander.=You see an imperial commander. -Our troops will crush you!=Our troops will crush you! -imperial general=imperial general -You see an imperial general with a huge scar above his eye. He is still strong, don't think that he will not see you!=You see an imperial general with a huge scar above his eye. He is still strong, don't think that he will not see you! -Our troops will crush you!=Our troops will crush you! -emperor dalmung=emperor dalmung -You see an emperor dalmung who stands before you. Once a powerful warlock trained by a mighty magician, but he disappeared decades ago. Now it seems he's in league with Ivan Abe!=You see an emperor dalmung who stands before you. Once a powerful warlock trained by a mighty magician, but he disappeared decades ago. Now it seems he's in league with Ivan Abe! -My empire is undefeatable!=My empire is undefeatable! -kalavan housewife=kalavan housewife -You see a kalavan housewife. She lives in Kalavan city and she very angry with rude people who want to steal her food out of the kitchen and destroy her beautiful flower garden!=You see a kalavan housewife. She lives in Kalavan city and she very angry with rude people who want to steal her food out of the kitchen and destroy her beautiful flower garden! -kalavan peasant=kalavan peasant -You see a kalavan peasant. He works in the fields around Kalavan city. As he heard there were strangers in the city he came to help the kalavan citizens in fighting the enemies!=You see a kalavan peasant. He works in the fields around Kalavan city. As he heard there were strangers in the city he came to help the kalavan citizens in fighting the enemies! -kalavan wisewoman=kalavan wisewoman -You see a kalavan wisewoman. Normally she sits in her small room and does some handworking but now she wants to have some action!=You see a kalavan wisewoman. Normally she sits in her small room and does some handworking but now she wants to have some action! -kalavan citizen=kalavan citizen -You see a kalavan citizen. He lives in kalavan city and tries to protect it from invaders!=You see a kalavan citizen. He lives in kalavan city and tries to protect it from invaders! -madaram archer=madaram archer -You see a madaram archer.=You see a madaram archer. -madaram peasant=madaram peasant -You see a madaram peasant.=You see a madaram peasant. -madaram trooper=madaram trooper -You see a madaram trooper.=You see a madaram trooper. -madaram soldier=madaram soldier -You see a madaram soldier.=You see a madaram soldier. -madaram healer=madaram healer -You see a madaram healer.=You see a madaram healer. -madaram axeman=madaram axeman -You see a madaram axeman.=You see a madaram axeman. -madaram queen=madaram queen -You see a madaram queen.=You see a madaram queen. -madaram hero=madaram hero -You see a madaram hero.=You see a madaram hero. -madaram windwalker=madaram windwalker -You see a madaram windwalker.=You see a madaram windwalker. -madaram cavalry=madaram cavalry -You see a madaram cavalry.=You see a madaram cavalry. -madaram stalker=madaram stalker -You see a madaram stalker.=You see a madaram stalker. -madaram buster blader=madaram buster blader -You see a madaram buster blader.=You see a madaram buster blader. -madaram pawn=madaram pawn -You see a pawn for a game of chess. It will reward nothing if killed.=You see a pawn for a game of chess. It will reward nothing if killed. -madaram rook=madaram rook -You see a rook for a game of chess. It will scarcely reward you to kill it.=You see a rook for a game of chess. It will scarcely reward you to kill it. -madaram knight=madaram knight -You see a knight, a chess piece. He won't reward you if you kill him.=You see a knight, a chess piece. He won't reward you if you kill him. -madaram bishop=madaram bishop -You see a bishop for a game of chess. It will reward you nothing if you slay it.=You see a bishop for a game of chess. It will reward you nothing if you slay it. -madaram king=madaram king -You see a king to play chess with, he will reward you nothing if you kill him.=You see a king to play chess with, he will reward you nothing if you kill him. -madaram queen =madaram queen -You see a queen piece for a game of chess. She will reward nothing if you kill her.=You see a queen piece for a game of chess. She will reward nothing if you kill her. -mithrilbourgh soldier=mithrilbourgh soldier -You see a mithrilbourgh soldier.=You see a mithrilbourgh soldier. -mithrilbourgh archer=mithrilbourgh archer -You see a mithrilbourgh archer.=You see a mithrilbourgh archer. -mithrilbourgh knight=mithrilbourgh knight -You see a mithrilbourgh knight.=You see a mithrilbourgh knight. -mithrilbourgh elite archer=mithrilbourgh elite archer -You see a mithrilbourgh elite archer.=You see a mithrilbourgh elite archer. -mithrilbourgh sergeant=mithrilbourgh sergeant -You see a mithrilbourgh sergeant.=You see a mithrilbourgh sergeant. -mithrilbourgh lieutenant=mithrilbourgh lieutenant -You see a mithrilbourgh lieutenant.=You see a mithrilbourgh lieutenant. -mithrilbourgh colonel=mithrilbourgh colonel -You see a mithrilbourgh colonel.=You see a mithrilbourgh colonel. -mithrilbourgh wizard=mithrilbourgh wizard -You see a mithrilbourgh wizard.=You see a mithrilbourgh wizard. -mithrilbourgh general=mithrilbourgh general -You see a mithrilbourgh general. Some call him Hawkeyes. He cast his gaze over the city walls and saw you coming from miles away.=You see a mithrilbourgh general. Some call him Hawkeyes. He cast his gaze over the city walls and saw you coming from miles away. -monk=monk -You see a monk. Monks are very calm and peaceful. They live alone among themselves, but they don't want to be interrupted by anyone.=You see a monk. Monks are very calm and peaceful. They live alone among themselves, but they don't want to be interrupted by anyone. -Meet you after Cataclysm.=Meet you after Cataclysm. -The end of the world is nigh...=The end of the world is nigh... -Yield to the Power!=Yield to the Power! -I see you, heretic!=I see you, heretic! -darkmonk=darkmonk -You see a Darkmonk. Darkmonks can control dark powers, and they will do anything to get you out of their way!=You see a Darkmonk. Darkmonks can control dark powers, and they will do anything to get you out of their way! -Hail to the greater Power!=Hail to the greater Power! -The Cataclysm cometh!=The Cataclysm cometh! -devil monk=devil monk -You see a devil monk. He is a member of a devil cult which practises black rituals.=You see a devil monk. He is a member of a devil cult which practises black rituals. -risecia thug=risecia thug -You see a risecia thug, stay away from him!=You see a risecia thug, stay away from him! -risecia archer=risecia archer -You see a risecia archer, just one of the warriors who wants to protect his chief.=You see a risecia archer, just one of the warriors who wants to protect his chief. -risecia swordsman=risecia swordsman -You see a risecia swordsman, the blade of his sword will cut your head!=You see a risecia swordsman, the blade of his sword will cut your head! -Chief Falatheen of the Risecia=Chief Falatheen of the Risecia -You see the Chief Falatheen of the Risecia, he doesn't want to die! His little warriors protect him against invaders.=You see the Chief Falatheen of the Risecia, he doesn't want to die! His little warriors protect him against invaders. -littlewitch=littlewitch -You see a little witch. Witches are are evil little girls who are very dangerous.=You see a little witch. Witches are are evil little girls who are very dangerous. -littlewizard=littlewizard -You see a little wizard. Although he is a child, he is already able to cast powerful spells.=You see a little wizard. Although he is a child, he is already able to cast powerful spells. -robot aruthon=robot aruthon -You see a robot aruthon.=You see a robot aruthon. -dark aruthon=dark aruthon -You see a dark aruthon.=You see a dark aruthon. -leprechaun=leprechaun -You see a leprechaun, you would be lucky if you found his treasure.=You see a leprechaun, you would be lucky if you found his treasure. -They're always after my Lucky Charms!!=They're always after my Lucky Charms!! -Clovers and rainbows... stars and horseshoes...=Clovers and rainbows... stars and horseshoes... -littlefairy=littlefairy -You see a little fairy, which is magical and nice.=You see a little fairy, which is magical and nice. -clurichaun=clurichaun -You see a clurichaun. He is rich, but you don't know if you are able to find his gold!=You see a clurichaun. He is rich, but you don't know if you are able to find his gold! -Floating blue balloons...=Floating blue balloons... -Join me for a drink, lassie?=Join me for a drink, lassie? -brain ahouga=brain ahouga -You see a brain ahouga, a very strange creature only living in your dreams!=You see a brain ahouga, a very strange creature only living in your dreams! -blue mohiko=blue mohiko -You see a blue mohiko.=You see a blue mohiko. -red roohako=red roohako -You see a red roohako. It is coming for you in your deepest dreams.=You see a red roohako. It is coming for you in your deepest dreams. -arachne=arachne -You see Arachne, a horrid half woman half spider. With so many legs she can hit you very fast, and her poisonous bite is fierce.=You see Arachne, a horrid half woman half spider. With so many legs she can hit you very fast, and her poisonous bite is fierce. -cat woman=cat woman -You see a cat woman. She's agile, stealthy, fast and sharp, so stay focused and don't get cut!=You see a cat woman. She's agile, stealthy, fast and sharp, so stay focused and don't get cut! -frogman=frogman -You see a frogman which lives in a lake which isn't very deep.=You see a frogman which lives in a lake which isn't very deep. -elite frogman=elite frogman -You see an elite frogman. His huge eyes let you shiver!=You see an elite frogman. His huge eyes let you shiver! -wizard frogman=wizard frogman -You see a mighty wizard frogman. His wand and magical cloak show his wisdom and power.=You see a mighty wizard frogman. His wand and magical cloak show his wisdom and power. -kobold=kobold -You see a kobold. People say that it is a mythical spirit.=You see a kobold. People say that it is a mythical spirit. -Grrrr! Wooof!=Grrrr! Wooof! -soldier kobold=soldier kobold -You see a soldier kobold, he's protecting his king.=You see a soldier kobold, he's protecting his king. -Woorf wer warf!=Woorf wer warf! -archer kobold=archer kobold -You see an archer kobold. It runs away from humans.=You see an archer kobold. It runs away from humans. -Wssss! Warf wit!=Wssss! Warf wit! -veteran kobold=veteran kobold -You see a veteran kobold, it is mighty!=You see a veteran kobold, it is mighty! -leader kobold=leader kobold -You see a leader kobold, one of the mighty warriors.=You see a leader kobold, one of the mighty warriors. -minotaur=minotaur -You see a minotaur. He escaped from his labyrinth and is very angry now!=You see a minotaur. He escaped from his labyrinth and is very angry now! -You will never find the way out!=You will never find the way out! -minotaur king=minotaur king -You see a Minotaur King. He doesn't have to escape from his labyrinth to be very angry...=You see a Minotaur King. He doesn't have to escape from his labyrinth to be very angry... -I am the king of this area!=I am the king of this area! -naga=naga -You see a naga. She awaits unwary victims to whom she may give her deadly kiss.=You see a naga. She awaits unwary victims to whom she may give her deadly kiss. -high naga=high naga -You see a high naga.=You see a high naga. -armed naga=armed naga -You see an armed naga.=You see an armed naga. -lamia=lamia -You see a lamia.=You see a lamia. -ratman=ratman -You see a ratman. He is a mutation that comes from old times.=You see a ratman. He is a mutation that comes from old times. -You, humans, never leave us alone!=You, humans, never leave us alone! -ratwoman=ratwoman -You see a ratwoman. She is a mutation of women with the rats in the century XVIII.=You see a ratwoman. She is a mutation of women with the rats in the century XVIII. -You humans never leave us alone!=You humans never leave us alone! -archrat=archrat -You see an archrat. You will be distracted with its aura, be careful...=You see an archrat. You will be distracted with its aura, be careful... -Feel my wrath!=Feel my wrath! -You're messing with the rats?!=You're messing with the rats?! -Your death has come!=Your death has come! -sheepman=sheepman -You see a sheepman.=You see a sheepman. -I'm no fluffy lamb!=I'm no fluffy lamb! -armored sheepman=armored sheepman -You see an armored sheepman.=You see an armored sheepman. -You're baaaarmy to attack me!=You're baaaarmy to attack me! -elder sheepman=elder sheepman -You see an elder sheepman.=You see an elder sheepman. -Bah! Stay away!=Bah! Stay away! -elite sheepman=elite sheepman -You see an elite sheepman.=You see an elite sheepman. -Baaaaack away!=Baaaaack away! -werewolf=werewolf -You see a werewolf. Normally he is a human but at fullmoon nights he turns into a dangerous beast! His poison seems to last forever.=You see a werewolf. Normally he is a human but at fullmoon nights he turns into a dangerous beast! His poison seems to last forever. -xenocium=xenocium -You see a xenocium.=You see a xenocium. -oni warrior=oni warrior -You see an oni warrior. His purple skin color should not lull you into a false sense of security!=You see an oni warrior. His purple skin color should not lull you into a false sense of security! -oni archer=oni archer -You see an oni archer. His arrows will follow you everywhere for protecting his queen!=You see an oni archer. His arrows will follow you everywhere for protecting his queen! -oni priest=oni priest -You see a mysterious oni priest, uttering strange incantations.=You see a mysterious oni priest, uttering strange incantations. -oni king=oni king -You see the oni king. His power leads brave oni troops around his kingdom.=You see the oni king. His power leads brave oni troops around his kingdom. -Revere me, the Oni King!=Revere me, the Oni King! -oni queen=oni queen -You see the oni queen. She supports her husband and takes an eye onto their kingdom while he conquers new land and fights on battlefields.=You see the oni queen. She supports her husband and takes an eye onto their kingdom while he conquers new land and fights on battlefields. -You must bow before the Oni Queen.=You must bow before the Oni Queen. -orc=orc -You see an orc which is noisy and silly.=You see an orc which is noisy and silly. -orc spearman=orc spearman -You see an orc spearman. Run away fast before a spear pierces you.=You see an orc spearman. Run away fast before a spear pierces you. -orc hunter=orc hunter -You see an orc hunter. It it seeking for you!=You see an orc hunter. It it seeking for you! -orc warrior=orc warrior -You see an orc warrior which doesn't like you searching in the mountain.=You see an orc warrior which doesn't like you searching in the mountain. -orc chief=orc chief -You see an orc chief which is too busy to attend to you!=You see an orc chief which is too busy to attend to you! -mountain orc=mountain orc -You see a mountain orc.=You see a mountain orc. -mountain orc warrior=mountain orc warrior -You see a mountain orc warrior.=You see a mountain orc warrior. -mountain orc hunter=mountain orc hunter -You see a mountain orc hunter.=You see a mountain orc hunter. -mountain orc chief=mountain orc chief -You see a mountain orc chief.=You see a mountain orc chief. -cyclops=cyclops -You see a cyclops. It just has one eye.=You see a cyclops. It just has one eye. -ogre=ogre -You see an ogre, it is nude and rude!=You see an ogre, it is nude and rude! -elder ogre=elder ogre -You see an elder ogre, he's in the last moments of his life, take pity on him.=You see an elder ogre, he's in the last moments of his life, take pity on him. -ogre soldier=ogre soldier -You see an ogre soldier, smelling like it didn't have had a bath for years. Ewww.=You see an ogre soldier, smelling like it didn't have had a bath for years. Ewww. -superogre=superogre -You see a superogre which belongs to the family of very material creatures, the Ogres. Ever since this one happened upon a peculiar helmet and has been revered among its peers. Take care, it throws a mean punch!=You see a superogre which belongs to the family of very material creatures, the Ogres. Ever since this one happened upon a peculiar helmet and has been revered among its peers. Take care, it throws a mean punch! -troll=troll -You see a troll, be careful in his terrain.=You see a troll, be careful in his terrain. -red troll=red troll -You see a red troll, he's burning with anger.=You see a red troll, he's burning with anger. -cave troll=cave troll -You see a cave troll.=You see a cave troll. -Uggga yggga arghhhhh=Uggga yggga arghhhhh -Ooggga UGGga arhhh=Ooggga UGGga arhhh -zombie rat=zombie rat -You see a hungry zombie rat and it doesn't like to see you.=You see a hungry zombie rat and it doesn't like to see you. -SHAAAAAAA!!=SHAAAAAAA!! -SHREEAK!!=SHREEAK!! -ghost hound=ghost hound -As you catch sight of its shaggy shape and gleaming evil eyes, you suddenly remember all the horror stories around the dreaded ghost hound!=As you catch sight of its shaggy shape and gleaming evil eyes, you suddenly remember all the horror stories around the dreaded ghost hound! -Ghhrr!=Ghhrr! -WooOouuuUuh!=WooOouuuUuh! -fallen warrior=fallen warrior -You see a fallen warrior.=You see a fallen warrior. -fallen priest=fallen priest -You see a fallen priest. He was once just a regular human priest, until the dark powers corrupted him and turned him to the dark side.=You see a fallen priest. He was once just a regular human priest, until the dark powers corrupted him and turned him to the dark side. -fallen high priest=fallen high priest -You see a fallen high priest.=You see a fallen high priest. -lich=lich -You see a lich, a dark desolate creature which has been doomed to wander in endless turmoil, feeding on the souls of the righteous.=You see a lich, a dark desolate creature which has been doomed to wander in endless turmoil, feeding on the souls of the righteous. -dead lich=dead lich -You see a dead lich with his huge green and heavy cloak. Although he is dead and bony, he'll seek revenge for his too early passing and will always return fast and ruthless!=You see a dead lich with his huge green and heavy cloak. Although he is dead and bony, he'll seek revenge for his too early passing and will always return fast and ruthless! -high lich=high lich -The terrible grinning skull of a high lich leers at you as it prepares to slaughter you in a myriad of magical ways.=The terrible grinning skull of a high lich leers at you as it prepares to slaughter you in a myriad of magical ways. -death knight=death knight -You see a death knight. Some time ago he fought in tournaments where his only aim was to kill as many enemies as possible.=You see a death knight. Some time ago he fought in tournaments where his only aim was to kill as many enemies as possible. -necrosophia=necrosophia -You see Necrosophia. She is a mighty beast, she will burn you with fire while you are disoriented by the rainbow beans.=You see Necrosophia. She is a mighty beast, she will burn you with fire while you are disoriented by the rainbow beans. -mummy=mummy -You see a mummy which usually sleeps in a coffin.=You see a mummy which usually sleeps in a coffin. -royal mummy=royal mummy -You see a royal mummy. You wonder what keeps this king's soul from finding peace.=You see a royal mummy. You wonder what keeps this king's soul from finding peace. -skeleton=skeleton -You see a skeleton which survives to get revenge.=You see a skeleton which survives to get revenge. -Thou shall not pass!=Thou shall not pass! -tiny skelly=tiny skelly -You see a tiny skelly. It is silly, little and evil, be careful with its poison...=You see a tiny skelly. It is silly, little and evil, be careful with its poison... -Thou shall not maim!=Thou shall not maim! -warrior skeleton=warrior skeleton -You see a warrior skeleton, it's just a normal skeleton with a shield!=You see a warrior skeleton, it's just a normal skeleton with a shield! -Thou shall not kill!=Thou shall not kill! -elder skeleton=elder skeleton -You see the bones of an old man. He lived to a ripe old age, and is sure to know a thing or two that separates him from all the vengeful skeletons that died young.=You see the bones of an old man. He lived to a ripe old age, and is sure to know a thing or two that separates him from all the vengeful skeletons that died young. -Thou shall not survive!=Thou shall not survive! -demon skeleton=demon skeleton -You see a demon skeleton. Demon skeletons have ascended from the underworld and they are going to take over this world. You really should slay them before it is too late!=You see a demon skeleton. Demon skeletons have ascended from the underworld and they are going to take over this world. You really should slay them before it is too late! -Thou shall not rob!=Thou shall not rob! -gashadokuro=gashadokuro -A giant figure made of bone, known only as gashadokuro, towers before you. He is created from the bones of starving humans.=A giant figure made of bone, known only as gashadokuro, towers before you. He is created from the bones of starving humans. -ghost=ghost -You see a ghost, don't be afraid! It doesn't seem to be evil.=You see a ghost, don't be afraid! It doesn't seem to be evil. -Ooooouuuuuh!=Ooooouuuuuh! -death=death -You see Death. Do you have the will to stare him in the face? Today he's not in the mood to bargain with your soul.=You see Death. Do you have the will to stare him in the face? Today he's not in the mood to bargain with your soul. -Judgement is coming!=Judgement is coming! -Uuuuhhhhh!, i'm gonna get you!=Uuuuhhhhh!, i'm gonna get you! -invisible man=invisible man -You see nothing but the landscape. As you stare at the point where you think you have seen something, you suddenly see the slight contour of a man!=You see nothing but the landscape. As you stare at the point where you think you have seen something, you suddenly see the slight contour of a man! -My unnatural state should evoke fear in you ...=My unnatural state should evoke fear in you ... -green phantom=green phantom -You see a green phantom.=You see a green phantom. -incorporeal armor=incorporeal armor -You see an incorporeal armor. You are quite surprised as it walks towards you because there is nobody in it.=You see an incorporeal armor. You are quite surprised as it walks towards you because there is nobody in it. -Thou shall not pillage!=Thou shall not pillage! -black death=black death -You see a black death. With his scythe he reaps your soul and then he takes it over the river where it has to stay until judgement day arrives.=You see a black death. With his scythe he reaps your soul and then he takes it over the river where it has to stay until judgement day arrives. -Your time to die has come!=Your time to die has come! -Say goodbye to this world!=Say goodbye to this world! -golden death=golden death -You see the golden death. He came to take your soul away to pay for all your sins.=You see the golden death. He came to take your soul away to pay for all your sins. -You will dream of me tonight...=You will dream of me tonight... -You will pay for your greed!=You will pay for your greed! -vampire bride=vampire bride -You see a vampire bride. She was resurrected from the dead by a wicked vampire lord.=You see a vampire bride. She was resurrected from the dead by a wicked vampire lord. -vampirette=vampirette -You see a Vampirette. Beware of its sharp teeth! She wants to drink your blood and turn you into a vampire!=You see a Vampirette. Beware of its sharp teeth! She wants to drink your blood and turn you into a vampire! -vampire lord=vampire lord -You see a vampire lord. Be careful, he is after your blood!=You see a vampire lord. Be careful, he is after your blood! -bloody zombie=bloody zombie -You see a bloody zombie. Can you see the drops of blood which fall from his body?=You see a bloody zombie. Can you see the drops of blood which fall from his body? -brrraaaaaaaaaains=brrraaaaaaaaaains -zombie=zombie -You see a zombie. It woke up just to take revenge, be silent.=You see a zombie. It woke up just to take revenge, be silent. -headless monster=headless monster -You see a headless monster, how can he hear that we come to him?=You see a headless monster, how can he hear that we come to him? -rotten zombie=rotten zombie -You see a rotten zombie. You are disgusted as you see its rotten skin that hangs down in pieces from his half rotten bones.=You see a rotten zombie. You are disgusted as you see its rotten skin that hangs down in pieces from his half rotten bones. -bluuergghhhoooorrghhhhh=bluuergghhhoooorrghhhhh diff --git a/data/languages/es.txt b/data/languages/es.txt index 0d4abc6e4e..24a41b8c52 100644 --- a/data/languages/es.txt +++ b/data/languages/es.txt @@ -1,529 +1,2886 @@ -dress=vestido -You see a dress, which provides practically no defense. It is barely worth picking up.=Ves un vestido, que no proporciona prácticamente defensa alguna. Apenas vale la pena recogerlo. -leather armor=armadura de cuero -You see a worn suit of leather armor. It will provide only minimal protection.=Ves un traje de armadura de cuero desgastado. Sólo te proporcionará una mínima protección. -leather cuirass=coraza de cuero -You see a leather cuirass, well made but still able to be pierced by sharp swords.=Ves una coraza de cuero, bien hecha pero que todavía puede ser traspasada por espadas afiladas. -leather scale armor=armadura de escamas de cuero -You see a suit of leather scale armor. The leather is toughened somehow for better protection.=Ves un traje de armadura de escamas de cuero. El cuero ha sido endurecido de alguna forma para una mejor protección. -pauldroned leather cuirass=coraza de cuero con hombreras. -You see a pauldroned leather cuirass. It was crafted to withstand minor attacks.=Ves una coraza de cuero con hombreras. Fue diseñada para soportar ataques menores. -studded armor=armadura tachonada -You see a suit of studded armor. Some studs are loose or rusty but it is still fit for purpose.=Ves un traje de armadura tachonada. Algunos tachones están sueltos u oxidados pero todavía es adecuada para su finalidad. -chain armor=cota de malla -You see a suit of chain armor. Perhaps even if you don't need it, you can sell it.=Ves un traje de armadura de cota de malla. Quizá incluso si no la necesitas, puedas venderla. -enhanced chainmail=cota de malla mejorada -You see a robust suit of enhanced chainmail. The links are closely spaced so even the sharpest point should not pierce it.=Ves un traje robusto de cota de malla. Los eslabones están muy próximos entre sí por lo que incluso la punta más afilada no debería de poder traspasarla. -scale armor=armadura de escamas -You see a suit of scale armor. It is not flexible like chain, but will be a better defense.=Ves un traje de armadura de escamas. No es flexible como la cota de malla, pero te proporcionará una mejor defensa. -iron scale armor=armadura de escamas de hierro -You see a suit of iron scale armor, whose rounded scales will protect you from many blows.=Ves un traje de armadura de escamas de hierro, cuyas escamas redondeadas te protegerán de muchos golpes. -golden chainmail=cota de malla de oro -You see a valuable suit of golden chainmail. It is crafted from fine links of expensive metal.=Ves un valioso traje de cota de malla de oro. Se ha elaborado a partir de finos eslabones de caro metal. -pauldroned iron cuirass=coraza de hierro con hombreras -You see a formidable pauldroned iron cuirass. The pauldrons (shoulder pieces) will protect your upper half.=Ves una formidable coraza de hierro con hombreras. Las hombreras (las piezas de los hombros) protegerán tu mitad superior. -plate armor=armadura de placas de acero -You see a suit of plate armor. You imagine it would be heavy but the steel is well tempered and the suit is very strong.=Ves un traje de armadura de placas de acero. Te imaginas que será muy pesada pero el acero está bien templado y el traje es muy resistente. -red armor=armadura roja -You see a suit of red armor. The color is a striking crimson.=Ves un traje de armadura roja. Su color es de un llamativo tono carmesí. -blue armor=armadura azul -You see a suit of blue armor. The color is stunning azure.=Ves un traje de armadura azul. Su color es un impresionante azul. -elvish armor=armadura élfica -You see a suit of elvish armor. It's made from tree leaves! But somehow constructed to give strong defense.=Ves un traje de armadura élfica ¡Está elaborada a partir de hojas de árbol! Pero construido de alguna forma para proporcionar una resistente defensa. + +## +## Language: Spanish (es) +## Translators: unknown, AntumDeluge +## + + +# items names and descriptions + +## armors + barbarian armor=armadura de bárbaro You see a suit of barbarian armor. It's dirty and smelly, but still, rather strong.=Ves un traje de armadura de bárbaro. Está sucia y huele mal, pero aún así, es bastante resistente. -golden armor=armadura de oro -You see a suit of golden armor. It's made from the best materials with great attention to detail.=Ves un traje de armadura de oro. Está elaborada a partir de los mejores materiales con gran atención al detalle. -shadow armor=armadura de sombra -You see a suit of shadow armor. It's hard to decide with what it is made, it is so dark.=Ves un traje dearmadura de sombra. Es difícil decidir de qué está hecha, es tan oscura. -dwarvish armor=armadura enana -You see a dwarvish armor. It's made with ingenuity and skill.=Ves una armadura enana. Se ha elaborado con ingenio y habilidad. -stone armor=armadura de piedra -You see a suit of stone armor. Swords will crumble if they try to pierce it.=Ves un traje de armadura de piedra. Las espadas se quebrarán si intentan traspasarla. -ice armor=armadura de hielo -You see a suit of ice armor, to protect even the coldest of hearts.=Ves un traje de armadura de hielo, para proteger incluso al mas frío de los corazones. -chaos armor=armadura del caos -You see a suit of chaos armor. The reflecting contrasting colors make no sense to you.=Ves un traje de armadura del caos. Los contrastantes colores que se reflejan en ella para ti no tienen ningún sentido. -xeno armor=armadura de xeno -You see a suit of xeno armor. The colors remind you of the xenocium hybrid creature.=Ves un traje de armadura de xeno. Sus colores te recuerdan a la criatura híbrida xenocium. -mainio armor=armadura de mainio -You see a strange strange suit of mainio armor. What it lacks in subtlety it makes up for with strength.=Ves un extraño traje de armadura de mainio. Lo que le falta en sutileza lo suple con fortaleza. -magic plate armor=armadura de placas de acero mágica -You see a suit of magic plate armor. The steel is well tempered and the suit is very strong.=Ves un traje de armadura de placas de acero mágica. El acero está bien templado y el traje es muy resistente. + black armor=armadura negra You see a suit of black armor. Does it put you in a dark mood?=Ves un traje de armadura negra ¿Te pone de negro humor? + +blue armor=armadura azul +You see a suit of blue armor. The color is stunning azure.=Ves un traje de armadura azul. Su color es un impresionante azul. + +chain armor=cota de malla +You see a suit of chain armor. Perhaps even if you don't need it, you can sell it.=Ves un traje de armadura de cota de malla. Quizá incluso si no la necesitas, puedas venderla. + +chaos armor=armadura del caos +You see a suit of chaos armor. The reflecting contrasting colors make no sense to you.=Ves un traje de armadura del caos. Los contrastantes colores que se reflejan en ella para ti no tienen ningún sentido. + +dress=vestido +You see a dress, which provides practically no defense. It is barely worth picking up.=Ves un vestido, que no proporciona prácticamente defensa alguna. Apenas vale la pena recogerlo. + +dwarvish armor=armadura enana +You see a dwarvish armor. It's made with ingenuity and skill.=Ves una armadura enana. Se ha elaborado con ingenio y habilidad. + +elvish armor=armadura élfica +You see a suit of elvish armor. It's made from tree leaves! But somehow constructed to give strong defense.=Ves un traje de armadura élfica ¡Está elaborada a partir de hojas de árbol! Pero construido de alguna forma para proporcionar una resistente defensa. + +enhanced chainmail=cota de malla mejorada +You see a robust suit of enhanced chainmail. The links are closely spaced so even the sharpest point should not pierce it.=Ves un traje robusto de cota de malla. Los eslabones están muy próximos entre sí por lo que incluso la punta más afilada no debería de poder traspasarla. + +golden armor=armadura de oro +You see a suit of golden armor. It's made from the best materials with great attention to detail.=Ves un traje de armadura de oro. Está elaborada a partir de los mejores materiales con gran atención al detalle. + +golden chainmail=cota de malla de oro +You see a valuable suit of golden chainmail. It is crafted from fine links of expensive metal.=Ves un valioso traje de cota de malla de oro. Se ha elaborado a partir de finos eslabones de caro metal. + +ice armor=armadura de hielo +You see a suit of ice armor, to protect even the coldest of hearts.=Ves un traje de armadura de hielo, para proteger incluso al mas frío de los corazones. + +iron scale armor=armadura de escamas de hierro +You see a suit of iron scale armor, whose rounded scales will protect you from many blows.=Ves un traje de armadura de escamas de hierro, cuyas escamas redondeadas te protegerán de muchos golpes. + +leather armor=armadura de cuero +You see a worn suit of leather armor. It will provide only minimal protection.=Ves un traje de armadura de cuero desgastado. Sólo te proporcionará una mínima protección. + +leather cuirass=coraza de cuero +You see a leather cuirass, well made but still able to be pierced by sharp swords.=Ves una coraza de cuero, bien hecha pero que todavía puede ser traspasada por espadas afiladas. + +leather scale armor=armadura de escamas de cuero +You see a suit of leather scale armor. The leather is toughened somehow for better protection.=Ves un traje de armadura de escamas de cuero. El cuero ha sido endurecido de alguna forma para una mejor protección. + +magic plate armor=armadura de placas de acero mágica +You see a suit of magic plate armor. The steel is well tempered and the suit is very strong.=Ves un traje de armadura de placas de acero mágica. El acero está bien templado y el traje es muy resistente. + +mainio armor=armadura de mainio +You see a strange suit of mainio armor. What it lacks in subtlety it makes up for with strength.=Ves un extraño traje de armadura de mainio. Lo que le falta en sutileza lo suple con fortaleza. + mithril armor=armadura de mithril You see a suit of mithril armor. Its tightly woven links are truly amazing.=Ves un traje de armadura de mithril. Sus eslabones apretadamente tejidos son verdaderamante asombrosos. + +pauldroned iron cuirass=coraza de hierro con hombreras +You see a formidable pauldroned iron cuirass. The pauldrons (shoulder pieces) will protect your upper half.=Ves una formidable coraza de hierro con hombreras. Las hombreras (las piezas de los hombros) protegerán tu mitad superior. + +pauldroned leather cuirass=coraza de cuero con hombreras +You see a pauldroned leather cuirass. It was crafted to withstand minor attacks.=Ves una coraza de cuero con hombreras. Fue diseñada para soportar ataques menores. + +plate armor=armadura de placas de acero +You see a suit of plate armor. You imagine it would be heavy but the steel is well tempered and the suit is very strong.=Ves un traje de armadura de placas de acero. Te imaginas que será muy pesada pero el acero está bien templado y el traje es muy resistente. + +red armor=armadura roja +You see a suit of red armor. The color is a striking crimson.=Ves un traje de armadura roja. Su color es de un llamativo tono carmesí. + +royal armor= +You see a suit of royal armor. This armor is fit for a king.= + +scale armor=armadura de escamas +You see a suit of scale armor. It is not flexible like chain, but will be a better defense.=Ves un traje de armadura de escamas. No es flexible como la cota de malla, pero te proporcionará una mejor defensa. + +scuba gear= +You see a scuba suit with a heavy helmet attached. It is dense enough to offer protection under the waves but of little use on land.= + +shadow armor=armadura de sombra +You see a suit of shadow armor. It's hard to decide with what it is made, it is so dark.=Ves un traje dearmadura de sombra. Es difícil decidir de qué está hecha, es tan oscura. + +stone armor=armadura de piedra +You see a suit of stone armor. Swords will crumble if they try to pierce it.=Ves un traje de armadura de piedra. Las espadas se quebrarán si intentan traspasarla. + +studded armor=armadura tachonada +You see a suit of studded armor. Some studs are loose or rusty but it is still fit for purpose.=Ves un traje de armadura tachonada. Algunos tachones están sueltos u oxidados pero todavía es adecuada para su finalidad. + +xeno armor=armadura de xeno +You see a suit of xeno armor. The colors remind you of the xenocium hybrid creature.=Ves un traje de armadura de xeno. Sus colores te recuerdan a la criatura híbrida xenocium. + + +## arrows + wooden arrow=flecha de madera You see a wooden arrow. With a supply of arrows in one hand and a bow in the other you can attack from a distance.=Ves una flecha de madera. Con un surtido de flechas en una mano y un arco en la otra puedes atacar a distancia. + steel arrow=flecha de acero You see a shining steel arrow. With plenty of these, and a good bow, you'll be a formidable archer.=Ves una flecha de reluciente acero. Con muchas de éstas, y un buen arco, serás un arquero formidable. + golden arrow=flecha de oro -You see a golden arrow. With a fist full of these and a good bow, you will surely take down your enemies quicker.=Ves una flecha de oro. Con un puñado de éstas y un buen arco, derribarás a tus enemigos más rápido. +You see a golden arrow. With a fist full of these and a good bow, you will surely take down your enemies quicker.=Ves una flecha de oro. Con un puñado de éstas y un buen arco, derribarás a tus enemigos más rápido. + +fire arrow=flecha de fuego +You see a fire arrow. The tip burns brightly.=Ves una flecha de fuego. La punta arde brillantemente. + +ice arrow=flecha de hielo +You see an ice arrow. It can deliver a bitter sting.=Ves una flecha de hielo. Puede infligir una picadura penosa. + +light arrow=flecha de luz +You see a light arrow. It shines with a holy radiance.=Ves una flecha de luz. Brilla con un resplandor santo. + +dark arrow=flecha de oscuridad +You see a dark arrow. It's hard to describe.=Ves una flecha de oscuridad. Es difícil de describir. + power arrow=flecha de potencia -You see a power arrow. The sting of this arrow will surely defeat your enemies, when equipped with a bow.=Ves una flecha de potencia. La punzada de esta flecha seguramente derrotará a tus enemigos, cuando vaya equipada con un arco. -sickle=hoz -You see a sickle, it is a useful tool for farming.=Ves una hoz, es una herramienta útil para la labranza. -old scythe=guadaña vieja -You see an old scythe, it used to be weapon of death. But now it is only a useful tool for farming.=Ves una vieja guadaña, solía ser el arma de la muerte. Pero ahora sólo es una útil herramienta de labranza. -small axe=hacha pequeña -You see a small axe. It appears well made but you suspect it is too light to do much damage.=Ves un hacha pequeña. Parece bien elaborada pero sospechas que es demasiado ligera para hacer mucho daño. -hand axe=hacha de mano -You see a hand axe, well worn from years of use but may prove useful.=Ves un hacha de mano, muy desgastada por años de uso pero podría resultar útil. +You see a power arrow. The sting of this arrow will surely defeat your enemies, when equipped with a bow.=Ves una flecha de potencia. La punzada de esta flecha seguramente derrotará a tus enemigos, cuando vaya equipada con un arco. + + +## axes + axe=hacha You see a rusty but heavy axe. What it lacks in sharpness it makes up for in weight.=Ves un hacha oxidada pero pesada. Lo que le falta en filo lo suple con peso -battle axe=hacha de batalla -You see a battle axe. It is well crafted and sharp despite its age.=Ves un hacha de batalla. Está bien diseñada y afilada a pesar de su antigüedad. -scythe=guadaña -You see the weapon of death - a scythe. It looks dangerous but perhaps also a useful tool for farming.=Ves el arma de la muerte - una guadaña. Parece peligrosa pero quizá también sea una herramienta útil para la labranza. -twoside axe=hacha de doble hoja -You see a well crafted twoside axe.=Ves un hacha de doble hoja bien elaborada. + bardiche=bardiche You see a rare and powerful bardiche.=Ves una rara y poderosa bardiche. -golden twoside axe=hacha de doble hoja de oro -You see a golden twoside axe. This axe is a rare find.=Ves un hacha de doble hoja de oro. Este hacha es un raro hallazgo. -halberd=alabarda -You see a rare halberd, with an axe's blade fused onto a shaft. It is both heavy and keen-edged.=Ves una rara alabarda, con una hoja de hacha enastada en un astil. Es a la vez pesada y de borde afilado. -magic twoside axe=hacha de doble hoja mágica -You see a magic twoside axe. The nature of the magic isn't clear to you.=Ves un hacha de doble hoja mágica. La naturaleza de la magia no está clara para ti. -durin axe=hacha de durin -You see a durin axe, a great prize from the dwarves.=Ves un hacha de durin, un gran premio de los enanos. -black scythe=guadaña negra -You see a black scythe. Its attack is awesome for a farming tool.=Ves una guadaña negra. Su ataque es asombroso para tratarse de una herramienta de labranza. -chaos axe=hacha del caos -You see a chaos axe. The handle is some alloy you can't recognize and the blade is golden colored, but thinking too long about it makes you confused.=Ves un hacha del caos. El astil es de una aleación que no sabes reconocer y la hoja es de color dorado, pero pensar mucho en ello te causa confusión. + +battle axe=hacha de batalla +You see a battle axe. It is well crafted and sharp despite its age.=Ves un hacha de batalla. Está bien diseñada y afilada a pesar de su antigüedad. + black halberd=alabarda negra You see a black halberd. It's immensely heavy so will make huge blows but you can only swing it at a very slow rate.=Ves una alabarda negra. Es inmensamente pesada asi que producirá enormes golpes pero sólo la puedes mover a un ritmo muy lento. + +black scythe=guadaña negra +You see a black scythe. Its attack is awesome for a farming tool.=Ves una guadaña negra. Su ataque es asombroso para tratarse de una herramienta de labranza. + +chaos axe=hacha del caos +You see a chaos axe. The handle is some alloy you can't recognize and the blade is golden colored, but thinking too long about it makes you confused.=Ves un hacha del caos. El astil es de una aleación que no sabes reconocer y la hoja es de color dorado, pero pensar mucho en ello te causa confusión. + +durin axe=hacha de durin +You see a durin axe, a great prize from the dwarves.=Ves un hacha de durin, un gran premio de los enanos. + +golden twoside axe=hacha de doble hoja de oro +You see a golden twoside axe. This axe is a rare find.=Ves un hacha de doble hoja de oro. Este hacha es un raro hallazgo. + +halberd=alabarda +You see a rare halberd, with an axe's blade fused onto a shaft. It is both heavy and keen-edged.=Ves una rara alabarda, con una hoja de hacha enastada en un astil. Es a la vez pesada y de borde afilado. + +hand axe=hacha de mano +You see a hand axe, well worn from years of use but may prove useful.=Ves un hacha de mano, muy desgastada por años de uso pero podría resultar útil. + +magic twoside axe=hacha de doble hoja mágica +You see a magic twoside axe. The nature of the magic isn't clear to you.=Ves un hacha de doble hoja mágica. La naturaleza de la magia no está clara para ti. + +old scythe=guadaña vieja +You see an old scythe, it used to be a weapon of death. But now it is only a useful tool for farming.=Ves una vieja guadaña, solía ser el arma de la muerte. Pero ahora sólo es una útil herramienta de labranza. + +scythe=guadaña +You see the weapon of death - a scythe. It looks dangerous but perhaps also a useful tool for farming.=Ves el arma de la muerte - una guadaña. Parece peligrosa pero quizá también sea una herramienta útil para la labranza. + +sickle=hoz +You see a sickle, it is a useful tool for farming.=Ves una hoz, es una herramienta útil para la labranza. + +small axe=hacha pequeña +You see a small axe. It appears well made but you suspect it is too light to do much damage.=Ves un hacha pequeña. Parece bien elaborada pero sospechas que es demasiado ligera para hacer mucho daño. + +twoside axe=hacha de doble hoja +You see a well crafted twoside axe.=Ves un hacha de doble hoja bien elaborada. + + +## books + black book=libro negro You see the black book that Jynath kept out for so long.=Ves el libro negro que Jynath mantuvo oculto durante tanto tiempo. + blue book=libro azul You see a book with the words GEMS AND MINERALS on the front cover.=Ves un libro azul con las palabras GEMAS Y MINERALES en la cubierta frontal. -leather boots=botas de cuero -You see a simple pair of leather boots to give your feet some protection.=Ves un simple par de botas de cuero para dar algo de protección a tus pies. -studded boots=botas tachonadas -You see a pair of worn studded boots with moderate protection for your feet.=Ves un par de botas tachonadas desgastadas que proporcionan una protección moderada a tus pies. -chain boots=botas de cota de malla -You see a pair of chain boots, quite rusty but still strong.=Ves un par de botas de cota de malla, bastante oxidadas pero todavía resistentes. -red boots=botas rojas. -You see a pair of red boots, the color reminds you of fresh blood.=Ves un par de botas rojas, su color te recuerda a la sangre fresca. -steel boots=botas de acero -You see a pair of steel boots, fit for any soldier.=Ves un par de botas de acero, adecuadas para cualquier soldado. -blue boots=botas azules -You see a pair of blue boots.=Ves un par de botas azules. -elvish boots=botas élficas -You see a pair of elvish boots.=Ves un par de botas élficas. -golden boots=botas de oro -You see a pair of golden boots. They will be heavy to wear but well worth their weight.=Ves un par de botas de oro. Serán pesadas de llevar pero valen su peso. -shadow boots=botas de sombra -You see a pair of shadow boots. You can't fathom what they are made of.=Ves un par de botas de sombra. No puedes entender de qué están hechas. -stone boots=botas de piedra -You see a pair of stone boots. They may be heavy, but they protect your feet well.=Ves un par de botas de piedra. Puede que sean pesadas, pero te protegen bien los pies. -chaos boots=botas del caos -You see a pair of chaos boots. Your steps are unsteady in them, but the extra defense they give is worth it.=Ves un par de botas del caos. Tus pasos son inestables con ellas, pero la defensa extra que proporcionan lo vale. -mainio boots=botas de mainio -You see a pair of mainio boots. It's hard to believe that boots looking like this would protect your feet so well as they do.=Ves un par de botas de mainio. Cuesta creer que unas botas con esa apariencia te pudiesen proteger los pies tan bien como lo hacen. -green thing boots=botas de la cosa verde -You see a pair of green thing boots. They're slimy inside, ewwww.=Ves un par de botas de la cosa verde. Estan pegajosas por dentro, puajjj. -xeno boots=botas de xeno -You see a fine specimen of craftmanship, a pair of xeno boots.=Ves una excelente obra de artesanía, un par de botas de xeno. + +bestiary=bestiario +You see a bestiary, containing information about enemies found in Faiumoni.=Ves un bestiario que contiene información sobre los enemigos encontrados en Faiumoni. + + +## boots + black boots=botas negras You stub your toe on this pair of black boots. Perhaps they bring you bad luck?=te diste en el dedo del pie con algo usando estas botas ¿Quizá te dan mala suerte? -mithril boots=botas de mithril -You see a sparkling set of mithril boots.=Ves un brillante conjunto de botas de mithril. + +blue boots=botas azules +You see a pair of blue boots.=Ves un par de botas azules. + +chain boots=botas de cota de malla +You see a pair of chain boots, quite rusty but still strong.=Ves un par de botas de cota de malla, bastante oxidadas pero todavía resistentes. + +chaos boots=botas del caos +You see a pair of chaos boots. Your steps are unsteady in them, but the extra defense they give is worth it.=Ves un par de botas del caos. Tus pasos son inestables con ellas, pero la defensa extra que proporcionan lo vale. + +elvish boots=botas élficas +You see a pair of elvish boots.=Ves un par de botas élficas. + +golden boots=botas de oro +You see a pair of golden boots. They will be heavy to wear but well worth their weight.=Ves un par de botas de oro. Serán pesadas de llevar pero valen su peso. + +green thing boots=botas de la cosa verde +You see a pair of green thing boots. They're slimy inside, ewwww.=Ves un par de botas de la cosa verde. Estan pegajosas por dentro, puajjj. + killer boots=botas asesinas You see a high heeled pair of killer boots. Are you brave enough to wear them?=Ves un par de botas asesinas de tacón alto ¿Eres lo bastante valiente como para llevarlas? + +leather boots=botas de cuero +You see a simple pair of leather boots to give your feet some protection.=Ves un simple par de botas de cuero para dar algo de protección a tus pies. + +magic plate boots= + +mainio boots=botas de mainio +You see a pair of mainio boots. It's hard to believe that boots looking like this would protect your feet so well as they do.=Ves un par de botas de mainio. Cuesta creer que unas botas con esa apariencia te pudiesen proteger los pies tan bien como lo hacen. + +mithril boots=botas de mithril +You see a sparkling set of mithril boots.=Ves un brillante conjunto de botas de mithril. + +red boots=botas rojas +You see a pair of red boots, the color reminds you of fresh blood.=Ves un par de botas rojas, su color te recuerda a la sangre fresca. + +royal boots= +You see a pair of royal boots. They have magical properties= + +shadow boots=botas de sombra +You see a pair of shadow boots. You can't fathom what they are made of.=Ves un par de botas de sombra. No puedes entender de qué están hechas. + +steel boots=botas de acero +You see a pair of steel boots, fit for any soldier.=Ves un par de botas de acero, adecuadas para cualquier soldado. + +stone boots=botas de piedra +You see a pair of stone boots. They may be heavy, but they protect your feet well.=Ves un par de botas de piedra. Puede que sean pesadas, pero te protegen bien los pies. + +studded boots=botas tachonadas +You see a pair of worn studded boots with moderate protection for your feet.=Ves un par de botas tachonadas desgastadas que proporcionan una protección moderada a tus pies. + +xeno boots=botas de xeno +You see a fine specimen of craftmanship, a pair of xeno boots.=Ves una excelente obra de artesanía, un par de botas de xeno. + + +## boxes + present=regalo You see a present. Open it.=Ves un regalo. Abrelo. + basket=cesta You see an easter basket. Look inside.=Ves una cesta de Pascua. Mira dentro de ella. + stocking=calcetín You see a christmas stocking. Look inside.=Ves un calcetín de Navidad. Mira dentro de él. -cloak=capa -You see a simple cloth cloak.=Ves una simple capa de tela. -elf cloak=capa de elfo -You see a white elf cloak. It doesn't look very warm.=Ves una capa de elfo blanca. No parece muy cálida. -dwarf cloak=capa enana -You see a dwarf cloak. Apparently this passes for fashion in some parts, but to you it looks more functional than fashionable.=Ves una capa enana. Aparentemente esto es lo que se entiende por moda en algunos lugares, pero a ti te resulta más funcional que de moda. -red cloak=capa roja -You see a red cloak covered in polka dots. It's terribly snazzy.=Ves una capa roja cubierta de lunares. Es terriblemente elegante. -elvish cloak=capa élfica -You see an elvish cloak. The material is dark, but thick and warm.=Ves una capa élfica. Su material es oscuro, pero es grueso y cálido. -blue striped cloak=capa de rayas azules -You see a fashionable blue striped cloak.=Ves una capa de rayas azules de moda. -blue elf cloak=capa de elfo azul -You see a blue elf cloak. The material is thick and warm.=Ves una capa de elfo azul. El material es grueso y cálido. -stone cloak=capa de piedra -You see a stone cloak. It's hard to tell how it was made, as the fibres look like narrow threads of mineral.=Ves una capa de piedra. Es difícil decir como se elaboró, ya que las fibras parecen delgados hilos de mineral. -green dragon cloak=capa de dragón verde. -You see a huge green dragon cloak. It's probably too big for you but, even dragging on the floor it will provide excellent protection.=Ves una enorme capa de dragón verde. Es probablemente demasiado grande para ti pero, aunque arrastre por el suelo te proporcionará una protección excelente. -bone dragon cloak=capa de dragón de hueso -You see a bone dragon cloak. It's incredibly old, like its last owner.=Ves una capa de dragón de hueso. Es increíblemente antigua, como su último dueño. -lich cloak=capa de lich -You see a purple lich cloak. It's stained with blood - yours, from the previous undead owner, or perhaps the blood of his enemies...=Ves una capa púrpura de lich. Está manchada de sangre - la tuya, la de su anterior dueño, o quizá la sangre de sus enemigos... -vampire cloak=capa de vampiro -You see a vibrant red vampire cloak. Wearing it sends a shiver down your spine.=Ves una capa de vampiro de vibrante color rojo. Llevarla puesta te produce un escalofrío por tu columna vertebral. -shadow cloak=capa de sombra -You see a shadow cloak, in colors of the shadow armor.=Ves una capa de sombra, en los mismos colores que la armadura de sombra. -blue dragon cloak=capa de dragón azul -You see a blue dragon cloak. The weave is tight and strong, though you wish the color drew less attention.=Ves una capa de dragón azul. La trama es apretada y resistente, aunque desearías que su color llamase menos la atención. -red dragon cloak=capa de dragón rojo -You see a red dragon cloak. He didn't have one for a long time but finally caved in to peer pressure and got one.=Ves una capa de dragón rojo. Él no había tenido ninguna durante mucho tiempo pero finalmente cedió a la presión de sus compañeros y se buscó una. -black dragon cloak=capa de dragón negro -You see a torn section from an enormous black dragon cloak. You're very lucky: the piece is just the right size for a human to wear.=Ves un retal arrancado de una enorme capa de dragón negro. Tienes mucha suerte: la pieza es justo del tamaño adecuado para que lo lleve un humano. -golden cloak=capa de oro -You see a golden cloak. See how the light glistens off the gold, it is a prized possession.=Ves una capa de oro. Ves cómo la luz brilla saliendo del oro, es una preciada posesión. -chaos cloak=capa del caos -You see a chaos cloak. The colors look like oil shimmering on a puddle.=Ves una capa del caos. Sus colores parecen aceite brillando en un charco. -mainio cloak=capa de mainio -You see a mainio cloak. It's flashing colors are absurd! But you wear it because it's very well made.=Ves una capa de mainio ¡Sus colores parpadeantes son absurdos! Pero la llevas puesta porque está muy bien elaborada. -xeno cloak=capa de xeno -You see a xeno cloak. It's heavy and thick, giving you a good protection.=Ves una capa de xeno. Es pesada y gruesa, proporcionandote una buena protección. + + +## cloaks + black cloak=capa negra You see a black cloak. The last owner had black thoughts. Will you?=Ves una capa negra. Su último dueño tuvo negros pensamientos ¿Los tendrás tú? + +black dragon cloak=capa de dragón negro +You see a torn section from an enormous black dragon cloak. You're very lucky: the piece is just the right size for a human to wear.=Ves un retal arrancado de una enorme capa de dragón negro. Tienes mucha suerte: la pieza es justo del tamaño adecuado para que lo lleve un humano. + +blue dragon cloak=capa de dragón azul +You see a blue dragon cloak. The weave is tight and strong, though you wish the color drew less attention.=Ves una capa de dragón azul. La trama es apretada y resistente, aunque desearías que su color llamase menos la atención. + +blue elf cloak=capa de elfo azul +You see a blue elf cloak. The material is thick and warm.=Ves una capa de elfo azul. El material es grueso y cálido. + +blue striped cloak=capa de rayas azules +You see a fashionable blue striped cloak.=Ves una capa de rayas azules de moda. + +bone dragon cloak=capa de dragón de hueso +You see a bone dragon cloak. It's incredibly old, like its last owner.=Ves una capa de dragón de hueso. Es increíblemente antigua, como su último dueño. + +chaos cloak=capa del caos +You see a chaos cloak. The colors look like oil shimmering on a puddle.=Ves una capa del caos. Sus colores parecen aceite brillando en un charco. + +cloak=capa +You see a simple cloth cloak.=Ves una simple capa de tela. + +dwarf cloak=capa enana +You see a dwarf cloak. Apparently this passes for fashion in some parts, but to you it looks more functional than fashionable.=Ves una capa enana. Aparentemente esto es lo que se entiende por moda en algunos lugares, pero a ti te resulta más funcional que de moda. + +elf cloak=capa de elfo +You see a white elf cloak. It doesn't look very warm.=Ves una capa de elfo blanca. No parece muy cálida. + +elvish cloak=capa élfica +You see an elvish cloak. The material is dark, but thick and warm.=Ves una capa élfica. Su material es oscuro, pero es grueso y cálido. + +golden cloak=capa de oro +You see a golden cloak. See how the light glistens off the gold, it is a prized possession.=Ves una capa de oro. Ves cómo la luz brilla saliendo del oro, es una preciada posesión. + +green dragon cloak=capa de dragón verde +You see a huge green dragon cloak. It's probably too big for you but, even dragging on the floor it will provide excellent protection.=Ves una enorme capa de dragón verde. Es probablemente demasiado grande para ti pero, aunque arrastre por el suelo te proporcionará una protección excelente. + +lich cloak=capa de lich +You see a purple lich cloak. It's stained with blood - yours, from the previous undead owner, or perhaps the blood of his enemies...=Ves una capa púrpura de lich. Está manchada de sangre - la tuya, la de su anterior dueño, o quizá la sangre de sus enemigos... + +magic cloak= +You see a magic cloak.= + +mainio cloak=capa de mainio +You see a mainio cloak. Its flashing colors are absurd! But you wear it because it's very well made.=Ves una capa de mainio ¡Sus colores parpadeantes son absurdos! Pero la llevas puesta porque está muy bien elaborada. + mithril cloak=capa de mithril You see a mithril cloak. Its weave is so fine and its looks are breathtaking.=Ves una capa de mithril. Su trama es tan fina y su apariencia quita la respiración. -rift cloak=capa de rift -You see a rift cloak. Almost useless in the battle, but saves your life when you enter a magical rift.=Ves una capa de rift. Casi inútil en la batalla, pero te salva la vida cuando entras en un rift mágico. -club=garrote -You see a wooden club. It won't kill much but is better than no weapon.=Ves un garrote de madera. No matará mucho pero es mejor que no tener ningún arma. -staff=bastón -You see a staff. This is a seldom seen or used weapon.=Ves un bastón. Ésta es un arma que rara vez se ve o se usa. -mace=maza -You see a mace, much neglected in years of use.=Ves una maza, muy descuidada en los años de uso. -flail=mangual -You see a military flail. Swinging it in circles will tire you but it will be an effective weapon.=Ves un mangual militar. Moverlo en círculos te cansará pero será un arma efectiva. -enhanced mace=maza mejorada -You see an enhanced mace. The heavy attacking end makes it hard to balance.=Ves una maza mejorada. Su pesado extremo de ataque la hace difícil de equilibrar. -golden mace=maza de oro -You see a shining golden mace. Despite looking ornamental, it is sharp enough to do some damage.=Ves una maza de oro brillante. A pesar de parece ornamental, está lo bastante afilada para hacer algún daño. -skull staff=bastón de calavera -You see a rare skull staff. It appears mystical but will work like any other staff.=Ves un bastón de calavera. Parece místico pero funcionará como cualquier otro bastón. -morning star=lucero del alba -You see a morning star. This is a very rare item indeed.=Ves un lucero del alba. Éste es realmente un artículo raro. -hammer=martillo -You see a common hammer. It is heavy enough that you should be able to bludgeon your enemies with it, but only slowly=Ves un martillo común. Es bastante pesado por lo que deberías de ser capaz de aporrear a tus enemigos con él, pero sólo con lentitud. -golden hammer=martillo de oro -You see a valuable golden hammer. This golden weapon may be worth a lot to someone. It has a slow rate of attack.=Ves un valioso martillo de oro. Este arma de oro puede valer muchísimo para alguien. Tiene un ritmo de ataque lento. -war hammer=martillo de guerra -You see a huge war hammer. It seems strong but is so heavy that you can only lift it to attack every so often..=Ves un enorme martillo de guerra. Parece potente pero es tan pesado que sólo lo puedes levantar de vez en cuando para atacar.. -ice war hammer=martillo de guerra de hielo -You see a ice war hammer. The last owner must have been huge, to wield it.=Ves un martillo de guerra de hielo. Su último dueño debe de haber sido enorme, para manejarlo. + +red cloak=capa roja +You see a red cloak covered in polka dots. It's terribly snazzy.=Ves una capa roja cubierta de lunares. Es terriblemente elegante. + +red dragon cloak=capa de dragón rojo +You see a red dragon cloak. He didn't have one for a long time but finally caved in to peer pressure and got one.=Ves una capa de dragón rojo. Él no había tenido ninguna durante mucho tiempo pero finalmente cedió a la presión de sus compañeros y se buscó una. + +royal cloak=capa real +You see a royal cloak. It will protect you against mighty foes.=Ves una capa real. Te protegará contra enemigos poderosos. + +shadow cloak=capa de sombra +You see a shadow cloak, in colors of the shadow armor.=Ves una capa de sombra, en los mismos colores que la armadura de sombra. + +stone cloak=capa de piedra +You see a stone cloak. It's hard to tell how it was made, as the fibres look like narrow threads of mineral.=Ves una capa de piedra. Es difícil decir como se elaboró, ya que las fibras parecen delgados hilos de mineral. + +vampire cloak=capa de vampiro +You see a vibrant red vampire cloak. Wearing it sends a shiver down your spine.=Ves una capa de vampiro de vibrante color rojo. Llevarla puesta te produce un escalofrío por tu columna vertebral. + +xeno cloak=capa de xeno +You see a xeno cloak. It's heavy and thick, giving you good protection.=Ves una capa de xeno. Es pesada y gruesa, proporcionandote una buena protección. + + +## clubs + chaos hammer=martillo del caos You see a chaos hammer. It is a fearsome weapon.=Ves un martillo del caos. Es un arma temible. -ugmash=ugmash -You see a huge ugmash. It can attack with a faster rate than many other clubs.=Ves un enorme ugmash. Puede atacar a un ritmo más rápido que muchos otros garrotes. -vulcano hammer=martillo de vulcano -You see a vulcano hammer. It's said to be a weapon of a God, but attacks with a snail like pace.=Ves un martillo de vulcano. Se dice que es el arma de un dios, pero ataca al ritmo de un caracol. + +club=garrote +You see a wooden club. It won't kill much but is better than no weapon.=Ves un garrote de madera. No matará mucho pero es mejor que no tener ningún arma. + club of thorns=garrote de espinas You see the club of thorns. The thorns are full with blood of tortured humans. It is hugely powerful but will drain your life to use it.=Ves el garrote de espinas. Las espinas estan llenas de sangre de humanos torturados. Es enormemente poderosa pero el usarla agotará tu vida. -necromancers staff=baston de nigromante -You see a cunningly beautiful staff. If you listen carefully you can hear its previous owners' souls screaming from within it.=Ves un bastón engañosamente hermoso. Si escuchas atentamente puedes oir las almas de sus anteriores dueños gritar desde dentro. + +enhanced mace=maza mejorada +You see an enhanced mace. The heavy attacking end makes it hard to balance.=Ves una maza mejorada. Su pesado extremo de ataque la hace difícil de equilibrar. + +flail=mangual +You see a flail. Swinging it in circles will tire you but it will be an effective weapon.=Ves un mangual. Moverlo en círculos te cansará pero será un arma efectiva. + +golden hammer=martillo de oro +You see a valuable golden hammer. This golden weapon may be worth a lot to someone. It has a slow rate of attack.=Ves un valioso martillo de oro. Este arma de oro puede valer muchísimo para alguien. Tiene un ritmo de ataque lento. + +golden mace=maza de oro +You see a shining golden mace. Despite looking ornamental, it is sharp enough to do some damage.=Ves una maza de oro brillante. A pesar de parece ornamental, está lo bastante afilada para hacer algún daño. + +hammer=martillo +You see a common hammer. It is heavy enough that you should be able to bludgeon your enemies with it, but only slowly=Ves un martillo común. Es bastante pesado por lo que deberías de ser capaz de aporrear a tus enemigos con él, pero sólo con lentitud. + +ice war hammer=martillo de guerra de hielo +You see an ice war hammer. The last owner must have been huge to wield it.=Ves un martillo de guerra de hielo. Su último dueño debe de haber sido enorme para manejarlo. + +mace=maza +You see a mace, much neglected in years of use.=Ves una maza, muy descuidada en los años de uso. + +morning star=lucero del alba +You see a morning star. This is a very rare item indeed.=Ves un lucero del alba. Éste es realmente un artículo raro. + rod of the gm=vara del DJ -You see a rod of the GM. This rod is reserved for those with special powers, be careful not to let it fall into the wrong hands.=Ves una vara del DJ. Esta vara se reserva a aquellos con poderes especiales, ten cuidado de no dejar que caiga en las manos equivocadas. +You see a rod of the GM. This rod is reserved for those with special powers, be careful not to let it fall into the wrong hands.=Ves una vara del DJ. Esta vara se reserva a aquellos con poderes especiales, ten cuidado de no dejar que caiga en las manos equivocadas. + +skull staff=bastón de calavera +You see a rare skull staff. It appears mystical but will work like any other staff.=Ves un bastón de calavera. Parece místico pero funcionará como cualquier otro bastón. + +staff=bastón +You see a staff. This is a seldom seen or used weapon.=Ves un bastón. Ésta es un arma que rara vez se ve o se usa. + +ugmash=ugmash +You see a huge ugmash. It can attack with a faster rate than many other clubs.=Ves un enorme ugmash. Puede atacar a un ritmo más rápido que muchos otros garrotes. + +vulcano hammer=martillo de vulcano +You see a vulcano hammer. It's said to be a weapon of a God, but attacks with a snail like pace.=Ves un martillo de vulcano. Se dice que es el arma de un dios, pero ataca al ritmo de un caracol. + +war hammer=martillo de guerra +You see a huge war hammer. It seems strong but is so heavy that you can only lift it to attack every so often.=Ves un enorme martillo de guerra. Parece potente pero es tan pesado que sólo lo puedes levantar de vez en cuando para atacar. + + +## containers + +keyring=llavero +You see a keyring.=Ves un llavero. + empty sack=saco vacío You see an empty sack. It will not stand up, unless you fill it first.=Ves un saco vacío. No se mantendrá en pie, a menos que lo llenes primero. + flask=frasco -You see a flask. This flask is good for storing medicine.=Ves un frasco. Este frasco es bueno para almacenar medicina. +You see a flask. It's good for storing medicine.=Ves un frasco. Es bueno para almacenar medicina. + +eared bottle=botella con asas +You see an eared bottle. It is empty, someone drank the contents.=Ves una botella con asas. Está vacía, alguien se bebió su contenido. + +slim bottle=botella estrecha +You see a slim bottle. It is empty, someone drank the contents.=Ves una botella estrecha. Está vacía, alguien se bebió su contenido. + +fat bottle=botella gruesa +You see a fat bottle. It is empty, someone drank the contents.=Ves una botella gruesa. Está vacía, alguien se bebió su contenido. + +empty goblet=copa vacía +You see an empty goblet. You wonder what could it hold.=Ves una copa vacía. Te preguntas que podría contener. + +goblet=copa +You see a goblet. You look at its contents, and shiver at the blood inside.=Ves una copa. Miras su contenido, y sientes escalofríos al ver sangre dentro. + +vial=ampolleta +You see a vial used in chemestry.=Ves una ampolleta utilizada en química. + + +## crystals + +red emotion crystal=cristal rojo de emoción +You see a red emotion crystal. It feels hot to the touch.=Ves un cristal rojo de emoción. Se siente caliente al tacto. + +purple emotion crystal=cristal morado de emoción +You see a purple emotion crystal. It feels cold to the touch.=Ves un cristal morado de emoción. Se siente frío al tacto. + +yellow emotion crystal=cristal amarillo de emoción +You see a yellow emotion crystal. It energizes you.=Ves un cristal amarillo de emoción. Te da energía. + +pink emotion crystal=cristal rosa de emoción +You see a pink emotion crystal. It almost feels soft.=Ves un cristal rosa de emoción. Casi se siente suave. + +blue emotion crystal=cristal azul de emoción +You see a blue emotion crystal. While holding it your worries seem to disappear.=Ves un cristal azul de emoción. Mientras al sujetarlo tus preocupaciones parecen desaparecer. + + +## documents + +note=nota +You see a note.=Ves una nota. + +coupon=cupón +You see a coupon.=Ves un cupón. + +closed envelope=sobre cerrado + +opened envelope=sobre abierto + +sealed envelope=sobre sellado + +unsealed envelope=sobre sellado abierto + +assassins id=identificación de asesino +You see an assassin's ID. Perhaps this can be put to good use.=Ves una identificación de asesino. Quizá puedas hacer un buen uso de ella. + +map=mapa +You see a hand drawn map, but no matter how you look at it, nothing on it looks familiar.=Ves un mapa dibujado a mano. Pero por mucho que lo mires, no hay nada en él que parece familiar. + + +## drinks + +olive oil=aceite de oliva +You see a bottle of olive oil. Do not drink it pure, it will make you feel sick!=Ves una botella de aceite de oliva. No lo bebas puro, ¡te hará sentir mal! + +vinegar=vinagre +You see a bottle of vinegar. Do not drink it pure, it will make you feel sick!=Ves una botella de vinagre. No lo bebas puro, ¡te hará sentir mal! + +fierywater=aguardiente +You see a bottle of fierywater. Do not drink it pure, you would evaporate!=Ves una botella de aguardiente. No te la bebas pura, ¡Te evaporarías! + +mild koboldish torcibud=suave torcibud koboldiano +You see a bottle of mild koboldish torcibud. It will help you recover when you're all bruised and battered.=Ves una botella de suave torcibud koboldiano. Te ayudará a recuperarte cuando estés todo magullado. + +strong koboldish torcibud=fuerte torcibud koboldiano +You see a bottle of strong koboldish torcibud. It will help you recover when you're all bruised and battered.=Ves una botella de fuerte torcibud koboldiano. Te ayudará a recuperarte cuando estés todo magullado. + +vsop koboldish torcibud=torcibud koboldiano de estaca vieja muy especial +You see a bottle of VSOP koboldish torcibud, specially aged in oak casks wrapped by the darkness of a dungeon cellar. It will help you recover when you're all bruised and battered.=Ves un torcibud koboldiano de estaca vieja muy especial, especialmente envejecida en barricas de roble envueltas por la oscuridad de la bodega de una mazmorra. Te ayudará a recuperarte cuando estés todo magullado. + +apple juice=zumo de manzana +You see apple juice. On a hot summer day, it would taste great.=Ves zumo de manzana. En un día de verano caluroso, sabría estupendo. + +beer=cerveza +You see a beer. On a hot summer day, it would taste great.=Ves una cerveza. En un día de verano caluroso, sabría estupendo. + +wine=vino +You see a glass of red wine. It will quench your thirst.=Ves un vaso de vino tinto. Saciará tu sed. + +water=agua +You see a bottle of fresh spring water. It's really tasty and fresh.=Ves una botella de agua fresca de manantial. Es realmente sabrosa y fresca. + +pina colada=piña colada +You see a pina colada. It looks delicious but tastes dangerously strong!=Ves una piña colada. ¡Parece deliciosa pero sabe peligrosamente fuerte! + +tea=té +You see a lovely cup of tea.=Ves una encantadora taza de té. + +milk=leche +You see a small bottle of fresh milk.=Ves una pequeña botella de leche fresca. + +antidote=antídoto +You see an antidote, to cure you of poison and protect you for a while.=Ves un antídoto, para curarte de venenos y protegerte durante un rato. + +greater antidote=antidoto mayor +You see a greater antidote, to cure you of poison and protect you for a long while.=Ves un antidoto mayor, para curarte de venenos y protegerte durante un largo rato. + +mana=maná +You see an bottle of mana, to increase your mana rapidly.=Ves una botella de maná, para rellenar tu maná rápidamente. + +poison=veneno +You see a bottle of poison. Don't drink it!=Ves una botella de veneno ¡No te la tomes! + +greater poison=veneno mayor +You see a greater poison. Don't drink it!=Ves un veneno mayor ¡No te lo tomes! + +deadly poison=veneno mortal +You see a deadly poison. Don't drink it!=Ves un veneno mortal ¡No te lo tomes! + +disease poison=veneno de enfermar +You see a disease poison. Be careful, it's wicked.=Ves un veneno de enfermar. Ten cuidado, es malvado. + +mega poison=mega veneno +You see a mega poison. This is some wicked poison.=Ves un mega veneno. Éste es un veneno malvado. + +twilight elixir=elixir del crepúsculo +You see a bottle of medicinal elixir.=Ves una botella de elixir medicinal. + +minor potion=poción menor +You see a minor potion. It's not much but may save you in a tight spot.=Ves una poción menor. No es mucho pero puede salvarte en una situación difícil. + +soup=sopa +You see a bowl of soup. Its contents fill you up.=Ves un plato de sopa. Su contenido te llena. + +fish soup=sopa de pescado +You see a bowl of fish soup. Its contents fill you up.=Ves un plato de sopa de pescado. Su contenido te llena. + +potion=poción +You see a potion. Take it to battle and you may survive longer.=Ves una poción. Llevatela a la batalla y puedes sobrevivir por más tiempo. + +greater potion=poción mayor +You see a greater potion, invaluable in battle.=Ves una poción mayor, inestimable en la batalla. + +mega potion=mega poción +You see a mega potion. It strengthens greatly.=Ves una mega poción. Te fortalece en gran medida. + +love potion=poción de amor +You see a love potion. It looks powerful so take care to use it carefully!=Ves una poción de amor ¡Parece potente así que ten cuidado de usarla con precaución! + +sedative=sedante +You see a sedative. It's effects can protect you from a shocked state.=Ves un sedante. Sus efectos pueden protegerte de un estado de shock. + +ashen holy water=agua santa cenizada +A bottle of ashen holy water.=Ves una botella de agua santa cenizada. + +cobra venom=veneno de cobra +You see a vial filled with cobra venom. Even in this small amount, it could be deadly if ingested.=Ves una ampolleta llena de veneno de cobra. Aun en esta pequeña cantidad, podría ser letal si se ingiere. + +antivenom=antiveneno +You see a vial filled with antivenom.=Ves una ampolleta llena de antiveneno. + + +## flowers + +rhosyd=rhosyd +You see a beautiful and rare rhosyd flower.=Ves una bella y rara flor de rhosyd. + +rose=rosa +You see a single red rose. It smells heavenly.=Ves una sola rosa roja. + +lilia=lilia +You see a lilia, a single precious bloom.=Ves una lilia, una sola flor preciosa. + +daisies=margaritas +You see a bunch of daisies. They are cheap and cheerful!=Ves un ramo de margaritas ¡Son baratas y alegran! + +pansy=pensamiento +You see a pansy, so colorful and beautiful.=Ves un pensamiento, tan colorido y hermoso. + +zantedeschia=zantedeschia +You see a zantedeschia, a delicate bloom with the sweetest fragrance.=Ves una zantedeschia, una flor delicada con la más dulce de las fragancias. + + +## food + +apple=manzana +You see a nice shiny apple.=Ves una bonita y brillante manzana. + +purple apple=manzana morada +You see a purple apple. It is loaded with memories=Ves una manzana morada. Está cargada de recuerdos. + +mauve apple=manzana malva +You see a mauve apple. It is loaded with memories=Ves una manzana malva. Está cargada de recuerdos. + +black apple=manzana negra +You see a black apple. It must be a gift from lil Johnnnny. He's the guy with black items.=Ves una manzana negra. Debe de ser un regalo del pequeño Johnnny + +lemon=limón +You see a lemon. Could be an ingredient for the right chef...=Ves un limón. Podría ser un ingrediente para el chef adecuado. + +banana=banana +You see a banana. Be careful of monkeys when you have them in your bag!=Ves una banana. ¡Ten cuidado con los monos cuando las llevas en tu bolso! + +cherry=cereza +You see a cherry. Its tangy taste revives you a little.=Ves una cereza. Su sabor amargo te revive un poco. + +coconut=coco +You see a coconut. Luckily it's already cracked and you can drink from it.=Ves un coco. Afortunadamente ya está abierto y puedes beber de él. + +corn=maíz +You see an ear of corn. It could be eaten raw, grilled or turned into flour... Your choice.=Ves una oreja de maíz. Se puede comer cruda, asada o convertirla en harina... Tú decides. + +grapes= +You see a bunch of grapes. They look quite juicy and tasty.= + +olive= +You see a small ripe olive. It can be eaten or used to create olive oil.= + +pear= +You see a pear. Its lovely smell convinces you of taking it.= + +pineapple=piña +You see a tropical pineapple.=Ves una piña tropical. + +pomegranate= +You see a pomegranate. If you eat it, you will be in great shape soon.= + +watermelon= +You see a watermelon. It refreshes your throat during hot summer days.= + +artichoke=alcachofa +You see an artichoke. It can be an ingredient, or you can eat it.=Ves una alcachofa. Puede ser un ingrediente, o te la puedes comer. + +broccoli=brocoli +You see a bunch of broccoli.=Ves un manojo de brocoli. + +carrot=zanahoria +You see a carrot. It can be an ingredient, or you can eat it.=Ves una zanahoria. Puede ser un ingrediente, o te la puedes comer. + +cauliflower=coliflor +You see a fresh head of cauliflower.=Ves la cabeza de una fresca coliflor. + +zucchini=calabacín +You see a zucchini. It can be an ingredient, or you can eat it.=Ves un calabacín. Puede ser un ingrediente, o te lo puedes comer. + +collard=col +You see a collard. It's rare and strangely magical.=Ves una col. Es rara y extrañamente mágica. + +garlic=ajo +You see some fresh garlic.=Ves un poco de ajo fresco. + +leek=puerro +You see a fresh leek.=Ves un puerro fresco. + +onion=cebolla +You see an onion.=Ves una cebolla. + +lettuce=lechuga + +spinach=espinacas +You see spinach. It is enriched with vitamins.=Ves espinacas. Está enriquecida con vitaminas. + +tomato=tomate +You see a tomato, put it in a salad or eat it.=Ves un tomate, ponlo en la ensalada o cometelo. + +egg=huevo +You see an egg.=Ves un huevo. + +button mushroom=champiñón común +You see a button mushroom. It can be an ingredient, or you can eat it.=Ves un champiñón común. Puede ser un ingrediente, o te lo puedes comer. + +porcini=seta calabaza +You see a porcini. It can be an ingredient, or you can eat it.=Ves una seta calabaza. Puede ser un ingrediente, o te la puedes comer. + +toadstool=seta venenosa +You see a toadstool. Beware!=Ves una seta venenosa ¡Ten cuidado! + +char=salvelinus +You see a char, with a fleshy pink belly.=Ves un salvelinus, con un vientre carnoso de color rosa. + +cod=bacalao +You see a cod.=Ves un bacalao. + +smoked cod=bacalao ahumado +You see a smoked cod.=Ves un bacalao ahumado. + +mackerel=caballa +You see a mackerel.=Ves una caballa. + +perch=perca +You see a perch.=Ves una perca. + +roach=rutilo +You see a roach.=Ves un rutilo + +clownfish=pez payaso +You see a striped clownfish.=Ves un pez payaso a rayas. + +red lionfish=pterois rojo +You see a venomous coral reef red lionfish.=Ves un pterois rojo venenoso de los arrecifes de coral + +surgeonfish=pez cirujano +You see a bright blue surgeonfish.=Ves un pez cirujano de color azul brillante. + +trout=trucha +You see a trout.=Ves una trucha. + +smoked trout=trucha ahumado +You see a smoked trout.=Ves una trucha ahumada. + +bread=pan +You see a loaf of bread. It is freshly baked and ready to eat.=Ves una barra de pan. Está recién horneada y lista para comer. + +pie=pastel +You see a meat and vegetable pie.=Ves un pastel de carne y verduras. + +apple pie=tarta de manzana +You see a delicious apple pie, made with only genuine ingredients.=Ves una deliciosa tarta de manzana, elaborada sólo con los ingredientes genuinos. + +cherry pie=tarta de cereza +You see a delicious cherry pie, made with only genuine ingredients.=Ves una tarta de cereza deliciosa, elaborada sólo con los ingredientes genuinos. + +fish pie=empanada de pescado +You see a fish and leek pie. It's not too stodgy so you can eat it faster than a meat pie.=Ves una empanada de pescado y puerros. No es demasiado pesada así que te la puedes comer más rápido que un pastel de carne. + +crepes suzette=crepe suzette +You see a crepes suzette topped with hot chocolate sauce.=Ves una crepe suzette cubierta con salsa de chocolate caliente. + +sandwich=sandwich +You see a ham and cheese sandwich.=Ves un sandwich de jamón y queso. + +smoked ham=jamón ahumado +You see a smoked ham.=Ves un jamón ahumado. + +sugar=azúcar +You see a sack of raw sugar.=Ves un saco de azúcar no refinado. + +butter=mantequilla +You see some butter. It is an ingredient or can be eaten if you like.=Ves un poco de mantequilla. Es un ingrediente o se puede comer si quieres. + +honey=miel +You see a jar of sticky sweet honey. It's the clear runny kind.=Ves una jarra de miel dulce y pegajosa. Es de tipo fluído y claro. + +icecream=helado +You see a wafer cone with vanilla ice cream and chocolate flakes.=Ves un cucurucho de helado de vainilla y virutas de chocolate. + +licorice=regaliz +You see some delicious licorice. Your mouth waters.=Ves un poco de delicioso regaliz. Se te hace la boca agua. + +hotdog=perrito caliente +You see what looks like a tasty hotdog on a bun.=Ves lo que parece ser un sabroso perrito caliente en un bollo. + +cheeseydog=perrito caliente con queso +You see what looks like a tasty cheeseydog on a bun.=Ves lo que parece ser un sabroso perrito caliente con queso en un bollo. + +cheese sausage=salchicha de queso +You see what looks like a tasty cheese sausage.=Ves lo que parece ser una sabrosa salchicha de queso. + +sausage=salchicha +You see what looks like a tasty sausage.=Ves lo que parece ser una sabrosa salchicha. + +tuna sandwich=sandwich de atún +You see what looks like a tuna sandwich.=Ves lo que parece ser un sandwich de atún. + +chocolate shake=batido de chocolate +You see a scrumptious chocolate shake. Absolutely NO calories.=Ves un batido de chocolate para chuparse los dedos. Absolutamente SIN calorías. + +vanilla shake=batido de vainilla +You see a yummy vanilla shake. Absolutely NO calories.=Ves un sabroso batido de vainilla. Absolutamente SIN calorías. + +potato=papa +You see a potato.=Ves una papa. + +habanero pepper= +You see a habanero pepper. Could be a very HOT ingredient for a decent meal...= + +pinto beans= +You see a bunch of pinto beans. An ingredient a chef will surely appreciate...= + +cheese=queso +You see some cheese. Eat it and see what happens.=Ves un poco de queso. Cometelo y verás lo que pasa. + +chicken=pollo +You see a tasty leg of chicken.=Ves un sabroso muslo de pollo. + +chocolate bar=barrita de chocolate +You see an absolutely yummy chocolate bar.=Ves una barrita de chocolate absolutamente deliciosa. + +candycane= +You see a delicious candy cane.= + +greenmint= +You see a green mint.= + +gumdrops= +You see a mix of gum drops.= + +lemondrop= +You see a sweet and tangy lemon drop.= + +lollipop= +You see a lollipop.= + +peppermint= +You see a sweet, little peppermint.= + +taffy= +You see some ooey gooey taffy.= + +fairy cake=fairy cake +You see a luscious fairy cake with icing and sprinkles on top.=Ves un delicioso fairy cake con cobertura y fideos de chocolate por encima. + +ham=jamón +You see a juicy ham.=Ves un jugoso jamón. + +meat=carne +You see a piece of meat.=Ves un pedazo de carne. + +smoked meat=carne ahumado +You see a piece of smoked meat.=Ves un trozo de carne ahumada. + +easter egg=huevo de pascua +You see a lovely chocolate easter egg. It's huge!=Ves un encantador huevo de pascua ¡Es enorme! + +small easter egg=huevo de pascua pequeño +You see a small chocolate easter egg. Yummy!=Ves un pequeño huevo de pascua de chocolate ¡Delicioso! + +grilled steak=filete a la parrilla +You see a fresh grilled steak. It smells awesome and is really juicy.=Ves un filete a la parrilla recién hecho. Huele fenomenal y está realmente jugoso. + +pizza=pizza +You see a tasty pizza, and hope they put extra cheese on it.=Ves una sabrosa pizza, y esperas que le hayan puesto extra de queso por encima. + + +## furniture + +picture in wooden frame= +You see a picture in a wooden frame= + + +## helmets + +leather helmet=casco de cuero +You see a leather helmet, it seems more like a hat than a defensive item.=Ves un casco de cuero, parece más un sombrero que un elemento defensivo. + +trophy helmet=casco trofeo +You see a Trophy Helmet. Those who were brave and finished the death match, win this helmet.= + +robins hat=sombrero de robin +You see a robins hat. It's cute but that's about it.=Ves un sombrero de robin. Es gracioso pero eso es todo. + +studded helmet=casco tachonado +You see a studded helmet, it should protect your head against cuts but perhaps not hard blows.=Ves un casco tachonado, debería de protegerte la cabeza de cortes pero quizá no de golpes fuertes. + +elvish hat= +You see an elvish hat. The elf craftmanship has made the leafy material stronger than one would think.= + +chain helmet=casco de cota de malla +You see a solid chain helmet, it will be excellent protection for your head.=Ves un sólido casco de cota de malla, será una excelente protección para la cabeza. + +viking helmet=casco de vikingo +You see a viking helmet. The previous owner wore it to many battles.=Ves un casco de vikingo. Su anterior dueño lo llevó consigo a muchas batallas. + +aventail=almófar +You see an aventail. It's a clever design.=Ves un almófar. Es un diseño ingenioso. + +blue helmet=casco azul +You see a brand new blue helmet.=Ves un casco azul nuevecito. + +red helmet=casco rojo +You see a red helmet, it reminds you of a jester's hat but you can be sure it has better protection than that.=Ves un casco rojo, te recuerda al sombrero de un bufón pero puedes estar seguro de que te proporcionará una protección mejor que esa. + +golden helmet=casco de oro +You see a heavy golden helmet. The thick metal should protect you against almost anything.=Ves un pesado casco de oro. El grueso metal debería de protegerte contra casi todo. + +horned golden helmet=casco de oro con cuernos +You see a horned golden helmet.=Ves un casco de oro con cuernos. + +shadow helmet=casco de sombra +You see a heavy shadow helmet. Instead of shining, it darkly gleams.=Ves un pesado casco de sombra. En lugar de brillar, emite destellos oscuros. + +xeno helmet= +You see a xeno helmet.= + +chaos helmet=casco del caos +You see a chaos helmet.=Ves un casco del caos. + +mainio helmet=casco de mainio +You see a strange mainio helmet. You can barely see when wearing it, but it protects your head very well.=Ves un extraño casco de mainio. Apenas puedes ver cuando lo llevas puesto, pero te protege muy bien la cabeza. + +magic chain helmet=casco de cota de malla mágico +You see a magic chain helmet.=Ves un casco de cota de malla mágico. + +liberty helmet=casco de la libertad +You see the liberty helmet. It's light and strong, you feel free wearing it.=Ves el casco de la libertad. Es ligero y resistente, te sientes libre al llevarlo puesto. + +black helmet=casco negro +You see a black helmet.=Ves un casco negro + +mithril helmet=casco de mithril +You see a mithril helmet.=Ves un casco de mithril. + +royal helmet= +You see a royal helmet.= + + +## herbs + +arandula=arandula +You see an arandula. It has medicinal properties, if the right person uses it.=Ves una arandula. Tiene propiedades medicinales, si es usada por la persona adecuada. + +kokuda=kokuda +You see a kokuda. It has medicinal properties, if the right person uses it.=Ves una kokuda. Tiene propiedades medicinales, si es usada por la persona adecuada. + +kekik=kekik +You see a kekik. It is a herb which can have healing properties.=Ves una kekik. Es una hierba que puede tener propiedades curativas. + +mandragora=mandrágora +You see a root of mandragora. It has medicinal properties, if the right person uses it.=Ves una raíz de mandrágora. Tiene propiedades medicinales, si es usada por la persona adecuada. + +sclaria=sclaria +You see the herb sclaria. The smell is calming.=Ves la hierba sclaria. Su olor es calmante. + +reindeer moss= +You see reindeer moss, something that grows only around Semos. It is quite a delicacy to reindeer, but extremely bitter to humans.= + + +## jewellery + +emerald=esmeralda +You see an emerald, a precious gemstone.=Ves una esmeralda, una gema preciosa. + +sapphire=zafiro +You see a sapphire, a precious gemstone.=Ves un zafiro, una gema preciosa. + +carbuncle=carbúnculo +You see a carbuncle, a precious gemstone related to the garnet.=Ves un carbúnculo, una gema preciosa relacionada con el granate. + +obsidian=obsidiana +You see a piece of obsidian, it's known as volcanic glass because it forms when lava cools rapidly. When used as a gemstone it may look jet black or silvery grey, depending on how it is cut.=Ves una pieza de obsidiana, es conocida como vidrio volcánico porque se forma cuando la lava se enfría rápidamente. Cuando se usa como gema puede parecer negro azabache o gris plateado, dependiendo de cómo se corte. + +amethyst= +You see an amethyst, a precious gemstone.= + +diamond=diamante +You see a diamond, not just a beautiful gem but also the hardest naturally occurring material.=Ves un diamante, no es sólo una hermosa gema sino también el material más duro de origen natural. + +black pearl=perla negra +You see a black pearl, so dark and shiny.=Ves una perla negra, tan oscura y brillante. + + +## keys + +dungeon silver key=llave de plata de la mazmorra +You see a silver dungeon key. Use it only if you are brave.=Ves una llave de plata de la mazmorra. Usala sólo si eres valiente. + +lich gold key=llave de oro del lich +You see a golden lich key. It will open the door to terrible things.=Ves una llave de oro del lick. Abrirá la puerta a cosas terribles. + +minotaur key=llave del minotauro +You see a fancy bronze key. It smells faintly of minotaur. Perhaps one dropped it?=Ves una lujosa llave de bronce. Huele ligeramente a minotauro. ¿Quizá se le cayó a uno? + +nalwor bank key=llave del banco de nalwor +You see a key to the customer room of Nalwor Bank.=Ves una llave de la sala de clientes del Banco de Nalwork. + +kotoch prison key=llave de la prisión de kotoch +You see an ancient copper key, enscribed with something in Orcish.=Ves una antigua llave de cobre, grabada con algo en Orco. + +kanmararn prison key=llave de la prisión de kanmararn +You see a key which matches the locks in the Kanmararn prison.=Ves una llave que coincide con las cerraduras de la prisión de Kanmararn. + +sedah gate key=llave de la puerta de sedah +You see a key for the gate to the imperial city of Sedah.=Ves una llave que abre la puerta que lleva a la ciudad imperial de Sedah. + +pet sanctuary key=llave del santuario de mascotas +You see a key for the pet sanctuary in Ados.=Ves una llave que abre el santuario de mascotas en Ados. + +house key=llave de la casa +You should not see this description=No deberías de poder ver esta descripción + +gate key= +You should not see this description=No deberías de poder ver esta descripción + +master key=llave maestra + +small key=llave pequeña +You see the key for Zara's row house in Ados.=Ves la llave de la row house de Zara en Ados. + + +## legs + +black legs=quijotes negros +You see a pair of black legs.=Ves un par de quijotes negros. + +blue legs=quijotes azules +You see a pair of shiny new blue legs.=Ves un par de quijotes azules nuevos y brillantes. + +chain legs=quijotes de cota de malla +You see a pair of well made chain legs. They should protect you against any attacks aimed low.=Ves un par de quijotes de cota de malla bien elaborados. Deberían de protegerte frente a cualquier ataque bajo. + +chaos legs=quijotes del caos +You see a pair of chaos legs. The strange colors confuse you.=Ves un par de quijotes del caos. Sus extraños colores te confunden. + +dwarvish legs= +You see a pair of dwarvish legs, designed by master craftsmen.= + +elvish legs=quijotes élficos +You see a pair of elvish legs.=Ves un par de quijotes élficos. + +golden legs=quijotes de oro +You see a pair of shining, strong golden legs.=Ves un par de quijotes de oro, resistentes y relucientes. + +jewelled legs=quijotes enjoyados +You see a beautiful pair of jewelled legs. A large red gem glints at you and they're encrusted all over with sapphires.=Ves un hermoso par de quijotes enjoyados. Una gema roja grande emite reflejos hacia ti y tienen incrustaciones de zafiro por todas partes. + +leather legs=quijotes de cuero +You see a pair of leather legs. They look new but they're poor quality.=Ves un par de quijotes de cuero. Parecen nuevos pero son de mala calidad. + +magic plate legs= + +mainio legs=quijotes de mainio +You see a pair of odd looking mainio legs. They are somewhat startling but they give you good protection.=Ves un par de quijotes de mainio de extraño aspecto. Son de algún modo sorprendentes pero te proporcionan una buena proteccion. + +mithril legs=quijotes de mithril +You see a pair of mithril legs. They will serve you well.=Ves un par de quijotes de mithril. Te servirán de mucho. + +red legs=quijotes rojos +A pair of red legs are in your grasp.=Un par de quijotes rojos están a tu alcance. + +royal legs= +You see a pair of royal legs.= + +shadow legs=quijotes de sombra +You see a pair of shadow legs. The weight of them is incredible.=Ves un par de quijotes de sombra. Su peso es increíble. + +stone legs= +You see a pair of stone legs, heavy and durable.= + +studded legs=quijotes tachonados +You see a pair of blood stained studded legs. You're not sure if the blood came from their last owner... or his enemies.=Ves un par de quijotes tachonados manchados de sangre. No estás seguro de si la sangre es de su último dueño... o de sus enemigos. + +xeno legs=quijotes de xeno +You see a pair of xeno legs. They're very strong and look brand new.=Ves un par de quijotes de xeno. Son muy resistentes y parecen nuevecitos. + + +## miscs + +decent meal= +You see a decent meal for Groongo Rahnnt. Deliver it to him quickly!= + +bulb=bulbo +You see a bulb. It can be planted in fertile ground where it will thrive.=Ves un bulbo. Puede ser plantado en terreno fértil donde se prosperará. + +seed=semilla +You see a seed. It can be planted in fertile ground where it will thrive.=Ves un semilla. Puede ser plantado en terreno fértil donde se prosperará. + +giant heart=corazón gigante +You see a giant heart. This proves that a giant was defeated, and has magical properties that you are not sure of.=Ves un corazón gigante. Ésto demuestra que un gigante fue derrotado, y tiene propiedades mágicas de las que no estás seguro. + +dice=dados +You see a set of dice. May lady luck be with you.=Ves un grupo de dados. Que la suerte te acompañe. + +cards=cartas +You see a deck of cards. Shuffle them well, to get a winning hand.=Ves una baraja de cartas. Barajealas bien, para ganar la mano. + +teddy=peluche +You see a teddy bear. It's soft and cuddly, and children love them.=Ves un oso de peluche. Es suave y tierno, y a los niños les encanta. + +rodent trap= +You see a rodent trap.= + +vampirette entrails=entrañas de vampiresa +You see some vampirette entrails. These guts prove that a vampirette was slain.=Ves unas entrañas de vampiresa. Estas vísceras demuestran que una vampiresa fue asesinada. + +bat entrails=entrañas de murciégalo +You see some bat entrails, full of the blood of a living creature.=Ves unas entrañas de murciégalo, llenas de sangre de algún ser vivo. + +suntan cream=crema bronceadora +You see a tube of suntan cream and notice that the outside is slippery. A nice scent of coconut mixed with bannana wafts into the air from it.= + +marbles=canicas +You see some marbles. Boys will be boys.=Ves unas canicas. Son cosas de niños. + +canned tuna=atún en lata +You see what appears to be canned tuna. Too bad you don't have a can opener.=Ves lo que parece ser atún enlatado. Lástima que no tengas un abrelatas. + +snowglobe=esfera de nieve +You see a snowglobe. It's so festive, it reminds you of Christmas!=Ves una esfera de nieve. ¡Es tan festiva, te recuerda a la Navidad! + +bobbin=canilla +You see a bobbin for a sewing machine.=Ves una canilla para una máquina de coser. + +venom gland= +You see the venom gland of a giant king cobra. It is filled with deadly toxin.= + +pelt= +You see a pelt of animal skin. It's used to make clothes and leather armor.= + +leather thread= +You see a strand of sturdy leather thread.= + +unicorn horn= +You see a spiral unicorn horn. It is highly valued by traders and used in the creation of magical items.= + + +## missiles + +snowball=bola de nieve +You see a snowball. You can throw it at your friends or put it in your bag to keep your fish fresh. But be careful, there could be little stones in it.=Ves una bola de nieve. Puedes tirarselas a tus amigos o ponerlas en tu bolsa para conservar el pescado fresco. Pero ten cuidado, podría haber piedrecitas dentro de ella. + +vomit=vómito +You see a pile of vomit. Gross!=Ves un montón de vómito ¡Qué desagradable! + +shuriken=shuriken +You see a shuriken. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results.=Ves un shuriken. Necesitarás estar cerca antes de que lo puedas usar como proyectil. Permanece recto, no inclinado, para obtener los mejores resultados. + +fire shuriken=shuriken de fuego +You see a fire shuriken. Throwing this flaming missile unleashes fiendish power.=Ves un shuriken de fuego. El lanzamiento de este proyectil en llamas desata un poder diabólico. + +wooden spear=lanza de madera +You see a wooden spear. You can attack at a distance with this primitive weapon. Stand square on, not diagonally, for the best results.=Ves una lanza de madera. Puedes atacar a distancia con este arma primitiva. Permanece recto, no inclinado, para obtener los mejores resultados. + + +## money + +money=dinero + + +## ranged + +wooden bow=arco de madera +You see a wooden bow. With this in one hand, and wooden arrows in the other, you may be able to attack from a distance.=Ves un arco de madera. Con esto en una mano, y flechas de madera en la otra, puedes ser capaz de atacar a distancia. + +longbow=arco largo +You see a longbow. Equipped with this, and a supply of arrows, you will have an effective ranged attack.=Ves un arco largo. Equipado con ésto, y con un surtido de flechas, conseguirás un efectivo ataque a distancia. + +composite bow=arco compuesto +You see a composite bow. It's better strung than most bows, for excellent ranged attack.=Ves un arco compuesto. Está mejor tensado que la mayoría de arcos, para proporcionar un excelente ataque a distancia. + +crossbow=ballesta +You see a crossbow. The attack is high but it has a very short range. What do you prefer?=Ves una ballesta. Su ataque es elevado pero cubre una distancia muy corta. ¿Qué prefieres? + +hunter crossbow=ballesta de cazador +You see a hunter crossbow. You must decide if the high attack is worth the short range it has.=Ves una ballesta de cazador. Debes decidir si su elevado ataque compensa la corta distancia que cubre. + +mithril bow=arco de mithril +You see a light and strong mithril bow. Its impeccable workmanship affords incredible accuracy.=Ves un arco de mithril ligero y resistente. Su impecable elaboración permite una precisión increíble. + + +## relics + +skull ring=anillo de calavera +You see a skull ring.=Ves un anillo de calavera. + +amulet=amuleto + + +## resources + +wood=madera +You see some wood, fallen from a nearby tree.=Ves un poco de madera, caída de algún árbol cercano. + +sugar cane=caña de azúcar +You see a sugar cane. You can grind it for raw sugar.=Ves una caña de azúcar. Puedes molerla para obtener azúcar no refinado. + +grain=trigo +You see a sheaf of grain.=Ves una gavilla de trigo. + +flour=harina +You see a sack of flour. It can be used in recipes for pizzas and sandwiches.= + +iron ore=mineral de hierro + +coal=carbón +You see a black and shiny piece of stone coal. It stayed in the ground for million of years.=Ves un pedazo de piedra de carbón negro y brillante. Permaneció enterrado durante millones de años. + +wool=lana +You see wool. It was recently shorn.=Ves lana. Fue esquilada recientemente. + +yarn=hilo de lana +You see a ball of yarn. It was spun with care.=Ves un ovillo de lana. Fue hilado con sumo cuidado. + +cloth=tela +You see a roll of cloth. It is of first-class quality.=Ves un rollo de tela. Es de primera calidad. + +gold nugget=pepita de oro +You see a gold nugget. Its rich golden color shines, and it may be useful.=Ves una pepita de oro. Su vivo color dorado brilla,y puede resultar útil. + +mithril nugget=pepita de mithril +You see a mithril nugget. Its pale silvery color shines, and it may be useful.=Ves una pepita de mithril. Su pálido color plateado brilla, y puede resultar útil. + +iron bar= + +gold bar=barra de oro +You see a gold bar. It will make beautiful jewelry, or fetch a fine price.=Ves una barra de oro. Servirá para hacer hermosas joyas, o se pagará un buen precio por ella. + +mithril bar=barra de mithril +You see a mithril bar. It can be made into excellent armor, or fetch a fine price.=Ves una barra de mithril. Se puede elaborar una armadura excelente con ella, o se puede pagar un buen precio por ella. + +golden egg=huevo de oro +You see a valuable golden egg.=Ves un valioso huevo de oro. + +spotted egg=huevo con manchas +You see a spotted egg. They are said to be delicious.=Ves un huevo con manchas. Se dice que son deliciosos. + +silk gland=glándula de seda +You see a silk gland from a giant spider. Its contents are extremely valuable.=Ves una glándula de seda de una araña gigante. Su contenido es extremadamente valioso. + +silk thread=hilo de seda +You see a spool of silk thread, spun from the glands of a giant spider.=Ves un carrete de hilo de seda, hilado de las glándulas de una araña gigante. + +oil=aceite +You see a small can of oil.=Ves una pequeña lata de aceite. + +mithril thread=hilo de mithril +You see a spool of mithril thread, made from silk fused with mithril.=Ves un carrete de hilo de mithril, elaborado a partir de seda fundida con mithril. + +mithril fabric=tela de mithril +You see a couple yards of mithril fabric. It's very sparkly!=Ves un par de metros de tela de mithril ¡Emite muchos destellos! + +horse hair=pelo de caballo +You see some horse hairs.=Ves un poco de pelos de caballo. + +beeswax=cera de abeja +You see a piece of beeswax.=Ves un pedazo de cera de abeja. + +candle=vela +You see a candle in a candle stick.=Ves una vela en un candelero. + +charcoal= +You see charcoal, a component of carbon steel.= + + +## rings + +medicinal ring= +You see a medicinal ring. It may protect you from being poisoned when worn on the finger.= + +antivenom ring= +You see an antivenom ring. It has a good chance to protect you from poisoning when worn on the finger.= + +placid ring= +You see a placid ring. It may reduce the effects of being shocked when worn on the finger.= + +insulated ring= +You see an insulated ring that may protect from being shocked. It must be worn on the finger.= + +wedding ring=anillo de boda +You see a golden wedding ring, a symbol of love.= + +engagement ring=anillo de compromiso +You see a diamond engagement ring, a symbol of love (or a symbol of wealth?)=Ves un anillo de compromiso de diamante, un símbolo de amor (¿o un símbolo de riqueza?) + +emerald ring=anillo de esmeralda +You see an emerald ring, known as ring of life. Wear it, and you risk less from death.=Ves un anillo de esmeralda, conocido como el anillo de la vida. Llevalo, y arriesgarás menos al morir. + +turtle shell ring= +You see a turtle shell ring.= + +imperial ring= +You see an imperial ring. It is imbued with the strength of a king.= + +enhanced imperial ring= +You see an enhanced imperial ring. It eminates the auras of strength and defense.= + +ring of clarity= +You see a ring of clarity. You feel more aware while wearing it.= + + +## scrolls + +summon scroll=pergamino de invocación +You see a summon scroll. It summons a creature to aid in your adventures.= + +summon pet scroll= + +blank pet scroll= + +home scroll=pergamino del hogar +You see a home scroll, it will take you back to semos city.=Ves un pergamino del hogar, te llevará de regreso a la ciudad de semos. + +nalwor city scroll=pergamino de la ciudad de nalwor +You see a nalwor city scroll, it will take you back to nalwor city.=Ves un pergamino de la ciudad de nalwor, te llevará de regreso a la ciudad de nalwor. + +ados city scroll=pergamino de la ciudad de ados +You see an ados city scroll, it will take you back to ados city.=Ves un pergamino de la ciudad de ados, te llevará de regreso a la ciudad de ados. + +fado city scroll=pergamino de la ciudad de fado +You see a fado city scroll, it will take you back to fado city.=Ves un pergamino de la ciudad de fado, te llevará de regreso a la ciudad de fado. + +kirdneh city scroll=pergamino de la ciudad de kirdneh +You see a kirdneh city scroll, it will take you back to kirdneh city.=Ves un pergamino de la ciudad de kirdneh, te llevará de regreso a la ciudad de kirdneh. + +kalavan city scroll=pergamino de la ciudad de kalavan +You see a kalavan city scroll, it will take you back to kalavan city.=Ves un pergamino de la ciudad de kalavan, te llevará de regreso a la ciudad de kalavan. + +deniran city scroll= +You see a deniran city scroll, it will take you back to deniran city.= + +marked scroll=pergamino marcado + +invitation scroll=pergamino de invitación + +empty scroll=pergamino en blanco +You see an empty scroll. If used correctly it will return you to where you marked it at.=Ves un pergamino en blanco. Usado correctamente te llevará de regreso a donde lo marcaste. + +rainbow beans=haba del arco iris +You see a small pile of rainbow beans. One says something odd.=Ves un pequeño montón de habas del arco iris. Una dice algo raro. + +balloon=globo +You see a bright red balloon. You can use it to float high up into the clouds.=Ves un globo de color rojo brillante. Puedes usarlo para flotar en lo alto de las nubes. + +twilight moss=musgo del crepúsculo +You see a strange looking piece of moss. It looks like it could have hallucinogenic properties.=Ves un pedazo de musgo de aspecto extraño. Parece que podría tener propiedades alucinógenas. + + +## shields + +black shield=escudo negro +You see a black shield. Its darkness will surely protect you.=Ves un escudo negro. Su oscuridad seguro que te protegerá. + +blue shield=escudo azul +You see a blue shield. It feels light in your hand but is strong enough for excellent protection.=Ves un escudo azul. Notas en tu mano que es ligero pero es lo bastante resistente como para proporcionar una excelente protección. + +buckler=broquel +You see a small buckler. It may protect you against swords and maces but against stronger weapons you do not trust it.=Ves un pequeño broquel. Puede protegerte frente a espadas y mazas pero frente a armas más potentes no te confíes. + +chaos shield=escudo del caos +You see a chaos shield. Its charming appearance will fool your enemies.=Ves un escudo del caos. Su encantadora apariencia engañará a tus enemigos. + +crown shield=escudo de la corona +You see a mighty crown shield. It is in perfect condition.=Ves un poderoso escudo de la corona. Esta en perfectas condiciones. + +elvish shield=escudo élfico +You see an elvish shield. It looks like one big tree leaf!=Ves un escudo élfico ¡Se parece a una gran hoja de árbol! + +enhanced lion shield=escudo de león mejorado +An enhanced lion shield glints at you. It will defend you well.=Un escudo de león mejorado emite destellos hacia ti. Te defenderá bien. + +golden shield=escudo de oro +You see a golden shield, could it be the ultimate in defense?=Ves un escudo de oro, ¿podría ser lo último en defensa? + +green dragon shield=escudo de dragón verde +You see a green dragon shield, scaly and strong.=Ves un escudo de dragón verde, escamoso y resistente. + +lion shield=escudo de león +You see a valuable lion shield. It is heavy enough to defend you against strong attacks.=Ves un valioso escudo de león. Es lo bastante pesado como para defenderte frente a ataques potentes. + +magic plate shield=escudo mágico de placas de acero +You see a magic plate shield. It has a magical quality surrounding it.=Ves un escudo mágico de placas de acero. Tiene una cualidad mágica que lo envuelve. + +mainio shield=escudo de mainio +You see a strange looking mainio shield. You have no idea what it's made of, but it seems to protect you well by distracting your enemies.=Ves un escudo de mainio de extraña apariencia. No tienes ni idea de lo que está hecho, pero parece que te protege bien al distraer a tus enemigos. + +mithril shield=escudo de mithril +You see a mithril shield. You will most surely be protected.=Ves un escudo de mithril. Casi con toda seguridad estarás protegido. + +plate shield=escudo de placas de acero +You see a solid plate shield. It is in good condition, and might be worth a little.=Ves un sólido escudo de placas de acero. Está en buenas condiciones, y podría valer bastante. + +red shield=escudo rojo +You see a red shield. It is a stunning shade of scarlet.=Ves un escudo rojo. Es de un imponente tono escarlata. + +royal shield= +You see a royal shield. It has a magical quality surrounding it.= + +shadow shield=escudo de sombra +You see a shadow shield. Rather than reflecting light, it seems to absorb it.=Ves un escudo de sombra. En lugar de reflejar la luz, parece absorberla. + +skull shield=escudo de calavera +The face of a skull scowls at you from this fearsome skull shield.=La cara de una calavera te frunce el ceño desde este temible escudo de calavera + +studded shield=escudo tachonado +You see a studded shield. It's a little dented but seems robust enough.=Ves un escudo tachonado. Está un poco abollado peroparece bastante robusto. + +unicorn shield=escudo del unicornio +You see a unicorn shield. It's beautifully crafted and will give you excellent protection.=Ves un escudo del unicornio. Está bellamente elaborado y te proporcionará una protección excelente. + +wooden shield=escudo de madera +You see a wooden shield. It is made of cheap wood and heavily used. However, it still seems to offer you some kind of basic protection.=Ves un escudo de madera. Está elaborado a partir de madera barata y ha sido muy utilizado. Sin embargo, todavía parece proporcionar algún tipo de protección básica. + +xeno shield=escudo de xeno +You see a lightweight but strong xeno shield, in the colors of the creature it was looted from.=Ves un escudo de xeno ligero pero resistente, de los mismos tonos que la criatura a la que se le saqueó. + +ice shield= + + +## special + +lucky charm=amuleto de la suerte +You see a lucky charm. Some believe that it helps their karma in some way, but others think that is superstition.=Ves un amuleto de la suerte. Algunos creen que ayuda a su karma de algún modo, pero otros piensan que es una superstición. + +mithril clasp=cierre de mithril +You see a clasp made from mithril, it would provide the fastening for a cloak.=Ves un cierre elaborado a partir de mithril, te proporcionaría la fijación para una capa. + +mythical egg=huevo mítico +You see a mythical egg, but you do not know what it is for.=Ves un huevo mítico, pero no sabes para qué es. + +magical eggshells=cáscaras de huevo mágicas +You see shards from a mythical egg. They have magical properties.=Ves fragmentos de un huevo mítico. Tienen propiedades mágicas. + +golden gm token=ficha de dj de oro +You see a golden gm token. Does it bring you pride?= + +silvery gm token=ficha de dj de plata +You see a silvery gm token. Does it bring you pride?= + +bronze gm token=ficha de dj de bronce +You see a bronze gm token. Does it bring you pride?= + +four-leaf clover= +You see a lucky four-leaf clover. What are the chances of finding one!?= + + +## swords + +assassin dagger=daga de asesino +You see an assassin dagger, easy to pull out and use when needed.=Ves una daga de asesino, fácil de sacar y de usar cuando se necesite. + +biting sword=espada mordaz +The tip of this biting sword is chiseled to a lethal point. It will serve you well.=La punta de esta espada mordaz está cincelada hasta un punto letal. + +black sword=espada negra +You see a black sword. Its darkness will surely protect you.=Ves una espada negra. Su oscuridad seguro que te protegerá. + +broadsword=espada ancha +You see a mighty broadsword. Enemies will see their death.=Ves una poderosa espada ancha. Los enemigos verán su muerte. + +buster=buster +You see a buster, with its distinctive, oversized blade.=Ves una espada buster, con su distintiva hoja sobredimensionada. + +chaos dagger=daga del caos +You see a chaos dagger, it is very lightweight and sharp. It will heal you while hurting them.=Ves una daga del caos, es muy ligera y afilada. + +chaos sword=espada del caos +You see a chaos sword. Wielding it allows to hurt your foes and to heal you at the same time.=Ves una espada del caos. Empuñarla te permite hacer daño a tus enemigos y curarte al mismo tiempo. + +claymore=claymore +This claymore is very hard to lift as it is so heavy. It will be a good weapon if you can swing it.=Esta claymore es muy difícil de levantar ya que es muy pesada. + +dagger=daga +You see a dagger, it is little more than decorative but you can jab pretty fast with it.=Ves una daga, es poco más que decorativa pero puedes punzar bastante rápido con ella. + +dark dagger=daga oscura +You see a dark dagger. It's designed to be used with a fast motion.=Ves una daga oscura. Se ha diseñado para ser utilizada con un movimiento rápido. + +demon fire sword=espada de fuego de demonio +The demon fire sword, a horrible sword taken from hell's deepest dungeons.=La espada de fuego de demonio, una espada horrible sacada de las más profundas mazmorras del infierno. + +demon sword=espada de demonio +Demon skeletons return from hell with this demon sword.=Los esqueletos demonio regresan del infierno con esta espada de demonio. + +drow sword=espada drow +This is a genuine drow sword.=Esta es una genuina espada drow. + +elvish sword=espada élfica +This is an elvish sword. It is finely crafted.=Esta es una espada élfica. Está finamente elaborada. + +fire sword=espada de fuego +Flames race along this fire sword as you wield it.=Las llamas corren a lo largo de esta espada de fuego mientras la esgrimes. + +golden blade=cuchilla de oro +You see the rare and precious golden blade. It can steal life from those you stab with it.=Ves la rara y preciosa cuchilla de oro. Puede robar la vida a aquellos que apuñales con ella. + +golden orc sword=espada de oro de orco +You see a golden orc sword, with its heavy and deadly appearance it seems also valuable.=Ves una espada de oro de orco, con su pesada y mortal apariencia parece tan valiosa. + +great sword=gran espada +This rare great sword is heavy and of good craftsmanship. It can only attack incredibly slowly.=Esta rara gran espada es pesada y de buena fabricación. + +hell dagger=daga del infierno +You see a hell dagger. Is the color that of the burning fires of Hell?=Ves una daga del infierno ¿Es ése el color de las llamas del infierno? + +ice sword=espada de hielo +You see an ice sword, the blade is lethally cold to the touch, and you know it is one of the best weapons you'll find.=Ves una espada de hielo, la hoja es letalmente fría al tacto, y sabes que es una de las mejores armas que vas a encontrar. + +imperator sword=espada imperator +You see a an imperator sword, crafted from you know not what.=Ves una espada imperator, elaborada a partir de ya sabes qué. + +immortal sword=espada inmortal +This immortal sword is a gift of gods to the chosen one. It is heavy and of exquisite craftsmanship.=Esta espada inmortal es un regalo de los dioses al elegido. Es pesada y de una elaboración exquisita. + +katana=katana +You see a valuable katana, with a lethal looking blade.=Ves una valiosa katana, con una hoja de apariencia letal. + +knife=cuchillo +You see a blunt knife which looks capable of little damage.=Ves un cuchillo desafilado que parece capaz de hacer poco daño. + +l hand sword=espada de mano i +You see l hand sword. It must be used with right hand sword.=Ves una espada de mano i. Debe usarse con una espada de mano derecha. + +night dagger=daga de la noche +You see a night dagger, to be used with stealth.=Ves una daga de la noche, para ser utilizada con sigilo. + +nihonto=nihonto +You see a samurai's nihonto sword with a razor sharp blade.=Ves una espada nihonto de samurai con una hoja tan afilada como la de una cuchilla. + +obsidian knife=cuchillo de obsidiana +You see a knife whose blade is crafted to a lethal edge from the finest obsidian.=Ves un cuchillo cuya hoja se ha trabajado hasta conseguir un filo letal a partir de la obsidiana más pura. + +orc sword=espada de orco +You see an orc sword which has a heavy and deadly appearance.=Ves una espada de orco que tiene una apariencia pesada y mortal. + +orcish sword=espada orca +This orcish sword is badly done and weighs a lot.=Esta espada orca está mal hecha y pesa un montón. + +r hand sword=espada de mano d +You see r hand sword. It must be used with a left hand sword.=Ves una espada de mano d. Debe usarse con una espada de mano izquierda. + +royal dagger= +You see the rare and precious royal dagger. It can steal life from those you stab with it.= + +scimitar=cimitarra +This scimitar catches the light as you look at it, it will be a powerful weapon.=Esta cimitarra atrapa la luz mientras la miras, sera un arma poderosa. + +short sword=espada corta +You see a well kept short sword which will be easy to carry as you travel.=Ves una espada corta bien conservada que será fácil de llevar cuando viajes. + +soul dagger=daga del alma +You see a soul dagger. It's so light and sharp that you can stab at a very fast rate.=Ves una daga del alma. Es tan ligera y afilada que puedes apuñalar a un ritmo muy rápido. + +sword=espada +You see a sharp sword which might even be worth a little.=Ves una espada afilada que podría incluso valer un poco. + +vampire sword=espada de vampiro +You see the vampire sword, which will suck life from those you attack with it.=Ves la espada de vampiro, que absorberá la vida de aquellos a los que ataques con ella. + +xeno sword=espada de xeno +You wield a xeno sword. Despite the great strength it is strangely light.=Tu esgrimes una espada de xeno. A pesar de su gran potencia es extrañamente ligera. + +training sword= +You see a wooden training sword not even useful for cutting butter. But it is light and easy to swing.= + + +## tokens + +arrow game token=ficha del juego de la flecha +You see a token, that when placed in the right spot could point you in the right direction.=Ves una ficha, que cuando se coloque en el lugar correcto podría señalarte la dirección correcta. + +o board token=ficha del tablero o +You see an O token.=Ves una ficha O + +x board token=ficha del tablero x +You see an X token.=Ves una ficha X + + +## tools + +food mill=molino de alimentos +You see a food mill. It is the traditional tool to mill food.=Ves un molino de alimentos. Es la herramienta tradicional para moler alimentos. + +sugar mill=molino de azúcar +You see a sugar mill. You can obtain sugar if you grind canes with it.=Ves un molino de azúcar. Puedes obtener azúcar si mueles cañas con él. + +scroll eraser=pergamino borrador +You see a scroll eraser. Put a marked scroll in the opposite hand to clean the scroll.=Ves un pergamino borrador. Pon un pergamino marcado en la mano opuesta para limpiar el pergamino. + +pestle and mortar=maza y mortero +You see a pestle and mortar. This is the traditional tool to pound herbs, food and other substances.=Ves una maza y un mortero. Esta es la herramienta tradicional para machacar hierbas, alimentos y otras sustancias. + +gold pan=sartén del oro +You see a gold pan. It is the traditional tool to prospect for gold.=Ves una sartén del oro. Es la herramienta tradicional para buscar oro. + +fishing rod=caña de pescar +You see a fishing rod. If you find a fishing spot you can catch fish with it.=Ves una caña de pescar. Si encuentra un lugar para pescar puedes capturar peces con ella. + +lamp= +You see a lamp.= + +magical scissors=tijeras mágicas +You see a sparkling pair of magical scissors. Just think what they could cut!=Ves un par de tijeras mágicas centelleantes ¡Piensa sólo en lo que podrían cortar! + +magical needle=aguja mágica +You see a magical needle with a very sharp point.=Ves una aguja mágica con una punta muy afilada. + +pick=pico +You see a pick. It is a tool for excavating coal.=Ves un pico. Es una herramienta para la extracción de carbón. + +shovel= +You see a shovel, a tool for digging.= + +leather needle= +You see a heavy leather needle for working thick hide.= + +rotary cutter= +You see a rotary cutter. It can be used to cut up pizza. But perhaps it can be used on other things as well.= + +metal detector= +You may find buried treasures with this metal detector.= + +rope= +You see a sturdy rope.= + + +## whips + +venom whip= +You see a venom whip. Its thong has been saturated with cobra venom.= + + +# creatures names and descriptions + +## angel + +dark angel=ángel oscuro +You see a dark angel. He is an strong evil angel, and for now he is under cover of darkness.=Ves a un ángel oscuro. Es un ángel poderoso y malvado, y por ahora está al amparo de la oscuridad. + +dark archangel=arcángel oscuro +You see a dark archangel. After she fell from heaven her soul became as dark as she is now.=Ves a una arcángel oscura. Después de que cayese del cielo su alma se volvió tan oscura como ella lo es ahora. + +fallen angel=ángel caído +You see a fallen angel. When he turned away from good, he fell down from heaven.=Ves a un ángel caído. Cuando se apartó del bien, se cayó del cielo. + +baby angel=bebé angel +You see a baby angel. Will it ever get a chance to grow up?=Ves a un bebé angel ¿Tendrá alguna vez la oportunidad de hacerse mayor? + +angel=ángel +You see an angel. With his flame sword he guards the way and doesn't let you pass.=Ves a un ángel. Con su espada flamígera protege el camino y no te deja pasar. + +archangel=arcángel +You see an archangel. It is Heaven's most terrifying weapon, a fierce and absolute force of primordial creation.= + + +## animal + +crab=cangrejo +You see a little crab that comes from the beach, be careful of its pincers.=Ves un pequeño cangrejo que viene de la playa, ten cuidado con sus pinzas. + +deer=ciervo +You see a defenseless deer, do not have fear of it.=Ves un ciervo indefenso, no le tengas miedo. +You see a reindeer running freely, and wonder if he is one of Santa's special helpers.= + +bat=murciégalo +You see a bat, it will seek your blood, don't be distracted...=Ves un murciégalo, buscará tu sangre, no te distraigas... + +killer bat=murciégalo asesino +You see a killer bat which will suck your blood!=¡Ves un murciégalo asesino que te chupará la sangre! + +giant bat= + +giant killer bat= +You see a giant killer bat. As if small wasn't pesky enough, these giant ones are capable of taking down anyone who dares come before them.= + +piglet=cerdito +You see a piglet, a bit dirty.=Ves un cerdito, un poco sucio. + +boar=verraco +You see a boar, they are pretty wild!=Ves un verraco, ¡son bastante salvajes! + +bull=toro +You see a bull, run away!!!=Ves un toro, ¡sal corriendo! + +cow=vaca +You see a cow, taste her delicious milk.=Ves una vaca, prueba su deliciosa leche. + +goat=cabra +You see a goat, which is wild and lives in the mountain.=Ves una cabra, que es salvaje y vive en la montaña. + +ram=carnero +You see a ram, maybe you are able to shear it?=Ves un carnero, ¿quizá seas capaz trasquilarlo? + +horse=caballo +You see a horse, don't slap it or it will run away!=Ves un caballo, ¡no les des una palmada o saldrá corriendo! + +white horse=caballo blanco +You see a white horse, they love to stay with their brown friends.=Ves un caballo blanco, les gusta estar con sus amigos marrones. + +monkey=mono +You see a monkey, would you like to offer it a banana?=Ves un mono, ¿Te gustaría ofrecerle un plátano? + +fox=zorro +You see a fox, it becomes extinct!=Ves un zorro, ¡se esta extinguiendo! + +wolf=lobo +You see a wolf. It is wild and very hungry!=Ves un lobo ¡Es salvaje y está muy hambriento! + +big bad wolf=lobo grande y malo +You see a big bad wolf. It is really wild, hungry and really dangerous!=Ves un lobo grande y malo ¡Es verdaderamente salvaje, está hambriento y es realmente peligroso! + +lion=león +You see a lion, dangerous and hungry...=Ves un león, peligroso y hambriento... + +tiger=tigre +You see a tiger, which likes deer and human meat.=Ves un tigre, al que le gustan los ciervos y la carne humana. + +elephant=elephant +You see an elephant, a huge animal with lots of ham and meat. Are you sure that you want to attack it?=Ves un elefante, un animal enorme con un montón de jamón y de carne. + +bull elephant= +You see an aggressive bull elephant.= + +musth elephant= +You see a musth elephant. It is highly aggressive and unpredictable.= + +woolly mammoth= + +babybear=cachorro de oso +You see a babybear, isn't it sweet?=Ves un cachorro de oso, ¿a que es dulce? + +panda=panda +You see a panda which comes from an exotic land.=ves un panda que viene de una tierra exótica. + +bear=oso +You see a bear. You are lucky and found it awake.=Ves un oso. Has tenido suerte y lo has encontrado despierto. + +black bear=oso negro +You see a strong black bear which hibernates in winter.=Ves un fuerte oso negro que hiberna en invierno. + +polar bear= +You see a polar bear which lives in the icy desert. Beware! It can take you down with its powerful claws!= + + +## animal_aquatic + +kraken= +From the depths of the sea rises a Kraken!= + +neo kraken= +From the horrible darkness of the deep sea rises a giant NEO kraken!= + +shark= +A shark! Best move cautiously.= + + +## animal_fowl + +chick=pollito +You see a little chick, it doesn't like to be away from its mother.=Ves un pequeño pollito, no le gusta estar lejos de su madre. + +chicken=pollo +You see a chicken, isn't it nice?=Ves un pollo, ¿a que es bonito? + +dodo= +You see a dodo bird, aren't they supposed to be extinct?= + +mother hen=madre gallina +You see a mother hen, she will probably hurt you if you touch her sons.=Ves una madre gallina, probablemente te haga daño si tocas a sus hijos + +penguin=pingüino +You see a penguin, smart and silly.=Ves un pingüino, bobo y elegante + +pigeon=paloma +You see a pigeon, it loves the little seeds.=Ves una paloma, le encantan las semillas pequeñas. + + +## beast + +young beholder=joven beholder +You see a young beholder. His huge eye will find you everywhere!=Ves un joven beholder ¡Su ojo gigante te encontrará en cualquier lugar! + +beholder=beholder +You see a beholder which can see you very well with his huge eye...=Ves un beholder que puede verte muy bien con su enorme ojo... + +elder beholder=beholder anciano +You see an elder beholder, his nasty eye will scare you.=Ves un beholder anciano, su desagradable ojo te asustará. + +gargoyle=gárgola +You see a gargoyle, You should think that it is a statue, but no, it is alive!=Ves a una gárgola, Pensarías que es una estatua, pero no, ¡está viva! + +elder gargoyle=gárgola anciana +You see an elder gargoyle, it is wise...=Ves a una gárgola anciana, es sabia... + +dark gargoyle=gárgola oscura +You see a dark gargoyle, do you feel the fear?=Ves a una gárgola oscura, ¿sientes el miedo? + +venom gargoyle=gárgola de veneno +You see a venom gargoyle, a hideous and poisonous creature.=Ves a una gárgola de veneno, una criatura horrible y venenosa. + +mimic= +You see a mimic. Its content might be different from what you expect.= + +snarfkin=snarfkin +You see a snarfkin, the favorite pet of wizards. Be careful, its claws are poisonous!=Ves a un snarfkin, la mascota favorita de los magos. Ten cuidado, ¡sus garras son venenosas! + +googon=googon +You see a googon that comes from the worst place in this world...Be careful this creature is a deadly killer.=Ves a un googon que viene del peor lugar de este mundo... Ten cuidado esta criatura es una asesina mortal. + +grobble monster=monstruo grobble +You see a grobble monster. You might find this under your bed, inside your closet, or within your darkest nightmares.= + +winged blurble=blurble alado + +thing=cosa +You see a thing. It looks like a very mutated human and is so scary that you think it has to come from another world.=Ves a una cosa. Parece un humano muy mutado y es tan espantoso que crees que ha venido de otro mundo. + + +## chess + +chaos pawn=peón del caos +You see a pawn for a game of chess. He will drop nothing if you kill him.=Ves un peón para una partida de ajedrez. No dejará caer nada si lo matas. + +chaos rook=torre del caos +You see a rook for a game of chess. He will drop nothing if you kill him.=Ves una torre para una partida de ajedrez. No dejará caer nada si la matas. + +chaos knight=caballo del caos +You see a knight for a game of chess. He will drop nothing if you kill him.=Ves un caballo para una partida de ajedrez. No dejará caer nada si lo matas. + +chaos bishop=alfil del caos +You see a bishop for a game of chess. He will not reward you if you kill him.=Ves un alfil para una partida de ajedrez. No te recompensará con nada si lo matas. + +chaos king=rey del caos +You see a king of a game of chess. He will reward nothing if you kill him.=Ves un rey para una partida de ajedrez. No te recompensará con nada si lo matas. + +chaos queen=reina del caos +You see a queen piece for a game of chess. She will reward nothing if you kill her.=Ves una pieza reina para una partida de ajedrez. No te recompensará con nada si la matas. + +madaram pawn=peón madaram +You see a pawn for a game of chess. It will reward nothing if killed.=Ves un peón para una partida de ajedrez. No te recompensará con nada si lo matas. + +madaram rook=torre madaram +You see a rook for a game of chess. It will scarcely reward you to kill it.=Ves una torre para una partida de ajedrez. Apenas te recompensará con algo si la matas. + +madaram knight=caballo madaram +You see a knight, a chess piece. He won't reward you if you kill him.=Ves un caballo, una pieza de ajedrez. No te recompensará con nada si lo matas. + +madaram bishop=alfil madaram +You see a bishop for a game of chess. It will reward you nothing if you slay it.=Ves un alfil para una partida de ajedrez. No te recompensará con nada si lo matas. + +madaram king=rey madaram +You see a king to play chess with, he will reward you nothing if you kill him.=Ves un rey para jugar al ajedrez con él, no te recompensará con nada si lo matas. + +madaram queen=reina madaram +You see a queen piece for a game of chess. She will reward nothing if you kill her.=Ves una pieza reina para una partida de ajedrez. No te recompensará con nada si la matas. + + +## demon + +violet avenger=vengador violeta + +devil queen=reina del diablo + +demon=demonio +You see a demon. He personifies evil.=Ves un demonio. Personifica el mal. + +imperial demon servant=sirviente demonio imperial +You see an imperial demon servant who carries out whims of his demon lords.= + +imperial demon lord=señor demonio imperial +You see an imperial demon lord. He holds the rank of General among demons, and very few out rank it in hell.= + +balrog=balrog +You see a huge balrog. He is slow hitting, but terribly strong as he stands towering above you.=Ves a un enorme balrog. Es lento en golpear, pero terriblemente potente mientras permanece de pie imponiendose por encima de ti. + +imp=diablillo +You see an imp. He may look a little bit cute, but don't forget he is one of the devil's servants!=Ves a un diablillo¡ Puede parecer un poquito lindo, pero no olvides que es uno de los siervos del demonio! + +winged blue imp=diablillo azul alado +You see a winged blue imp. It's endowed with the power of evil. His trident is able to poison even the bravest warriors.= + +water demon= + + +## dragon + +chaos green dragonrider=jinete de dragón verde del caos +You see a chaos green dragonrider. Don't misjudge him as to believe it's a normal green dragon, as it is controlled by a mysterious sorcerer seated on his back.= + +chaos red dragonrider=jinete de dragón rojo del caos +You see a chaos red dragonrider. He might look like a red dragon, but the sorcerer seated on his back guides him to give even more strength out.= + +green dragon=dragón verde +You see a green dragon. He looks very tall and strong.= + +bone dragon=dragon de hueso +You see a bone dragon. It's been said that there are creatures with enough will to live that their souls still cling to life even as their flesh and blood has failed them. This is living proof.=Ves a un dragón de hueso. Se dice que hay criaturas con la suficiente voluntad de vivir como para que sus almas todavía se aferren a la vida aún cuando su carne y su sangre les han abandonado. + +blue dragon=dragón azul +You see a blue dragon. It has a very beautiful blue skin which shimmers mysteriously in the light. But don't forget that it also has claws and teeth!=Ves a un dragón azul. Tiene un tono de piel azul muy bonito que brilla misteriosamente en la luz¡Pero no olvides que también tiene garras y dientes! + +red dragon=dragón rojo +You see into the emerald eyes of a red dragon. With these it can spot prey in the darkness. Its red scales serve it well, reflecting any light that shines on them blinding enemies. The scales are prized for their beauty, and difficult to obtain from such a ferocious beast.= + +black dragon=dragón negro +You see a black dragon. As you see this huge black creature you almost faint! If you concentrate you can feel the deep growling of it in your bones.=Ves un dragón negro ¡Al ver a esta enorme criatura negra casi te desmayas! + +flying golden dragon=dragón de oro volador +You see a flying golden dragon. As it spreads its mighty wings you shiver... this mystical creature seems to have much power in it!=Ves a un dragón de oro volador. A medida que extiende sus poderosas alas te estremeces... ¡esta criatura mística parece tener mucho poder en su interior! + +twin headed dragon=dragón de dos cabezas +You see a twin headed dragon. The two heads are staring at you with their four eyes!=Ves a un dragón de dos cabezas ¡Las dos cabezas te están mirando con sus cuatro ojos! + +rysin dragon= + + +## dwarf + +duergar=duérgar +You see a duergar. It is a close relative to dwarves but with a diabolically tainted blood.= + +duergar axeman=hachero duérgar + +elder duergar=duérgar anciano + +duergar elite=duérgar de élite + +duergar hero=héroe duérgar + +duergar wizard=mago duérgar + +black duergar=duérgar negro + +duergar king=rey duérgar + +dwarf=enano +You see a dwarf who works in the mine to find some iron for us.=Ves a un enano que trabaja en la mina para encontrar un poco de hierro para nosotros. + +dwarf guardian=guardián enano +You see a dwarf guardian who fights against the humans.=Ves a un guardián enano que lucha contra los humanos. + +elder dwarf=enano anciano +You see an elder dwarf, a mighty warrior tired of fighting.=Ves a un enano anciano, un poderoso guerrero cansado de luchar. + +hero dwarf=héroe enano +You see a hero dwarf. He is an old dwarf warrior who became a hero for fighting in many dwarf wars and he is very famous for his strength and brutality.=Ves a un héroe enano. Es un viejo guerrero enano que se convirtión en héroe por luchar en muchas guerras de enanos y es muy famoso por su fuerza y brutalidad. + +leader dwarf=líder de los enanos +You see a leader dwarf. He is the most mighty dwarf of his tribe so he enjoys much popularity there.=Ves a un líder de los enanos. Es el enano más poderoso de su tribu así que disfruta de una gran popularidad allí. + +giant dwarf=enano gigante +You see a giant dwarf. It is very hard for him to be a giant dwarf because some of the other dwarves don't really see him as one of them because of his height... But in his big heart he is a dwarf and in a battle he will fight against each enemy!=Ves a un enano gigante. Es muy duro para él ser un gigante por algunos de los otros enanos no le ven realmente como uno de los suyos por su estatura... Pero en su gran corazón se siente un enano y en la batalla luchará contra cada enemigo! + +Dhohr Nuggetcutter=Dhohr Nuggetcutter + +Lord Durin=Lord Durin +You see the mighty Lord Durin, an ancient dwarf who has led his race through countless battles.=Ves al poderoso Lord Durin, un antiguo enano que ha guiado a su raza a en incontables batallas. + +mountain dwarf=enano de montaña +You see a mountain dwarf. He likes earth, stones and rocks. Deep within the mountain, where no animal and no human has ever been, you can hear the sound of his pickaxe as he searches for gold and ore.=Ves a un enano de montaña. Le gusta la tierra, las piedras y las rocas. En lo profundo de la montaña, donde ningún animal ni ser humano ha estado nunca, puedes oir el sonido de su piqueta mientras busca mineral de oro y de hierro. + +mountain dwarf guardian=guardián enano de montaña +You see a mountain dwarf guardian. He has to watch out for enemies and guards the mountain dwarfs while they are doing their work.=Ves a un guardián enano de montaña. Tiene que estar alerta de los enemigos y protege las montañas enanas mientras ellos hacen su trabajo. + +mountain elder dwarf=enano de montaña anciano +You see a mountain elder dwarf. For many years he worked deep in the mountain and found much gold and ore in his life. Soon he will be too old to do this work but at the moment he is still a strong dwarf.=Ves a un enano de montaña anciano. Durante muchos años trabajó en lo profundo de la montaña y encontró mucho mineral de oro y de hierro en su vida. Pronto será demasiado viejo para hacer este trabajo pero por el momento todavía es un enano fuerte. + +mountain hero dwarf=héroe enano de la montaña +You see a mountain hero dwarf. Many, many dwarves admire him! He was a member of a dwarvish group which discovered a very big gold deposit!=Ves a un héroe enano de la montaña ¡Muchos, muchísimos enanos le admiran! ¡Fue miembro de un grupo de enanos que descubrió a depósito de oro muy grande! + +mountain leader dwarf=líder de los enanos de montaña +You see a mountain leader dwarf. He is responsible for the mountain dwarves and their work, and also for the organization of the guardian troops. But in serious cases he will also fight on his own!=Ves a un líder de los enanos de montaña. Es responsable de las montañas de los enanos y de sus trabajos, y también de la organización de las tropas guardianas ¡Pero en situaciones graves también luchará por sí sólo! + + +## elemental + +air elemental=elemental de aire +You see an air elemental and feel a fresh breeze past you.=Ves a un elemental de aire y sientes una fresca brisa que te atraviesa. + +djinn=djinn +You see a djinn. He isn't caught in a bottle or lamp so someone must have let him free and gave him the order to kill you.=Ves a un djinn. No está atrapado en una botella o lámpara así que alguien debe de haberle soltado y le dió la orden de matarte. + +nymph=ninfa +You see a nymph. She loves nature very much! As you take a look at her she feels disturbed in sun-bathing and talking to the birds, perhaps she was best left alone.=Ves a una ninfa ¡Le gusta muchísimo la naturaleza! Al echarle un vistazo se siente molestada en su tomar el sol y en su hablar con los pájaros, quizá habría sido mejor dejarla a solas. + +earth elemental=elemental de tierra +You see an earth elemental. The earth is quaking as it moves over the surface.=Ves a un elemental de tierra. La tierra tiembla mientras se mueve sobre su superficie. + +fire elemental=elemental de fuego +You see a fire elemental and hardly can stand the heat.=Ves a un elemental de fuego y apenas puedes soportar el calor. + +legendary ice elemental= +You see a Legendary Elemental of Ice...= + +ice elemental=elemental de hielo +You see an ice elemental and sense a shiver on your spine.=Ves a un elemental de hielo y sientes un escalofrío en tu columna vertebral. + +ice giant=gigante de hielo +You see an ice giant. His skin is covered with frozen hoarfrost and his breath feels like the cold north wind.=Ves a un gigante de hielo. Su piel está cubierta de escarcha helada y su aliento se siente como el frío viento del norte. + +water elemental=elemental de agua +You see a Water elemental which is made of water and various other liquids.= + +ent=ent +You see an ent. First you think it is a tree but you will get a surprise when it starts to walk!=Ves a un ent !Primero piensas que es un árbol pero te llevas una sorpresa cuando empieza a caminar! + +old ent=ent viejo +You see an old ent. It has seen many years passing by and survived a lot of storms. You can see that it is very old because it has already lost all of its leaves and most of its bark is broken...=Ves a un viejo ent. Ha visto muchos años pasar y ha sobrevivido a muchas tormentas. Puedes ver que es muy viejo porque ya ha perdido todas sus hojas y la mayor parte de su corteza está rota... + +entwife=esposaent +You see an entwife. It is stronger than an ent and lives in a group in an ancient forest.=Ves a una esposaent. Es más fuerte que un ent y vive en grupo en un bosque antiguo. + + +## elf + +albino elf swordsman=espadachín elfo albino +You see an albino elf swordsman who is a professional in using swords. He can attack you from behind in a blink of an eye.= + +albino elf archer=arquero elfo albino +You see an albino elf archer. He is so sharp-eyed that he could hit a bee from a distance of 100 meters.= + +albino elf magician=mago elfo albino +You see an albino elf magician. He knows all the secrets of magic. You'd be better to not let him unleash his full power!= + +albino elf queen=reina elfa albina +You see an albino elf queen. She is the matriarch of the albino elf race and she will do everything to protect it.= + +albino elf king=rey elfo albino +You see an albino elf king. He is the patriarch of albino elves and won countless of battles with his powerful albino elf army.= + +child dark elf=niño elfo oscuro +You see a child dark elf. You should know that dark elves are already quite strong and intelligent when they are children. Don't think you can fight him so easily!=Ves a un niño elfo oscuro. Deberías saber que los elfos oscuros son ya bastante fuertes e inteligentes cuando son niños ¡No creas que puedes enfrentarte a él tan fácilmente! + +dark elf archer=arquero elfo oscuro +You see a dark elf archer. Dark elf archer's flawless aim can be fatal for those who dare to oppose them.= + +dark elf=elfo oscuro + +dark elf elite archer=arquero de élite elfo oscuro + +dark elf captain=capitán elfo oscuro + +dark elf knight=caballero elfo oscuro + +dark elf general=general elfo oscuro +You see a Dark Elf General. It is commanding the evil troops of Nalwor Caves against intruders.= + +dark elf wizard=mago elfo oscuro +Dark elf wizards are very powerful and dangerous. They are specialised in dark magic.= + +dark elf viceroy=virrey elfo oscuro + +dark elf ranger=explorador elfo oscuro + +dark elf sacerdotist=sacerdotisa elfa oscura + +dark elf admiral=almirante elfo oscuro + +dark elf master=Maestre elfo oscuro +You see a dark elf master. Beware of his high skills on magic and swordsmanship!= + +dark elf matronmother=matrona madre elfa oscura +Dusky and shrouded, this matriarch rules her clan with a soft touch and sharp fangs. An expert in venoms and poisons, you wouldn't ever want to turn your back on her.= + +elf=elfo +You see an elf which loves the forest and lives in peace.=Ves a un elfo al que le encanta el bosque y que vive en paz. + +militia elf=elfo de la milicia +You see a militia elf which trains hard to beat you!=¡Ves a un elfo de la milicia que se entrena duramente para vencerte! + +archer elf=arquero elfo +You see an archer elf which can do much damage with its wooden arrows.=Ves a un arquero elfo que puede hacer mucho daño con sus flechas de madera. + +soldier elf=soldado elfo +You see a soldier elf, a good partner of the militia elves.=Ves a un soldado elfo, un buen compañero de la milicia de los elfos. + +mage elf=maga elfa +You see a mage elf. She can feel pure magic when she stands outside in the forest and just frees her mind.=Ves a una maga elfa. Se puede sentir magia pura cuando se encuentra fuera en el bosque y simplemente libera su mente. + +commander elf=comandante elfo +You see a busy commander elf. He has control over the army of elves.=Ves a un ocupado comandante elfo. Tiene el control del ejército de los elfos. + +archmage elf=archimaga elfa +You see an archmage elf. She is pretty.=Ves a una archimaga elfa. Es guapa. + +elf sacerdotist=sacerdotisa elfa + +elf wizard=mago elfo +You see an elf wizard. He is a very wise elf who knows almost every kind of magic. He likes to use nature spells to make every plant and creature fertile, but he is also able to defend himself when he has to do.=Ves a un mago elfo. Es un elfo muy sabio que sabe casi cualquier tipo de magia. Le gusta usar hechizos de la naturaleza para hacer a cualquier planta o animal fértil, pero también es capaz de defenderse cuando tiene que hacerlo. + +elf bodyguard=guardaespaldas elfo + + +## giant + +amazoness giant=giganta amazona +You see an amazoness giant. She looks very beautiful but don't be deceived by that!=Ves a una giganta amazona ¡Parece muy hermosa pero no te dejes engañar por eso! + +giant=gigante +You look up into the face of a huge giant. His physical proportions and extraordinary strength should give you goosebumps.= + +elder giant=gigante anciano +You see an elder giant. For many years he lived alone and had to hide because he was hunted by humans! Now he is old and has decided not to hide any longer because he wants to see something else in his life.=Ves a un gigante anciano. Durante muchos años vivió sólo y tuvo que esconderse ¡porque era cazado por los humanos! Ahora es viejo y ha decidido no esconderse por más tiempo porque quiere ver otra cosa más en su vida. + +master giant= Maestre gigante +You see a master giant with dark black eyes. His blood pumps loudly into his heart.= + +black giant=gigante negro +You see a tall black giant. His armor and sword will hit you hard. Be prepared!= + +imperial general giant=general imperial gigante +You see an imperial general giant. He lurks in caves with his comrades to take you down.= + +kasarkutominubat=kasarkutominubat +You see kasarkutominubat, a creature not known to many, told it was a human champion long ago, now it resembles nothing but that, with a mask bearing thousands of souls on it and hands running with nothing but blood.= + + +## gnome + +gnome=gnomo +You see a gnome who lives in the mountains.=Ves a un gnomo que vive en las montañas. + +mage gnome=mago gnomo +You see a mage gnome, she cleans the dirty mountain.=Ves a una maga gnoma, ella limpia la sucia montaña. + +infantry gnome=gnomo de infantería +You see an infantry gnome who will protect his little town against the humans!=Ves a un gnomo de infantería que protegerá su pequeña aldea frente a ¡los humanos! + +cavalryman gnome=soldado de caballería gnomo +You see a cavalryman gnome who does long travels to search for gold.=Ves a un soldado de caballería gnomo que realiza largos viajes en busca de oro. + +giant gnome= + +giant gnomess= + + +## goblin + +goblin=goblin +You see a goblin, it is an evil and mischievous creature.=Ves a un goblin, es una criatura malvada y maliciosa. + +veteran goblin=veterano goblin +You see a veteran goblin, an old warrior...=Ves a un goblin veterano, un viejo guerrero... + +soldier goblin=soldado goblin +You see a soldier goblin, a young warrior.=Ves a un soldado goblin, un joven guerrero. + + +## golem + +dwarf golem=golem enano +You see a dwarf golem. A long time ago a dwarf got turned into a big rock by some mystical forces. To save their friend the other dwarves created a golem out of this rock! From this day the dwarf golem lives together with them.=Ves a un golem enano. Hace mucho tiempo un enano fue convertido en una gran roca por unas fuerzas místicas. Para salvar a su amigo los otros enanos crearon un golem ¡a partir de esta roca! Desde este día el golem enano convive con ellos. + +ice golem=golem de hielo +You see an Ice golem. Ice Golems are elementals made out of ice. You should watch out for its icy fists or you might get hurt.= + +stone golem=golem de piedra +You see a stone golem, it only exists to protect. What was it crafted for to protect, and what would one do if it lost its purpose?=Ves a un golem de piedra, existe solamente para proteger. ¿Qué fue diseñado para proteger, y qué haría si perdiese su propósito? + +wooden golem=golem de madera +You see a wooden golem. The scroll with ancient words in his head keeps it walking.=Ves a un golem de madera. El pergamino con las antiguas palabras en su cabeza lo mantiene caminando. + + +## human + +kalavan housewife=ama de casa kalavan +You see a kalavan housewife. She lives in Kalavan city and she is very angry with rude people who want to steal her food out of the kitchen and destroy her beautiful flower garden!=Ves a una ama de casa kalavan. Vive en la ciudad de Kalavan y ¡se enfada mucho con la gente ruda que quiere robar comida de su cocina y destruir su precioso jardín! + +kalavan peasant=campesino kalavan +You see a kalavan peasant. He works in the fields around Kalavan city. As he heard there were strangers in the city he came to help the kalavan citizens in fighting the enemies!=Ves un campesino kalavan. Trabaja en los campos de alrededor de la ciudad de Kalavan. Cuando oyó que había extranjeros en la ciudad vino a ayudar a los ciudadanos de kalavan a luchar contra los enemigos! + +kalavan wisewoman=mujer sabia de kalavan +You see a kalavan wisewoman. Normally she sits in her small room and does some handworking but now she wants to have some action!=Ves a una mujer sabia de kalavan. Normalmente se sienta en su pequeña habitación y hace labores manuales pero ahora quiere tener algo de acción! + +kalavan citizen=ciudadano de kalavan +You see a kalavan citizen. He lives in kalavan city and tries to protect it from invaders!=Ves a un ciudadano de kalavan. Vive en la ciudad de kalavan ¡e intenta protegerla frente a los invasores! + +monk=monje +You see a monk. Monks are very calm and peaceful. They live alone among themselves, but they don't want to be interrupted by anyone.= + +darkmonk=monje oscuro +You see a Darkmonk. Darkmonks can control dark powers, and they will do anything to get you out of their way!= + +devil monk=monje del diablo +You see a devil monk. He is a member of a devil cult which practises black rituals.=Ves a un monje del diablo. Es miembro de un culto al diablo que practica negros rituales. + +littlewitch=brujita +You see a little witch. These little girls are witches and that makes them very dangerous.=Ves a una brujita. Estas niñitas son brujas y eso las hace muy peligrosas. + +littlewizard=pequeño mago +You see a little wizard. Although he is a child, he is already able to cast powerful spells.=Ves a un pequeño mago. Aunque es un crío, ya es capaz de lanzar poderosos hechizos. + + +## human_amazon + +amazoness archer=arquera amazona +You see an amazoness archer. She handles her bow quite well and her arrows are very dangerous when she shoots them at you out of the forest!=Ves a una arquera amazona ¡Maneja su arco bastante bien y sus flechas son muy peligrosas cuando las dispara hacia ti desde el bosque! + +amazoness archer commander=arquera comandante amazona +You see an amazoness archer commander. She is the leader of amazoness archers. Her bow skills are incredible and her arrows pierce everything.= + +amazoness bodyguard=guardaespaldas amazona +You see an amazoness bodyguard who will guarantee the safety to the people of Amazon island.= + +amazoness coastguard=guardacostas amazona +You see an amazoness coastguard. She protects the coasts of Amazon Island from intruders. She is not too happy to see you running in her island.= + +amazoness coastguard mistress=guardacostas Maestre amazona +You see an amazoness coastguard mistress. Even if they are women, these blue haired mistresses should not be underestimated.= + +amazoness commander=comandante amazona +You see an amazoness commander who is specialised in eliminating any strangers who dare to take a step to her island.= + +amazoness elite coastguard=guardacostas de élite amazona +You see an amazoness elite coastguard. When it is her shift to protect the island, you would just wish to have stayed at home.= + +amazoness hunter=cazadora amazona +You see an amazoness hunter. She creeps through the forest to hunt animals. But she doesn't like that you are in her hunting area!=Ves a una cazadora amazona ¡Se arrastra por el bosque para cazar animales ¡Pero no le gusta que estés en su zona de caza! + +amazoness vigilance=vigilante amazona +You see an amazoness vigilance. Her senses are well tuned, so you have to be very silent when you don't want her to hear you!=Ves a una vigilante amazona ¡Sus sentidos están muy desarrollados, así que tienes que ser muy silencioso cuando no quieras que te oiga! + +amazoness imperator=imperator amazona +You see an amazoness imperator. She is the emperor of Amazon island and has ruled it for decades and won't let you get past her very easily.= + + +## human_assassin + +bandit=bandido +You see a lean and mean bandit. He's out to get your life and purse!=Ves a un bandido preparado y listo para actuar ¡Hará lo que sea necesario para llevarse tu vida y tu dinero! + +disciple assassin=discípulo de asesino +You see a disciple assassin. He is quite young but he has learned to fight and kill already.=Ves a un discípulo de asesino. Es bastante joven pero ya ha aprendido a luchar y a matar. + +apprentice assassin=aprendiz de asesino +You see an apprentice assassin. He has been taught some very awesome moves and methods, so he is already able to kill an enemy almost perfectly!=Ves a un aprendiz de asesino. Le han enseñado algunos movimientos y métodos muy impresionantes, así que ya es capaz de matar a un enemigo casi perfectamente! + +hunter=cazador + +assassin=asesino +You see an assassin. He is very good at killing his victims silently without being seen by anyone.=Ves a un asesino. Es muy bueno matando a sus victimas silenciosamente sin ser visto por nadie. + +ninja=ninja +Your keen eyes made you able to see this fast ninja. Prepare for a fight!= + +samurai=samurai +If this samurai failed to uphold his honor, he could only regain it by performing seppuku.= + +shadows assassin=asesino de las sombras +You see a shadows assassin. He knows how to become one with the shadows and to move in the dark without being seen or making any sound.=Ves a un asesino de las sombras. Sabe como hacerse uno con las sombras y moverse en la oscuridad sin ser visto o hacer ningún sonido. + +master assassin=Maestre asesino +You see a master assassin. He is a master of assassination! He is able to kill an enemy without letting him know that he is already there!=Ves a un Maestre asesino ¡Es un maestro del asesinato! ¡Es capaz de matar a un enemigo sin dejar que sepa que ya está allí! + + +## human_barbarian + +barbarian=bárbaro + +barbarian wolf=lobo bárbaro + +barbarian elite=bárbaro de élite + +barbarian priest=sacerdote bárbaro + +barbarian chaman=chamán bárbaro + +barbarian leader=líder bárbaro + +barbarian king=rey bárbaro + + +## human_blordrough + +blordrough quartermaster=intendente de blordrough +You see a man who gave up his life as a puppet of the Blordrough army, all for the love of dragons. He is wonderfully happy.= + +blordrough corporal=cabo de blordrough +You see a man who feels his past indicates how he should be treated now but desperately wishes to exercise his intelect in a game of chess.= + +blordrough storm trooper=soldado de asalto de blordrough +You see a man who is but a fraction of his former glory as one of the most feared foes of Faiumoni. Now, his main concern is keeping his house clean and tidy.= + +blordrough soldier= +You see a man who is fast and strong.= + +blordrough elite= +You see a man who is aging, but he knows how to fight.= + +blordrough infantry= +You see a man who has flaming red hair and red beady eyes.= + +blordrough captain= +You see a seasoned warrior.= + +blordrough general= +You see a lean, mean, fighting machine.= + + +## human_cannibal + +cannibal woman=mujer caníbal +You see a cannibal woman. She wants to make a soup out of your bones!=Ves a una mujer caníbal ¡Quiere hacer una sopa con tus huesos! + +cannibal=caníbal +You see a cannibal. He hunts you for cooking and eating your fresh meat! It's useless to tell him that your flesh is not good, better fight him or run away!=Ves a un caníbal. Te caza para cocinar y comerse tu carne fresca! + +elder cannibal=caníbal anciano +You see an elder cannibal. For years it has eaten humans. It will not stop until its hunger is satisfied.= + +cannibal warrior= +You see a cannibal warrior.= + + +## human_chaos + +chaos soldier=soldado del caos +You see a chaos soldier. Chaos soldiers are servants of the evil. Killing you is one of their evil plans.= + +chaos warrior=guerrero del caos +You see a chaos warrior. This chaotic creature is fighting with a big blade to slay his enemies.= + +chaos commander=comandante del caos +You see a chaos commander who is working on an evil plan. You should take care of him before he fulfills his duty.= + +chaos sorcerer=hechicero del caos +You see a chaos sorcerer. Chaos sorcerers will cast some evil spells on you if you don't obey them.= + +chaos dragonrider=jinete de dragón del caos +You see a chaos dragonrider. He is very well trained in dragon riding, but he can also fight well on the ground.=Ves a un jinete de dragón del caos. Está muy bien entrenado en montar a dragón, pero también puede luchar bien en el suelo. + +chaos lord=señor del caos +You see a chaos lord who is one of the strongest chaos creatures in Faiumoni. You should prepare well before attacking him.= + +chaos overlord=gran señor del caos +You see a chaos overlord. It is him who directs the creation of evil plans and gives orders to all chaotic creatures which exist.=Ves a un gran señor del caos. Es él quien dirige la creación de los planes malvados y da las órdenes a todas las criaturas que existen. + + +## human_imperial + +imperial defender=defensor imperial +You see an imperial defender. He and his comrades won't let you walk into their territory easily.= + +imperial veteran=veterano imperial +You see an imperial veteran. He fought in many battles and wars. Now he is old, but over the years he always looked for a new enemy to fight against.=Ves a un veterano imperial. Luchó en muchas batallas y guerras. Ahora es viejo, pero a lo largo de los años siempre ha buscado un nuevo enemigo contra el que luchar. + +imperial archer=arquero imperial + +imperial priest=sacerdote imperial +You see an imperial priest. The church has fallen out of favor with the citizens as of late, but this imperial priest still clings to the faith.= + +imperial elite guardian=guardián de élite imperial +You see an Imperial Elite Guardian. He is a very well trained guardian and he will protect his environment until the end.= + +imperial scientist=científico imperial +You see an imperial scientist. He is quite clever but also a little bit crazy and unfortunately he uses his intelligence for evil experiments.=Ves a un científico imperial. Es bastante listo pero también un poquito loco y desafortunadamente usa su inteligencia para experimentos malvados. + +imperial high priest=sumo sacerdote imperial +You see an imperial high priest who's troubled by the actions of the imperial science department. Be careful, he might take those frustrations out on you!= + +imperial archer leader=líder arquero imperial + +imperial elite archer=arquero de élite imperial + +imperial leader=líder imperial +You see a well dressed imperial leader. Its troops will try to dash you.= + +imperial knight=caballero imperial + +imperial chief=jefe imperial + +imperial commander=comandante imperial + +imperial general=general imperial +You see an imperial general with a huge scar above his eye. He is still strong, don't think that he will not see you!= + +emperor dalmung=emperador dalmung +You see emperor dalmung who stands before you. Once a powerful warlock trained by a mighty magician, but he disappeared decades ago. Now it seems he's in league with Ivan Abe!= + + +## human_madaram + +madaram archer=arquero madaram + +madaram peasant=campesino madaram + +madaram trooper=soldado de asalto madaram + +madaram soldier=soldado madaram + +madaram healer=sanador madaram + +madaram axeman=hachero madaram + +madaram queen=reina madaram + +madaram hero=héroe madaram + +madaram windwalker=windwalker madaram + +madaram cavalry=caballería madaram + +madaram stalker=acechador madaram + +madaram buster blader=buster blader madaram + + +## human_mithrilbourgh + +mithrilbourgh soldier=soldado de mithrilbourgh + +mithrilbourgh archer=arquero de mithrilbourgh + +mithrilbourgh knight=caballero de mithrilbourgh + +mithrilbourgh elite archer=arquero de élite de mithrilbourgh + +mithrilbourgh sergeant=sargento de mithrilbourgh + +mithrilbourgh lieutenant=lugarteniente de mithrilbourgh + +mithrilbourgh colonel=coronel de mithrilbourgh + +mithrilbourgh wizard=mago de mithrilbourgh + +mithrilbourgh general=general de mithrilbourgh +You see a mithrilbourgh general. Some call him Hawkeyes. He cast his gaze over the city walls and saw you coming from miles away.= + + +## human_risecia + +risecia thug=matón risecia +You see a risecia thug, stay away from him!=Ves a un matón risecia, ¡mantente alejado de él! + +risecia archer=arquero risecia +You see a risecia archer, just one of the warriors who wants to protect his chief.=Ves a un arquero risecia, sólo uno más de los guerreros que quieren proteger a su jefe. + +risecia swordsman=espadachín risecia +You see a risecia swordsman, the blade of his sword will cut your head!=Ves a un espadachín risecia, ¡la hoja de su espada te cortará la cabeza! + +Chief Falatheen of the Risecia=Jefe Falatheen de los Risecia +You see the Chief Falatheen of the Risecia, he doesn't want to die! His little warriors protect him against invaders.=Ves al Jefe Falatheen de los Risecia, ¡no quiere morir! Sus pequeños guerreros le protegen frente a los invasores. + + +## humanoid + +robot aruthon=robot aruthon + +dark aruthon=aruthon oscuro + +leprechaun=leprechaun +You see a leprechaun, you would be lucky if you found his treasure.=Ves a un leprechaun, tendrías suerte si encontrases su tesoro. + +littlefairy=hadita +You see a little fairy, which is magical and nice.=Ves a una hadita, que es mágica y preciosa. + +clurichaun=clurichaun +You see a clurichaun. He is rich, but you don't know if you are able to find his gold!=Ves a un clurichaun. Es rico, pero no sabes si serás capaz de encontrar su oro! + +brain ahouga=cerebro ahouga +You see a brain ahouga, a very strange creature only living in your dreams!=Ves a un cerebro ahouga, ¡una criatura muy extraña que sólo vive en tus sueños! + +blue mohiko=mohiko azul +You see a blue mohiko. A brown alien with a large blue mohawk.= + +red roohako=roohako rojo +You see a red roohako. It is coming for you in your deepest dreams.= + + +## hybrid + +arachne=arachne +You see Arachne, a horrid half woman half spider. With so many legs she can hit you very fast, and her poisonous bite is fierce.=Ves a Arachne, un ser horrible mitad mujer mitad araña. Con tantas piernas puede golpearte muy rápido, y su mordedura venenosa es feroz. + +frogman=hombre-rana +You see a frogman which lives in a lake which isn't very deep.=Ves a un hombre-rana que vive en un lago no muy profundo. + +elite frogman=hombre-rana de élite +You see an elite frogman. His huge eyes let you shiver!= + +wizard frogman=mago hombre-rana +You see a mighty wizard frogman. His wand and magical cloak show his wisdom and power.= + +minotaur=minotauro +You see a minotaur. He escaped from his labyrinth and is very angry now!=Ves a un minotauro ¡Ha escapado de su laberinto y ahora está muy enfadado! + +minotaur king=rey minotauro +You see a Minotaur King. He doesn't have to escape from his labyrinth to be very angry...= + +naga=naga +You see a naga. She awaits unwary victims to whom she may give her deadly kiss.= + +high naga=naga suprema + +armed naga=naga con brazos + +lamia= + +ratman=hombre rata +You see a ratman. He is a mutation that comes from old times.=Ves a un hombre rata. Es una mutación que procede de tiempos remotos. + +ratwoman=mujer rata +You see a ratwoman. She is a mutation of women with the rats in the century XVIII.=Ves a una mujer rata. Es una mutación de mujeres con ratas en el siglo XVIII. + +archrat=archirrata +You see an archrat. You will be distracted with its aura, be careful...=Ves una archirrata. Te distraerás con su aura, ten cuidado... + +sheepman=hombre oveja + +armored sheepman=hombre oveja blindado + +elder sheepman=hombre oveja anciano + +elite sheepman=hombre oveja de élite + +xenocium=xenocium + + +## insect + +killer bee= +You see a killer bee which will sting you to death!= + +tiny scarab= +You see a tiny scarab. It is an insect familiar to desert ecologies that will eat almost anything, including dung. Its hard carapace provides protection from the sun and being crushed, so don't think a little stomp will stop it.= + +water scarab= + + +## kobold + +kobold=kobold +You see a kobold. People say that it is a mythical spirit.=Ves a un kobold. La gente dice que es un espíritu mítico. + +soldier kobold=soldado kobold +You see a soldier kobold, he's protecting his king.=Ves a un soldado kobold, está protegiendo a su rey. + +archer kobold=arquero kobold +You see an archer kobold. It runs away from humans.=Ves a un arquero kobold. Sale corriendo de los humanos. + +veteran kobold=veterano kobold +You see a veteran kobold, it is mighty!=Ves a un veterano kobold, ¡es poderoso! + +leader kobold=líder kobold +You see a leader kobold, one of the mighty warriors.=Ves a un líder kobold, uno de los poderosos guerreros. + +giant kobold=kobold gigante +You see a giant kobold. Though generally playful it doesn't quite know its own strength. When it asks for a hug, it's best to decline that offer.=Ves a un kobold gigante. Aunque por lo general es juguetón no conoce su propia fuerza. Cuando pide un abrazo, es mejor rechazar la oferta. + + +## mutant + +cat woman=mujer gata +You see a cat woman. She's agile, stealthy, fast and sharp, so stay focused and don't get cut!= + +dark mutant=mutante oscuro + +glow monster=monstruo del resplandor +You see a glow monster. Its gleam in the dark sewers fightens you.= + +imperial experiment=experimento imperial +You see an imperial experiment which mirrors the fate of many of science departments test subjects. Its skin glows and standing before it taxes your body.= + +imperial mutant=mutante imperial + +poison shroom= +You see a toadstool... spitting poison at you?= + +werewolf=hombre lobo +You see a werewolf. Normally he is a human but at fullmoon nights he turns into a dangerous beast! His poison seems to last forever.=Ves a un hombre lobo. Normalmente es un humano pero ¡en las noches de luna llena se convierte en una bestia peligrosa! Su veneno parece durar para siempre. + + +## mythical_being + +mermaid= +You see a mermaid. They perfer their own company and are not too happy to see you here.= + +emerald mermaid= +You see an emerald mermaid.= + +amethyst mermaid= +You see an amethyst mermaid.= + +ruby mermaid= +You see a ruby mermaid.= + +sapphire mermaid= +You see a sapphire mermaid.= + +unicorn=unicornio +You see a rare unicorn. It is widely believed to be only a myth.=Ves un raro unicornio. En general se cree que es sólo un mito. + +white unicorn= + +black unicorn= + +golden unicorn= + +pegasus=pegaso +You see Pegasus, the winged horse. His name means Lightning and he carries bolts of Thunder.=Ves a Pegasus, el caballo alado. Su nombre significa Relámpago y lleva los rayos del Trueno. + +baby pegasus= + +bronze pegasus= + +ivory pegasus= + +ebony pegasus= + +centaur=centauro +You see a noble centaur, half man, half horse. He looks capable of great damage but thankfully seems to mean you no harm.=Ves a un noble centauro, mitad hombre, mitad caballo. Parece capaz de causar gran daño pero afortunadamente no parece tener intención de herirte. + +solar centaur= + +glacier centaur= + +centaur king= + + +## ogre + +ogre=ogro +You see an ogre, it is nude and rude!=Ves a un ogro, ¡está desnudo y es rudo! + +elder ogre=ogro anciano +You see an elder ogre, he's in the last moments of his life, take pity on him.=Ves a un ogro anciano, está en los últimos momentos de su vida, ten compasión de él. + +ogre soldier=soldado ogro +You see an ogre soldier, smelling like it didn't have had a bath for years. Ewww.=Ves a un soldado ogro, huele como si no se hubiese bañado durante años. Puaj! + +superogre=superogro +You see a superogre which belongs to the family of very material creatures, the Ogres. Ever since this one happened upon a peculiar helmet and has been revered among its peers. Take care, it throws a mean punch!= + + +## oni + +oni warrior=guerrero oni +You see an oni warrior. His purple skin color should not lull you into a false sense of security!= + +oni archer=arquero oni +You see an oni archer. His arrows will follow you everywhere for protecting his queen!= + +oni priest=sacerdote oni +You see a mysterious oni priest, uttering strange incantations.=Ves a un misterioso sacerdote oni, pronunciando extraños encantamientos. + +oni king=rey oni +You see the oni king. His power leads brave oni troops around his kingdom.= + +oni queen=reina oni +You see the oni queen. She supports her husband and takes an eye onto their kingdom while he conquers new land and fights on battlefields.= + + +## orc + +orc=orco +You see an orc which is noisy and silly.=Ves a un orco que es ruidoso y tonto. + +orc spearman=lancero orco +You see an orc spearman. Run away fast before a spear pierces you.=Ves a un lancero orco. Sal corriendo rápido antes de que una lanza te atraviese. + +orc hunter=cazador orco +You see an orc hunter. It it seeking for you!=Ves a un cazador orco ¡Te está buscando a ti! + +orc warrior=guerrero orco +You see an orc warrior which doesn't like you searching in the mountain.=Ves a un guerrero orco al que no le gusta que andes buscando en la montaña. + +orc chief=jefe orco +You see an orc chief which is too busy to attend to you!=Ves a un jefe orco ¡que está demasiado ocupado para atenderte! + +mountain orc=orco de montaña +You see a mountain orc. Not very cunning but his robustness, stench and loudness could still overwhelm you!= + +mountain orc warrior=guerrero orco de montaña +You see a mountain orc warrior. Not very cunning but you should not underestimate his robustness and stench!= + +mountain orc hunter=cazador orco de montaña +You see a mountain orc hunter. He's in search for raw meat... You could be his next lunch!= + +mountain orc chief=jefe orco de montaña +You see a mountain orc chief. He's proud, loud and reeks like a thousand orcs together...= + +golden orc= + + +## reptile + +grass snake=serpiente de hierba +You see a grass snake which is quite dangerous.=Ves una serpiente de hierba que es bastante peligrosa. + +snake=serpiente +You see a snake, it came from the forest to poison you!=Ves una serpiente, ¡vino desde el bosque a envenenarte! + +cobra= + +king cobra= +You see a king cobra. It is extremely venomous.= + +caiman=caimán +You see a caiman, which is hidden in the water.=Ves un caimán, que está escondido en el agua. + +crocodile=crocodilo +You see a crocodile, it arises from the deep waters in the rivers.=Ves un crocodilo, surge de las aguas profundas de los ríos. + +turtle= +You see a turtle swimming quietly in the water. It doesn't want to be disturbed.= + + +## rodent + +mouse=ratón +You see a small mouse that seems to be very hungry.=Ves un pequeño ratón que parece estar muy hambriento. + +rat=rata +You see a small rat that seems to be extremely hungry.=Ves una pequeña rata que parece estar extremadamente hambrienta. + +caverat=rata de cueva +You see a caverat which is dirty and loves the dungeons.=Ves una rata de cueva que es sucia y le encantan las mazmorras. + +venomrat=rata de veneno +You see a venomrat, extremely poisonous.=Ves una rata de veneno, extremadamente venenosa. + +zombie rat=rata zombi +You see a hungry zombie rat and it doesn't like to see you.=Ves una hambrienta rata zombi y a ella no le gusta verte a ti. + +razorrat=rata de cuchilla +You see a razorrat, its noise will make you crazy!=Ves una rata de cuchilla, ¡su ruído te hará perder la cabeza! + +giantrat=ratagigante +You see a giant rat. It seems like it survived from an experiment which went wrong.=Ves una rata gigante. Parece que haya sobrevivido a un experimento que salió mal. + +beaver=castor +You see a beaver. It likes to build dams in the river.=Ves un castor. Le gusta construir presas en el río. + + +## slime + +black slime=baba negra +You see a black slime, with a sticky trail behind it.=Ves una baba negra, que va dejando un rastro pegajoso. + +brown slime=baba marrón +You see a brown slime, sticky and hungry.=Ves una baba marrón, pegajosa y hambrienta. + +green slime=baba verde +You see a green slime, sticky and shiny.=Ves una baba verde, pegajosa y brillante. + +twilight slime=baba del crepúsculo +You see a twilight slime, so strange and beautiful. Why is it coming towards you? Runnnnnnnnn!=Ves una baba del crepúsculo, tan extraña y bella ¿Por qué se está acercando hacia ti? ¡Correeeeeeeee! + + +## spider + +spider=araña +You see a spider. It makes a giant spider web to trap you!=Ves una araña ¡Está haciendo una telaraña gigante para atraparte! + +poisonous spider=araña venenosa +You see a poisonous spider. Be careful! Its poison is very strong! It will make you very weak and can probably kill you!=Ves una araña venenosa ¡Ten cuidado! ¡Su veneno es muy fuerte! ¡Te debilitará y probablemente puede matarte! + +giant spider=araña gigante +You see a giant spider. It has very long legs which are quite dangerous.=Ves a una araña gigante. Tiene patas muy largas que son bastante peligrosas. + + +## troll + +cyclops=cíclope +You see a cyclops. It just has one eye.=Ves a un cíclope. Sólo tiene un ojo. + +troll=trol +You see a troll, be careful in his terrain.=Ves a un trol, ten cuidado en su terreno. + +red troll=trol rojo +You see a red troll, he's burning with anger.=Ves a un trol rojo, está quemando de ira. + +cave troll=trol de cueva + + +## undead + +ghost hound=perro fantasma +As you catch sight of its shaggy shape and gleaming evil eyes, you suddenly remember all the horror stories around the dreaded ghost hound!=Al avistar su forma peluda y sus malvados ojos brillantes, de repente recuerdas todas las historias de terror acerca del temido perro fantasma! + +death knight=caballero de la muerte +You see a death knight. Some time ago he fought in tournaments where his only aim was to kill as many enemies as possible.=Ves a un caballero de la muerte. Hace algún tiempo luchó en torneos donde su único objetivo era matar tantos enemigos como fuese posible. + +necrosophia=necrosophia +You see Necrosophia. She is a mighty beast, she will burn you with fire while you are disoriented by the rainbow beans.= + +green phantom=fantasma verde + + +## undead_lich + +lich=lich +You see a lich, a dark desolate creature which has been doomed to wander in endless turmoil, feeding on the souls of the righteous.=Ves a un lich, una criatura oscura solitaria que ha sido condenada a vagar en confusión sin fin, alimentándose de las almas de los justos. + +dead lich=lich muerto +You see a dead lich with his huge green and heavy cloak. Although he is dead and bony, he'll seek revenge for his too early passing and will always return fast and ruthless!= + +high lich=lich supremo +The terrible grinning skull of a high lich leers at you as it prepares to slaughter you in a myriad of magical ways.=La terrible sonrisa de la calavera de un lich supremo te mira de reojo al prepararse para matarte de una miríada de mágicas maneras. + + +## undead_mummy + +mummy=momia +You see a mummy which usually sleeps in a coffin.=Ves a una momia que suele dormir en un sarcófago. + +royal mummy=momia real +You see a royal mummy. You wonder what keeps this king's soul from finding peace.=Ves a una momia real. Te preguntas qué le impide al alma de este rey encontrar la paz. + + +## undead_skeleton + +skeleton=esqueleto +You see a skeleton which survives to get revenge.=Ves a un esqueleto que sobrevive para vengarse. + +tiny skelly=pequeño esqueletito +You see a tiny skelly. It is silly, little and evil, be careful with its poison...=Ves a un pequeño esqueletito. Es tonto, pequeño y malvado, ten cuidado con su veneno... + +warrior skeleton=guerrero esqueleto +You see a warrior skeleton, it's just a normal skeleton with a shield!=Ves a un guerrero esqueleto, ¡es sólo un esqueleto normal con un escudo! + +elder skeleton=esqueleto anciano +You see the bones of an old man. He lived to a ripe old age, and is sure to know a thing or two that separates him from all the vengeful skeletons that died young.= + +demon skeleton=esqueleto demonio +You see a demon skeleton. Demon skeletons have ascended from the underworld and they are going to take over this world. You really should slay them before it is too late!= + +gashadokuro=gashadokuro +A giant figure made of bone, known only as gashadokuro, towers before you. He is created from the bones of starving humans.=Una figura gigante hecha de hueso, conocida sólo como gashadokuro, se impone ante ti. Se ha creado a partir de los huesos de humanos muertos de hambre. + +fallen warrior=guerrero caído +You see a fallen warrior. Once he fought in great battles but now he has fallen to the depths of hell only to come back up to get his revenge.= + +fallen priest=sacerdote caído +You see a fallen priest. He was once just a regular human priest, until the dark powers corrupted him and turned him to the dark side.= + +fallen high priest=sumo sacerdote caído +You see a fallen high priest. It is manifested through the dark forces seized of this world.= + + +## undead_specter + +ghost=fantasma +You see a ghost, don't be afraid! It doesn't seem to be evil.=Ves un fantasma, ¡no tengas miedo! No parece ser malvado. + +death=muerte +You see Death. Do you have the will to stare him in the face? Today he's not in the mood to bargain with your soul.=Ves a la Muerte ¿Tendrás el valor de mirarla a la cara? Hoy no está de humor para negociar con tu alma. + +black death=muerte negra +You see a black death. With his scythe he reaps your soul and then he takes it over the river where it has to stay until judgement day arrives.=Ves una muerte negra. Con su guadaña siega tu alma y después se la lleva al río donde tiene que permanecer hasta que llegue el día del juicio. + +golden death=muerte de oro +You see the golden death. He came to take your soul away to pay for all your sins.=Ves a la muerte de oro. Vino a llevarse tu alma para que pague por todos tus pecados. + +invisible man=hombre invisible +You see nothing but the landscape. As you stare at the point where you think you have seen something, you suddenly see the slight contour of a man!=No ves nada excepto el paisaje. Al mirar fijamente al punto donde crees haber visto algo, de repente ¡ves el ligero contorno de un hombre! + +incorporeal armor=armadura incorpórea +You see an incorporeal armor. You are quite surprised as it walks towards you because there is nobody in it.=Ves una armadura incorpórea. Te sorprende bastante al caminar hacia ti porque no hay nadie dentro de ella. + + +## undead_vampire + +vampire bride=novia vampira +You see a vampire bride. She was resurrected from the dead by a wicked vampire lord.=Ves a una novia vampira. Fue resucitada de entre los muertos por un malvado señor vampiro. + +vampirette=vampiresa +You see a Vampirette. Beware of its sharp teeth! She wants to drink your blood and turn you into a vampire!= + +vampire lord=señor vampiro +You see a vampire lord. Be careful, he is after your blood!=Ves a un señor vampiro. Ten cuidado, ¡va tras tu sangre! + + +## undead_zombie + +bloody zombie=zombi sangriento +You see a bloody zombie. Can you see the drops of blood which fall from his body?=Ves a un zombi sangriento. ¿Puedes ver las gotas de sangre que caen de su cuerpo? + +zombie=zombi +You see a zombie. It woke up just to take revenge, be silent.=Ves a un zombi. Se despertó solo para vengarse, estate en silencio. + +headless monster=monstruo sin cabeza +You see a headless monster, how can he hear that we come to him?=Ves a un monstruo sin cabeza, ¿Cómo ha podido oír que nos acercábamos a él? + +rotten zombie=zombi podrido +You see a rotten zombie. You are disgusted as you see its rotten skin that hangs down in pieces from his half rotten bones.=Ves a un zombi podrido. Estás asqueado al ver su piel podrida que cuelga hecha jirones de sus huesos medio podridos. + + +# strings not found in template + +rift cloak=capa de rift +You see a rift cloak. Almost useless in the battle, but saves your life when you enter a magical rift.=Ves una capa de rift. Casi inútil en la batalla, pero te salva la vida cuando entras en un rift mágico. +necromancers staff=baston de nigromante +You see a cunningly beautiful staff. If you listen carefully you can hear its previous owners' souls screaming from within it.=Ves un bastón engañosamente hermoso. Si escuchas atentamente puedes oir las almas de sus anteriores dueños gritar desde dentro. bottle=botella You see a bottle. It is empty, someone drank the contents.=Ves una botella. Está vacía, alguien se bebió su contenido. big bottle=botella grande You see a big bottle.=Ves una botella grande. -eared bottle=botella con asas -You see an eared bottle. It is empty, someone drank the contents.=Ves una botella con asas. Está vacía, alguien se bebió su contenido. -fat bottle=botella gruesa -You see a fat bottle. It is empty, someone drank the contents.=Ves una botella gruesa. Está vacía, alguien se bebió su contenido. short rounded bottle=botella redondeada baja You see a short rounded bottle. It is empty, someone drank the contents.=Ves una botella redondeada baja. Está vacía, alguien se bebió su contenido. short triangular bottle=botella triangular baja You see a short triangular bottle. It is empty, someone drank the contents.=Ves una botella triangular baja. Está vacía, alguien se bebió su contenido. -slim bottle=botella estrecha -You see a slim bottle. It is empty, someone drank the contents.=Ves una botella estrecha. Está vacía, alguien se bebió su contenido. squared bottle=botella cuadrada You see a squared bottle. It is empty, someone drank the contents.=Ves una botella cuadrada. Está vacía, alguien se bebió su contenido. tall bottle=botella alta. You see a tall bottle. It is empty, someone drank the contents.=Ves una botella alta. Está vacía, alguien se bebió su contenido. triangular bottle=botella triangular You see a triangular bottle. It is empty, someone drank the contents.=Ves una botella triangular. Está vacía, alguien se bebió su contenido. - -empty goblet=copa vacía -You see an empty goblet. You wonder what could it hold.=Ves una copa vacía. Te preguntas que podría contener. -goblet=copa -You see a goblet. You look at its contents, and shiver at the blood inside.=Ves una copa. Miras su contenido, y sientes escalofríos al ver sangre dentro. -keyring=llavero -You see a keyring.=Ves un llavero. -note=nota -You see a note.=Ves una nota. -coupon=cupón -You see a coupon.=Ves un cupón. -closed envelope=sobre cerrado -opened envelope=sobre abierto -sealed envelope=sobre sellado -unsealed envelope=sobre sellado abierto -assassins id=identificación de asesino -You see an assassin's ID. Perhaps this can be put to good use.=Ves una identificación de asesino. Quizá puedas hacer un buen uso de ella. -map=mapa blank scroll=pergamino en blanco You see a rolled up blank scroll.=Ves un pergamino enrollado y en blanco ice scroll=pergamino de hielo You see a scroll, circled by ice crystals. You can feel its cold aura.=Ves un pergamino, rodeado por cristales de hielo. Puedes sentir su fría aura. -fierywater=aguardiente -You see a bottle of fierywater. Do not drink it pure, you would evaporate!=Ves una botella de aguardiente. No te la bebas pura, ¡Te evaporarías! -mild koboldish torcibud=suave torcibud koboldiano -You see a bottle of mild koboldish torcibud. It will help you recover when you're all bruised and battered.=Ves una botella de suave torcibud koboldiano. Te ayudará a recuperarte cuando estés todo magullado. -strong koboldish torcibud=fuerte torcibud koboldiano -You see a bottle of strong koboldish torcibud. It will help you recover when you're all bruised and battered.=Ves una botella de fuerte torcibud koboldiano. Te ayudará a recuperarte cuando estés todo magullado. -vsop koboldish torcibud=torcibud koboldiano de estaca vieja muy especial -You see a bottle of VSOP koboldish torcibud, specially aged in oak casks wrapped by the darkness of a dungeon cellar. It will help you recover when you're all bruised and battered.=Ves un torcibud koboldiano de estaca vieja muy especial, especialmente envejecida en barricas de roble envueltas por la oscuridad de la bodega de una mazmorra. Te ayudará a recuperarte cuando estés todo magullado. -apple juice=zumo de manzana -You see apple juice. On a hot summer day, it would taste great.=Ves zumo de manzana. En un día de verano caluroso, sabría estupendo. -beer=cerveza -You see a beer. On a hot summer day, it would taste great.=Ves una cerveza. En un día de verano caluroso, sabría estupendo. -wine=vino -You see a glass of red wine. It will quench your thirst.=Ves un vaso de vino tinto. Saciará tu sed. -water=agua -You see a bottle of fresh spring water. It's really tasty and fresh.=Ves una botella de agua fresca de manantial. Es realmente sabrosa y fresca. -pina colada=piña colada -You see a pina colada. It looks delicious but tastes dangerously strong!.=Ves una piña colada ¡Parece deliciosa pero sabe peligrosamente fuerte! -tea=té -You see a lovely cup of tea.=Ves una encantadora taza de té. -milk=leche -You see a small bottle of fresh milk.=Ves una pequeña botella de leche fresca. -antidote=antídoto -You see an antidote, to cure you of poison and protect you for a while.=Ves un antídoto, para curarte de venenos y protegerte durante un rato. -greater antidote=antidoto mayor -You see a greater antidote, to cure you of poison and protect you for a long while.=Ves un antidoto mayor, para curarte de venenos y protegerte durante un largo rato. -poison=veneno -You see a bottle of poison. Don't drink it!=Ves una botella de veneno ¡No te la tomes! -greater poison=veneno mayor -You see a greater poison. Don't drink it!=Ves un veneno mayor ¡No te lo tomes! -deadly poison=veneno mortal -You see a deadly poison. Don't drink it!=Ves un veneno mortal ¡No te lo tomes! -disease poison=veneno de enfermar -You see a disease poison. Be careful, it's wicked.=Ves un veneno de enfermar. Ten cuidado, es malvado. -mega poison=mega veneno -You see a mega poison. This is some wicked poison.=Ves un mega veneno. Éste es un veneno malvado. -twilight elixir=elixir del crepúsculo -You see a bottle of medicinal elixir.=Ves una botella de elixir medicinal. -minor potion=poción menor -You see a minor potion. It's not much but may save you in a tight spot.=Ves una poción menor. No es mucho pero puede salvarte en una situación difícil. -soup=sopa -You see a bowl of soup. Its contents fill you up.=Ves un plato de sopa. Su contenido te llena. -fish soup=sopa de pescado -You see a bowl of fish soup. Its contents fill you up.=Ves un plato de sopa de pescado. Su contenido te llena. -potion=poción -You see a potion. Take it to battle and you may survive longer.=Ves una poción. Llevatela a la batalla y puedes sobrevivir por más tiempo. -greater potion=poción mayor -You see a greater potion, invaluable in battle.=Ves una poción mayor, inestimable en la batalla. -mega potion=mega poción -You see a mega potion. It strengthens greatly.=Ves una mega poción. Te fortalece en gran medida. -love potion=poción de amor -You see a love potion. It looks powerful so take care to use it carefully!=Ves una poción de amor ¡Parece potente así que ten cuidado de usarla con precaución! -rhosyd=rhosyd -You see a beautiful and rare rhosyd flower.=Ves una bella y rara flor de rhosyd. -rose=rosa -You see a single red rose. It smells heavenly.=Ves una sola rosa roja. -lilia=lilia -You see a lilia, a single precious bloom.=Ves una lilia, una sola flor preciosa. -daisies=margaritas -You see a bunch of daisies. They are cheap and cheerful!=Ves un ramo de margaritas ¡Son baratas y alegran! -pansy=pensamiento -You see a pansy, so colorful and beautiful.=Ves un pensamiento, tan colorido y hermoso. -zantedeschia=zantedeschia -You see a zantedeschia, a delicate bloom with the sweetest fragrance.=Ves una zantedeschia, una flor delicada con la más dulce de las fragancias. -cherry=cereza -You see a cherry. Its tangy taste revives you a little.=Ves una cereza. Su sabor amargo te revive un poco. -butter=mantequilla -You see some butter. It is an ingredient or can be eaten if you like.=Ves un poco de mantequilla. Es un ingrediente o se puede comer si quieres. -cheese=queso -You see some cheese. Eat it and see what happens.=Ves un poco de queso. Cometelo y verás lo que pasa. -coconut=coco -You see a coconut. Luckily it's already cracked and you can drink from it.=Ves un coco. Afortunadamente ya está abierto y puedes beber de él. -icecream=helado -You see a wafer cone with vanilla ice cream and a chocolate flake.=Ves un cucurucho de helado de vainilla y virutas de chocolate. -pineapple=piña -You see a tropical pineapple.=Ves una piña tropical. -artichoke=alcachofa -You see an artichoke. It can be an ingredient, or you can eat it.=Ves una alcachofa. Puede ser un ingrediente, o te la puedes comer. -carrot=zanahoria -You see a carrot. It can be an ingredient, or you can eat it.=Ves una zanahoria. Puede ser un ingrediente, o te la puedes comer. -salad=ensalada -You see a salad.=Ves una ensalada. -tomato=tomate -You see a tomato, put it in a salad or eat it.=Ves un tomate, ponlo en la ensalada o cometelo. -apple=manzana -You see a nice shiny apple.=Ves una bonita y brillante manzana. -black apple=manzana negra -You see a black apple. It must be a gift from lil Johnnnny. He's the guy with black items.=Ves una manzana negra. Debe de ser un regalo del pequeño Johnnny -bread=pan -You see a loaf of bread. It is freshly baked and ready to eat.=Ves una barra de pan. Está recién horneada y lista para comer. -button mushroom=champiñón común -You see a button mushroom. It can be an ingredient, or you can eat it.=Ves un champiñón común. Puede ser un ingrediente, o te lo puedes comer. -porcini=seta calabaza -You see a porcini. It can be an ingredient, or you can eat it.=Ves una seta calabaza. Puede ser un ingrediente, o te la puedes comer. -spinach=espinacas -You see spinach. It is enriched with vitamins.=Ves espinacas. Está enriquecida con vitaminas. -courgette=calabacín -You see a courgette. It can be an ingredient, or you can eat it.=Ves un calabacín. Puede ser un ingrediente, o te lo puedes comer. -collard=col -You see a collard. It's rare and strangely magical.=Ves una col. Es rara y extrañamente mágica. -onion=cebolla -You see an onion.=Ves una cebolla. -garlic=ajo -You see some fresh garlic.=Ves un poco de ajo fresco. -cauliflower=coliflor -You see a fresh head of cauliflower.=Ves la cabeza de una fresca coliflor. -broccoli=brocoli -You see a bunch of broccoli.=Ves un manojo de brocoli. -leek=puerro -You see a fresh leek.=Ves un puerro fresco. -perch=perca -You see a perch.=Ves una perca. -mackerel=caballa -You see a mackerel.=Ves una caballa. -roach=rutilo -You see a roach.=Ves un rutilo -char=salvelinus -You see a char, with a fleshy pink belly.=Ves un salvelinus, con un vientre carnoso de color rosa. -clownfish=pez payaso -You see a striped clownfish.=Ves un pez payaso a rayas. -surgeonfish=pez cirujano -You see a bright blue surgeonfish.=Ves un pez cirujano de color azul brillante. -red lionfish=pterois rojo -You see a venomous coral reef red lionfish.=Ves un pterois rojo venenoso de los arrecifes de coral -egg=huevo -You see an egg.=Ves un huevo. -chicken=pollo -You see a tasty leg of chicken.=Ves un sabroso muslo de pollo. -meat=carne -You see a piece of meat.=Ves un pedazo de carne. -trout=trucha -You see a trout.=Ves una trucha. -cod=bacalao -You see a cod.=Ves un bacalao. -ham=jamón -You see a juicy ham.=Ves un jugoso jamón. -toadstool=seta venenosa -You see a toadstool. Beware!=Ves una seta venenosa ¡Ten cuidado! -fairy cake=fairy cake -You see a luscious fairy cake with icing and sprinkles on top.=Ves un delicioso fairy cake con cobertura y fideos de chocolate por encima. -crepes suzette=crepe suzette -You see a crepes suzette topped with hot chocolate sauce.=Ves una crepe suzette cubierta con salsa de chocolate caliente. -sandwich=sandwich -You see a ham and cheese sandwich.=Ves un sandwich de jamón y queso. -pie=pastel -You see a meat and vegetable pie.=Ves un pastel de carne y verduras. -fish pie=empanada de pescado -You see a fish and leek pie. It's not too stodgy so you can eat it faster than a meat pie.=Ves una empanada de pescado y puerros. No es demasiado pesada así que te la puedes comer más rápido que un pastel de carne. -apple pie=tarta de manzana -You see a delicious apple pie, made with only genuine ingredients.=Ves una deliciosa tarta de manzana, elaborada sólo con los ingredientes genuinos. -cherry pie=tarta de cereza -You see a delicious cherry pie, made with only genuine ingredients.=Ves una tarta de cereza deliciosa, elaborada sólo con los ingredientes genuinos. -pizza=pizza -You see a tasty pizza, and hope they put extra cheese on it.=Ves una sabrosa pizza, y esperas que le hayan puesto extra de queso por encima. -easter egg=huevo de pascua -You see a lovely chocolate easter egg. It's huge!=Ves un encantador huevo de pascua ¡Es enorme! -small easter egg=huevo de pascua pequeño -You see a small chocolate easter egg. Yummy!=Ves un pequeño huevo de pascua de chocolate ¡Delicioso! -chocolate bar=barrita de chocolate -You see an absolutely yummy chocolate bar.=Ves una barrita de chocolate absolutamente deliciosa. -tuna sandwich=sandwich de atún -You see what looks like tuna sandwich.=Ves lo que parece ser un sandwich de atún. -sausage=salchicha -You see what looks like a tasty sausage.=Ves lo que parece ser una sabrosa salchicha. -cheese sausage=salchicha de queso -You see what looks like a tasty cheese sausage.=Ves lo que parece ser una sabrosa salchicha de queso. -hotdog=perrito caliente -You see what looks like a tasty hotdog on a bun.=Ves lo que parece ser un sabroso perrito caliente en un bollo. -cheeseydog=perrito caliente con queso -You see what looks like a tasty cheeseydog on a bun.=Ves lo que parece ser un sabroso perrito caliente con queso en un bollo. -vanilla shake=batido de vainilla -You see a yummy vanilla shake. Absolutely NO calories.=Ves un sabroso batido de vainilla. Absolutamente SIN calorías. -chocolate shake=batido de chocolate -You see a scrumptious chocolate shake. Absolutely NO calories.=Ves un batido de chocolate para chuparse los dedos. Absolutamente SIN calorías. -honey=miel -You see a jar of sticky sweet honey. It's the clear runny kind.=Ves una jarra de miel dulce y pegajosa. Es de tipo fluído y claro. -sugar=azúcar -You see a sack of raw sugar.=Ves un saco de azúcar no refinado. -licorice=regaliz -You see some delicious licorice. Your mouth waters.=Ves un poco de delicioso regaliz. Se te hace la boca agua. -grilled steak=filete a la parrilla -You see a fresh grilled steak. It smells awesome and is really juicy.=Ves un filete a la parrilla recién hecho. Huele fenomenal y está realmente jugoso. -leather helmet=casco de cuero -You see a leather helmet, it seems more like a hat than a defensive item.=Ves un casco de cuero, parece más un sombrero que un elemento defensivo. -trophy helmet=casco trofeo -robins hat=sombrero de robin -You see a robins hat. It's cute but that's about it.=Ves un sombrero de robin. Es gracioso pero eso es todo. -studded helmet=casco tachonado -You see a studded helmet, it should protect your head against cuts but perhaps not hard blows.=Ves un casco tachonado, debería de protegerte la cabeza de cortes pero quizá no de golpes fuertes. -chain helmet=casco de cota de malla -You see a solid chain helmet, it will be excellent protection for your head.=Ves un sólido casco de cota de malla, será una excelente protección para la cabeza. -viking helmet=casco de vikingo -You see a viking helmet. The previous owner wore it to many battles.=Ves un casco de vikingo. Su anterior dueño lo llevó consigo a muchas batallas. -aventail=almófar -You see a aventail. It's a clever design.=Ves un almófar. Es un diseño ingenioso. -red helmet=casco rojo -You see a red helmet, it reminds you of a jester's hat but you can be sure it has better protection than that.=Ves un casco rojo, te recuerda al sombrero de un bufón pero puedes estar seguro de que te proporcionará una protección mejor que esa. -blue helmet=casco azul -You see a brand new blue helmet.=Ves un casco azul nuevecito. -golden helmet=casco de oro -You see a heavy golden helmet. The thick metal should protect you against almost anything.=Ves un pesado casco de oro. El grueso metal debería de protegerte contra casi todo. -shadow helmet=casco de sombra -You see a heavy shadow helmet. Instead of shining, it darkly gleams.=Ves un pesado casco de sombra. En lugar de brillar, emite destellos oscuros. -horned golden helmet=casco de oro con cuernos -You see a horned golden helmet.=Ves un casco de oro con cuernos. -chaos helmet=casco del caos -You see a chaos helmet.=Ves un casco del caos. -mainio helmet=casco de mainio -You see a strange mainio helmet. You can barely see when wearing it, but it protects your head very well.=Ves un extraño casco de mainio. Apenas puedes ver cuando lo llevas puesto, pero te protege muy bien la cabeza. -magic chain helmet=casco de cota de malla mágico -You see a magic chain helmet.=Ves un casco de cota de malla mágico. -liberty helmet=casco de la libertad -You see the liberty helmet. It's light and strong, you feel free wearing it.=Ves el casco de la libertad. Es ligero y resistente, te sientes libre al llevarlo puesto. -black helmet=casco negro -You see a black helmet.=Ves un casco negro -mithril helmet=casco de mithril -You see a mithril helmet.=Ves un casco de mithril. -arandula=arandula -You see a arandula. It has medicinal properties, if the right person uses it.=Ves una arandula. Tiene propiedades medicinales, si es usada por la persona adecuada. -kokuda=kokuda -You see a kokuda. It has medicinal properties, if the right person uses it.=Ves una kokuda. Tiene propiedades medicinales, si es usada por la persona adecuada. -kekik=kekik -You see a kekik. It is a herb which can have healing properties.=Ves una kekik. Es una hierba que puede tener propiedades curativas. -mandragora=mandrágora -You see a root of mandragora. It has medicinal properties, if the right person uses it.=Ves una raíz de mandrágora. Tiene propiedades medicinales, si es usada por la persona adecuada. -sclaria=sclaria -You see the herb sclaria. The smell is calming.=Ves la hierba sclaria. Su olor es calmante. -emerald=esmeralda -You see an emerald, a precious gemstone.=Ves una esmeralda, una gema preciosa. -sapphire=zafiro -You see a sapphire, a precious gemstone.=Ves un zafiro, una gema preciosa. -carbuncle=carbúnculo -You see a carbuncle, a precious gemstone related to the garnet.=Ves un carbúnculo, una gema preciosa relacionada con el granate. -obsidian=obsidiana -You see a piece of obsidian, it's known as volcanic glass because it forms when lava cools rapidly. When used as a gemstone it may look jet black or silvery grey, depending on how it is cut.=Ves una pieza de obsidiana, es conocida como vidrio volcánico porque se forma cuando la lava se enfría rápidamente. Cuando se usa como gema puede parecer negro azabache o gris plateado, dependiendo de cómo se corte. -diamond=diamante -You see a diamond, not just a beautiful gem but also the hardest naturally occurring material .=Ves un diamante, no es sólo una hermosa gema sino también el material más duro de origen natural. -black pearl=perla negra -You see a black pearl, so dark and shiny.=Ves una perla negra, tan oscura y brillante. -dungeon silver key=llave de plata de la mazmorra -You see a silver dungeon key. Use it only if you are brave.=Ves una llave de plata de la mazmorra. Usala sólo si eres valiente. -lich gold key=llave de oro del lich -You see a golden lich key. It will open the door to terrible things.=Ves una llave de oro del lick. Abrirá la puerta a cosas terribles. -minotaur key=llave del minotauro -You see a fancy bronze key. It smells faintly of minotaur, perhaps one dropped it?=Ves una lujosa llave de bronce. Huele ligeramente a minotauro, ¿quizá se le cayó a uno? -nalwor bank key=llave del banco de nalwor -You see a key to the customer room of Nalwor Bank.=Ves una llave de la sala de clientes del Banco de Nalwork. -kotoch prison key=llave de la prisión de kotoch -You see an ancient copper key, enscribed with something in Orcish.=Ves una antigua llave de cobre, grabada con algo en Orco. -kanmararn prison key=llave de la prisión de kanmararn. -You see a key which matches the locks in the Kanmararn prison.=Ves una llave que coincide con las cerraduras de la prisión de Kanmararn. -sedah gate key=llave de la puerta de sedah -You see a key for the gate to the imperial city of Sedah.=Ves una llave que abre la puerta que lleva a la ciudad imperial de Sedah. -pet sanctuary key=llave del santuario de mascotas -You see a key for the pet sanctuary in Ados.=Ves una llave que abre el santuario de mascotas en Ados. -house key=llave de la casa -You should not see this description=No deberías de poder ver esta descripción -master key=llave maestra fado hotel key 001=llave 001 del hotel de fado You see a key to room #001 in the Fado Hotel.=Ves una llave de la habitación #001 del hotel de Fado. fado hotel key 002=llave 002 del hotel de fado @@ -540,446 +2897,45 @@ fado hotel key 007=llave 007 del hotel de fado You see a key to room #007 in the Fado Hotel.=Ves una llave de la habitación #007 del hotel de Fado. fado hotel key 008=llave 008 del hotel de fado You see a key to room #008 in the Fado Hotel.=Ves una llave de la habitación #008 del hotel de Fado. -small key=llave pequeña -You see the key for Zara's row house in Ados.=Ves la llave de la row house de Zara en Ados. -leather legs=quijotes de cuero -You see a pair of leather legs. They look new but they're poor quality.=Ves un par de quijotes de cuero. Parecen nuevos pero son de mala calidad. -studded legs=quijotes tachonados -You see a pair of blood stained studded legs. You're not sure if the blood came from their last owner ... or his enemies.=Ves un par de quijotes tachonados manchados de sangre. No estás seguro de si la sangre es de su último dueño ... o de sus enemigos. -chain legs=quijotes de cota de malla -You see a pair of well made chain legs. They should protect you against any attacks aimed low.=Ves un par de quijotes de cota de malla bien elaborados. Deberían de protegerte frente a cualquier ataque bajo. -red legs=quijotes rojos -A pair of red legs are in your grasp.=Un par de quijotes rojos están a tu alcance. -elvish legs=quijotes élficos -You see a pair of elvish legs.=Ves un par de quijotes élficos. -blue legs=quijotes azules -You see a pair of shiny new blue legs.=Ves un par de quijotes azules nuevos y brillantes. -golden legs=quijotes de oro -You see a pair of shining, strong golden legs.=Ves un par de quijotes de oro, resistentes y relucientes. -shadow legs=quijotes de sombra -You see a pair of shadow legs. The weight of them is incredible.=Ves un par de quijotes de sombra. Su peso es increíble. -mainio legs=quijotes de mainio -You see a pair of odd looking mainio legs. They are somewhat startling but they give you a good protection.=Ves un par de quijotes de mainio de extraño aspecto. Son de algún modo sorprendentes pero te proporcionan una buena proteccion. -chaos legs=quijotes del caos -You see a pair of chaos legs. The strange colors confuse you.=Ves un par de quijotes del caos. Sus extraños colores te confunden. -xeno legs=quijotes de xeno -You see a pair of xeno legs. They're very strong and look brand new.=Ves un par de quijotes de xeno. Son muy resistentes y parecen nuevecitos. -jewelled legs=quijotes enjoyados -You see a beautiful pair of jewelled legs. A large red gem glints at you and they're encrusted all over with sapphires.=Ves un hermoso par de quijotes enjoyados. Una gema roja grande emite reflejos hacia ti y tienen incrustaciones de zafiro por todas partes. -black legs=quijotes negros -You see a pair of black legs.=Ves un par de quijotes negros. -mithril legs=quijotes de mithril -You see a pair of mithril legs. They will serve you well.=Ves un par de quijotes de mithril. Te servirán de mucho. -bulb=bulbo -You see a bulb. It can be planted anywhere, but it will only thrive on fertile ground.=Ves un bulbo. Puede ser plantado en cualquier sitio, pero sólo prosperará en un terreno fértil. -seed=semilla -You see a seed. It can be planted anywhere, but it will only thrive on fertile ground.=Ves una semilla. Puede ser plantada en cualquier sitio, pero sólo prosperará en un terreno fértil. -giant heart=corazón gigante -You see a giant heart. This proves that a giant was defeated, and has magical properties that you are not sure of.=Ves un corazón gigante. Ésto demuestra que un gigante fue derrotado, y tiene propiedades mágicas de las que no estás seguro. -dice=dados -You see a set of dice. May lady luck be with you.=Ves un grupo de dados. Que la suerte te acompañe. -cards=cartas -You see a deck of cards. Shuffle them well, to get a winning hand.=Ves una baraja de cartas. Barajealas bien, para ganar la mano. -teddy=peluche -You see a teddy bear. Its soft and cuddly, and children love them.=Ves un oso de peluche. Es suave y tierno, y a los niños les encanta. -vampirette entrails=entrañas de vampiresa -You see some vampirette entrails. These guts proves that a vampirette was slain.=Ves unas entrañas de vampiresa. Estas vísceras demuestran que una vampiresa fue asesinada. -bat entrails=entrañas de murciégalo -You see some bat entrails, full of the blood of a living creature.=Ves unas entrañas de murciégalo, llenas de sangre de algún ser vivo. -suntan cream=crema bronceadora -marbles=canicas -You see some marbles. Boys will be boys.=Ves unas canicas. Son cosas de niños. -canned tuna=atún en lata -You see tuna in a can. Too bad you don't have a can opener.=Ves atún en una lata. Lástima que no tengas un abrelatas. -snowglobe=esfera de nieve -You see a snowglobe. You are SUCH a tourist!=Ves una esfera de nieve ¡Estás hecho un turista! -bobbin=canilla -You see a bobbin for a sewing machine.=Ves una canilla para una máquina de coser. -snowball=bola de nieve -You see snowballs. You can throw them at your friends or put it in your bag to keep your fish fresh. But be careful, there could be little stones in it.=Ves bolas de nieve. Puedes tirarselas a tus amigos o ponerlas en tu bolsa para conservar el pescado fresco. Pero ten cuidado, podría haber piedrecitas dentro de ella. -vomit=vómito -You see a pile of vomit. Gross!=Ves un montón de vómito ¡Qué desagradable! dart=dardo You see a dart. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results.=Ves un dardo. Necesitarás estar cerca antes de que lo puedas usar como proyectil. Permanece recto, no inclinado, para obtener los mejores resultados. power dart=dardo de potencia You see a power dart. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results.=Ves un dardo de potencia. Necesitarás estar cerca antes de que lo puedas usar como proyectil. Permanece recto, no inclinado, para obtener los mejores resultados. -shuriken=shuriken -You see a shuriken. You'll need to be close before you can use it as a missile. Stand square on, not diagonally, for the best results.=Ves un shuriken. Necesitarás estar cerca antes de que lo puedas usar como proyectil. Permanece recto, no inclinado, para obtener los mejores resultados. -fire shuriken=shuriken de fuego -You see a fire shuriken. Throwing this flaming missile unleashes fiendish power.=Ves un shuriken de fuego. El lanzamiento de este proyectil en llamas desata un poder diabólico. -wooden spear=lanza de madera -You see a wooden spear. You can attack at a distance with this primitive weapon. Stand square on, not diagonally, for the best results.=Ves una lanza de madera. Puedes atacar a distancia con este arma primitiva. Permanece recto, no inclinado, para obtener los mejores resultados. -money=dinero -wooden bow=arco de madera -You see a wooden bow. With this in one hand, and wooden arrows in the other, you may be able to attack from a distance.=Ves un arco de madera. Con esto en una mano, y flechas de madera en la otra, puedes ser capaz de atacar a distancia. -longbow=arco largo -You see a longbow. Equipped with this, and a supply of arrows, you will have an effective ranged attack.=Ves un arco largo. Equipado con ésto, y con un surtido de flechas, conseguirás un efectivo ataque a distancia. -composite bow=arco compuesto -You see a composite bow. It's better strung than most bows, for excellent ranged attack.=Ves un arco compuesto. Está mejor tensado que la mayoría de arcos, para proporcionar un excelente ataque a distancia. -crossbow=ballesta -You see a crossbow. The attack is high but it has a very short range, what do you prefer?=Ves una ballesta. Su ataque es elevado pero cubre una distancia muy corta, ¿qué prefieres? -hunter crossbow=ballesta de cazador -You see a hunter crossbow. You must decide if the high attack is worth the short range it has.=Ves una ballesta de cazador. Debes decidir si su elevado ataque compensa la corta distancia que cubre. -mithril bow=arco de mithril -You see a light and strong mithril bow. Its impeccable workmanship affords incredible accuracy.=Ves un arco de mithril ligero y resistente. Su impecable elaboración permite una precisión increíble. angel figurine=figurilla de ángel You see a sparkling angel figurine.=Ves una figurilla de ángel resplandeciente. -skull ring=anillo de calavera -You see a skull ring.=Ves un anillo de calavera. -amulet=amuleto ankh=anj You see a mysterious ankh. You don't know what it is for.=Ves un misterioso anj. No sabes para qué sirve. -wood=madera -You see some wood, fallen from a nearby tree.=Ves un poco de madera, caída de algún árbol cercano. -sugar cane=caña de azúcar -You see a sugar cane. You can grind it for raw sugar.=Ves una caña de azúcar. Puedes molerla para obtener azúcar no refinado. -grain=trigo -You see a sheaf of grain.=Ves una gavilla de trigo. -flour=harina -iron ore=mineral de hierro -coal=carbón -You see a black and shiny piece of stone coal. It stayed in the ground for million of years.=Ves un pedazo de piedra de carbón negro y brillante. Permaneció enterrado durante millones de años. -wool=lana -You see wool. It was recently shorn.=Ves lana. Fue esquilada recientemente. -yarn=hilo de lana -You see a ball of yarn. It was spun with care.=Ves un ovillo de lana. Fue hilado con sumo cuidado. -cloth=tela -You see a roll of cloth. It is of first-class quality.=Ves un rollo de tela. Es de primera calidad. -gold nugget=pepita de oro -You see a gold nugget. Its rich golden color shines, and it may be useful.=Ves una pepita de oro. Su vivo color dorado brilla,y puede resultar útil. -mithril nugget=pepita de mithril -You see a mithril nugget. Its pale silvery color shines, and it may be useful.=Ves una pepita de mithril. Su pálido color plateado brilla, y puede resultar útil. iron=hierro -gold bar=barra de oro -You see a gold bar. It will make beautiful jewelry, or fetch a fine price.=Ves una barra de oro. Servirá para hacer hermosas joyas, o se pagará un buen precio por ella. -mithril bar=barra de mithril -You see a mithril bar. It can be made into excellent armor, or fetch a fine price.=Ves una barra de mithril. Se puede elaborar una armadura excelente con ella, o se puede pagar un buen precio por ella. -golden egg=huevo de oro -You see a valuable golden egg.=Ves un valioso huevo de oro. -spotted egg=huevo con manchas -You see a spotted egg. They are said to be delicious.=Ves un huevo con manchas. Se dice que son deliciosos. reptile egg=huevo de reptil You see a valuable reptile egg.=Ves un valioso huevo de reptil. -silk gland=glándula de seda -You see a silk gland from a giant spider. Its contents are extremely valuable.=Ves una glándula de seda de una araña gigante. Su contenido es extremadamente valioso. -silk thread=hilo de seda -You see a spool of silk thread, spun from the glands of a giant spider..=Ves un carrete de hilo de seda, hilado de las glándulas de una araña gigante.. -oil=aceite -You see a small can of oil.=Ves una pequeña lata de aceite. -mithril thread=hilo de mithril -You see a spool of mithril thread, made from silk fused with mithril.=Ves un carrete de hilo de mithril, elaborado a partir de seda fundida con mithril. -mithril fabric=tela de mithril -You see a couple yards of mithril fabric. It's very sparkly!=Ves un par de metros de tela de mithril ¡Emite muchos destellos! -horse hair=pelo de caballo -You see some horse hairs.=Ves un poco de pelos de caballo. -beeswax=cera de abeja -You see a piece of beeswax.=Ves un pedazo de cera de abeja. -candle=vela -You see a candle in a candle stick.=Ves una vela en un candelero. -wedding ring=anillo de boda -engagement ring=anillo de compromiso -You see a diamond engagement ring, a symbol of love (or a symbol of wealth?)=Ves un anillo de compromiso de diamante, un símbolo de amor (¿o un símbolo de riqueza?) -emerald ring=anillo de esmeralda -You see an emerald ring, known as ring of life. Wear it, and you risk less from death.=Ves un anillo de esmeralda, conocido como el anillo de la vida. Llevalo, y arriesgarás menos al morir. -summon scroll=pergamino de invocación -nalwor city scroll=pergamino de la ciudad de nalwor -You see a nalwor city scroll, it will take you back to nalwor city.=Ves un pergamino de la ciudad de nalwor, te llevará de regreso a la ciudad de nalwor. -ados city scroll=pergamino de la ciudad de ados -You see a ados city scroll, it will take you back to ados city.=Ves un pergamino de la ciudad de ados, te llevará de regreso a la ciudad de ados. -fado city scroll=pergamino de la ciudad de fado -You see a fado city scroll, it will take you back to fado city.=Ves un pergamino de la ciudad de fado, te llevará de regreso a la ciudad de fado. -kirdneh city scroll=pergamino de la ciudad de kirdneh -You see a kirdneh city scroll, it will take you back to kirdneh city.=Ves un pergamino de la ciudad de kirdneh, te llevará de regreso a la ciudad de kirdneh. -kalavan city scroll=pergamino de la ciudad de kalavan -You see a kalavan city scroll, it will take you back to kalavan city.=Ves un pergamino de la ciudad de kalavan, te llevará de regreso a la ciudad de kalavan. -marked scroll=pergamino marcado -invitation scroll=pergamino de invitación -home scroll=pergamino del hogar -You see a home scroll, it will take you back to semos city.=Ves un pergamino del hogar, te llevará de regreso a la ciudad de semos. -empty scroll=pergamino en blanco -You see an empty scroll. If used correctly it will return you to where you marked it at.=Ves un pergamino en blanco. Usado correctamente te llevará de regreso a donde lo marcaste. -rainbow beans=haba del arco iris -You see a small pile of rainbow beans. One says something odd.=Ves un pequeño montón de habas del arco iris. Una dice algo raro. -balloon=globo -You see a bright red balloon. You can use it to float high up into the clouds.=Ves un globo de color rojo brillante. Puedes usarlo para flotar en lo alto de las nubes. -twilight moss=musgo del crepúsculo -You see a strange looking piece of moss. It looks like it could have hallucinogenic properties.=Ves un pedazo de musgo de aspecto extraño. Parece que podría tener propiedades alucinógenas. -buckler=broquel -You see a small buckler. It may protect you against swords and maces but against stronger weapons you do not trust it.=Ves un pequeño broquel. Puede protegerte frente a espadas y mazas pero frente a armas más potentes no te confíes. -wooden shield=escudo de madera -You see a wooden shield. It is made of cheap wood and heavily used. However, it still seems to offer you some kind of basic protection.=Ves un escudo de madera. Está elaborado a partir de madera barata y ha sido muy utilizado. Sin embargo, todavía parece proporcionar algún tipo de protección básica. -studded shield=escudo tachonado. -You see a studded shield. It's a little dented but seems robust enough.=Ves un escudo tachonado. Está un poco abollado peroparece bastante robusto. -plate shield=escudo de placas de acero -You see a solid plate shield. It is in good condition, and might be worth a little.=Ves un sólido escudo de placas de acero. Está en buenas condiciones, y podría valer bastante. -lion shield=escudo de león -You see a valuable lion shield. It is heavy enough to defend you against strong attacks.=Ves un valioso escudo de león. Es lo bastante pesado como para defenderte frente a ataques potentes. -unicorn shield=escudo del unicornio -You see a unicorn shield. It's beautifully crafted and will give you excellent protection.=Ves un escudo del unicornio. Está bellamente elaborado y te proporcionará una protección excelente. -enhanced lion shield=escudo de león mejorado -An enhanced lion shield glints at you. It will defend you well.=Un escudo de león mejorado emite destellos hacia ti. Te defenderá bien. -skull shield=escudo de calavera -The face of a skull scowls at you from this fearsome skull shield.=La cara de una calavera te frunce el ceño desde este temible escudo de calavera -crown shield=escudo de la corona -You see a mighty crown shield. It is in perfect condition.=Ves un poderoso escudo de la corona. Esta en perfectas condiciones. -red shield=escudo rojo -You see a red shield. It is a stunning shade of scarlet.=Ves un escudo rojo. Es de un imponente tono escarlata. -blue shield=escudo azul -You see a blue shield. It feels light in your hand but is strong enough for excellent protection.=Ves un escudo azul. Notas en tu mano que es ligero pero es lo bastante resistente como para proporcionar una excelente protección. -elvish shield=escudo élfico -You see an elvish shield. It looks like one big tree leaf!=Ves un escudo élfico ¡Se parece a una gran hoja de árbol! -golden shield=escudo de oro -You see a golden shield, could it be the ultimate in defense?=Ves un escudo de oro, ¿podría ser lo último en defensa? -green dragon shield=escudo de dragón verde -You see an green dragon shield, scaly and strong.=Ves un escudo de dragón verde, escamoso y resistente. -shadow shield=escudo de sombra -You see a shadow shield. Rather than reflecting light, it seems to absorb it.=Ves un escudo de sombra. En lugar de reflejar la luz, parece absorberla. -mainio shield=escudo de mainio -You see a strange looking mainio shield. You have no idea what it's made of, but it seems to protect you well by distracting your enemies.=Ves un escudo de mainio de extraña apariencia. No tienes ni idea de lo que está hecho, pero parece que te protege bien al distraer a tus enemigos. -chaos shield=escudo del caos -You see a chaos shield. Its charming appearance will fool your enemies.=Ves un escudo del caos. Su encantadora apariencia engañará a tus enemigos. -magic plate shield=escudo mágico de placas de acero -You see a magic plate shield. Its has a magical quality surrounding it.=Ves un escudo mágico de placas de acero. Tiene una cualidad mágica que lo envuelve. -xeno shield=escudo de xeno -You see a lightweight but strong xeno shield, in the colors of the creature it was looted from.=Ves un escudo de xeno ligero pero resistente, de los mismos tonos que la criatura a la que se le saqueó. -black shield=escudo negro. -You see a black shield. Its darkness will surely protect you.=Ves un escudo negro. Su oscuridad seguro que te protegerá. -mithril shield=escudo de mithril -You see a mithril shield. You will most surely be protected.=Ves un escudo de mithril. Casi con toda seguridad estarás protegido. -lucky charm=amuleto de la suerte -You see a lucky charm. Some believe that it helps their karma in some way, but others think that is superstition.=Ves un amuleto de la suerte. Algunos creen que ayuda a su karma de algún modo, pero otros piensan que es una superstición. -mithril clasp=cierre de mithril -You see a clasp made from mithril, it would provide the fastening for a cloak.=Ves un cierre elaborado a partir de mithril, te proporcionaría la fijación para una capa. -mythical egg=huevo mítico -You see a mythical egg, but you do not know, what it is for.=Ves un huevo mítico, pero no sabes, para qué es. -magical eggshells=cáscaras de huevo mágicas -You see shards from a mythical egg. They have magical properties.=Ves fragmentos de un huevo mítico. Tienen propiedades mágicas. -golden gm token=ficha de dj de oro -silvery gm token=ficha de dj de plata -bronze gm token=ficha de dj de bronce -assassin dagger=daga de asesino -You see an assassin dagger, easy to pull out and use when needed.=Ves una daga de asesino, fácil de sacar y de usar cuando se necesite. -knife=cuchillo -You see a blunt knife which looks capable of little damage.=Ves un cuchillo desafilado que parece capaz de hacer poco daño. -night dagger=daga de la noche -You see a night dagger, to be used with stealth.=Ves una daga de la noche, para ser utilizada con sigilo. -obsidian knife=cuchillo de obsidiana -You see a knife whose blade is crafted to a lethal edge from finest obsidian.=Ves un cuchillo cuya hoja se ha trabajado hasta conseguir un filo letal a partir de la obsidiana más pura. -soul dagger=daga del alma -You see a soul dagger. It's so light and sharp that you can stab at a very fast rate.=Ves una daga del alma. Es tan ligera y afilada que puedes apuñalar a un ritmo muy rápido. -golden blade=cuchilla de oro -You see the rare and precious golden blade. It can steal life from those you stab with it.=Ves la rara y preciosa cuchilla de oro. Puede robar la vida a aquellos que apuñales con ella. -dagger=daga -You see a dagger, it is little more than decorative but you can jab pretty fast with it.=Ves una daga, es poco más que decorativa pero puedes punzar bastante rápido con ella. -dark dagger=daga oscura -You see a dark dagger. It's designed to be used with a fast motion.=Ves una daga oscura. Se ha diseñado para ser utilizada con un movimiento rápido. -hell dagger=daga del infierno -You see a hell dagger. Is the color that of the burning fires of Hell?=Ves una daga del infierno ¿Es ése el color de las llamas del infierno? -chaos dagger=daga del caos -You see a chaos dagger, it is very lightweight and sharp. It will heal you while hurting them.=Ves una daga del caos, es muy ligera y afilada. -short sword=espada corta -You see a well kept short sword which will be easy to carry as you travel.=Ves una espada corta bien conservada que será fácil de llevar cuando viajes. -r hand sword=espada de mano d -You see r hand sword. It must be used with a left hand sword.=Ves una espada de mano d. Debe usarse con una espada de mano izquierda. -l hand sword=espada de mano i -You see l hand sword. It must be used with right hand sword.=Ves una espada de mano i. Debe usarse con una espada de mano derecha. -sword=espada -You see a sharp sword which might even be worth a little.=Ves una espada afilada que podría incluso valer un poco. -imperator sword=espada imperator -You see a an imperator sword, crafted from you know not what.=Ves una espada imperator, elaborada a partir de ya sabes qué. -scimitar=cimitarra -This scimitar catches the light as you look at it, it will be a powerful weapon.=Esta cimitarra atrapa la luz mientras la miras, sera un arma poderosa. -katana=katana -You see a valuable katana, with a lethal looking blade.=Ves una valiosa katana, con una hoja de apariencia letal. -nihonto=nihonto -You see a samurai's nihonto sword with a razor sharp blade.=Ves una espada nihonto de samurai con una hoja tan afilada como la de una cuchilla. -buster=buster -You see a buster, with its distinctive, oversized blade.=Ves una espada buster, con su distintiva hoja sobredimensionada. -orc sword=espada de orco -You see an orc sword which has a heavy and deadly appearance.=Ves una espada de orco que tiene una apariencia pesada y mortal. -demon sword=espada de demonio -Demon skeletons return from hell with this demon sword.=Los esqueletos demonio regresan del infierno con esta espada de demonio. -vampire sword=espada de vampiro -You see the vampire sword, which will suck life from those you attack with it.=Ves la espada de vampiro, que absorberá la vida de aquellos a los que ataques con ella. -claymore=claymore -This claymore is very hard to lift as it is so heavy. It will be a good weapon if you can swing it.=Esta claymore es muy difícil de levantar ya que es muy pesada. -biting sword=espada mordaz -The point of this biting sword is chiseled to a lethal point. It will serve you well.=La punta de esta espada mordaz está cincelada hasta un punto letal. -fire sword=espada de fuego -Flames race along this fire sword as you wield it.=Las llamas corren a lo largo de esta espada de fuego mientras la esgrimes. -ice sword=espada de hielo -You see an ice sword, the blade is lethally cold to the touch, and you know it is one of the best weapons you'll find.=Ves una espada de hielo, la hoja es letalmente fría al tacto, y sabes que es una de las mejores armas que vas a encontrar. -chaos sword=espada del caos -The chaos sword. It hurts them but it heals you.=La espada del caos. Les hace daño a ellos pero a ti te cura. -broadsword=espada ancha -You see a mighty broadsword. Enemies will see their death.=Ves una poderosa espada ancha. Los enemigos verán su muerte. -golden orc sword=espada de oro de orco -You see a golden orc sword, with its heavy and deadly appearance it seems also valuable.=Ves una espada de oro de orco, con su pesada y mortal apariencia parece tan valiosa. -elvish sword=espada élfica -This an elvish sword. It looks like it is made from tree leaves!=Esta es una espada élfica ¡Parece que está hecha de hojas de árbol! -drow sword=espada drow -This is a genuine drow sword.=Esta es una genuina espada drow. -great sword=gran espada -This rare great sword is heavy and of good craftsmanship. It can only attack incredibly slowly.=Esta rara gran espada es pesada y de buena fabricación. -orcish sword=espada orca -This orcish sword is badly done and weighs a lot.=Esta espada orca está mal hecha y pesa un montón. -immortal sword=espada inmortal -This immortal sword is a gift of gods to the chosen one. It is heavy and of exquisite craftsmanship.=Esta espada inmortal es un regalo de los dioses al elegido. Es pesada y de una elaboración exquisita. -xeno sword=espada de xeno -You wield a xeno sword. Despite the great strength it is strangely light.=Tu esgrimes una espada de xeno. A pesar de su gran potencia es extrañamente ligera. -black sword=espada negra -You see a black sword. Its darkness will surely protect you.=Ves una espada negra. Su oscuridad seguro que te protegerá. -demon fire sword=espada de fuego de demonio -The demon fire sword, a horrible sword taken from hell's deepest dungeons.=La espada de fuego de demonio, una espada horrible sacada de las más profundas mazmorras del infierno. -arrow game token=ficha del juego de la flecha -You see a token, that when placed in the right spot could point you in the right direction.=Ves una ficha, que cuando se coloque en el lugar correcto podría señalarte la dirección correcta. -o board token=ficha del tablero o -You see an O token.=Ves una ficha O -x board token=ficha del tablero x -You see an X token.=Ves una ficha X -food mill=molino de alimentos -You see a food mill. It is the traditional tool to mill food.=Ves un molino de alimentos. Es la herramienta tradicional para moler alimentos. -sugar mill=molino de azúcar -You see a sugar mill. You can obtain sugar if you grind canes with it.=Ves un molino de azúcar. Puedes obtener azúcar si mueles cañas con él. -scroll eraser=pergamino borrador -You see a scroll eraser. Put a marked scroll in the opposite hand to clean the scroll.=Ves un pergamino borrador. Pon un pergamino marcado en la mano opuesta para limpiar el pergamino. -pestle and mortar=maza y mortero -You see a pestle and mortar. This is the traditional tool to pound herbs, food and other substances.=Ves una maza y un mortero. Esta es la herramienta tradicional para machacar hierbas, alimentos y otras sustancias. -gold pan=sartén del oro -You see a gold pan. It is the traditional tool to prospect for gold.=Ves una sartén del oro. Es la herramienta tradicional para buscar oro. -fishing rod=caña de pescar -You see a fishing rod. If you find a fishing spot you can catch fish with it.=Ves una caña de pescar. Si encuentra un lugar para pescar puedes capturar peces con ella. -magical scissors=tijeras mágicas. -You see a sparkling pair of magical scissors. Just think what they could cut!=Ves un par de tijeras mágicas centelleantes ¡Piensa sólo en lo que podrían cortar! -magical needle=aguja mágica -You see a magical needle with a very sharp point.=Ves una aguja mágica con una punta muy afilada. -pick=pico -You see a pick. It is a tool which helps you to get some coal.=Ves un pico. Es una herramienta que te ayuda a obtener algo de carbón. -albino elf swordsman=espadachín elfo albino -albino elf archer=arquero elfo albino -albino elf magician=mago elfo albino -albino elf queen=reina elfa albina -albino elf king=rey elfo albino -amazoness archer=arquera amazona -You see an amazoness archer. She handles her bow quite well and her arrows are very dangerous when she shoots them at you out of the forest!=Ves a una arquera amazona ¡Maneja su arco bastante bien y sus flechas son muy peligrosas cuando las dispara hacia ti desde el bosque! -amazoness hunter=cazadora amazona -You see an amazoness hunter. She creeps through the forest to hunt animals. But she doesn't like that you are in her hunting area!=Ves a una cazadora amazona ¡Se arrastra por el bosque para cazar animales ¡Pero no le gusta que estés en su zona de caza! -amazoness coastguard=guardacostas amazona -amazoness archer commander=arquera comandante amazona -amazoness elite coastguard=guardacostas de élite amazona -amazoness bodyguard=guardaespaldas amazona -amazoness coastguard mistress=guardacostas Maestre amazona -amazoness commander=comandante amazona -amazoness vigilance=vigilante amazona -You see an amazoness vigilance. Her senses are well tuned, so you have to be very silent when you don't want her to hear you!=Ves a una vigilante amazona ¡Sus sentidos están muy desarrollados, así que tienes que ser muy silencioso cuando no quieras que te oiga! -amazoness imperator=imperator amazona -deer=ciervo -You see a defenseless deer, do not have fear of it.=Ves un ciervo indefenso, no le tengas miedo. -crab=cangrejo -You see a little crab that comes from the beach, be careful of its pincers.=Ves un pequeño cangrejo que viene de la playa, ten cuidado con sus pinzas. + +# creature speach not yet supported by parser + click-clack-click=clic-clac-clic -bat=murciégalo -You see a bat, it will seek your blood, don't be distracted...=Ves un murciégalo, buscará tu sangre, no te distraigas... -chicken=pollo -You see a chicken, isn't it nice?.=Ves un pollo, ¿a que es bonito? cluck cluck cluck=cloc cloc cloc cluck cluck=cloc cloc -penguin=pingüino -You see a penguin, smart and silly.=Ves un pingüino, bobo y elegante Oou! Oou!=Oou! Oou! *flap* *flap*=*flap* *flap* -pigeon=paloma -You see a pigeon, it loves the little seeds.=Ves una paloma, le encantan las semillas pequeñas. -monkey=mono -You see a monkey, would you like to offer it a banana?=Ves un mono, ¿Te gustaría ofrecerle un plátano? Aooooooouuhhh!!=Aooooooouuhhh!! -boar=verraco -You see a boar, they are pretty wild!=Ves un verraco, ¡son bastante salvajes! Oink!!=Oink!! -wolf=lobo -You see a wolf. It is wild and very hungry!=Ves un lobo ¡Es salvaje y está muy hambriento! Aooooooouuhhh!!=Aooooooouuhhh!! -big bad wolf=lobo grande y malo -You see a big bad wolf. It is really wild, hungry and really dangerous!=Ves un lobo grande y malo ¡Es verdaderamente salvaje, está hambriento y es realmente peligroso! Aooooooouuhhh!!=Aooooooouuhhh!! -fox=zorro -You see a fox, it becomes extinct!=Ves un zorro, ¡se esta extinguiendo! -caiman=caimán -You see a caiman, which is hidden in the water.=Ves un caimán, que está escondido en el agua. Grrr Grrr!=Grrr Grrr! -babybear=cachorro de oso -You see a babybear, isn't it sweet?=Ves un cachorro de oso, ¿a que es dulce? Roar!=Roar! -panda=panda -You see a panda which comes from an exotic land.=ves un panda que viene de una tierra exótica. Roar!=Roar! -snake=serpiente -You see a snake, it came from the forest to poison you!=Ves una serpiente, ¡vino desde el bosque a envenenarte! SsSssSsssssss!=SsSssSsssssss! -bear=oso -You see a bear. You are lucky and found it awake.=Ves un oso. Has tenido suerte y lo has encontrado despierto. Roar!=Roar! -lion=león -You see a lion, dangerous and hungry...=Ves un león, peligroso y hambriento... Grooooaaaarrrr!=Grooooaaaarrrr! -elephant=elephant -You see an elephant, a huge animal with lots of ham and meat. Are you sure that you want to attack it?=Ves un elefante, un animal enorme con un montón de jamón y de carne. WHOOOOEOEOEZZZZZZ!!!=WHOOOOEOEOEZZZZZZ!!! -tiger=tigre -You see a tiger, which likes deer and human meat.=Ves un tigre, al que le gustan los ciervos y la carne humana. Miiiiaaauuu!=Miiiiaaauuu! -killer bat=murciégalo asesino -You see a killer bat which will suck your blood!=¡Ves un murciégalo asesino que te chupará la sangre! -green slime=baba verde -You see a green slime, sticky and shiny.=Ves una baba verde, pegajosa y brillante. -black bear=oso negro -You see a strong black bear which hibernates in winter.=Ves un fuerte oso negro que hiberna en invierno. ROOOOAAAAR!=ROOOOAAAAR! -brown slime=baba marrón -You see a brown slime, sticky and hungry.=Ves una baba marrón, pegajosa y hambrienta. -spider=araña -You see a spider. It makes a giant spider web to trap you!=Ves una araña ¡Está haciendo una telaraña gigante para atraparte! -black slime=baba negra -You see a black slime, with a sticky trail behind it.=Ves una baba negra, que va dejando un rastro pegajoso. -poisonous spider=araña venenosa -You see a poisonous spider. Be careful! Its poison is very strong! It will make you very weak and can probably kill you!=Ves una araña venenosa ¡Ten cuidado! ¡Su veneno es muy fuerte! ¡Te debilitará y probablemente puede matarte! -twilight slime=baba del crepúsculo -You see a twilight slime, so strange and beautiful. Why is it coming towards you? Runnnnnnnnn!=Ves una baba del crepúsculo, tan extraña y bella ¿Por qué se está acercando hacia ti? ¡Correeeeeeeee! -young beholder=joven beholder -You see a young beholder. His huge eye will find you everywhere!=Ves un joven beholder ¡Su ojo gigante te encontrará en cualquier lugar! Defend yourself! I can see you!=¡Defiendete! ¡Puedo verte! -beholder=beholder -You see a beholder which can see you very well with its huge eye...=Ves un beholder que puede verte muy bien con su enorme ojo... -elder beholder=beholder anciano -You see an elder beholder, his nasty eye will scare you.=Ves un beholder anciano, su desagradable ojo te asustará. -chaos soldier=soldado del caos -chaos warrior=guerrero del caos Defy order! Seek chaos!=¡Desafía al orden! ¡Busca el caos! -chaos commander=comandante del caos Chaos in the wrath!=¡El caos en la ira! -chaos sorcerer=hechicero del caos I will summon chaos and fury upon you!=¡Invocaré el caos y la furia sobre ti! -chaos dragonrider=jinete de dragón del caos -You see a chaos dragonrider. He is very well trained in dragon riding, but he can also fight well on the ground.=Ves a un jinete de dragón del caos. Está muy bien entrenado en montar a dragón, pero también puede luchar bien en el suelo. -chaos lord=señor del caos Chaos will rule!=¡El caos reinará! -chaos overlord=gran señor del caos -You see a chaos overlord. It is he who directs the creation of evil plans and gives orders to all chaotic creatures which exist.=Ves a un gran señor del caos. Es él quien dirige la creación de los planes malvados y da las órdenes a todas las criaturas que existen. I am become death, destroyer of worlds.=Me he convertido en la muerte, en el destructor de mundos. -chaos pawn=peón del caos -You see a pawn for a game of chess. He will drop nothing if you kill him.=Ves un peón para una partida de ajedrez. No dejará caer nada si lo matas. -chaos rook=torre del caos -You see a rook for a game of chess. He will drop nothing if you kill him.=Ves una torre para una partida de ajedrez. No dejará caer nada si la matas. -chaos knight=caballo del caos -You see a knight for a game of chess. He will drop nothing if you kill him.=Ves un caballo para una partida de ajedrez. No dejará caer nada si lo matas. -chaos bishop=alfil del caos -You see a bishop for a game of chess. He will not reward you if you kill him.=Ves un alfil para una partida de ajedrez. No te recompensará con nada si lo matas. -chaos king=rey del caos -You see a king of a game of chess. He will reward nothing if you kill him.=Ves un rey para una partida de ajedrez. No te recompensará con nada si lo matas. -chaos queen=reina del caos -You see a queen piece for a game of chess. She will reward nothing if you kill her.=Ves una pieza reina para una partida de ajedrez. No te recompensará con nada si la matas. pirate=pirata -You see a bandit. Together with his partners he commits many different crimes to get much money or some rare and valuable items.=Ves a un bandido. Junto a sus compañeros comete muy diversos crímenes para conseguir mucho dinero o algunos objetos raros y valiosos. Aye, my hearties!=¡Sí, mis valientes! Yohoho and a boddle of rum!=¡Yohoho y una bodella de rón! pirate wench=chica pirata @@ -996,606 +2952,146 @@ pirate leader=líder de los piratas Be quiet or I will lock you on my ship, just like I locked up the girl Susi!=¡Estate quieto o te encerraré en mi barco, como encerré a esa niña llamada Susi! You remind me of the Ados girl we stole to clean our ship!=¡Me recuerdas a la niña de Ados que robamos para que limpie nuestro barco! You'd make a nice playmate for the Susi girl on our ship...=Serías un buen compañero de juegos para la niña llamada Susi en nuestro barco... -child dark elf=niño elfo oscuro -You see a child dark elf. You should know that dark elves are already quite strong and intelligent when they are children. Don't think you can fight him so easily!=Ves a un niño elfo oscuro. Deberías saber que los elfos oscuros son ya bastante fuertes e inteligentes cuando son niños ¡No creas que puedes enfrentarte a él tan fácilmente! -dark elf archer=arquero elfo oscuro -dark elf=elfo oscuro -dark elf elite archer=arquero de élite elfo oscuro -dark elf captain=capitán elfo oscuro Think not to defy the dark elves!=¡No pienses en desafiar a los elfos oscuros! -dark elf knight=caballero elfo oscuro -dark elf general=general elfo oscuro We will never return to the light!=¡Nunca regresaremos a la luz! -dark elf wizard=mago elfo oscuro The Spider Queen will avenge me!=¡La Reina Araña me vengará! -dark elf viceroy=virrey elfo oscuro Keep away from me, sunlight dweller!=¡Alejate de mí, morador de la luz! -dark elf ranger=explorador elfo oscuro Keep away from me, sunlight dweller!=¡Alejate de mí, morador de la luz! -dark elf sacerdotist=sacerdotisa elfa oscura The Queen of Spiders will take her revenge on you!=¡La Reina de las Arañas consumará su venganza sobre ti! -dark elf admiral=almirante elfo oscuro Fear the dark elf armies!=¡Temed a los ejércitos de los elfos oscuros! We are the darkness you fear!=¡Somos la oscuridad que temes! -dark elf master=Maestre elfo oscuro You will never see the light again!=¡No volverás a ver la luz! Darkness will fall upon you!=¡La oscuridad caerá sobre ti! -dark elf matronmother=matrona madre elfa oscura You disturbed my rest!=¡Has perturbado mi descanso! Now, you will pay.=Ahora, lo pagarás. -imp=diablillo -You see an imp. He may look a little bit cute, but don't forget he is one of the devil's servants!=Ves a un diablillo¡ Puede parecer un poquito lindo, pero no olvides que es uno de los siervos del demonio! -winged blue imp=diablillo azul alado -demon=demonio -You see a demon. He personifies evil.=Ves un demonio. Personifica el mal. -imperial demon servant=sirviente demonio imperial -imperial demon lord=señor demonio imperial -dwarf=enano -You see a dwarf who works in the mine to find some iron for us.=Ves a un enano que trabaja en la mina para encontrar un poco de hierro para nosotros. -dwarf guardian=guardián enano -You see a dwarf guardian who fights against the humans.=Ves a un guardián enano que lucha contra los humanos. -elder dwarf=enano anciano -You see an elder dwarf, a mighty warrior tired of fighting.=Ves a un enano anciano, un poderoso guerrero cansado de luchar. -hero dwarf=héroe enano -You see a hero dwarf. He is an old dwarf warrior who became a hero for fighting in many dwarf wars and he is very famous for his strength and brutality.=Ves a un héroe enano. Es un viejo guerrero enano que se convirtión en héroe por luchar en muchas guerras de enanos y es muy famoso por su fuerza y brutalidad. -duergar=duérgar -duergar axeman=hachero duérgar -leader dwarf=líder de los enanos -You see a leader dwarf. He is the most mighty dwarf of his tribe so he enjoys much popularity there.=Ves a un líder de los enanos. Es el enano más poderoso de su tribu así que disfruta de una gran popularidad allí. -elder duergar=duérgar anciano -duergar elite=duérgar de élite -duergar hero=héroe duérgar We mighty duergars will crush the dwarves!=¡Nosotros los poderosos duérgars aplastaremos a los enanos! -duergar wizard=mago duérgar -mountain dwarf=enano de montaña -You see a mountain dwarf. He likes earth, stones and rocks. Deep within the mountain, where no animal and no human has ever been, you can hear the sound of his pickaxe as he searches for gold and ore.=Ves a un enano de montaña. Le gusta la tierra, las piedras y las rocas. En lo profundo de la montaña, donde ningún animal ni ser humano ha estado nunca, puedes oir el sonido de su piqueta mientras busca mineral de oro y de hierro. -mountain dwarf guardian=guardián enano de montaña -You see a mountain dwarf guardian. He has to watch out for enemies and guards the mountain dwarfs while they are doing their work.=Ves a un guardián enano de montaña. Tiene que estar alerta de los enemigos y protege las montañas enanas mientras ellos hacen su trabajo. -black duergar=duérgar negro Dwarves are the enemy ... but we kill all who cross our path!=Los enanos son el enemigo... ¡pero matamos a quien se cruza en nuestro camino! -mountain elder dwarf=enano de montaña anciano -You see a mountain elder dwarf. For many years he worked deep in the mountain and found much gold and ore in his life. Soon he will be too old to do this work but at the moment he is still a strong dwarf.=Ves a un enano de montaña anciano. Durante muchos años trabajó en lo profundo de la montaña y encontró mucho mineral de oro y de hierro en su vida. Pronto será demasiado viejo para hacer este trabajo pero por el momento todavía es un enano fuerte. -mountain hero dwarf=héroe enano de la montaña -You see a mountain hero dwarf. Many, many dwarves admire him! He was a member of a dwarvish group which discovered a very big gold deposit!=Ves a un héroe enano de la montaña ¡Muchos, muchísimos enanos le admiran! ¡Fue miembro de un grupo de enanos que descubrió a depósito de oro muy grande! -mountain leader dwarf=líder de los enanos de montaña -You see a mountain leader dwarf. He is responsible for the mountain dwarves and their work, and also for the organization of the guardian troops. But in serious cases he will also fight on his own!=Ves a un líder de los enanos de montaña. Es responsable de las montañas de los enanos y de sus trabajos, y también de la organización de las tropas guardianas ¡Pero en situaciones graves también luchará por sí sólo! -duergar king=rey duérgar -giant dwarf=enano gigante -You see a giant dwarf. It is very hard for him to be a giant dwarf because some of the other dwarves don't really see him as one of them because of his height... But in his big heart he is a dwarf and in a battle he will fight against each enemy!=Ves a un enano gigante. Es muy duro para él ser un gigante por algunos de los otros enanos no le ven realmente como uno de los suyos por su estatura... Pero en su gran corazón se siente un enano y en la batalla luchará contra cada enemigo! -Dhohr Nuggetcutter=Dhohr Nuggetcutter -Lord Durin=Lord Durin -You see the mighty Lord Durin, an ancient dwarf who has led his race through countless battles.=Ves al poderoso Lord Durin, un antiguo enano que ha guiado a su raza a en incontables batallas. Don't call me Friend.=No me llames Amigo. -nymph=ninfa -You see a nymph. She loves nature very much! As you take a look at her she feels disturbed in sun-bathing and talking to the birds, perhaps she was best left alone.=Ves a una ninfa ¡Le gusta muchísimo la naturaleza! Al echarle un vistazo se siente molestada en su tomar el sol y en su hablar con los pájaros, quizá habría sido mejor dejarla a solas. -earth elemental=elemental de tierra -You see an earth elemental. The earth is quaking as it moves over the surface.=Ves a un elemental de tierra. La tierra tiembla mientras se mueve sobre su superficie. -fire elemental=elemental de fuego -You see a fire elemental and hardly can stand the heat.=Ves a un elemental de fuego y apenas puedes soportar el calor. -water elemental=elemental de agua -air elemental=elemental de aire -You see an air elemental and feel a fresh breeze past you.=Ves a un elemental de aire y sientes una fresca brisa que te atraviesa. -ice elemental=elemental de hielo -You see an ice elemental and sense a shiver on your spine.=Ves a un elemental de hielo y sientes un escalofrío en tu columna vertebral. -djinn=djinn -You see a djinn. He isn't caught in a bottle or lamp so someone must have let him free and gave him the order to kill you.=Ves a un djinn. No está atrapado en una botella o lámpara así que alguien debe de haberle soltado y le dió la orden de matarte. -elf=elfo -You see an elf which loves the forest and lives in peace.=Ves a un elfo al que le encanta el bosque y que vive en paz. -militia elf=elfo de la milicia -You see a militia elf which trains hard to beat you!=¡Ves a un elfo de la milicia que se entrena duramente para vencerte! -archer elf=arquero elfo -You see an archer elf which can do much damage with its wooden arrows.=Ves a un arquero elfo que puede hacer mucho daño con sus flechas de madera. -soldier elf=soldado elfo -You see a soldier elf, a good partner of the militia elves.=Ves a un soldado elfo, un buen compañero de la milicia de los elfos. -mage elf=maga elfa -You see a mage elf. She can feel pure magic when she stands outside in the forest and just frees her mind.=Ves a una maga elfa. Se puede sentir magia pura cuando se encuentra fuera en el bosque y simplemente libera su mente. -commander elf=comandante elfo -You see a busy commander elf. He has control over the army of elves.=Ves a un ocupado comandante elfo. Tiene el control del ejército de los elfos. -archmage elf=archimaga elfa -You see an archmage elf. She is pretty.=Ves a una archimaga elfa. Es guapa. -elf sacerdotist=sacerdotisa elfa -elf wizard=mago elfo -You see an elf wizard. He is a very wise elf who knows almost every kind of magic. He likes to use nature spells to make every plant and creature fertile, but he is also able to defend himself when he has to do.=Ves a un mago elfo. Es un elfo muy sabio que sabe casi cualquier tipo de magia. Le gusta usar hechizos de la naturaleza para hacer a cualquier planta o animal fértil, pero también es capaz de defenderse cuando tiene que hacerlo. -elf bodyguard=guardaespaldas elfo -ent=ent -You see an ent. First you think it is a tree but you will get a surprise when it starts to walk!=Ves a un ent !Primero piensas que es un árbol pero te llevas una sorpresa cuando empieza a caminar! -old ent=ent viejo. -You see an old ent. It has seen many years passing by and survived a lot of storms. You can see that it is very old because it has already lost all of its leaves and most of its bark is broken...=Ves a un viejo ent. Ha visto muchos años pasar y ha sobrevivido a muchas tormentas. Puedes ver que es muy viejo porque ya ha perdido todas sus hojas y la mayor parte de su corteza está rota... -entwife=esposaent -You see an entwife. It is stronger than an ent and lives in a group in an ancient forest.=Ves a una esposaent. Es más fuerte que un ent y vive en grupo en un bosque antiguo. -leprechaun=leprechaun -You see a leprechaun, you would be lucky if you found his treasure.=Ves a un leprechaun, tendrías suerte si encontrases su tesoro. They're always after my Lucky Charms!!=¡¡Siempre van en busca de mis Amuletos de la Suerte!! Clovers and rainbows... stars and horseshoes...=Tréboles y arco iris... estrellas y herraduras... -littlefairy=hadita -You see a little fairy, which is magical and nice.=Ves a una hadita, que es mágica y preciosa. -clurichaun=clurichaun -You see a clurichaun. He is rich, but you don't know if you are able to find his gold!=Ves a un clurichaun. Es rico, pero no sabes si serás capaz de encontrar su oro! Join me for a drink, lassie?=¿Te tomas una copa conmigo, muchacha? Floating blue balloons...=Globos azules flotando... -baby angel=bebé angel -You see a baby angel. Will it ever get a chance to grow up?=Ves a un bebé angel ¿Tendrá alguna vez la oportunidad de hacerse mayor? I want to grow to be mighty!=¡Quiero crecer para ser poderoso! -chick=pollito -You see a little chick, it doesn't like to be away from its mother.=Ves un pequeño pollito, no le gusta estar lejos de su madre. peep peep=pip pip peep peep peep=pip pip pip -piglet=cerdito -You see a piglet, a bit dirty.=Ves un cerdito, un poco sucio. oink=oink oink oink=oink oink shnuffle=shnuffle -mother hen=madre gallina -You see a mother hen, she will probably hurt you if you touch her sons.=Ves una madre gallina, probablemente te haga daño si tocas a sus hijos peep peep=pip pip peep peep peep=pip pip pip -ram=carnero -You see a ram, maybe you are able to shear it?=Ves un carnero, ¿quizá seas capaz trasquilarlo? Baaaaaa=Baaaaaa -beaver=castor -You see a beaver. It likes to build dams in the river.=Ves un castor. Le gusta construir presas en el río. -cow=vaca -You see a cow, taste her delicious milk.=Ves una vaca, prueba su deliciosa leche. Moooo moooo mooooooo=Moooo moooo mooooooo mooooo=mooooo -goat=cabra -You see a goat, which is wild and lives in the mountain.=Ves una cabra, que es salvaje y vive en la montaña. -horse=caballo -You see a horse, don't slap it or it will run away!=Ves un caballo, ¡no les des una palmada o saldrá corriendo! neigghhh neigghhh=neigghhh neigghhh -white horse=caballo blanco -You see a white horse, they love to stay with their brown friends.=Ves un caballo blanco, les gusta estar con sus amigos marrones. naayyy=naayyy -bull=toro -You see a bull, run away!!!=Ves un toro, ¡sal corriendo! MOOOOO!!!=MOOOOO!!! -grass snake=serpiente de hierba -You see a grass snake which is quite dangerous.=Ves una serpiente de hierba que es bastante peligrosa. sssSSSsssss...=sssSSSsssss... -frogman=hombre-rana -You see a frogman which lives in a lake which isn't very deep.=Ves a un hombre-rana que vive en un lago no muy profundo. -elite frogman=hombre-rana de élite -wizard frogman=mago hombre-rana -gargoyle=gárgola -You see a gargoyle, You should think that it is a statue, but no, it is alive!=Ves a una gárgola, Pensarías que es una estatua, pero no, ¡está viva! -elder gargoyle=gárgola anciana -You see an elder gargoyle, it is wise...=Ves a una gárgola anciana, es sabia... -dark gargoyle=gárgola oscura -You see a dark gargoyle, do you feel the fear?=Ves a una gárgola oscura, ¿sientes el miedo? -venom gargoyle=gárgola de veneno -You see a venom gargoyle, a hideous and poisonous creature.=Ves a una gárgola de veneno, una criatura horrible y venenosa. -cyclops=cíclope -You see a cyclops. It just has one eye.=Ves a un cíclope. Sólo tiene un ojo. -ogre=ogro -You see an ogre, it is nude and rude!=Ves a un ogro, ¡está desnudo y es rudo! -elder ogre=ogro anciano -You see an elder ogre, he's in the last moments of his life, take pity on him.=Ves a un ogro anciano, está en los últimos momentos de su vida, ten compasión de él. -ogre soldier=soldado ogro -You see an ogre soldier, smelling like it didn't have had a bath for years. Ewww.=Ves a un soldado ogro, huele como si no se hubiese bañado durante años. Puaj! -superogre=superogro -giantrat=ratagigante -You see a giant rat. It seems like it survived from an experiment which went wrong.=Ves una rata gigante. Parece que haya sobrevivido a un experimento que salió mal. -crocodile=crocodilo -You see a crocodile, it arises from the deep waters in the rivers.=Ves un crocodilo, surge de las aguas profundas de los ríos. Rrrrrrrrr=Rrrrrrrrr -dwarf golem=golem enano -You see a dwarf golem. A long time ago a dwarf got turned into a big rock by some mystical forces. To save their friend the other dwarves created a golem out of this rock! From this day the dwarf golem lives together with them.=Ves a un golem enano. Hace mucho tiempo un enano fue convertido en una gran roca por unas fuerzas místicas. Para salvar a su amigo los otros enanos crearon un golem ¡a partir de esta roca! Desde este día el golem enano convive con ellos. -giant=gigante Fee Fie Foe Fum!!= ¡¡Fi Fa Fo Fum!! I seek the blood of a Semos Man!=¡Busco la sangre de un Hombre de Semos! -elder giant=gigante anciano -You see an elder giant. For many years he lived alone and had to hide because he was hunted by humans! Now he is old and has decided not to hide any longer because he wants to see something else in his life.=Ves a un gigante anciano. Durante muchos años vivió sólo y tuvo que esconderse ¡porque era cazado por los humanos! Ahora es viejo y ha decidido no esconderse por más tiempo porque quiere ver otra cosa más en su vida. Gnomes! Run away!=¡Gnomos! ¡Huyamos! You may steal my money but you'll never take my heart!=¡Podéis robarme mi dinero pero nunca me robaréis mi corazón! -master giant= Maestre gigante Thou shalt die!= ¡Morirás! Worms will feast on your corpse!= ¡Los gusanos se darán un festín con tu cadáver! -ice giant=gigante de hielo -You see an ice giant. His skin is covered with frozen hoarfrost and his breath feels like the cold north wind.=Ves a un gigante de hielo. Su piel está cubierta de escarcha helada y su aliento se siente como el frío viento del norte. -amazoness giant=giganta amazona -You see an amazoness giant. She looks very beautiful but don't be deceived by that!=Ves a una giganta amazona ¡Parece muy hermosa pero no te dejes engañar por eso! -fallen angel=ángel caído -You see a fallen angel. When he turned away from good, he fell down from heaven.=Ves a un ángel caído. Cuando se apartó del bien, se cayó del cielo. You are DOOMED.=Estás CONDENADO I am here to destroy all people.=Estoy aquí para destruir a toda la gente. -black giant=gigante negro Who dare challenge me?=¿Quién se atreve a desafiarme? Leave me now, before it is too late!=¡Marchaos ahora, antes de que sea demasiado tarde! -angel=ángel -You see an angel. With his flame sword he guards the way and doesn't let you pass.=Ves a un ángel. Con su espada flamígera protege el camino y no te deja pasar. Meet thy maker!=¡Conoce a tu creador! Mere mortals cannot challenge me!=¡Los simples mortales no pueden desafiarme! -archangel=arcángel -You see an archangel. She wants to kill you because she doesn't want you in the clouds.=Ves a una arcángel. Quiere matarte porque no quiere que estés en las nubes. Leave now, before it is too late for you.=Marchate ahora, antes de que sea demasiado tarde para ti. Your destiny is not here.=Tu destino no está aquí. Neither the earth nor the sky has a place for you...=Ni la tierra ni el cielo tienen un lugar para ti... -dark angel=ángel oscuro -You see a dark angel. He is an strong evil angel, and for now he is under cover of darkness.=Ves a un ángel oscuro. Es un ángel poderoso y malvado, y por ahora está al amparo de la oscuridad. My will be done over land and sky!=¡Mi voluntad se hará en la tierra y en el cielo! Mine is the Power and the Glory!=¡Mío es el poder y la gloria! -dark archangel=arcángel oscuro -You see a dark archangel, after she fell from heaven her soul became as dark as she is now.=Ves a una arcángel oscura, después de que cayese del cielo su alma se volvió tan oscura como ella lo es ahora. I will strike you down with more power than you can imagine!!= ¡¡Yo te derribaré con más poder del que puedas imaginar!! My will be done!=¡Mi voluntad se hará! -gnome=gnomo -You see a gnome who lives in the mountains.=Ves a un gnomo que vive en las montañas. Giiiiiaaants! Run away!=¡Giganteeeeees! ¡Huyamos! -mage gnome=mago gnomo -You see a mage gnome, she cleans the dirty mountain.=Ves a una maga gnoma, ella limpia la sucia montaña. Giiiiiaaants! Run away!=¡Giganteeeeees! ¡Huyamos! -infantry gnome=gnomo de infantería -You see an infantry gnome who will protect his little town against the humans!=Ves a un gnomo de infantería que protegerá su pequeña aldea frente a ¡los humanos! Ready! Aim! Runnnnnnnnnnn=¡Preparados! ¡Apunten! ¡Corraaaaaaaaaaaan! -cavalryman gnome=soldado de caballería gnomo -You see a cavalryman gnome who does long travels to search for gold.=Ves a un soldado de caballería gnomo que realiza largos viajes en busca de oro. Ready! Aim! Runnnnnnnnnnn=¡Preparados! ¡Apunten! ¡Corraaaaaaaaaaaan! -goblin=goblin -You see a goblin, it is an evil and mischievous creature.=Ves a un goblin, es una criatura malvada y maliciosa. Umm! Tasty meat.=¡Umm! Carne sabrosa. -veteran goblin=veterano goblin -You see a veteran goblin, an old warrior...=Ves a un goblin veterano, un viejo guerrero... Breakfast time!=¡Hora de desayunar! -soldier goblin=soldado goblin -You see a soldier goblin, a young warrior.=Ves a un soldado goblin, un joven guerrero. -wooden golem=golem de madera -You see a wooden golem. The scroll with ancient words in his head keeps it walking.=Ves a un golem de madera. El pergamino con las antiguas palabras en su cabeza lo mantiene caminando. -stone golem=golem de piedra -You see a stone golem, it exists to protect and nothing else. What was it crafted to protect, and what would one do if it lost it's purpose?=Ves a un golem de piedra, existe para proteger y nada más ¿Qué fue diseñado para proteger, y qué haría si perdiese su propósito? -ice golem=golem de hielo -green dragon=dragón verde -giant kobold=kobold gigante -You see a giant kobold. Though generally playful it doesn't quite know it's own strength. When it asks for a hug, it's best to decline that offer.=Ves a un kobold gigante. Aunque por lo general es juguetón no conoce su propia fuerza. Cuando pide un abrazo, es mejor rechazar la oferta. Hug?=¿Un abrazo? -bone dragon=dragon de hueso -You see a bone dragon. It's been said that there are creatures with enough will to live that their souls still cling to life even as their flesh and blood has failed them. This is living proof.=Ves a un dragón de hueso. Se dice que hay criaturas con la suficiente voluntad de vivir como para que sus almas todavía se aferren a la vida aún cuando su carne y su sangre les han abandonado. -blue dragon=dragón azul -You see a blue dragon. It has a very beautiful blue skin which shimmers mysteriously in the light. But don't forget that it also has claws and teeth!=Ves a un dragón azul. Tiene un tono de piel azul muy bonito que brilla misteriosamente en la luz¡Pero no olvides que también tiene garras y dientes! -giant spider=araña gigante -You see a giant spider. It has very long legs which are quite dangerous.=Ves a una araña gigante. Tiene patas muy largas que son bastante peligrosas. -red dragon=dragón rojo -flying golden dragon=dragón de oro volador -You see a flying golden dragon. As it spreads its mighty wings you shiver... this mystical creature seems to have much power in it!=Ves a un dragón de oro volador. A medida que extiende sus poderosas alas te estremeces... ¡esta criatura mística parece tener mucho poder en su interior! -twin headed dragon=dragón de dos cabezas -You see a twin headed dragon. The two heads are staring at you with their four eyes!=Ves a un dragón de dos cabezas ¡Las dos cabezas te están mirando con sus cuatro ojos! -chaos green dragonrider=jinete de dragón verde del caos Wheee!!=¡¡Uiií!! It's fun up here!=¡Es divertido estar aquí arriba! -thing=cosa -You see a thing. It looks like a very mutated human and is so scary that you think it has to come from another world.=Ves a una cosa. Parece un humano muy mutado y es tan espantoso que crees que ha venido de otro mundo. -chaos red dragonrider=jinete de dragón rojo del caos Charrrrrrrrge!=¡A la cargaaaaaaaa! -black death=muerte negra -You see black death. With his scythe he reaps your soul and then he takes it over the river where it has to stay until judgement day.=Ves a la muerte negra. Con su guadaña siega tu alma y después se la lleva al río donde tiene que permanecer hasta el día del juicio. Your time to die has come!=¡Ha llegado tu hora de morir! Say goodbye to this world!=¡Di adiós a este mundo! -golden death=muerte de oro -You see the golden death. He came to take your soul away to pay for all your sins.=Ves a la muerte de oro. Vino a llevarse tu alma para que pague por todos tus pecados. You will dream of me tonight...=Soñarás conmigo esta noche... You will pay for your greed!=¡Pagarás por tu codicia! -risecia thug=matón risecia -You see a risecia thug, stay away from him!=Ves a un matón risecia, ¡mantente alejado de él! -risecia archer=arquero risecia -You see a risecia archer, just one of the warriors who wants to protect his chief.=Ves a un arquero risecia, sólo uno más de los guerreros que quieren proteger a su jefe. -risecia swordsman=espadachín risecia -You see a risecia swordsman, the blade of his sword will cut your head!=Ves a un espadachín risecia, ¡la hoja de su espada te cortará la cabeza! -Chief Falatheen of the Risecia=Jefe Falatheen de los Risecia -You see the Chief Falatheen of the Risecia, he doesn't want to die! His little warriors protect him against invaders.=Ves al Jefe Falatheen de los Risecia, ¡no quiere morir! Sus pequeños guerreros le protegen frente a los invasores. -cannibal woman=mujer caníbal -You see a cannibal woman. She wants to make a soup out of your bones!=Ves a una mujer caníbal ¡Quiere hacer una sopa con tus huesos! Zag-Zag!=Zag-Zag! -littlewitch=brujita -You see a little witch. Witches are are evil little girls who are very dangerous.=Ves a una brujita. Las brujas son niñitas malvadas que son muy peligrosas. -monk=monje The end of the world is nigh...=El fin del mundo está cerca... Yield to the Power!=¡sometete al poder! Meet you after Cataclysm.=Nos veremos después del Cataclismo. I see you, heretic!=¡Te estoy viendo, hereje! -littlewizard=pequeño mago -You see a little wizard. Although he is a child, he is already able to cast powerful spells.=Ves a un pequeño mago. Aunque es un crío, ya es capaz de lanzar poderosos hechizos. -kalavan housewife=ama de casa kalavan -You see a kalavan housewife. She lives in Kalavan city and she very angry with rude people who want to steal her food out of the kitchen and destroy her beautiful flower garden!=Ves a una ama de casa kalavan. Vive en la ciudad de Kalavan y ¡se enfada mucho con la gente ruda que quiere robar comida de su cocina y destruir su precioso jardín! -cannibal=caníbal -You see a cannibal. He hunts you for cooking and eating your fresh meat! It's useless to tell him that your flesh is not good, better fight him or run away!=Ves a un caníbal. Te caza para cocinar y comerse tu carne fresca! Ou-Garrrr!=Ou-Garrrr! -kalavan peasant=campesino kalavan -You see a kalavan peasant. He works in the fields around Kalavan city. As he heard there were strangers in the city he came to help the kalavan citizens in fighting the enemies!=Ves un campesino kalavan. Trabaja en los campos de alrededor de la ciudad de Kalavan. Cuando oyó que había extranjeros en la ciudad vino a ayudar a los ciudadanos de kalavan a luchar contra los enemigos! -kalavan wisewoman=mujer sabia de kalavan -You see a kalavan wisewoman. Normally she sits in her small room and does some handworking but now she wants to have some action!=Ves a una mujer sabia de kalavan. Normalmente se sienta en su pequeña habitación y hace labores manuales pero ahora quiere tener algo de acción! -kalavan citizen=ciudadano de kalavan -You see a kalavan citizen. He lives in kalavan city and tries to protect it from invaders!=Ves a un ciudadano de kalavan. Vive en la ciudad de kalavan ¡e intenta protegerla frente a los invasores! -elder cannibal=caníbal anciano Yummie!=mmmmm! -imperial defender=defensor imperial -bandit=bandido -You see a lean and mean bandit. He's out to get your life and purse!=Ves a un bandido preparado y listo para actuar ¡Hará lo que sea necesario para llevarse tu vida y tu dinero! -imperial veteran=veterano imperial -You see an imperial veteran. He fought in many battles and wars. Now he is old, but over the years he always looked for a new enemy to fight against.=Ves a un veterano imperial. Luchó en muchas batallas y guerras. Ahora es viejo, pero a lo largo de los años siempre ha buscado un nuevo enemigo contra el que luchar. -darkmonk=monje oscuro The Cataclysm cometh!=¡El cometa del Cataclismo! Hail to the greater Power!=¡Salve al Poder supremo! -barbarian=bárbaro -imperial archer=arquero imperial -disciple assassin=discípulo de asesino -You see a disciple assassin. He is quite young but he has learned to fight and kill already.=Ves a un discípulo de asesino. Es bastante joven pero ya ha aprendido a luchar y a matar. I'm gonna tell my mommy.=Se lo voy a decir a mi mamá. Teacher, make him stop.=Seño, digale que pare. My dad can beat your dad.=Mi papá puede ganar a tu papá. -imperial priest=sacerdote imperial -imperial elite guardian=guardián de élite imperial -barbarian wolf=lobo bárbaro -apprentice assassin=aprendiz de asesino -You see an apprentice assassin. He has been taught some very awesome moves and methods, so he is already able to kill an enemy almost perfectly!=Ves a un aprendiz de asesino. Le han enseñado algunos movimientos y métodos muy impresionantes, así que ya es capaz de matar a un enemigo casi perfectamente! Come on, i dare you.=Venga, a ver si te atreves. You want a piece of me?=¿Quieres un pedazo de mí? Any time you're ready.=Cuando estés listo. Bring it on!=¡Adelante! -hunter=cazador I always get my prey!=¡Siempre consigo a mi presa! -barbarian elite=bárbaro de élite -imperial scientist=científico imperial -You see an imperial scientist. He is quite clever but also a little bit crazy and unfortunately he uses his intelligence for evil experiments.=Ves a un científico imperial. Es bastante listo pero también un poquito loco y desafortunadamente usa su inteligencia para experimentos malvados. -barbarian priest=sacerdote bárbaro -imperial high priest=sumo sacerdote imperial -assassin=asesino -You see an assassin. He is very good at killing his victims silently without being seen by anyone.=Ves a un asesino. Es muy bueno matando a sus victimas silenciosamente sin ser visto por nadie. Time to die.=Es hora de que mueras. -barbarian chaman=chamán bárbaro King he say kill!=¡El rey ha dicho que te mate! -imperial archer leader=líder arquero imperial -barbarian leader=líder bárbaro Me destroy you!=¡Yo destruyo tú! -mithrilbourgh soldier=soldado de mithrilbourgh -imperial elite archer=arquero de élite imperial -ninja=ninja -imperial leader=líder imperial -mithrilbourgh archer=arquero de mithrilbourgh -mithrilbourgh knight=caballero de mithrilbourgh -imperial knight=caballero imperial -imperial chief=jefe imperial -mithrilbourgh elite archer=arquero de élite de mithrilbourgh -barbarian king=rey bárbaro -mithrilbourgh sergeant=sargento de mithrilbourgh -samurai=samurai -imperial commander=comandante imperial Our troops will crush you!=¡Nuestras tropas os aplastarán! -mithrilbourgh lieutenant=lugarteniente de mithrilbourgh -mithrilbourgh colonel=coronel de mithrilbourgh -mithrilbourgh wizard=mago de mithrilbourgh -shadows assassin=asesino de las sombras -You see a shadows assassin. He knows how to become one with the shadows and to move in the dark without being seen or making any sound.=Ves a un asesino de las sombras. Sabe como hacerse uno con las sombras y moverse en la oscuridad sin ser visto o hacer ningún sonido. -imperial general=general imperial Our troops will crush you!=¡Nuestras tropas os aplastarán! -mithrilbourgh general=general de mithrilbourgh -emperor dalmung=emperador dalmung My empire is undefeatable!=¡Mi imperio es inderrotable! -blordrough quartermaster=intendente de blordrough We might be in hiding, but we are STILL mighty.=Puede que nos escondamos, pero TODAVÍA somos poderosos. -imperial general giant=general imperial gigante -master assassin=Maestre asesino -You see a master assassin. He is a master of assassination! He is able to kill an enemy without letting him know that he is already there!=Ves a un Maestre asesino ¡Es un maestro del asesinato! ¡Es capaz de matar a un enemigo sin dejar que sepa que ya está allí! You've been warned before.=Ya te había advertido. Why do you return?=¿Por qué has vuelto? Have you lost your marbles?=¿Has perdido tus canicas? -blordrough corporal=cabo de blordrough You WILL show us the respect we deserve.=Tú nos MOSTRARÁS el respeto que merecemos. Care for a game of chess?=¿Te apetece una partida de ajedrez? -blordrough storm trooper=soldado de asalto de blordrough Wipe your feet before entering our domain.=Limpiate los pies antes de entrar en nuestro dominio. I'll teach you to defile our sanctuary.=Te voy a enseñar a profanar nuestro santuario. -tiny skelly=pequeño esqueletito -You see a tiny skelly. It is silly, little and evil, be careful with its poison...=Ves a un pequeño esqueletito. Es tonto, pequeño y malvado, ten cuidado con su veneno... Thou shall not maim!=¡Tú no mutilarás! -brain ahouga=cerebro ahouga -You see a brain ahouga, a very strange creature only living in your dreams!=Ves a un cerebro ahouga, ¡una criatura muy extraña que sólo vive en tus sueños! -red roohako=roohako rojo -robot aruthon=robot aruthon -violet avenger=vengador violeta -googon=googon -You see a googon that comes from the worst place in this world...Be careful this creature is a deadly killer.=Ves a un googon que viene del peor lugar de este mundo... Ten cuidado esta criatura es una asesina mortal. -blue mohiko=mohiko azul -snarfkin=snarfkin -You see a snarfkin, the favorite pet of wizards. Be careful, its claws are poisonous!=Ves a un snarfkin, la mascota favorita de los magos. Ten cuidado, ¡sus garras son venenosas! -dark aruthon=aruthon oscuro -grobble monster=monstruo grobble grobble grubble grobble=grobble grubble grobble gribble grobble grubble=gribble grobble grubble -winged blurble=blurble alado -xenocium=xenocium -necrosophia=necrosophia -arachne=arachne -You see Arachne, a horrid half woman half spider. With so many legs she can hit you very fast, and her poisonous bite is fierce.=Ves a Arachne, un ser horrible mitad mujer mitad araña. Con tantas piernas puede golpearte muy rápido, y su mordedura venenosa es feroz. -kobold=kobold -You see a kobold. People say that it is a mythical spirit.=Ves a un kobold. La gente dice que es un espíritu mítico. Grrrr! Wooof!=Grrrr! Wooof! -soldier kobold=soldado kobold -You see a soldier kobold, he's protecting his king.=Ves a un soldado kobold, está protegiendo a su rey. Woorf wer warf!=Woorf wer warf! -archer kobold=arquero kobold -You see an archer kobold. It runs away from humans.=Ves a un arquero kobold. Sale corriendo de los humanos. Wssss! Warf wit!=Wssss! Warf wit! -veteran kobold=veterano kobold -You see a veteran kobold, it is mighty!=Ves a un veterano kobold, ¡es poderoso! -leader kobold=líder kobold -You see a leader kobold, one of the mighty warriors.=Ves a un líder kobold, uno de los poderosos guerreros. -madaram archer=arquero madaram -madaram peasant=campesino madaram -madaram trooper=soldado de asalto madaram -madaram soldier=soldado madaram -madaram healer=sanador madaram -madaram axeman=hachero madaram -madaram queen=reina madaram -madaram hero=héroe madaram -madaram windwalker=windwalker madaram -madaram cavalry=caballería madaram -madaram stalker=acechador madaram -madaram buster blader=buster blader madaram -madaram pawn=peón madaram -You see a pawn for a game of chess. It will reward nothing if killed.=Ves un peón para una partida de ajedrez. No te recompensará con nada si lo matas. -madaram rook=torre madaram -You see a rook for a game of chess. It will scarcely reward you to kill it.=Ves una torre para una partida de ajedrez. Apenas te recompensará con algo si la matas. -madaram knight=caballo madaram -You see a knight, a chess piece. He won't reward you if you kill him.=Ves un caballo, una pieza de ajedrez. No te recompensará con nada si lo matas. -madaram bishop=alfil madaram -You see a bishop for a game of chess. It will reward you nothing if you slay it.=Ves un alfil para una partida de ajedrez. No te recompensará con nada si lo matas. -madaram king=rey madaram -You see a king to play chess with, he will reward you nothing if you kill him.=Ves un rey para jugar al ajedrez con él, no te recompensará con nada si lo matas. madaram queen =reina madaram -You see a queen piece for a game of chess. She will reward nothing if you kill her.=Ves una pieza reina para una partida de ajedrez. No te recompensará con nada si la matas. -kasarkutominubat=kasarkutominubat -minotaur=minotauro -You see a minotaur. He escaped from his labyrinth and is very angry now!=Ves a un minotauro ¡Ha escapado de su laberinto y ahora está muy enfadado! You will never find the way out!=¡Nunca encontrarás la salida! -minotaur king=rey minotauro I am the king of this area!=¡Soy el rey de esta área! -mummy=momia -You see a mummy which usually sleeps in a coffin.=Ves a una momia que suele dormir en un sarcófago. -royal mummy=momia real -You see a royal mummy. You wonder what keeps this king's soul from finding peace.=Ves a una momia real. Te preguntas qué le impide al alma de este rey encontrar la paz. -glow monster=monstruo del resplandor -dark mutant=mutante oscuro -cat woman=mujer gata -naga=naga -werewolf=hombre lobo -You see a werewolf. Normally he is a human but at fullmoon nights he turns into a dangerous beast! His poison seems to last forever.=Ves a un hombre lobo. Normalmente es un humano pero ¡en las noches de luna llena se convierte en una bestia peligrosa! Su veneno parece durar para siempre. -high naga=naga suprema -imperial experiment=experimento imperial -imperial mutant=mutante imperial Ouurgghh!=Ouurgghh! -armed naga=naga con brazos -black dragon=dragón negro -You see a black dragon. As you see this huge black creature you almost faint! If you concentrate you can feel the deep growling of it in your bones.=Ves un dragón negro ¡Al ver a esta enorme criatura negra casi te desmayas! -balrog=balrog -You see a huge balrog. He is slow hitting, but terribly strong as he stands towering above you.=Ves a un enorme balrog. Es lento en golpear, pero terriblemente potente mientras permanece de pie imponiendose por encima de ti. Growwwwwwwrrrrrrr!=Growwwwwwwrrrrrrr! Arrrrllllggggghhhh=Arrrrllllggggghhhh -gashadokuro=gashadokuro -A giant figure made of bone, known only as gashadokuro, towers before you. He is created from the bones of starving humans.=Una figura gigante hecha de hueso, conocida sólo como gashadokuro, se impone ante ti. Se ha creado a partir de los huesos de humanos muertos de hambre. -unicorn=unicornio -You see a rare unicorn. It is widely believed to be only a myth.=Ves un raro unicornio. En general se cree que es sólo un mito. -centaur=centauro - You see a noble centaur, half man, half horse. He looks capable of great damage but thankfully seems to mean you no harm.=Ves a un noble centauro, mitad hombre, mitad caballo. Parece capaz de causar gran daño pero afortunadamente no parece tener intención de herirte. -pegasus=pegaso -You see Pegasus, the winged horse. His name means Lightning and he carries bolts of Thunder.=Ves a Pegasus, el caballo alado. Su nombre significa Relámpago y lleva los rayos del Trueno. -oni warrior=guerrero oni -oni archer=arquero oni -oni priest=sacerdote oni -You see a mysterious oni priest, uttering strange incantations.=Ves a un misterioso sacerdote oni, pronunciando extraños encantamientos. -oni king=rey oni Revere me, the Oni King!=¡Veneradme, al Rey Oni! -oni queen=reina oni You must bow before the Oni Queen.=Debes inclinarte ante la Reina Oni. -orc=orco -You see an orc which is noisy and silly.=Ves a un orco que es ruidoso y tonto. -orc spearman=lancero orco -You see an orc spearman. Run away fast before a spear pierces you.=Ves a un lancero orco. Sal corriendo rápido antes de que una lanza te atraviese. -orc warrior=guerrero orco -You see an orc warrior which doesn't like you searching in the mountain.=Ves a un guerrero orco al que no le gusta que andes buscando en la montaña. -orc hunter=cazador orco -You see an orc hunter. It it seeking for you!=Ves a un cazador orco ¡Te está buscando a ti! -orc chief=jefe orco -You see an orc chief which is too busy to attend to you!=Ves a un jefe orco ¡que está demasiado ocupado para atenderte! -mountain orc=orco de montaña -mountain orc warrior=guerrero orco de montaña -mountain orc hunter=cazador orco de montaña -mountain orc chief=jefe orco de montaña -ratman=hombre rata -You see a ratman. He is a mutation that comes from old times.=Ves a un hombre rata. Es una mutación que procede de tiempos remotos. You, humans, never leave us alone!=¡Vosotros, los humanos, nunca nos dejáis en paz! -ratwoman=mujer rata -You see a ratwoman. She is a mutation of women with the rats in the century XVIII.=Ves a una mujer rata. Es una mutación de mujeres con ratas en el siglo XVIII. You humans never leave us alone!=¡Vosotros los humanos nunca nos dejáis en paz! -archrat=archirrata -You see an archrat. You will be distracted with its aura, be careful...=Ves una archirrata. Te distraerás con su aura, ten cuidado... You're messing with the rats?!=¡¿Te estás metiendo con las ratas?! Your death has come!=¡Tu muerte ha llegado! Feel my wrath!=¡Sufre mi ira! -sheepman=hombre oveja I'm no fluffy lamb!=¡No soy un esponjoso cordero! -armored sheepman=hombre oveja blindado You're baaaarmy to attack me!=¡Estás chiflado al atacarme! -elder sheepman=hombre oveja anciano Bah! Stay away!=¡Bah! ¡Mantente alejado! -elite sheepman=hombre oveja de élite Baaaaack away!= ¡Retrocede! -rat=rata -You see a small rat that seems to be extremely hungry.=Ves una pequeña rata que parece estar extremadamente hambrienta. Squeak! Squeak!=¡Cuic!¡Cuic! -mouse=ratón -You see a small mouse that seems to be very hungry.=Ves un pequeño ratón que parece estar muy hambriento. squeak ... squeak!=¡cuic ... cuic! -caverat=rata de cueva -You see a caverat which is dirty and loves the dungeons.=Ves una rata de cueva que es sucia y le encantan las mazmorras. -razorrat=rata de cuchilla -You see a razorrat, its noise will make you crazy!=Ves una rata de cuchilla, ¡su ruído te hará perder la cabeza! -venomrat=rata de veneno -You see a venomrat, extremely poisonous.=Ves una rata de veneno, extremadamente venenosa. -zombie rat=rata zombi -You see a hungry zombie rat and it doesn't like to see you.=Ves una hambrienta rata zombi y a ella no le gusta verte a ti. SHREEAK!!=SHREEAK!! SHAAAAAAA!!=SHAAAAAAA!! -troll=trol -You see a troll, be careful in his terrain.=Ves a un trol, ten cuidado en su terreno. -red troll=trol rojo -You see a red troll, he's burning with anger.=Ves a un trol rojo, está quemando de ira. -cave troll=trol de cueva Uggga yggga arghhhhh=Uggga yggga arghhhhh Ooggga UGGga arhhh=Ooggga UGGga arhhh -skeleton=esqueleto -You see a skeleton which survives to get revenge.=Ves a un esqueleto que sobrevive para vengarse. Thou shall not pass!=¡No pasarás! -ghost=fantasma -You see a ghost, don't be afraid! It doesn't seem to be evil.=Ves un fantasma, ¡no tengas miedo! No parece ser malvado. Ooooouuuuuh!=Ooooouuuuuh! -ghost hound=perro fantasma -As you catch sight of its shaggy shape and gleaming evil eyes, you suddenly remember all the horror stories around the dreaded ghost hound!=Al avistar su forma peluda y sus malvados ojos brillantes, de repente recuerdas todas las historias de terror acerca del temido perro fantasma! WooOouuuUuh!=WooOouuuUuh! Ghhrr!=Ghhrr! -bloody zombie=zombi sangriento -You see a bloody zombie. Can you see the drops of blood that fall from his body?=Ves a un zombi sangriento ¿Puedes ver las gotas de sangre que caen de su cuerpo? brrraaaaaaaaaains=cereeeeeeeebrooooos -zombie=zombi -You see a zombie. It woke up just to take revenge, be silent.=Ves a un zombi. Se despertó solo para vengarse, estate en silencio. -headless monster=monstruo sin cabeza -You see a headless monster, how can he hear that we come to him?=Ves a un monstruo sin cabeza, ¿Cómo ha podido oír que nos acercábamos a él? -warrior skeleton=guerrero esqueleto -You see a warrior skeleton, it's just a normal skeleton with a shield!=Ves a un guerrero esqueleto, ¡es sólo un esqueleto normal con un escudo! Thou shall not kill!=¡No matarás! -elder skeleton=esqueleto anciano Thou shall not survive!=¡No sobrevivirás! -rotten zombie=zombi podrido -You see a rotten zombie. You are disgusted as you see its rotten skin that hangs down in pieces from his half rotten bones.=Ves a un zombi podrido. Estás asqueado al ver su piel podrida que cuelga hecha jirones de sus huesos medio podridos. bluuergghhhoooorrghhhhh=bluuergghhhoooorrghhhhh -death=muerte -You see Death. Do you have the will to stare him in the face? Today he's not in the mood to bargain with your soul.=Ves a la Muerte ¿Tendrás el valor de mirarla a la cara? Hoy no está de humor para negociar con tu alma. Judgement is coming!=¡El juicio se acerca! Uuuuhhhhh!, i'm gonna get you!=¡Uuuuhhhhh!, te voy a coger! -demon skeleton=esqueleto demonio Thou shall not rob!=¡No robarás! -invisible man=hombre invisible -You see nothing but the landscape. As you stare at the point where you think you have seen something, you suddenly see the slight contour of a man!=No ves nada excepto el paisaje. Al mirar fijamente al punto donde crees haber visto algo, de repente ¡ves el ligero contorno de un hombre! My unnatural state should evoke fear in you ...=Mi estado no natural debería evocar el miedo en ti... -death knight=caballero de la muerte -You see a death knight. Some time ago he fought in tournaments where his only aim was to kill as many enemies as possible.=Ves a un caballero de la muerte. Hace algún tiempo luchó en torneos donde su único objetivo era matar tantos enemigos como fuese posible. -devil queen=reina del diablo Who wakes me from my meditation?=¿Quien me ha despertado de mi meditación? You will regret this!=¡Lo lamentarás! -fallen warrior=guerrero caído -green phantom=fantasma verde -lich=lich -You see a lich, a dark desolate creature which has been doomed to wander in endless turmoil, feeding on the souls of the righteous.=Ves a un lich, una criatura oscura solitaria que ha sido condenada a vagar en confusión sin fin, alimentándose de las almas de los justos. -fallen priest=sacerdote caído -incorporeal armor=armadura incorpórea -You see an incorporeal armor. You are quite surprised as it walks towards you because there is nobody in it.=Ves una armadura incorpórea. Te sorprende bastante al caminar hacia ti porque no hay nadie dentro de ella. Thou shall not pillage!=¡No saquearás! -dead lich=lich muerto -fallen high priest=sumo sacerdote caído -high lich=lich supremo -The terrible grinning skull of a high lich leers at you as it prepares to slaughter you in a myriad of magical ways.=La terrible sonrisa de la calavera de un lich supremo te mira de reojo al prepararse para matarte de una miríada de mágicas maneras. -devil monk=monje del diablo -You see a devil monk. He is a member of a devil cult which practises black rituals.=Ves a un monje del diablo. Es miembro de un culto al diablo que practica negros rituales. -vampire bride=novia vampira -You see a vampire bride. She was resurrected from the dead by a wicked vampire lord.=Ves a una novia vampira. Fue resucitada de entre los muertos por un malvado señor vampiro. -vampirette=vampiresa -vampire lord=señor vampiro -You see a vampire lord. Be careful, he is after your blood!=Ves a un señor vampiro. Ten cuidado, ¡va tras tu sangre! diff --git a/data/languages/template.txt b/data/languages/template.txt index 301920f472..3d244004c6 100644 --- a/data/languages/template.txt +++ b/data/languages/template.txt @@ -1097,6 +1097,7 @@ venom gland= You see the venom gland of a giant king cobra. It is filled with deadly toxin.= pelt= +You see a pelt of animal skin. It's used to make clothes and leather armor.= leather thread= You see a strand of sturdy leather thread.= @@ -1265,6 +1266,9 @@ You see an imperial ring. It is imbued with the strength of a king.= enhanced imperial ring= You see an enhanced imperial ring. It eminates the auras of strength and defense.= +ring of clarity= +You see a ring of clarity. You feel more aware while wearing it.= + ## scrolls @@ -1407,6 +1411,9 @@ You see a silvery gm token. Does it bring you pride?= bronze gm token= You see a bronze gm token. Does it bring you pride?= +four-leaf clover= +You see a lucky four-leaf clover. What are the chances of finding one!?= + ## swords @@ -1635,6 +1642,7 @@ You see a killer bat which will suck your blood!= giant bat= giant killer bat= +You see a giant killer bat. As if small wasn't pesky enough, these giant ones are capable of taking down anyone who dares come before them.= piglet= You see a piglet, a bit dirty.= @@ -1702,6 +1710,7 @@ black bear= You see a strong black bear which hibernates in winter.= polar bear= +You see a polar bear which lives in the icy desert. Beware! It can take you down with its powerful claws!= ## animal_aquatic @@ -2268,6 +2277,7 @@ cannibal= You see a cannibal. He hunts you for cooking and eating your fresh meat! It's useless to tell him that your flesh is not good, better fight him or run away!= elder cannibal= +You see an elder cannibal. For years it has eaten humans. It will not stop until its hunger is satisfied.= cannibal warrior= You see a cannibal warrior.= diff --git a/data/maps/Level -1/semos/mine_n2_e2.tmx b/data/maps/Level -1/semos/mine_n2_e2.tmx index d9d3c9d1e5..d425eecce5 100644 --- a/data/maps/Level -1/semos/mine_n2_e2.tmx +++ b/data/maps/Level -1/semos/mine_n2_e2.tmx @@ -1,212 +1,112 @@ - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - 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 -Zyr+BYtELmkAAAEA + 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 -cswLAAAAIEcfGQAAiGYFWjgd9QAAAQA= + H4sIAAAAAAAACu3bMQrAIAwFUHOrnqU6tPe/RCkOHUUQrPjelDGQ5ROSlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2dcbsDpjpMn8AoEORHQAAaMhRc6O9437eXeMdXw0AAAAAAAAAAACMcbjLAwAW5b8A+sn/AAAAAAAAAAAAAPAvD8b9VLMAAAEA - + - H4sIAAAAAAAAAO3c20rDQBAG4Lz/O1lBL6q3HloVD/sceuMUWppeGKOwmbL7ffBDSgM7sJDdzEKG -AQAAAAAAAAAAAIBaPiNf2UUAAAAAAAAAAAAAQEdKdgEAAFRVRtebrCJY1F3kPrsI0mwjTxP/l4Xq -IMdr5C3yuP/tud+f58hLdhGkeY98ZBcB/MvDcFy/6Y/1u2/Wb8b0dNq3e0//6Rtbv/V0aNuhpwMA -QJt27wIluwigqpJdAGnKjHv0/fqm79e+qbNefb/2Oevtm7NeDuz3+ma/1zf7PQAAAAAAAAAAMlxE -VtlFcGI8J7eVx7qKXFceg79Zck7WkZuFxmKeqTm5rDhu7WcNAHA+rPsA0Laa/QMAAAAAAAAAAAAA -AADOxzeEbpmpAAABAA== + H4sIAAAAAAAACu3c20rDQBAG4Lz/O1lBL6q3HloVD/sceuMUWppeGKOwmbL7ffBDSgM7sJDdzEKGAQAAAAAAAAAAAIBaPiNf2UUAAAAAAAAAAAAAQEdKdgEAAFRVRtebrCJY1F3kPrsI0mwjTxP/l4XqIMdr5C3yuP/tud+f58hLdhGkeY98ZBcB/MvDcFy/6Y/1u2/Wb8b0dNq3e0//6Rtbv/V0aNuhpwMAQJt27wIluwigqpJdAGnKjHv0/fqm79e+qbNefb/2Oevtm7NeDuz3+ma/1zf7PQAAAAAAAAAAMlxEVtlFcGI8J7eVx7qKXFceg79Zck7WkZuFxmKeqTm5rDhu7WcNAHA+rPsA0Laa/QMAAAAAAAAAAAAAAADOxzeEbpmpAAABAA== - + - H4sIAAAAAAAAAO3OAQ0AAAgDoPfPZyBj3E1IQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwy7QD -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPLYaZVTEAAAEA + H4sIAAAAAAAACu3OAQ0AAAgDoPfPZyBj3E1IQAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwy7QDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPLYaZVTEAAAEA - + - H4sIAAAAAAAAAO3cSw6CMBQF0DrC3xZcjTt2hcbEiaYxUmheP+dMmDC4CeTSPgIpAQAAAAAAAAAR -ztEBAAAAAAAABnKJDgAAAAD8tLyP9vAwj2t0AAAAAEjmUbMqmUvcDrvH2KzFTD3TB+P7vsZL9iyA -+jxzAKjN+3igVK4/aq1fzbbaZWYCvJhfAAAAsMYxOgAAAAAAAEzMNwQwN//zBQAAACDHvAgAAOhN -j/+mu3eY2X6xXI/XG3K29IAOyfunH3TIJ/fSmE7RAYDm6HsAAEZjxsNa7hmAfelVAIB5WQsCAAA9 -e9jTAAATewI4cp9TAAABAA== + H4sIAAAAAAAACu3cQQqDMBAF0Liytr1C73/NIrgqoVA1nZh5b+PGxQdlNF9MKQAAAAAAAABAhHt0AAAAAAAAgIE8ogMAAAAAX83b0Roe8nhGBwAAAICij8pqTy/xmk6PcViPma7MPBjf5zWeq2cBtOeZA0BrvscDe9XmR6v3V91Wv3QmwEp/AQAAwC9u0QEAAAAAACAx/xBAbvbzBQAAAKBGXwQAAFyNven+w3oRODIHzBDO4l4a0xIdAOiOeQ8AAAAAAAAAAAAAAJDXG8ZnpKUAAAEA - + - H4sIAAAAAAAAAO2T22prQQxD91P//5NLHgIh5DIX20ueSCBaOGePZS3377quP9u2bdu2bdu2bdu2 -bVver0Rnshnud9HZbI77/ad9nr+JzmfXcH4nOrfNsb8Estsc+0sgv81wvwR2sDn+dH47h/PIN/RO -dizzGdG72Qz3S2A/e4/57o3Qu9rz3Ee/e/63x/9P72rPa+cdeld7jX3UW/S+dh6rqHdshjudzTZ7 -u475o+icNsf+Eshqc+wvgbw2x/4SyGznMh+9C3oPO579XSPf0LvYMfx3b4beyV5nGPXOyls2x/1K -ejNqhq3JfuUGIubZ+2yq51VmsBn2K/dAd3SiFbv+JrqzE6ze7zfR+Tq7S6cjojN2c6ceR0Xn7OCu -3c2Izqrm7n2tiM5M+6SOVkXnVu6JzlrBn9y5Ko/i7qr8H9U1T9a7Kr4efj7/niUF/pGiGSr9XY2o -S85R0Rwz2Gf2bv467rBDhXbm0P1EdErnofnviu5opVM6A71/puj97Phb2P3G1vcnrd7NpzeiMj/m -oTvs7ndauZlPyspL99fd77Tz7TdF5Ft5y46/gZ17IDJG71CRqdMNrHY5+60a88x8nW9Atd8q0Uwj -O6mY8Wn2ThZKNFPFG4hg0oH9XTTTSD7k7a3mUBDNMpoHNW9lvoJolh1uYEbmz99AxpxRXubP30Dl -re3MVhDNL4sLwX52roJWstLMR3qsurGdeQqazUqznuWTPWNnloJO4F91A59mmb/5m//v3oD5847Y -c3VmRuYKreSkO1vpk75NxRtYzZixW3afNONf41+Va2Y2zVntBnbyVe2S1SfNW+EGMvhnZZzN2/EG -qhXNv0IZndLcCf672brw73YD2V1G5enEv+sNRPYanYFg/2l+ZI8076g7yJzdkf9MZpq1mr/1Qyrr -74nuXMkj3VCK3Cv6/VM80gml6N2y5nT2SBcKyr4FmoMK/3dd0MraNXpGN4/2QCt756xZ6h7dn1Yl -/4yZqh7dXUVV+0fMysxdzf/kGxjdLbPb3ezPM0/mfxPBf3VuVv6IjDNvqEn9Bkj9Av+bom9gZlc1 -/n9Pv/8C/5uybmB2Z5XOdvdTuedZqdwBpai9Ondw06/dQeQ+3XZ/pUz+EX1EdZmxwwn8b6q6AbKX -av7dbuCmyjuo7Ccrr9KOUaq+gczuKvKY/+94pa+OontW9UpX3UV3ruTVjk4Q3b2CV/s5TTSHbvx9 -B2c4uotTRHNR4B/VQ3fRjEj+Uft3F82J5B/dQWfRvEj+0R10FM1LgX9VD9manU2zUuqYzn1KftqR -oncZ2YvOpeYMmU0fv2KgJrqjk32i6E47WUVkFppBR/5KtxMlmgXt7orageag6l8U3bmqnzs6XXTf -9vc7rBC9b7T/AUzTPD0AAAEA + H4sIAAAAAAAACu2T22prQQxD91P//5NLHgohNMlcbC95tgSihXP2WNZyf67r+rFt27Zt27Zt27Zt27bl/Z/oTDbD/U90Npvj/vfTPs/fROezazi/E53b5thfAtltjv0lkN9muF8CO9gcfzq/ncN55Bt6JzuW+Yzo3WyG+yWwn73HfPdG6F3tee6j373+2/P/p3e157XzDr2rvcY+6i16XzuPVdQ7NsOdzmabvV3H/Fl0TptjfwlktTn2l0Bem2N/CWS2c5mP3gW9hx3P/k8j39C72DH8d2+G3sleZxj1zspbNsf9SnozaoatyX7lBiLm2ftsqudVZrAZ9iv3QHd0ohW7/ia6sxOs3u830fk6u0unI6IzdnOnHkdF5+zgrt3NiM6q5u59rYjOTPukjlZF51buic5awZ/cuSqP4u6q/J/VNU/Wuyq+nn6+/p4lBf6Rohkq/V2NqEvOUdEcM9hn9m7+Ou6wQ4V25tD9RHRK56H574ruaKVTOgO9f6bo/ez4W9j9xtb3J63ezac3ojI/56E77O53WrmZT8rKS/fX3e+08+03ReRbecuOv4GdeyAyRu9QkanTDax2OfutGvPMfJ1vQLXfKtFMIzupmPFp9k4WSjRTxRuIYNKB/Z9oppF8yNtbzaEgmmU0D2reynwF0Sw73MCMzJ+/gYw5o7zMn7+Bylvbma0gml8WF4L97FwFrWSlmY/0WHVjO/MUNJuVZj3LJ3vGziwFncC/6gY+zTJ/8zf/+96A+fOO2HN1ZkbmCq3kpDtb6ZO+TcUbWM2YsVt2nzTju/GvyjUzm+asdgM7+ap2yeqT5q1wAxn8szLO5u14A9WK5l+hjE5p7gT/3Wxd+He7gewuo/J04t/1BiJ7jc5AsP80P7JHmnfUHWTO7sh/JjPNWs3f+iGV9fdEd67kkW4oRe4V/f4pHumEUvRuWXM6e6QLBWXfAs1Bhf+7Lmhl7Ro9o5tHe6CVvXPWLHWP7k+rkn/GTFWP7q6iqv0jZmXmruZ/8g2M7pbZ7W7215kn83+I4L86Nyt/RMaZN9SkfgOk7sD/oegbmNlVjf/Py+934P9Q1g3M7qzS2e5+Kvc8K5U7oBS1V+cOHrrbHUTu0233/5TJP6KPqC4zdjiB/0NVN0D2Us2/2w08VHkHlf1k5VXaMUrVN5DZXUUe87+PV/rqKLpnVa901V1050pe7egE0d0reLWf00Rz6Mbfd3CGo7s4RTQXBf5RPXQXzYjkH7V/d9GcSP7RHXQXzYziH71/V9HMaP5VHaiKZqbQMZ2bvBF6Z9X+6F2q7oLeTb2n1Zl0F3f0fwzURHd0sk8U3Wknq4jMQjPoyF/pdqJEs6DdXVE70BxUfUfRnav6taPTRfdtf7/DCtH7RvsXoMjHOgAAAQA= - + - H4sIAAAAAAAAAO3BAQEAAACAkP6v7ggKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAauuOl9cAAAEA + H4sIAAAAAAAACu3BAQEAAACAkP6v7ggKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAauuOl9cAAAEA diff --git a/data/maps/world/sub_1_semos_dungeon.png b/data/maps/Level -1/semos/sub_1_semos_dungeon.png similarity index 100% rename from data/maps/world/sub_1_semos_dungeon.png rename to data/maps/Level -1/semos/sub_1_semos_dungeon.png diff --git a/data/maps/Level 0/semos/mountain_n2_w.tmx b/data/maps/Level 0/semos/mountain_n2_w.tmx index e05e2cb9be..19a62ed8c7 100644 --- a/data/maps/Level 0/semos/mountain_n2_w.tmx +++ b/data/maps/Level 0/semos/mountain_n2_w.tmx @@ -1,5 +1,5 @@ - + @@ -96,93 +96,93 @@ - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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 diff --git a/data/maps/Level 6/kikareukin/islands.tmx b/data/maps/Level 6/kikareukin/islands.tmx index 0eebaf3150..a17232a711 100644 --- a/data/maps/Level 6/kikareukin/islands.tmx +++ b/data/maps/Level 6/kikareukin/islands.tmx @@ -1,6 +1,6 @@ - - + + @@ -36,101 +36,111 @@ - + - + - + - + - + - + - + - + - + + + + + + - + - + - + - - + + - + - + - + - + - + + + + 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 diff --git a/data/maps/Level 7/kikareukin/clouds.tmx b/data/maps/Level 7/kikareukin/clouds.tmx index 2442e0d7ad..e698f1b03d 100644 --- a/data/maps/Level 7/kikareukin/clouds.tmx +++ b/data/maps/Level 7/kikareukin/clouds.tmx @@ -1,163 +1,70 @@ - - - - + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - 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 -skCCAEAAAA== + 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 -swBAAAA= + H4sIAAAAAAAACu3ToREAAAgDse6/MoYVMFxNMsGbTwAAgKtpBwCvPA4AAAAAAAAAAAAAADQtVS9QswBAAAA= - + - H4sIAAAAAAAAAO3ZywnAIAwAUOmhU3WUbtHP/F1AKEqqFt87epAgmhhNCQDaWHsHAECVLTN2LOXz -qAPMJHdugBjqSX+j5bjR4gGIcFX0XACzcQ8ERlaao+Q0WrHX4r2t6a6/AwY3S204g/Lxl+92t5oR -zr8iAAAA8GcPfSksnQBAAAA= + H4sIAAAAAAAACu3ZywnAIAwAUOmhU3WUbtHP/F1AKEqqFt87epAgmhhNCQDaWHsHAECVLTN2LOXzqAPMJHdugBjqSX+j5bjR4gGIcFX0XACzcQ8ERlaao+Q0WrHX4r2t6a6/AwY3S204g/Lxl+92t5oRzr8iAAAA8GcPfSksnQBAAAA= - + - H4sIAAAAAAAAAO2VwQ6DMAxDu//d/593mjQh2rWJE6fUlnwCHL8UxKu19pJlWU7yrzJnVuC9in0W -LO6n85/A7hG7O4Od3bka+457eRv5r5yja1UdKTYbm78Beu2+A1SfCOaMHSDPIvP8EXuZ3f3KrN3Z -Ed9hND9K0V1O57fMqMzP+g4QusvK6BXBj9prNP/J7BH81XmfyL/Ki+x7l1+VH9l5lF+Nf+be3c4c -vSdrDpsJ8X5Y+dlcbH5vjmVOBf6Z7NP5vTtYzR3dN6OIvUacUy/Xq8yuiP6e56O+Tessq1C7i+T/ -N4+hbP67mSxdO2SwX2ezhDh3Zn+vqvEzdlmFn/k+sXdgyendH/FPyuK3dB1dW8nNZPfm7MzvzRk9 -Y83Neve9ORG9vOxIfrSzO1Th7vVhza7Az5rNYmf7dH5ZlmW576/YPcQvy7Lc9wcfY9XjAEAAAA== + H4sIAAAAAAAACu2VwQ6DMAxDu//d/593mjQh2rWJE6fUlnwCHL8UxKu19pJlWU7yrzJnVuC9in0WLO6n85/A7hG7O4Od3bka+457eRv5r5yja1UdKTYbm78Beu2+A1SfCOaMHSDPIvP8EXuZ3f3KrN3ZEd9hND9K0V1O57fMqMzP+g4QusvK6BXBj9prNP/J7BH81XmfyL/Ki+x7l1+VH9l5lF+Nf+be3c4cvSdrDpsJ8X5Y+dlcbH5vjmVOBf6Z7NP5vTtYzR3dN6OIvUacUy/Xq8yuiP6e56O+Tessq1C7i+T/N4+hbP67mSxdO2SwX2ezhDh3Zn+vqvEzdlmFn/k+sXdgyendH/FPyuK3dB1dW8nNZPfm7MzvzRk9Y83Neve9ORG9vOxIfrSzO1Th7vVhza7Az5rNYmf7dH5ZlmW576/YPcQvy7Lc9wcfY9XjAEAAAA== - + - H4sIAAAAAAAAAO3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA + H4sIAAAAAAAACu3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA diff --git a/data/maps/parallax/semos_surrounds.png b/data/maps/parallax/semos_surrounds.png new file mode 100644 index 0000000000..0cd89374be Binary files /dev/null and b/data/maps/parallax/semos_surrounds.png differ diff --git a/data/maps/parallax/semos_surrounds.xcf.bz2 b/data/maps/parallax/semos_surrounds.xcf.bz2 new file mode 100644 index 0000000000..2ee3271b77 Binary files /dev/null and b/data/maps/parallax/semos_surrounds.xcf.bz2 differ diff --git a/data/maps/parallax/stars.png b/data/maps/parallax/stars.png new file mode 100644 index 0000000000..57b30aa0d9 Binary files /dev/null and b/data/maps/parallax/stars.png differ diff --git a/data/maps/parallax/view_wizards_tower.png b/data/maps/parallax/view_wizards_tower.png new file mode 100644 index 0000000000..5439c84b69 Binary files /dev/null and b/data/maps/parallax/view_wizards_tower.png differ diff --git a/data/maps/tileset/sky/cloud/clouds.png b/data/maps/tileset/sky/cloud/clouds.png index 146691a7e2..3aea517077 100644 Binary files a/data/maps/tileset/sky/cloud/clouds.png and b/data/maps/tileset/sky/cloud/clouds.png differ diff --git a/data/music/dark_quest.flac b/data/music/dark_quest.flac index ede999f738..a74e320eac 100644 Binary files a/data/music/dark_quest.flac and b/data/music/dark_quest.flac differ diff --git a/data/music/dark_quest.ogg b/data/music/dark_quest.ogg index 09bfdbe1c4..4f46b750c4 100644 Binary files a/data/music/dark_quest.ogg and b/data/music/dark_quest.ogg differ diff --git a/data/music/jump_and_run_tropics.ogg b/data/music/jump_and_run_tropics.ogg new file mode 100644 index 0000000000..4a8af28c32 Binary files /dev/null and b/data/music/jump_and_run_tropics.ogg differ diff --git a/data/music/night_town.flac b/data/music/night_town.flac index d6e6a52ba4..14ecb76a52 100644 Binary files a/data/music/night_town.flac and b/data/music/night_town.flac differ diff --git a/data/music/night_town.ogg b/data/music/night_town.ogg index f1bf179f74..fa4ee09746 100644 Binary files a/data/music/night_town.ogg and b/data/music/night_town.ogg differ diff --git a/data/music/settlement_of_the_frontier.ogg b/data/music/settlement_of_the_frontier.ogg index f36eb8184f..2fb8b50061 100644 Binary files a/data/music/settlement_of_the_frontier.ogg and b/data/music/settlement_of_the_frontier.ogg differ diff --git a/data/music/spinning_tavern.ogg b/data/music/spinning_tavern.ogg index c013bd0cbb..858026343f 100644 Binary files a/data/music/spinning_tavern.ogg and b/data/music/spinning_tavern.ogg differ diff --git a/data/script/quest/LostEngagementRing.lua b/data/script/quest/LostEngagementRing.lua index 6164e3ae9e..7aeeca5b3a 100644 --- a/data/script/quest/LostEngagementRing.lua +++ b/data/script/quest/LostEngagementRing.lua @@ -1,6 +1,6 @@ --[[ *************************************************************************** - * Copyright © 2022-2023 - Stendhal * + * Copyright © 2022-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -589,7 +589,20 @@ local prepareMetalDetectorLender = function() end -- hint to players that they can borrow metal detector - lender:addOffer("I also have some items that I don't mind #loaning out.") + --~ lender:addOffer("I also have some items that I don't mind #loaning out.") + -- must remove original response to "offer" else function to force "loan" chat option trigger + -- doesn't work + lender:removeTransition(ConversationStates.ATTENDING, ConversationPhrases.OFFER_MESSAGES, nil) + lender:add( + ConversationStates.ATTENDING, + ConversationPhrases.OFFER_MESSAGES, + nil, + ConversationStates.ATTENDING, + "Please check the blackboard for a list of items that I buy. I also have some items that I" + .. " don't mind #loaning out.", + function(player, sentence, raiser) + lender:forceWordsInCurrentConversation({"loan"}) + end) lender:add( ConversationStates.ATTENDING, @@ -599,7 +612,9 @@ local prepareMetalDetectorLender = function() "So, you want to borrow my metal detector? Well, I don't lend things out" .. " without some form of collateral. What would you like to leave" .. " behind?", - nil) + function(player, sentence, raiser) + lender:forceWordsInCurrentConversation({"nothing"}) + end) lender:add( ConversationStates.ATTENDING, diff --git a/data/sprites/entity/overlay/beams.png b/data/sprites/entity/overlay/beams.png new file mode 100644 index 0000000000..c548edc8b1 Binary files /dev/null and b/data/sprites/entity/overlay/beams.png differ diff --git a/data/sprites/entity/overlay/electricity.png b/data/sprites/entity/overlay/electricity.png new file mode 100644 index 0000000000..935e24b8a8 Binary files /dev/null and b/data/sprites/entity/overlay/electricity.png differ diff --git a/data/sprites/entity/overlay/electricity_blue.png b/data/sprites/entity/overlay/electricity_blue.png new file mode 100644 index 0000000000..36e8506313 Binary files /dev/null and b/data/sprites/entity/overlay/electricity_blue.png differ diff --git a/data/sprites/entity/overlay/electricity_red.png b/data/sprites/entity/overlay/electricity_red.png new file mode 100644 index 0000000000..d7decb4708 Binary files /dev/null and b/data/sprites/entity/overlay/electricity_red.png differ diff --git a/data/sprites/entity/overlay/electricity_violet.png b/data/sprites/entity/overlay/electricity_violet.png new file mode 100644 index 0000000000..b983ab27e3 Binary files /dev/null and b/data/sprites/entity/overlay/electricity_violet.png differ diff --git a/data/sprites/entity/overlay/flame.png b/data/sprites/entity/overlay/flame.png new file mode 100644 index 0000000000..598b111b96 Binary files /dev/null and b/data/sprites/entity/overlay/flame.png differ diff --git a/data/sprites/entity/overlay/flame.xcf.bz2 b/data/sprites/entity/overlay/flame.xcf.bz2 new file mode 100644 index 0000000000..eb40bfaa92 Binary files /dev/null and b/data/sprites/entity/overlay/flame.xcf.bz2 differ diff --git a/data/sprites/entity/overlay/flame_blue.png b/data/sprites/entity/overlay/flame_blue.png new file mode 100644 index 0000000000..aeb4c907f8 Binary files /dev/null and b/data/sprites/entity/overlay/flame_blue.png differ diff --git a/data/sprites/entity/overlay/flame_blue.xcf.bz2 b/data/sprites/entity/overlay/flame_blue.xcf.bz2 new file mode 100644 index 0000000000..b5ddd78fd7 Binary files /dev/null and b/data/sprites/entity/overlay/flame_blue.xcf.bz2 differ diff --git a/data/sprites/entity/overlay/ice_sparkles.png b/data/sprites/entity/overlay/ice_sparkles.png new file mode 100644 index 0000000000..ec8a9d690d Binary files /dev/null and b/data/sprites/entity/overlay/ice_sparkles.png differ diff --git a/data/sprites/entity/overlay/ice_sparkles_2x.png b/data/sprites/entity/overlay/ice_sparkles_2x.png new file mode 100644 index 0000000000..bc2f30ac1a Binary files /dev/null and b/data/sprites/entity/overlay/ice_sparkles_2x.png differ diff --git a/data/sprites/entity/overlay/magic_sparkles.png b/data/sprites/entity/overlay/magic_sparkles.png new file mode 100644 index 0000000000..4533da8d6a Binary files /dev/null and b/data/sprites/entity/overlay/magic_sparkles.png differ diff --git a/data/sprites/entity/overlay/necro_flames.png b/data/sprites/entity/overlay/necro_flames.png new file mode 100644 index 0000000000..ff7bbc29de Binary files /dev/null and b/data/sprites/entity/overlay/necro_flames.png differ diff --git a/data/sprites/entity/overlay/necro_flames.xcf.bz2 b/data/sprites/entity/overlay/necro_flames.xcf.bz2 new file mode 100644 index 0000000000..b28980b15b Binary files /dev/null and b/data/sprites/entity/overlay/necro_flames.xcf.bz2 differ diff --git a/data/sprites/entity/overlay/transform.png b/data/sprites/entity/overlay/transform.png new file mode 100644 index 0000000000..0bbcd5b922 Binary files /dev/null and b/data/sprites/entity/overlay/transform.png differ diff --git a/data/sprites/items/ring/clarity.png b/data/sprites/items/ring/clarity.png new file mode 100644 index 0000000000..8d301e7c82 Binary files /dev/null and b/data/sprites/items/ring/clarity.png differ diff --git a/data/sprites/items/ring/clarity.xcf.bz2 b/data/sprites/items/ring/clarity.xcf.bz2 new file mode 100644 index 0000000000..b47a11d9f9 Binary files /dev/null and b/data/sprites/items/ring/clarity.xcf.bz2 differ diff --git a/data/sprites/items/special/clover.png b/data/sprites/items/special/clover.png new file mode 100644 index 0000000000..f8c2aea34d Binary files /dev/null and b/data/sprites/items/special/clover.png differ diff --git a/data/sprites/monsters/albino_elf/albino_elf_magician.png b/data/sprites/monsters/albino_elf/albino_elf_magician.png index f20db6278b..684b47a615 100644 Binary files a/data/sprites/monsters/albino_elf/albino_elf_magician.png and b/data/sprites/monsters/albino_elf/albino_elf_magician.png differ diff --git a/data/sprites/monsters/albino_elf/albino_elf_magician.xcf.bz2 b/data/sprites/monsters/albino_elf/albino_elf_magician.xcf.bz2 index 5110c8905a..dc9b89d5cf 100644 Binary files a/data/sprites/monsters/albino_elf/albino_elf_magician.xcf.bz2 and b/data/sprites/monsters/albino_elf/albino_elf_magician.xcf.bz2 differ diff --git a/data/sprites/monsters/angel/fallen_angel.png b/data/sprites/monsters/angel/fallen_angel.png index 958d568dee..c7c27d3307 100644 Binary files a/data/sprites/monsters/angel/fallen_angel.png and b/data/sprites/monsters/angel/fallen_angel.png differ diff --git a/data/sprites/monsters/angel/fallen_angel.xcf.bz2 b/data/sprites/monsters/angel/fallen_angel.xcf.bz2 index 669090d31e..e3e494ea12 100644 Binary files a/data/sprites/monsters/angel/fallen_angel.xcf.bz2 and b/data/sprites/monsters/angel/fallen_angel.xcf.bz2 differ diff --git a/data/sprites/monsters/chaos/chaos.xcf.bz2 b/data/sprites/monsters/chaos/chaos.xcf.bz2 index bca0151ac0..d773f1063b 100644 Binary files a/data/sprites/monsters/chaos/chaos.xcf.bz2 and b/data/sprites/monsters/chaos/chaos.xcf.bz2 differ diff --git a/data/sprites/monsters/chaos/chaos_commander.png b/data/sprites/monsters/chaos/chaos_commander.png index f6d449c1ba..5ef20c70bc 100644 Binary files a/data/sprites/monsters/chaos/chaos_commander.png and b/data/sprites/monsters/chaos/chaos_commander.png differ diff --git a/data/sprites/monsters/chaos/chaos_lord.png b/data/sprites/monsters/chaos/chaos_lord.png index 047d690a4c..e86b9bc806 100644 Binary files a/data/sprites/monsters/chaos/chaos_lord.png and b/data/sprites/monsters/chaos/chaos_lord.png differ diff --git a/data/sprites/monsters/chaos/chaos_lord.xcf.bz2 b/data/sprites/monsters/chaos/chaos_lord.xcf.bz2 new file mode 100644 index 0000000000..aa08f03840 Binary files /dev/null and b/data/sprites/monsters/chaos/chaos_lord.xcf.bz2 differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_master.png b/data/sprites/monsters/dark_elf/dark_elf_master.png index 0566641528..9e18daea0a 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_master.png and b/data/sprites/monsters/dark_elf/dark_elf_master.png differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_master.xcf.bz2 b/data/sprites/monsters/dark_elf/dark_elf_master.xcf.bz2 index 4a9cf47653..4ae0d02116 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_master.xcf.bz2 and b/data/sprites/monsters/dark_elf/dark_elf_master.xcf.bz2 differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_matronmother.png b/data/sprites/monsters/dark_elf/dark_elf_matronmother.png index 0fadf46963..a6c581f4b2 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_matronmother.png and b/data/sprites/monsters/dark_elf/dark_elf_matronmother.png differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_matronmother.xcf.bz2 b/data/sprites/monsters/dark_elf/dark_elf_matronmother.xcf.bz2 index 57bf3cf31a..ce177bf63b 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_matronmother.xcf.bz2 and b/data/sprites/monsters/dark_elf/dark_elf_matronmother.xcf.bz2 differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_ranger.png b/data/sprites/monsters/dark_elf/dark_elf_ranger.png index 88a29a813f..95783e4cfd 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_ranger.png and b/data/sprites/monsters/dark_elf/dark_elf_ranger.png differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_ranger.xcf.bz2 b/data/sprites/monsters/dark_elf/dark_elf_ranger.xcf.bz2 index 85f9cd10d3..050dbfd060 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_ranger.xcf.bz2 and b/data/sprites/monsters/dark_elf/dark_elf_ranger.xcf.bz2 differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_viceroy.png b/data/sprites/monsters/dark_elf/dark_elf_viceroy.png index f16f0ad71e..32023486dc 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_viceroy.png and b/data/sprites/monsters/dark_elf/dark_elf_viceroy.png differ diff --git a/data/sprites/monsters/dark_elf/dark_elf_viceroy.xcf.bz2 b/data/sprites/monsters/dark_elf/dark_elf_viceroy.xcf.bz2 index ef0bd2fb68..87e17e888e 100644 Binary files a/data/sprites/monsters/dark_elf/dark_elf_viceroy.xcf.bz2 and b/data/sprites/monsters/dark_elf/dark_elf_viceroy.xcf.bz2 differ diff --git a/data/sprites/monsters/demon/imperial_demon_lord.png b/data/sprites/monsters/demon/imperial_demon_lord.png index 835c072e7b..ef4f0e1daa 100644 Binary files a/data/sprites/monsters/demon/imperial_demon_lord.png and b/data/sprites/monsters/demon/imperial_demon_lord.png differ diff --git a/data/sprites/monsters/demon/imperial_demon_lord.xcf.bz2 b/data/sprites/monsters/demon/imperial_demon_lord.xcf.bz2 index 49e6299ab4..732df6dbfa 100644 Binary files a/data/sprites/monsters/demon/imperial_demon_lord.xcf.bz2 and b/data/sprites/monsters/demon/imperial_demon_lord.xcf.bz2 differ diff --git a/data/sprites/monsters/elf/soldier_elf.png b/data/sprites/monsters/elf/soldier_elf.png index 9e561a18a3..26d4f1cf20 100644 Binary files a/data/sprites/monsters/elf/soldier_elf.png and b/data/sprites/monsters/elf/soldier_elf.png differ diff --git a/data/sprites/monsters/elf/soldier_elf.xcf.bz2 b/data/sprites/monsters/elf/soldier_elf.xcf.bz2 index f0c5ea27a7..2775692634 100644 Binary files a/data/sprites/monsters/elf/soldier_elf.xcf.bz2 and b/data/sprites/monsters/elf/soldier_elf.xcf.bz2 differ diff --git a/data/sprites/monsters/goblin/soldier_goblin.png b/data/sprites/monsters/goblin/soldier_goblin.png index 50ea138141..19f4ac5071 100644 Binary files a/data/sprites/monsters/goblin/soldier_goblin.png and b/data/sprites/monsters/goblin/soldier_goblin.png differ diff --git a/data/sprites/monsters/goblin/soldier_goblin.xcf.bz2 b/data/sprites/monsters/goblin/soldier_goblin.xcf.bz2 index d6f973a8ff..005c4733af 100644 Binary files a/data/sprites/monsters/goblin/soldier_goblin.xcf.bz2 and b/data/sprites/monsters/goblin/soldier_goblin.xcf.bz2 differ diff --git a/data/sprites/monsters/goblin/veteran_goblin.png b/data/sprites/monsters/goblin/veteran_goblin.png index 716736f9c9..3ed428dd2b 100644 Binary files a/data/sprites/monsters/goblin/veteran_goblin.png and b/data/sprites/monsters/goblin/veteran_goblin.png differ diff --git a/data/sprites/monsters/goblin/veteran_goblin.xcf.bz2 b/data/sprites/monsters/goblin/veteran_goblin.xcf.bz2 new file mode 100644 index 0000000000..f0fc7dcaa6 Binary files /dev/null and b/data/sprites/monsters/goblin/veteran_goblin.xcf.bz2 differ diff --git a/data/sprites/monsters/human/barbarian_elite.png b/data/sprites/monsters/human/barbarian_elite.png index e8821b435e..9429c2e59b 100644 Binary files a/data/sprites/monsters/human/barbarian_elite.png and b/data/sprites/monsters/human/barbarian_elite.png differ diff --git a/data/sprites/monsters/human/barbarian_elite.xcf.bz2 b/data/sprites/monsters/human/barbarian_elite.xcf.bz2 index d2058d07d0..7bf1333783 100644 Binary files a/data/sprites/monsters/human/barbarian_elite.xcf.bz2 and b/data/sprites/monsters/human/barbarian_elite.xcf.bz2 differ diff --git a/data/sprites/monsters/human/emperor_dalmung.png b/data/sprites/monsters/human/emperor_dalmung.png index b940e2a318..3123b8bfbf 100644 Binary files a/data/sprites/monsters/human/emperor_dalmung.png and b/data/sprites/monsters/human/emperor_dalmung.png differ diff --git a/data/sprites/monsters/human/emperor_dalmung.xcf.bz2 b/data/sprites/monsters/human/emperor_dalmung.xcf.bz2 index 2d9fb92977..0ca31d37a9 100644 Binary files a/data/sprites/monsters/human/emperor_dalmung.xcf.bz2 and b/data/sprites/monsters/human/emperor_dalmung.xcf.bz2 differ diff --git a/data/sprites/monsters/human_blordrough/storm_trooper.png b/data/sprites/monsters/human_blordrough/storm_trooper.png index 89641e2963..1cab49840d 100644 Binary files a/data/sprites/monsters/human_blordrough/storm_trooper.png and b/data/sprites/monsters/human_blordrough/storm_trooper.png differ diff --git a/data/sprites/monsters/human_blordrough/storm_trooper.xcf.bz2 b/data/sprites/monsters/human_blordrough/storm_trooper.xcf.bz2 new file mode 100644 index 0000000000..05f3f47ed9 Binary files /dev/null and b/data/sprites/monsters/human_blordrough/storm_trooper.xcf.bz2 differ diff --git a/data/sprites/monsters/hybrid/necrosophia.png b/data/sprites/monsters/hybrid/necrosophia.png index ac7fe7e18b..08c92a782b 100644 Binary files a/data/sprites/monsters/hybrid/necrosophia.png and b/data/sprites/monsters/hybrid/necrosophia.png differ diff --git a/data/sprites/monsters/minotaur/mino_king.png b/data/sprites/monsters/minotaur/mino_king.png index e987befeb2..d7fffed657 100644 Binary files a/data/sprites/monsters/minotaur/mino_king.png and b/data/sprites/monsters/minotaur/mino_king.png differ diff --git a/data/sprites/monsters/minotaur/mino_king.xcf.bz2 b/data/sprites/monsters/minotaur/mino_king.xcf.bz2 index 4954d0f6c2..7170859464 100644 Binary files a/data/sprites/monsters/minotaur/mino_king.xcf.bz2 and b/data/sprites/monsters/minotaur/mino_king.xcf.bz2 differ diff --git a/data/sprites/monsters/minotaur/minotaur.png b/data/sprites/monsters/minotaur/minotaur.png index 50b4b4c80e..cb600a7370 100644 Binary files a/data/sprites/monsters/minotaur/minotaur.png and b/data/sprites/monsters/minotaur/minotaur.png differ diff --git a/data/sprites/monsters/minotaur/minotaur.xcf.bz2 b/data/sprites/monsters/minotaur/minotaur.xcf.bz2 new file mode 100644 index 0000000000..d13bf7d09d Binary files /dev/null and b/data/sprites/monsters/minotaur/minotaur.xcf.bz2 differ diff --git a/data/sprites/monsters/mutant/armed_naga.png b/data/sprites/monsters/mutant/armed_naga.png index c9068078a9..91bd989022 100644 Binary files a/data/sprites/monsters/mutant/armed_naga.png and b/data/sprites/monsters/mutant/armed_naga.png differ diff --git a/data/sprites/monsters/mutant/armed_naga.xcf.bz2 b/data/sprites/monsters/mutant/armed_naga.xcf.bz2 new file mode 100644 index 0000000000..acbd78c884 Binary files /dev/null and b/data/sprites/monsters/mutant/armed_naga.xcf.bz2 differ diff --git a/data/sprites/monsters/mutant/dark_mutant.png b/data/sprites/monsters/mutant/dark_mutant.png index e661070060..2ababbdb81 100644 Binary files a/data/sprites/monsters/mutant/dark_mutant.png and b/data/sprites/monsters/mutant/dark_mutant.png differ diff --git a/data/sprites/monsters/mutant/dark_mutant.xcf.bz2 b/data/sprites/monsters/mutant/dark_mutant.xcf.bz2 index b3cedda916..d90fff4391 100644 Binary files a/data/sprites/monsters/mutant/dark_mutant.xcf.bz2 and b/data/sprites/monsters/mutant/dark_mutant.xcf.bz2 differ diff --git a/data/sprites/monsters/mutant/glow_monster.png b/data/sprites/monsters/mutant/glow_monster.png index 14ea00981e..fa5ac40e07 100644 Binary files a/data/sprites/monsters/mutant/glow_monster.png and b/data/sprites/monsters/mutant/glow_monster.png differ diff --git a/data/sprites/monsters/mutant/glow_monster.xcf.bz2 b/data/sprites/monsters/mutant/glow_monster.xcf.bz2 index e0f6454cda..8ecff3be2c 100644 Binary files a/data/sprites/monsters/mutant/glow_monster.xcf.bz2 and b/data/sprites/monsters/mutant/glow_monster.xcf.bz2 differ diff --git a/data/sprites/monsters/mutant/naga.xcf b/data/sprites/monsters/mutant/naga.xcf deleted file mode 100644 index 94e02fef2a..0000000000 Binary files a/data/sprites/monsters/mutant/naga.xcf and /dev/null differ diff --git a/data/sprites/monsters/mutant/naga.xcf.bz2 b/data/sprites/monsters/mutant/naga.xcf.bz2 new file mode 100644 index 0000000000..68ec17e565 Binary files /dev/null and b/data/sprites/monsters/mutant/naga.xcf.bz2 differ diff --git a/data/sprites/monsters/oni/oni_priest.png b/data/sprites/monsters/oni/oni_priest.png index a65f943b2f..688a4ee346 100644 Binary files a/data/sprites/monsters/oni/oni_priest.png and b/data/sprites/monsters/oni/oni_priest.png differ diff --git a/data/sprites/monsters/oni/oni_priest.xcf.bz2 b/data/sprites/monsters/oni/oni_priest.xcf.bz2 index a9b15391bf..66a9611b55 100644 Binary files a/data/sprites/monsters/oni/oni_priest.xcf.bz2 and b/data/sprites/monsters/oni/oni_priest.xcf.bz2 differ diff --git a/data/sprites/monsters/ratfolk/archrat.png b/data/sprites/monsters/ratfolk/archrat.png index d8bec6dc99..f9094ec98e 100644 Binary files a/data/sprites/monsters/ratfolk/archrat.png and b/data/sprites/monsters/ratfolk/archrat.png differ diff --git a/data/sprites/monsters/ratfolk/archrat.xcf.bz2 b/data/sprites/monsters/ratfolk/archrat.xcf.bz2 index de2884ed53..a62aea37a2 100644 Binary files a/data/sprites/monsters/ratfolk/archrat.xcf.bz2 and b/data/sprites/monsters/ratfolk/archrat.xcf.bz2 differ diff --git a/data/sprites/npc/blackwizardpriestnpc.png b/data/sprites/npc/blackwizardpriestnpc.png index c71b9908b0..5edf77d331 100644 Binary files a/data/sprites/npc/blackwizardpriestnpc.png and b/data/sprites/npc/blackwizardpriestnpc.png differ diff --git a/data/sprites/npc/blackwizardpriestnpc.xcf.bz2 b/data/sprites/npc/blackwizardpriestnpc.xcf.bz2 index 1910b00496..6c208120bf 100644 Binary files a/data/sprites/npc/blackwizardpriestnpc.xcf.bz2 and b/data/sprites/npc/blackwizardpriestnpc.xcf.bz2 differ diff --git a/data/sprites/npc/cloverhunternpc.png b/data/sprites/npc/cloverhunternpc.png new file mode 100644 index 0000000000..70bef835c2 Binary files /dev/null and b/data/sprites/npc/cloverhunternpc.png differ diff --git a/data/sprites/npc/cloverhunternpc.xcf.bz2 b/data/sprites/npc/cloverhunternpc.xcf.bz2 new file mode 100644 index 0000000000..9f37afbb19 Binary files /dev/null and b/data/sprites/npc/cloverhunternpc.xcf.bz2 differ diff --git a/data/sprites/npc/elfbuyernpc.png b/data/sprites/npc/elfbuyernpc.png index ddedfefce6..c5ffbcc791 100644 Binary files a/data/sprites/npc/elfbuyernpc.png and b/data/sprites/npc/elfbuyernpc.png differ diff --git a/data/sprites/npc/elfbuyernpc.xcf.bz2 b/data/sprites/npc/elfbuyernpc.xcf.bz2 index 82c42e4355..138ca53876 100644 Binary files a/data/sprites/npc/elfbuyernpc.xcf.bz2 and b/data/sprites/npc/elfbuyernpc.xcf.bz2 differ diff --git a/data/sprites/npc/elfprincessnpc.png b/data/sprites/npc/elfprincessnpc.png index a67066bb1e..64583d1218 100644 Binary files a/data/sprites/npc/elfprincessnpc.png and b/data/sprites/npc/elfprincessnpc.png differ diff --git a/data/sprites/npc/elfprincessnpc.xcf.bz2 b/data/sprites/npc/elfprincessnpc.xcf.bz2 index 94a5016c77..96e21069e8 100644 Binary files a/data/sprites/npc/elfprincessnpc.xcf.bz2 and b/data/sprites/npc/elfprincessnpc.xcf.bz2 differ diff --git a/data/sprites/npc/highpriestnpc.png b/data/sprites/npc/highpriestnpc.png index d25f35f42e..16277b2e69 100644 Binary files a/data/sprites/npc/highpriestnpc.png and b/data/sprites/npc/highpriestnpc.png differ diff --git a/data/sprites/npc/highpriestnpc.xcf.bz2 b/data/sprites/npc/highpriestnpc.xcf.bz2 index ebc505f690..7152bc2f23 100644 Binary files a/data/sprites/npc/highpriestnpc.xcf.bz2 and b/data/sprites/npc/highpriestnpc.xcf.bz2 differ diff --git a/data/sprites/npc/kid1npc.png b/data/sprites/npc/kid1npc.png index b56b9204d4..8acdf13df0 100644 Binary files a/data/sprites/npc/kid1npc.png and b/data/sprites/npc/kid1npc.png differ diff --git a/data/sprites/npc/kid1npc.xcf.bz2 b/data/sprites/npc/kid1npc.xcf.bz2 index 151bb70799..678bbbf4b4 100644 Binary files a/data/sprites/npc/kid1npc.xcf.bz2 and b/data/sprites/npc/kid1npc.xcf.bz2 differ diff --git a/data/sprites/npc/minernpc.png b/data/sprites/npc/minernpc.png index 75e08802c7..6dbf5f020a 100644 Binary files a/data/sprites/npc/minernpc.png and b/data/sprites/npc/minernpc.png differ diff --git a/data/sprites/npc/minernpc.xcf.bz2 b/data/sprites/npc/minernpc.xcf.bz2 index e6269ebb8e..ddec21419f 100644 Binary files a/data/sprites/npc/minernpc.xcf.bz2 and b/data/sprites/npc/minernpc.xcf.bz2 differ diff --git a/data/sprites/templates/human/adult_thin_lay.png b/data/sprites/templates/human/adult_thin_lay.png new file mode 100644 index 0000000000..b509400e40 Binary files /dev/null and b/data/sprites/templates/human/adult_thin_lay.png differ diff --git a/data/sprites/templates/human/adult_thin_lay.xcf.bz2 b/data/sprites/templates/human/adult_thin_lay.xcf.bz2 new file mode 100644 index 0000000000..5c9613db68 Binary files /dev/null and b/data/sprites/templates/human/adult_thin_lay.xcf.bz2 differ diff --git a/data/sprites/templates/human/adult_thin_lay_closed_eyes.png b/data/sprites/templates/human/adult_thin_lay_closed_eyes.png new file mode 100644 index 0000000000..d805081195 Binary files /dev/null and b/data/sprites/templates/human/adult_thin_lay_closed_eyes.png differ diff --git a/data/sprites/weather/clouds.png b/data/sprites/weather/clouds.png new file mode 100644 index 0000000000..8668b93bd1 Binary files /dev/null and b/data/sprites/weather/clouds.png differ diff --git a/data/sprites/weather/fog_ani.png b/data/sprites/weather/fog_ani.png new file mode 100644 index 0000000000..3353ab8f27 Binary files /dev/null and b/data/sprites/weather/fog_ani.png differ diff --git a/doc/CHANGES.txt b/doc/CHANGES.txt index 51a39be1f5..20be3f04f1 100644 --- a/doc/CHANGES.txt +++ b/doc/CHANGES.txt @@ -2,6 +2,65 @@ Changelog --------- --------- +1.49 + +*world* +- outfit prices in Gwen's shop reduced + +*web client* +- improved lighting effects + +*bug fixes* +- fixed invalid head outfit index of Mr Ross +- fixed silent NPC paths +- fixed desktop webview client window freezing on Windows when closed +- fixed nativehelper extension process not exiting cleanly on Windows +- set User-Agent string for Wikipedia API requests +- trimmed large music files to decrease size to 2MB or less +- fixed Gourmet achievement + + +1.48 + +*world* +- House Buying displays under open quests when taxes owed +- added music to Athor Island +- Mayor Chalmers won't accept experimental sandwiches for Daily Item Quest +- touched up and added equipment to many entity sprites +- Amber wanders randomly instead of using set path +- changed turtle behavior to wander +- added wander behavior to crab in addition to patrol +- new items: + - four-leaf clover: a special item that spawns on random maps + - ring of clarity: increases attack accuracy by 25% +- new achievement: Luck O' the Irish: Find the lucky four-leaf clover +- new NPC: Maple, a clover hunter +- new quest: Lucky Four-Leaf Clover + +*chat* +- new offers with Harold are announced in chat + +*web client* +- added visuals tab to settings dialog +- animated fog +- animated clouds in Kika Clouds +- added support for parallax scrolling backgrounds +- added parallax backgrounds to Kika Islands & Kika Clouds maps +- added parallax backgrounds to Wizards Tower +- added quick menu button to toggle pathfinding via ground +- added support for animated entity overlay effects +- added transformation effect to vampires +- cleaned up bestiary window layout +- performance optimization: sound events too far away to be heard are not loaded +- outfit dialog preview can be rotated + +*bug fixes* +- fixed multiple issues with Akutagawa's repair shop + +*experimental features* +- built-in support added for capturing video of web client viewport + + 1.47 *world* @@ -13,15 +72,17 @@ Changelog *web client* - fixed text extending past edge of notification bubbles - added support for saving chat log to file -- bigger buttons for small touch enabled devices +- larger buttons for touch-only devices - travel log displays while waiting for response from server - fixed infinite request loop when travel log items technically empty - supports optional tab placement above panel contents - travel log tabs match theme - added basic lighting effects support +- added sound tab to settings dialog *server fixes* - fixed Seven Cherubs not showing up as completed +- fixed unable to start deathmatch after logout while in arena 1.46 @@ -645,7 +706,7 @@ Changelog *other* - started to work on a list of all contributors, please help to add to it: - https://github.com/arianne/stendhal/blob/master/doc/contributors.md + https://codeberg.org/arianne/stendhal/src/commit/master/doc/contributors.md 1.34 @@ -1353,7 +1414,7 @@ the huge task of creating & replacing sprite & tileset images. - Support Postgres databases in addition to H2 (recommended) and MySQL - Setup Travis for continuous integration tests and fixed some order dependent tests - Migrated test coverage analysis from Cobertura to Jacoco -- Git repository is mirrored instantaneously between Github and Sourceforge in both directions +- Git repository is mirrored instantaneously between Codeberg, Github and Sourceforge in both directions - Yearly special event quests now pick up the current year as part of their quest slot automatically - Backend for puzzles @@ -2516,7 +2577,7 @@ read https://stendhalgame.org/wiki/Automatic_Software_Updater - Tab completion for online nick names works now without /who called before - orc spearman creature - /atlas /faq and /manual commands to open useful pages in browser -- Harold tweets trades, the official stendhal server uses https://twitter.com/stendhaltrade +- Harold tweets trades, the official stendhal server uses https://mastodon.social/@stendhaltrade - magical damage types of weapons (fire, ice ...) - resistance/vulnerability of creatures to magical weapons (fire, ice, ...) - attack sweeps look different depending on the attack type diff --git a/doc/ReleaseNotes.txt b/doc/ReleaseNotes.txt new file mode 100644 index 0000000000..a6a45801cd --- /dev/null +++ b/doc/ReleaseNotes.txt @@ -0,0 +1,18 @@ + +Stendhal 1.48: Aesthetically Pleasing +------------------------------------- + +The world of Faiumoni has just gotten a little fancier. You may notice that the clouds have cleared +in Kikareukin, revealing a beautiful view of Semos below. + + +Technical Notes +--------------- + +Users that adjusted volume levels in the Android/web client in the previous release will initially +experience low volume. Simply re-adjust to desired levels. The issue was related to how volume +levels are configured and stored. The Stendhal server uses an integer value in the range 0-100 to +defne volume, while HTMLAudioElement used in the web client uses a floating point value in the range +0.0-1.0. It was intented for the web client to match the server and convert to/from float values +when adjusting volume. However, an oversight led to float values being stored in the configration +instead of integer. diff --git a/doc/contributors.md b/doc/contributors.md index 97a478d743..85efebc731 100644 --- a/doc/contributors.md +++ b/doc/contributors.md @@ -996,6 +996,8 @@ The following people released their work to the public with a suitable license f * [Hawkeye_Sprout](https://freesound.org/people/Hawkeye_Sprout/) * [YuriNikolai](https://opengameart.org/users/yurinikolai) * [Inchadney](http://inchadney.com/sounds.html) +* [Bart Kelsey](https://opengameart.org/users/bart) +* [Aidan Walker](https://opengameart.org/users/aidanwalker) diff --git a/doc/contributors/contributors.json b/doc/contributors/contributors.json index 97cf2fdfca..76eae1c64b 100644 --- a/doc/contributors/contributors.json +++ b/doc/contributors/contributors.json @@ -4763,6 +4763,14 @@ "name": "Inchadney", "link": "http://inchadney.com/sounds.html", "link_alt": "https://freesound.org/people/inchadney/" + }, + { + "name": "Bart Kelsey", + "link": "https://opengameart.org/users/bart" + }, + { + "name": "Aidan Walker", + "link": "https://opengameart.org/users/aidanwalker" } ] } diff --git a/doc/manual/lua.md b/doc/manual/lua.md deleted file mode 100644 index 6ecc76a72e..0000000000 --- a/doc/manual/lua.md +++ /dev/null @@ -1,131 +0,0 @@ -## Stendhal Lua Scripting HowTo - -*NOTE: The Lua implementation & this documentation may not be complete.* - -### Using the Logger - -A global "logger" object is provided for accessing the logger instance. - -Examples of usage: -```lua -logger:info("Info level message") -logger:warn("Warning level message") -logger:error("Error level message") -``` - -### Creating New Object Instances - -To create a new instance of a class, use the `luajava.newInstance` method. If the -instantiation requires parameters, include them after the class: - -```lua --- create a new entity instance with no parameters -local npc = luajava.newInstance("games.stendhal.server.entity.npc.SpeakerNPC") - --- create a new entity instance with a parameter -local npc = luajava.newInstance("games.stendhal.server.entity.npc.SpeakerNPC", "Lua") -``` - -#### Adding Entities to the Game - -Use `game:setSone` to set the zone to work with. Add any entities with `game:add`: - -```lua -if game:setZone("0_semos_city") then - local npc = luajava.newInstance("games.stendhal.server.entity.npc.SpeakerNPC", "Lua") - game:add(npc) -end -``` - -#### Adding NPCs with npcHelper Object - -`npcHelper` is an object to aid in creating NPCs. It is invoked as `npcHelper:createSpeakerNPC()`. -- There are two types of NPCs that can be created: - 1. `createSpeakerNPC(name)` - 2. `createSilentNPC()` -- The NPCs path can be set with `npcHelper:setPath(npc, nodes)`. - - ***npc*** is the NPC instance. - - ***nodes*** is a table of coordinates for the path the NPC should follow. - -Example: -```lua --- Set zone to Semos City -if game:setZone("0_semos_city") then - - -- Use helper object to create a new NPC - local npc = npcHelper:createSpeakerNPC("Lua") - npc:setEntityClass("littlegirlnpc") - npc:setPosition(10, 55) - npc:setBaseSpeed(0.1) - npc:setCollisionAction(CollisionAction.STOP) - - local nodes = { - {10, 55}, - {11, 55}, - {11, 56}, - {10, 56}, - } - - -- Use helper object to create NPC path - npcHelper:setPath(npc, nodes) - - -- Dialogue - npc:addJob("Actually, I am jobless.") - npc:addGoodbye(); - - -- Some custom replies using conditions & actions - npc:add( - ConversationStates.IDLE, - ConversationPhrases.GREETING_MESSAGES, - nil, - ConversationStates.ATTENDING, - "I am sad, because I do not have a job.", - nil - ) - npc:add( - ConversationStates.ATTENDING, - "Lua", - newCondition("PlayerNextToCondition"), - ConversationStates.ATTENDING, - "Um, could you back up please? I can smell your breath.", - newAction("NPCEmoteAction", "coughs", false) - ) - npc:add( - ConversationStates.ATTENDING, - "Lua", - newCondition("NotCondition", newCondition("PlayerNextToCondition")), - ConversationStates.ATTENDING, - "That's my name, don't wear it out!", - newAction("NPCEmoteAction", "giggles", false) - ) - - -- Add the NPC to the world - game:add(npc) -end -``` - -#### Adding Signs with Helper Functions - -Signs can be created with the `game:createSign` helper function. It optionally -takes 1 argument: `true` (default) or `false`. If `true`, the sign will be visible -with collision. Otherwise it will be invisible without collision. These attributes -can also be changed with the `Sign:setResistance` & `Sign:setEntityClass` methods. - -```lua --- Set zone to Semos City -if game:setZone("0_semos_city") then - -- Set up a sign for Lua - local sign = game:createSign() - sign:setEntityClass("signpost") - sign:setPosition(12, 55) - sign:setText("Meet Lua!") - - -- Add the sign to the world - game:add(sign) -end -``` - -Some enumerations accessible to Lua engine: -- [ConversationStates](../../src/games/stendhal/server/entity/npc/ConversationStates.java) -- [ConversationPhrases](../../src/games/stendhal/server/entity/npc/ConversationPhrases.java) -- [CollisionAction](../../src/games/stendhal/server/entity/CollisionAction.java) diff --git a/doc/sources/audio-music.txt b/doc/sources/audio-music.txt index ec404d0031..63f281f1d3 100644 --- a/doc/sources/audio-music.txt +++ b/doc/sources/audio-music.txt @@ -23,6 +23,7 @@ deep_forest.ogg Storyteller; GPL dungeon_entrance.ogg Storyteller; GPL eerie_curious.ogg Leerie; CC BY 3.0; https://opengameart.org/node/43367 ice_cave.* Tad Miller; CC BY 4.0; https://opengameart.org/node/109496 +jump_and_run_tropics Bart Kelsey; CC BY-SA 3.0; https://opengameart.org/node/3801 kings_feast.ogg RandomMind; CC0; https://opengameart.org/node/88287 lost_in_the_fog.ogg Storyteller; GPL market_day.ogg RandomMind; CC0; https://opengameart.org/node/93955 diff --git a/doc/sources/graphics-entity.txt b/doc/sources/graphics-entity.txt index 7c6fdabf01..98f4509fd0 100644 --- a/doc/sources/graphics-entity.txt +++ b/doc/sources/graphics-entity.txt @@ -39,6 +39,7 @@ chieffalatheennpc.png Svetlana Kushnariova (Cabbit) bleutailfly, Jordan childnpc.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969 cloakedwomannpc.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://sourceforge.net/p/arianne/patches/719/, https://opengameart.org/node/72969, https://opengameart.org/node/24823 cloakedwoman2npc.png Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://sourceforge.net/p/arianne/patches/719/, https://opengameart.org/node/72969, https://opengameart.org/node/24823 +cloverhunternpc Svetlana Kushnariova (Cabbit), bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/24944 confectionerapplepienpc.png Lori Angela Nagel (Jastiv); CC BY 3.0/4.0; https://opengameart.org/node/82267; Copyright Lori Angela Nagel 2018 confectionercherrypienpc.png Lori Angela Nagel (Jastiv); CC BY 3.0/4.0; https://opengameart.org/node/82267; Copyright Lori Angela Nagel 2018 curatornpc.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969 @@ -55,7 +56,7 @@ elfbankladynpc.* Svetlana Kushnariova (Cabbit), Jordan Irwin (Antum elfbuyernpc.* Svetlana Kushnariova (Cabbit), bleutailfly; OGA BY 3.0; https://opengameart.org/node/24944 elfflowergrowernpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/83661 elfmayornpc.* Svetlana Kushnariova (Cabbit), Fabzy, bleutailfly; CC BY-SA 3.0; https://opengameart.org/node/24944, https://opengameart.org/node/23314 -elfprincessnpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, Rawdanitsu, olonu, Jordan Irwin (AntumDeluge); CC BY 3.0/4.0; https://opengameart.org/node/72969, https://opengameart.org/node/74802, https://opengameart.org/node/18068 +elfprincessnpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, olonu, Jordan Irwin (AntumDeluge); CC BY 3.0/4.0; https://opengameart.org/node/72969, https://opengameart.org/node/74802 estateagentnpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, olonu, bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969, https://opengameart.org/node/72198, https://github.com/arianne/stendhal/issues/131 estateagent2npc.* Svetlana Kushnariova (Cabbit), olonu, bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969, https://github.com/arianne/stendhal/issues/132 executivenpc.* Svetlana Kushnariova (Cabbit), olonu, bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969, https://github.com/arianne/stendhal/issues/131 @@ -82,7 +83,7 @@ gypsywomannpc.png Svetlana Kushnariova (Cabbit), Jordan Irwin (Antum halfelfnpc.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, Stendhal hanknpc.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969 heraldnpc.png CC0 (placeholder) -highpriestnpc.* Svetlana kushnariova (Cabbit), Sollision, Andrew Kelley (superjoe), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/76967, https://opengameart.org/node/17839 +highpriestnpc.* Svetlana kushnariova (Cabbit), Sollision, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/76967 hoeingmannpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, Jordan Irwin (AntumDeluge); CC BY 3.0/4.0; https://opengameart.org/node/72969, https://opengameart.org/node/74802; Original Pepper&Carrot webcomic created by David Revoy. holidaymakernpc.png Svetlana Kushnariova (Cabbit), Sebastian Riedel (Baŝto), Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/90784 hotelchefnpc.* Svetlana Kushnariova (Cabbit), diamonddmgirl, One Man Army, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/83661, https://opengameart.org/node/69527 @@ -252,7 +253,7 @@ data/sprites/entity/albino_elf/ albino_elf_archer.* Svetlana Kushnariova (Cabbit), Michael Williams (Bizmaster Studios), olonu, Jordan Irwin (AntumDeluge); CC BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/146 albino_elf_king.* Svetlana Kushnariova (Cabbit), olonu, ztn, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/161, https://opengameart.org/node/69738 -albino_elf_magician.* Svetlana Kushnariova (Cabbit), olonu, Rawdanitsu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/104 +albino_elf_magician.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/104 albino_elf_queen.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969 albino_elf_swordsman.* Svetlana Kushnariova (Cabbit), Dungeon Crawl Stone Soup, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/115, https://opengameart.org/node/12210 @@ -332,7 +333,7 @@ data/sprites/entity/chaos/ chaos_commander.png Stephen Challener (Redshrike), Bertram, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/77901 chaos_dragonrider.png Stephen Challener (Redshrike), Bertram, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/77901 -chaos_lord.png Stephen Challener (Redshrike), Bertram, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/77901 +chaos_lord Stephen Challener (Redshrike), Bertram, Dan Štefka (sufan02), olonu, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/77901, https://opengameart.org/node/68435 chaos_overlord.png Stephen Challener (Redshrike), Bertram, johnny_automatic, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/77901 chaos_soldier.png Stephen Challener (Redshrike), Bertram; CC BY-SA 3.0; https://opengameart.org/node/12858; commissioned by Bertram chaos_sorceror.png Stephen Challener (Redshrike); OGA BY 3.0; https://opengameart.org/node/5215 @@ -349,12 +350,12 @@ dark_elf_captain.* Svetlana Kushnariova (Cabbit), olonu, Muhammad Amiruddi dark_elf_elite_archer.* Svetlana Kushnariova (Cabbit), Nila122, SCay, olonu, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/29293, https://opengameart.org/node/76291, https://github.com/arianne/stendhal/issues/150 dark_elf_general.* Svetlana Kushnariova (Cabbit), olonu, LordNeo, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/122, https://opengameart.org/node/72969, https://opengameart.org/node/10878 dark_elf_knight.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), Vollrat, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969, https://opengameart.org/node/11152, https://opengameart.org/node/72575 -dark_elf_master.* Svetlana Kushnariova (Cabbit), olonu, Andrew Kelley (superjoe), Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/116, https://opengameart.org/node/72969, https://opengameart.org/node/17839 +dark_elf_master.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/116, https://opengameart.org/node/72969, https://opengameart.org/node/17839 dark_elf_matronmother.* Svetlana Kushnariova (Cabbit), olonu, 59naga, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/119, https://opengameart.org/node/72969, https://opengameart.org/node/15101 dark_elf_ranger.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/95, https://opengameart.org/node/72969 -dark_elf_sacerdotist.* Svetlana Kushnariova (Cabbit), olonu, jaschon, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/105, https://opengameart.org/node/72969, https://openclipart.org/detail/92953 -dark_elf_viceroy.* Svetlana Kushnariova (Cabbit), olonu, LordNeo, Andrew Kelley (superjoe), Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/112, https://opengameart.org/node/72969, https://opengameart.org/node/17839 -dark_elf_wizard.* Svetlana Kushnariova (Cabbit), olonu, jaschon, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/107, https://opengameart.org/node/72969, https://openclipart.org/detail/92953 +dark_elf_sacerdotist.* Svetlana Kushnariova (Cabbit), olonu, Jason Rebuck (jaschon), Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/105, https://opengameart.org/node/72969, https://openclipart.org/detail/92953 +dark_elf_viceroy.* Svetlana Kushnariova (Cabbit), olonu, LordNeo, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/112, https://opengameart.org/node/72969 +dark_elf_wizard.* Svetlana Kushnariova (Cabbit), olonu, Jason Rebuck (jaschon), Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/arianne/stendhal/issues/107, https://opengameart.org/node/72969, https://openclipart.org/detail/92953 data/sprites/entity/demon/ @@ -362,7 +363,7 @@ data/sprites/entity/demon/ demon.png Stephen Challener (Redshrike), William.Thompsonj; OGA BY 3.0; https://opengameart.org/node/20926 fire_demon.* Svetlana Kushnariova (Cabbit), olonu, Carl Olsson (surt), ahmadmanga; CCO; https://github.com/arianne/stendhal/issues/135 imp.png Stephen Challener (Redshrike), William.Thompsonj, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/20926 -imperial_demon_lord.png Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), olonu, Andrew Kelley (superjoe), johnny_automatic, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://github.com/arianne/stendhal/issues/78, https://openclipart.org/detail/4365 +imperial_demon_lord.png Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), olonu, johnny_automatic, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://github.com/arianne/stendhal/issues/78, https://openclipart.org/detail/4365 imperial_demon_servant.png Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), Bertram, olonu; CC BY-SA 3.0; https://github.com/arianne/stendhal/issues/120, https://opengameart.org/node/12858 water_demon.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969 winged_blue_imp.* Stephen Challener (Redshrike), William.Thompsonj, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/20926 @@ -425,7 +426,7 @@ elf_sacerdotist.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDelu elf_wizard.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/69 mage_elf.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/96 militia_elf.* Svetlana Kushnariova (Cabbit), olonu, Chris Hamons, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/100 -soldier_elf.* Svetlana Kushnariova (Cabbit), olonu, kanaya2529, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/101 +soldier_elf.* Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/101 data/sprites/entity/ent/ @@ -536,7 +537,7 @@ assassin.* Svetlana Kushnariova (Cabbit), diamonddmgirl, olonu, bandit.* Svetlana Kushnariova (Cabbit), diamonddmgirl, Chris_M_The_Game_Dude, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/83661, https://opengameart.org/node/53600 barbarian.* Svetlana Kushnariova (Cabbit), bleutailfly, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/24944 barbarian_chaman.* Svetlana Kushnariova (Cabbit), Carl Olsson (surt), bleutailfly, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/24944, https://opengameart.org/node/2261 -barbarian_elite.* Svetlana Kushnariova (Cabbit), bleutailfly; OGA BY 3.0; https://github.com/bleutailfly/bleu-doodles/blob/d44aeb6/characters/barbarian3.png, https://opengameart.org/node/24944 +barbarian_elite.* Svetlana Kushnariova (Cabbit), bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://github.com/bleutailfly/bleu-doodles/blob/d44aeb6/characters/barbarian3.png, https://opengameart.org/node/24944 barbarian_king.* Svetlana Kushnariova (Cabbit), diamonddmgirl, Bart Kelsey, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/72969, https://opengameart.org/node/83661, https://opengameart.org/node/3755 barbarian_leader.* Svetlana Kushnariova (Cabbit), bleutailfly; OGA BY 3.0; https://github.com/bleutailfly/bleu-doodles/blob/d44aeb6/characters/barbarian4.png, https://opengameart.org/node/72969 barbarian_priest.* Svetlana Kushnariova (Cabbit), bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/24944 @@ -548,7 +549,7 @@ cannibalwoman.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDel darkmonk.png Svetlana Kushnariova (Cabbit), olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/124 deniran_stormtrooper.* Svetlana Kushnariova (Cabbit), Eldiran, bleutailfly; OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/50932 elder_cannibal.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/107646 -emperor_dalmung.* Svetlana Kushnariova (Cabbit), wulax, DkuCook, olonu, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/11165, https://opengameart.org/node/82451 +emperor_dalmung.* Svetlana Kushnariova (Cabbit), wulax, Jason Rebuck (jaschon), olonu, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/11165, https://openclipart.org/detail/92953 hunter.* Svetlana Kushnariova (Cabbit), Michael Williams (BizmasterStudios), olonu; Jordan Irwin (AntumDeluge); https://opengameart.org/node/72969, https://opengameart.org/node/66349 imperial_archer.* Svetlana Kushnariova (Cabbit), olonu, SoCay, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/151 imperial_archer_leader.* Svetlana Kushnariova (Cabbit), olonu, SoCay, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://github.com/arianne/stendhal/issues/160 @@ -608,7 +609,7 @@ general.* Svetlana Kushnariova (Cabbit), Eldiran, bleutailfly, Jordan I infantry.png Svetlana Kushnariova (Cabbit), Eldiran, bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/87473 quartermaster.png Svetlana Kushnariova (Cabbit), Eldiran, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/87473 soldier.png Svetlana Kushnariova (Cabbit), Eldiran, bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969 -storm_trooper.png Svetlana Kushnariova (Cabbit), Eldiran, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/87473 +storm_trooper Svetlana Kushnariova (Cabbit), Eldiran, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/87473 data/sprites/entity/hybrid/ @@ -682,10 +683,10 @@ royal_mummy.* Svetlana Kushnariova (Cabbit), Jordan Irwin (AntumDeluge); OGA BY data/sprites/entity/mutant/ -armed_naga.png bleutailfly; OGA BY 3.0; https://opengameart.org/node/99050 +armed_naga bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/99050 cat_woman.* Svetlana Kushnariova (Cabbit), bleutailfly, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/84455, https://opengameart.org/node/88413 dark_mutant.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), onu/olonu; CC BY-SA 3.0; https://github.com/arianne/stendhal/issues/72 -glow_monster.png Charles Sanchez (CharlesGabriel, Antifarea); CC BY 3.0; https://opengameart.org/node/4125 +glow_monster Charles Sanchez (CharlesGabriel, Antifarea), Jordan Irwin (AntumDeluge); CC BY 3.0; Stendhal, https://opengameart.org/node/4125 high_naga.png bleutailfly; OGA BY 3.0; https://opengameart.org/node/99050 imperial_experiment.png artisticdude, Tuomo Untinen (Reemax), onu/olonu; CC0; https://github.com/arianne/stendhal/issues/74 imperial_mutant* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), William.Thompsonj, olonu, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/72969, https://opengameart.org/node/20926 @@ -765,9 +766,20 @@ bullseye.* Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal training_dummy.* Danimal, Kimmo Rundelin (kiheru), ScratchIO, Jordan Irwin (AntumDeluge); Danimal; CC BY-SA 3.0; Stendhal, https://opengameart.org/node/17272; https://opengameart.org/users/danimal, https://opengameart.org/node/108295 +data/sprites/entity/overlay/ + +beams Rawdanitsu, olonu; CC0; https://opengameart.org/node/18068, https://github.com/arianne/stendhal/issues/104 +electricity* Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal +flame* DkuCook, Jordan Irwin (AntumDeluge); CC0; https://opengameart.org/node/76781, https://opengameart.org/node/82451 +ice_sparkles* Jordan Irwin (AntumDeluge); OGA BY 3.0 or newer; Stendhal +magic_sparkles Rawdanitsu, olonu; CC0; https://opengameart.org/node/18068 +necro_flames Wolf W. (Ouren), Jordan Irwin (AntumDeluge); CC BY 3.0; https://opengameart.org/node/45981 +transform Aidan Walker; CC0; https://opengameart.org/node/160840 + + data/sprites/entity/ratfolk/ -archrat.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), DkuCook, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/82451 +archrat.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/82451 ratman.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/82451 ratwoman.* Svetlana Kushnariova (Cabbit), Stephen Challener (Redshrike), Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/82451 diff --git a/doc/sources/graphics-item.txt b/doc/sources/graphics-item.txt index 04c74eeea6..274defccf5 100644 --- a/doc/sources/graphics-item.txt +++ b/doc/sources/graphics-item.txt @@ -27,7 +27,7 @@ data/sprites/items/armor/ barbarian_armor.png Dungeon Crawl Stone Soup; CC0; https://opengameart.org/node/12210, https://crawl.develz.org/wordpress/ black_armor.png Dungeon Crawl Stone Soup; CC0; https://opengameart.org/node/12210, https://crawl.develz.org/wordpress/ blue_armor.png bleutailfly; OGA BY 3.0; Stendhal -chain_armor.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/13879 +chain_armor.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/13879 chaos_armor.png ScratchIO; CC0; https://opengameart.org/node/108448 dress.png Katherine Fitch (amaranthia); CC0; https://opengameart.org/node/51660 dwarvish_armor.png Dungeon Crawl Stone Soup; CC0; https://opengameart.org/node/12210, https://crawl.develz.org/wordpress/ @@ -94,7 +94,7 @@ data/sprites/items/boots/ black_boots.png David E. Gervais; CC BY 3.0; http://pousse.rapiere.free.fr/tome/; edited? blue_boots.png bleutailfly; OGA BY 3.0; Stendhal -chain_boots.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/13879 +chain_boots.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/13879 chaos_boots.png ScratchIO; CC0; https://opengameart.org/node/108448 darkangels_clan_boots.png Groundhug McHampster (h4mt4r0); GPL 2.0 or newer; Stendhal (re-color of elvish_boots) elvish_boots.png ScratchIO, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/108781 @@ -236,7 +236,7 @@ minor_potion.png Groundhug McHampster (h4mt4r0); GPL 2.0 or newer; pina_colada2.png nicubunu, Bluelads4; GPL 2.0 or newer; Based on CC0 image: https://openclipart.org/detail/12995 poison.png Groundhug McHampster (h4mt4r0); GPL 2.0 or newer; Stendhal, https://sourceforge.net/p/arianne/patches/280/ potion.png Groundhug McHampster (h4mt4r0); GPL 2.0 or newer; Stendhal, https://sourceforge.net/p/arianne/patches/280/ -red_juice.png Open Clipart Library, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/14614 +red_juice.png Muga, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/14614 sedative.* Jordan Irwin (AntumDeluge); CC0; Stendhal soup.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal tea.png strawheart; CC BY-SA 3.0; https://opengameart.org/node/6309 @@ -250,7 +250,7 @@ daisies.png Katie Russell (kymara); GPL 2.0 or newer; Stendhal exotic_flower.png bleutailfly; OGA BY 3.0; Stendhal lilia.png Katie Russell (kymara); GPL 2.0 or newer; Stendhal pansy.png cookieeater, Jordan Irwin (AntumDeluge); CC0; https://openclipart.org/detail/183397 -rose.png Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://freesvg.org/red-rose-with-thorns +rose.png kokon_art, Kevin David Pointon (Firkin), Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/303415 zantedeschia.png omero; CC BY-SA 3.0; Stendhal data/sprites/items/food/ @@ -282,7 +282,6 @@ cod.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal cod_smoked.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal collard.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal corn.* ScratchIO, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/99282 -crab_meat.* Jordan Irwin (AntumDeluge); CC0; https://opengameart.org/node/20711 crepesuzette.png omero; CC BY-SA 3.0; Stendhal easter_egg.png dStulle; CC0; https://openclipart.org/detail/6695, https://freesvg.org/vector-illustration-of-white-egg egg.png dStulle; CC0; https://openclipart.org/detail/6695, https://freesvg.org/vector-illustration-of-white-egg @@ -333,7 +332,7 @@ vanillashake.png tigertoes; CC BY-SA 3.0; Stendhal watermelon.png RedQueen; OGA BY 3.0; Stendhal zucchini.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal -data/sprites/items/food/furniture +data/sprites/items/furniture picture_in_wooden_frame.png Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://opengameart.org/node/79120, "convert -resize 22X20" of stendhal-website/images/photos @@ -374,7 +373,7 @@ data/sprites/items/helmet/ aventail.png Dungeon Crawl Stone Soup, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/12210 black_helmet.* Dungeon Crawl Stone Soup; CC0; https://opengameart.org/node/12210, https://crawl.develz.org/wordpress/ blue_helmet.png Dungeon Crawl Stone Soup; CC0; https://opengameart.org/node/12210, https://crawl.develz.org/wordpress/ -chain_helmet.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/13879 +chain_helmet.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/13879 chaos_helmet.png ScratchIO; CC0; https://opengameart.org/node/108448 elvish_hat.png sandcat01, Jordan Irwin (AntumDluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/69757 enhanced_golden_helmet.* Chrisdesign, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/4338 @@ -395,10 +394,10 @@ xeno_helmet.png David E. Gervais; CC BY 3.0; http://pousse.rapiere.f data/sprites/items/herb/ -arandula.png Jordan Irwin (AntumDeluge), rg1024; OGA BY 3.0; Stendhal, https://openclipart.org/detail/50251, https://freesvg.org/nature-in-spring -kekik.png frankes; CC0; https://openclipart.org/detail/264098 -kokuda.png Firkin; CC0; https://openclipart.org/detail/297464 -mandragora.png Ferdinand Lucas Bauer, Jordan Irwin (AntumDeluge); CC0; https://openclipart.org/detail/345843, Stendhal +arandula.png rg1024, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/50251 +kekik.png frankes, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/264098 +kokuda.png Kevin David Pointon (Firkin), Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/297464 +mandragora.png Ferdinand Lucas Bauer, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/345843 reindeer_moss.png tigertoes; CC BY-SA 3.0; Stendhal sclaria.* martinix, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/3558 @@ -456,7 +455,7 @@ data/sprites/items/legs/ black_legs.png Katie Russel (kymara); GPL 2.0 or newer; Stendhal blue_legs.png bleutailfly; OGA BY 3.0; Stendhal -chain_legs.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/13879 +chain_legs.png Clint Bellanger, Justin Nichol, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/13879 chaos_legs.png ScratchIO; CC0; https://opengameart.org/node/108448 dwarvish_legs.* Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal elvish_legs.* ScratchIO, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://opengameart.org/node/108781 @@ -482,7 +481,7 @@ canotuna.png tigertoes; CC BY-SA 3.0; Stendhal cards.png Daniel Herding (mort); GPL 2.0 or newer; Stendhal decent_meal.png omero; CC BY-SA 3.0; Stendhal dice.png Daniel Herding (mort); GPL 2.0 or newer; Stendhal -giant_heart.png Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/291622 +giant_heart.png Lazur, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/291622 lemon.png omero; CC BY-SA 3.0; Stendhal leather_thread.png Jordan Irwin (AntumDeluge); CC0; Stendhal marbles.* SpiderDave; CC0; https://opengameart.org/node/15250 @@ -572,6 +571,7 @@ antipoison_rings.xcf.bz2 Jordan Irwin (AntumDeluge); OGA BY 3.0; https://openga antitoxin_ring.png Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/20712 antivenom_ring.png Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/20712 aquamarine-ring.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal +clarity.* Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/20712 emerald-ring.png Kimmo Rundelin (kiheru), Katie Russell (kymara); CC BY-SA 3.0; Stendhal engagement_ring.png Katie Russell (kymara), Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal gold-ring.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal @@ -627,7 +627,7 @@ ice_shield.png Megajack64; CC BY 3.0/4.0; https://github.com/aria lich_shield.png David E. Gervais; CC BY 3.0; http://pousse.rapiere.free.fr/tome/; edited? lion_shield.png David E. Gervais; CC BY 3.0; http://pousse.rapiere.free.fr/tome/ magic_plate_shield.* Hernandack, Jordan Irwin (AntumDeluge); OGA BY 3.0; https://opengameart.org/node/91222 -mainio_shield.png ScratchIO, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://opengameart.org/node/97166 +mainio_shield.png ScratchIO; CC0; https://opengameart.org/node/97166 mithril_shield.png tigertoes; CC BY-SA 3.0; Stendhal; from OGA Heather Lee Harvey (Emerald) EmeraldActivities.com & https://opengameart.org/users/emerald plate_shield.png David E. Gervais; CC BY 3.0; http://pousse.rapiere.free.fr/tome/ red_shield.png bleutailfly; OGA BY 3.0; Stendhal @@ -641,6 +641,7 @@ xeno_shield.png Dungeon Crawl Stone Soup; CC0; https://opengameart data/sprites/items/special/ +clover Almeidah, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/216715 eggshells.png dStulle, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/6695, https://freesvg.org/vector-illustration-of-white-egg GM_tokend_bronze.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal GM_tokend_gold1.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal @@ -649,7 +650,7 @@ GM_tokend_old_bronze.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal GM_tokend_old_gold.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal GM_tokend_old_silver.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal GM_tokend_silver.png Anders Asplund (Danter); GPL 2.0 or newer; Stendhal -lucky_charm.* Almeidah, Jordan Irwin (AntumDeluge); CC0; Stendhal, https://openclipart.org/detail/216715 +lucky_charm.* Almeidah, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/216715 mithril_clasp.png tigertoes; CC BY-SA 3.0; Stendhal mythical_egg.png dStulle, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/6695, https://freesvg.org/vector-illustration-of-white-egg @@ -716,7 +717,7 @@ pestleandmortar.png omero; CC BY-SA 3.0; Stendhal pick.png Michael Williams (BizmasterStudios); CC BY 3.0; https://opengameart.org/node/84600 rope.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal rotary_cutter.* Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal -scrolleraser.* crisg, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/188214, https://freesvg.org/vector-clip-artof-speedy-eraser +scrolleraser.* crisg, Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal, https://openclipart.org/detail/188214 shovel.png laabadon, Jordan Irwin (AntumDeluge); CC0; https://openclipart.org/detail/116893 sugarmill.png Alejandro Linares Garcia, Jordan Irwin (AntumDeluge); CC BY-SA 3.0; https://commons.wikimedia.org/wiki/File:SugarCaneMillTapachula.jpg wiresnips.* rones, Jordan Irwin (AntumDeluge); CC0; https://openclipart.org/detail/216787 diff --git a/doc/sources/graphics-misc.txt b/doc/sources/graphics-misc.txt index 61a8c6789c..8ce7b0adfc 100644 --- a/doc/sources/graphics-misc.txt +++ b/doc/sources/graphics-misc.txt @@ -53,15 +53,20 @@ data/gui/joystick/ data/gui/quickmenu/ -chat* narrowhouse; CC0; https://openclipart.org/detail/171388 -chatopts* Jordan Irwin (AntumDeluge); CC0; Stendhal -joystick* sagism; CC0; https://openclipart.org/detail/172636 -log* Dungeon Crawl Stone Soup; CC0; Stendhal, https://opengameart.org/node/12210 -main* Jordan Irwin (AntumDeluge); CC0; Stendhal -menu* Martin Owens (doctormo); CC0; https://openclipart.org/detail/3879 -rotate* rg1024; CC0; https://openclipart.org/detail/20247 -settings* ben; CC0; https://openclipart.org/detail/192629 -sound* mightyman; CC0; https://openclipart.org/detail/165992 +chat* narrowhouse; CC0; https://openclipart.org/detail/171388 +chatopts* Jordan Irwin (AntumDeluge); CC0; Stendhal +joystick* sagism; CC0; https://openclipart.org/detail/172636 +log* Dungeon Crawl Stone Soup; CC0; Stendhal, https://opengameart.org/node/12210 +main* Jordan Irwin (AntumDeluge); CC0; Stendhal +menu* Martin Owens (doctormo); CC0; https://openclipart.org/detail/3879 +pathfinding* Jordan Irwin (AntumDeluge); CC0; Stendhal +rotate* rg1024; CC0; https://openclipart.org/detail/20247 +settings* ben; CC0; https://openclipart.org/detail/192629 +sound* mightyman; CC0; https://openclipart.org/detail/165992 + +data/maps/parallax/ + +stars Jordan Irwin (AntumDeluge); CC0; Stendhal data/sprites/ @@ -147,15 +152,15 @@ sword_light.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal data/sprites/cursor/ activity.png zoli111, Hendrik Brummermann; GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ -attack.png Jordan Irwin (AntumDeluge); CC0; Stendhal -bag.* zoli111, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ -emptybag.png zoli111, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ -harvest.png Jordan Irwin (AntumDeluge); CC0; Stendhal +attack.png zoli111, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ +bag.* zoli111, rg1024, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/, https://openclipart.org/detail/31207 +emptybag.png zoli111, rg1024, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/, https://openclipart.org/detail/31207 +harvest.png zoli111, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ highlight.* zoli111; GPL 3.0; https://gitlab.com/zoli111/simp1e/ itemequip.* zoli111; GPL 3.0; https://gitlab.com/zoli111/simp1e/ itempickupfromslot.png zoli111; GPL 3.0; https://gitlab.com/zoli111/simp1e/ (copy of itemequip.png) itemuse.png zoli111, Hendrik Brummermann, Katie Russell (kymara); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ -lockedbag.png zoli111, Hendrik Brummermann, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ +lockedbag.png zoli111, rg1024, Hendrik Brummermann, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/, https://openclipart.org/detail/31207 look.* zoli111, Jordan Irwin (AntumDeluge); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ normal.* zoli111; GPL 3.0; https://gitlab.com/zoli111/simp1e/ portal.png zoli111, Katie Russell (kymara); GPL 3.0; Stendhal, https://gitlab.com/zoli111/simp1e/ @@ -304,7 +309,9 @@ arrow_switch.* Jordan Irwin (AntumDeluge); OGA BY 3.0; Stendhal data/sprites/weather/ +clouds janay; CC BY-SA 3.0; Stendhal fog.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal +fog_ani Jordan Irwin (AntumDeluge); CC0; Stendhal, https://gmic.eu/ fog_heavy.png Kimmo Rundelin (kiheru); CC BY-SA 3.0; Stendhal rain.* Jordan Irwin (AntumDeluge), Kimmo Rundelin (kiheru); OGA BY 3.0; Stendhal rain_heavy.* Jordan Irwin (AntumDeluge), Kimmo Rundelin (kiheru); OGA BY 3.0; Stendhal diff --git a/libs/marauroa.jar b/libs/marauroa.jar index 2ab331f0b5..746bc22386 100644 Binary files a/libs/marauroa.jar and b/libs/marauroa.jar differ diff --git a/package-lock.json b/package-lock.json index 32126ed830..0c823a1bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,736 @@ { "name": "stendhal", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stendhal", "version": "1.0.0", "license": "AGPL-3.0-or-later", + "devDependencies": { + "esbuild": "0.28.1", + "showdown": "^2.1.0", + "typedoc": "^0.28.15", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.20.0.tgz", + "integrity": "sha512-Wa57i+bMpK6PGJZ1f2myxo3iO+K/kZikcyvH8NIqNNZhQUbDav7V9LQmWOXhf946mz5c1NZ19WMsGYiDKTryzQ==", + "dev": true, + "license": "MIT", "dependencies": { - "typescript": "^5.1.0" + "@shikijs/engine-oniguruma": "^3.20.0", + "@shikijs/langs": "^3.20.0", + "@shikijs/themes": "^3.20.0", + "@shikijs/types": "^3.20.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz", + "integrity": "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz", + "integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz", + "integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.20.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz", + "integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/showdown": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.0.0" + }, + "bin": { + "showdown": "bin/showdown.js" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.me/tiviesantos" + } + }, + "node_modules/typedoc": { + "version": "0.28.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.15.tgz", + "integrity": "sha512-mw2/2vTL7MlT+BVo43lOsufkkd2CJO4zeOSuWQQsiXoV2VuEn7f6IZp2jsUDPmBMABpgR0R5jlcJ2OGEFYmkyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.17.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23,13 +738,29 @@ "engines": { "node": ">=14.17" } - } - }, - "dependencies": { - "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/package.json b/package.json index 6619d1b7a2..9b000be13f 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,10 @@ }, "homepage": "https://arianne-project.org", "private": true, - "dependencies": { - "typescript": "^5.2.0" - }, "devDependencies": { + "esbuild": "0.28.1", "showdown": "^2.1.0", - "typedoc": "^0.25.12" + "typedoc": "^0.28.15", + "typescript": "^5.9.3" } } diff --git a/src/games/stendhal/client/gui/KTextEdit.java b/src/games/stendhal/client/gui/KTextEdit.java index 67a3ebe3fa..5c0448eeef 100644 --- a/src/games/stendhal/client/gui/KTextEdit.java +++ b/src/games/stendhal/client/gui/KTextEdit.java @@ -569,7 +569,7 @@ class KTextEdit extends JComponent { */ class LinkListener { /** Allowed patterns for links to be opened in a browser. */ - String p = "^(http|https)://(|www\\.)(faiumoni.de|stendhalgame.org|arianne.sf.net|arianne-project.org|arianne.sourceforge.net|sourceforge.net|sf.net|download.oracle.com|libregamewiki.org|freesound.org|opengameart.org|openclipart.org|github.io|github.com|arianne.github.io|postsai.github.io)(/.*)?"; + String p = "^(http|https)://(|www\\.)(faiumoni.de|stendhalgame.org|arianne.sf.net|arianne-project.org|codeberg.org|arianne.sourceforge.net|sourceforge.net|sf.net|download.oracle.com|libregamewiki.org|freesound.org|opengameart.org|openclipart.org|github.io|github.com|arianne.github.io|postsai.github.io)(/.*)?"; final Pattern whitelist = Pattern.compile(p); diff --git a/src/games/stendhal/client/gui/imageviewer/RPEventImageViewer.java b/src/games/stendhal/client/gui/imageviewer/RPEventImageViewer.java index 45d5d413de..7b14d5dcdc 100644 --- a/src/games/stendhal/client/gui/imageviewer/RPEventImageViewer.java +++ b/src/games/stendhal/client/gui/imageviewer/RPEventImageViewer.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,6 +12,7 @@ ***************************************************************************/ package games.stendhal.client.gui.imageviewer; +import java.net.URI; import java.net.URL; import org.apache.log4j.Logger; @@ -53,7 +54,7 @@ public final class RPEventImageViewer { try { URL url = null; if (path.startsWith("http://") || path.startsWith("https://")) { - url = new URL(path); + url = new URI(path).toURL(); } else { url = DataLoader.getResource(path); } diff --git a/src/games/stendhal/client/sound/manager/SoundManagerNG.java b/src/games/stendhal/client/sound/manager/SoundManagerNG.java index 501c408910..8c285ad69b 100644 --- a/src/games/stendhal/client/sound/manager/SoundManagerNG.java +++ b/src/games/stendhal/client/sound/manager/SoundManagerNG.java @@ -260,18 +260,17 @@ public class SoundManagerNG { mSoundSystem.start(); } - public synchronized Sound openSound(AudioResource AudioResource, - SoundFileType fileType, int numSamplesPerChunk, - boolean enableStreaming) { + public synchronized Sound openSound(AudioResource ar, SoundFileType fileType, + int numSamplesPerChunk, boolean enableStreaming) { Sound sound = null; try { - SoundFile file = new SoundFile(AudioResource, fileType, + SoundFile file = new SoundFile(ar, fileType, numSamplesPerChunk, enableStreaming); sound = new Sound(); sound.file.set(file); - } catch (IOException exception) { - logger.warn(exception); + } catch (IOException e) { + logger.warn("Cannot open sound: " + ar.getName(), e); return null; } diff --git a/src/games/stendhal/client/sprite/DataLoader.java b/src/games/stendhal/client/sprite/DataLoader.java index ba7a037418..9b3e3b8c5d 100644 --- a/src/games/stendhal/client/sprite/DataLoader.java +++ b/src/games/stendhal/client/sprite/DataLoader.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2012-2022 Faiumoni e. V. * + * (C) Copyright 2012-2025 Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -15,6 +15,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import java.util.Enumeration; @@ -60,8 +62,8 @@ public class DataLoader { File file = contentFilenameMapping.get(slashlessName); if (file != null) { try { - return new URL("jar:" + file.toURI().toASCIIString() + "!/" + slashlessName); - } catch (MalformedURLException e) { + return new URI("jar:" + file.toURI().toASCIIString() + "!/" + slashlessName).toURL(); + } catch (MalformedURLException | URISyntaxException e) { logger.error(e, e); } } diff --git a/src/games/stendhal/client/sprite/SpriteStore.java b/src/games/stendhal/client/sprite/SpriteStore.java index 7ad4600047..cdc00ebd3c 100644 --- a/src/games/stendhal/client/sprite/SpriteStore.java +++ b/src/games/stendhal/client/sprite/SpriteStore.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -22,6 +22,8 @@ import java.awt.HeadlessException; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import javax.imageio.ImageIO; @@ -353,7 +355,7 @@ public class SpriteStore { URL url; if (ref.startsWith("http://") || ref.startsWith("https://")) { logger.info("Loading sprite from a URL..."); - url = new URL(ref); + url = new URI(ref).toURL(); } else { url = DataLoader.getResource(ref); } @@ -372,7 +374,7 @@ public class SpriteStore { // use ImageIO to read the image in sourceImage = ImageIO.read(url); - } catch (final IOException e) { + } catch (final IOException | URISyntaxException e) { logger.error("Failed to read: " + ref, e); if (ref.equals(FAILSAFE_ICON_REF)) { diff --git a/src/games/stendhal/client/update/HttpClient.java b/src/games/stendhal/client/update/HttpClient.java index 50d83d12cc..5cb0b66690 100644 --- a/src/games/stendhal/client/update/HttpClient.java +++ b/src/games/stendhal/client/update/HttpClient.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2016 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -20,6 +20,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; +import java.net.URI; import java.net.URL; import java.util.HashSet; import java.util.Properties; @@ -109,13 +110,15 @@ public class HttpClient { // sometimes problems with the webservers being slow or not responding // at all. try { - final URL url = new URL(urlString); + final URL url = new URI(urlString).toURL(); int retryCount = 0; int myTimeout = timeout; while (is == null) { retryCount++; try { connection = (HttpURLConnection) url.openConnection(); + // Wikipedia API now requires User-Agent string + connection.setRequestProperty("User-Agent", "StendhalClient/1.49.5 (https://arianne-project.org/contact.html)"); connection.setConnectTimeout(myTimeout); connection.setInstanceFollowRedirects(true); connection.setUseCaches(false); @@ -134,7 +137,7 @@ public class HttpClient { if(!passedRedirects.contains(newUrl)) { // open the new connnection again passedRedirects.add(newUrl); - connection = (HttpURLConnection) new URL(newUrl).openConnection(); + connection = (HttpURLConnection) new URI(newUrl).toURL().openConnection(); redirect = isRedirect(connection.getResponseCode()); } else { System.err.println(String.format("The URL '%s' leads into a circular redirect.", url)); diff --git a/src/games/stendhal/common/CollisionDetection.java b/src/games/stendhal/common/CollisionDetection.java index 7022aa0571..585f95e445 100644 --- a/src/games/stendhal/common/CollisionDetection.java +++ b/src/games/stendhal/common/CollisionDetection.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003 - Marauroa * + * (C) Copyright 2003-2024 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -12,7 +12,6 @@ ***************************************************************************/ package games.stendhal.common; - import java.awt.geom.Rectangle2D; import games.stendhal.common.tiled.LayerDefinition; @@ -22,10 +21,10 @@ import games.stendhal.common.tiled.LayerDefinition; * not with any of the non trespasable areas of the world. */ public class CollisionDetection { + private CollisionMap map; private int width; - private int height; /** @@ -40,8 +39,10 @@ public class CollisionDetection { /** * Initialize the collision map to desired size. * - * @param width width of the map - * @param height height of the map + * @param width + * Width of the map. + * @param height + * Height of the map. */ public void init(final int width, final int height) { if (this.width != width || this.height != height) { @@ -59,8 +60,10 @@ public class CollisionDetection { /** * Set a position in the collision map to static collision. * - * @param x x coordinate - * @param y y coordinate + * @param x + * X coordinate. + * @param y + * Y coordinate. */ public void setCollide(final int x, final int y) { if ((x < 0) || (x >= width) || (y < 0) || (y >= height)) { @@ -72,7 +75,8 @@ public class CollisionDetection { /** * Fill the collision map from layer data. * - * @param collisionLayer static collision information + * @param collisionLayer + * Static collision information. */ public void setCollisionData(final LayerDefinition collisionLayer) { // First we build the int array. @@ -83,7 +87,7 @@ public class CollisionDetection { for (int x = 0; x < width; x++) { /* * NOTE: Right now our collision detection system is binary, so - * something or is blocked or is not. + * something is blocked or is not. */ if (collisionLayer.getTileAt(x, y) != 0) { map.set(x, y); @@ -119,9 +123,10 @@ public class CollisionDetection { /** * Check if a rectangle is at least partially outside the map. * - * @param shape area to be checked - * @return true if shape is at least partially outside the map, - * false otherwise + * @param shape + * Area to be checked. + * @return + * {@code true} if shape is at least partially outside the map, {@code false} otherwise. */ public boolean leavesZone(final Rectangle2D shape) { final double x = shape.getX(); @@ -132,12 +137,34 @@ public class CollisionDetection { return (x < 0) || (x + w > width) || (y < 0) || (y + h > height); } + /** + * Get the width of the collision map. + * + * @return + * Map width. + */ + public int getWidth() { + return width; + } + + /** + * Get the height of the collision map. + * + * @return + * Map height. + */ + public int getHeight() { + return height; + } + /** * Check if a rectangle overlaps colliding areas. * - * @param shape checked area - * @return true if the shape enters in any of the non - * trespassable areas of the map, false otherwise + * @param shape + * Checked Area + * @return + * {@codetrue} if the shape enters in any of the non-trespassable areas of the map, + * {@code false} otherwise. */ public boolean collides(final Rectangle2D shape) { final double x = shape.getX(); @@ -147,16 +174,20 @@ public class CollisionDetection { return collides(x, y, w, h); } - /** * Check if a rectangle overlaps colliding areas. * - * @param x x-position - * @param y y-position - * @param w width - * @param h height - * @return true if the shape enters in any of the non - * trespassable areas of the map, false otherwise + * @param x + * Rectangle X position. + * @param y + * Rectangle Y position. + * @param w + * Rectangle width. + * @param h + * Rectangle height. + * @return + * {@code true} if the shape enters in any of the non-trespassable areas of the map, + * {@code false} otherwise. */ public boolean collides(final double x, final double y, final double w, final double h) { /* @@ -178,10 +209,12 @@ public class CollisionDetection { /** * Check if a location is marked with collision. * - * @param x x coordinate - * @param y y coordinate - * @return true if the map position is a collision tile, - * otherwise false + * @param x + * X coordinate. + * @param y + * Y coordinate. + * @return + * {@code true} if the map position is a collision tile, otherwise {@code false}. */ public boolean collides(final int x, final int y) { if ((x < 0) || (x >= width)) { @@ -193,22 +226,4 @@ public class CollisionDetection { } return map.get(x, y); } - - /** - * Get the width of the collision map. - * - * @return width - */ - public int getWidth() { - return width; - } - - /** - * Get the height of the collision map. - * - * @return height - */ - public int getHeight() { - return height; - } } diff --git a/src/games/stendhal/common/CollisionMap.java b/src/games/stendhal/common/CollisionMap.java index 4f19386f9f..f5b95d351e 100644 --- a/src/games/stendhal/common/CollisionMap.java +++ b/src/games/stendhal/common/CollisionMap.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -19,12 +19,24 @@ import java.util.BitSet; import games.stendhal.common.tiled.LayerDefinition; + +/** + * A map containing collision information. + */ public class CollisionMap { private final int width; private final int height; private final BitSet[] colls; + /** + * Creates a new empty collision map. + * + * @param width + * Node width of new map. + * @param height. + * Node height of new map. + */ public CollisionMap(final int width, final int height) { this.width = width; this.height = height; @@ -35,6 +47,12 @@ public class CollisionMap { } + /** + * Creates a new collision map using layers definition. + * + * @param layer + * Definition used to create map. + */ public CollisionMap(final LayerDefinition layer) { this(layer.getWidth(), layer.getHeight()); for (int x = 0; x < width; x++) { @@ -46,23 +64,94 @@ public class CollisionMap { } } + /** + * Retrieves width of collision map. + */ public int getWidth() { return width; } + /** + * Retrieves height of collision map. + */ public int getHeight() { return height; } + /** + * Checks if node is a collision tile. + * + * @param i + * Node X coordinate. + * @param j + * Node Y coordinate. + * @return + * {@code true} if node has collision. + */ public boolean get(final int i, final int j) { return colls[i].get(j); } + /** + * Sets a collision node. + * + * @param i + * Node X coordinate. + * @param j + * Node Y coordinate. + */ public void set(final int i, final int j) { - colls[i].set(j); } + /** + * Sets collision for a rectangle area. + * + * @param shape + * Area to be set. + */ + public void set(final Rectangle2D shape) { + int y = (int) shape.getY(); + for (int x = (int) shape.getX(); x < shape.getX() + shape.getWidth(); x++) { + colls[x].set(y, (int) (y + shape.getHeight())); + } + } + + /** + * Removes collision from a node. + * + * @param i + * Node X coordinate. + * @param k + * Node Y coordinate. + */ + public void unset(final int i, final int k) { + colls[i].clear(k); + } + + /** + * Removes all collision from the map. + */ + public void clear() { + for (int i = 0; i < this.width; i++) { + colls[i].clear(); + } + } + + /** + * Checks for collision in a rectangle area. + * + * @param x + * Beginning node X coordinate. + * @param y + * Beginning node Y coordinate. + * @param width + * Width of area to be checked. + * @param height + * Height of area to be checked. + * @return + * {@code true} if collision is found in area. + */ public boolean collides(final int x, final int y, final int width, final int height) { if (x < 0 || x - 1 + width >= this.width) { return true; @@ -80,16 +169,16 @@ public class CollisionMap { return !result.get(y, y + height).isEmpty(); } - public void clear() { - for (int i = 0; i < this.width; i++) { - colls[i].clear(); - } - - } + /** + * Creates a new collision map. + * + * @param layer + * Layer definition. + * @return + * Resulting collision map. + */ public static CollisionMap create(final LayerDefinition layer) { - - CollisionMap collissionMap = new CollisionMap(layer.getWidth(), layer - .getHeight()); + CollisionMap collissionMap = new CollisionMap(layer.getWidth(), layer.getHeight()); for (int x = 0; x < layer.getWidth(); x++) { for (int y = 0; y < layer.getHeight(); y++) { if (layer.getTileAt(x, y) != 0) { @@ -99,19 +188,4 @@ public class CollisionMap { } return collissionMap; } - - public void unset(final int i, final int k) { - colls[i].clear(k); - - } - - public void set(final Rectangle2D shape) { - int y = (int) shape.getY(); - for (int x = (int) shape.getX(); x < shape.getX() + shape.getWidth(); x++) { - - colls[x].set(y, (int) (y + shape.getHeight())); - } - - } - } diff --git a/src/games/stendhal/common/Constants.java b/src/games/stendhal/common/Constants.java index 7451efad3a..f4f2b35c13 100644 --- a/src/games/stendhal/common/Constants.java +++ b/src/games/stendhal/common/Constants.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2020 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,9 +12,14 @@ package games.stendhal.common; /** - * Constants about slots + * General constants. */ public final class Constants { + + public static final String[] EQUIPMENT_SLOTS = { + "head", "rhand", "lhand", "armor", "finger", "cloak", "legs", "feet" + }; + /** * All the slots considered to be "with" the entity. Listed in priority * order (i.e. bag first). @@ -24,4 +29,7 @@ public final class Constants { "pouch", "bag", "keyring", "portfolio", "back", "belt", "head", "rhand", "lhand", "armor", "finger", "cloak", "legs", "feet" }; + + /** Distance at which entity sounds can be heard. */ + public static final int DEFAULT_SOUND_RADIUS = 23; } diff --git a/src/games/stendhal/common/Debug.java b/src/games/stendhal/common/Debug.java index 7b8468604c..02e0f91f7b 100644 --- a/src/games/stendhal/common/Debug.java +++ b/src/games/stendhal/common/Debug.java @@ -26,7 +26,7 @@ public class Debug { /** version. */ // Note: This line is updated by build.xml using a regexp so be sure to adjust it in case you modify this line. - public static final String VERSION = "1.46.5"; + public static final String VERSION = "1.49.5"; /** pre release suffix */ // Note: This line is updated by build.xml using a regexp so be sure to adjust it in case you modify this line. diff --git a/src/games/stendhal/common/Rand.java b/src/games/stendhal/common/Rand.java index 42bf72385f..e49aec9ecf 100644 --- a/src/games/stendhal/common/Rand.java +++ b/src/games/stendhal/common/Rand.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003 - Marauroa * + * (C) Copyright 2003-2024 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -206,4 +206,14 @@ public class Rand { public static boolean flipCoin(final double propability) { return rand.nextDouble() <= propability; } + + /** + * Flip a coin to decide between true and false based on even probability. + * + * @return + * {@code true} or {@code false} randomly. + */ + public static boolean flipCoin() { + return flipCoin(0.5); + } } diff --git a/src/games/stendhal/common/grammar/Grammar.java b/src/games/stendhal/common/grammar/Grammar.java index 7f802c2b05..32a3c798c2 100644 --- a/src/games/stendhal/common/grammar/Grammar.java +++ b/src/games/stendhal/common/grammar/Grammar.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2009-2023 - Stendhal * + * (C) Copyright 2009-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -526,7 +526,8 @@ public class Grammar { // first of all handle words which do not change } else if (enoun.endsWith("money") || enoun.endsWith("dice") || enoun.endsWith("sheep") || enoun.equals("deer") - || enoun.equals("moose") || enoun.equals("magic")){ + || enoun.equals("moose") || enoun.equals("magic") + || enoun.equals("chaos")) { return enoun + postfix; // ok and now all the special cases @@ -650,7 +651,8 @@ public class Grammar { || enoun.endsWith("sheep") || enoun.endsWith("legs") || enoun.endsWith("boots") || enoun.endsWith("pegasus") || enoun.endsWith("djinn") - || enoun.equals("deer") || enoun.equals("moose") || enoun.equals("magic")) { + || enoun.equals("deer") || enoun.equals("moose") || enoun.equals("magic") + || enoun.equals("chaos")) { return enoun + postfix; // now all the special cases @@ -777,6 +779,23 @@ public class Grammar { return plnoun(quantity, noun); } + /** + * Converts a noun to possessive form. + * + * @param noun + * The noun to examine. + * @param plural + * Whether the noun is considered to be plural. + * @return + * Possessive form. + */ + public static String possessive(final String noun, final boolean plural) { + if (plural && noun.endsWith("s")) { + return noun + "'"; + } + return noun + "'s"; + } + /** * Returns either the plural or singular form of the given noun, depending * on the quantity; also prefixes the quantity. @@ -815,6 +834,49 @@ public class Grammar { return quantityplnoun(quantity, noun); } + /** + * Returns either the plural or singular form of the given noun, depending + * on the quantity; also prefixes the quantity. In case the quantity is exactly + * 1, the specified prefix is used. Note: There is some additional magic to convert + * "a" and "A" to "an" and "An" in case that is required by the noun. + * + * @param quantity + * The quantity to examine + * @param noun + * The noun to examine + * @param one replacement for "1". + * @param possessive Whether the noun should be considered to be in possessive form. + * @return Either "[quantity] [noun]" or "[quantity]" + plural("[noun]") as + * appropriate + */ + public static String quantityplnoun(final int quantity, String noun, final String one, + final boolean possessive) { + final String word = plnoun(quantity, noun); + + if (quantity == 1) { + String phrase; + if (one.equals("a")) { + phrase = a_noun(word); + } else if (one.equals("A")) { + phrase = A_noun(word); + } else if (one.equals("")) { + phrase = word; + } else { + phrase = one + " " + word; + } + if (possessive) { + phrase = phrase.replaceAll(word + "$", possessive(word, false)); + } + return phrase; + } else { + noun = plural(noun); + if (possessive) { + noun = possessive(noun, true); + } + return Integer.toString(quantity) + " " + noun; + } + } + /** * Returns either the plural or singular form of the given noun, depending * on the quantity; also prefixes the quantity. In case the quantity is exactly @@ -830,21 +892,27 @@ public class Grammar { * appropriate */ public static String quantityplnoun(final int quantity, final String noun, final String one) { - final String word = plnoun(quantity, noun); + return quantityplnoun(quantity, noun, one, false); + } - if (quantity == 1) { - if (one.equals("a")) { - return a_noun(word); - } else if (one.equals("A")) { - return A_noun(word); - } else if (one.equals("")) { - return word; - } else { - return one + " " + word; - } - } else { - return Integer.toString(quantity) + " " + plural(noun); - } + + /** + * Returns either the plural or singular possessive form of the given noun, depending + * on the quantity; also prefixes the quantity. In case the quantity is exactly + * 1, the specified prefix is used. Note: There is some additional magic to convert + * "a" and "A" to "an" and "An" in case that is required by the noun. + * + * @param quantity + * The quantity to examine + * @param noun + * The noun to examine + * @param one replacement for "1". + * @return Either "[quantity] [noun]" or "[quantity]" + plural("[noun]") as + * appropriate + */ + public static String quantityplnounPossessive(final int quantity, final String noun, + final String one) { + return quantityplnoun(quantity, noun, one, true); } /** diff --git a/src/games/stendhal/server/core/account/NPCNameValidator.java b/src/games/stendhal/server/core/account/NPCNameValidator.java index 0e5ef1d749..48bde89f33 100644 --- a/src/games/stendhal/server/core/account/NPCNameValidator.java +++ b/src/games/stendhal/server/core/account/NPCNameValidator.java @@ -19,6 +19,9 @@ import marauroa.common.game.Result; /** * validates name is not an NPC name * + * TODO: this should also check the reserved names list for dynamically created NPCs + * `games.stendhal.server.entity.npc.NPCList.reserved` + * * @author kymara */ public class NPCNameValidator implements AccountParameterValidator { diff --git a/src/games/stendhal/server/core/config/ShopsXMLLoader.java b/src/games/stendhal/server/core/config/ShopsXMLLoader.java index 1e829daf60..d011a52e4d 100644 --- a/src/games/stendhal/server/core/config/ShopsXMLLoader.java +++ b/src/games/stendhal/server/core/config/ShopsXMLLoader.java @@ -15,7 +15,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -43,8 +42,6 @@ public class ShopsXMLLoader extends DefaultHandler { private final static Logger logger = Logger.getLogger(ShopsXMLLoader.class); - private static boolean initialized = false; - private Map>> inventories; private List configurators; @@ -53,49 +50,8 @@ public class ShopsXMLLoader extends DefaultHandler { private ShopInventory currentInventory; private String currentItem; - /** The singleton instance. */ - private static ShopsXMLLoader instance; - /** - * Singleton access method. - * - * @return - * The static instance. - */ - @Deprecated - public static ShopsXMLLoader get() { - if (instance == null) { - instance = new ShopsXMLLoader(); - } - - return instance; - } - - @Deprecated - public void init() { - if (initialized) { - logger.warn("Tried to re-initialize shops loader"); - return; - } - - final String xml = "/data/conf/shops.xml"; - final InputStream in = getClass().getResourceAsStream(xml); - - if (in == null) { - logger.info("Shops config (" + xml + ") not found, not loading"); - return; - } - - try { - load(new URI(xml)); - in.close(); - initialized = true; - } catch (final SAXException | URISyntaxException | IOException e) { - logger.error(e); - } - } - public void load(final URI uri) throws SAXException { try { // reset data diff --git a/src/games/stendhal/server/core/config/ZonesXMLLoader.java b/src/games/stendhal/server/core/config/ZonesXMLLoader.java index 0798dfd9d2..c199e83f74 100644 --- a/src/games/stendhal/server/core/config/ZonesXMLLoader.java +++ b/src/games/stendhal/server/core/config/ZonesXMLLoader.java @@ -4,7 +4,7 @@ * $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -31,9 +31,6 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; -// -// - import games.stendhal.common.tiled.LayerDefinition; import games.stendhal.common.tiled.StendhalMapStructure; import games.stendhal.server.core.config.zone.AttributesXMLReader; @@ -226,6 +223,8 @@ public final class ZonesXMLLoader { // Effect layers are optional too loadOptionalLayer(zone, zonedata, "blend_ground"); loadOptionalLayer(zone, zonedata, "blend_roof"); + // alternative floor layer for clients that support parallax backgrounds + loadOptionalLayer(zone, zonedata, "0_floor_parallax"); zone.addCollisionLayer(name + ".collision", zonedata.getLayer("collision")); diff --git a/src/games/stendhal/server/core/config/zone/TMXLoader.java b/src/games/stendhal/server/core/config/zone/TMXLoader.java index 383824270e..1b55092d6d 100644 --- a/src/games/stendhal/server/core/config/zone/TMXLoader.java +++ b/src/games/stendhal/server/core/config/zone/TMXLoader.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -16,7 +16,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; import java.util.List; import java.util.Properties; import java.util.zip.GZIPInputStream; @@ -194,6 +194,42 @@ public class TMXLoader { return layer; } + /** + * Checks value of layer's "visible" attribute. + * + * Default is {@code true} if attribute is not set. + * + * @param attributes + * Layer's attributes. + * @return + * {@code true} if "visible" attribute is NOT set or its value is not "0". + */ + private boolean layerVisible(final NamedNodeMap attributes) { + final Node attrVisible = attributes.getNamedItem("visible"); + if (attrVisible != null) { + return !"0".equals(attrVisible.getNodeValue()); + } + return true; + } + + /** + * Checks value of layer's "locked" attribute. + * + * Default is {@code false} if attribute not set. + * + * @param attributes + * Layer's attributes. + * @return + * {@code true} if "locked" attribute IS set and its value is not "0". + */ + private boolean layerLocked(final NamedNodeMap attributes) { + final Node attrLocked = attributes.getNamedItem("locked"); + if (attrLocked != null) { + return !"0".equals(attrLocked.getNodeValue()); + } + return false; + } + private void buildMap(final Document doc) throws Exception { Node mapNode = doc.getDocumentElement(); @@ -218,7 +254,13 @@ public class TMXLoader { if ("tileset".equals(sibs.getNodeName())) { stendhalMap.addTileset(unmarshalTileset(sibs)); } else if ("layer".equals(sibs.getNodeName())) { - stendhalMap.addLayer(readLayer(sibs)); + final NamedNodeMap attributes = sibs.getAttributes(); + if (!layerVisible(attributes) && layerLocked(attributes)) { + // ignore invisible locked layers (marked for non-use) + logger.info("Ignoring layer \"" + attributes.getNamedItem("name").getNodeValue() + "\""); + } else { + stendhalMap.addLayer(readLayer(sibs)); + } } } } @@ -260,8 +302,8 @@ public class TMXLoader { final String xmlFile = makeUrl(filename); // xmlPath = makeUrl(xmlPath); - final URL url = new URL(xmlFile); - is = url.openStream(); + final URI uri = new URI(xmlFile); + is = uri.toURL().openStream(); } // Wrap with GZIP decoder for .tmx.gz files diff --git a/src/games/stendhal/server/core/engine/SingletonRepository.java b/src/games/stendhal/server/core/engine/SingletonRepository.java index af49bfdde0..229f630f2d 100644 --- a/src/games/stendhal/server/core/engine/SingletonRepository.java +++ b/src/games/stendhal/server/core/engine/SingletonRepository.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,7 +12,6 @@ ***************************************************************************/ package games.stendhal.server.core.engine; -import games.stendhal.server.core.config.ShopsXMLLoader; import games.stendhal.server.core.events.LoginNotifier; import games.stendhal.server.core.events.LogoutNotifier; import games.stendhal.server.core.events.TurnNotifier; @@ -267,11 +266,4 @@ public class SingletonRepository { public static CloneManager getCloneManager() { return CloneManager.get(); } - - /** - * Retrieves XML loader for NPC shops. - */ - public static ShopsXMLLoader getShopsXMLLoader() { - return ShopsXMLLoader.get(); - } } diff --git a/src/games/stendhal/server/core/engine/StendhalRPRuleProcessor.java b/src/games/stendhal/server/core/engine/StendhalRPRuleProcessor.java index e7fef8a150..b24ad8beb3 100644 --- a/src/games/stendhal/server/core/engine/StendhalRPRuleProcessor.java +++ b/src/games/stendhal/server/core/engine/StendhalRPRuleProcessor.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Marauroa * + * (C) Copyright 2003-2025 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -16,7 +16,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; @@ -313,6 +314,12 @@ public class StendhalRPRuleProcessor implements IRPRuleProcessor { logger.error("error in beginTurn", e); } + try { + executeZonePreLogic(); + } catch (final Exception e) { + logger.error("error in beginTurn", e); + } + try { handlePlayersRmTexts(); } catch (final Exception e) { @@ -368,6 +375,12 @@ public class StendhalRPRuleProcessor implements IRPRuleProcessor { }); } + protected void executeZonePreLogic() { + for (final IRPZone zoneI : SingletonRepository.getRPWorld()) { + ((StendhalRPZone) zoneI).preLogic(); + } + } + protected void handleKilledEntities() { /* * This is here because there is a split between last hit and the moment @@ -531,9 +544,9 @@ public class StendhalRPRuleProcessor implements IRPRuleProcessor { if (config.has("server_welcome")) { msg = config.get("server_welcome"); if (msg.startsWith("http://") || msg.startsWith("https://")) { - final URL url = new URL(msg); + final URI uri = new URI(msg); HttpURLConnection.setFollowRedirects(false); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); try { final BufferedReader br = new BufferedReader( new InputStreamReader(connection.getInputStream(), "UTF-8")); @@ -547,7 +560,7 @@ public class StendhalRPRuleProcessor implements IRPRuleProcessor { } } } - } catch (final IOException e) { + } catch (final IOException | URISyntaxException e) { if (firstWelcomeException) { logger.warn("Can't read server_welcome from marauroa.ini", e); firstWelcomeException = false; diff --git a/src/games/stendhal/server/core/engine/StendhalRPZone.java b/src/games/stendhal/server/core/engine/StendhalRPZone.java index e7081d7383..1fe8210796 100644 --- a/src/games/stendhal/server/core/engine/StendhalRPZone.java +++ b/src/games/stendhal/server/core/engine/StendhalRPZone.java @@ -39,6 +39,7 @@ import games.stendhal.common.Debug; import games.stendhal.common.Direction; import games.stendhal.common.Line; import games.stendhal.common.MathHelper; +import games.stendhal.common.Rand; import games.stendhal.common.filter.FilterCriteria; import games.stendhal.common.grammar.Grammar; import games.stendhal.common.tiled.LayerDefinition; @@ -448,12 +449,28 @@ public class StendhalRPZone extends MarauroaRPZone { attributes = attr; } + /** + * Sets collision information for this zone. + * + * @param name + * Layer name. + * @param collisionLayer + * Layer definition. + */ public void addCollisionLayer(final String name, final LayerDefinition collisionLayer) throws IOException { addToContent(name, collisionLayer.encode()); collisionMap.setCollisionData(collisionLayer); } + /** + * Sets protection information for this zone. + * + * @param name + * Layer name. + * @param protectionLayer + * Layer definition. + */ public void addProtectionLayer(final String name, final LayerDefinition protectionLayer) throws IOException { addToContent(name, protectionLayer.encode()); @@ -1102,9 +1119,9 @@ public class StendhalRPZone extends MarauroaRPZone { * Checks an area for collision. * * @param shape - * Rectangle area. + * Rectangle area. * @return - * true if any collision tiles are found in the area. + * {@code true} if any collision tiles are found in the area. */ public boolean collides(final Rectangle2D shape) { return collisionMap.collides(shape); @@ -1523,6 +1540,19 @@ public class StendhalRPZone extends MarauroaRPZone { } } + public void preLogic() { + for (final NPC npc : npcs) { + try { + // SpeakerNPC logic should have already been executed in rule processor + if (!(npc instanceof SpeakerNPC)) { + npc.preLogic(); + } + } catch (final Exception e) { + logger.error("Error in npc pre-logic for zone " + getID().getID(), e); + } + } + } + public void logic() { for (final NPC npc : npcs) { try { @@ -1590,23 +1620,60 @@ public class StendhalRPZone extends MarauroaRPZone { return false; } - + /** + * Get entities on map filtered by criteria. + * + * @param criteria + * Filter criteria. + * @return + * List of entities that meet {@code criteria}. + */ public List getFilteredEntities(final FilterCriteria criteria) { final List result = new LinkedList(); - for (final RPObject obj : objects.values()) { if (obj instanceof Entity) { final Entity entity = (Entity) obj; if (criteria.passes(entity)) { result.add(entity); } - } } - return result; + } + /** + * Get entities on map of a specific class type. + * + * @param clazz + * Entity class type. + * @param subclasses + * If {@code true}, include subclass instances. + * @return + * List of entities of type {@code clazz}. + */ + public List getEntitiesOfClass(Class clazz, + boolean subclasses) { + return getFilteredEntities(new FilterCriteria() { + @Override + public boolean passes(Entity o) { + if (subclasses) { + return clazz.isInstance(o); + } + return o.getClass() == clazz; + } + }); + } + /** + * Get entities on map of a specific class type including subclass instances. + * + * @param clazz + * Entity class type. + * @return + * List of entities of type {@code clazz}. + */ + public List getEntitiesOfClass(Class clazz) { + return getEntitiesOfClass(clazz, true); } /** @@ -1971,4 +2038,38 @@ public class StendhalRPZone extends MarauroaRPZone { } return (WeatherEntity) entities.get(0); } + + /** + * Attempts to find a position where an entity can be spawned. + * + * @param entity + * Entity attempting to spawn. + * @param checkObjects + * If {@code false}, only the collision map will be used. + * @return + * Appropriate position or `null` if none found. + */ + public Point getRandomSpawnPosition(final Entity entity, boolean checkObjects) { + final short retries = 50; + for (short t = 0; t < retries; t++) { + final int x = Rand.rand(getWidth()); + final int y = Rand.rand(getHeight()); + if (!collides(entity, x, y, checkObjects)) { + return new Point(x, y); + } + } + return null; + } + + /** + * Attempts to find a position where an entity can be spawned. + * + * @param entity + * Entity attempting to spawn. + * @return + * Appropriate position or `null` if none found. + */ + public Point getRandomSpawnPosition(final Entity entity) { + return getRandomSpawnPosition(entity, true); + } } diff --git a/src/games/stendhal/server/core/pathfinder/FixedPath.java b/src/games/stendhal/server/core/pathfinder/FixedPath.java index 1b8f3c649c..8c620daca4 100644 --- a/src/games/stendhal/server/core/pathfinder/FixedPath.java +++ b/src/games/stendhal/server/core/pathfinder/FixedPath.java @@ -4,7 +4,7 @@ * $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -61,7 +61,7 @@ public class FixedPath { protected int pos; /** - * Positions in the entity's path where it will suspend for a determined amount of turns. + * Positions in the entity's path where it will suspend for a determined amount of time. */ protected Map> suspensions = new HashMap<>(); @@ -226,7 +226,7 @@ public class FixedPath { * Add a suspension to the entity's path. * * @param duration - * Amount of time (in turns) the entity will be suspended. + * Amount of time (in seconds) the entity will be suspended. * @param dir * Direction to face while suspended, or null * if direction should not be changed. @@ -244,7 +244,7 @@ public class FixedPath { * * * @param duration - * Amount of time (in turns) the entity will be suspended. + * Amount of time (in seconds) the entity will be suspended. * @param pos * The position(s) in the path where to add the suspension. */ @@ -282,7 +282,7 @@ public class FixedPath { * @param pos * Path position to check. * @return - * Duration (in turns) the entity should suspend or null. + * Duration (in seconds) the entity should suspend or null. */ public Integer getSuspendValue(final int pos) { return suspensions.get(pos).first(); diff --git a/src/games/stendhal/server/core/rp/StendhalQuestSystem.java b/src/games/stendhal/server/core/rp/StendhalQuestSystem.java index 21decb3ecf..43daf24e99 100644 --- a/src/games/stendhal/server/core/rp/StendhalQuestSystem.java +++ b/src/games/stendhal/server/core/rp/StendhalQuestSystem.java @@ -138,6 +138,7 @@ public class StendhalQuestSystem { loadQuest(new LearnAboutOrbs()); loadQuest(new LookBookforCeryl()); loadQuest(new LookUpQuote()); + loadQuest(new LuckyFourLeafClover()); loadQuest(new KanmararnSoldiers()); loadQuest(new KillBlordroughs()); loadQuest(new KillDarkElves()); diff --git a/src/games/stendhal/server/core/rp/achievement/factory/ObtainAchievementsFactory.java b/src/games/stendhal/server/core/rp/achievement/factory/ObtainAchievementsFactory.java index 3d960eacd1..1a785e27da 100644 --- a/src/games/stendhal/server/core/rp/achievement/factory/ObtainAchievementsFactory.java +++ b/src/games/stendhal/server/core/rp/achievement/factory/ObtainAchievementsFactory.java @@ -116,6 +116,13 @@ public class ObtainAchievementsFactory extends AbstractAchievementFactory { } })); + // find the lucky four-leaf clover + achievements.add(createAchievement( + "obtain.clover", "Luck O' the Irish", + "Find the lucky four-leaf clover", + Achievement.MEDIUM_BASE_SCORE, true, + new PlayerHasHarvestedNumberOfItemsCondition(1, "four-leaf clover"))); + return achievements; } } diff --git a/src/games/stendhal/server/core/rp/achievement/factory/ProductionAchievementFactory.java b/src/games/stendhal/server/core/rp/achievement/factory/ProductionAchievementFactory.java index 5acc7f1bea..d4da39bc6f 100644 --- a/src/games/stendhal/server/core/rp/achievement/factory/ProductionAchievementFactory.java +++ b/src/games/stendhal/server/core/rp/achievement/factory/ProductionAchievementFactory.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -26,7 +26,6 @@ import games.stendhal.server.entity.npc.behaviour.journal.ProducerRegister; import games.stendhal.server.entity.npc.condition.AndCondition; import games.stendhal.server.entity.npc.condition.PlayerProducedNumberOfItemsCondition; import games.stendhal.server.entity.npc.condition.QuestCompletedCondition; -import games.stendhal.server.entity.npc.condition.QuestStateStartsWithCondition; import games.stendhal.server.entity.player.Player; @@ -64,7 +63,7 @@ public class ProductionAchievementFactory extends AbstractAchievementFactory { Achievement.MEDIUM_BASE_SCORE, true, new AndCondition( new PlayerProducedNumberOfItemsCondition(1, foods), - new QuestStateStartsWithCondition("coal_for_haunchy","waiting;")))); + new QuestCompletedCondition("coal_for_haunchy")))); final List drinklist = producerRegister.getProducedItemNames("drink"); diff --git a/src/games/stendhal/server/entity/Entity.java b/src/games/stendhal/server/entity/Entity.java index 5d39418dc8..54641c7dc0 100644 --- a/src/games/stendhal/server/entity/Entity.java +++ b/src/games/stendhal/server/entity/Entity.java @@ -54,7 +54,7 @@ public abstract class Entity extends RPObject implements Killer { private int y; // Initial coordinates - private Point origin; + protected Point origin; /** * Amount of resistance this has with other entities (0-100). diff --git a/src/games/stendhal/server/entity/GuidedEntity.java b/src/games/stendhal/server/entity/GuidedEntity.java index 2ea2749128..67026fc719 100644 --- a/src/games/stendhal/server/entity/GuidedEntity.java +++ b/src/games/stendhal/server/entity/GuidedEntity.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2022 - Arianne * + * Copyright © 2003-2025 - Arianne * *************************************************************************** *************************************************************************** * * @@ -13,7 +13,6 @@ package games.stendhal.server.entity; import static games.stendhal.common.constants.General.PATHSET; -import java.awt.Point; import java.awt.geom.Rectangle2D; import java.util.LinkedList; import java.util.List; @@ -388,7 +387,7 @@ public abstract class GuidedEntity extends ActiveEntity { final GuidedEntity tmp = this; - TurnNotifier.get().notifyInTurns(guide.path.getSuspendValue(guide.pathPosition), new TurnListener() { + TurnNotifier.get().notifyInSeconds(guide.path.getSuspendValue(guide.pathPosition), new TurnListener() { @Override public void onTurnReached(final int currentTurn) { setBaseSpeed(storedSpeed); @@ -422,7 +421,7 @@ public abstract class GuidedEntity extends ActiveEntity { * Add a suspension to the entity's path. * * @param duration - * Amount of time (in turns) the entity will be suspended. + * Amount of time (in seconds) the entity will be suspended. * @param dir * Direction to face while suspended, or null * if direction should not be changed. @@ -437,8 +436,7 @@ public abstract class GuidedEntity extends ActiveEntity { * Add a suspension to the entity's path. * * @param duration - * Amount of time (in turns) the entity will be suspended. - * if direction should not be changed. + * Amount of time (in seconds) the entity will be suspended. * @param pos * The position(s) in the path where to add the suspension. */ @@ -528,13 +526,11 @@ public abstract class GuidedEntity extends ActiveEntity { * * @return true if the entity has moved outside its movement * area, othwewise false + * @deprecated Use {@code NPC.setIdleBehaviour(new WanderIdleBehaviour())} for random movement. */ + @Deprecated public final boolean atMovementRadius() { - Point difference = getDistanceFromOrigin(); - - // Set the maximum movement distance at exact radius - int max = movementRadius - 1; - return (movementRadius > 0 && (difference.getX() > max || difference.getY() > max)); + return movementRadius > 0 && Math.sqrt(getDistanceFromOrigin()) >= movementRadius; } protected final Direction getDirectionFromOrigin() { @@ -543,21 +539,15 @@ public abstract class GuidedEntity extends ActiveEntity { return dir; } + /** - * Get the distance that the entity has moved away from its starting point + * Get the distance that the entity has moved away from its starting point. * - * @return The distance from entity's starting point + * @return + * Squared distance from starting point. */ - protected final Point getDistanceFromOrigin() { - int originX = getOrigin().x; - int originY = getOrigin().y; - int currentX = getX(); - int currentY = getY(); - - int Xdiff = Math.abs(currentX - originX); - int Ydiff = Math.abs(currentY - originY); - - return new Point(Xdiff, Ydiff); + public double getDistanceFromOrigin() { + return squaredDistance(origin.x, origin.y); } /** @@ -569,7 +559,10 @@ public abstract class GuidedEntity extends ActiveEntity { /** * Changed path of entity when radius is reached + * + * @deprecated Use {@code NPC.setIdleBehaviour(new WanderIdleBehaviour())} for random movement. */ + @Deprecated public void onOutsideMovementRadius() { List nodes = new LinkedList(); diff --git a/src/games/stendhal/server/entity/RPEntity.java b/src/games/stendhal/server/entity/RPEntity.java index 767a1f5c06..cd023fe4bd 100644 --- a/src/games/stendhal/server/entity/RPEntity.java +++ b/src/games/stendhal/server/entity/RPEntity.java @@ -2138,10 +2138,10 @@ public abstract class RPEntity extends CombatEntity { * @return true iff dropping the desired amount was successful. */ public boolean drop(final String name, final int amount) { - return drop(nameMatches(name), amount); + return drop(Item.nameMatches(name), amount); } - private boolean isEquipped(Predicate condition, int amount) { + protected boolean isEquipped(Predicate condition, int amount) { Iterable matching = getAllEquipped(condition)::iterator; int count = 0; for (Item item : matching) { @@ -2153,7 +2153,7 @@ public abstract class RPEntity extends CombatEntity { return false; } - private boolean drop(Predicate condition, int amount) { + protected boolean drop(Predicate condition, int amount) { if (!isEquipped(condition, amount)) { return false; } @@ -2277,7 +2277,7 @@ public abstract class RPEntity extends CombatEntity { * number. */ public boolean isEquipped(final String name, final int amount) { - return isEquipped(nameMatches(name), amount); + return isEquipped(Item.nameMatches(name), amount); } /** @@ -2331,7 +2331,7 @@ public abstract class RPEntity extends CombatEntity { * @return The number of carried items */ public int getNumberOfEquipped(final String name) { - return equippedStream().filter(nameMatches(name)) + return equippedStream().filter(Item.nameMatches(name)) .mapToInt(Item::getQuantity).sum(); } @@ -2345,7 +2345,7 @@ public abstract class RPEntity extends CombatEntity { */ public int getTotalNumberOf(final String name) { Stream allItems = slots().stream().flatMap(this::slotStream); - return allItems.filter(nameMatches(name)).mapToInt(Item::getQuantity).sum(); + return allItems.filter(Item.nameMatches(name)).mapToInt(Item::getQuantity).sum(); } /** @@ -2358,7 +2358,7 @@ public abstract class RPEntity extends CombatEntity { * found */ public Item getFirstEquipped(final String name) { - return equippedStream().filter(nameMatches(name)).findFirst().orElse(null); + return equippedStream().filter(Item.nameMatches(name)).findFirst().orElse(null); } /** @@ -2371,13 +2371,25 @@ public abstract class RPEntity extends CombatEntity { * found */ public List getAllEquipped(final String name) { - return getAllEquipped(nameMatches(name)); + return getAllEquipped(Item.nameMatches(name)); } - private List getAllEquipped(Predicate condition) { + protected List getAllEquipped(Predicate condition) { return equippedStream().filter(condition).collect(Collectors.toList()); } + /** + * Retrieves items carried in equipment slots (weapons, armor, etc.). + * + * @return + * Equipped items. + */ + protected List getAllEquipment() { + final Stream slotNames = Slots.EQUIPMENT.getNames().stream(); + final Stream slots = slotNames.map(this::getSlot).filter(Objects::nonNull); + return slots.flatMap(this::slotStream).collect(Collectors.toList()); + } + /** * Retrieves all of an item with matching info string. * @@ -3451,24 +3463,16 @@ public abstract class RPEntity extends CombatEntity { /** * Get a stream of all equipped items. * + * FIXME: perhaps should be renamed to avoid confusion with equipment slots (weapons, armor, etc.) + * * @return equipped items */ - private Stream equippedStream() { + protected Stream equippedStream() { Stream slotNames = Slots.CARRYING.getNames().stream(); Stream slots = slotNames.map(this::getSlot).filter(Objects::nonNull); return slots.flatMap(this::slotStream); } - /** - * A convenience method for getting a method for matching item names. - * - * @param name name to match - * @return a predicate for matching the name - */ - private Predicate nameMatches(String name) { - return it -> name.equalsIgnoreCase(it.getName()); - } - /** * Sets the attribute to define the shadow that the client should use. * diff --git a/src/games/stendhal/server/entity/creature/Creature.java b/src/games/stendhal/server/entity/creature/Creature.java index fef7884fa6..ca58989cce 100644 --- a/src/games/stendhal/server/entity/creature/Creature.java +++ b/src/games/stendhal/server/entity/creature/Creature.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,6 +12,8 @@ ***************************************************************************/ package games.stendhal.server.entity.creature; +import static games.stendhal.common.Constants.DEFAULT_SOUND_RADIUS; + import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; @@ -43,7 +45,6 @@ import games.stendhal.server.entity.creature.impl.attack.AttackStrategy; import games.stendhal.server.entity.creature.impl.attack.AttackStrategyFactory; import games.stendhal.server.entity.creature.impl.heal.HealerBehavior; import games.stendhal.server.entity.creature.impl.heal.HealerBehaviourFactory; -import games.stendhal.server.entity.creature.impl.idle.IdleBehaviour; import games.stendhal.server.entity.creature.impl.idle.IdleBehaviourFactory; import games.stendhal.server.entity.item.Corpse; import games.stendhal.server.entity.item.Item; @@ -127,7 +128,6 @@ public class Creature extends NPC { private int respawnTime; private Map aiProfiles; - private IdleBehaviour idler; private int targetX; @@ -135,8 +135,6 @@ public class Creature extends NPC { private final int attackTurn = Rand.rand(5); - private boolean isIdle; - /** The type of the damage this creature does */ private Nature damageType = Nature.CUT; /** The type of the damage this creature does in ranged attacks */ @@ -518,7 +516,7 @@ public class Creature extends NPC { } } } - idler = IdleBehaviourFactory.get(aiProfiles); + setIdleBehaviour(IdleBehaviourFactory.get(aiProfiles)); } /** @@ -908,11 +906,20 @@ public class Creature extends NPC { // do nothing } + @Override + public void preLogic() { + // do nothing + } + @Override public void logic() { healer.heal(this); if (!this.getZone().getPlayerAndFriends().isEmpty()) { if (strategy.hasValidTarget(this)) { + if (idler != null) { + // disable stop/direction queues so creature doesn't stop chasing + idler.reset(); + } strategy.getBetterAttackPosition(this); this.applyMovement(); if (strategy.canAttackNow(this)) { @@ -989,7 +996,7 @@ public class Creature extends NPC { * FIXME: doesn't play sound */ private void loopMovementSound() { - movementSoundEvent = new SoundEvent(movementSound, SOUND_RADIUS, 100, SoundLayer.CREATURE_NOISE); + movementSoundEvent = new SoundEvent(movementSound, DEFAULT_SOUND_RADIUS, 100, SoundLayer.CREATURE_NOISE); this.addEvent(movementSoundEvent); this.notifyWorldAboutChanges(); } diff --git a/src/games/stendhal/server/entity/creature/impl/idle/CamouflagedIdleBehaviour.java b/src/games/stendhal/server/entity/creature/impl/idle/CamouflagedIdleBehaviour.java index c5beaf7cae..a136bae280 100644 --- a/src/games/stendhal/server/entity/creature/impl/idle/CamouflagedIdleBehaviour.java +++ b/src/games/stendhal/server/entity/creature/impl/idle/CamouflagedIdleBehaviour.java @@ -10,19 +10,22 @@ ***************************************************************************/ package games.stendhal.server.entity.creature.impl.idle; -import games.stendhal.server.entity.creature.Creature; +import games.stendhal.server.entity.npc.NPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.AbstractIdleBehaviour; +import games.stendhal.server.entity.npc.behaviour.impl.idle.IdleBehaviour; + /** * An idle behaviour, where the idling creature stays invisible * * @author madmetzger */ -class CamouflagedIdleBehaviour implements IdleBehaviour { +class CamouflagedIdleBehaviour extends AbstractIdleBehaviour { private final IdleBehaviour base = new Patroller(); @Override - public void perform(Creature creature) { + public void perform(NPC creature) { creature.setVisibility(50); this.base.perform(creature); } diff --git a/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviourFactory.java b/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviourFactory.java index 0e656be254..311bb40468 100644 --- a/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviourFactory.java +++ b/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviourFactory.java @@ -15,15 +15,32 @@ package games.stendhal.server.entity.creature.impl.idle; import java.util.Map; +import games.stendhal.common.MathHelper; +import games.stendhal.common.Rand; +import games.stendhal.server.entity.npc.behaviour.impl.idle.IdleBehaviour; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; + public class IdleBehaviourFactory { private static final IdleBehaviour nothing = new StandOnIdle(); public static IdleBehaviour get(final Map aiProfiles) { - if (aiProfiles.containsKey("patrolling")) { - return new Patroller(); + IdleBehaviour behaviour = nothing; + if (aiProfiles.containsKey("wander")) { + final int wanderRadius = MathHelper.parseIntDefault(aiProfiles.get("wander"), 0); + if (wanderRadius > 0) { + behaviour = new WanderIdleBehaviour(wanderRadius); + } else { + behaviour = new WanderIdleBehaviour(); + } + if (aiProfiles.containsKey("patrolling")) { + // randomly select between "patrolling" & "wander" for individual entity instance + behaviour = Rand.flipCoin() ? new Patroller() : behaviour; + } + } else if (aiProfiles.containsKey("patrolling")) { + behaviour = new Patroller(); } else if (aiProfiles.containsKey("camouflage")) { - return new CamouflagedIdleBehaviour(); + behaviour = new CamouflagedIdleBehaviour(); } - return nothing; + return behaviour; } } diff --git a/src/games/stendhal/server/entity/creature/impl/idle/Patroller.java b/src/games/stendhal/server/entity/creature/impl/idle/Patroller.java index 323323a632..c6422e8416 100644 --- a/src/games/stendhal/server/entity/creature/impl/idle/Patroller.java +++ b/src/games/stendhal/server/entity/creature/impl/idle/Patroller.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2012 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -13,7 +13,7 @@ package games.stendhal.server.entity.creature.impl.idle; import games.stendhal.common.Direction; -import games.stendhal.server.entity.creature.Creature; +import games.stendhal.server.entity.npc.NPC; class Patroller extends StandOnIdle { private int minX; @@ -26,7 +26,7 @@ class Patroller extends StandOnIdle { * * @param creature */ - private void initArea(final Creature creature) { + private void initArea(final NPC creature) { minX = creature.getX() - 3; maxX = creature.getX() + 2 + (int) (creature.getWidth()); minY = creature.getY() - 3; @@ -34,7 +34,7 @@ class Patroller extends StandOnIdle { } @Override - public void perform(final Creature creature) { + public void perform(final NPC creature) { if (!creature.getZone().getPlayerAndFriends().isEmpty()) { if (creature.hasPath()) { creature.followPath(); @@ -86,7 +86,7 @@ class Patroller extends StandOnIdle { * @return true if the creature would leave the area, * false otherwise */ - private boolean weWouldLeaveArea(final Creature creature, final Direction d) { + private boolean weWouldLeaveArea(final NPC creature, final Direction d) { return (creature.getY() + d.getdy() < minY) || (creature.getY() + d.getdy() > maxY) || (creature.getX() + d.getdx() < minX) diff --git a/src/games/stendhal/server/entity/creature/impl/idle/StandOnIdle.java b/src/games/stendhal/server/entity/creature/impl/idle/StandOnIdle.java index 222366f83e..23de764f83 100644 --- a/src/games/stendhal/server/entity/creature/impl/idle/StandOnIdle.java +++ b/src/games/stendhal/server/entity/creature/impl/idle/StandOnIdle.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -15,11 +15,13 @@ package games.stendhal.server.entity.creature.impl.idle; import games.stendhal.common.Rand; import games.stendhal.server.entity.Entity; import games.stendhal.server.entity.RPEntity; -import games.stendhal.server.entity.creature.Creature; +import games.stendhal.server.entity.npc.NPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.AbstractIdleBehaviour; -public class StandOnIdle implements IdleBehaviour { + +public class StandOnIdle extends AbstractIdleBehaviour { @Override - public void perform(final Creature creature) { + public void perform(final NPC creature) { retreatUnderFire(creature); } @@ -29,7 +31,7 @@ public class StandOnIdle implements IdleBehaviour { * @param creature The creature that should try to retreat. * @return true if trying to escape, false if retreatin is not needed */ - protected boolean retreatUnderFire(final Creature creature) { + protected boolean retreatUnderFire(final NPC creature) { for (RPEntity attacker : creature.getAttackingRPEntities()) { if (attacker.canDoRangeAttack(creature, attacker.getMaxRangeForArcher())) { retreat(creature, attacker); @@ -49,7 +51,7 @@ public class StandOnIdle implements IdleBehaviour { * @param creature The creature that tries to retreat. * @param enemy The enemy to run away from. */ - private void retreat(final Creature creature, final Entity enemy) { + private void retreat(final NPC creature, final Entity enemy) { creature.clearPath(); creature.faceToward(enemy); creature.setDirection(creature.getDirection().oppositeDirection()); diff --git a/src/games/stendhal/server/entity/item/BreakableWeapon.java b/src/games/stendhal/server/entity/item/BreakableWeapon.java index 3e60e9389e..f29766ecde 100644 --- a/src/games/stendhal/server/entity/item/BreakableWeapon.java +++ b/src/games/stendhal/server/entity/item/BreakableWeapon.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2018-2023 - Stendhal * + * Copyright © 2018-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -17,11 +17,17 @@ import java.util.Map; import games.stendhal.common.Rand; import games.stendhal.server.entity.RPEntity; + /** * An item that wears & breaks. + * + * TODO: + * - create unit test + * - convert to "broken" item when "breaks" */ public class BreakableWeapon extends Weapon { + /** Item condition descriptions based on number of uses. */ private static final Map conditions = new LinkedHashMap() {{ put("new", 1.0); put("like new", 0.75); @@ -30,13 +36,32 @@ public class BreakableWeapon extends Weapon { put("very worn", 0.0); }}; + /** Property denoting whether player has been notified that item is about to break. */ private boolean notified = false; + /** + * Creates a breakable weapon item. + * + * @param name + * Item name. + * @param clazz + * Item class. + * @param subclass + * Item subclass. + * @param attributes + * Item attributes. + */ public BreakableWeapon(String name, String clazz, String subclass, Map attributes) { super(name, clazz, subclass, attributes); } + /** + * Copy constructor. + * + * @param item + * Item to be copied. + */ public BreakableWeapon(final BreakableWeapon item) { super(item); } @@ -47,7 +72,7 @@ public class BreakableWeapon extends Weapon { } /** - * Sets the item's state back to new. + * Sets the item's condition back to new (0 uses). */ @Override public void repair() { @@ -60,20 +85,26 @@ public class BreakableWeapon extends Weapon { * Checks the used state of the item. * * @return - * true if the item has deteriorated. + * {@code true} if the item has been used and is deteriorated. */ public boolean isUsed() { return getUses() > 0; } /** - * Increment number of times used. + * Increments number of times item has been used. */ @Override public void deteriorate() { put("uses", getUses() + 1); } + /** + * Increments number of times item has been used and notifies user if about to break. + * + * @param user + * Entity using item when deterioration takes place. + */ @Override public void deteriorate(final RPEntity user) { deteriorate(); @@ -83,6 +114,12 @@ public class BreakableWeapon extends Weapon { } } + /** + * Notifies user if item is about to break. + * + * @param user + * Entity using item. + */ private void onWeakened(final RPEntity user) { if (!notified) { user.sendPrivateText("Your " + getName() + " is about to break."); @@ -90,6 +127,12 @@ public class BreakableWeapon extends Weapon { } } + /** + * Retrieves condition description based on item's current state. + * + * @return + * Condition description. + */ public String getConditionName() { final Double condition = getCondition(); for (final String conditionName: conditions.keySet()) { @@ -101,6 +144,12 @@ public class BreakableWeapon extends Weapon { return "about to break"; } + /** + * Retrieves numeric value representing item's current condition state. + * + * @return + * Condition state. + */ private double getCondition() { return 1 - (getUses() / (double) getDurability()); } @@ -109,7 +158,7 @@ public class BreakableWeapon extends Weapon { * Checks if the item has no uses remaining. * * @return - * true if uses are as much or more than base_uses. + * {@code true} if item's condition state is 0 or less. */ public boolean isBroken() { final double condition = getCondition(); @@ -133,10 +182,22 @@ public class BreakableWeapon extends Weapon { return Rand.randUniform(1, 100) <= chanceOfBreak; } + /** + * Retrieves item's durability. + * + * @return + * Number of uses before chance of break. + */ public int getDurability() { return getInt("durability"); } + /** + * Retrieves number of times item has been used. + * + * @return + * Number of times used. + */ public int getUses() { return getInt("uses"); } diff --git a/src/games/stendhal/server/entity/item/Item.java b/src/games/stendhal/server/entity/item/Item.java index f262176b37..293cfad372 100644 --- a/src/games/stendhal/server/entity/item/Item.java +++ b/src/games/stendhal/server/entity/item/Item.java @@ -17,6 +17,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.function.Predicate; import org.apache.log4j.Logger; @@ -170,6 +171,9 @@ public class Item extends PassiveEntity implements TurnListener, EquipListener, // Some items have ranged attack values entity.addAttribute("ratk", Type.SHORT, Definition.HIDDEN); + // alters weapon accuracy by a percentage + entity.addAttribute("accuracy_bonus", Type.FLOAT, Definition.HIDDEN); + // Some items indicate how often you can attack. entity.addAttribute("rate", Type.SHORT, Definition.HIDDEN); @@ -248,8 +252,9 @@ public class Item extends PassiveEntity implements TurnListener, EquipListener, // True for items that should be bound automatically at loot (or login) entity.addAttribute("autobind", Type.FLAG, (byte) (Definition.HIDDEN | Definition.VOLATILE)); - // Number of uses for BreakableWeapon + // number of times item can be used before chance of break ({@code BreakableWeapon}) entity.addAttribute("durability", Type.INT, Definition.VOLATILE); + // current number of times item has been used entity.addAttribute("uses", Type.INT); // player that owns the item @@ -874,6 +879,10 @@ public class Item extends PassiveEntity implements TurnListener, EquipListener, stats.append("%"); } } + if (has("accuracy_bonus")) { + stats.append(" ACCURACY-BONUS: "); + stats.append((getDouble("accuracy_bonus") + "%").replaceFirst("\\.[0]+%$", "%")); + } if (has("min_level")) { stats.append(" MIN-LEVEL: "); @@ -1036,5 +1045,43 @@ public class Item extends PassiveEntity implements TurnListener, EquipListener, return false; } + /** + * Determines if an item can be submitted as quest item. + * + * Currently only makes special case for experimental sandwich rewarded from Meal for Groongo + * quest. + * + * @return + * {@code false} if the item should not be submitted in quests. + */ + public boolean isSubmittable() { + if ("sandwich".equals(getName()) && getDescription().contains("experimental")) { + return false; + } + return true; + } + /** + * A convenience method for getting a method for matching item names. + * + * @param name + * Item name to match. + * @return + * A predicate for matching the name. + */ + public static Predicate nameMatches(String name) { + return item -> name.equalsIgnoreCase(item.getName()); + } + + /** + * A convenience method for getting a method for matching item names and submittable status. + * + * @param name + * Item name to match. + * @return + * A predicate for matching the name. + */ + public static Predicate nameMatchesSubmittable(String name) { + return item -> name.equalsIgnoreCase(item.getName()) && item.isSubmittable(); + } } diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/CloverSpawner.java b/src/games/stendhal/server/entity/mapstuff/spawner/CloverSpawner.java new file mode 100644 index 0000000000..0fcd8c957d --- /dev/null +++ b/src/games/stendhal/server/entity/mapstuff/spawner/CloverSpawner.java @@ -0,0 +1,176 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.entity.mapstuff.spawner; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; + +import games.stendhal.common.Rand; +import games.stendhal.server.core.engine.SingletonRepository; +import games.stendhal.server.core.engine.StendhalRPWorld; +import games.stendhal.server.core.engine.StendhalRPZone; +import games.stendhal.server.core.events.TurnNotifier; +import games.stendhal.server.entity.item.Item; +import games.stendhal.server.util.TimeUtil; + + +/** + * Manages four-leaf clover spawning. + * + * TODO: + * - only spawn on grass and dirt tiles + */ +public class CloverSpawner extends PassiveEntityRespawnPoint { + + private static Logger logger = Logger.getLogger(CloverSpawner.class); + + private boolean initialized = false; + + private Item grownClover; + + private static CloverSpawner instance; + + + /** + * Retrieves singleton instance. + */ + public static CloverSpawner get() { + if (instance == null) { + instance = new CloverSpawner(); + } + return instance; + } + + /** + * Hidden singleton constructor. + */ + private CloverSpawner() { + super("four-leaf clover", 0); + grownClover = null; + hasPickableFruit = false; + } + + /** + * Adds clover to world and starts spawn cycle. + */ + public void init() { + if (initialized) { + // don't allow more than one clover spawner in world + logger.warn("Tried to re-initialize clover spawner"); + return; + } + initialized = true; + + // spawn clover at server start + spawn(); + // set next spawn at midnight + TurnNotifier.get().notifyInSeconds(TimeUtil.secondsToMidnight(), CloverSpawner.get()); + } + + /** + * Adds clover to world. + */ + private boolean spawn() { + // remove clover & grower from current zone + StendhalRPZone currentZone = getZone(); + if (currentZone != null) { + if (grownClover != null) { + StendhalRPZone cloverZone = grownClover.getZone(); + if (cloverZone == null) { + logger.warn("Clover's zone not available"); + } else if (!currentZone.equals(cloverZone)) { + logger.warn("Clover (" + cloverZone.getName() + ") not located in same zone as spawner (" + + currentZone.getName() + ")"); + cloverZone.remove(grownClover); + } else { + currentZone.remove(grownClover); + } + } + currentZone.remove(this); + } + grownClover = null; + + StendhalRPWorld world = SingletonRepository.getRPWorld(); + // acceptable zones + List zones = new ArrayList<>(); + for (String region: world.getRegions()) { + for (StendhalRPZone zone: world.getAllZonesFromRegion(region, true, true, true)) { + if (zone.getLevel() == 0) { + zones.add(zone); + } + } + } + + StendhalRPZone newZone = Rand.rand(zones); + Point pos = newZone.getRandomSpawnPosition(this, false); + if (pos == null) { + logger.warn("Unable to find suitable spawn position in zone " + newZone.getName() + + ", retrying in 15 minutes"); + hasPickableFruit = false; + return false; + } + setPosition(pos.x, pos.y); + newZone.add(this); + // set state to allow player to pick up + growNewFruit(); + logger.info("Spawned four-leaf clover at " + newZone.getName() + " " + getX() + "," + getY()); + return true; + } + + @Override + public void onTurnReached(int currentTurn) { + // spawn a new clover + if (spawn()) { + // set re-spawn for next day at midnight + TurnNotifier.get().notifyInSeconds(TimeUtil.secondsToMidnight(), this); + } else { + // failed, retry in 15 minutes + TurnNotifier.get().notifyInSeconds(TimeUtil.SECONDS_IN_MINUTE * 15, this); + } + } + + @Override + public void onFruitPicked(final Item picked) { + hasPickableFruit = false; + grownClover = null; + if (picked != null) { + picked.setPlantGrower(null); + } + } + + @Override + protected Item growNewFruit() { + Item grownItem = super.growNewFruit(); + if (grownItem != null) { + grownClover = grownItem; + } + return grownClover; + } + + @Override + public void setToFullGrowth() { + growNewFruit(); + } + + /** + * Checks if a clover is available to be picked. + * + * @return + * {@code true} if clover was spawned and not yet picked. + */ + public boolean hasPickableClover() { + return grownClover != null; + } +} diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/FlowerGrower.java b/src/games/stendhal/server/entity/mapstuff/spawner/FlowerGrower.java index 6089d75678..2cedd912d6 100644 --- a/src/games/stendhal/server/entity/mapstuff/spawner/FlowerGrower.java +++ b/src/games/stendhal/server/entity/mapstuff/spawner/FlowerGrower.java @@ -144,13 +144,14 @@ public class FlowerGrower extends VegetableGrower { } @Override - protected void growNewFruit() { + protected Item growNewFruit() { if (isOnFreeFertileGround()) { - super.growNewFruit(); + return super.growNewFruit(); } else { if (getZone() != null) { getZone().remove(this); } } + return null; } } diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/GrowingPassiveEntityRespawnPoint.java b/src/games/stendhal/server/entity/mapstuff/spawner/GrowingPassiveEntityRespawnPoint.java index a216c64211..3d24d28360 100644 --- a/src/games/stendhal/server/entity/mapstuff/spawner/GrowingPassiveEntityRespawnPoint.java +++ b/src/games/stendhal/server/entity/mapstuff/spawner/GrowingPassiveEntityRespawnPoint.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003 - Marauroa * + * (C) Copyright 2003-2024 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -163,7 +163,7 @@ public abstract class GrowingPassiveEntityRespawnPoint extends } @Override - protected void growNewFruit() { + protected Item growNewFruit() { setRipeness(ripeness + 1); logger.debug("Grow " + ripeness + " up to " + maxRipeness); notifyWorldAboutChanges(); @@ -171,6 +171,7 @@ public abstract class GrowingPassiveEntityRespawnPoint extends if (ripeness < maxRipeness) { SingletonRepository.getTurnNotifier().notifyInTurns(getRandomTurnsForRegrow(), this); } + return null; } @Override diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/PassiveEntityRespawnPoint.java b/src/games/stendhal/server/entity/mapstuff/spawner/PassiveEntityRespawnPoint.java index c87ba3d3a7..597351c234 100644 --- a/src/games/stendhal/server/entity/mapstuff/spawner/PassiveEntityRespawnPoint.java +++ b/src/games/stendhal/server/entity/mapstuff/spawner/PassiveEntityRespawnPoint.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Marauroa * + * (C) Copyright 2003-2024 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -49,7 +49,7 @@ public class PassiveEntityRespawnPoint extends Entity implements TurnListener { /** * Is there still a fruit that has not yet been picked up? */ - private boolean hasPickableFruit; + protected boolean hasPickableFruit; /** * The name of the fruit (Item) that is grown by the PlantGrower. @@ -164,7 +164,8 @@ public class PassiveEntityRespawnPoint extends Entity implements TurnListener { /** * Creates a new fruit. */ - protected void growNewFruit() { + protected Item growNewFruit() { + Item grownItem = null; if (!hasPickableFruit) { LOGGER.debug("Growing " + growingItemName); @@ -172,8 +173,7 @@ public class PassiveEntityRespawnPoint extends Entity implements TurnListener { final StendhalRPZone zone = world.getZone(getID().getZoneID()); // create a new grown item - final Item grownItem = SingletonRepository.getEntityManager().getItem( - growingItemName); + grownItem = SingletonRepository.getEntityManager().getItem(growingItemName); grownItem.setPlantGrower(this); grownItem.setPosition(getX(), getY()); grownItem.setFromCorpse(true); @@ -183,6 +183,7 @@ public class PassiveEntityRespawnPoint extends Entity implements TurnListener { zone.add(grownItem, false); hasPickableFruit = true; } + return grownItem; } public void setToFullGrowth() { diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/SheepFood.java b/src/games/stendhal/server/entity/mapstuff/spawner/SheepFood.java index 045698323b..4e387887d7 100644 --- a/src/games/stendhal/server/entity/mapstuff/spawner/SheepFood.java +++ b/src/games/stendhal/server/entity/mapstuff/spawner/SheepFood.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2020 - Marauroa * + * (C) Copyright 2003-2024 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -80,9 +80,10 @@ public class SheepFood extends PassiveEntityRespawnPoint { } @Override - protected void growNewFruit() { + protected Item growNewFruit() { setAmount(amount + 1); notifyWorldAboutChanges(); + return null; } @Override diff --git a/src/games/stendhal/server/entity/npc/ConversationPhrases.java b/src/games/stendhal/server/entity/npc/ConversationPhrases.java index 34ecae5e7a..04765bb972 100644 --- a/src/games/stendhal/server/entity/npc/ConversationPhrases.java +++ b/src/games/stendhal/server/entity/npc/ConversationPhrases.java @@ -66,9 +66,11 @@ public class ConversationPhrases { public static final List PURCHASE_MESSAGES = Arrays.asList("buy", "purchase"); + public static final ImmutableSet REPAIR_MESSAGES = ImmutableSet.of("repair", "fix"); + public static final ImmutableSet KNOWN = ImmutableSet.of("hello", "help", "job", - "task", "complete", "done", "another", "offer", "yes", "no", "bye", "buy", "sell", "heal", - "challenge"); + "task", "complete", "done", "another", "offer", "yes", "no", "bye", "buy", "sell", "repair", + "heal", "challenge"); /** * Combine a string collection (list) with additional strings. diff --git a/src/games/stendhal/server/entity/npc/NPC.java b/src/games/stendhal/server/entity/npc/NPC.java index abc101bd3d..8b0a755d1e 100644 --- a/src/games/stendhal/server/entity/npc/NPC.java +++ b/src/games/stendhal/server/entity/npc/NPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Marauroa * + * (C) Copyright 2003-2025 - Marauroa * *************************************************************************** *************************************************************************** * * @@ -12,6 +12,8 @@ ***************************************************************************/ package games.stendhal.server.entity.npc; +import static games.stendhal.common.Constants.DEFAULT_SOUND_RADIUS; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,6 +30,7 @@ import games.stendhal.server.core.pathfinder.Path; import games.stendhal.server.entity.DressedEntity; import games.stendhal.server.entity.Entity; import games.stendhal.server.entity.item.Corpse; +import games.stendhal.server.entity.npc.behaviour.impl.idle.IdleBehaviour; import games.stendhal.server.events.SoundEvent; import marauroa.common.game.Definition; import marauroa.common.game.Definition.Type; @@ -42,10 +45,6 @@ public abstract class NPC extends DressedEntity { * specified sounds. */ private static final int SOUND_PROBABILITY = 20; - /** - * Creature sound radius. - */ - protected static final int SOUND_RADIUS = 23; /** * Minimum delay in milliseconds between playing creature sounds. */ @@ -83,6 +82,11 @@ public abstract class NPC extends DressedEntity { /** The time stamp of previous sound event. */ private long lastSoundTime; + protected IdleBehaviour idler; + // considered idle by default + protected boolean isIdle = true; + + public static void generateRPClass() { try { final RPClass npc = new RPClass("npc"); @@ -309,17 +313,29 @@ public abstract class NPC extends DressedEntity { } } + public void preLogic() { + if (idler != null) { + // handled in pre-logic to minimize sprite jumping + idler.perform(this); + } + } + @Override public void logic() { - if (atMovementRadius()) { - onOutsideMovementRadius(); - } - if (!hasPath()) { - if (logger.isDebugEnabled()) { - String title = getTitle(); - String zone = getZone().getName(); - String coords = Integer.toString(getX()) + ", " + Integer.toString(getY()); - logger.debug("Moving entity " + title + " at " + zone + " " + coords + " does not have a path"); + if (idler != null) { + // handled in pre-logic + return; + } else { + if (!hasPath()) { + if (atMovementRadius()) { + onOutsideMovementRadius(); + } + if (logger.isDebugEnabled()) { + String title = getTitle(); + String zone = getZone().getName(); + String coords = Integer.toString(getX()) + ", " + Integer.toString(getY()); + logger.debug("Moving entity " + title + " at " + zone + " " + coords + " does not have a path"); + } } } @@ -330,6 +346,8 @@ public abstract class NPC extends DressedEntity { /** * Give NPC a random path + * + * NOTE: can this be replaced with {@code NPC.setIdleBehaviour(new WanderIdleBehaviour())}? */ public void moveRandomly() { setRandomPathFrom(getX(), getY(), getMovementRange() / 2); @@ -347,6 +365,14 @@ public abstract class NPC extends DressedEntity { pauseTurnsRemaining = pauseTurns; } + @Override + protected void onMoved(final int oldX, final int oldY, final int newX, final int newY) { + super.onMoved(oldX, oldY, newX, newY); + if (idler != null) { + idler.onMoved(this); + } + } + /** * Pause the entity when path is completed. * Call setDirection() first to specify which @@ -384,9 +410,59 @@ public abstract class NPC extends DressedEntity { long time = System.currentTimeMillis(); if (lastSoundTime + SOUND_DEAD_TIME < time) { lastSoundTime = time; - this.addEvent(new SoundEvent(Rand.rand(sounds), SOUND_RADIUS, 100, SoundLayer.CREATURE_NOISE)); + this.addEvent(new SoundEvent(Rand.rand(sounds), DEFAULT_SOUND_RADIUS, 100, + SoundLayer.CREATURE_NOISE)); this.notifyWorldAboutChanges(); } } } + + /** + * Sets the idle movement behavior manager. + * + * Not related to idle conversation state. + * + * @param idler + * Manager for executing behavior. + */ + public void setIdleBehaviour(final IdleBehaviour idler) { + this.idler = idler; + } + + /** + * Faces a new random direction. + */ + public void changeDirection() { + final Direction oldDir = getDirection(); + Direction newDir = Direction.rand(); + short failsafe = 50; + while (newDir.equals(oldDir)) { + // find a direction different than previous + newDir = Direction.rand(); + failsafe--; + if (failsafe < 1) { + break; + } + } + if (newDir.equals(oldDir)) { + logger.warn("Failed to change entity's direction"); + } + setDirection(newDir); + } + + @Override + protected void handleSimpleCollision(final int nx, final int ny) { + if (isIdle && idler != null && idler.handleSimpleCollision(this, nx, ny)) { + return; + } + super.handleSimpleCollision(nx, ny); + } + + @Override + protected void handleObjectCollision() { + if (isIdle && idler != null && idler.handleObjectCollision(this)) { + return; + } + super.handleObjectCollision(); + } } diff --git a/src/games/stendhal/server/entity/npc/PassiveNPC.java b/src/games/stendhal/server/entity/npc/PassiveNPC.java index d5f17ed2b8..d1aed34a07 100644 --- a/src/games/stendhal/server/entity/npc/PassiveNPC.java +++ b/src/games/stendhal/server/entity/npc/PassiveNPC.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2019-2023 - Arianne * + * (C) Copyright 2019-2024 - Arianne * *************************************************************************** *************************************************************************** * * @@ -128,13 +128,11 @@ public abstract class PassiveNPC extends NPC { } } - @Override - protected void onMoved(final int oldX, final int oldY, final int newX, final int newY) { - super.onMoved(oldX, oldY, newX, newY); - } - @Override protected void handleObjectCollision() { + if (isIdle && idler != null && idler.handleObjectCollision(this)) { + return; + } if (pathIsBlocked()) { stop(); return; @@ -155,6 +153,9 @@ public abstract class PassiveNPC extends NPC { @Override protected void handleSimpleCollision(final int nx, final int ny) { + if (isIdle && idler != null && idler.handleSimpleCollision(this, nx, ny)) { + return; + } CollisionAction action = getCollisionAction(); if (!ignoresCollision()) { if (usesRandomPath()) { diff --git a/src/games/stendhal/server/entity/npc/SpeakerNPC.java b/src/games/stendhal/server/entity/npc/SpeakerNPC.java index a2fb21db75..cae97528d0 100644 --- a/src/games/stendhal/server/entity/npc/SpeakerNPC.java +++ b/src/games/stendhal/server/entity/npc/SpeakerNPC.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -171,6 +171,8 @@ public class SpeakerNPC extends PassiveNPC { * a set of blue words used since the start of the conversation */ private Set learnedWordsInCurrentConversation = new HashSet<>(); + /** Chat options triggers to be shown even if not registered with transition. */ + private Set forcedWordsInCurrentConversation = new HashSet<>(); /** * alternative image for website @@ -349,6 +351,7 @@ public class SpeakerNPC extends PassiveNPC { } setIdea(null); learnedWordsInCurrentConversation = new HashSet<>(); + forcedWordsInCurrentConversation = new HashSet<>(); // send chat option event when NPC becomes "idle" if (wasAttending instanceof Player) { engine.addChatOptionsEvent((Player) wasAttending); @@ -421,6 +424,7 @@ public class SpeakerNPC extends PassiveNPC { // respond to player in the chat log before the player says something. } + @Override public void preLogic() { if (this.getZone().getPlayerAndFriends().isEmpty() && !isTalking() && !actingAlone) { @@ -437,10 +441,14 @@ public class SpeakerNPC extends PassiveNPC { if (getAttending() != null) { setAttending(null); } - if (hasPath()) { - setSpeed(getBaseSpeed()); + if (idler != null) { + idler.perform(this); + } else { + if (hasPath()) { + setSpeed(getBaseSpeed()); + } + applyMovement(); } - applyMovement(); } else if (attending != null) { // If the player is too far away if ((attending.squaredDistance(this) > squaredGoodByeRange) @@ -504,14 +512,65 @@ public class SpeakerNPC extends PassiveNPC { public void say(final String text) { // turn towards player if necessary, then say it. say(text, true); + } + + protected void say(final String text, final boolean turnToPlayer) { + // be polite and face the player we are talking to + if (turnToPlayer && (attending != null)) { + faceToward(attending); + } + super.say(text); learnWordsInCurrentConversation(text); } + /** + * Adds a trigger that should be shown as chat option even if not registered in a transition. + * + * @param triggers + * Trigger word(s). + */ + public void forceWordsInCurrentConversation(String... triggers) { + for (String trigger: triggers) { + trigger = trigger.toLowerCase(Locale.ENGLISH); + if (!forcedWordsInCurrentConversation.contains(trigger)) { + forcedWordsInCurrentConversation.add(trigger); + } + } + } + + /** + * Retrieves triggers that should be shown as chat options even if not registered in a transition. + * + * @return + * Trigger words. + */ + public Set getForcedWordsInCurrentConversation() { + return forcedWordsInCurrentConversation; + } + + /** + * Adds a word to list of learned words. + * + * @param trigger + * Word to be added. + */ + private void addLearnedWordInCurrentConversation(String trigger) { + trigger = trigger.toLowerCase(Locale.ENGLISH); + if (!hasLearnedWordInCurrentConversation(trigger)) { + learnedWordsInCurrentConversation.add(trigger); + } + } + public boolean hasLearnedWordInCurrentConversation(String trigger) { return learnedWordsInCurrentConversation.contains(trigger); } private void learnWordsInCurrentConversation(String text) { + if (!(getAttending() instanceof Player)) { + // don't remember words when not interacting with a player + return; + } + int pos = 0; pos = text.indexOf('#', pos); loop: @@ -522,7 +581,7 @@ public class SpeakerNPC extends PassiveNPC { continue; } else if (next == '\'') { int end = text.indexOf('\'', pos + 2); - learnedWordsInCurrentConversation.add(text.substring(pos + 2, end).toLowerCase(Locale.ENGLISH)); + addLearnedWordInCurrentConversation(text.substring(pos + 2, end)); pos = text.indexOf('#', end); continue; } else { @@ -530,26 +589,17 @@ public class SpeakerNPC extends PassiveNPC { for (i = pos + 1; i < text.length(); i++) { char endChar = text.charAt(i); if (!Character.isJavaIdentifierPart(endChar)) { - learnedWordsInCurrentConversation.add(text.substring(pos + 1, i).toLowerCase(Locale.ENGLISH)); + addLearnedWordInCurrentConversation(text.substring(pos + 1, i)); pos = text.indexOf('#', i); continue loop; } } - learnedWordsInCurrentConversation.add(text.substring(pos + 1).toLowerCase(Locale.ENGLISH)); + addLearnedWordInCurrentConversation(text.substring(pos + 1)); break; } } } - protected void say(final String text, final boolean turnToPlayer) { - // be polite and face the player we are talking to - if (turnToPlayer && (attending != null)) { - faceToward(attending); - } - - super.say(text); - } - /** Message when NPC is attending another player. * @param text to say to bothering player * @param action to perform @@ -767,7 +817,56 @@ public class SpeakerNPC extends PassiveNPC { return(engine.remove(label)); } + /** + * Removes matching transitions. + * + * @param state + * @param trigger + * @param condition + * @return + * {@code true} if at least one transition was removed. + */ + public boolean del(final ConversationStates state, final Expression trigger, + final ChatCondition condition) { + return engine.remove(state, trigger, condition); + } + /** + * Wrapper method. + * + * Lua struggles with overloaded methods, so create a wrapper with a different name. + * + * @param state + * @param trigger + * @param condition + * @return + * {@code true} if at least one transition was removed. + */ + public boolean removeTransition(final ConversationStates state, final Object trigger, + final ChatCondition condition) { + Expression expr; + if (trigger instanceof Expression) { + expr = (Expression) trigger; + } else if (trigger instanceof String) { + expr = new Expression(String.valueOf(trigger), ExpressionType.UNKNOWN); + } else if (trigger instanceof List) { + final List l = (List) trigger; + boolean res = false; + for (final Object obj: l) { + if (obj instanceof String) { + if (del(state, new Expression(String.valueOf(obj), ExpressionType.UNKNOWN), condition)) { + res = true; + } + } + } + return res; + } else { + logger.warn("Unknown trigger:" + trigger); + return false; + } + + return del(state, expr, condition); + } public void listenTo(final Player player, final String text) { tell(player, text); @@ -1171,7 +1270,8 @@ public class SpeakerNPC extends PassiveNPC { * Keyword(s) to be added. */ public void addKnownChatOptions(final String... keywords) { - for (final String keyword: keywords) { + for (String keyword: keywords) { + keyword = keyword.toLowerCase(Locale.ENGLISH); if (!knownChatOptions.contains(keyword)) { knownChatOptions.add(keyword); } diff --git a/src/games/stendhal/server/entity/npc/action/DropItemAction.java b/src/games/stendhal/server/entity/npc/action/DropItemAction.java index 947f098851..15ff716a17 100644 --- a/src/games/stendhal/server/entity/npc/action/DropItemAction.java +++ b/src/games/stendhal/server/entity/npc/action/DropItemAction.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -61,7 +61,7 @@ public class DropItemAction implements ChatAction { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { - final boolean res = player.drop(itemName, amount); + final boolean res = player.dropSubmittable(itemName, amount); if (!res) { logger.error("Cannot drop " + amount + " " + itemName, new Throwable()); diff --git a/src/games/stendhal/server/entity/npc/action/DropRecordedItemAction.java b/src/games/stendhal/server/entity/npc/action/DropRecordedItemAction.java index 515b0920d6..3ed96c7663 100755 --- a/src/games/stendhal/server/entity/npc/action/DropRecordedItemAction.java +++ b/src/games/stendhal/server/entity/npc/action/DropRecordedItemAction.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -81,7 +81,7 @@ public class DropRecordedItemAction implements ChatAction { if(elements.length > 1) { amount=MathHelper.parseIntDefault(elements[1], 1); } - final boolean res = player.drop(itemname, amount); + final boolean res = player.dropSubmittable(itemname, amount); if (!res) { logger.error("Cannot drop " + amount + " " + itemname, new Throwable()); } diff --git a/src/games/stendhal/server/entity/npc/behaviour/impl/SellerBehaviour.java b/src/games/stendhal/server/entity/npc/behaviour/impl/SellerBehaviour.java index e41596a9ec..613c5bd71a 100644 --- a/src/games/stendhal/server/entity/npc/behaviour/impl/SellerBehaviour.java +++ b/src/games/stendhal/server/entity/npc/behaviour/impl/SellerBehaviour.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2011 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -126,7 +126,7 @@ public class SellerBehaviour extends MerchantBehaviour { public Item getAskedItem(final String askedItem, final Player player) { final Item item = SingletonRepository.getEntityManager().getItem(askedItem); - if (item != null && item.has("autobind")) { + if (item != null && item.has("autobind") && player != null) { // respect autobind attrubute item.setBoundTo(player.getName()); } diff --git a/src/games/stendhal/server/entity/npc/behaviour/impl/idle/AbstractIdleBehaviour.java b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/AbstractIdleBehaviour.java new file mode 100644 index 0000000000..101a3e7e48 --- /dev/null +++ b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/AbstractIdleBehaviour.java @@ -0,0 +1,41 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.entity.npc.behaviour.impl.idle; + +import games.stendhal.server.entity.npc.NPC; + + +public abstract class AbstractIdleBehaviour implements IdleBehaviour { + + @Override + public abstract void perform(NPC npc); + + @Override + public void onMoved(NPC npc) { + // does nothing in this implementation + } + + @Override + public void reset() { + // does nothing in this implementation + } + + @Override + public boolean handleSimpleCollision(NPC npc, int nx, int ny) { + return false; + } + + @Override + public boolean handleObjectCollision(NPC npc) { + return false; + } +} diff --git a/src/games/stendhal/server/entity/npc/behaviour/impl/idle/IdleBehaviour.java b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/IdleBehaviour.java new file mode 100644 index 0000000000..bc7b61c9f7 --- /dev/null +++ b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/IdleBehaviour.java @@ -0,0 +1,58 @@ +/* $Id$ */ +/*************************************************************************** + * (C) Copyright 2003-2024 - Stendhal * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.entity.npc.behaviour.impl.idle; + +import games.stendhal.server.entity.npc.NPC; + + +public interface IdleBehaviour { + + void perform(NPC npc); + + /** + * Called when entity's position has changed. + * + * @param npc + * Moving entity. + */ + void onMoved(NPC npc); + + /** + * There may be need to reset certain attributes. + */ + void reset(); + + /** + * Can be called to execute tile collision behavior. + * + * @param npc + * Entity being acted on. + * @param nx + * Horizontal position where entity collides. + * @param ny + * Vertical position where entity collides. + * @return + * {@code true} if collision handling should not propagate. + */ + boolean handleSimpleCollision(NPC npc, int nx, int ny); + + /** + * Can be called to execute entity collision behavior. + * + * @param npc + * Entity being acted on. + * @return + * {@code true} if collision handling should not propagate. + */ + boolean handleObjectCollision(NPC npc); +} diff --git a/src/games/stendhal/server/entity/npc/behaviour/impl/idle/WanderIdleBehaviour.java b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/WanderIdleBehaviour.java new file mode 100644 index 0000000000..159fc8fc17 --- /dev/null +++ b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/WanderIdleBehaviour.java @@ -0,0 +1,308 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.entity.npc.behaviour.impl.idle; + +import java.awt.Point; +import java.util.List; + +import org.apache.log4j.Logger; + +import games.stendhal.common.Direction; +import games.stendhal.common.Rand; +import games.stendhal.server.core.pathfinder.FixedPath; +import games.stendhal.server.core.pathfinder.Node; +import games.stendhal.server.core.pathfinder.Path; +import games.stendhal.server.entity.creature.impl.idle.StandOnIdle; +import games.stendhal.server.entity.npc.NPC; + + +/** + * Behavior for entity that should wander randomly while idle. + * + * Note: there is still some sprite jumping when entity stops, but that may be a client-side issue + * + * FIXME: need to handle reaching zone borders + * + * TODO: + * - add option to disable stopping (for butterflies) + */ +public class WanderIdleBehaviour extends StandOnIdle { + + private static Logger logger = Logger.getLogger(WanderIdleBehaviour.class); + + /** Minimum amount of time in milliseconds entity must remain stopped. */ + private static final int STOP_DELAY_MIN = 5000; + /** Minimum amount of time in milliseconds entity must remain moving. */ + private static final int MOVE_DELAY_MIN = 15000; + + /** Timestamp after which next change may be executed. */ + private long nextChange; + /** Property to ensure good direction to change to. */ + private Direction lastKnownDir; + /** Denotes a direction change should occur after entity position changes. */ + private boolean directionChangeQueued; + /** Denotes stop should occur after entity position changes. */ + private boolean stopQueued; + /** Current speed entity should be moving. */ + private double speed; + + /** Max distance from origin point entity can wander. Should not be used for creatures. */ + private int radiusLimit; + private int sqRadiusLimit; + /** Denotes entity is on path to return to original position. */ + private boolean returningToOrigin; + + + public WanderIdleBehaviour() { + super(); + final long initTime = System.currentTimeMillis(); + // entity is assumed to be in a stopped state at initialization + nextChange = initTime + STOP_DELAY_MIN; + lastKnownDir = Direction.DOWN; + directionChangeQueued = false; + stopQueued = false; + speed = 0; + radiusLimit = 0; + sqRadiusLimit = 0; + returningToOrigin = false; + } + + public WanderIdleBehaviour(int radiusLimit) { + this(); + this.radiusLimit = radiusLimit; + sqRadiusLimit = radiusLimit * radiusLimit; + } + + @Override + public void reset() { + // remove queues + directionChangeQueued = false; + stopQueued = false; + // FIXME: non-creatures may not want to reset this (currently only used by Creature class) + returningToOrigin = false; + } + + @Override + public void perform(final NPC npc) { + // FIXME: this check should be done in entity logic + if (!npc.getZone().getPlayerAndFriends().isEmpty()) { + final boolean hasPath = npc.hasPath(); + final boolean stopped = stopped(); + + if (hasPath) { + if (returningToOrigin && stopped) { + logger.warn("Entity " + npc.getName() + " returning to origin impeded"); + speed = npc.getBaseSpeed(); + npc.setSpeed(speed); + } + npc.followPath(); + npc.applyMovement(); + } else { + returningToOrigin = isOutsideRadiusLimit(npc); + if (!retreatUnderFire(npc)) { + if (returningToOrigin) { + returnToOrigin(npc); + } else { + if (!stopped) { + // speed must be updated every iteration because of call to `retreatUnderFire` + npc.setSpeed(speed); + npc.applyMovement(); + } + performStopMove(npc); + } + } + } + } + } + + /** + * Randomly stops or moves entity. + * + * @param npc + * Entity being acted on. + */ + private void performStopMove(final NPC npc) { + final long performTime = System.currentTimeMillis(); + final int diff = (int) (nextChange - performTime); + if (diff >= 0) { + if (!stopped()) { + maybeChangeDirection(); + } + return; + } + + // randomly choose when to execute next change after wait period expired + if (Rand.rand(40) == 0) { + if (stopped()) { + startMove(npc, performTime); + } else { + stop(npc, performTime); + } + } + } + + /** + * Stops entity movement and sets timeout. + * + * @param npc + * Entity being acted on. + * @param performTime + * Time at which action is being performed. + */ + private void stop(final NPC npc, final long performTime) { + // next update minimum delay + nextChange = performTime + STOP_DELAY_MIN; + final Direction dir = npc.getDirection(); + if (!Direction.STOP.equals(dir)) { + lastKnownDir = dir; + } + // don't stop until after position change to prevent sprite jumping + stopQueued = true; + } + + /** + * Starts entity movement and sets timeout. + * + * @param npc + * Entity being acted on. + * @param performTime + * Time at which action is being performed. + */ + private void startMove(final NPC npc, final long performTime) { + // next update minimum delay + nextChange = performTime + MOVE_DELAY_MIN; + if (Direction.STOP.equals(npc.getDirection())) { + npc.setDirection(lastKnownDir); + } + lastKnownDir = npc.getDirection(); + + // 1 in 4 chance of direction change with move + if (Rand.rand(4) == 0) { + changeDirection(npc); + } + speed = npc.getBaseSpeed(); + } + + /** + * Checks if entity is considered to be in a still state. + * + * Note: cannot rely on {@code ActiveEntity.stopped()} + * + * @return + * {@code true} if entity's current direction represents "stopped". + */ + private boolean stopped() { + return speed == 0; + } + + + /** + * Changes entity's direction and updates last known direction. + * + * @param npc + * Entity that is changing direction. + */ + private void changeDirection(final NPC npc) { + npc.changeDirection(); + final Direction dir = npc.getDirection(); + if (!Direction.STOP.equals(dir)) { + lastKnownDir = dir; + } + directionChangeQueued = false; + } + + /** + * Conditionally changes entity direction dependent on random roll. + */ + private void maybeChangeDirection() { + // 1 in 20 chance to change direction + if (Rand.rand(20) == 0) { + // don't change direction until after position change to prevent sprite jumping + directionChangeQueued = true; + } + } + + @Override + public void onMoved(NPC npc) { + if (stopQueued) { + hardStop(npc); + } + if (directionChangeQueued) { + changeDirection(npc); + } + } + + /** + * Forces entity to stop movement. + * + * @param npc + * Entity to stop. + */ + private void hardStop(NPC npc) { + npc.stop(); + npc.applyMovement(); + npc.setPath(null); + speed = 0; + stopQueued = false; + } + + @Override + public boolean handleSimpleCollision(final NPC npc, final int nx, final int ny) { + return handleObjectCollision(npc); + } + + @Override + public boolean handleObjectCollision(final NPC npc) { + if (returningToOrigin) { + returningToOrigin = false; + hardStop(npc); + // flip coin to determine change direction immediately or wait for next call to `perform` + if (Rand.flipCoin()) { + changeDirection(npc); + startMove(npc, System.currentTimeMillis()); + } + } else { + changeDirection(npc); + } + return true; + } + + /** + * Checks if the entity has moved beyond a set distance limit from its origin. + * + * @param npc + * Entity to be checked. + * @return + * {@code true} if limit has been set and entity's distance from origin is greater than limit. + */ + private boolean isOutsideRadiusLimit(NPC npc) { + if (radiusLimit < 1) { + return false; + } + return npc.getDistanceFromOrigin() > sqRadiusLimit; + } + + /** + * Returns entity to point of origin when reaching movement radius boundary. + * + * @param npc + * Entity to be returned to origin. + */ + private void returnToOrigin(NPC npc) { + reset(); + // was set to "false" in `reset` + returningToOrigin = true; + final Point origin = npc.getOrigin(); + final List path = Path.searchPath(npc, origin.x, origin.y); + npc.setPath(new FixedPath(path, false)); + } +} diff --git a/src/games/stendhal/server/entity/npc/behaviour/impl/idle/package-info.java b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/package-info.java new file mode 100644 index 0000000000..05807f7b61 --- /dev/null +++ b/src/games/stendhal/server/entity/npc/behaviour/impl/idle/package-info.java @@ -0,0 +1 @@ +package games.stendhal.server.entity.npc.behaviour.impl.idle; diff --git a/src/games/stendhal/server/entity/npc/condition/PlayerHasItemWithHimCondition.java b/src/games/stendhal/server/entity/npc/condition/PlayerHasItemWithHimCondition.java index b0275357a7..220a9359cc 100644 --- a/src/games/stendhal/server/entity/npc/condition/PlayerHasItemWithHimCondition.java +++ b/src/games/stendhal/server/entity/npc/condition/PlayerHasItemWithHimCondition.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -60,7 +60,7 @@ public class PlayerHasItemWithHimCondition implements ChatCondition { @Override public boolean fire(final Player player, final Sentence sentence, final Entity entity) { - return player.isEquipped(itemName, amount); + return player.isSubmittableEquipped(itemName, amount); } @Override diff --git a/src/games/stendhal/server/entity/npc/condition/PlayerHasRecordedItemWithHimCondition.java b/src/games/stendhal/server/entity/npc/condition/PlayerHasRecordedItemWithHimCondition.java index 0d4de4d66b..c8e7b0b8cd 100644 --- a/src/games/stendhal/server/entity/npc/condition/PlayerHasRecordedItemWithHimCondition.java +++ b/src/games/stendhal/server/entity/npc/condition/PlayerHasRecordedItemWithHimCondition.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -62,7 +62,7 @@ public class PlayerHasRecordedItemWithHimCondition implements ChatCondition { public boolean fire(final Player player, final Sentence sentence, final Entity entity) { String itemName=player.getRequiredItemName(questName,index); int amount = player.getRequiredItemQuantity(questName,index); - return player.isEquipped(itemName, amount); + return player.isSubmittableEquipped(itemName, amount); } @Override diff --git a/src/games/stendhal/server/entity/npc/fsm/Engine.java b/src/games/stendhal/server/entity/npc/fsm/Engine.java index 266ea9d92d..0bc80d966a 100644 --- a/src/games/stendhal/server/entity/npc/fsm/Engine.java +++ b/src/games/stendhal/server/entity/npc/fsm/Engine.java @@ -1,6 +1,6 @@ // $Id$ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -341,6 +341,25 @@ public class Engine { return res; } + /** + * Removes matching transitions. + * + * @param state + * @param trigger + * @param condition + * @return + * {@code true} if at least one transition was removed. + */ + public boolean remove(final ConversationStates state, final Expression trigger, + final ChatCondition condition) { + Transition transition = get(state, trigger, condition); + while (transition != null) { + stateTransitionTable.remove(transition); + transition = get(state, trigger, condition); + } + return stateTransitionTable.indexOf(transition) < 0; + } + /** * Create a collection of trigger expressions from trigger strings * while checking for duplicate transitions. diff --git a/src/games/stendhal/server/entity/npc/quest/BuiltQuest.java b/src/games/stendhal/server/entity/npc/quest/BuiltQuest.java index 2eef1a713c..221d6770b9 100644 --- a/src/games/stendhal/server/entity/npc/quest/BuiltQuest.java +++ b/src/games/stendhal/server/entity/npc/quest/BuiltQuest.java @@ -72,32 +72,47 @@ public class BuiltQuest extends AbstractQuest { if (isRepeatable(player)){ res.add(history.getWhenQuestCanBeRepeated()); } - String completionsShown = history.getWhenCompletionsShown(); + + // completions + int count = MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0); + final String completionsShown = formatCompletionsString(history.getWhenCompletionsShown(), + count); if (completionsShown != null) { - final int count = MathHelper.parseIntDefault(player.getQuest(questSlot, 2), 0); - if (count > 0) { - completionsShown = completionsShown.replace("[count]", String.valueOf(count)); - final int idx1 = completionsShown.indexOf("["); - final int idx2 = completionsShown.indexOf("]"); - if (idx1 > -1 && idx2 > idx1+1) { - final String ctype = completionsShown.substring(idx1+1, idx2); - completionsShown = completionsShown.replace("[" + ctype + "]", Grammar.plnoun(count, ctype)); - } - res.add(completionsShown); - } + res.add(completionsShown); } + // early completions + count = MathHelper.parseIntDefault(player.getQuest(questSlot, 3), 0); + final String earlyCompletionsShown = formatCompletionsString( + history.getWhenEarlyCompletionsShown(), count); + if (earlyCompletionsShown != null) { + res.add(earlyCompletionsShown); + } + return res; } + private String formatCompletionsString(String st, final int count) { + if (st == null || count < 1) { + return null; + } + st = st.replace("[count]", String.valueOf(count)); + final int idx1 = st.indexOf("["); + final int idx2 = st.indexOf("]"); + if (idx1 > -1 && idx2 > idx1+1) { + final String ctype = st.substring(idx1+1, idx2); + st = st.replace("[" + ctype + "]", Grammar.plnoun(count, ctype)); + } + return st; + } + @Override public void addToWorld() { fillQuestInfo( questBuilder.info().getName(), questBuilder.info().getDescription(), questBuilder.info().getRepeatableAfterMinutes() > 0, - // FIXME: use static 2 if quest manuscript always stores completions count at index 2 - // instead of manually setting for each individual quest - questBuilder.getCompletionsIndexes()); + // all quest manuscript instances should be using index 2 for completions count + 2); ChatCondition questCompletedCondition = questBuilder.task().buildQuestCompletedCondition(questSlot); ChatAction questCompleteAction = questBuilder.task().buildQuestCompleteAction(questSlot); diff --git a/src/games/stendhal/server/entity/npc/quest/DeliverItemQuestOfferBuilder.java b/src/games/stendhal/server/entity/npc/quest/DeliverItemQuestOfferBuilder.java index e606fbe8fc..bc5a45c6bd 100644 --- a/src/games/stendhal/server/entity/npc/quest/DeliverItemQuestOfferBuilder.java +++ b/src/games/stendhal/server/entity/npc/quest/DeliverItemQuestOfferBuilder.java @@ -1,9 +1,23 @@ +/*************************************************************************** + * (C) Copyright 2023-2024 - Faiumoni e.V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ package games.stendhal.server.entity.npc.quest; import java.util.LinkedList; import java.util.List; import java.util.Map; +import games.stendhal.common.grammar.Grammar; +import games.stendhal.common.parser.Sentence; +import games.stendhal.server.entity.Entity; import games.stendhal.server.entity.npc.ChatAction; import games.stendhal.server.entity.npc.ChatCondition; import games.stendhal.server.entity.npc.ConversationPhrases; @@ -14,16 +28,23 @@ import games.stendhal.server.entity.npc.action.SetQuestAction; import games.stendhal.server.entity.npc.action.SetQuestAndModifyKarmaAction; import games.stendhal.server.entity.npc.condition.AndCondition; import games.stendhal.server.entity.npc.condition.NotCondition; +import games.stendhal.server.entity.npc.condition.PlayerHasItemWithHimCondition; import games.stendhal.server.entity.npc.condition.QuestActiveCondition; import games.stendhal.server.entity.npc.condition.QuestCompletedCondition; import games.stendhal.server.entity.npc.condition.QuestNotStartedCondition; import games.stendhal.server.entity.npc.condition.TimePassedCondition; +import games.stendhal.server.entity.player.Player; public class DeliverItemQuestOfferBuilder extends QuestOfferBuilder { protected String respondIfUnableToWearUniform = "I am sorry, I don't have a task for your right now."; protected String respondIfLastQuestFailed; protected String respondIfInventoryIsFull; + protected String respondToAnotherIfLostItem; + protected String respondToAnotherIfNotLostItem = "You are still carrying the [item]."; + protected String respondToAnotherIfNoMoney = "I need to make a profit. I can't give you a bunch" + + " of free [items]. Come back when you have [charge] money."; + // hide constructor DeliverItemQuestOfferBuilder() { @@ -46,6 +67,30 @@ public class DeliverItemQuestOfferBuilder extends QuestOfferBuilder 0; + } + }; + + // player carrying item so cannot restart + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.ABORT_MESSAGES, + new AndCondition( + new QuestActiveCondition(questSlot), + new PlayerHasItemWithHimCondition(itemName) + ), + ConversationStates.ATTENDING, + respondToAnotherIfNotLostItem.replace("[item]", itemName), + null); + + // player has lost item and isn't charged to restart + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.ABORT_MESSAGES, + new AndCondition( + new QuestActiveCondition(questSlot), + new NotCondition(new PlayerHasItemWithHimCondition(itemName)), + new NotCondition(playerIsChargedCondition)), + ConversationStates.ATTENDING, + null, + startAction); + + // player has lost item and is charged to restart + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.ABORT_MESSAGES, + new AndCondition( + new QuestActiveCondition(questSlot), + new NotCondition(new PlayerHasItemWithHimCondition(itemName)), + playerIsChargedCondition + ), + ConversationStates.RESTART_OFFERED, + respondToAnotherIfLostItem + .replace("[item]", itemName) + .replace("[charge]", String.valueOf(chargeForLostItem)), + null); + + // player doesn't wan't to pay to restart + npc.add( + ConversationStates.RESTART_OFFERED, + ConversationPhrases.NO_MESSAGES, + null, + ConversationStates.ATTENDING, + respondToReject, + null); + + // player wants to pay to restart but doesn't have enough money + npc.add( + ConversationStates.RESTART_OFFERED, + ConversationPhrases.YES_MESSAGES, + new NotCondition(new PlayerHasItemWithHimCondition("money", chargeForLostItem)), + ConversationStates.ATTENDING, + respondToAnotherIfNoMoney + .replace("[items]", Grammar.plural(itemName)) + .replace("[item]", itemName) + .replace("[charge]", String.valueOf(chargeForLostItem)), + null); + + // player wants to pay to restart + npc.add( + ConversationStates.RESTART_OFFERED, + ConversationPhrases.YES_MESSAGES, + new PlayerHasItemWithHimCondition("money", chargeForLostItem), + ConversationStates.ATTENDING, + null, + startAction); + } + for (Map.Entry, String> entry : additionalReplies.entrySet()) { npc.add( ConversationStates.QUEST_OFFERED, diff --git a/src/games/stendhal/server/entity/npc/quest/DeliverItemTask.java b/src/games/stendhal/server/entity/npc/quest/DeliverItemTask.java index d8d45a3d49..aabee9f4d5 100644 --- a/src/games/stendhal/server/entity/npc/quest/DeliverItemTask.java +++ b/src/games/stendhal/server/entity/npc/quest/DeliverItemTask.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -16,7 +16,12 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.apache.log4j.Logger; + +import games.stendhal.common.MathHelper; import games.stendhal.common.Rand; +import games.stendhal.common.constants.SoundID; +import games.stendhal.common.constants.SoundLayer; import games.stendhal.common.grammar.Grammar; import games.stendhal.common.parser.Sentence; import games.stendhal.server.core.engine.SingletonRepository; @@ -33,14 +38,18 @@ import games.stendhal.server.entity.npc.condition.AlwaysFalseCondition; import games.stendhal.server.entity.npc.condition.AlwaysTrueCondition; import games.stendhal.server.entity.npc.condition.OutfitCompatibleWithClothesCondition; import games.stendhal.server.entity.player.Player; +import games.stendhal.server.events.SoundEvent; import games.stendhal.server.util.StringUtils; public class DeliverItemTask extends QuestTaskBuilder { + private static final Logger logger = Logger.getLogger(DeliverItemTask.class); + private Outfit outfit; private String itemDescription; private String itemName; + private int chargeForLostItem = 100; private Map orders = new HashMap<>(); @@ -70,6 +79,15 @@ public class DeliverItemTask extends QuestTaskBuilder { return this; } + public DeliverItemTask chargeForLostItem(int chargeForLostItem) { + if (chargeForLostItem < 0) { + logger.warn("Tried to set charge for lost item to negative value"); + chargeForLostItem = 0; + } + this.chargeForLostItem = chargeForLostItem; + return this; + } + public DeliverItemOrder order() { return new DeliverItemOrder(this); } @@ -103,19 +121,24 @@ public class DeliverItemTask extends QuestTaskBuilder { * time, or if he doesn't have a delivery to do currently. */ boolean isDeliveryTooLate(final Player player, String questSlot) { - if (player.hasQuest(questSlot) && !player.isQuestCompleted(questSlot)) { - final String[] questData = player.getQuest(questSlot).split(";"); - final String customerName = questData[0]; - final DeliverItemOrder customerData = orders.get(customerName); - final long bakeTime = Long.parseLong(questData[1]); - final long expectedTimeOfDelivery = bakeTime - + (long) 60 * 1000 * customerData.getExpectedMinutes(); - if (System.currentTimeMillis() > expectedTimeOfDelivery) { - return true; + try { + if (player.hasQuest(questSlot) && !player.isQuestCompleted(questSlot)) { + final String[] questData = player.getQuest(questSlot).split(";"); + final String customerName = questData[0]; + final DeliverItemOrder customerData = orders.get(customerName); + final long bakeTime = Long.parseLong(questData[1]); + final long expectedTimeOfDelivery = bakeTime + + (long) 60 * 1000 * customerData.getExpectedMinutes(); + if (System.currentTimeMillis() > expectedTimeOfDelivery) { + return true; + } else { + return false; + } } + } catch (final NumberFormatException | ArrayIndexOutOfBoundsException e) { + logger.error(e); } - return false; - + return true; } /** Takes away the player's uniform, if the he is wearing it. @@ -149,7 +172,13 @@ public class DeliverItemTask extends QuestTaskBuilder { return new ChatAction() { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { - final String name = Rand.rand(getAllowedCustomers(player)); + // target same NPC if player requests to restart + String name = player.getQuest(questSlot, 0); + final List allowedNames = getAllowedCustomers(player); + final boolean restarting = allowedNames.contains(name); + if (!restarting) { + name = Rand.rand(allowedNames); + } final DeliverItemOrder data = orders.get(name); Map params = new HashMap<>(); @@ -167,8 +196,17 @@ public class DeliverItemTask extends QuestTaskBuilder { if (outfit != null) { player.setOutfit(outfit, true); } - player.setQuest(questSlot, 0, name); - new SetQuestToTimeStampAction(questSlot, 1).fire(player, null, npc); + if (restarting) { + // player is charged for having lost item + player.addEvent(new SoundEvent(SoundID.COMMERCE, SoundLayer.CREATURE_NOISE)); + player.drop("money", chargeForLostItem); + } else { + player.setQuest(questSlot, 0, name); + } + // time limit for restarting is not updated unless slot state was corrupted + if (!restarting || MathHelper.parseLongDefault(player.getQuest(questSlot, 1), 0) == 0) { + new SetQuestToTimeStampAction(questSlot, 1).fire(player, null, npc); + } } else { npc.say(respondIfInventoryIsFull); } @@ -246,6 +284,10 @@ public class DeliverItemTask extends QuestTaskBuilder { return itemName; } + int getChargeForLostItem() { + return chargeForLostItem; + } + @Override List calculateHistoryProgress(QuestHistoryBuilder historyBuilder, Player player, String questSlot) { DeliverItemQuestHistoryBuilder history = (DeliverItemQuestHistoryBuilder) historyBuilder; diff --git a/src/games/stendhal/server/entity/npc/quest/QuestBuilder.java b/src/games/stendhal/server/entity/npc/quest/QuestBuilder.java index 0637ea124d..8f49079967 100644 --- a/src/games/stendhal/server/entity/npc/quest/QuestBuilder.java +++ b/src/games/stendhal/server/entity/npc/quest/QuestBuilder.java @@ -11,9 +11,6 @@ ***************************************************************************/ package games.stendhal.server.entity.npc.quest; -import marauroa.common.Pair; - - /** * builds a quest * @@ -29,9 +26,6 @@ public class QuestBuilder completionsIndexes; - /** * creates a QuestBuilder @@ -40,7 +34,6 @@ public class QuestBuilder(null, null); } /** @@ -110,38 +103,4 @@ public class QuestBuilder getCompletionsIndexes() { - return completionsIndexes; - } } diff --git a/src/games/stendhal/server/entity/npc/quest/QuestHistoryBuilder.java b/src/games/stendhal/server/entity/npc/quest/QuestHistoryBuilder.java index 58e8eda3b8..a66d8bffe2 100644 --- a/src/games/stendhal/server/entity/npc/quest/QuestHistoryBuilder.java +++ b/src/games/stendhal/server/entity/npc/quest/QuestHistoryBuilder.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2022 - Faiumoni e.V. * + * (C) Copyright 2022-2024 - Faiumoni e.V. * *************************************************************************** *************************************************************************** * * @@ -24,6 +24,8 @@ public class QuestHistoryBuilder { private String whenQuestWasCompleted; private String whenQuestCanBeRepeated; private String whenCompletionsShown; + private String whenEarlyCompletionsShown; + // hide constructor QuestHistoryBuilder() { @@ -72,6 +74,22 @@ public class QuestHistoryBuilder { return this; } + /** + * Will be shown in travel log when player has complete quest within time limit at least + * 1 time. Instances of "[count"] in the string will be replaced with the number of on-time + * completions. Anything else within square brackets ("[]") will be replaced with the + * equivalent singular or plural noun form. + * + * @param whenEarlyCompletionsShown + * Text to display in history. + * @return + * This history builder instance. + */ + public QuestHistoryBuilder whenEarlyCompletionsShown(String whenEarlyCompletionsShown) { + this.whenEarlyCompletionsShown = whenEarlyCompletionsShown; + return this; + } + String getWhenNpcWasMet() { return whenNpcWasMet; } @@ -100,4 +118,7 @@ public class QuestHistoryBuilder { return this.whenCompletionsShown; } + String getWhenEarlyCompletionsShown() { + return this.whenEarlyCompletionsShown; + } } diff --git a/src/games/stendhal/server/entity/npc/shop/ShopsList.java b/src/games/stendhal/server/entity/npc/shop/ShopsList.java index 6079798486..8b44f8e0a0 100644 --- a/src/games/stendhal/server/entity/npc/shop/ShopsList.java +++ b/src/games/stendhal/server/entity/npc/shop/ShopsList.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -95,30 +95,14 @@ public final class ShopsList { } /** - * Gets the items offered by a shop with their prices. + * Add items to a shop. * * @param name - * Shop name. - * @return - * Item names and prices. - */ - @Deprecated - public Map get(final String name) { - if (sellerContents.containsKey(name)) { - return sellerContents.get(name); - } - return buyerContents.get(name); - } - - /** - * Add an items to a shop. - * - * @param name - * Shop name. + * Shop name. * @param stype - * Seller, buyer, or trader shop. + * Seller, buyer, or trader shop. * @param inventory - * Item list with prices. + * Item list with prices. */ public void add(final String name, final ShopType stype, final ItemShopInventory inventory) { final Map selectedContents = getContents(stype); @@ -137,36 +121,20 @@ public final class ShopsList { * Add an item to a shop. * * @param name - * Shop name. + * Shop name. * @param stype - * Seller, buyer, or trader shop. + * Seller, buyer, or trader shop. * @param item - * Name of item to add. + * Item name. * @param price - * Value of the item. - * @deprecated Shops should be defined on data/conf/shops/*.xml + * Item price. */ - @Deprecated public void add(final String name, final ShopType stype, final String item, final int price) { final ItemShopInventory inventory = new ItemShopInventory(stype, name); inventory.put(item, price); add(name, stype, inventory); } - /** - * Add an item to a buyer shop. - * - * @param name - * Shop name. - * @param item - * Name of item to add. - * @param price - * Value of the item. - */ - public void addBuyer(final String name, final String item, final int price) { - add(name, ShopType.ITEM_BUY, item, price); - } - /** * Configures an NPC for a shop. * diff --git a/src/games/stendhal/server/entity/player/Player.java b/src/games/stendhal/server/entity/player/Player.java index 8829142e62..5850c7eb97 100644 --- a/src/games/stendhal/server/entity/player/Player.java +++ b/src/games/stendhal/server/entity/player/Player.java @@ -3247,6 +3247,21 @@ public class Player extends DressedEntity implements UseListener { return Integer.parseInt(values[0]) * Integer.parseInt(values[1]); } + /** + * Retrieves amount to apply to player ATK to adjust hit chance. + * + * @return + * Value of player ATK multiplied by percentage defined in item "accuracy_bonus" attribute. + */ + private int getAccuracyBonus() { + double accBonus = 0; + for (final Item equip: getAllEquipment()) { + accBonus += equip.has("accuracy_bonus") ? (equip.getDouble("accuracy_bonus") / 100) : 0; + } + // base on raw uncapped value + return (int) Math.round(this.atk * accBonus); + } + /** * This handicap increases chance that a player can hit an enemy to * make the game feel more fair. Hit chance is based on raw atk stat, @@ -3259,7 +3274,8 @@ public class Player extends DressedEntity implements UseListener { protected int calculateRiskForCanHit(final int roll, final int defenderDEF, final int attackerATK) { // use 30 as multiple for players instead of 20 - return ((int) Math.round(HIT_CHANCE_MULTIPLIER * 1.5)) * attackerATK - roll * defenderDEF; + return ((int) Math.round(HIT_CHANCE_MULTIPLIER * 1.5)) * (attackerATK + this.getAccuracyBonus()) + - roll * defenderDEF; } /** @@ -3272,4 +3288,61 @@ public class Player extends DressedEntity implements UseListener { // compensate for player hit chance handicap return (int) Math.round(super.damageDone(defender, attackingWeaponsValue, damageType) / 1.35); } + + /** + * Gets an item that is carried by the RPEntity. If the item is stackable, gets all that are on + * the first stack that is found. + * + * @param itemName + * The item's name + * @return + * The item, or a stack of stackable items, or an empty list if nothing was found. + */ + public List getAllSubmittableEquipped(String itemName) { + return getAllEquipped(Item.nameMatchesSubmittable(itemName)); + } + + /** + * Gets the number of quest submittable items of the given name that are carried by the player. + * The item can be stackable or non-stackable. + * + * @param itemName + * The item's name + * @return + * Number of carried submittable items. + */ + public int getNumberOfSubmittableEquipped(String itemName) { + return equippedStream().filter(Item.nameMatchesSubmittable(itemName)) + .mapToInt(Item::getQuantity).sum(); + } + + /** + * Determines if this player is equipped with a minimum quantity of a submittable item. + * + * @param itemName + * The item's name. + * @param amount + * The minimum amount. + * @return + * {@code true} if the item is equipped with the minimum number and is submittable. + */ + public boolean isSubmittableEquipped(String itemName, int amount) { + return isEquipped(Item.nameMatchesSubmittable(itemName), amount); + } + + /** + * Removes a specific amount of an item from the RPEntity if it is quest submittable. The item can + * either be stackable or non-stackable. The units can be distributed over different slots. If the + * player doesn't have enough units of the item, it doesn't remove anything. + * + * @param itemName + * Name of the item. + * @param amount + * Number of units that should be dropped. + * @return + * {@code true} if dropping the desired amount was successful. + */ + public boolean dropSubmittable(String itemName, int amount) { + return drop(Item.nameMatchesSubmittable(itemName), amount); + } } diff --git a/src/games/stendhal/server/entity/slot/Slots.java b/src/games/stendhal/server/entity/slot/Slots.java index a8bdf10d26..0e06b7146f 100644 --- a/src/games/stendhal/server/entity/slot/Slots.java +++ b/src/games/stendhal/server/entity/slot/Slots.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2014 - Faiumoni e. V. * + * (C) Copyright 2014-2024 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -22,6 +22,8 @@ import games.stendhal.common.Constants; */ public enum Slots { + /** Slots where equipment can be carried for use (weapons, armor, and rings). */ + EQUIPMENT(ImmutableList.copyOf(Constants.EQUIPMENT_SLOTS)), /** * slots which may be carried by an entity (e. g. a bag) */ diff --git a/src/games/stendhal/server/events/ChatOptionsEvent.java b/src/games/stendhal/server/events/ChatOptionsEvent.java index 9352796d13..3f6579e946 100644 --- a/src/games/stendhal/server/events/ChatOptionsEvent.java +++ b/src/games/stendhal/server/events/ChatOptionsEvent.java @@ -13,10 +13,12 @@ package games.stendhal.server.events; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TreeSet; import com.google.common.base.Function; @@ -64,7 +66,8 @@ public class ChatOptionsEvent extends RPEvent { } // common triggers for merchants & producers - private static final List merchantActivities = new LinkedList<>(Arrays.asList("buy", "sell")); + private static final List merchantActivities = new LinkedList<>(Arrays.asList("buy", + "sell", "repair")); private String trigger; private String label; @@ -154,11 +157,28 @@ public class ChatOptionsEvent extends RPEvent { processTransition(npc, player, res, sentence, transition); } } + + if (npc.getAttending() instanceof Player) { + final Set temp = new HashSet<>(); + temp.addAll(npc.getForcedWordsInCurrentConversation()); + temp.addAll(npc.getKnownChatOptions()); + for (final String trigger: temp) { + final ChatOption copt = new ChatOption(trigger); + if (!res.contains(copt)) { + res.add(copt); + } + } + } + return res; } private void processTransition(SpeakerNPC npc, Player player, TreeSet res, Sentence sentence, final Transition transition) { + // FIXME: + // - Triggers that are parsed in a chat option's `fire` method are not included in the NPC's + // transitions, an example of this is Gordon with "rent". Until a fix is made these triggers + // must be added using `SpeakerNPC.addKnownChatOptions`. for(Expression expr : transition.getTriggers()) { if (transition.getCondition() != null) { if (!transition.getCondition().fire(player, sentence, npc)) { @@ -169,7 +189,6 @@ public class ChatOptionsEvent extends RPEvent { String trigger = expr.getNormalized().toLowerCase(Locale.ENGLISH); ChatOption option = new ChatOption(trigger); if (ConversationPhrases.KNOWN.contains(trigger) - || npc.getKnownChatOptions().contains(trigger) || npc.hasLearnedWordInCurrentConversation(trigger) || npc.hasLearnedWordInCurrentConversation(Grammar.plural(trigger))) { res.add(option); diff --git a/src/games/stendhal/server/maps/Region.java b/src/games/stendhal/server/maps/Region.java index d4716b402e..9f5cca10dc 100644 --- a/src/games/stendhal/server/maps/Region.java +++ b/src/games/stendhal/server/maps/Region.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2019 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -22,6 +22,7 @@ public final class Region { public static final String ATHOR_ISLAND = "Athor Island"; public static final String ATLANTIS = "Atlantis"; public static final String DENIRAN = "Deniran"; + public static final String DENIRAN_CITY = "Deniran City"; public static final String SEMOS_CITY = "Semos City"; public static final String SEMOS_SURROUNDS = "Semos Surrounds"; public static final String SEMOS_MINES = "Semos Mines"; diff --git a/src/games/stendhal/server/maps/ados/city/DogNPC.java b/src/games/stendhal/server/maps/ados/city/DogNPC.java index e819f3021a..dd11ae6fd1 100644 --- a/src/games/stendhal/server/maps/ados/city/DogNPC.java +++ b/src/games/stendhal/server/maps/ados/city/DogNPC.java @@ -17,9 +17,9 @@ import java.util.Map; import games.stendhal.server.core.config.ZoneConfigurator; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.core.events.UseListener; -//import games.stendhal.server.entity.CollisionAction; import games.stendhal.server.entity.RPEntity; import games.stendhal.server.entity.npc.SilentNPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; /** * A playful puppy @@ -43,9 +43,7 @@ public class DogNPC implements ZoneConfigurator { dog.setDescription("You see Tommy."); dog.setEntityClass("animal/puppy"); dog.setBaseSpeed(0.5); - dog.moveRandomly(); - dog.setRandomMovementRadius(5, true); - //dog.setCollisionAction(CollisionAction.REVERSE); + dog.setIdleBehaviour(new WanderIdleBehaviour(5)); dog.setSounds(Arrays.asList("dog-small-bark-1", "dog-small-bark-2")); zone.add(dog); } diff --git a/src/games/stendhal/server/maps/ados/outside/VeterinarianNPC.java b/src/games/stendhal/server/maps/ados/outside/VeterinarianNPC.java index f3ccba0e65..26eb617dce 100644 --- a/src/games/stendhal/server/maps/ados/outside/VeterinarianNPC.java +++ b/src/games/stendhal/server/maps/ados/outside/VeterinarianNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -32,6 +32,7 @@ import games.stendhal.server.entity.npc.EventRaiser; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.SellerAdder; import games.stendhal.server.entity.npc.behaviour.impl.SellerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.entity.player.Player; @@ -74,7 +75,8 @@ public class VeterinarianNPC implements ZoneConfigurator { addJob("I'm the veterinarian."); - new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("healing")) { + new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("healing", + ShopType.ITEM_SELL)) { @Override public int getUnitPrice(final String item) { diff --git a/src/games/stendhal/server/maps/ados/rock/WeaponsCollectorNPC.java b/src/games/stendhal/server/maps/ados/rock/WeaponsCollectorNPC.java index 91f2020d29..9d38741b36 100644 --- a/src/games/stendhal/server/maps/ados/rock/WeaponsCollectorNPC.java +++ b/src/games/stendhal/server/maps/ados/rock/WeaponsCollectorNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -21,6 +21,7 @@ import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.BuyerAdder; import games.stendhal.server.entity.npc.behaviour.impl.QuestCompletedBuyerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; public class WeaponsCollectorNPC implements ZoneConfigurator { @@ -53,7 +54,9 @@ public class WeaponsCollectorNPC implements ZoneConfigurator { addJob("I'm much too old for hard work. I'm just living here as a hermit."); addGoodbye("It was nice to meet you."); // will buy black items once the Ultimate Collector quest is completed - new BuyerAdder().addBuyer(this, new QuestCompletedBuyerBehaviour("ultimate_collector", "I'll buy black items from you when you have completed each #challenge I set you.", shops.get("buyblack")), false); + new BuyerAdder().addBuyer(this, new QuestCompletedBuyerBehaviour("ultimate_collector", + "I'll buy black items from you when you have completed each #challenge I set you.", + shops.get("buyblack", ShopType.ITEM_BUY)), false); } /* remaining behaviour is defined in: * maps.quests.WeaponsCollector, diff --git a/src/games/stendhal/server/maps/ados/rosshouse/FatherNPC.java b/src/games/stendhal/server/maps/ados/rosshouse/FatherNPC.java index 7c33fcc042..5bbdc40927 100644 --- a/src/games/stendhal/server/maps/ados/rosshouse/FatherNPC.java +++ b/src/games/stendhal/server/maps/ados/rosshouse/FatherNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -75,7 +75,7 @@ public class FatherNPC implements ZoneConfigurator { }; - npc.setOutfit(1, 34, 7, null, 0, null, 27, null, 0); + npc.setOutfit(1, 34, 0, null, 0, null, 27, null, 0); npc.setPosition(12, 7); npc.setDirection(Direction.DOWN); npc.initHP(100); diff --git a/src/games/stendhal/server/maps/athor/ship/ScubaNPC.java b/src/games/stendhal/server/maps/athor/ship/ScubaNPC.java index ff55f9590f..ccad3a4615 100644 --- a/src/games/stendhal/server/maps/athor/ship/ScubaNPC.java +++ b/src/games/stendhal/server/maps/athor/ship/ScubaNPC.java @@ -1,6 +1,6 @@ /* $Id: CaptainNPC.java,v 1.23 2013/06/10 22:13:14 bluelads99 Exp $ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -26,6 +26,7 @@ import games.stendhal.server.entity.npc.EventRaiser; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.SellerAdder; import games.stendhal.server.entity.npc.behaviour.impl.QuestCompletedSellerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.entity.player.Player; import games.stendhal.server.maps.athor.ship.AthorFerry.Status; @@ -51,7 +52,9 @@ public class ScubaNPC implements ZoneConfigurator { addGoodbye("So long..."); addHelp("Hm, maybe you'd like to go on an adventure?"); addOffer("To licensed divers I can sell #scuba #gear."); - new SellerAdder().addSeller(this, new QuestCompletedSellerBehaviour("get_diving_license", "I can't sell #scuba #gear to just anyone!", shops.get("sellScubaStuff")), false); + new SellerAdder().addSeller(this, new QuestCompletedSellerBehaviour("get_diving_license", + "I can't sell #scuba #gear to just anyone!", + shops.get("sellScubaStuff", ShopType.ITEM_SELL)), false); addJob("I'm an assistant on this ship."); //scuba gear phrases diff --git a/src/games/stendhal/server/maps/deathmatch/DeathmatchArea.java b/src/games/stendhal/server/maps/deathmatch/DeathmatchArea.java index 49c7dada70..800c7ba684 100644 --- a/src/games/stendhal/server/maps/deathmatch/DeathmatchArea.java +++ b/src/games/stendhal/server/maps/deathmatch/DeathmatchArea.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -54,6 +54,8 @@ public class DeathmatchArea implements LoginListener { "0_semos_mountain_n2_w"), 104, 123); } player.teleport(cowardSpot.getZone(), cowardSpot.getX(), cowardSpot.getY(), Direction.DOWN, player); + // update slot so player can start again + player.setQuest("deathmatch", 0, "cancel"); player.sendPrivateText("You wake up far away from the city in the mountains. But you don't know what happened."); } diff --git a/src/games/stendhal/server/maps/deathmatch/DeathmatchEngine.java b/src/games/stendhal/server/maps/deathmatch/DeathmatchEngine.java index de8f82d222..c1fec1818d 100644 --- a/src/games/stendhal/server/maps/deathmatch/DeathmatchEngine.java +++ b/src/games/stendhal/server/maps/deathmatch/DeathmatchEngine.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -27,6 +27,9 @@ import games.stendhal.server.entity.player.Player; /** * this is the internal class which handles an active deathmatch session. + * + * FIXME: much of the code here overwrites entire slot string, should probably be changed to only + * affect relevant parts */ public class DeathmatchEngine implements TurnListener { /** The amount of milliseconds to wait before bail takes effect. */ diff --git a/src/games/stendhal/server/maps/deniran/cityinterior/accessoryshop/DealerNPC.java b/src/games/stendhal/server/maps/deniran/cityinterior/accessoryshop/DealerNPC.java index 869117f31c..253b8673fb 100644 --- a/src/games/stendhal/server/maps/deniran/cityinterior/accessoryshop/DealerNPC.java +++ b/src/games/stendhal/server/maps/deniran/cityinterior/accessoryshop/DealerNPC.java @@ -24,7 +24,6 @@ import games.stendhal.server.core.pathfinder.Node; import games.stendhal.server.entity.mapstuff.sign.OutfitShopSign; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.shop.OutfitShopInventory; -import games.stendhal.server.util.TimeUtil; /** @@ -62,7 +61,7 @@ public class DealerNPC implements ZoneConfigurator { new Node(9, 6), new Node(9, 4) ), true)); - gwen.addSuspend(TimeUtil.TURNS_IN_MINUTE / 2, Direction.DOWN, 4); + gwen.addSuspend(30, Direction.DOWN, 4); return gwen; } diff --git a/src/games/stendhal/server/maps/deniran/cityinterior/dressshop/OutfitLenderNPC.java b/src/games/stendhal/server/maps/deniran/cityinterior/dressshop/OutfitLenderNPC.java index 03ab9f194d..14b068d526 100644 --- a/src/games/stendhal/server/maps/deniran/cityinterior/dressshop/OutfitLenderNPC.java +++ b/src/games/stendhal/server/maps/deniran/cityinterior/dressshop/OutfitLenderNPC.java @@ -131,7 +131,7 @@ public class OutfitLenderNPC implements ZoneConfigurator { lender.setPathAndPosition(new FixedPath(nodes, true)); lender.setRetracePath(); - lender.addSuspend(TimeUtil.TURNS_IN_MINUTE / 4, Direction.DOWN, 0); + lender.addSuspend(15, Direction.DOWN, 0); zone.add(lender); } diff --git a/src/games/stendhal/server/maps/deniran/cityinterior/potionsshop/PotionsDealerNPC.java b/src/games/stendhal/server/maps/deniran/cityinterior/potionsshop/PotionsDealerNPC.java index 72d4e38b74..bfb7707ba2 100644 --- a/src/games/stendhal/server/maps/deniran/cityinterior/potionsshop/PotionsDealerNPC.java +++ b/src/games/stendhal/server/maps/deniran/cityinterior/potionsshop/PotionsDealerNPC.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2023 - Arianne * + * Copyright © 2003-2024 - Arianne * *************************************************************************** *************************************************************************** * * @@ -34,6 +34,7 @@ import games.stendhal.server.entity.npc.action.NPCEmoteAction; import games.stendhal.server.entity.npc.action.PlaySoundAction; import games.stendhal.server.entity.npc.action.SayTextAction; import games.stendhal.server.entity.npc.condition.PlayerNextToCondition; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.entity.player.Player; @@ -53,8 +54,8 @@ public class PotionsDealerNPC implements ZoneConfigurator { ); final ShopsList shops = SingletonRepository.getShopsList(); - final Map pricesBuy = shops.get("deniranpotionsbuy"); - final Map pricesSell = shops.get("deniranpotionssell"); + final Map pricesBuy = shops.get("deniranpotionsbuy", ShopType.ITEM_BUY); + final Map pricesSell = shops.get("deniranpotionssell", ShopType.ITEM_SELL); npc.addGreeting("Welcome to Deniran's potion shop."); npc.addJob("I manage this potion shop. Ask me about my #prices."); diff --git a/src/games/stendhal/server/maps/deniran/cityinterior/tannery/TannerNPC.java b/src/games/stendhal/server/maps/deniran/cityinterior/tannery/TannerNPC.java index 9f89917ecc..6f94d16b9c 100644 --- a/src/games/stendhal/server/maps/deniran/cityinterior/tannery/TannerNPC.java +++ b/src/games/stendhal/server/maps/deniran/cityinterior/tannery/TannerNPC.java @@ -66,7 +66,7 @@ public class TannerNPC implements ZoneConfigurator { private final static String QUEST_SLOT = "money_" + FEATURE_SLOT; private final static String QUEST_NAME = "Money Pouch"; - // players must have looted at least 1,000,000 money to get the money pouch + // players must have looted at least 100,000 money to get the money pouch private static final int requiredMoneyLoot = 100000; private static final int serviceFee = 50000; private static final int TAN_TIME = TimeUtil.MINUTES_IN_DAY; @@ -383,7 +383,7 @@ public class TannerNPC implements ZoneConfigurator { @Override public String getRegion() { - return Region.DENIRAN; + return Region.DENIRAN_CITY; } @Override diff --git a/src/games/stendhal/server/maps/deniran/cityoutside/CloverHunterNPC.java b/src/games/stendhal/server/maps/deniran/cityoutside/CloverHunterNPC.java new file mode 100644 index 0000000000..2d3beace13 --- /dev/null +++ b/src/games/stendhal/server/maps/deniran/cityoutside/CloverHunterNPC.java @@ -0,0 +1,64 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.maps.deniran.cityoutside; + +import java.util.Map; + +import games.stendhal.server.core.config.ZoneConfigurator; +import games.stendhal.server.core.engine.StendhalRPZone; +import games.stendhal.server.entity.npc.ConversationPhrases; +import games.stendhal.server.entity.npc.ConversationStates; +import games.stendhal.server.entity.npc.SpeakerNPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; +import games.stendhal.server.entity.npc.condition.NotCondition; +import games.stendhal.server.entity.npc.condition.QuestRegisteredCondition; + + +public class CloverHunterNPC implements ZoneConfigurator { + + @Override + public void configureZone(StendhalRPZone zone, Map attributes) { + zone.add(buildNPC()); + } + + private SpeakerNPC buildNPC() { + SpeakerNPC npc = new SpeakerNPC("Maple"); + npc.setEntityClass("cloverhunternpc"); + npc.setDescription("You see a young woman scanning the ground with her magnifying glass."); + npc.setPosition(68, 67); + npc.setIdleBehaviour(new WanderIdleBehaviour()); + + buildDialogue(npc); + + return npc; + } + + private void buildDialogue(SpeakerNPC npc) { + npc.addGreeting("Hello fellow clover hunter!"); + npc.addGoodbye("May luck shine brightly o'er you!"); + npc.addJob("I'm a clover hunter. I'm searching for the lucky four-leaf #clover."); + npc.addHelp("Four-leaf #clovers are extremely rare. If you find one, it is said you will have" + + " excellent luck!"); + npc.addOffer("I can tell you a little about #clovers."); + npc.addReply("clover", "Clovers can grow just about anywhere in the sunlight. So don't go" + + " looking for any underground. Ones with four leaves are especially rare and are a" + + " challenging #task to find."); + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.QUEST_MESSAGES, + new NotCondition(new QuestRegisteredCondition("lucky_four_leaf_clover")), + ConversationStates.ATTENDING, + "No, no thank you. I can find a four-leaf clover on my own.", + null); + } +} diff --git a/src/games/stendhal/server/maps/fado/city/RabbitNPC.java b/src/games/stendhal/server/maps/fado/city/RabbitNPC.java index f70118f9e9..ee9b4361a2 100644 --- a/src/games/stendhal/server/maps/fado/city/RabbitNPC.java +++ b/src/games/stendhal/server/maps/fado/city/RabbitNPC.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -18,6 +18,7 @@ import java.util.Map; import games.stendhal.server.core.config.ZoneConfigurator; import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.entity.npc.SilentNPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; /** * Rabbits @@ -50,7 +51,7 @@ public class RabbitNPC implements ZoneConfigurator { mammal.setDescription("You see a rabbit."); mammal.setEntityClass("animal/rabbit"); mammal.setBaseSpeed(0.2); - mammal.moveRandomly(); + mammal.setIdleBehaviour(new WanderIdleBehaviour()); mammal.setTitle("rabbit"); mammal.setPathCompletedPause(20); zone.add(mammal); diff --git a/src/games/stendhal/server/maps/fado/forest/CraftsmanNPC.java b/src/games/stendhal/server/maps/fado/forest/CraftsmanNPC.java index 14ba5ee1ad..4cd24a1e64 100644 --- a/src/games/stendhal/server/maps/fado/forest/CraftsmanNPC.java +++ b/src/games/stendhal/server/maps/fado/forest/CraftsmanNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -25,6 +25,7 @@ import games.stendhal.server.entity.CollisionAction; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.BuyerAdder; import games.stendhal.server.entity.npc.behaviour.impl.QuestCompletedBuyerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; @@ -68,7 +69,9 @@ public class CraftsmanNPC implements ZoneConfigurator { //addGreeting("Welcome to this forest, south of Or'ril river."); addJob("I'm a craftsman. One day I hope to craft such items as the green elves can make."); addHelp("My friend Orchiwald is a great story teller, he would speak with you about the albino elves and how we come to be here."); - new BuyerAdder().addBuyer(this, new QuestCompletedBuyerBehaviour("elvish_armor", "I'm not able to buy anything from you right now.", shops.get("buyelvish")), false); + new BuyerAdder().addBuyer(this, new QuestCompletedBuyerBehaviour("elvish_armor", + "I'm not able to buy anything from you right now.", + shops.get("buyelvish", ShopType.ITEM_BUY)), false); addGoodbye("Bye."); } }; diff --git a/src/games/stendhal/server/maps/fado/forest/GreeterNPC.java b/src/games/stendhal/server/maps/fado/forest/GreeterNPC.java index c44e2ba689..d4183436e2 100644 --- a/src/games/stendhal/server/maps/fado/forest/GreeterNPC.java +++ b/src/games/stendhal/server/maps/fado/forest/GreeterNPC.java @@ -25,6 +25,7 @@ import games.stendhal.server.entity.CollisionAction; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.SellerAdder; import games.stendhal.server.entity.npc.behaviour.impl.SellerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; /** @@ -72,7 +73,8 @@ public class GreeterNPC implements ZoneConfigurator { addReply("nomadic", "We don't have a permanent home, we travel instead between forests and glens. When we find a clearing we like, we settle. We liked this one because of the ancient #stones near by."); addReply("stones", "They have some mystical quality. We like to be by them for the changing of the seasons."); addHelp("I would sell you enchanted scrolls to return to Fado City. I have a source of cheap ones."); - new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("fadoscrolls")) { + new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("fadoscrolls", + ShopType.ITEM_SELL)) { @Override public int getUnitPrice(final String item) { diff --git a/src/games/stendhal/server/maps/fado/forest/OldWomanNPC.java b/src/games/stendhal/server/maps/fado/forest/OldWomanNPC.java index 862daa42ab..be1d6b63bf 100644 --- a/src/games/stendhal/server/maps/fado/forest/OldWomanNPC.java +++ b/src/games/stendhal/server/maps/fado/forest/OldWomanNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -12,15 +12,12 @@ ***************************************************************************/ package games.stendhal.server.maps.fado.forest; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import games.stendhal.server.core.config.ZoneConfigurator; import games.stendhal.server.core.engine.StendhalRPZone; -import games.stendhal.server.core.pathfinder.FixedPath; -import games.stendhal.server.core.pathfinder.Node; import games.stendhal.server.entity.npc.SpeakerNPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; /** * Creates Jefs mother Amber in Fado Forest and other areas (she moves in different zones) @@ -43,6 +40,7 @@ public class OldWomanNPC implements ZoneConfigurator { private void buildNPC(final StendhalRPZone zone) { final SpeakerNPC npc = new SpeakerNPC("Amber") { + /* @Override protected void createPath() { final List nodes = new LinkedList(); @@ -110,6 +108,7 @@ public class OldWomanNPC implements ZoneConfigurator { nodes.add(new Node(38,8)); setPath(new FixedPath(nodes, true)); } + */ @Override protected void createDialog() { @@ -131,6 +130,7 @@ public class OldWomanNPC implements ZoneConfigurator { npc.setPosition(38, 8); npc.initHP(100); npc.setDescription("You see an elder woman. She looks a bit disorientated."); + npc.setIdleBehaviour(new WanderIdleBehaviour()); // hide location from website npc.hideLocation(); zone.add(npc); diff --git a/src/games/stendhal/server/maps/global/FourLeafClover.java b/src/games/stendhal/server/maps/global/FourLeafClover.java new file mode 100644 index 0000000000..9ea8355cac --- /dev/null +++ b/src/games/stendhal/server/maps/global/FourLeafClover.java @@ -0,0 +1,34 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.maps.global; + +import java.util.Map; + +import games.stendhal.server.core.config.ZoneConfigurator; +import games.stendhal.server.core.engine.CachedActionManager; +import games.stendhal.server.core.engine.StendhalRPZone; +import games.stendhal.server.entity.mapstuff.spawner.CloverSpawner; + + +public class FourLeafClover implements ZoneConfigurator { + + @Override + public void configureZone(StendhalRPZone zone, Map attributes) { + // don't add to world until server maps are loaded + CachedActionManager.get().register(new Runnable() { + @Override + public void run() { + CloverSpawner.get().init(); + } + }); + } +} diff --git a/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviour.java b/src/games/stendhal/server/maps/global/package-info.java similarity index 73% rename from src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviour.java rename to src/games/stendhal/server/maps/global/package-info.java index e4830199e4..f2543a265a 100644 --- a/src/games/stendhal/server/entity/creature/impl/idle/IdleBehaviour.java +++ b/src/games/stendhal/server/maps/global/package-info.java @@ -1,6 +1,5 @@ -/* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * Copyright © 2024 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -10,11 +9,4 @@ * (at your option) any later version. * * * ***************************************************************************/ -package games.stendhal.server.entity.creature.impl.idle; - -import games.stendhal.server.entity.creature.Creature; - -public interface IdleBehaviour { - - void perform(Creature creature); -} +package games.stendhal.server.maps.global; diff --git a/src/games/stendhal/server/maps/nalwor/forest/ArcheryRange.java b/src/games/stendhal/server/maps/nalwor/forest/ArcheryRange.java index 2bce043656..c8a85cecb2 100644 --- a/src/games/stendhal/server/maps/nalwor/forest/ArcheryRange.java +++ b/src/games/stendhal/server/maps/nalwor/forest/ArcheryRange.java @@ -11,8 +11,6 @@ ***************************************************************************/ package games.stendhal.server.maps.nalwor.forest; -import static games.stendhal.server.maps.nalwor.forest.AssassinRepairerAdder.ID_NO_AFFORD; - import java.awt.Point; import java.awt.geom.Rectangle2D; import java.lang.ref.WeakReference; @@ -63,6 +61,7 @@ import games.stendhal.server.entity.npc.condition.TimePassedCondition; import games.stendhal.server.entity.player.Player; import games.stendhal.server.events.ShowItemListEvent; import games.stendhal.server.maps.nalwor.forest.AssassinRepairerAdder.AssassinRepairer; +import games.stendhal.server.maps.nalwor.forest.AssassinRepairerAdder.ResponseId; import games.stendhal.server.util.TimeUtil; @@ -206,7 +205,7 @@ public class ArcheryRange implements ZoneConfigurator,LoginListener,LogoutListen private void initNPC() { repairerAdder = new AssassinRepairerAdder(); - ranger = repairerAdder.new AssassinRepairer(npcName, repairableSellPrices) { + ranger = new AssassinRepairerAdder.AssassinRepairer(npcName, repairableSellPrices) { @Override public void say(final String text) { // don't turn toward player @@ -320,8 +319,8 @@ public class ArcheryRange implements ZoneConfigurator,LoginListener,LogoutListen repairPrices.put(itemName, repairableSellPrices.get(itemName) / 2); } - repairerAdder.add(ranger, repairPrices); - repairerAdder.setReply(ID_NO_AFFORD, "You don't have enough money. Get outta here!"); + repairerAdder.add(ranger); + ranger.setReply(ResponseId.NO_AFFORD, "You don't have enough money. Get outta here!"); } /** diff --git a/src/games/stendhal/server/maps/nalwor/forest/AssassinRepairerAdder.java b/src/games/stendhal/server/maps/nalwor/forest/AssassinRepairerAdder.java index 47ddca18b7..e7c9b2fa25 100644 --- a/src/games/stendhal/server/maps/nalwor/forest/AssassinRepairerAdder.java +++ b/src/games/stendhal/server/maps/nalwor/forest/AssassinRepairerAdder.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2020-2023 - Stendhal * + * Copyright © 2020-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -11,9 +11,8 @@ ***************************************************************************/ package games.stendhal.server.maps.nalwor.forest; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.log4j.Logger; @@ -40,200 +39,142 @@ import games.stendhal.server.events.SoundEvent; import marauroa.common.Pair; +/** + * Functions for repairing items that are "worn" or "broken". + * + * TODO: + * - merge functionality with games.stendhal.server.entity.npc.behaviour.adder.RepairerAdder + * - add behavior for repairing "broken" items (more expensive than just "worn") + */ public class AssassinRepairerAdder { private static final Logger logger = Logger.getLogger(AssassinRepairerAdder.class); - //private SpeakerNPC repairer; - private Map priceList; - - private String currentRepairItem = null; - private Integer currentRepairCount = null; - private Integer currentRepairFee = null; - - private static final List repairPhrases = Arrays.asList("repair", "fix"); - - // reply IDs - public final static String ID_DENIED = "denied"; - public final static String ID_UNDECLARED = "undeclared"; - public final static String ID_NO_REPAIR = "cannot_repair"; - public final static String ID_NO_CARRY = "not_carrying"; - public final static String ID_NO_AFFORD = "cannot_afford"; - public final static String ID_SAY_COUNT = "say_count"; - public final static String ID_REJECT_REPAIR = "reject_repair"; - public final static String ID_DROPPED = "dropped"; - public final static String ID_REPAIR_DONE = "repair_done"; - public final static String ID_ERROR = "error"; - - final Map replies = new HashMap() {{ - put(ID_DENIED, "Only members of the assassins guild can have items repaired."); - put(ID_UNDECLARED, "Please tell me what you would repaired."); - put(ID_NO_REPAIR, null); - put(ID_NO_CARRY, null); - put(ID_NO_AFFORD, "You don't have enough money."); - put(ID_SAY_COUNT, null); - put(ID_REJECT_REPAIR, "Good luck then. Remember, once they break, they can't be repaired."); - put(ID_DROPPED, "Did you drop the item?"); - put(ID_REPAIR_DONE, null); - put(ID_ERROR, "It appears I am unable to process the transaction. I'm sorry."); - }}; - - - public void add(final AssassinRepairer repairer, final Map priceList) { - //this.repairer = repairer; - this.priceList = priceList; + /** + * IDs for responses to repair requests. + * + * TODO: + * - move to behavior + */ + public static enum ResponseId { + DENIED, // player does not satisfy preconditions + UNDECLARED, // player did not specify which item to repair + NO_REPAIR, // NPC does not repair requested item + NO_CARRY, // player is not carrying requested item + NO_AFFORD, // player doesn't have enough money to pay for repair + SAY_COUNT, // NPC should say how many items player is carrying that need repair + REJECT_REPAIR, // player does not confirm repair after request + DROPPED, // player dropped item after request + REPAIR_DONE, // repair was completed + ERROR; // an unknown error occurred + } + /** + * Adds repairer behavior to an NPC. + * + * @param repairer + * Item repairing NPC. + */ + public void add(final AssassinRepairer repairer) { + // requests repair but does not meet preconditions repairer.add(ConversationStates.ATTENDING, - repairPhrases, + ConversationPhrases.REPAIR_MESSAGES, new NotCondition(new PlayerHasItemWithHimCondition("assassins id")), ConversationStates.ATTENDING, - //"Only members of the assassins guild can have their #'auto crossbows' repaired.", - getReply("denied"), - null); + null, + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + repairer.say(repairer.getReply(ResponseId.DENIED)); + } + }); + // meets requirements and does meet preconditions repairer.add(ConversationStates.ATTENDING, - repairPhrases, + ConversationPhrases.REPAIR_MESSAGES, new PlayerHasItemWithHimCondition("assassins id"), - ConversationStates.QUESTION_2, + null, null, requestRepairAction(repairer)); + // changes mind / does not want repair repairer.add(ConversationStates.QUESTION_2, ConversationPhrases.NO_MESSAGES, null, ConversationStates.ATTENDING, - getReply(ID_REJECT_REPAIR), - null); - - // player dropped item before accepting - repairer.add(ConversationStates.QUESTION_2, - ConversationPhrases.YES_MESSAGES, - new NotCondition(needsRepairCondition()), - ConversationStates.ATTENDING, - getReply(ID_DROPPED), - null); + null, + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + repairer.say(repairer.getReply(ResponseId.REJECT_REPAIR)); + } + }); // this should not happen repairer.add(ConversationStates.QUESTION_2, ConversationPhrases.YES_MESSAGES, - feeNotSetCondition(), - ConversationStates.ATTENDING, - getReply(ID_ERROR), - null); - - repairer.add(ConversationStates.QUESTION_2, - ConversationPhrases.YES_MESSAGES, - new AndCondition( - needsRepairCondition(), - new NotCondition(canAffordCondition())), - ConversationStates.ATTENDING, - getReply(ID_NO_AFFORD), - null); - - repairer.add(ConversationStates.QUESTION_2, - ConversationPhrases.YES_MESSAGES, - new AndCondition( - needsRepairCondition(), - canAffordCondition()), + feeNotSetCondition(repairer), ConversationStates.ATTENDING, null, - repairAction()); - } - - private void reset() { - currentRepairItem = null; - currentRepairCount = null; - currentRepairFee = null; - } - - public void setReply(final String id, final String... phrases) { - /* - if (phrases.length > 2) { - logger.warn("Maximum of 2 phrases allowed: singular & plural forms"); - } - */ - - if (replies.containsKey(id)) { - if (phrases.length == 1) { - replies.put(id, phrases[0]); - } else { - replies.put(id, new Pair(phrases[0], phrases[1])); - - if (phrases.length > 2) { - logger.warn("Maximum of 2 phrases allowed: singular & plural forms"); - } - } - - return; - } - - logger.warn("Tried to add unused reply: " + id); - } - - @SuppressWarnings("unchecked") - private String getReply(final String id, final boolean plural) { - final Object reply = replies.get(id); - if (reply instanceof Pair) { - if (plural) { - return ((Pair) reply).second(); - } else { - return ((Pair) reply).first(); - } - } - - return (String) reply; - } - - private String getReply(final String id) { - return getReply(id, false); - } - - private void setRepairItem(final String itemName) { - for (final String repairable: priceList.keySet()) { - if (itemName.toLowerCase().equals(repairable.toLowerCase())) { - currentRepairItem = repairable; - return; - } - } - - currentRepairItem = itemName; - } - - private boolean canRepair(final String item) { - for (String bow: priceList.keySet()) { - if (item.toLowerCase().equals(bow.toLowerCase())) { - return true; - } - } - - return false; - } - - private void setRepairCount(final Player player) { - if (!canRepair(currentRepairItem)) { - return; - } - - int count = 0; - for (final Item item: player.getAllEquipped(currentRepairItem)) { - if (((BreakableWeapon) item).isUsed()) { - count++; - } - } - - currentRepairCount = count; - } - - private void calculateRepairFee() { - for (final String item: priceList.keySet()) { - if (currentRepairItem.toLowerCase().equals(item.toLowerCase())) { - currentRepairFee = currentRepairCount * (priceList.get(currentRepairItem)); - return; - } - } + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + repairer.say(repairer.getReply(ResponseId.ERROR)); + } + }); + + // player dropped item before accepting + repairer.add(ConversationStates.QUESTION_2, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + new NotCondition(feeNotSetCondition(repairer)), + itemsChangedCondition(repairer)), + ConversationStates.ATTENDING, + null, + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + repairer.say(repairer.getReply(ResponseId.DROPPED)); + } + }); + + // wants repair but doesn't have enough money + repairer.add(ConversationStates.QUESTION_2, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + new NotCondition(feeNotSetCondition(repairer)), + new NotCondition(itemsChangedCondition(repairer)), + new NotCondition(canAffordCondition(repairer))), + ConversationStates.ATTENDING, + null, + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + repairer.say(repairer.getReply(ResponseId.NO_AFFORD)); + } + }); + + // wants repair and does have enough money + repairer.add(ConversationStates.QUESTION_2, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + new NotCondition(feeNotSetCondition(repairer)), + new NotCondition(itemsChangedCondition(repairer)), + canAffordCondition(repairer)), + ConversationStates.ATTENDING, + null, + repairAction(repairer)); } + /** + * Creates action for requesting item(s) repair. + * + * @param repairer + * Item repairing NPC. + * @return + * Action for requesting repair. + */ private ChatAction requestRepairAction(final AssassinRepairer repairer) { return new ChatAction() { @Override @@ -241,40 +182,44 @@ public class AssassinRepairerAdder { final int repairables = repairer.getNumberOfRepairables(); String request = sentence.getTrimmedText(); - if (repairPhrases.contains(request.toLowerCase())) { + if (ConversationPhrases.REPAIR_MESSAGES.contains(request.toLowerCase(Locale.ENGLISH))) { if (repairables > 1) { - repairer.say(getReply(ID_UNDECLARED)); + repairer.say(repairer.getReply(ResponseId.UNDECLARED)); repairer.setCurrentState(ConversationStates.ATTENDING); return; } - // player does not need to specify item name if repairer only repairs one item request = request + " " + repairer.getFirstRepairable(); } - for (final String rWord: repairPhrases) { - if (request.startsWith(rWord)) { + // compare with lower-case to make request line case-insensitive + String requestL = request.toLowerCase(Locale.ENGLISH); + for (final String rWord: ConversationPhrases.REPAIR_MESSAGES) { + if (requestL.startsWith(rWord.toLowerCase(Locale.ENGLISH))) { + // trim trigger word request = request.substring(rWord.length() + 1); break; } } - setRepairItem(request); - setRepairCount(player); + repairer.setRepairItem(request); + repairer.setRepairCount(player); - if (currentRepairCount == null) { - String cannotRepairReply = getReply(ID_NO_REPAIR); + if (!repairer.canRepair()) { + String cannotRepairReply = repairer.getReply(ResponseId.NO_REPAIR); if (cannotRepairReply == null) { - cannotRepairReply = "I do not repair " + Grammar.plural(currentRepairItem) + "."; + cannotRepairReply = "I do not repair " + Grammar.plural(repairer.currentRepairItem) + + "."; } repairer.say(cannotRepairReply); repairer.setCurrentState(ConversationStates.ATTENDING); return; - } else if (currentRepairCount < 1) { - String notCarryingReply = getReply(ID_NO_CARRY); + } else if (repairer.currentRepairCount < 1) { + String notCarryingReply = repairer.getReply(ResponseId.NO_CARRY); if (notCarryingReply == null) { - notCarryingReply = "You don't have any #'" + Grammar.plural(currentRepairItem) + "' that need repaired."; + notCarryingReply = "You don't have any #'" + Grammar.plural(repairer.currentRepairItem) + + "' that need repaired."; } repairer.say(notCarryingReply); @@ -282,18 +227,19 @@ public class AssassinRepairerAdder { return; } - calculateRepairFee(); - final boolean multiple = currentRepairCount > 1; + repairer.calculateRepairFee(); + final boolean multiple = repairer.currentRepairCount > 1; String sayCountReply; if (multiple) { - sayCountReply = getReply(ID_SAY_COUNT, true); + sayCountReply = repairer.getReply(ResponseId.SAY_COUNT, true); } else { - sayCountReply = getReply(ID_SAY_COUNT); + sayCountReply = repairer.getReply(ResponseId.SAY_COUNT); } if (sayCountReply == null) { - final StringBuilder sb = new StringBuilder("You have " + Integer.toString(currentRepairCount) + " used " + currentRepairItem); //Grammar.plnoun(usedBows, bowType)); // formats name all lowercase + final StringBuilder sb = new StringBuilder("You have " + repairer.currentRepairCount + + " used " + repairer.currentRepairItem); //Grammar.plnoun(repairer.currentRepairCount, repairer.currentRepairItem)); // FIXME: formats name all lowercase if (multiple) { sb.append("s"); } @@ -303,136 +249,381 @@ public class AssassinRepairerAdder { } else { sb.append("it"); } - sb.append(" for " + Integer.toString(currentRepairFee) + " money. Would you like me to do so?"); - + sb.append(" for " + repairer.currentRepairFee + " money. Would you like me to do so?"); sayCountReply = sb.toString(); } repairer.say(sayCountReply); + repairer.setCurrentState(ConversationStates.QUESTION_2); } }; } - private ChatAction repairAction() { + /** + * Creates action for executing item(s) repair. + * + * @param repairer + * Item repairing NPC. + * @return + * Action for executing repair. + */ + private ChatAction repairAction(AssassinRepairer repairer) { return new ChatAction() { @Override - public void fire(final Player player, final Sentence sentence, final EventRaiser repairer) { - //final int usedBows = getUsedBowsCount(player); - player.drop("money", currentRepairFee); + public void fire(final Player player, final Sentence sentence, final EventRaiser raiser) { + player.drop("money", repairer.currentRepairFee); - for (final Item bow: player.getAllEquipped(currentRepairItem)) { + for (final Item bow: player.getAllEquipped(repairer.currentRepairItem)) { final BreakableWeapon breakable = (BreakableWeapon) bow; if (breakable.isUsed()) { breakable.repair(); } } - final boolean multiple = currentRepairCount > 1; + final boolean multiple = repairer.currentRepairCount > 1; String doneReply; if (multiple) { - doneReply = getReply(ID_REPAIR_DONE, true); + doneReply = repairer.getReply(ResponseId.REPAIR_DONE, true); } else { - doneReply = getReply(ID_REPAIR_DONE); + doneReply = repairer.getReply(ResponseId.REPAIR_DONE); } if (doneReply == null) { doneReply = "Done! Your "; if (multiple) { - doneReply += Grammar.plural(currentRepairItem) + " are "; + doneReply += Grammar.plural(repairer.currentRepairItem) + " are "; } else { - doneReply += currentRepairItem + " is "; + doneReply += repairer.currentRepairItem + " is "; } doneReply += "as good as new."; } - /* - if (currentRepairCount > 1) { - ranger.say("Done! Your " + Grammar.plural(bowType) + " are as good as new."); - } else { - ranger.say("Done! Your " + bowType + " is as good as new."); - } - */ - repairer.say(doneReply); repairer.addEvent(new SoundEvent(SoundID.COMMERCE, SoundLayer.CREATURE_NOISE)); // reset item name, count, & fee back to null - /* - reset(); - */ + repairer.reset(); } }; } - private ChatCondition feeNotSetCondition() { + /** + * Creates condition to check if fee has been determined. + * + * @param repairer + * Item repairing NPC. + * @return + * Condition. + */ + private ChatCondition feeNotSetCondition(AssassinRepairer repairer) { return new ChatCondition() { @Override - public boolean fire(final Player player, final Sentence sentence, final Entity repairer) { - if (currentRepairFee == null) { + public boolean fire(final Player player, final Sentence sentence, final Entity raiser) { + if (repairer.currentRepairFee == null) { logger.error("Cannot create transaction, repair fee not set"); return true; } - return false; } }; } - private ChatCondition canAffordCondition() { + /** + * Creates condition to check if player can afford fee. + * + * @param repairer + * Item repairing NPC. + * @return + * Condition. + */ + private ChatCondition canAffordCondition(AssassinRepairer repairer) { return new ChatCondition() { @Override - public boolean fire(final Player player, final Sentence sentence, final Entity repairer) { - return player.isEquipped("money", currentRepairFee); + public boolean fire(final Player player, final Sentence sentence, final Entity raiser) { + return player.isEquipped("money", repairer.currentRepairFee); } }; } - private ChatCondition needsRepairCondition() { + /** + * Creates condition to check if number of items in need of repair changed after initial request + * (e.g. player either dropped item and/or picked up from ground to try to get discount). + * + * @param repairer + * Item repairing NPC. + * @return + * Condition. + */ + private ChatCondition itemsChangedCondition(AssassinRepairer repairer) { return new ChatCondition() { @Override - public boolean fire(final Player player, final Sentence sentence, Entity repairer) { - return currentRepairCount != null && currentRepairCount > 0; + public boolean fire(Player player, Sentence sentence, Entity entity) { + return repairer.getWornItemsCount(player) != repairer.currentRepairCount; } }; } - public class AssassinRepairer extends SpeakerNPC { + /** + * NPC entity that supports repairing items. + * + * TODO: + * - move to separate class file in package {@code games.stendhal.server.entity.npc} or ... + * - move members and methods to behavior class + */ + public static class AssassinRepairer extends SpeakerNPC { + private static Logger logger = Logger.getLogger(AssassinRepairer.class); + + /** Prices for repairing items. */ private final Map repairList; + /** Subject item name of current conversation. */ + private String currentRepairItem = null; + /** Subject number of items to repair of current conversation. */ + private Integer currentRepairCount = null; + /** Subject fee of current conversation. */ + private Integer currentRepairFee = null; + + /** Responses for repair requests conditions. */ + private final Map replies = new HashMap() {{ + put(ResponseId.DENIED, "Only members of the assassins guild can have items repaired."); + put(ResponseId.UNDECLARED, "Please tell me what you would like repaired."); + put(ResponseId.NO_REPAIR, null); + put(ResponseId.NO_CARRY, null); + put(ResponseId.NO_AFFORD, "You don't have enough money."); + put(ResponseId.SAY_COUNT, null); + put(ResponseId.REJECT_REPAIR, "Good luck then. Remember, once they break, they can't be repaired."); + put(ResponseId.DROPPED, "Did you drop the item?"); + put(ResponseId.REPAIR_DONE, null); + put(ResponseId.ERROR, "It appears I am unable to process the transaction. I'm sorry."); + }}; + + + /** + * Creates a new NPC. + * + * @param name + * Name of NPC. + * @param repairList + * List of item names and prices for repair. + */ public AssassinRepairer(String name, final Map repairList) { super(name); - this.repairList = repairList; } - @Override - public void onGoodbye(final RPEntity attending) { - // reset item name, count, & fee to null - reset(); + /** + * Resets subjects of current conversation. + */ + private void reset() { + currentRepairItem = null; + currentRepairCount = null; + currentRepairFee = null; + } + + /** + * Adds/Changes a reply for a certain condition. + * + * @param id + * Condition ID. + * @param responses + * Replies for condition. + */ + public void setReply(final ResponseId id, final String... responses) { + if (replies.containsKey(id)) { + if (responses.length == 1) { + replies.put(id, responses[0]); + } else { + replies.put(id, new Pair(responses[0], responses[1])); + if (responses.length > 2) { + logger.warn("Maximum of 2 phrases allowed: singular & plural forms"); + } + } + return; + } + logger.warn("Tried to add unused reply: " + id); + } + + /** + * Retrieves reply for certain condition. + * + * @param id + * Condition ID. + * @param plural + * {@code true} if multiple items are to be repaired. + * @return + * NPC response to condition. + */ + private String getReply(final ResponseId id, final boolean plural) { + final Object reply = replies.get(id); + if (reply instanceof Pair) { + Pair p = (Pair) reply; + if (plural) { + return (String) p.second(); + } else { + return (String) p.first(); + } + } + return (String) reply; + } + + /** + * Retrieves reply for certain condition. + * + * @param id + * Condition ID. + * @return + * NPC response to condition. + */ + private String getReply(final ResponseId id) { + return getReply(id, false); + } + + /** + * Sets subject item name of current conversation. + * + * @param itemName + * Name of item. + */ + private void setRepairItem(String itemName) { + itemName = Grammar.singular(itemName); + for (final String repairable: repairList.keySet()) { + if (itemName.toLowerCase(Locale.ENGLISH).equals(repairable.toLowerCase(Locale.ENGLISH))) { + // use name from repair list in case request does not match character case (e.g. + // repairable item is "foo" but player requested "Foo") + currentRepairItem = repairable; + return; + } + } + currentRepairItem = itemName; + } + + /** + * Checks if current item subject is included in list of items that NPC can repair. + * + * @return + * {@code true} if repairing item is supported. + */ + public boolean canRepair() { + if (currentRepairItem == null) { + return false; + } + String itemNameL = currentRepairItem.toLowerCase(Locale.ENGLISH); + for (String repairable: repairList.keySet()) { + if (repairable.toLowerCase(Locale.ENGLISH).equals(itemNameL)) { + return true; + } + } + return false; } /** * Retrieves number of item types that can be repaired by this NPC. * * @return - * Number of repairable item types. + * Number of supported item types. */ public int getNumberOfRepairables() { return repairList.size(); } /** - * Retrieves the first item name from repair list. + * Retrieves number of items player is carrying that need repair. + * + * @param player + * Player requesting repair. + * @return + * Number of "worn" items player is carrying. + */ + private int getWornItemsCount(final Player player) { + if (!canRepair()) { + return 0; + } + int count = 0; + for (final Item item: player.getAllEquipped(currentRepairItem)) { + if (((BreakableWeapon) item).isUsed()) { + count++; + } + } + return count; + } + + /** + * Sets subject number of items to repair of current conversation. + * + * @param player + * Player requesting repair. + */ + private void setRepairCount(final Player player) { + currentRepairCount = getWornItemsCount(player); + } + + /** + * Retrieves price of single item repair. * * @return - * First item. + * Unit price. + */ + private Integer getUnitPrice() { + if (currentRepairItem == null) { + logger.warn("Repair item name not set, cannot get unit price."); + return null; + } + String itemNameL = currentRepairItem.toLowerCase(Locale.ENGLISH); + for (String repairable: repairList.keySet()) { + if (repairable.toLowerCase(Locale.ENGLISH).equals(itemNameL)) { + return repairList.get(repairable); + } + } + logger.warn("Item " + currentRepairItem + " not found in repair list, cannot get unit price"); + return null; + } + + /** + * Sets subject fee of current conversation. + */ + private void calculateRepairFee() { + if (currentRepairCount == null) { + logger.warn("Repair count not set, cannot calculate repair fee"); + return; + } + Integer unitPrice = getUnitPrice(); + if (unitPrice != null) { + currentRepairFee = currentRepairCount * unitPrice; + } + } + + /** + * Retrieves the first item name from repair list. + * + * Used when player asks for repair without declaring item name. + * + * @return + * Name of first item in list of items that can be repaired. */ - @SuppressWarnings("unchecked") public String getFirstRepairable() { return repairList.keySet().toArray(new String[] {})[0]; } + + /** + * Overridden to reset subjects at end of conversation. + */ + @Override + public void onGoodbye(final RPEntity attending) { + reset(); + } + + /** + * Overridden to reset subjects at end of conversation. + */ + @Override + public void setCurrentState(ConversationStates state) { + super.setCurrentState(state); + if (ConversationStates.IDLE.equals(state)) { + reset(); + } + } } } diff --git a/src/games/stendhal/server/maps/nalwor/forest/DojoSellerNPC.java b/src/games/stendhal/server/maps/nalwor/forest/DojoSellerNPC.java index 076d2efa43..66549050df 100644 --- a/src/games/stendhal/server/maps/nalwor/forest/DojoSellerNPC.java +++ b/src/games/stendhal/server/maps/nalwor/forest/DojoSellerNPC.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2020 - Arianne * + * Copyright © 2020-2024 - Arianne * *************************************************************************** *************************************************************************** * * @@ -11,7 +11,9 @@ ***************************************************************************/ package games.stendhal.server.maps.nalwor.forest; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import games.stendhal.common.Direction; @@ -25,6 +27,7 @@ import games.stendhal.server.entity.npc.action.SayTextAction; import games.stendhal.server.entity.npc.behaviour.adder.SellerAdder; import games.stendhal.server.entity.npc.behaviour.impl.SellerBehaviour; import games.stendhal.server.entity.npc.condition.PlayerHasItemWithHimCondition; +import games.stendhal.server.entity.npc.shop.ItemShopInventory; import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.maps.nalwor.forest.AssassinRepairerAdder.AssassinRepairer; @@ -35,66 +38,67 @@ import games.stendhal.server.maps.nalwor.forest.AssassinRepairerAdder.AssassinRe */ public class DojoSellerNPC implements ZoneConfigurator { - private static StendhalRPZone dojoZone; + /** Zone where NPC is to be located. */ + private static StendhalRPZone dojo; + /** NPC being configured. */ + private static AssassinRepairer seller; + /** List of items NPC sells. */ + private static ItemShopInventory inventory; - private final String sellerName = "Akutagawa"; - private AssassinRepairer seller; - - private AssassinRepairerAdder repairerAdder; - - private static final Map repairableSellPrices = new LinkedHashMap() {{ - put("training sword", 2100); + /** List of items NPC repairs. */ + private static List repairables = new ArrayList() {{ + add("training sword"); }}; + @Override public void configureZone(final StendhalRPZone zone, final Map attributes) { - dojoZone = zone; - + dojo = zone; + inventory = ShopsList.get().get("dojo", ShopType.ITEM_SELL); initNPC(); - initShop(); - initRepairShop(); - initDialogue(); } + /** + * Entry point for setting up NPC. + */ private void initNPC() { - repairerAdder = new AssassinRepairerAdder(); + initRepairShop(); + initSellShop(); + initDialogue(); - seller = repairerAdder.new AssassinRepairer(sellerName, repairableSellPrices); seller.setEntityClass("samurai2npc"); seller.setIdleDirection(Direction.LEFT); seller.setPosition(37, 80); - - dojoZone.add(seller); + dojo.add(seller); } - private void initDialogue() { - seller.addGreeting("If you're looking for training equipment, you have come to the right place."); - seller.addGoodbye(); - seller.addOffer("See my blackboard for what I sell. I can also #repair any used #'training swords' that you have."); - seller.addJob("I run the assassins' dojo shop where we sell equipment and do #repairs on #'training swords'."); - seller.addQuest("I don't have any task for you to do. I only #fix and sell equipment."); - seller.addHelp("If you want to train in the dojo, I recommend that you buy a #'training sword'."); - seller.addReply("training sword", "My training swords are light and easy to swing. And just because" - + " they are made out of wood, doesn't mean that it won't hurt if you get whacked with one."); + /** + * Sets up seller/repairer NPC and repair items prices. + * + * If players bring their worn training swords they can get them repaired for half the + * price of buying a new one. + */ + private void initRepairShop() { + final Map repairPrices = new LinkedHashMap<>(); + for (String itemName: repairables) { + if (inventory.containsKey(itemName)) { + // repairing is half the cost of buying new + repairPrices.put(itemName, inventory.getPrice(itemName) / 2); + } + } + // initialize NPC repairer + seller = new AssassinRepairerAdder.AssassinRepairer("Akutagawa", repairPrices); + new AssassinRepairerAdder().add(seller); } - private void initShop() { - final Map pricesSell = new LinkedHashMap<>(); - for (final String itemName: repairableSellPrices.keySet()) { - pricesSell.put(itemName, repairableSellPrices.get(itemName)); - } - pricesSell.put("shuriken", 80); - pricesSell.put("fire shuriken", 105); - - final ShopsList shops = ShopsList.get(); - for (final String itemName: pricesSell.keySet()) { - shops.add("dojosell", ShopType.ITEM_SELL, itemName, pricesSell.get(itemName)); - } - + /** + * Sets up item shop and shop sign. + */ + private void initSellShop() { final String rejectedMessage = "Only members of the assassin guild can trade here."; // can only purchase if carrying assassins id - final SellerBehaviour sellerBehaviour = new SellerBehaviour(pricesSell) { + final SellerBehaviour sellerBehaviour = new SellerBehaviour(inventory) { @Override public ChatCondition getTransactionCondition() { return new PlayerHasItemWithHimCondition("assassins id"); @@ -107,37 +111,35 @@ public class DojoSellerNPC implements ZoneConfigurator { }; new SellerAdder().addSeller(seller, sellerBehaviour, false); - final ShopSign shopSign = new ShopSign("dojosell", "Assassins' Dojo Shop", sellerName + " sells the following items", true) { - /** - * Can only view sign if carrying assassins id. - */ + final ShopSign shopSign = new ShopSign("dojo", "Assassins' Dojo Shop", seller.getName() + + " sells the following items", true) { @Override public boolean onUsed(final RPEntity user) { if (user.isEquipped("assassins id")) { return super.onUsed(user); } else { + // can only view sign if carrying assassins id seller.say(rejectedMessage); } - return true; } }; shopSign.setEntityClass("blackboard"); shopSign.setPosition(36, 81); - - dojoZone.add(shopSign); + dojo.add(shopSign); } /** - * If players bring their worn training swords they can get them repaired for half the - * price of buying a new one. + * Sets up NPC's general dialogue. */ - private void initRepairShop() { - final Map repairPrices = new LinkedHashMap<>(); - for (final String itemName: repairableSellPrices.keySet()) { - repairPrices.put(itemName, repairableSellPrices.get(itemName) / 2); - } - - repairerAdder.add(seller, repairPrices); + private void initDialogue() { + seller.addGreeting("If you're looking for training equipment, you have come to the right place."); + seller.addGoodbye(); + seller.addOffer("See my blackboard for what I sell. I can also #repair any used #'training swords' that you have."); + seller.addJob("I run the assassins' dojo shop where we sell equipment and do #repairs on #'training swords'."); + seller.addQuest("I don't have any task for you to do. I only #fix and sell equipment."); + seller.addHelp("If you want to train in the dojo, I recommend that you buy a #'training sword'."); + seller.addReply("training sword", "My training swords are light and easy to swing. And just because" + + " they are made out of wood, doesn't mean that it won't hurt if you get whacked with one."); } } diff --git a/src/games/stendhal/server/maps/quests/AGrandfathersWish.java b/src/games/stendhal/server/maps/quests/AGrandfathersWish.java index 197ca35ff2..9da130023b 100644 --- a/src/games/stendhal/server/maps/quests/AGrandfathersWish.java +++ b/src/games/stendhal/server/maps/quests/AGrandfathersWish.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2022 - Arianne * + * Copyright © 2003-2024 - Arianne * *************************************************************************** *************************************************************************** * * @@ -94,7 +94,7 @@ public class AGrandfathersWish extends AbstractQuest { @Override public String getRegion() { - return Region.DENIRAN; + return Region.DENIRAN_CITY; } @Override diff --git a/src/games/stendhal/server/maps/quests/AbstractQuest.java b/src/games/stendhal/server/maps/quests/AbstractQuest.java index 825320fb4a..e60dd0f2b7 100644 --- a/src/games/stendhal/server/maps/quests/AbstractQuest.java +++ b/src/games/stendhal/server/maps/quests/AbstractQuest.java @@ -15,6 +15,7 @@ package games.stendhal.server.maps.quests; import java.util.ArrayList; import java.util.List; +import games.stendhal.common.MathHelper; import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.entity.npc.NPCList; import games.stendhal.server.entity.player.Player; @@ -159,7 +160,7 @@ public abstract class AbstractQuest implements IQuest { stateIndex = completionsIndexes.first(); } if (stateIndex != null && state.length > stateIndex && !"".equals(state[stateIndex])) { - return Integer.parseInt(state[stateIndex]); + return MathHelper.parseIntDefault(state[stateIndex], completed ? 1 : 0); } } // default is to return 1 if quest is in complete state and 0 otherwise diff --git a/src/games/stendhal/server/maps/quests/AmazonPrincess.java b/src/games/stendhal/server/maps/quests/AmazonPrincess.java index a7f31c3660..c078d034a5 100644 --- a/src/games/stendhal/server/maps/quests/AmazonPrincess.java +++ b/src/games/stendhal/server/maps/quests/AmazonPrincess.java @@ -92,9 +92,6 @@ public class AmazonPrincess implements QuestManuscript { .rewardWith(new PlaySoundAction("kiss-female-01")) .rewardWith(new InflictStatusOnNPCAction("pina colada")); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/Campfire.java b/src/games/stendhal/server/maps/quests/Campfire.java index 8e13da23a1..72b419fa42 100644 --- a/src/games/stendhal/server/maps/quests/Campfire.java +++ b/src/games/stendhal/server/maps/quests/Campfire.java @@ -91,9 +91,6 @@ public class Campfire implements QuestManuscript { .rewardWith(new EquipItemAction("charcoal", 10)) .rewardWith(new EquipRandomItemAction("meat=10;ham=10", false, "Thank you! Here, take [this_these] [number_item] and charcoal!")); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/CleanAthorsUnderground.java b/src/games/stendhal/server/maps/quests/CleanAthorsUnderground.java index 0114e67723..cd47a1f1df 100644 --- a/src/games/stendhal/server/maps/quests/CleanAthorsUnderground.java +++ b/src/games/stendhal/server/maps/quests/CleanAthorsUnderground.java @@ -99,9 +99,6 @@ public class CleanAthorsUnderground implements QuestManuscript { .rewardWith(new IncreaseKarmaAction(10.0)) .rewardWith(new EquipItemAction("greater potion", 10)); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/CloakCollector.java b/src/games/stendhal/server/maps/quests/CloakCollector.java index d33b0fd42d..f3cefa45f2 100644 --- a/src/games/stendhal/server/maps/quests/CloakCollector.java +++ b/src/games/stendhal/server/maps/quests/CloakCollector.java @@ -85,6 +85,8 @@ public class CloakCollector extends AbstractQuest implements BringListOfItemsQue "Cloaks Collector", "Josephine wants cloaks in many colors.", false); + + npcs.get("Josephine").addKnownChatOptions("cloaks"); } private void step_1() { diff --git a/src/games/stendhal/server/maps/quests/CoalForHaunchy.java b/src/games/stendhal/server/maps/quests/CoalForHaunchy.java index 7c061659d7..e58ff46667 100644 --- a/src/games/stendhal/server/maps/quests/CoalForHaunchy.java +++ b/src/games/stendhal/server/maps/quests/CoalForHaunchy.java @@ -97,9 +97,6 @@ public class CoalForHaunchy implements QuestManuscript { .rewardWith(new EquipRandomAmountOfItemAction("grilled steak", 1, 4, 1, "Thank you! Take [this_these] [number_item] from my grill!")); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/CollectEnemyData.java b/src/games/stendhal/server/maps/quests/CollectEnemyData.java index 6ec4444c7d..9cf7392d62 100644 --- a/src/games/stendhal/server/maps/quests/CollectEnemyData.java +++ b/src/games/stendhal/server/maps/quests/CollectEnemyData.java @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2020-2024 - Faiumoni e. V. * + * Copyright © 2020-2025 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -376,7 +376,7 @@ public class CollectEnemyData extends AbstractQuest { new ChatAction() { @Override public void fire(final Player player, final Sentence sentence, final EventRaiser raiser) { - npc.say("Thanks so much for you help. Now I have all the information I need to complete my #bestiary." + npc.say("Thanks so much for your help. Now I have all the information I need to complete my #bestiary." + " If you would like one of your own, I can sell you one."); player.addKarma(200.0); }})); diff --git a/src/games/stendhal/server/maps/quests/DailyItemQuest.java b/src/games/stendhal/server/maps/quests/DailyItemQuest.java index 2996212220..52cfa843ed 100644 --- a/src/games/stendhal/server/maps/quests/DailyItemQuest.java +++ b/src/games/stendhal/server/maps/quests/DailyItemQuest.java @@ -19,9 +19,11 @@ import java.util.List; import java.util.Map; import games.stendhal.common.grammar.Grammar; +import games.stendhal.common.parser.Sentence; import games.stendhal.server.entity.npc.ChatAction; import games.stendhal.server.entity.npc.ConversationPhrases; import games.stendhal.server.entity.npc.ConversationStates; +import games.stendhal.server.entity.npc.EventRaiser; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.action.DropRecordedItemAction; import games.stendhal.server.entity.npc.action.IncreaseKarmaAction; @@ -407,9 +409,20 @@ public class DailyItemQuest extends AbstractQuest { new NotCondition(new PlayerHasRecordedItemWithHimCondition(QUEST_SLOT,0))), ConversationStates.ATTENDING, null, - new SayRequiredItemAction(QUEST_SLOT,0,"You didn't fetch [item]" - + " yet. Go and get it and say #complete only once you're done.")); - + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser raiser) { + if ("sandwich".equals(player.getRequiredItemName(QUEST_SLOT, 0)) + && player.getNumberOfEquipped("sandwich") >= player.getRequiredItemQuantity(QUEST_SLOT, 0)) { + // experimental sandwich was detected in condition + npc.say("There is something strange about those sandwiches. Come back when you have" + + " some that are unmodified."); + } else { + new SayRequiredItemAction(QUEST_SLOT,0,"You didn't fetch [item]" + + " yet. Go and get it and say #complete only once you're done.").fire(player, sentence, raiser); + } + } + }); } private void abortQuest() { @@ -463,7 +476,7 @@ public class DailyItemQuest extends AbstractQuest { if (player.hasQuest(QUEST_SLOT) && !player.isQuestCompleted(QUEST_SLOT)) { String questItem = player.getRequiredItemName(QUEST_SLOT,0); int amount = player.getRequiredItemQuantity(QUEST_SLOT,0); - if (!player.isEquipped(questItem, amount)) { + if (!player.isSubmittableEquipped(questItem, amount)) { res.add("I have been asked to fetch " + Grammar.quantityplnoun(amount, questItem, "a") + " to help Ados. I haven't got " + Grammar.itthem(amount) + " yet."); } else { diff --git a/src/games/stendhal/server/maps/quests/EggsForMarianne.java b/src/games/stendhal/server/maps/quests/EggsForMarianne.java index e8e8eb8c44..449d57fe32 100644 --- a/src/games/stendhal/server/maps/quests/EggsForMarianne.java +++ b/src/games/stendhal/server/maps/quests/EggsForMarianne.java @@ -58,7 +58,7 @@ public class EggsForMarianne implements QuestManuscript { .internalName("eggs_for_marianne") .repeatableAfterMinutes(60) .minLevel(0) - .region(Region.DENIRAN) + .region(Region.DENIRAN_CITY) .questGiverNpc("Marianne"); quest.history() @@ -97,9 +97,6 @@ public class EggsForMarianne implements QuestManuscript { .rewardWith(new IncreaseKarmaAction(50)) .rewardWith(new EquipRandomItemAction("pansy=12;daisies=12", false, "Thank you! Here, take [this_these] [number_item]!")); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/FindRatChildren.java b/src/games/stendhal/server/maps/quests/FindRatChildren.java index a64cb2d806..eafddd8add 100644 --- a/src/games/stendhal/server/maps/quests/FindRatChildren.java +++ b/src/games/stendhal/server/maps/quests/FindRatChildren.java @@ -187,74 +187,82 @@ public class FindRatChildren extends AbstractQuest { ConversationStates.QUESTION_1, "If you found any of my #children, please tell me their name.", null); - for(final String name : NEEDED_KIDS) { - npc.add(ConversationStates.QUESTION_1, name, null, - ConversationStates.QUESTION_1, null, + for (final String name : NEEDED_KIDS) { + npc.add( + ConversationStates.QUESTION_1, + name, + null, + ConversationStates.QUESTION_1, + null, new ChatAction() { - @Override - public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { - final String npcQuestText = player.getQuest(QUEST_SLOT).toLowerCase(); - final String[] npcDoneText = npcQuestText.split(":"); - final String lookingStr; - final String saidStr; - if (npcDoneText.length > 1) { - lookingStr = npcDoneText[0]; - saidStr = npcDoneText[1]; - } else { - // compatibility with broken quests - should never happen - logger.warn("Player " + player.getTitle() + " found with find_rat_kids quest slot in state " + player.getQuest(QUEST_SLOT) + " - now setting this to done."); - player.setQuest(QUEST_SLOT, "done"); - npc.say("Sorry, it looks like you have already found them after all. I got confused."); - player.notifyWorldAboutChanges(); - npc.setCurrentState(ConversationStates.ATTENDING); - return; - } + @Override + public void fire(final Player player, final Sentence sentence, final EventRaiser npc) { + final String npcQuestText = player.getQuest(QUEST_SLOT).toLowerCase(); + final String[] npcDoneText = npcQuestText.split(":"); + final String lookingStr; + final String saidStr; + if (npcDoneText.length > 1) { + lookingStr = npcDoneText[0]; + saidStr = npcDoneText[1]; + } else { + // compatibility with broken quests - should never happen + logger.warn("Player " + player.getTitle() + " found with find_rat_kids quest slot in state " + player.getQuest(QUEST_SLOT) + " - now setting this to done."); + player.setQuest(QUEST_SLOT, "done"); + npc.say("Sorry, it looks like you have already found them after all. I got confused."); + player.notifyWorldAboutChanges(); + npc.setCurrentState(ConversationStates.ATTENDING); + return; + } - final List looking = Arrays.asList(lookingStr.split(";")); - final List said = Arrays.asList(saidStr.split(";")); - String reply = ""; - List missing = missingNames(player); - final boolean isMissing = missing.contains(name); + final List looking = Arrays.asList(lookingStr.split(";")); + final List said = Arrays.asList(saidStr.split(";")); + String reply = ""; + List missing = missingNames(player); + final boolean isMissing = missing.contains(name); - if (isMissing && looking.contains(name) && !said.contains(name)) { - // we haven't said the name yet so we add it to the list - player.setQuest(QUEST_SLOT, lookingStr - + ":" + saidStr + ";" + name); - reply = "Thank you."; - } else if (!looking.contains(name)) { - // we have said it was a valid name but haven't seen them - reply = "I don't think you actually checked if they were ok."; - } else if (!isMissing && said.contains(name)) { - // we have said the name so we are stupid! - reply = "Yes you told me that they were ok already, thanks."; - } else { - assert false; - } + if (isMissing && looking.contains(name) && !said.contains(name)) { + // we haven't said the name yet so we add it to the list + player.setQuest(QUEST_SLOT, lookingStr + + ":" + saidStr + ";" + name); + reply = "Thank you."; + } else if (!looking.contains(name)) { + // we have said it was a valid name but haven't seen them + reply = "I don't think you actually checked if they were ok."; + } else if (!isMissing && said.contains(name)) { + // we have said the name so we are stupid! + reply = "Yes you told me that they were ok already, thanks."; + } else { + assert false; + } - // we may have changed the missing list - missing = missingNames(player); + // we may have changed the missing list + missing = missingNames(player); - if (!missing.isEmpty()) { - reply += " If you have seen any of my other children, please tell me who."; - npc.say(reply); - } else { - player.addXP(5000); - player.addKarma(15); - reply += " Now that I know my kids are safe, I can set my mind at rest."; - npc.say(reply); - player.setQuest(QUEST_SLOT, "done;" + System.currentTimeMillis()); - player.notifyWorldAboutChanges(); - npc.setCurrentState(ConversationStates.ATTENDING); - } - } - }); + if (!missing.isEmpty()) { + reply += " If you have seen any of my other children, please tell me who."; + npc.say(reply); + } else { + player.addXP(5000); + player.addKarma(15); + reply += " Now that I know my kids are safe, I can set my mind at rest."; + npc.say(reply); + player.setQuest(QUEST_SLOT, "done;" + System.currentTimeMillis()); + player.notifyWorldAboutChanges(); + npc.setCurrentState(ConversationStates.ATTENDING); + } + } + }); } final List triggers = new ArrayList(); triggers.add(ConversationPhrases.NO_EXPRESSION); triggers.addAll(ConversationPhrases.GOODBYE_MESSAGES); - npc.add(ConversationStates.QUESTION_1, triggers, null, - ConversationStates.IDLE, "No problem, come back later.", null); + npc.add( + ConversationStates.QUESTION_1, + triggers, + null, + ConversationStates.IDLE,"No problem, come back later.", + null); // player says something which isn't in the needed kids list. npc.add( diff --git a/src/games/stendhal/server/maps/quests/HouseBuying.java b/src/games/stendhal/server/maps/quests/HouseBuying.java index efd5f2aa62..1ee06e172b 100644 --- a/src/games/stendhal/server/maps/quests/HouseBuying.java +++ b/src/games/stendhal/server/maps/quests/HouseBuying.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -16,6 +16,7 @@ import java.util.LinkedList; import games.stendhal.server.entity.player.Player; import games.stendhal.server.maps.quests.houses.HouseBuyingMain; +import games.stendhal.server.maps.quests.houses.HouseTax; public class HouseBuying extends AbstractQuest { private static final String QUEST_SLOT = "house"; @@ -62,4 +63,14 @@ public class HouseBuying extends AbstractQuest { public String getNPCName() { return "Barrett Holmes"; } + + /** + * Retrieves house tax manager. + * + * @return + * {@code HouseTax} instance. + */ + public HouseTax getHouseTax() { + return quest.getHouseTax(); + } } diff --git a/src/games/stendhal/server/maps/quests/KillEnemyArmy.java b/src/games/stendhal/server/maps/quests/KillEnemyArmy.java index 77bee6a39d..be6b89400e 100644 --- a/src/games/stendhal/server/maps/quests/KillEnemyArmy.java +++ b/src/games/stendhal/server/maps/quests/KillEnemyArmy.java @@ -508,7 +508,7 @@ public class KillEnemyArmy extends AbstractQuest { fillQuestInfo( "Kill Enemy Army", "Despot Halb Errvl has a vendetta against any army who opposes him.", - true); + true, 3); step_1(); } @@ -596,16 +596,4 @@ public class KillEnemyArmy extends AbstractQuest { public String getRegion() { return Region.SEMOS_SURROUNDS; } - - @Override - public int getCompletedCount(final Player player) { - int count = 0; - if (player.hasQuest(QUEST_SLOT)) { - final String[] state = player.getQuest(QUEST_SLOT).split(";"); - if (state.length > 0) { - count = Integer.parseInt(state[state.length-1]); - } - } - return count; - } } diff --git a/src/games/stendhal/server/maps/quests/KillGnomes.java b/src/games/stendhal/server/maps/quests/KillGnomes.java index 5e4468c033..b9e2f72bc4 100644 --- a/src/games/stendhal/server/maps/quests/KillGnomes.java +++ b/src/games/stendhal/server/maps/quests/KillGnomes.java @@ -92,9 +92,6 @@ public class KillGnomes implements QuestManuscript { .rewardWith(new IncreaseXPAction(100)) .rewardWith(new EquipItemAction("potion", 3)); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/LuckyFourLeafClover.java b/src/games/stendhal/server/maps/quests/LuckyFourLeafClover.java new file mode 100644 index 0000000000..be2f84d533 --- /dev/null +++ b/src/games/stendhal/server/maps/quests/LuckyFourLeafClover.java @@ -0,0 +1,277 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.maps.quests; + +import java.util.LinkedList; +import java.util.List; + +import games.stendhal.common.MathHelper; +import games.stendhal.common.grammar.Grammar; +import games.stendhal.common.parser.Sentence; +import games.stendhal.server.core.engine.SingletonRepository; +import games.stendhal.server.core.engine.StendhalRPZone; +import games.stendhal.server.entity.npc.ChatAction; +import games.stendhal.server.entity.npc.ChatCondition; +import games.stendhal.server.entity.npc.ConversationPhrases; +import games.stendhal.server.entity.npc.ConversationStates; +import games.stendhal.server.entity.npc.EventRaiser; +import games.stendhal.server.entity.npc.SpeakerNPC; +import games.stendhal.server.entity.npc.action.DecreaseKarmaAction; +import games.stendhal.server.entity.npc.action.DropItemAction; +import games.stendhal.server.entity.npc.action.EquipItemAction; +import games.stendhal.server.entity.npc.action.IncreaseKarmaAction; +import games.stendhal.server.entity.npc.action.IncreaseXPAction; +import games.stendhal.server.entity.npc.action.IncrementQuestAction; +import games.stendhal.server.entity.npc.action.MultipleActions; +import games.stendhal.server.entity.npc.action.SetQuestAction; +import games.stendhal.server.entity.npc.condition.AndCondition; +import games.stendhal.server.entity.npc.condition.NotCondition; +import games.stendhal.server.entity.npc.condition.PlayerHasItemWithHimCondition; +import games.stendhal.server.entity.npc.condition.QuestActiveCondition; +import games.stendhal.server.entity.npc.condition.QuestNotActiveCondition; +import games.stendhal.server.entity.npc.condition.QuestStateGreaterThanCondition; +import games.stendhal.server.entity.player.Player; +import games.stendhal.server.maps.Region; +import games.stendhal.server.maps.deniran.cityoutside.CloverHunterNPC; +import games.stendhal.server.util.ResetSpeakerNPC; + + +/** + * Rewards: + * - 20 greater potions + * - 1,000 XP + * - karma + * - 10,000 ATK XP (first time only) + * - 10,000 DEF XP (first time only) + * - +150 max HP (first time only) + * + * TODO: + * - maybe convert to manuscript + */ +public class LuckyFourLeafClover extends AbstractQuest { + + private static String QUEST_SLOT = "lucky_four_leaf_clover"; + + + @Override + public String getSlotName() { + return QUEST_SLOT; + } + + @Override + public String getName() { + return "Lucky Four-Leaf Clover"; + } + + @Override + public String getNPCName() { + return "Maple"; + } + + @Override + public String getRegion() { + return Region.DENIRAN_CITY; + } + + @Override + public List getHistory(Player player) { + List res = new LinkedList<>(); + + String npcName = getNPCName(); + String state = player.getQuest(QUEST_SLOT, 0); + int count = MathHelper.parseIntDefault(player.getQuest(QUEST_SLOT, 1), 0); + boolean hasClover = player.isEquipped("four-leaf clover"); + + res.add(npcName + " asked me to find a rare four-leaf clover."); + if ("rejected".equals(state)) { + res.add("No thanks, I have all the luck I need."); + } else if ("start".equals(state)) { + if (hasClover) { + res.add("I found it! How incredibly lucky!"); + } else { + res.add("I don't have any yet."); + } + } else if ("done".equals(state)) { + res.add("Fortune shone on me. I found a four-leaf clover."); + } + if (count > 0) { + res.add("I have found and given " + count + " " + Grammar.plnoun(count, "flour-leaf clover") + + " to " + npcName + "."); + } + return res; + } + + @Override + public void addToWorld() { + fillQuestInfo(getName(), getNPCName() + " is looking for a rare four-leaf clover", true, 1); + requestStep(); + bringStep(); + } + + private void requestStep() { + SpeakerNPC npc = SingletonRepository.getNPCList().get(getNPCName()); + + ChatCondition isRepeat = new QuestStateGreaterThanCondition(QUEST_SLOT, 1, 0); + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.QUEST_MESSAGES, + new QuestActiveCondition(QUEST_SLOT), + ConversationStates.ATTENDING, + "I already asked you to find me a four-leaf clover.", + null); + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.QUEST_MESSAGES, + new AndCondition( + new QuestNotActiveCondition(QUEST_SLOT), + new NotCondition(isRepeat)), + ConversationStates.QUEST_OFFERED, + "I'm looking for a rare four-leaf clover. Do you want to help me find one?", + null); + + npc.add( + ConversationStates.ATTENDING, + ConversationPhrases.QUEST_MESSAGES, + new AndCondition( + new QuestNotActiveCondition(QUEST_SLOT), + isRepeat), + ConversationStates.QUEST_OFFERED, + "I want another four-leaf clover. Do you want to help again?", + null); + + npc.add( + ConversationStates.QUEST_OFFERED, + ConversationPhrases.NO_MESSAGES, + null, + ConversationStates.IDLE, + "That's okay, I can find one on my own anyway.", + new MultipleActions( + new DecreaseKarmaAction(20), + new SetQuestAction(QUEST_SLOT, 0, "rejected"))); + + npc.add( + ConversationStates.QUEST_OFFERED, + ConversationPhrases.YES_MESSAGES, + null, + ConversationStates.ATTENDING, + "Great! Bring it here if you find one. And if you want some tips on #clover hunting, just" + + " ask.", + new SetQuestAction(QUEST_SLOT, 0, "start")); + } + + private void bringStep() { + SpeakerNPC npc = SingletonRepository.getNPCList().get(getNPCName()); + + ChatCondition hasClover = new PlayerHasItemWithHimCondition("four-leaf clover"); + ChatCondition canGiveClover = new AndCondition( + new QuestActiveCondition(QUEST_SLOT), + hasClover); + ChatCondition isRepeat = new QuestStateGreaterThanCondition(QUEST_SLOT, 1, 0); + + ChatAction baseReward = new MultipleActions( + new SetQuestAction(QUEST_SLOT, 0, "done"), + new IncrementQuestAction(QUEST_SLOT, 1, 1), + new DropItemAction("four-leaf clover"), + new EquipItemAction("greater potion", 20), + new IncreaseXPAction(1000), + new IncreaseKarmaAction(50)); + ChatAction firstReward = new MultipleActions( + baseReward, + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser npc) { + player.setAtkXP(player.getAtkXP() + 10000); + player.setDefXP(player.getDefXP() + 10000); + player.setBaseHP(player.getBaseHP() + 150); + } + }); + + // TODO: add condition for saying "done" when not carrying clover + + npc.add( + ConversationStates.IDLE, + ConversationPhrases.GREETING_MESSAGES, + canGiveClover, + ConversationStates.QUEST_ITEM_BROUGHT, + "Oh my! You found a clover. It has four leaves! Can I have it? Please, please, please ...", + null); + + npc.add( + ConversationStates.ATTENDING, + //Arrays.asList("clover", "four-leaf clover"), // will interfere with standard response + ConversationPhrases.FINISH_MESSAGES, + canGiveClover, + ConversationStates.QUEST_ITEM_BROUGHT, + "Oh my! You found a clover. It has four leaves! Can I have it? Please, please, please ...", + null); + + npc.add( + ConversationStates.QUEST_ITEM_BROUGHT, + ConversationPhrases.NO_MESSAGES, + null, + ConversationStates.IDLE, + "Hmph! I can find a better one anyway.", + // denying her the clover costs karma + new DecreaseKarmaAction(25)); + + // player dropped clover + npc.add( + ConversationStates.QUEST_ITEM_BROUGHT, + ConversationPhrases.YES_MESSAGES, + new NotCondition(hasClover), + ConversationStates.IDLE, + "What are you trying to pull!? I saw you drop it.", + // denying her the clover costs karma + new DecreaseKarmaAction(25)); + + npc.add( + ConversationStates.QUEST_ITEM_BROUGHT, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + hasClover, + new NotCondition(isRepeat)), + ConversationStates.IDLE, + "Thanks so much! I'm going to add this to my collection. Here are some potions to show my" + + " gratitude.", + firstReward); + + npc.add( + ConversationStates.QUEST_ITEM_BROUGHT, + ConversationPhrases.YES_MESSAGES, + new AndCondition( + hasClover, + isRepeat), + ConversationStates.IDLE, + "Thanks again! I'm going to have so much luck! Here are some more potions to show my" + + " gratitude.", + baseReward); + } + + @Override + public boolean removeFromWorld() { + // TODO: should clover spawner be initialized & removed with quest instead of zone configurator? + SpeakerNPC npc = SingletonRepository.getNPCList().get(getNPCName()); + StendhalRPZone npcZone = npc.getZone(); + if (npcZone != null) { + return ResetSpeakerNPC.reload(new CloverHunterNPC(), npcZone, getNPCName()); + } + return ResetSpeakerNPC.reload(new CloverHunterNPC(), getNPCName()); + } + + @Override + public boolean isRepeatable(Player player) { + // always repeatable + return true; + } +} diff --git a/src/games/stendhal/server/maps/quests/PizzaDelivery.java b/src/games/stendhal/server/maps/quests/PizzaDelivery.java index 15ef5c555b..c42c3e5e43 100644 --- a/src/games/stendhal/server/maps/quests/PizzaDelivery.java +++ b/src/games/stendhal/server/maps/quests/PizzaDelivery.java @@ -60,6 +60,9 @@ import games.stendhal.server.util.ResetSpeakerNPC; */ public class PizzaDelivery implements QuestManuscript { + private static final String questSlot = "pizza_delivery"; + + @Override public DeliverItemQuestBuilder story() { DeliverItemQuestBuilder quest = new DeliverItemQuestBuilder(); @@ -68,7 +71,7 @@ public class PizzaDelivery implements QuestManuscript { quest.info() .name("Pizza Delivery") .description("Leander's pizza business is doing so well that he now recruits delivery boys and girls.") - .internalName("pizza_delivery") + .internalName(questSlot) .repeatableAfterMinutes(0) .minLevel(0) .region(Region.SEMOS_CITY) @@ -84,7 +87,8 @@ public class PizzaDelivery implements QuestManuscript { .whenInTime("If I hurry, I might still get there, with the pizza hot.") .whenOutOfTime("The pizza has already gone cold.") .whenQuestWasCompleted("I delivered the last pizza Leander gave to me.") - .whenQuestCanBeRepeated("But I'd bet, Leander has more orders."); + .whenQuestCanBeRepeated("But I'd bet, Leander has more orders.") + .whenEarlyCompletionsShown("I have delivered [count] hot [pizza]."); quest.offer() @@ -95,13 +99,15 @@ public class PizzaDelivery implements QuestManuscript { .respondToReject("Too bad. I hope my daughter #Sally will soon come back from her camp to help me with the deliveries.") .remind("You still have to deliver a pizza to [customerName], and hurry!") .respondIfLastQuestFailed("I see you failed to deliver the pizza to [customerName] in time. Are you sure you will be more reliable this time?") - .respondIfInventoryIsFull("Come back when you have space to carry the pizza!"); + .respondIfInventoryIsFull("Come back when you have space to carry the pizza!") + .respondToAnotherIfLostItem("You lost the [item]? Well, it will cost you [charge] money. Do you want another?"); quest.task() .itemName("pizza") .itemDescription("You see a [flavor].") - .outfitUniform(new Outfit(null, Integer.valueOf(990), null, null, null, null, null, null, null)); + .outfitUniform(new Outfit(null, Integer.valueOf(990), null, null, null, null, null, null, null)) + .chargeForLostItem(500); // Don't add Sally here, as it would conflict with Leander telling // about his daughter. @@ -289,13 +295,11 @@ public class PizzaDelivery implements QuestManuscript { .respondToMissingItem("A pizza? Where?") .npcStatusEffect("pizza"); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } + // TODO: Remove program logic from manuscript classes public boolean removeFromWorld() { boolean res = ResetSpeakerNPC.reload(new ChefNPC(), "Leander") && ResetSpeakerNPC.reload(new games.stendhal.server.maps.ados.rock.WeaponsCollectorNPC(), "Balduin") @@ -353,5 +357,4 @@ public class PizzaDelivery implements QuestManuscript { } return res; } - } diff --git a/src/games/stendhal/server/maps/quests/SuppliesForPhalk.java b/src/games/stendhal/server/maps/quests/SuppliesForPhalk.java index f3f6bf5efe..3923278344 100644 --- a/src/games/stendhal/server/maps/quests/SuppliesForPhalk.java +++ b/src/games/stendhal/server/maps/quests/SuppliesForPhalk.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -139,6 +139,13 @@ import games.stendhal.server.maps.Region; // the extra parts in the quest state are for wrvil and mrotho not to give them cloaks and armor twice actions.add(new SetQuestAndModifyKarmaAction(QUEST_SLOT, "clothes;none;none", 2.0)); actions.add(new InflictStatusOnNPCAction("sandwich")); + actions.add(new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser raiser) { + // workaround because "clothes" is not parsed from NPC dialogue + npc.forceWordsInCurrentConversation("clothes"); + } + }); npc.add(ConversationStates.QUEST_ITEM_QUESTION, ConversationPhrases.YES_MESSAGES, new AndCondition( @@ -436,7 +443,13 @@ import games.stendhal.server.maps.Region; new QuestInStateCondition(QUEST_SLOT, 0, "clothes"), ConversationStates.ATTENDING, "I am waiting for you to bring me new #clothes from Wrvil and Mrotho.", - null); + new ChatAction() { + @Override + public void fire(Player player, Sentence sentence, EventRaiser raiser) { + // workaround because "clothes" is not parsed from NPC dialogue + npc.forceWordsInCurrentConversation("clothes"); + } + }); } @Override diff --git a/src/games/stendhal/server/maps/quests/TrapsForKlaas.java b/src/games/stendhal/server/maps/quests/TrapsForKlaas.java index 768fa078ec..fcaee6c589 100644 --- a/src/games/stendhal/server/maps/quests/TrapsForKlaas.java +++ b/src/games/stendhal/server/maps/quests/TrapsForKlaas.java @@ -90,14 +90,16 @@ public class TrapsForKlaas implements QuestManuscript { .whenQuestWasRejected("I do not care about dealing with rodents.") .whenQuestWasAccepted("I promised to gather 20 rodent traps and bring them to Klaas.") .whenTaskWasCompleted("I got enough traps.") - .whenQuestWasCompleted("I gave the rodent traps to Klaas. I got some experience and antidotes.") + .whenQuestWasCompleted("I gave the rodent traps to Klaas. I got some experience and antidotes." + + " He also mentioned an apothecary and said I should speak to the soldiers guarding Ados" + + " City.") .whenQuestCanBeRepeated("I should check if Klaas needs my help again."); quest.offer() .respondToRequest("The rats down here have been getting into the food storage. Would you help me rid us of the varmints?") .respondToUnrepeatableRequest("Thanks for the traps. Now the food will be safe. But I may need your help again soon.") .respondToRepeatedRequest("The rats down here have been getting into the food storage. Would you help me rid us of the varmints?") - .respondToAccept("Thanks, I need you to bring me bring me 20 #rodent #traps. Please hurry! We can't afford to lose anymore food.") + .respondToAccept("Thanks, I need you to bring me 20 #rodent #traps. Please hurry! We can't afford to lose anymore food.") .respondToReject("Don't waste my time. I've got to protect the cargo.") .rejectionKarmaPenalty(5.0) .remind("I believe, I already asked you to get me 20 rodent traps."); @@ -155,9 +157,6 @@ public class TrapsForKlaas implements QuestManuscript { "You lost the note? Well, I could write another one. But it doesn't look like you have room to carry it.", null); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } diff --git a/src/games/stendhal/server/maps/quests/UltimateCollector.java b/src/games/stendhal/server/maps/quests/UltimateCollector.java index 63124a7d7f..b5ba911e90 100644 --- a/src/games/stendhal/server/maps/quests/UltimateCollector.java +++ b/src/games/stendhal/server/maps/quests/UltimateCollector.java @@ -49,6 +49,7 @@ import games.stendhal.server.entity.npc.condition.QuestCompletedCondition; import games.stendhal.server.entity.npc.condition.QuestNotCompletedCondition; import games.stendhal.server.entity.npc.condition.QuestNotStartedCondition; import games.stendhal.server.entity.npc.condition.TimePassedCondition; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.player.Player; import games.stendhal.server.events.SoundEvent; import games.stendhal.server.maps.Region; @@ -402,7 +403,8 @@ public class UltimateCollector extends AbstractQuest { private void replaceLRSwords() { final SpeakerNPC npc = npcs.get("Balduin"); - final Map prices = SingletonRepository.getShopsList().get("twohandswords"); + final Map prices = SingletonRepository.getShopsList().get("twohandswords", + ShopType.ITEM_SELL); final EntityManager em = SingletonRepository.getEntityManager(); class ReplaceSwordAction implements ChatAction { diff --git a/src/games/stendhal/server/maps/quests/UnicornHornsForZelan.java b/src/games/stendhal/server/maps/quests/UnicornHornsForZelan.java index d51af46307..b382a37293 100644 --- a/src/games/stendhal/server/maps/quests/UnicornHornsForZelan.java +++ b/src/games/stendhal/server/maps/quests/UnicornHornsForZelan.java @@ -76,7 +76,7 @@ public class UnicornHornsForZelan implements QuestManuscript { + " surrounding Atlantis. If you are a brave sort I could" + " really use some help gathering unicorn horns." + " Will you help me?") - .respondToAccept("Great! Be careful out there lots of large" + .respondToAccept("Great! I need 10 unicorn horns. Be careful, out there are lots of large" + " monsters, and those centaurs are really nasty.") .respondToReject("Thats ok, I will find someone else to help me.") .rejectionKarmaPenalty(10.0) @@ -99,9 +99,6 @@ public class UnicornHornsForZelan implements QuestManuscript { .rewardWith(new EquipItemAction("soup", 3)) .rewardWith(new EquipItemAction("money", 20000)); - // completions count is stored in 3rd index of quest slot - quest.setCompletionsIndexes(2); - return quest; } } diff --git a/src/games/stendhal/server/maps/quests/houses/HouseBuyingMain.java b/src/games/stendhal/server/maps/quests/houses/HouseBuyingMain.java index e9616591da..d0726596c0 100644 --- a/src/games/stendhal/server/maps/quests/houses/HouseBuyingMain.java +++ b/src/games/stendhal/server/maps/quests/houses/HouseBuyingMain.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -90,7 +90,7 @@ public class HouseBuyingMain { if(playerHousePortal!=null) { int unpaidPeriods = houseTax.getUnpaidTaxPeriods(player); if (unpaidPeriods>0) { - hist.add("I owe " + Grammar.quantityplnoun(unpaidPeriods, "month", "one") + " worth of tax."); + hist.add("I owe " + Grammar.quantityplnounPossessive(unpaidPeriods, "month", "one") + " worth of taxes."); } else { hist.add("I am up to date with my house tax payments."); } @@ -119,6 +119,17 @@ public class HouseBuyingMain { } public boolean isCompleted(final Player player) { - return HouseUtilities.getPlayersHouse(player)!=null; + return HouseUtilities.getPlayersHouse(player) != null + && houseTax.getUnpaidTaxPeriods(player) == 0; + } + + /** + * Retrieves house tax manager. + * + * @return + * {@code HouseTax} instance. + */ + public HouseTax getHouseTax() { + return houseTax; } } diff --git a/src/games/stendhal/server/maps/quests/houses/HouseTax.java b/src/games/stendhal/server/maps/quests/houses/HouseTax.java index 4e0cdddd71..e8e6c2fe07 100644 --- a/src/games/stendhal/server/maps/quests/houses/HouseTax.java +++ b/src/games/stendhal/server/maps/quests/houses/HouseTax.java @@ -48,13 +48,13 @@ import marauroa.server.game.db.DAORegister; /** * House tax, and confiscation of houses. */ -class HouseTax implements TurnListener { +public class HouseTax implements TurnListener { /** The base amount of tax per month. */ protected static final int BASE_TAX = 1000; private static final Logger logger = Logger.getLogger(HouseTax.class); /** How often the tax should be paid. Time in seconds. */ - private static final int TAX_PAYMENT_PERIOD = 30 * TimeUtil.SECONDS_IN_DAY; + public static final int TAX_PAYMENT_PERIOD = 30 * TimeUtil.SECONDS_IN_DAY; /** How often the payments should be checked. Time in seconds */ private static final int TAX_CHECKING_PERIOD = TimeUtil.SECONDS_IN_DAY; /** How many tax payments can be unpaid. Any more and the house will be confiscated */ @@ -67,7 +67,7 @@ class HouseTax implements TurnListener { private long previouslyChecked = 0; - public HouseTax() { + HouseTax() { setupTaxman(); SingletonRepository.getTurnNotifier().notifyInSeconds(TAX_CHECKING_PERIOD, this); } @@ -88,7 +88,7 @@ class HouseTax implements TurnListener { * @param periods the number of months the player has to pay at once * @return the amount of debt */ - private int getTaxDebt(final int periods) { + public int getTaxDebt(final int periods) { int debt = 0; for (int i = 0; i < periods; i++) { @@ -104,7 +104,7 @@ class HouseTax implements TurnListener { * @param player the player to be checked * @return number of periods */ - protected int getUnpaidTaxPeriods(final Player player) { + public int getUnpaidTaxPeriods(final Player player) { final HousePortal portal = HouseUtilities.getPlayersHouse(player); int payments = 0; @@ -120,7 +120,7 @@ class HouseTax implements TurnListener { * @param portal the portal to be checked * @return number of periods */ - private int getUnpaidTaxPeriods(final HousePortal portal) { + public int getUnpaidTaxPeriods(final HousePortal portal) { final int timeDiffSeconds = (int) ((System.currentTimeMillis() - portal.getExpireTime()) / 1000); return Math.max(0, timeDiffSeconds / TAX_PAYMENT_PERIOD); } diff --git a/src/games/stendhal/server/maps/quests/houses/HouseUtilities.java b/src/games/stendhal/server/maps/quests/houses/HouseUtilities.java index 61847ef73e..6f47425253 100644 --- a/src/games/stendhal/server/maps/quests/houses/HouseUtilities.java +++ b/src/games/stendhal/server/maps/quests/houses/HouseUtilities.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -58,7 +58,7 @@ public class HouseUtilities { * @return portal to the house owned by the player, or null * if he does not own one. */ - protected static HousePortal getPlayersHouse(final Player player) { + public static HousePortal getPlayersHouse(final Player player) { if (player.hasQuest(HOUSE_QUEST_SLOT)) { final String claimedHouse = player.getQuest(HOUSE_QUEST_SLOT); @@ -114,6 +114,23 @@ public class HouseUtilities { return null; } + /** + * Find a portal corresponding to a house ID. + * + * @param doorId + * House portal ID. + * @return + * The portal to the house or {@code null} if no such house exists. + */ + public static HousePortal getHousePortal(final String doorId) { + for (final HousePortal portal: getHousePortals()) { + if (doorId.equals(portal.getDoorId())) { + return portal; + } + } + return null; + } + /** * Get a list of all house portals available to players. * diff --git a/src/games/stendhal/server/maps/quests/revivalweeks/FishermansDaughterSellingNPC.java b/src/games/stendhal/server/maps/quests/revivalweeks/FishermansDaughterSellingNPC.java index c54c5c245c..70d5d77c94 100644 --- a/src/games/stendhal/server/maps/quests/revivalweeks/FishermansDaughterSellingNPC.java +++ b/src/games/stendhal/server/maps/quests/revivalweeks/FishermansDaughterSellingNPC.java @@ -22,6 +22,7 @@ import games.stendhal.server.entity.npc.NPCList; import games.stendhal.server.entity.npc.SpeakerNPC; import games.stendhal.server.entity.npc.behaviour.adder.SellerAdder; import games.stendhal.server.entity.npc.behaviour.impl.SellerBehaviour; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.maps.ados.market.FishermansDaughterNPC; @@ -50,7 +51,8 @@ public class FishermansDaughterSellingNPC implements LoadableContent { addOffer("I sell some snacks and drinks during the Mine Town Weeks. Please take a look at the list close to the stand."); addQuest("I heard that #Susi would love to make friends, she is inside of the house. Or you can ask Fidorea next to me about a little run."); addGoodbye("Bye, hope you'll enjoy the days here!"); - new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("sellrevivalweeks")), false); + new SellerAdder().addSeller(this, new SellerBehaviour(shops.get("sellrevivalweeks", + ShopType.ITEM_SELL)), false); } @Override diff --git a/src/games/stendhal/server/maps/semos/city/PuppyNPC.java b/src/games/stendhal/server/maps/semos/city/PuppyNPC.java index dc286763ab..b92f3a3fd3 100644 --- a/src/games/stendhal/server/maps/semos/city/PuppyNPC.java +++ b/src/games/stendhal/server/maps/semos/city/PuppyNPC.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2010 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -20,6 +20,7 @@ import games.stendhal.server.core.engine.StendhalRPZone; import games.stendhal.server.core.events.UseListener; import games.stendhal.server.entity.RPEntity; import games.stendhal.server.entity.npc.SilentNPC; +import games.stendhal.server.entity.npc.behaviour.impl.idle.WanderIdleBehaviour; /** * A playful puppy @@ -44,8 +45,7 @@ public class PuppyNPC implements ZoneConfigurator { dog.setDescription("You see a playful puppy."); dog.setEntityClass("animal/puppy"); dog.setBaseSpeed(0.5); - dog.moveRandomly(); - dog.setRandomMovementRadius(25, true); + dog.setIdleBehaviour(new WanderIdleBehaviour(25)); dog.setSounds(Arrays.asList("dog-small-bark-1", "dog-small-bark-2")); zone.add(dog); } diff --git a/src/games/stendhal/server/maps/semos/city/SignLessorNPC.java b/src/games/stendhal/server/maps/semos/city/SignLessorNPC.java index 47ee9bd181..1e76990aa0 100644 --- a/src/games/stendhal/server/maps/semos/city/SignLessorNPC.java +++ b/src/games/stendhal/server/maps/semos/city/SignLessorNPC.java @@ -48,6 +48,7 @@ import games.stendhal.server.entity.npc.condition.PlayerHasItemWithHimCondition; import games.stendhal.server.entity.npc.condition.PlayerHasStorableEntityCondition; import games.stendhal.server.entity.npc.condition.TextHasParameterCondition; import games.stendhal.server.entity.player.Player; +import games.stendhal.server.events.ChatOptionsEvent; import games.stendhal.server.util.StringUtils; /** @@ -186,6 +187,10 @@ public class SignLessorNPC implements ZoneConfigurator { } }; + + npc.addKnownChatOptions("rent"); + ChatOptionsEvent.addMerchantActivity("rent"); + npc.setPosition(20, 50); npc.setCollisionAction(CollisionAction.STOP); npc.setEntityClass("signguynpc"); diff --git a/src/games/stendhal/server/maps/semos/tavern/market/PrepareOfferHandler.java b/src/games/stendhal/server/maps/semos/tavern/market/PrepareOfferHandler.java index 93b9f7dad0..537810ca6c 100644 --- a/src/games/stendhal/server/maps/semos/tavern/market/PrepareOfferHandler.java +++ b/src/games/stendhal/server/maps/semos/tavern/market/PrepareOfferHandler.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -20,6 +20,7 @@ import games.stendhal.common.constants.SoundLayer; import games.stendhal.common.grammar.Grammar; import games.stendhal.common.parser.Expression; import games.stendhal.common.parser.Sentence; +import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.engine.dbcommand.LogTradeEventCommand; import games.stendhal.server.entity.item.Item; import games.stendhal.server.entity.item.OwnedItem; @@ -221,11 +222,18 @@ public class PrepareOfferHandler { if (TradingUtility.canPlayerAffordTradingFee(player, price)) { if (createOffer(player, item, price, quantity)) { TradingUtility.substractTradingFee(player, price); - new AsynchronousProgramExecutor("trade", buildTweetMessage(item, quantity, price)).start(); + String msg = buildTweetMessage(item, quantity, price); + new AsynchronousProgramExecutor("trade", msg).start(); DBCommandQueue.get().enqueue(new LogTradeEventCommand(player, item, quantity, price)); npc.addEvent(new SoundEvent(SoundID.COMMERCE2, SoundLayer.CREATURE_NOISE)); npc.say("I added your offer to the trading center and took the fee of "+ fee +"."); npc.setCurrentState(ConversationStates.ATTENDING); + + // announce to chat/IRC + Player postman = SingletonRepository.getRuleProcessor().getPlayer("postman"); + if (postman != null) { + postman.sendPrivateText(npc.getName() + ": " + msg); + } } else { npc.say("You don't have " + Grammar.quantityplnoun(quantity, item.getName(), "a") + "."); } diff --git a/src/games/stendhal/server/script/ManageClover.java b/src/games/stendhal/server/script/ManageClover.java new file mode 100644 index 0000000000..dd5ac1ba1f --- /dev/null +++ b/src/games/stendhal/server/script/ManageClover.java @@ -0,0 +1,78 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.script; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import games.stendhal.common.NotificationType; +import games.stendhal.server.core.engine.StendhalRPZone; +import games.stendhal.server.core.scripting.AbstractAdminScript; +import games.stendhal.server.entity.mapstuff.spawner.CloverSpawner; +import games.stendhal.server.util.TimeUtil; + + +/** + * Locates or re-spawns four-leaf clover. + */ +public class ManageClover extends AbstractAdminScript { + + @Override + protected void run(List args) { + final String command = args.get(0).toLowerCase(Locale.ENGLISH); + final CloverSpawner spawner = CloverSpawner.get(); + // re-spawn is always set to midnight + final String timeToRespawn = TimeUtil.timeUntil(TimeUtil.secondsToMidnight()); + if ("status".equals(command)) { + String zoneDesc = "not found"; + final StendhalRPZone zone = spawner.getZone(); + if (zone != null) { + zoneDesc = zone.getName() + " at " + spawner.getX() + "," + spawner.getY(); + } + final boolean available = spawner.hasPickableClover(); + admin.sendPrivateText("Location: " + zoneDesc + "\nClover available: " + + (available ? "yes" : "no") + "\nRespawns in " + timeToRespawn); + } else if ("respawn".equals(command)) { + spawner.onTurnReached(0); + final StendhalRPZone zone = spawner.getZone(); + if (spawner.hasPickableClover() && zone != null) { + admin.sendPrivateText("Spawned clover on " + zone.getName() + " at " + spawner.getX() + "," + + spawner.getY() + "\nRespawns in " + timeToRespawn); + } else { + admin.sendPrivateText(NotificationType.ERROR, + "Failed to spawn clover, will retry in 15 minutes"); + } + } else { + // show unmodified command + admin.sendPrivateText(NotificationType.ERROR, "Unknown command \"" + args.get(0) + "\""); + } + } + + @Override + public int getMinParams() { + return 1; + } + + @Override + public int getMaxParams() { + return 1; + } + + @Override + protected List getParamDetails() { + return Arrays.asList( + "status: Retrieves spawner location and clover availability.", + "respawn: Forces clover to respawn immediately in random location." + ); + } +} diff --git a/src/games/stendhal/server/script/ManageHouse.java b/src/games/stendhal/server/script/ManageHouse.java new file mode 100644 index 0000000000..e624532871 --- /dev/null +++ b/src/games/stendhal/server/script/ManageHouse.java @@ -0,0 +1,500 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +package games.stendhal.server.script; + +import static games.stendhal.server.maps.quests.houses.HouseTax.TAX_PAYMENT_PERIOD; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import games.stendhal.common.NotificationType; +import games.stendhal.server.constants.StandardMessages; +import games.stendhal.server.core.engine.SingletonRepository; +import games.stendhal.server.core.scripting.AbstractAdminScript; +import games.stendhal.server.entity.mapstuff.portal.HousePortal; +import games.stendhal.server.entity.player.Player; +import games.stendhal.server.maps.quests.HouseBuying; +import games.stendhal.server.maps.quests.houses.HouseTax; +import games.stendhal.server.maps.quests.houses.HouseUtilities; +import games.stendhal.server.util.TimeUtil; + + +/** + * Script for managing and debugging House Buying quest. + */ +public class ManageHouse extends AbstractAdminScript { + + /** Quest ID/name. */ + private static final String SLOT = "house"; + /** Player instance being updated. */ + private Player player; + /** Housing tax manager instance. */ + private HouseTax houseTax; + + private static final List COMMANDS = Arrays.asList( + "status", "info", "inspect", + "unset-house", + "set-house", + "set-lock", + "set-unpaid" + ); + + + @Override + protected void run(final List args) { + final int argc = args.size(); + final String name = args.get(0); + final String command = args.get(1).toLowerCase(Locale.ENGLISH); + if (!checkCommand(command) || !checkParamCount(argc, command)) { + return; + } + player = SingletonRepository.getRuleProcessor().getPlayer(name); + if (player == null) { + StandardMessages.playerNotOnline(admin, name); + return; + } + final HouseBuying quest = (HouseBuying) + SingletonRepository.getStendhalQuestSystem().getQuestFromSlot(ManageHouse.SLOT); + if (quest == null) { + admin.sendPrivateText(NotificationType.ERROR, "Quest \"" + ManageHouse.SLOT + + "\" not available."); + return; + } + houseTax = quest.getHouseTax(); + + if (Arrays.asList("status", "info", "inspect").contains(command)) { + showStatus(); + } else if ("unset-house".equals(command)) { + unsetHouse(); + } else if ("set-house".equals(command)) { + final String city = String.join(" ", args.subList(2, argc-1)); + final Integer number = checkIntParam(args.get(argc-1), ""); + if (number == null) { + return; + } + setHouse(city, number); + } else if ("set-lock".equals(command)) { + final Integer number = checkIntParam(args.get(2), ""); + if (number == null) { + return; + } + setLock(number); + } else if ("set-unpaid".equals(command)) { + final Integer periods = checkIntParam(args.get(2), ""); + if (periods == null) { + return; + } + setUnpaidTaxPeriods(periods); + } else { + StandardMessages.unknownCommand(admin, command); + showUsage(); + } + } + + /** + * Checks for a valid command. + * + * @param command + * Command being executed. + * @return + * {@code true} if the command is recognized. + */ + private boolean checkCommand(final String command) { + if (!ManageHouse.COMMANDS.contains(command)) { + StandardMessages.unknownCommand(admin, command); + showUsage(); + return false; + } + return true; + } + + /** + * Checks for correct parameter count for a specific command. + * + * @param argc + * Total parameter count. + * @param command + * Sub-command being executed. + * @return + * {@code true} if parameter count is within min/max values. + */ + private boolean checkParamCount(final int argc, final String command) { + final int min = getMinParams(command); + final int max = getMaxParams(command); + if (argc < min) { + StandardMessages.missingParameter(admin, command); + showUsage(); + return false; + } else if (max > -1 && argc > max) { + StandardMessages.excessParameter(admin, command); + showUsage(); + return false; + } + return true; + } + + /** + * Checks a string for number representation. + * + * @param value + * String to be checked. + * @param param + * Parameter name (e.g. ). + * @return + * Numeric value of {@code value} or {@code null} if not a number. + */ + private Integer checkIntParam(final String value, final String param) { + try { + return Integer.parseInt(value); + } catch (final NumberFormatException e) { + StandardMessages.paramMustBeNumber(admin, param); + showUsage(); + } + return null; + } + + /** + * Retrieves player's house. + * + * @return + * House portal owned by player or {@code null}. + */ + private HousePortal getHouse() { + return HouseUtilities.getPlayersHouse(player); + } + + /** + * Displays housing status of player to admin. + */ + private void showStatus() { + String msg = "Housing status for player " + player.getName() + ":"; + msg += "\n  Quest slot: " + ManageHouse.SLOT; + msg += "\n  Quest state: " + player.getQuest(ManageHouse.SLOT); + final boolean taxing = houseTax != null; + msg += "\n  Taxing status: " + (taxing ? "active" : "unavailable"); + msg += "\n  Owns house: "; + final HousePortal portal = getHouse(); + if (portal == null) { + msg += "no"; + } else { + msg += portal.getDoorId(); + msg += "\n  Lock number: " + portal.getLockNumber(); + if (taxing) { + final int periods = houseTax.getUnpaidTaxPeriods(portal); + msg += "\n  Unpaid tax periods: " + periods + " (" + houseTax.getTaxDebt(periods) + + " money)"; + } + int secs = (int) ((portal.getExpireTime() - System.currentTimeMillis()) / 1000); + msg += "\n  Expiration: "; + if (secs > 0) { + final int days = Math.max(0, secs / TimeUtil.SECONDS_IN_DAY); + secs %= TimeUtil.SECONDS_IN_DAY; + final int hours = Math.max(0, secs / TimeUtil.SECONDS_IN_HOUR); + secs %= TimeUtil.SECONDS_IN_HOUR; + final int mins = Math.max(0, secs / TimeUtil.SECONDS_IN_MINUTE); + secs %= TimeUtil.SECONDS_IN_MINUTE; + msg += days + "d " + hours + "h " + mins + "m " + secs + "s"; + } else if (secs < 0) { + msg += "expired"; + } + } + admin.sendPrivateText(msg); + } + + /** + * Notifies player and admin about changes to quest slot state. + * + * @param oldState + * Previous state of quest. + */ + private void onQuestChanged(final String oldState) { + StandardMessages.changedQuestState(admin, player, ManageHouse.SLOT, oldState, + player.getQuest(ManageHouse.SLOT)); + } + + /** + * Notifies player and admin about changes to house ownership. + * + * @param oldPortal + * Owned house portal before change. + */ + private void onHouseChanged(final HousePortal oldPortal) { + final String oldId = oldPortal != null ? oldPortal.getDoorId() : "(none)"; + final HousePortal portal = getHouse(); + final String newId = portal != null ? portal.getDoorId() : "(none)"; + if (!newId.equals(oldId)) { + player.sendPrivateText(NotificationType.SUPPORT, "Admin " + admin.getTitle() + + " changed your house from '" + oldId + "' to '" + newId + "'"); + } + showStatus(); + } + + /** + * Notifies player that housing debt has changed. + */ + private void onDebtChanged() { + player.sendPrivateText(NotificationType.SUPPORT, "Admin " + admin.getTitle() + + " adjusted your housing status, you currently owe " + getCurrentDebt() + " in taxes."); + showStatus(); + } + + /** + * Changes house lock and unsets owner. + * + * @param portal + * House portal to be updated. + */ + private void repossess(final HousePortal portal) { + if (portal != null) { + // repossess house + portal.changeLock(); + portal.setOwner(""); + } + } + + /** + * Removes player housing status. + * + * FIXME: this should probably require a confirmation + */ + private void unsetHouse() { + // NOTE: need to get portal before changing slot state + final HousePortal oldPortal = getHouse(); + final String oldState = player.getQuest(ManageHouse.SLOT); + player.setQuest(ManageHouse.SLOT, null); + if (oldPortal == null) { + admin.sendPrivateText("Player " + player.getName() + " does not own a house."); + if (oldState != null) { + onQuestChanged(oldState); + } + return; + } + HousePortal newPortal = getHouse(); + if (newPortal == null) { + // DEBUG: + System.out.println("repossed with setQuest: old portal owner: " + oldPortal.getOwner()); + + // portal was repossessed with setQuest + onHouseChanged(oldPortal); + return; + } + // repossess old house + repossess(oldPortal); + newPortal = getHouse(); + if (newPortal != null) { + admin.sendPrivateText(NotificationType.WARNING, "Failed to reposses old house."); + } + onHouseChanged(oldPortal); + } + + /** + * Sets player housing status. + * + * NOTE: any keys player currently has will not work with house portal + * + * FIXME: this should probably require a confirmation + * + * @param city + * Name of city where house is located. + * @param number + * House ID/number. + */ + private void setHouse(final String city, final int number) { + final String id = city + " " + number; + final HousePortal portal = HouseUtilities.getHousePortal(id); + if (portal == null) { + admin.sendPrivateText(NotificationType.ERROR, "\"" + id + "\" does not exist"); + return; + } + + // DEBUG: + System.out.println("owner is null: " + (portal.getOwner() == null)); + + final String name = player.getName(); + final String currentOwner = portal.getOwner(); + if (currentOwner != null && !"".equals(currentOwner)) { + if (!name.equals(currentOwner)) { + admin.sendPrivateText(NotificationType.ERROR, "\"" + id + "\" is already owned by " + + currentOwner); + return; + } else { + admin.sendPrivateText(NotificationType.ERROR, "Player " + name + " already owns \"" + + id + "\""); + return; + } + } + final HousePortal oldPortal = HouseUtilities.getPlayersHouse(player); + // repossess old house + repossess(oldPortal); + portal.changeLock(); + portal.setOwner(name); + // expiration must be set before update quest slot + setExpiration(oldPortal != null ? oldPortal.getExpireTime() / 1000 + : System.currentTimeMillis() + (TAX_PAYMENT_PERIOD * 5)); + player.setQuest(ManageHouse.SLOT, String.valueOf(number)); + onHouseChanged(oldPortal); + } + + /** + * Does nothing. + * + * @param number + * New lock number. + */ + private void setLock(@SuppressWarnings("unused") final int number) { + admin.sendPrivateText(NotificationType.WARNING, "Changing lock may be unsafe as it could make" + + " old keys work again. It would be better to update keys."); + return; + /* + final HousePortal portal = getHouse(); + if (portal == null) { + admin.sendPrivateText(NotificationType.ERROR, "Player does not own a house, cannot set lock."); + return; + } + portal.put("lock_number", number); + showStatus(); + */ + } + + /** + * Sets expiration time of portal. + * + * @param secs + * Amount of time in which portal expires. Use negative value for past due. + * @return + * {@code true} if new expiration differs from old. + */ + private boolean setExpiration(final long secs) { + if (houseTax == null) { + admin.sendPrivateText(NotificationType.WARNING, "House taxing not active."); + } + final HousePortal portal = getHouse(); + if (portal == null) { + admin.sendPrivateText("Player " + player.getName() + " does not own a house."); + return false; + } + final long oldExpire = portal.getExpireTime(); + portal.setExpireTime(System.currentTimeMillis() + (secs * 1000)); + return portal.getExpireTime() != oldExpire; + } + + /** + * Sets expiration time of portal. + * + * @param periods + * Number of payment periods. Use negative value for past due. + * @return + * {@code true} if new expiration differs from old. + */ + private boolean setExpiration(final int periods) { + return setExpiration((long) (periods * TAX_PAYMENT_PERIOD)); + } + + /** + * Sets the number of months for which player owes taxes. + * + * @param periods + * Number of payment periods. + */ + private void setUnpaidTaxPeriods(final int periods) { + if (setExpiration(-periods)) { + onDebtChanged(); + } else { + admin.sendPrivateText("No changes."); + showStatus(); + } + } + + /** + * Retrieves the number of unpaid tax periods by player. + * + * @return + * Tax periods. + */ + private int getUnpaidTaxPeriods() { + return houseTax == null ? 0 : houseTax.getUnpaidTaxPeriods(player); + } + + /** + * Calculates money currently owed in taxes by player. + * + * @return + */ + private int getCurrentDebt() { + return houseTax == null ? 0 : houseTax.getTaxDebt(getUnpaidTaxPeriods()); + } + + @Override + protected int getMinParams() { + return 2; + } + + /** + * Retrieves minimum number of parameters required for a command. + * + * @param command + * Sub-command being executed. + * @return + * Minimum parameter count. + */ + private int getMinParams(final String command) { + if ("set-house".equals(command)) { + return 4; + } else if (Arrays.asList("set-unpaid", "set-lock").contains(command)) { + return 3; + } + return getMinParams(); + } + + /** + * Retrieves maximum number of parameters required for a command. + * + * @param command + * Sub-command being executed. + * @return + * Maximum parameter count. + */ + private int getMaxParams(final String command) { + if ("unset-house".equals(command)) { + return 2; + } else if (Arrays.asList("set-unpaid", "set-lock").contains(command)) { + return 3; + } + return getMaxParams(); + } + + @Override + protected List getParamStrings() { + return Arrays.asList( + " status", + " unset-house", + " set-house ", + " set-lock ", + " set-unpaid " + ); + } + + @Override + protected List getParamDetails() { + return Arrays.asList( + "player: Name of player.", + "status: Show housing status.", + "unset-house: Remove quest state, house, and tax status from player.", + "set-house: Sets player's owned house.", + "city: Name of city where house is located (spaces allowed).", + "house-number: House ID/number.", + "lock-number: Lock ID/number.", + "count: Number of unpaid periods (months)." + ); + } +} diff --git a/src/games/stendhal/server/script/Maria.java b/src/games/stendhal/server/script/Maria.java index 2f94df03f4..1e666ffbad 100644 --- a/src/games/stendhal/server/script/Maria.java +++ b/src/games/stendhal/server/script/Maria.java @@ -37,6 +37,7 @@ import games.stendhal.server.entity.npc.condition.OrCondition; import games.stendhal.server.entity.npc.condition.QuestInStateCondition; import games.stendhal.server.entity.npc.condition.QuestNotInStateCondition; import games.stendhal.server.entity.npc.condition.TimePassedCondition; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.entity.player.Player; @@ -89,8 +90,8 @@ public class Maria extends ScriptImpl { int y = 4; String shop = "food&drinks"; final ShopsList shops = SingletonRepository.getShopsList(); - if (args.size() > 0 ) { - if (shops.get(args.get(0))!= null) { + if (args.size() > 0) { + if (shops.get(args.get(0), ShopType.ITEM_SELL) != null) { shop = args.get(0); } else { admin.sendPrivateText(args.get(0) @@ -120,7 +121,7 @@ public class Maria extends ScriptImpl { "You can see what I #offer and take a break to meet new people!"); npc.behave("bye", "Bye bye!"); try { - npc.behave("sell", SingletonRepository.getShopsList().get(shop)); + npc.behave("sell", SingletonRepository.getShopsList().get(shop, ShopType.ITEM_SELL)); } catch (final NoSuchMethodException e) { logger.error(e, e); } diff --git a/src/games/stendhal/server/script/TestShowItemList.java b/src/games/stendhal/server/script/TestShowItemList.java index 4d07edb279..cddada18dc 100644 --- a/src/games/stendhal/server/script/TestShowItemList.java +++ b/src/games/stendhal/server/script/TestShowItemList.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -14,6 +14,7 @@ package games.stendhal.server.script; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import games.stendhal.common.NotificationType; @@ -21,6 +22,7 @@ import games.stendhal.server.core.engine.SingletonRepository; import games.stendhal.server.core.scripting.ScriptImpl; import games.stendhal.server.entity.item.Item; import games.stendhal.server.entity.item.ItemInformation; +import games.stendhal.server.entity.npc.shop.ShopType; import games.stendhal.server.entity.npc.shop.ShopsList; import games.stendhal.server.entity.player.Player; import games.stendhal.server.events.ShowItemListEvent; @@ -43,14 +45,32 @@ public class TestShowItemList extends ScriptImpl { List itemList = new LinkedList(); + int argc = args.size(); if (args.isEmpty()) { itemList.add(prepareItem("club", 100)); itemList.add(prepareItem("leather armor", -100)); itemList.add(prepareItem("ice sword", -10000)); } else { - final String shopName = args.get(0); + final String shopName; + ShopType shopType = null; + if (argc > 1) { + shopType = ShopType.fromString(args.get(0).toLowerCase(Locale.ENGLISH)); + shopName = args.get(1); + } else { + shopName = args.get(0); + } ShopsList shops = SingletonRepository.getShopsList(); - Map items = shops.get(shopName); + Map items = null; + if (shopType != null) { + items = shops.get(shopName, shopType); + } else { + for (ShopType st: ShopType.values()) { + items = shops.get(shopName, st); + if (items != null) { + break; + } + } + } if (items == null) { admin.sendPrivateText(NotificationType.ERROR, "Shop \"" + shopName + "\" not found"); diff --git a/src/games/stendhal/server/util/AsynchronousProgramExecutor.java b/src/games/stendhal/server/util/AsynchronousProgramExecutor.java index 6ad5355559..f80939b764 100644 --- a/src/games/stendhal/server/util/AsynchronousProgramExecutor.java +++ b/src/games/stendhal/server/util/AsynchronousProgramExecutor.java @@ -64,7 +64,7 @@ public class AsynchronousProgramExecutor extends Thread { } /** - * sends the message to the twitter account + * sends the message to the social media account * * @param cmd command */ diff --git a/src/games/stendhal/server/util/TimeUtil.java b/src/games/stendhal/server/util/TimeUtil.java index 4acbb6e224..8d9d6ae1e5 100644 --- a/src/games/stendhal/server/util/TimeUtil.java +++ b/src/games/stendhal/server/util/TimeUtil.java @@ -16,6 +16,8 @@ ***************************************************************************/ package games.stendhal.server.util; +import java.time.LocalTime; + import games.stendhal.common.grammar.Grammar; /** @@ -37,8 +39,6 @@ public class TimeUtil { public static final int MINUTES_IN_YEAR = MINUTES_IN_DAY * 365; // 525,600 public static final int MINUTES_IN_HALF_YEAR = MINUTES_IN_YEAR / 2; // 262,800 - public static final int TURNS_IN_MINUTE = (int) (SECONDS_IN_MINUTE / 0.3); // 200 - /** * Create a text representing a saying of approximate time until. @@ -257,4 +257,13 @@ public class TimeUtil { } } + /** + * Retrieves seconds to next day. + * + * @return + * Remaining seconds to midnight hour in local time. + */ + public static int secondsToMidnight() { + return SECONDS_IN_DAY - LocalTime.now().toSecondOfDay(); + } } diff --git a/src/games/stendhal/tools/contributors/SourceForgeContributorsGenerator.java b/src/games/stendhal/tools/contributors/SourceForgeContributorsGenerator.java index b3e23a99c8..b02a4de591 100644 --- a/src/games/stendhal/tools/contributors/SourceForgeContributorsGenerator.java +++ b/src/games/stendhal/tools/contributors/SourceForgeContributorsGenerator.java @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -18,7 +18,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -135,7 +136,7 @@ public class SourceForgeContributorsGenerator { @SuppressWarnings("unchecked") private void fetchProfiles(Map> contributors) throws IOException { for (Map.Entry> entry : contributors.entrySet()) { - try (Reader reader = new InputStreamReader(new URL("https://sourceforge.net/rest/u/" + entry.getKey() +"/profile").openStream(), "UTF-8")) { + try (Reader reader = new InputStreamReader(new URI("https://sourceforge.net/rest/u/" + entry.getKey() +"/profile").toURL().openStream(), "UTF-8")) { Map contributor = entry.getValue(); Map profile = (Map) JSONValue.parse(reader); String fullname = (String) profile.get("name"); @@ -149,7 +150,7 @@ public class SourceForgeContributorsGenerator { if (country != null && !country.trim().equals("")) { contributor.put("country", country); } - } catch (FileNotFoundException e) { + } catch (FileNotFoundException | URISyntaxException e) { System.err.println("Profile " + entry.getKey() + " does not exist."); } } diff --git a/src/games/stendhal/tools/updateprop/UpdatePropUpdater.java b/src/games/stendhal/tools/updateprop/UpdatePropUpdater.java index d58abb8b56..adf676f581 100644 --- a/src/games/stendhal/tools/updateprop/UpdatePropUpdater.java +++ b/src/games/stendhal/tools/updateprop/UpdatePropUpdater.java @@ -1,6 +1,6 @@ /* $Id$ */ /*************************************************************************** - * (C) Copyright 2003-2011 - Stendhal * + * (C) Copyright 2003-2025 - Stendhal * *************************************************************************** *************************************************************************** * * @@ -18,7 +18,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -80,13 +81,14 @@ public class UpdatePropUpdater { * loads the current version of the update.properties * * @throws IOException in case of an input/output error + * @throws URISyntaxException */ - private void loadOldUpdateProperties() throws IOException { + private void loadOldUpdateProperties() throws IOException, URISyntaxException { prop = new Properties(); InputStream is; if (oldFile.indexOf(":") > 2) { - URL url = new URL(oldFile); - is = url.openStream(); + URI uri = new URI(oldFile); + is = uri.toURL().openStream(); } else { is = new FileInputStream(oldFile); } diff --git a/src/js/css/main.css b/src/js/css/main.css index b1cd853d05..76765f1468 100644 --- a/src/js/css/main.css +++ b/src/js/css/main.css @@ -41,6 +41,10 @@ input { display: none; } +.center-text { + text-align: center; +} + input:invalid { border: 2px dashed red; } @@ -145,6 +149,10 @@ ul.uniform { padding: 0; } +#outfit-preview, button#rotate-cc { + margin-left: 0.5em; +} + div.itemSlot { width: 32px; height: 32px; @@ -327,7 +335,7 @@ div.logcolR { background: url(/data/gui/danger.png) repeat; } -.qm-button, img#held-object { +.qm-button, canvas#held-object { position: absolute; display: none; opacity: 0.5; @@ -600,6 +608,12 @@ div.logcolR { overflow-y: scroll; } +.bestiary-cell { + min-width: 3em; + min-height: 1em; + padding: 4px; +} + .progressbar { border: 1px solid #005; line-height: 5vh; @@ -658,6 +672,10 @@ div.logcolR { padding-left: 15px; } +.indented { + margin-left: 1.5em; +} + .tally { font-family: Tally; } @@ -690,3 +708,16 @@ img#click-indicator { .pad-contents { padding: 0.5em; } + +table.shoptable { + border-collapse: collapse; +} + +table.shoptable th, table.shoptable td { + border: 1px solid var(--text-color); + padding: 2px; +} + +td.number { + text-align: right; +} diff --git a/src/js/jsxgraph-util.js b/src/js/jsxgraph-util.js deleted file mode 100644 index c2ab4938ab..0000000000 --- a/src/js/jsxgraph-util.js +++ /dev/null @@ -1,1169 +0,0 @@ -/* - From: http://jsxgraph.uni-bayreuth.de/wp/tag/gunzip/ - From: https://jsxgraph.org/wp/ - - Copyright 2008,2009 - Matthias Ehmann, - Michael Gerhaeuser, - Carsten Miller, - Bianca Valentin, - Alfred Wassermann, - Peter Wilfahrt - - This file is part of JSXGraph. - - JSXGraph is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - JSXGraph 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with JSXGraph. If not, see . -*/ - -/** - * @fileoverview Utilities for uncompressing and base64 decoding - */ - -/** - * @class Util class - * Class for gunzipping, unzipping and base64 decoding of files. - * It is used for reading GEONExT, Geogebra and Intergeo files. - * - * Only Huffman codes are decoded in gunzip. - * The code is based on the source code for gunzip.c by Pasi Ojala - * @see http://www.cs.tut.fi/~albert/Dev/gunzip/gunzip.c - * @see http://www.cs.tut.fi/~albert - */ -JXG = {}; -JXG.Util = {}; - -/** - * Unzip zip files - */ -JXG.Util.Unzip = function (barray){ - var outputArr = [], - output = "", - debug = false, - gpflags, - files = 0, - unzipped = [], - crc, - buf32k = new Array(32768), - bIdx = 0, - modeZIP=false, - - CRC, SIZE, - - bitReverse = [ - 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, - 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, - 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, - 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, - 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, - 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, - 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, - 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, - 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, - 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, - 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, - 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, - 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, - 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, - 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, - 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, - 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, - 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, - 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, - 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, - 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, - 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, - 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, - 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, - 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, - 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, - 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, - 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, - 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, - 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, - 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, - 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff - ], - - cplens = [ - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, - 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 - ], - - cplext = [ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, - 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99 - ], /* 99==invalid */ - - cpdist = [ - 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0007, 0x0009, 0x000d, - 0x0011, 0x0019, 0x0021, 0x0031, 0x0041, 0x0061, 0x0081, 0x00c1, - 0x0101, 0x0181, 0x0201, 0x0301, 0x0401, 0x0601, 0x0801, 0x0c01, - 0x1001, 0x1801, 0x2001, 0x3001, 0x4001, 0x6001 - ], - - cpdext = [ - 0, 0, 0, 0, 1, 1, 2, 2, - 3, 3, 4, 4, 5, 5, 6, 6, - 7, 7, 8, 8, 9, 9, 10, 10, - 11, 11, 12, 12, 13, 13 - ], - - border = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15], - - bA = barray, - - bytepos=0, - bitpos=0, - bb = 1, - bits=0, - - NAMEMAX = 256, - - nameBuf = [], - - fileout; - - function readByte(){ - bits+=8; - if (bytepos"); - return bA[bytepos++]; - } else - return -1; - }; - - function byteAlign(){ - bb = 1; - }; - - function readBit(){ - var carry; - bits++; - carry = (bb & 1); - bb >>= 1; - if (bb==0){ - bb = readByte(); - carry = (bb & 1); - bb = (bb>>1) | 0x80; - } - return carry; - }; - - function readBits(a) { - var res = 0, - i = a; - - while(i--) { - res = (res<<1) | readBit(); - } - if(a) { - res = bitReverse[res]>>(8-a); - } - return res; - }; - - function flushBuffer(){ - //document.write('FLUSHBUFFER:'+buf32k); - bIdx = 0; - }; - function addBuffer(a){ - SIZE++; - //CRC=updcrc(a,crc); - buf32k[bIdx++] = a; - outputArr.push(String.fromCharCode(a)); - //output+=String.fromCharCode(a); - if(bIdx==0x8000){ - //document.write('ADDBUFFER:'+buf32k); - bIdx=0; - } - }; - - function HufNode() { - this.b0=0; - this.b1=0; - this.jump = null; - this.jumppos = -1; - }; - - var LITERALS = 288; - - var literalTree = new Array(LITERALS); - var distanceTree = new Array(32); - var treepos=0; - var Places = null; - var Places2 = null; - - var impDistanceTree = new Array(64); - var impLengthTree = new Array(64); - - var len = 0; - var fpos = new Array(17); - fpos[0]=0; - var flens; - var fmax; - - function IsPat() { - while (1) { - if (fpos[len] >= fmax) - return -1; - if (flens[fpos[len]] == len) - return fpos[len]++; - fpos[len]++; - } - }; - - function Rec() { - var curplace = Places[treepos]; - var tmp; - if (debug) - document.write("
len:"+len+" treepos:"+treepos); - if(len==17) { //war 17 - return -1; - } - treepos++; - len++; - - tmp = IsPat(); - if (debug) - document.write("
IsPat "+tmp); - if(tmp >= 0) { - curplace.b0 = tmp; /* leaf cell for 0-bit */ - if (debug) - document.write("
b0 "+curplace.b0); - } else { - /* Not a Leaf cell */ - curplace.b0 = 0x8000; - if (debug) - document.write("
b0 "+curplace.b0); - if(Rec()) - return -1; - } - tmp = IsPat(); - if(tmp >= 0) { - curplace.b1 = tmp; /* leaf cell for 1-bit */ - if (debug) - document.write("
b1 "+curplace.b1); - curplace.jump = null; /* Just for the display routine */ - } else { - /* Not a Leaf cell */ - curplace.b1 = 0x8000; - if (debug) - document.write("
b1 "+curplace.b1); - curplace.jump = Places[treepos]; - curplace.jumppos = treepos; - if(Rec()) - return -1; - } - len--; - return 0; - }; - - function CreateTree(currentTree, numval, lengths, show) { - var i; - /* Create the Huffman decode tree/table */ - //document.write("
createtree
"); - if (debug) - document.write("currentTree "+currentTree+" numval "+numval+" lengths "+lengths+" show "+show); - Places = currentTree; - treepos=0; - flens = lengths; - fmax = numval; - for (i=0;i<17;i++) - fpos[i] = 0; - len = 0; - if(Rec()) { - //fprintf(stderr, "invalid huffman tree\n"); - if (debug) - alert("invalid huffman tree\n"); - return -1; - } - if (debug){ - document.write('
Tree: '+Places.length); - for (var a=0;a<32;a++){ - document.write("Places["+a+"].b0="+Places[a].b0+"
"); - document.write("Places["+a+"].b1="+Places[a].b1+"
"); - } - } - - /*if(show) { - var tmp; - for(tmp=currentTree;tmpjump?tmp->jump-currentTree:0,(tmp->jump?tmp->jump-currentTree:0)*6+0xcf0); - if(!(tmp.b0 & 0x8000)) { - //fprintf(stdout, " 0x%03x (%c)", tmp->b0,(tmp->b0<256 && isprint(tmp->b0))?tmp->b0:'�'); - } - if(!(tmp.b1 & 0x8000)) { - if((tmp.b0 & 0x8000)) - fprintf(stdout, " "); - fprintf(stdout, " 0x%03x (%c)", tmp->b1,(tmp->b1<256 && isprint(tmp->b1))?tmp->b1:'�'); - } - fprintf(stdout, "\n"); - } - }*/ - return 0; - }; - - function DecodeValue(currentTree) { - var len, i, - xtreepos=0, - X = currentTree[xtreepos], - b; - - /* decode one symbol of the data */ - while(1) { - b=readBit(); - if (debug) - document.write("b="+b); - if(b) { - if(!(X.b1 & 0x8000)){ - if (debug) - document.write("ret1"); - return X.b1; /* If leaf node, return data */ - } - X = X.jump; - len = currentTree.length; - for (i=0;i>1); - if(j > 23) { - j = (j<<1) | readBit(); /* 48..255 */ - - if(j > 199) { /* 200..255 */ - j -= 128; /* 72..127 */ - j = (j<<1) | readBit(); /* 144..255 << */ - } else { /* 48..199 */ - j -= 48; /* 0..151 */ - if(j > 143) { - j = j+136; /* 280..287 << */ - /* 0..143 << */ - } - } - } else { /* 0..23 */ - j += 256; /* 256..279 << */ - } - if(j < 256) { - addBuffer(j); - //document.write("out:"+String.fromCharCode(j)); - /*fprintf(errfp, "@%d %02x\n", SIZE, j);*/ - } else if(j == 256) { - /* EOF */ - break; - } else { - var len, dist; - - j -= 256 + 1; /* bytes + EOF */ - len = readBits(cplext[j]) + cplens[j]; - - j = bitReverse[readBits(5)]>>3; - if(cpdext[j] > 8) { - dist = readBits(8); - dist |= (readBits(cpdext[j]-8)<<8); - } else { - dist = readBits(cpdext[j]); - } - dist += cpdist[j]; - - /*fprintf(errfp, "@%d (l%02x,d%04x)\n", SIZE, len, dist);*/ - for(j=0;jparam: "+literalCodes+" "+distCodes+" "+lenCodes+"
"); - for(j=0; j<19; j++) { - ll[j] = 0; - } - - // Get the decode tree code lengths - - //document.write("
"); - for(j=0; jll:'+ll); - len = distanceTree.length; - for (i=0; idistanceTree"); - for(var a=0;a"+distanceTree[a].b0+" "+distanceTree[a].b1+" "+distanceTree[a].jump+" "+distanceTree[a].jumppos); - /*if (distanceTree[a].jumppos!=-1) - document.write(" "+distanceTree[a].jump.b0+" "+distanceTree[a].jump.b1); - */ - } - } - //document.write('
tree created'); - - //read in literal and distance code lengths - n = literalCodes + distCodes; - i = 0; - var z=-1; - if (debug) - document.write("
n="+n+" bits: "+bits+"
"); - while(i < n) { - z++; - j = DecodeValue(distanceTree); - if (debug) - document.write("
"+z+" i:"+i+" decode: "+j+" bits "+bits+"
"); - if(j<16) { // length of code in bits (0..15) - ll[i++] = j; - } else if(j==16) { // repeat last length 3 to 6 times - var l; - j = 3 + readBits(2); - if(i+j > n) { - flushBuffer(); - return 1; - } - l = i ? ll[i-1] : 0; - while(j--) { - ll[i++] = l; - } - } else { - if(j==17) { // 3 to 10 zero length codes - j = 3 + readBits(3); - } else { // j == 18: 11 to 138 zero length codes - j = 11 + readBits(7); - } - if(i+j > n) { - flushBuffer(); - return 1; - } - while(j--) { - ll[i++] = 0; - } - } - } - /*for(j=0; jliteralTree"); - while(1) { - j = DecodeValue(literalTree); - if(j >= 256) { // In C64: if carry set - var len, dist; - j -= 256; - if(j == 0) { - // EOF - break; - } - j--; - len = readBits(cplext[j]) + cplens[j]; - - j = DecodeValue(distanceTree); - if(cpdext[j] > 8) { - dist = readBits(8); - dist |= (readBits(cpdext[j]-8)<<8); - } else { - dist = readBits(cpdext[j]); - } - dist += cpdist[j]; - while(len--) { - var c = buf32k[(bIdx - dist) & 0x7fff]; - addBuffer(c); - } - } else { - addBuffer(j); - } - } - } - } while(!last); - flushBuffer(); - - byteAlign(); - return 0; - }; - - JXG.Util.Unzip.prototype.unzipFile = function(name) { - var i; - this.unzip(); - //alert(unzipped[0][1]); - for (i=0;i"); - } - */ - //alert(bA); - nextFile(); - return unzipped; - }; - - function nextFile(){ - if (debug) - alert("NEXTFILE"); - outputArr = []; - var tmp = []; - modeZIP = false; - tmp[0] = readByte(); - tmp[1] = readByte(); - if (debug) - alert("type: "+tmp[0]+" "+tmp[1]); - if (tmp[0] == parseInt("78",16) && tmp[1] == parseInt("da",16)){ //GZIP - if (debug) - alert("GEONExT-GZIP"); - DeflateLoop(); - if (debug) - alert(outputArr.join('')); - unzipped[files] = new Array(2); - unzipped[files][0] = outputArr.join(''); - unzipped[files][1] = "geonext.gxt"; - files++; - } - if (tmp[0] == parseInt("1f",16) && tmp[1] == parseInt("8b",16)){ //GZIP - if (debug) - alert("GZIP"); - //DeflateLoop(); - skipdir(); - if (debug) - alert(outputArr.join('')); - unzipped[files] = new Array(2); - unzipped[files][0] = outputArr.join(''); - unzipped[files][1] = "file"; - files++; - } - if (tmp[0] == parseInt("50",16) && tmp[1] == parseInt("4b",16)){ //ZIP - modeZIP = true; - tmp[2] = readByte(); - tmp[3] = readByte(); - if (tmp[2] == parseInt("3",16) && tmp[3] == parseInt("4",16)){ - //MODE_ZIP - tmp[0] = readByte(); - tmp[1] = readByte(); - if (debug) - alert("ZIP-Version: "+tmp[1]+" "+tmp[0]/10+"."+tmp[0]%10); - - gpflags = readByte(); - gpflags |= (readByte()<<8); - if (debug) - alert("gpflags: "+gpflags); - - var method = readByte(); - method |= (readByte()<<8); - if (debug) - alert("method: "+method); - - readByte(); - readByte(); - readByte(); - readByte(); - - var crc = readByte(); - crc |= (readByte()<<8); - crc |= (readByte()<<16); - crc |= (readByte()<<24); - - var compSize = readByte(); - compSize |= (readByte()<<8); - compSize |= (readByte()<<16); - compSize |= (readByte()<<24); - - var size = readByte(); - size |= (readByte()<<8); - size |= (readByte()<<16); - size |= (readByte()<<24); - - if (debug) - alert("local CRC: "+crc+"\nlocal Size: "+size+"\nlocal CompSize: "+compSize); - - var filelen = readByte(); - filelen |= (readByte()<<8); - - var extralen = readByte(); - extralen |= (readByte()<<8); - - if (debug) - alert("filelen "+filelen); - i = 0; - nameBuf = []; - while (filelen--){ - var c = readByte(); - if (c == "/" | c ==":"){ - i = 0; - } else if (i < NAMEMAX-1) - nameBuf[i++] = String.fromCharCode(c); - } - if (debug) - alert("nameBuf: "+nameBuf); - - //nameBuf[i] = "\0"; - if (!fileout) - fileout = nameBuf; - - var i = 0; - while (i < extralen){ - c = readByte(); - i++; - } - - CRC = 0xffffffff; - SIZE = 0; - - if (size = 0 && fileOut.charAt(fileout.length-1)=="/"){ - //skipdir - if (debug) - alert("skipdir"); - } - if (method == 8){ - DeflateLoop(); - if (debug) - alert(outputArr.join('')); - unzipped[files] = new Array(2); - unzipped[files][0] = outputArr.join(''); - unzipped[files][1] = nameBuf.join(''); - files++; - //return outputArr.join(''); - } - skipdir(); - } - } - }; - - function skipdir(){ - var crc, - tmp = [], - compSize, size, os, i, c; - - if ((gpflags & 8)) { - tmp[0] = readByte(); - tmp[1] = readByte(); - tmp[2] = readByte(); - tmp[3] = readByte(); - - if (tmp[0] == parseInt("50",16) && - tmp[1] == parseInt("4b",16) && - tmp[2] == parseInt("07",16) && - tmp[3] == parseInt("08",16)) - { - crc = readByte(); - crc |= (readByte()<<8); - crc |= (readByte()<<16); - crc |= (readByte()<<24); - } else { - crc = tmp[0] | (tmp[1]<<8) | (tmp[2]<<16) | (tmp[3]<<24); - } - - compSize = readByte(); - compSize |= (readByte()<<8); - compSize |= (readByte()<<16); - compSize |= (readByte()<<24); - - size = readByte(); - size |= (readByte()<<8); - size |= (readByte()<<16); - size |= (readByte()<<24); - - if (debug) - alert("CRC:"); - } - - if (modeZIP) - nextFile(); - - tmp[0] = readByte(); - if (tmp[0] != 8) { - if (debug) - alert("Unknown compression method!"); - return 0; - } - - gpflags = readByte(); - if (debug){ - if ((gpflags & ~(parseInt("1f",16)))) - alert("Unknown flags set!"); - } - - readByte(); - readByte(); - readByte(); - readByte(); - - readByte(); - os = readByte(); - - if ((gpflags & 4)){ - tmp[0] = readByte(); - tmp[2] = readByte(); - len = tmp[0] + 256*tmp[1]; - if (debug) - alert("Extra field size: "+len); - for (i=0;ihttp://www.webtoolkit.info/ -*/ -JXG.Util.Base64 = { - - // private property - _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", - - // public method for encoding - encode : function (input) { - var output = [], - chr1, chr2, chr3, enc1, enc2, enc3, enc4, - i = 0; - - input = JXG.Util.Base64._utf8_encode(input); - - while (i < input.length) { - - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output.push([this._keyStr.charAt(enc1), - this._keyStr.charAt(enc2), - this._keyStr.charAt(enc3), - this._keyStr.charAt(enc4)].join('')); - } - - return output.join(''); - }, - - // public method for decoding - decode : function (input, utf8) { - var output = [], - chr1, chr2, chr3, - enc1, enc2, enc3, enc4, - i = 0; - - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); - - while (i < input.length) { - - enc1 = this._keyStr.indexOf(input.charAt(i++)); - enc2 = this._keyStr.indexOf(input.charAt(i++)); - enc3 = this._keyStr.indexOf(input.charAt(i++)); - enc4 = this._keyStr.indexOf(input.charAt(i++)); - - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - - output.push(String.fromCharCode(chr1)); - - if (enc3 != 64) { - output.push(String.fromCharCode(chr2)); - } - if (enc4 != 64) { - output.push(String.fromCharCode(chr3)); - } - } - - output = output.join(''); - - if (utf8) { - output = JXG.Util.Base64._utf8_decode(output); - } - return output; - - }, - - // private method for UTF-8 encoding - _utf8_encode : function (string) { - string = string.replace(/\r\n/g,"\n"); - var utftext = ""; - - for (var n = 0; n < string.length; n++) { - - var c = string.charCodeAt(n); - - if (c < 128) { - utftext += String.fromCharCode(c); - } - else if((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } - else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } - - } - - return utftext; - }, - - // private method for UTF-8 decoding - _utf8_decode : function (utftext) { - var string = [], - i = 0, - c = 0, c2 = 0, c3 = 0; - - while ( i < utftext.length ) { - c = utftext.charCodeAt(i); - if (c < 128) { - string.push(String.fromCharCode(c)); - i++; - } - else if((c > 191) && (c < 224)) { - c2 = utftext.charCodeAt(i+1); - string.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63))); - i += 2; - } - else { - c2 = utftext.charCodeAt(i+1); - c3 = utftext.charCodeAt(i+2); - string.push(String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))); - i += 3; - } - } - return string.join(''); - }, - - _destrip: function (stripped, wrap){ - var lines = [], lineno, i, - destripped = []; - - if (wrap==null) - wrap = 76; - - stripped.replace(/ /g, ""); - lineno = stripped.length / wrap; - for (i = 0; i < lineno; i++) - lines[i]=stripped.substr(i * wrap, wrap); - if (lineno != stripped.length / wrap) - lines[lines.length]=stripped.substr(lineno * wrap, stripped.length-(lineno * wrap)); - - for (i = 0; i < lines.length; i++) - destripped.push(lines[i]); - return destripped.join('\n'); - }, - - decodeAsArray: function (input){ - var dec = this.decode(input), - ar = [], i; - for (i=0;i255){ - switch (c) { - case 8364: c=128; - break; - case 8218: c=130; - break; - case 402: c=131; - break; - case 8222: c=132; - break; - case 8230: c=133; - break; - case 8224: c=134; - break; - case 8225: c=135; - break; - case 710: c=136; - break; - case 8240: c=137; - break; - case 352: c=138; - break; - case 8249: c=139; - break; - case 338: c=140; - break; - case 381: c=142; - break; - case 8216: c=145; - break; - case 8217: c=146; - break; - case 8220: c=147; - break; - case 8221: c=148; - break; - case 8226: c=149; - break; - case 8211: c=150; - break; - case 8212: c=151; - break; - case 732: c=152; - break; - case 8482: c=153; - break; - case 353: c=154; - break; - case 8250: c=155; - break; - case 339: c=156; - break; - case 382: c=158; - break; - case 376: c=159; - break; - default: - break; - } - } - return c; -}; - -/** - * Decoding string into utf-8 - * @param {String} string to decode - * @return {String} utf8 decoded string - */ -JXG.Util.utf8Decode = function(utftext) { - var string = []; - var i = 0; - var c = 0, c1 = 0, c2 = 0; - - while ( i < utftext.length ) { - c = utftext.charCodeAt(i); - - if (c < 128) { - string.push(String.fromCharCode(c)); - i++; - } else if((c > 191) && (c < 224)) { - c2 = utftext.charCodeAt(i+1); - string.push(String.fromCharCode(((c & 31) << 6) | (c2 & 63))); - i += 2; - } else { - c2 = utftext.charCodeAt(i+1); - c3 = utftext.charCodeAt(i+2); - string.push(String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))); - i += 3; - } - }; - return string.join(''); -}; diff --git a/src/js/stendhal.html b/src/js/stendhal.html index 176bdcdb3b..c294d10012 100644 --- a/src/js/stendhal.html +++ b/src/js/stendhal.html @@ -86,6 +86,7 @@ +
@@ -165,7 +166,11 @@ @@ -271,15 +276,9 @@
- - - - - - @@ -289,7 +288,6 @@ -
@@ -320,6 +318,17 @@
+
+
+ + + + + + +
+
+
@@ -352,6 +361,10 @@
+ + - + - - - diff --git a/src/js/stendhal/Client.ts b/src/js/stendhal/Client.ts index 192c89463c..aa0e7885a8 100644 --- a/src/js/stendhal/Client.ts +++ b/src/js/stendhal/Client.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2023-2024 - Faiumoni e. V. * + * Copyright © 2023-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,25 +9,21 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa, Deserializer, RPObject } from "marauroa" +import { stendhal } from "./stendhal"; -import { PerceptionListener } from "./PerceptionListener"; +import { StendhalPerceptionListener } from "./PerceptionListener"; import { singletons } from "./SingletonRepo"; import { Paths } from "./data/Paths"; import { Color } from "./data/color/Color"; -import { Ground } from "./entity/Ground"; -import { RPObject } from "./entity/RPObject"; +import { EntityRegistry } from "./entity/EntityRegistry"; import { ui } from "./ui/UI"; import { UIComponentEnum } from "./ui/UIComponentEnum"; -import { BuddyListComponent } from "./ui/component/BuddyListComponent"; -import { MiniMapComponent } from "./ui/component/MiniMapComponent"; -import { PlayerEquipmentComponent } from "./ui/component/PlayerEquipmentComponent"; import { ZoneInfoComponent } from "./ui/component/ZoneInfoComponent"; import { ChooseCharacterDialog } from "./ui/dialog/ChooseCharacterDialog"; @@ -39,6 +35,13 @@ import { SingletonFloatingWindow } from "./ui/toolkit/SingletonFloatingWindow"; import { Chat } from "./util/Chat"; import { DialogHandler } from "./util/DialogHandler"; +import { Globals } from "./util/Globals"; +import { TileMap } from "data/TileMap"; +import { OutfitStore } from "data/OutfitStore"; +import { TileStore } from "data/TileStore"; +import { WeatherRenderer } from "util/WeatherRenderer"; +import { ViewPort } from "ui/ViewPort"; +import { htmlImageStore } from "data/HTMLImageStore"; /** @@ -61,6 +64,7 @@ export class Client { /** Singleton instance. */ private static instance: Client; + public loaded = false; /** @@ -77,7 +81,7 @@ export class Client { * Hidden singleton constructor. */ private constructor() { - // do nothing + // empty } /** @@ -90,18 +94,18 @@ export class Client { } this.initialized = true; - // add version & build info to DOM for retrieval by browser - document.documentElement.setAttribute("data-build-version", stendhal.data.build.version); - document.documentElement.setAttribute("data-build-build", stendhal.data.build.build); - - stendhal.paths = singletons.getPaths(); stendhal.config = singletons.getConfigManager(); stendhal.session = singletons.getSessionManager(); stendhal.actions = singletons.getSlashActionRepo(); + new EntityRegistry().init(); this.initData(); + this.initSound(); this.initUI(); - this.initZone(); + + // add version & build info to DOM for retrieval by browser + document.documentElement.setAttribute("data-build-version", stendhal.data.build?.version); + document.documentElement.setAttribute("data-build-build", stendhal.data.build?.build); } /** @@ -115,36 +119,30 @@ export class Client { stendhal.data.cstatus = singletons.getCStatus(); stendhal.data.cstatus.init(); stendhal.data.group = singletons.getGroupManager(); - stendhal.data.outfit = singletons.getOutfitStore(); - stendhal.data.sprites = singletons.getSpriteStore(); - stendhal.data.map = singletons.getMap(); // online players stendhal.players = []; } + /** + * Initializes sound manager. + * + * Should be called after config is initialized. + */ + private initSound() { + stendhal.sound = singletons.getSoundManager(); + // update sound levels from config at startup + stendhal.sound.onConfigUpdate(); + } + /** * Initializes GUI elements, input management, sound management, & other interface tools. */ private initUI() { stendhal.ui = stendhal.ui || {}; stendhal.ui.equip = singletons.getInventory(); - stendhal.ui.html = singletons.getHTMLManager(); - stendhal.ui.touch = singletons.getTouchHandler(); - stendhal.ui.viewport = singletons.getViewPort(); - // alias for backward-compat until changed in all source - stendhal.ui.gamewindow = stendhal.ui.viewport; - stendhal.ui.soundMan = singletons.getSoundManager(); - // update sound levels from config at startup - stendhal.ui.soundMan.onConfigUpdate(); + stendhal.ui.getMenuStyle = Globals.getMenuStyle; } - /** - * Builds initial zone for user to enter world. - */ - private initZone() { - stendhal.zone = singletons.getZone(); - stendhal.zone.ground = new Ground(); - } /** * Main startup routines. @@ -165,11 +163,11 @@ export class Client { singletons.getEventRegistry().init(); // initialize tileset animation data - singletons.getTileStore().init(); + TileStore.get().init(); // initialize emoji data singletons.getEmojiStore().init(); // initialize outfit data - stendhal.data.outfit.init(); + OutfitStore.get().init(); new DesktopUserInterfaceFactory().create(); @@ -192,11 +190,11 @@ export class Client { stendhal.ui.globalInternalWindow = new DialogHandler(); // pre-cache images & sounds - stendhal.data.sprites.startupCache(); - singletons.getSoundManager().startupCache(); + htmlImageStore.startupCache(); + stendhal.sound.startupCache(); if (document.getElementById("viewport")) { - stendhal.ui.gamewindow.draw.apply(stendhal.ui.gamewindow, arguments); + ViewPort.get().draw(); } } @@ -247,11 +245,14 @@ export class Client { * Registers Marauroa event handlers. */ registerMarauroaEventHandlers() { - marauroa.clientFramework.onDisconnect = function(_reason: string, _error: string) { + marauroa.clientFramework.onDisconnect = function(_reason: string, _code: number, _wasClean: boolean) { if (!Client.instance.unloading) { Chat.logH("error", "Disconnected from server."); + if (window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1" && window.location.hostname !== "::1") { + window.location.href = "/account/mycharacters.html"; + } } - }; + }.bind(this); marauroa.clientFramework.onLoginRequired = function(config: Record) { if (config["client_login_url"]) { @@ -268,26 +269,26 @@ export class Client { ui.createSingletonFloatingWindow( "Login", new LoginDialog(), - 100, 50); - }; + 100, 50).enableCloseButton(false); + }.bind(this); marauroa.clientFramework.onCreateAccountAck = function(username: string) { // TODO: We should login automatically alert("Account succesfully created, please login."); window.location.reload(); - }; + }.bind(this); marauroa.clientFramework.onCreateCharacterAck = function(charname: string, _template: any) { // Client.get().chooseCharacter(charname); - }; + }.bind(this); marauroa.clientFramework.onLoginFailed = function(_reason: string, _text: string) { alert("Login failed. " + _text); // TODO: Server closes the connection, so we need to open a new one window.location.reload(); - }; + }.bind(this); - marauroa.clientFramework.onAvailableCharacterDetails = function(characters: {[key: string]: RPObject}) { + marauroa.clientFramework.onAvailableCharacterDetails = (characters: {[key: string]: RPObject}) => { SingletonFloatingWindow.closeAll(); if (!Object.keys(characters).length && this.username) { marauroa.clientFramework.createCharacter(this.username, {}); @@ -309,7 +310,7 @@ export class Client { ui.createSingletonFloatingWindow( "Choose Character", new ChooseCharacterDialog(characters), - 100, 50); + 100, 50).enableCloseButton(false); }; marauroa.clientFramework.onTransferREQ = function(items: any) { @@ -319,9 +320,9 @@ export class Client { } items[i]["ack"] = true; } - }; + }.bind(this); - marauroa.clientFramework.onTransfer = function(items: any) { + marauroa.clientFramework.onTransfer = (items: any) => { var data = {} as any; var zoneName = "" for (var i in items) { @@ -333,34 +334,13 @@ export class Client { this.onDataMap(items[i]["data"]); } } - stendhal.data.map.onTransfer(zoneName, data); + TileMap.get().onTransfer(zoneName, data); }; // update user interface on perceptions if (document.getElementById("viewport")) { // override perception listener - marauroa.perceptionListener = new PerceptionListener(marauroa.perceptionListener); - marauroa.perceptionListener.onPerceptionEnd = function(_type: Int8Array, _timestamp: number) { - stendhal.zone.sortEntities(); - (ui.get(UIComponentEnum.MiniMap) as MiniMapComponent).draw(); - (ui.get(UIComponentEnum.BuddyList) as BuddyListComponent).update(); - stendhal.ui.equip.update(); - (ui.get(UIComponentEnum.PlayerEquipment) as PlayerEquipmentComponent).update(); - if (!this.loaded) { - this.loaded = true; - // delay visibile change of client a little to allow for initialisation in the background for a smoother experience - window.setTimeout(function() { - let body = document.getElementById("body")!; - body.style.cursor = "auto"; - document.getElementById("client")!.style.display = "block"; - document.getElementById("loginpopup")!.style.display = "none"; - - // initialize observer after UI is ready - singletons.getUIUpdateObserver().init(); - ui.onDisplayReady(); - }, 300); - } - } + marauroa.perceptionListener = new StendhalPerceptionListener(); } } @@ -373,7 +353,7 @@ export class Client { Chat.log("client", "Loading world..."); // play login sound for this user - singletons.getSoundManager().playGlobalizedEffect("ui/login"); + stendhal.sound.playGlobalizedEffect("ui/login"); } /** @@ -408,18 +388,19 @@ export class Client { document.getElementById("body")!.addEventListener("mouseenter", stendhal.main.onMouseEnter); - var gamewindow = document.getElementById("viewport")!; + let gamewindow = document.getElementById("viewport")!; + let viewPort = ViewPort.get(); gamewindow.setAttribute("draggable", "true"); - gamewindow.addEventListener("mousedown", stendhal.ui.gamewindow.onMouseDown); - gamewindow.addEventListener("dblclick", stendhal.ui.gamewindow.onMouseDown); - gamewindow.addEventListener("dragstart", stendhal.ui.gamewindow.onDragStart); - gamewindow.addEventListener("mousemove", stendhal.ui.gamewindow.onMouseMove); - gamewindow.addEventListener("touchstart", stendhal.ui.gamewindow.onMouseDown); - gamewindow.addEventListener("touchend", stendhal.ui.gamewindow.onTouchEnd); - gamewindow.addEventListener("dragover", stendhal.ui.gamewindow.onDragOver); - gamewindow.addEventListener("drop", stendhal.ui.gamewindow.onDrop); - gamewindow.addEventListener("contextmenu", stendhal.ui.gamewindow.onContentMenu); - gamewindow.addEventListener("wheel", stendhal.ui.gamewindow.onMouseWheel); + gamewindow.addEventListener("mousedown", viewPort.onMouseDown); + gamewindow.addEventListener("dblclick", viewPort.onMouseDown); + gamewindow.addEventListener("dragstart", viewPort.onDragStart); + gamewindow.addEventListener("mousemove", viewPort.onMouseMove); + gamewindow.addEventListener("touchstart", viewPort.onMouseDown, {passive: true}); + gamewindow.addEventListener("touchend", viewPort.onTouchEnd); + gamewindow.addEventListener("dragover", viewPort.onDragOver); + gamewindow.addEventListener("drop", viewPort.onDrop); + gamewindow.addEventListener("contextmenu", viewPort.onContentMenu); + gamewindow.addEventListener("wheel", viewPort.onMouseWheel, {passive: true}); singletons.getJoystickController().registerGlobalEventHandlers(); @@ -432,7 +413,7 @@ export class Client { // main sound button const soundButton = document.getElementById("soundbutton")!; soundButton.addEventListener("click", function(e: Event) { - stendhal.ui.soundMan.toggleSound(); + stendhal.sound.toggleSound(); }); // click/touch indicator @@ -447,7 +428,7 @@ export class Client { document.addEventListener("click", Client.handleClickIndicator); document.addEventListener("touchend", Client.handleClickIndicator); }; - click_indicator.src = stendhal.paths.gui + "/click_indicator.png"; + click_indicator.src = Paths.gui + "/click_indicator.png"; } /** @@ -457,35 +438,43 @@ export class Client { * Information about map. */ onDataMap(data: any) { + let map = TileMap.get(); + let viewPort = ViewPort.get(); var zoneinfo = {} as {[key: string]: string}; - var deserializer = marauroa.Deserializer.fromBase64(data); + var deserializer = Deserializer.fromBase64(data); deserializer.readAttributes(zoneinfo); (ui.get(UIComponentEnum.ZoneInfo) as ZoneInfoComponent).zoneChange(zoneinfo); // global zone music const musicVolume = parseFloat(zoneinfo["music_volume"]); - stendhal.ui.soundMan.playSingleGlobalizedMusic(zoneinfo["music"], + stendhal.sound.playSingleGlobalizedMusic(zoneinfo["music"], !Number.isNaN(musicVolume) ? musicVolume : 1.0); - // coloring information - if (zoneinfo["color_method"]) { - stendhal.ui.gamewindow.setColorMethod(zoneinfo["color_method"]); - } - if (zoneinfo["blend_method"]) { - stendhal.ui.gamewindow.setBlendMethod(zoneinfo["blend_method"]); - } - if (zoneinfo["color"]) { - const hsl = Color.numToHSL(Number(zoneinfo["color"])); - stendhal.ui.gamewindow.HSLFilter = hsl.toString(); - // deprecated - stendhal.ui.gamewindow.filter = "hue-rotate(" + hsl.H + "deg) saturate(" + hsl.S - + ") brightness(" + hsl.L + ")"; - } else { - stendhal.ui.gamewindow.HSLFilter = undefined; - stendhal.ui.gamewindow.filter = undefined; + // parallax background + if (stendhal.config.getBoolean("effect.parallax")) { + map.setParallax(zoneinfo["parallax"]); + map.setIgnoredTiles(zoneinfo["parallax_ignore_tiles"]); } - singletons.getWeatherRenderer().update(zoneinfo["weather"]); + // coloring information + if (zoneinfo["color"] && stendhal.config.getBoolean("effect.lighting")) { + if (zoneinfo["color_method"]) { + viewPort.setColorMethod(zoneinfo["color_method"]); + } + if (zoneinfo["blend_method"]) { + viewPort.setBlendMethod(zoneinfo["blend_method"]); + } + const hsl = Color.numToHSL(Number(zoneinfo["color"])); + viewPort.HSLFilter = hsl.toString(); + // deprecated + viewPort.filter = "hue-rotate(" + hsl.H + "deg) saturate(" + hsl.S + + ") brightness(" + hsl.L + ")"; + } else { + viewPort.HSLFilter = undefined; + viewPort.filter = undefined; + } + + WeatherRenderer.get().update(zoneinfo["weather"]); } /** @@ -500,7 +489,7 @@ export class Client { */ onMouseEnter(e: MouseEvent) { // use Stendhal's built-in cursor for entire page - (e.target as HTMLElement).style.cursor = "url(" + stendhal.paths.sprites + "/cursor/normal.png) 1 3, auto"; + (e.target as HTMLElement).style.cursor = "url(" + Paths.sprites + "/cursor/normal.png) 1 3, auto"; } /** diff --git a/src/js/stendhal/EventRegistry.ts b/src/js/stendhal/EventRegistry.ts deleted file mode 100644 index 2b652d769b..0000000000 --- a/src/js/stendhal/EventRegistry.ts +++ /dev/null @@ -1,422 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -declare var marauroa: any; -declare var stendhal: any; - -import { SoundID } from "./data/sound/SoundID"; - -import { RPEntity } from "./entity/RPEntity"; -import { RPObject } from "./entity/RPObject"; - -import { ChatOptionsEvent } from "./event/ChatOptionsEvent"; -import { ExamineEvent } from "./event/ExamineEvent"; -import { GroupChangeEvent } from "./event/GroupChangeEvent"; -import { GroupInviteEvent } from "./event/GroupInviteEvent"; -import { PlayerLoggedOnEvent } from "./event/PlayerLoggedOnEvent"; -import { PlayerLoggedOutEvent } from "./event/PlayerLoggedOutEvent"; -import { ProgressStatusEvent } from "./event/ProgressStatusEvent"; -import { RPEvent } from "./event/RPEvent"; -import { TradeEvent } from "./event/TradeEvent"; -import { ViewChangeEvent } from "./event/ViewChangeEvent"; - -import { ui } from "./ui/UI"; - -import { DialogContentComponent } from "./ui/toolkit/DialogContentComponent"; - -import { Chat } from "./util/Chat"; - - -export class EventRegistry { - - private initialized = false; - - /** Singleton instance. */ - private static instance: EventRegistry; - - - /** - * Retrieves singleton instance. - */ - static get(): EventRegistry { - if (!EventRegistry.instance) { - EventRegistry.instance = new EventRegistry(); - } - return EventRegistry.instance; - } - - /** - * Hidden singleton constructor. - */ - private constructor() { - // do nothing - } - - /** - * Registers standard events. - */ - init() { - if (this.initialized) { - console.warn("tried to re-initialize EventRegistry"); - return; - } - this.initialized = true; - - this.register("chat_options", new ChatOptionsEvent()); - this.register("examine", new ExamineEvent()); - this.register("group_change_event", new GroupChangeEvent()); - this.register("group_invite_event", new GroupInviteEvent()); - this.register("progress_status_event", new ProgressStatusEvent()); - this.register("trade_state_change_event", new TradeEvent()); - - this.register("attack", { - execute: function(entity: RPEntity) { - var target = entity.getAttackTarget(); - if (!target) { - return; - } - if (this.hasOwnProperty("hit")) { - var damage = parseInt(this["damage"], 10); - if (damage !== 0) { - target.onDamaged(entity, damage); - } else { - target.onBlocked(entity); - } - } else { - target.onMissed(entity); - } - entity.onAttackPerformed(parseInt(this["type"], 10), this.hasOwnProperty("ranged"), this["weapon"]); - } - }); // attack - - this.register("bestiary", { - execute: function(rpobject: RPObject) { - if (!this.hasOwnProperty("enemies")) { - // FIXME: proper logging of errors? - console.log("ERROR: event does not have \"enemies\" attribute"); - return; - } - - /* --- title & headers --- */ - - const header = ["Bestiary:", "\"???\" = unknown"]; - const hasRare = this["enemies"].includes("(rare)"); - const hasAbnormal = this["enemies"].includes("(abnormal)"); - - // show explanation of "rare" & "abnormal" creatures in header - if (hasRare || hasAbnormal) { - let subheader = ""; - if (!hasRare) { - subheader += "\"abnormal\""; - } else { - subheader += "\"rare\""; - if (hasAbnormal) { - subheader += " and \"abnormal\""; - } - } - header[1] = subheader + " creatures not required for achievements"; - } - - /* --- contents --- */ - - // TODO: clean up columns - - const content = new class extends DialogContentComponent {} ("empty-div-template"); - content.setConfigId("bestiary"); - content.componentElement.classList.add("bestiary"); - - const layout = document.createElement("div"); - layout.className = "horizontalgroup stretchgroup"; - const col1 = document.createElement("div"); - const col2 = document.createElement("div"); - const col3 = document.createElement("div"); - col1.className = "verticalgroup stretchgroup"; - col2.className = "verticalgroup stretchgroup"; - col3.className = "verticalgroup stretchgroup"; - - const t1 = document.createElement("div"); - const t2 = document.createElement("div"); - const t3 = document.createElement("div"); - t1.classList.add("shopcol"); - t2.classList.add("shopcol"); - t3.classList.add("shopcol"); - t1.textContent = "Name"; - t2.textContent = "Solo"; - t3.textContent = "Shared"; - - col1.appendChild(t1); - col2.appendChild(t2); - col3.appendChild(t3); - - for (const e of this["enemies"].split(";")) { - const info = e.split(","); - // empty text will not render outline - let solo = "-"; - let shared = "-"; - if (info[1] == "true") { - solo = "✔"; - } - if (info[2] == "true") { - shared = "✔"; - } - - const l1 = document.createElement("div"); - const l2 = document.createElement("div"); - const l3 = document.createElement("div"); - l1.classList.add("shopcol"); - l2.classList.add("shopcol"); - l3.classList.add("shopcol"); - - l1.textContent = info[0]; - l2.textContent = solo; - l3.textContent = shared; - - col1.appendChild(l1); - col2.appendChild(l2); - col3.appendChild(l3); - } - - layout.appendChild(col1); - layout.appendChild(col2); - layout.appendChild(col3); - content.componentElement.appendChild(layout); - - stendhal.ui.globalInternalWindow.set(ui.createSingletonFloatingWindow(header.join(" "), - content, 20, 20)); - } - }); // bestiary - - this.register("global_visual_effect", { - execute: function(rpobject: RPObject) { - // TODO: new GlobalVisualEffectEvent(); - } - }); // global_visual_effect - - this.register("image_event", { - execute: function(rpobject: RPObject) { - // TODO: new ImageEffectEvent(); - console.log("image_event", this, rpobject); - } - }); // image_event - - this.register("player_logged_on", new PlayerLoggedOnEvent()); - this.register("player_logged_out", new PlayerLoggedOutEvent()); - - this.register("private_text", { - soundTextEvents: { - "privmsg": true, - "support": true, - "tutorial": true - }, - - execute: function(rpobject: RPObject) { - const ttype = this["texttype"].toLowerCase(); - const msg = this["text"].replace(/\\r\\n/g, "\n").replace(/\\r/g, "\n"); - - let profile; - if (this.hasOwnProperty("profile")) { - profile = this["profile"]; - } else if (ttype === "tutorial") { - profile = "floattingladynpc"; - } - - if (ttype === "server" && msg.includes("\n")) { - Chat.log(ttype, msg.split("\n"), undefined, profile); - } else { - // scene settings messages should not disturb playing, just create some atmosphere - const headed = ttype !== "scene_setting"; - Chat.log(ttype, msg, undefined, profile, headed); - } - - // play notification sound - const notif = stendhal.config.get("chat.private.sound"); - if (notif && this.soundTextEvents[ttype]) { - stendhal.ui.soundMan.playGlobalizedEffect(notif); - } - } - }); // private_text - - this.register("reached_achievement", { - execute: function(rpobject: RPObject) { - stendhal.ui.gamewindow.addAchievementNotif(this["category"], this["title"], this["description"]); - } - }); // reached_achievement - - this.register("show_item_list", { - execute: function(rpobject: RPObject) { - let title = "Items"; - let caption = ""; - let items = []; - - if (this.hasOwnProperty("title")) { - title = this["title"]; - } - if (this.hasOwnProperty("caption")) { - caption = this["caption"]; - } - if (this.hasOwnProperty("content")) { - for (var obj in this["content"]) { - if (this["content"].hasOwnProperty(obj)) { - var slotObj = this["content"][obj]; - var data = this["content"][obj]["a"]; - const i = { - clazz: data["class"], - subclass: data["subclass"], - img: data["class"] + "/" + data["subclass"] + ".png", - price: data["price"], - desc: data["description_info"] - } - - // seller shops prefix prices with "-" - if (i.price.startsWith("-")) { - i.price = i.price.substr(1); - } - items.push(i); - } - } - } - - const content = new class extends DialogContentComponent {} ("empty-div-template"); - content.componentElement.classList.add("shopsign"); - const captionElement = document.createElement("div"); - captionElement.className = "horizontalgroup shopcaption"; - captionElement.textContent = caption + "\nItem\t-\tPrice\t-\tDescription"; - content.componentElement.appendChild(captionElement); - const itemList = document.createElement("div"); - itemList.className = "shoplist"; - content.componentElement.appendChild(itemList); - - // TODO: organize in columns & show item sprites - for (const i of items) { - const row = document.createElement("div"); - row.className = "horizontalgroup shoprow"; - const img = document.createElement("div"); - img.className = "shopcol"; - img.appendChild(stendhal.data.sprites.get(stendhal.paths.sprites + "/items/" + i.img)); - row.appendChild(img); - const price = document.createElement("div"); - price.className = "shopcol"; - price.textContent = i.price; - row.appendChild(price); - const desc = document.createElement("div"); - desc.className = "shopcol shopcolr"; - desc.textContent = i.desc; - row.appendChild(desc); - itemList.appendChild(row); - } - - stendhal.ui.globalInternalWindow.set( - ui.createSingletonFloatingWindow(title, content, 20, 20)); - } - }); // show_item_list - - this.register("show_outfit_list", { - execute: function(rpobject: RPObject) { - let title = "Outfits"; - let caption = ""; - let outfits = []; - - if (this.hasOwnProperty("title")) { - title = this["title"]; - } - if (this.hasOwnProperty("caption")) { - caption = this["caption"]; - } - if (this.hasOwnProperty("outfits")) { - for (let o of this["outfits"].split(":")) { - o = o.split(";"); - if (o.length > 2) { - outfits.push([o[0], o[1], o[2]]); - } - } - } - if (this.hasOwnProperty("show_base")) { - //Chat.log("normal", this["show_base"]); - } - - const content = new class extends DialogContentComponent {} ("empty-div-template"); - content.componentElement.classList.add("shopsign"); - const captionElement = document.createElement("div"); - captionElement.className = "horizontalgroup shopcaption"; - captionElement.textContent = caption; - content.componentElement.appendChild(captionElement); - const itemList = document.createElement("div"); - itemList.className = "shoplist"; - content.componentElement.appendChild(itemList); - - // TODO: organize in columns & show outfit sprites - for (const o of outfits) { - const row = document.createElement("div"); - row.className = "horizontalgroup shoprow"; - row.textContent = o[0] + ": " + o[2]; - itemList.appendChild(row); - } - - stendhal.ui.globalInternalWindow.set( - ui.createSingletonFloatingWindow(title, content, 20, 20)); - } - }); // show_outfit_list - - this.register("sound_event", { - execute: function(rpobject: RPObject) { - var volume = 1; - // Adjust by the server specified volume, if any - if (this.hasOwnProperty("volume")) { - volume *= parseInt(this["volume"], 10) / 100; - } - var radius = parseInt(this["radius"], 10); - - let sound = this["sound"]; - const sound_id = this["sound_id"]; - if (sound_id) { - sound = SoundID[sound_id]; - } - - stendhal.ui.soundMan.playLocalizedEffect(rpobject["_x"], rpobject["_y"], radius, this["layer"], sound, volume); - } - }); // sound_event - - this.register("text", { - execute: function(rpobject: RPObject) { - if (this.hasOwnProperty("range")) { - rpobject.say(this["text"], this["range"]); - } else { - rpobject.say(this["text"]); - } - } - }); // text - - this.register("transition_graph", { - execute: function(rpobject: RPObject) { - // TODO: new TransitionGraphEvent(); - } - }); // transition_graph - - this.register("view_change", new ViewChangeEvent()); - } - - /** - * Registers a new event. - * - * @param _type - * String identifier. - * @param _event - * Event to be registered. - */ - register(_type: string, _event: any) { - if (_event instanceof RPEvent) { - marauroa.rpeventFactory[_type] = _event; - } else { - marauroa.rpeventFactory[_type] = - marauroa.util.fromProto(marauroa.rpeventFactory["_default"], _event); - } - } -} diff --git a/src/js/stendhal/PerceptionListener.ts b/src/js/stendhal/PerceptionListener.ts index f2bccf6fdf..3e0ea4601a 100644 --- a/src/js/stendhal/PerceptionListener.ts +++ b/src/js/stendhal/PerceptionListener.ts @@ -9,29 +9,24 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { ui } from "ui/UI"; +import { stendhal } from "./stendhal"; -import { RPObject } from "./entity/RPObject"; +import { RPObject, PerceptionListener } from "marauroa"; +import { UIComponentEnum } from "ui/UIComponentEnum"; +import { MiniMapComponent } from "ui/component/MiniMapComponent"; +import { BuddyListComponent } from "ui/component/BuddyListComponent"; +import { PlayerEquipmentComponent } from "ui/component/PlayerEquipmentComponent"; +import { singletons } from "SingletonRepo"; +import { Client } from "Client"; +import { Zone } from "entity/Zone"; /** * Class to override `marauroa.perceptionListener` & handle creating * User object. */ -export class PerceptionListener { - [key: string]: any; - private readonly _super: any; - - - constructor(perceptionListener: any) { - this._super = perceptionListener; - for (const prop of Object.getOwnPropertyNames(perceptionListener)) { - if (typeof(this[prop]) === "undefined") { - // members & methods that are not overridden by this class - this[prop] = perceptionListener[prop]; - } - } - } +export class StendhalPerceptionListener extends PerceptionListener { /** * Action when an object is added to the world. @@ -42,7 +37,7 @@ export class PerceptionListener { * `true` if object was added. * @see marauroa.perceptionListener.onAdded */ - onAdded(obj: RPObject): boolean { + override onAdded(obj: RPObject): boolean { if (obj["c"] === "player") { if (obj.hasOwnProperty("a")) { if (obj["a"]["name"] === stendhal.session.getCharName()) { @@ -51,6 +46,28 @@ export class PerceptionListener { } } } - return this._super.onAdded(obj); + return super.onAdded(obj); + } + + override onPerceptionEnd(_type: Int8Array, _timestamp: number) { + Zone.get().sortEntities(); + (ui.get(UIComponentEnum.MiniMap) as MiniMapComponent).draw(); + (ui.get(UIComponentEnum.BuddyList) as BuddyListComponent).update(); + stendhal.ui.equip.update(); + (ui.get(UIComponentEnum.PlayerEquipment) as PlayerEquipmentComponent).update(); + if (!Client.get().loaded) { + Client.get().loaded = true; + // delay visibile change of client a little to allow for initialisation in the background for a smoother experience + window.setTimeout(function() { + let body = document.getElementById("body")!; + body.style.cursor = "auto"; + document.getElementById("client")!.style.display = "block"; + document.getElementById("loginpopup")!.style.display = "none"; + + // initialize observer after UI is ready + singletons.getUIUpdateObserver().init(); + ui.onDisplayReady(); + }, 300); + } } } diff --git a/src/js/stendhal/SingletonRepo.ts b/src/js/stendhal/SingletonRepo.ts index c0544ddcd3..229007b928 100644 --- a/src/js/stendhal/SingletonRepo.ts +++ b/src/js/stendhal/SingletonRepo.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2024 - Faiumoni e. V. * + * Copyright © 2003-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -10,43 +10,35 @@ ***************************************************************************/ import { Client } from "./Client"; -import { EventRegistry } from "./EventRegistry"; +import { EventRegistry } from "./event/EventRegistry"; import { SlashActionRepo } from "./SlashActionRepo"; -import { Zone } from "./entity/Zone"; - import { ConfigManager } from "./util/ConfigManager"; import { DownloadUtil } from "./util/DownloadUtil"; import { FacingHandler } from "./util/FacingHandler"; import { KeyHandler } from "./util/KeyHandler"; import { SessionManager } from "./util/SessionManager"; -import { WeatherRenderer } from "./util/WeatherRenderer"; -import { CStatus } from "./data/CStatus"; import { CacheManager } from "./data/CacheManager"; +import { CStatus } from "./data/CStatus"; import { EmojiStore } from "./data/EmojiStore"; import { GroupManager } from "./data/GroupManager"; -import { Map } from "./data/Map"; -import { OutfitStore } from "./data/OutfitStore"; import { Paths } from "./data/Paths"; -import { SpriteStore, store } from "./data/SpriteStore"; -import { TileStore } from "./data/TileStore"; +import { LoopedSoundSourceManager } from "./data/sound/LoopedSoundSourceManager"; +import { SoundManager } from "./data/sound/SoundManager"; + +import { Inventory } from "./ui/Inventory"; +import { SoftwareJoystickController } from "./ui/SoftwareJoystickController"; import { ui } from "./ui/UI"; import { UIComponentEnum } from "./ui/UIComponentEnum"; -import { HeldObjectManager } from "./ui/HeldObject"; -import { HTMLManager } from "./ui/HTMLManager"; -import { Inventory } from "./ui/Inventory"; -import { LoopedSoundSourceManager } from "./ui/LoopedSoundSourceManager"; -import { SoftwareJoystickController } from "./ui/SoftwareJoystickController"; -import { SoundManager } from "./ui/SoundManager"; -import { TouchHandler } from "./ui/TouchHandler"; import { UIUpdateObserver } from "./ui/UIUpdateObserver"; -import { ViewPort } from "./ui/ViewPort"; import { ChatInputComponent } from "./ui/component/ChatInputComponent"; - +/** + * @Deprecated use Class.get() directly to prevent dependency chain + */ export class SingletonRepo { static getCStatus(): CStatus { @@ -89,14 +81,6 @@ export class SingletonRepo { return GroupManager.get(); } - static getHeldObjectManager(): HeldObjectManager { - return HeldObjectManager.get(); - } - - static getHTMLManager(): HTMLManager { - return HTMLManager.get(); - } - static getInventory(): Inventory { return Inventory.get(); } @@ -113,14 +97,6 @@ export class SingletonRepo { return LoopedSoundSourceManager.get(); } - static getMap(): Map { - return Map.get(); - } - - static getOutfitStore(): OutfitStore { - return OutfitStore.get(); - } - static getPaths(): typeof Paths { return Paths; } @@ -137,33 +113,10 @@ export class SingletonRepo { return SoundManager.get(); } - static getSpriteStore(): SpriteStore { - return store; - } - - static getTileStore(): TileStore { - return TileStore.get(); - } - - static getTouchHandler(): TouchHandler { - return TouchHandler.get(); - } - static getUIUpdateObserver(): UIUpdateObserver { return UIUpdateObserver.get(); } - static getViewPort(): ViewPort { - return ViewPort.get(); - } - - static getWeatherRenderer(): WeatherRenderer { - return WeatherRenderer.get(); - } - - static getZone(): Zone { - return Zone.get(); - } } export const singletons = SingletonRepo; diff --git a/src/js/stendhal/SlashActionRepo.ts b/src/js/stendhal/SlashActionRepo.ts index 7bf7107a31..b528f5addd 100644 --- a/src/js/stendhal/SlashActionRepo.ts +++ b/src/js/stendhal/SlashActionRepo.ts @@ -9,8 +9,8 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "./stendhal"; import { singletons } from "./SingletonRepo"; @@ -19,6 +19,7 @@ import { DebugAction } from "./action/DebugAction"; import { OpenWebsiteAction } from "./action/OpenWebsiteAction"; import { ProgressStatusAction } from "./action/ProgressStatusAction"; import { ReTellAction } from "./action/ReTellAction"; +import { ScreenCaptureAction } from "./action/ScreenCaptureAction"; import { SettingsAction } from "./action/SettingsAction"; import { SlashActionImpl } from "./action/SlashAction"; import { TellAction } from "./action/TellAction"; @@ -29,6 +30,7 @@ import { UIComponentEnum } from "./ui/UIComponentEnum"; import { ChatLogComponent } from "./ui/component/ChatLogComponent"; import { Chat } from "./util/Chat"; +import { Debug } from "./util/Debug"; /** @@ -132,6 +134,7 @@ export class SlashActionRepo { "TOOLS": [ "progressstatus", "screenshot", + //"screencap", "atlas", "beginnersguide" ], @@ -220,6 +223,11 @@ export class SlashActionRepo { {type: "group", sparams: "status"} ] }; + + if (Debug.isActive("screencap")) { + grouping["TOOLS"].push("screencap"); + } + return { info: [ "For a detailed reference, visit #https://stendhalgame.org/wiki/Stendhal_Manual", @@ -758,7 +766,7 @@ export class SlashActionRepo { execute: (type: string, params: string[], remainder: string): boolean => { const newMode = !stendhal.config.getBoolean("input.doubleclick"); stendhal.config.set("input.doubleclick", newMode); - stendhal.ui.gamewindow.updateClickMode(); + ViewPort.get().updateClickMode(); if (newMode) { Chat.log("info", "Click mode is now set to double click."); @@ -1057,7 +1065,7 @@ export class SlashActionRepo { "mute": SlashActionImpl = { execute: (type: string, params: string[], remainder: string): boolean => { - singletons.getSoundManager().toggleSound(); + stendhal.sound.toggleSound(); if (stendhal.config.getBoolean("sound")) { Chat.log("info", "Sounds are now on."); } else { @@ -1122,6 +1130,8 @@ export class SlashActionRepo { } }; + "screencap" = new ScreenCaptureAction(); + "sentence": SlashActionImpl = { execute: (type: string, params: string[], remainder: string): boolean => { if (params == null) { @@ -1169,22 +1179,22 @@ export class SlashActionRepo { const layername = params[0]; let vol = params[1]; if (typeof(layername) === "undefined") { - const layers = ["master", ...stendhal.ui.soundMan.getLayerNames()]; + const layers = ["master", ...stendhal.sound.getLayerNames()]; Chat.log("info", "Please use /volume to adjust the volume."); Chat.log("client", " is one of \"" + layers.join("\", \"") + "\""); Chat.log("client", " is a number in the range 0 to 100."); Chat.log("client", "Current volume levels:"); for (const l of layers) { - Chat.log("client", "  - " + l + " -> " + stendhal.ui.soundMan.getVolume(l) * 100); + Chat.log("client", "  - " + l + " -> " + stendhal.sound.getVolume(l) * 100); } } else if (typeof(vol) !== "undefined") { if (!/^\d+$/.test(vol)) { Chat.log("error", "Value must be a number."); return true; } - if (stendhal.ui.soundMan.setVolume(layername, parseInt(vol, 10) / 100)) { + if (stendhal.sound.setVolume(layername, parseInt(vol, 10) / 100)) { Chat.log("client", "Channel \"" + layername + "\" volume set to " - + (stendhal.ui.soundMan.getVolume(layername) * 100) + "."); + + (stendhal.sound.getVolume(layername) * 100) + "."); } else { Chat.log("error", "Unknown layer \"" + layername + "\"."); } diff --git a/src/js/stendhal/action/AboutAction.ts b/src/js/stendhal/action/AboutAction.ts index bce90a0ea3..e5ba9ee862 100644 --- a/src/js/stendhal/action/AboutAction.ts +++ b/src/js/stendhal/action/AboutAction.ts @@ -10,7 +10,6 @@ * * ***************************************************************************/ -declare var stendhal: any; import { ParamList } from "./ParamList"; import { SlashAction } from "./SlashAction"; @@ -20,6 +19,7 @@ import { ui } from "../ui/UI"; import { AboutDialog } from "../ui/dialog/AboutDialog"; import { Pair } from "../util/Pair"; +import { stendhal } from "../stendhal"; export class AboutAction extends SlashAction { @@ -28,7 +28,7 @@ export class AboutAction extends SlashAction { execute(_type: string, _params: string[], _remainder: string): boolean { const wstate = stendhal.config.getWindowState("about"); - const offset = stendhal.ui.getPageOffset(); + const offset = ui.getPageOffset(); const content = new AboutDialog(); const dialog = ui.createSingletonFloatingWindow( diff --git a/src/js/stendhal/action/DebugAction.ts b/src/js/stendhal/action/DebugAction.ts index d1666521df..7b92d9fda7 100644 --- a/src/js/stendhal/action/DebugAction.ts +++ b/src/js/stendhal/action/DebugAction.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2022-2024 - Stendhal * + * (C) Copyright 2022-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,20 +9,20 @@ * * ***************************************************************************/ -import { singletons } from "../SingletonRepo"; - import { ui } from "../ui/UI"; import { UIComponentEnum } from "../ui/UIComponentEnum"; -import { SettingsDialog } from "../ui/dialog/SettingsDialog"; import { FloatingWindow } from "../ui/toolkit/FloatingWindow"; -import { SlashAction } from "./SlashAction"; -import { Chat } from "../util/Chat"; -import { ShowFloatingWindowComponent } from "../ui/component/ShowFloatingWindowComponent"; import { Panel } from "ui/toolkit/Panel"; +import { ShowFloatingWindowComponent } from "../ui/component/ShowFloatingWindowComponent"; +import { SlashAction } from "./SlashAction"; -declare var marauroa: any; -declare var stendhal: any; +import { Chat } from "../util/Chat"; +import { Debug } from "../util/Debug"; + +import { marauroa } from "marauroa"; +import { WeatherRenderer } from "util/WeatherRenderer"; +import { stendhal } from "../stendhal"; /** * performances debugging actions @@ -46,11 +46,16 @@ export class DebugAction extends SlashAction { } else if (params[0] === "log") { Chat.debugLogEnabled = true; } else if (params[0] === "settings") { - SettingsDialog.debugging = !SettingsDialog.debugging; - Chat.log("client", "experimental settings " + (SettingsDialog.debugging ? "enabled" : "disabled")); + Chat.log("client", "Experimental settings " + (Debug.toggle("settings") ? "enabled" + : "disabled")); } else if (params[0] === "touch") { - stendhal.ui.touch.setDebuggingEnabled(!stendhal.ui.touch.isDebuggingEnabled()); - Chat.log("client", "touch debugging " + (stendhal.ui.touch.isDebuggingEnabled() ? "enabled" : "disabled")); + Chat.log("client", "Touch debugging " + (Debug.toggle("touch") ? "enabled" : "disabled")); + } else if (params[0] === "screencap") { + Debug.setActive("screencap", !Debug.isActive("screencap")); + Chat.log("client", "Screen capture debugging " + (Debug.isActive("screencap") ? "enabled" + : "disabled")); + } else if(params[0] === "light") { + Debug.toggle("light"); } return true; } @@ -62,7 +67,8 @@ export class DebugAction extends SlashAction { " /debug ui [pop]", " /debug weather []", " /debug settings", - " /debug touch" + " /debug touch", + " /debug light" ]; Chat.log("client", usage); } @@ -131,14 +137,9 @@ export class DebugAction extends SlashAction { if (weather) { weather = weather.replace(/ /g, "_"); - const wfilename = stendhal.paths.weather + "/" + weather + ".png"; - if (!stendhal.data.sprites.getCached(wfilename)) { - Chat.logH("error", "unknown weather: " + wfilename); - return; - } + WeatherRenderer.get().update(weather); } - singletons.getWeatherRenderer().update(weather); - } + } }; diff --git a/src/js/stendhal/action/ScreenCaptureAction.ts b/src/js/stendhal/action/ScreenCaptureAction.ts new file mode 100644 index 0000000000..378fda38bb --- /dev/null +++ b/src/js/stendhal/action/ScreenCaptureAction.ts @@ -0,0 +1,42 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { SlashAction } from "./SlashAction"; + +import { ScreenCapture } from "../util/ScreenCapture"; + +import { stendhal } from "../stendhal"; +import { ViewPort } from "ui/ViewPort"; + + +export class ScreenCaptureAction extends SlashAction { + + readonly minParams = 0; + readonly maxParams = 0; + + override desc = "Start or stop capturing video of the client viewport"; + + private recorder?: ScreenCapture; + + + execute(type: string, params: string[], remainder: string): boolean { + if (this.recorder && ScreenCapture.isActive()) { + // currently recording + this.recorder.stop(); + this.recorder = undefined; + return true; + } + this.recorder = new ScreenCapture(); + this.recorder.start(ViewPort.get().getElement()); + return true; + } +} diff --git a/src/js/stendhal/action/SettingsAction.ts b/src/js/stendhal/action/SettingsAction.ts index 13dc33f351..61e00d60d0 100644 --- a/src/js/stendhal/action/SettingsAction.ts +++ b/src/js/stendhal/action/SettingsAction.ts @@ -10,7 +10,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { SlashAction } from "./SlashAction"; @@ -28,7 +28,7 @@ export class SettingsAction extends SlashAction { execute(_type: string, _params: string[], _remainder: string): boolean { const wstate = stendhal.config.getWindowState("menu"); - const offset = stendhal.ui.getPageOffset(); + const offset = ui.getPageOffset(); const content = new SettingsDialog(); const dialog = ui.createSingletonFloatingWindow( diff --git a/src/js/stendhal/action/SlashAction.ts b/src/js/stendhal/action/SlashAction.ts index 41c86e4126..0e0632a3d1 100644 --- a/src/js/stendhal/action/SlashAction.ts +++ b/src/js/stendhal/action/SlashAction.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var marauroa: any; +import { marauroa } from "marauroa" import { ParamList } from "./ParamList"; import { Pair } from "../util/Pair"; diff --git a/src/js/stendhal/data/CStatus.ts b/src/js/stendhal/data/CStatus.ts index feb9b2497b..a0e77f6992 100644 --- a/src/js/stendhal/data/CStatus.ts +++ b/src/js/stendhal/data/CStatus.ts @@ -9,8 +9,8 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; export class CStatus { diff --git a/src/js/stendhal/data/CacheManager.ts b/src/js/stendhal/data/CacheManager.ts index a65ff5c1c5..e3b66bb1bb 100644 --- a/src/js/stendhal/data/CacheManager.ts +++ b/src/js/stendhal/data/CacheManager.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; /** diff --git a/src/js/stendhal/data/EmojiStore.ts b/src/js/stendhal/data/EmojiStore.ts index 78a72e5e85..574ecce14c 100644 --- a/src/js/stendhal/data/EmojiStore.ts +++ b/src/js/stendhal/data/EmojiStore.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2023 - Stendhal * + * Copyright © 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,10 +9,9 @@ * * ***************************************************************************/ -import { Paths } from "./Paths"; import { JSONLoader } from "../util/JSONLoader"; - -declare var stendhal: any; +import { htmlImageStore } from "./HTMLImageStore"; +import { Paths } from "./Paths"; export class EmojiStore { @@ -71,7 +70,7 @@ export class EmojiStore { if (!filename) { return; } - return stendhal.data.sprites.get(filename); + return htmlImageStore.get(filename); } /** diff --git a/src/js/stendhal/data/EntityOverlayRegistry.ts b/src/js/stendhal/data/EntityOverlayRegistry.ts new file mode 100644 index 0000000000..3be6716b9b --- /dev/null +++ b/src/js/stendhal/data/EntityOverlayRegistry.ts @@ -0,0 +1,89 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { stendhal } from "../stendhal"; + +import { Entity } from "../entity/Entity"; + +import { DirectionalSkillEffect } from "../sprite/action/DirectionalSkillEffect"; +import { SkillEffect } from "../sprite/action/SkillEffect"; + +type AnimationTuple = [string, number]; +type AnimationEntry = Record; + + +/** + * Registry for persistent entity overlay effects. + * + * TODO: + * - move declarations to JSON file if we want support in Java client + * - better animation for electricity + */ +export namespace EntityOverlayRegistry { + /** Table containing animation definitions. */ + const AnimationTable: Record = { + "NPC": { + "Aenihata": ["electricity_blue", 50], + "Tywysoga": "magic_sparkles" + }, + + "creature": { + "albino elf magician": "beams", + "archrat": "flame", + "dark elf master": ["electricity_violet", 50], + "dark elf viceroy": ["electricity_blue", 50], + "emperor dalmung": "flame_blue", + "ice giant": "ice_sparkles_2x", + "ice golem": "ice_sparkles", + "imperial demon lord": ["electricity_red", 50], + "necrosophia": "necro_flames" + } + } + + /** Entities with direction dependent animation. */ + const directional = [ + "necrosophia" + ]; + + /** + * Function for handling retrieval of animation definitions. + * + * @param {string} type + * Entity type. + * @param {Entity} entity + * Entity with which the overlay is associated. + * @returns {SkillEffect|undefined} + * Overlay effect or `undefined` if no effect available. + */ + export function get(type: string, entity: Entity): SkillEffect|undefined { + if (!stendhal.config.getBoolean("effect.entity-overlay")) { + return undefined; + } + const group = AnimationTable[type]; + if (!group) { + return undefined; + } + const name = entity["name"]; + let aniDef: string|AnimationTuple = group[name]; + if (!aniDef) { + return undefined; + } + if (typeof(aniDef) === "string") { + // use a default delay value if none specified + aniDef = [aniDef as string, 0]; + } + if (directional.indexOf(name) > -1) { + return new DirectionalSkillEffect(entity, aniDef[0], aniDef[1]); + } + return new SkillEffect(aniDef[0], aniDef[1]); + } +} diff --git a/src/js/stendhal/data/GroupManager.ts b/src/js/stendhal/data/GroupManager.ts index 9423ba2f6a..3c2b6f9b49 100644 --- a/src/js/stendhal/data/GroupManager.ts +++ b/src/js/stendhal/data/GroupManager.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { SlashActionRepo } from "../SlashActionRepo"; diff --git a/src/js/stendhal/data/HTMLImageStore.ts b/src/js/stendhal/data/HTMLImageStore.ts new file mode 100644 index 0000000000..3ac5b9c9f4 --- /dev/null +++ b/src/js/stendhal/data/HTMLImageStore.ts @@ -0,0 +1,290 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Paths } from "./Paths"; +import { ImageFilter } from "../sprite/image/ImageFilter"; + +export class HTMLImageStore { + + private knownBrokenUrls: {[url: string]: boolean} = {}; + private images: {[filename: string]: CanvasImageSource} = {}; + + private knownShadows: {[key: string]: boolean} = { + "24x32": true, + "32x32": true, + "32x48": true, + "32x48_long": true, + "48x64": true, + "48x64_float": true, + "64x48": true, + "64x64": true, + "64x85": true, + "64x96": true, + "76x64": true, + "81x96": true, + "96x96": true, + "96x128": true, + "128x96": true, + "128x170": true, + "144x128": true, + "168x224": true, + "192x192": true, + "192x192_float": true, + "192x256": true, + "320x440": true, + "ent": true + }; + + // alternatives for known images that may be considered violent or mature + private knownSafeSprites: {[filename: string]: boolean} = { + [Paths.sprites + "/monsters/huge_animal/thing"]: true, + [Paths.sprites + "/monsters/mutant/imperial_mutant"]: true, + [Paths.sprites + "/monsters/undead/bloody_zombie"]: true, + [Paths.sprites + "/npc/deadmannpc"]: true + }; + + // TODO: move to animation.json + animations: {[key: string]: any} = { + idea: { + "love": {delay: 100, offsetX: 24, offsetY: -8} + } + }; + + + get(filename: string): any { + if (!filename) { + return {}; + } + if (filename.indexOf("undefined") > -1) { + if (!this.knownBrokenUrls[filename]) { + console.log("Broken image path: ", filename, new Error()); + } + this.knownBrokenUrls[filename] = true; + return {}; + } + if (this.images[filename]) { + return this.images[filename]; + } + var temp = new Image(); + temp.onerror = (function(t: HTMLImageElement, store: HTMLImageStore) { + return function() { + if (t.src && !store.knownBrokenUrls[t.src]) { + console.log("Broken image path:", t.src, new Error()); + store.knownBrokenUrls[t.src] = true; + } + const failsafe = store.getFailsafe(); + if (failsafe.src && t.src !== failsafe.src) { + t.src = failsafe.src; + } + }; + })(temp, this); + temp.src = filename; + this.images[filename] = temp; + return temp; + } + + /** + * Rotates an image. + * + * @param img + * Image to be rotated. + * @param angle + * Angle of rotation. + */ + private rotate(img: HTMLImageElement, angle: number) { + const canvas = document.getElementById("drawing-stage")! as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + // make sure working with blank canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.width = img.width; + canvas.height = img.height; + + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate(angle * Math.PI / 180); + ctx.translate(-canvas.width / 2, -canvas.height / 2); + ctx.drawImage(img, 0, 0); + + img.src = canvas.toDataURL("image/png"); + } + + /** + * Retrieves a rotated image. + * + * @param filename + * Path to target image file. + * @param angle + * Angle of rotation. + * @return + * HTMLImageElement. + */ + getRotated(filename: string, angle: number): any { + if (angle == 0) { + return this.get(filename); + } + const id = filename + "-rot" + angle; + if (this.images[id]) { + return this.get(id); + } + // NOTE: cannot use HTMLImageElement.cloneNode here, must get base image then transfer + // `src` property when ready + const img = new Image(); + img.onload = () => { + img.onload = null; + this.rotate(img, angle); + } + const baseImg = this.get(filename); + if (baseImg.complete) { + img.src = baseImg.src; + } else { + baseImg.onload = () => { + baseImg.onload = null; + img.src = baseImg.src; + } + } + this.images[id] = img; + return img; + } + + /** + * Adds an image to cache. + * + * @param {string} id + * Cache identifier. + * @param {SpriteImage} + * Image to be cached. + */ + cache(id: string, image: CanvasImageSource) { + this.images[id] = image; + } + + /** + * Used when we only want an image if it was previously cached. + * + * @param filename + * Full file path. + * @return + * HTMLImageElement or undefined. + */ + getCached(filename: string): any { + return this.images[filename]; + } + + /** + * Retrieves the failsafe sprite. + * + * @return + * HTMLImageElement with failsafe image data. + */ + getFailsafe(): HTMLImageElement { + const filename = Paths.sprites + "/failsafe.png"; + let failsafe = this.images[filename]; + if (failsafe) { + } else { + failsafe = new Image(); + failsafe.src = filename; + this.images[filename] = failsafe; + } + return failsafe as HTMLImageElement; + } + + /** + * Checks cached images for a valid filename. + * + * @param filename + * Image filename to be checked. + * @return + * Path to image or failsafe image file. + */ + checkPath(filename: string): string { + return this.get(filename).src; + } + + + /** + * @param {string} fileName + * @param {string} filter + * @param {number=} param + */ + getFiltered(fileName: string, filter: string, param?: number) { + const img = this.get(fileName); + if (!img.complete || img.width === 0 || img.height === 0) { + return img; + } + const filteredName = fileName + " " + filter + " " + param; + let filtered = this.images[filteredName]; + if (typeof(filtered) === "undefined") { + const canvas = document.createElement("canvas") as any; + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d")!; + ctx.drawImage(img, 0, 0); + const imgData = ctx.getImageData(0, 0, img.width, img.height); + new ImageFilter().filter(imgData, filter, param); + ctx.putImageData(imgData, 0, 0); + canvas.complete = true; + this.images[filteredName] = filtered = canvas; + } + + return filtered; + } + + + /** + * Retrieves a shadow sprite if the style is available. + * + * @param shadowStyle + * Style of shadow to get from cache. + * @return + * Image sprite or undefined. + */ + getShadow(shadowStyle: string): any { + if (this.knownShadows[shadowStyle]) { + const img = new Image(); + img.src = Paths.sprites + "/shadow/" + shadowStyle + ".png"; + return img; + } + return undefined; + } + + /** + * Checks if there is a "safe" image available for sprite. + * + * @param filename + * The sprite image base file path. + * @return + * true if a known safe image is available. + */ + hasSafeImage(filename: string): boolean { + return this.knownSafeSprites[filename] == true; + } + + /** + * Called at startup to pre-cache certain images. + */ + startupCache() { + // failsafe image + this.getFailsafe(); + // tutorial profile + this.get(Paths.sprites + "/npc/floattingladynpc.png"); + // achievement assets + this.get(Paths.gui + "/banner_background.png"); + for (const cat of ["commerce", "deathmatch", "experience", "fighting", "friend", + "interior_zone", "item", "obtain", "outside_zone", "production", "quest", + "quest_ados_items", "quest_kill_blordroughs", "quest_kirdneh_item", + "quest_mithrilbourgh_enemy_army", "quest_semos_monster", "special", + "underground_zone"]) { + this.get(Paths.achievements + "/" + cat + ".png"); + } + } +} + +// SpriteStore singleton instance +export const htmlImageStore = /* @__PURE__ */ new HTMLImageStore(); diff --git a/src/js/stendhal/data/ImagePreloader.ts b/src/js/stendhal/data/ImagePreloader.ts deleted file mode 100644 index cb83480709..0000000000 --- a/src/js/stendhal/data/ImagePreloader.ts +++ /dev/null @@ -1,174 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -declare var stendhal: any; - - -/* - * Start: http://www.webreference.com/programming/javascript/gr/column3/ - * ( https://web.archive.org/web/20110910064905/http://www.webreference.com:80/programming/javascript/gr/column3/index.html ) - * -function ImagePreloader(images, callback) { - // store the call-back - this.callback = callback; - - // initialize internal state. - this.nLoaded = 0; - this.nProcessed = 0; - stendhal.data.map.aImages = new Array; - - // record the number of images. - this.nImages = images.length; - - // for each image, call preload() - for ( var i = 0; i < images.length; i++) { - this.preload(images[i]); - } -} - -ImagePreloader.prototype.preload = function(image) { - // create new Image object and add to array - var oImage = new Image; - stendhal.data.map.aImages.push(oImage); - - // set up event handlers for the Image object - oImage.onload = ImagePreloader.prototype.onload; - oImage.onerror = ImagePreloader.prototype.onerror; - oImage.onabort = ImagePreloader.prototype.onabort; - - // assign pointer back to this. - oImage.oImagePreloader = this; - oImage.bLoaded = false; - - // assign the .src property of the Image object - oImage.src = image; -}; - -ImagePreloader.prototype.onComplete = function() { - this.nProcessed++; - if (this.nProcessed == this.nImages) { - this.callback(); - } -}; - -ImagePreloader.prototype.onload = function() { - this.bLoaded = true; - this.oImagePreloader.nLoaded++; - this.oImagePreloader.onComplete(); -}; - -ImagePreloader.prototype.onerror = function() { - this.bError = true; - this.oImagePreloader.onComplete(); - console.error("Error loading " + this.src); -}; - -ImagePreloader.prototype.onabort = function() { - this.bAbort = true; - this.oImagePreloader.onComplete(); - console.error("Loading " + this.src + " was aborted"); -}; - * - * End: http://www.webreference.com/programming/javascript/gr/column3/ - */ - - -/** - * preloads images - * - * @param images image url to load - * @param callback callback to invoke - * @constructor - */ -export class ImagePreloader extends Function { - - oImagePreloader: ImagePreloader; - src?: string; - callback: Function; - - // internal state - bLoaded: boolean; - bError: boolean; - bAbort: boolean; - nLoaded: number; - nProcessed: number; - - // number of images - nImages: number; - - - constructor(images: string[], callback: Function) { - super(); - - this.oImagePreloader = {} as ImagePreloader; - // store the call-back - this.callback = callback; - - // initialize internal state. - this.bLoaded = false; - this.bError = false; - this.bAbort = false; - this.nLoaded = 0; - this.nProcessed = 0; - stendhal.data.map.aImages = new Array(); - - // record the number of images. - this.nImages = images.length; - - // for each image, call preload() - for (let i = 0; i < images.length; i++) { - this.preload(images[i]); - } - } - - preload(image: string) { - // create new Image object and add to array - const oImage = new Image() as any; - stendhal.data.map.aImages.push(oImage); - - // set up event handlers for the Image object - oImage.onload = ImagePreloader.prototype.onload; - oImage.onerror = ImagePreloader.prototype.onerror; - oImage.onabort = ImagePreloader.prototype.onabort; - - // assign pointer back to this - oImage.oImagePreloader = this; - oImage.bLoaded = false; - - // assign the .src property of the Image object - oImage.src = image; - } - - onComplete() { - this.nProcessed++; - if (this.nProcessed == this.nImages) { - this.callback(); - } - } - - onload() { - this.bLoaded = true; - this.oImagePreloader.nLoaded++; - this.oImagePreloader.onComplete(); - } - - onerror() { - this.bError = true; - this.oImagePreloader.onComplete(); - console.error("Error loading " + this.src); - } - - onabort() { - this.bAbort = true; - this.oImagePreloader.onComplete(); - console.error("Loading " + this.src + " was aborted"); - } -} diff --git a/src/js/stendhal/data/Outfit.ts b/src/js/stendhal/data/Outfit.ts new file mode 100644 index 0000000000..6052266214 --- /dev/null +++ b/src/js/stendhal/data/Outfit.ts @@ -0,0 +1,232 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Pair } from "../util/Pair"; +import { StringUtil } from "../util/StringUtil"; +import { Paths } from "./Paths"; + +import { stendhal } from "../stendhal"; +import { ImageMetadata } from "sprite/image/ImageMetadata"; +import { MultilayerImageLoader } from "sprite/image/MultilayerImageLoader"; +import { OutfitStore } from "./OutfitStore"; + + +/** + * Represents an entity's sprite layers. + */ +export class Outfit { + + /** Sprite layers. */ + private readonly layers: {[name: string]: number}; + /** Layer coloring values. */ + private coloring?: {[name: string]: number}; + + + /** + * Creates a new outfit. + * + * @param layers + * Sprite layers. + */ + constructor(layers: {[layer: string]: number}={}) { + this.layers = layers; + } + + /** + * Builds an outfit from string values. + * + * @param {string} layers + * Outfit layers definition in "layer=index,..." format. + * @param {string=} coloring + * Outfit colors definition in "layer=color,..." format. + * @returns {Outfit} + * New outfit. + */ + static build(layers: string, coloring?: string): Outfit { + const olayers: {[name: string]: number} = {}; + for (const l of layers.split(",")) { + if (l.includes("=")) { + const temp = l.split("="); + olayers[temp[0]] = Number.parseInt(temp[1], 10); + } + } + const outfit = new Outfit(olayers); + if (coloring) { + const ocoloring: {[name: string]: number} = {}; + for (const c of coloring.split(",")) { + if (c.includes("=")) { + const temp = c.split("="); + ocoloring[temp[0]] = Number.parseInt(temp[1], 10); + } + } + outfit.setColoring(ocoloring); + } + return outfit; + } + + /** + * Sets a layer's sprite index. + * + * @param name + * Layer name. + * @param index + * Sprite image index. + */ + public setLayer(name: string, index: number) { + this.layers[name] = index; + } + + /** + * Unsets a layer's sprite index. + * + * @param name + * Layer name. + */ + public unsetLayer(name: string) { + delete this.layers[name]; + } + + /** + * Retrieves the index value of a specified layer. + * + * @param name + * Layer name. + * @return + * Sprite image index. + */ + public getLayerIndex(name: string): number|undefined { + return this.layers[name]; + } + + /** + * Retrieves sorted layer info. + * + * @return {util.Pair.Pair[]} + * Layers info. + */ + public getLayers(): Pair[] { + const layers: Pair[] = []; + // only include valid layers + for (const name of OutfitStore.get().getLayerNames()) { + let index = this.layers[name]; + if (index != undefined) { + layers.push(new Pair(name, index)); + } + } + return layers; + } + + /** + * Sets coloring info for this outfit. + * + * @param coloring {object} + * Color values indexed by layer name. + */ + public setColoring(coloring: {[name: string]: number}) { + this.coloring = coloring; + } + + /** + * Retrieves coloring info for this outfit. + * + * @return {object} + * Color values indexed by layer name or `undefined`. + */ + public getColoring(): {[name: string]: number}|undefined { + return this.coloring; + } + + /** + * Retrieves coloring for a single layer. + * + * @param name {string} + * Layer name. + * @return {number} + * Color value or `undefined`. + */ + public getLayerColor(name: string): number|undefined { + if (OutfitStore.get().isSkinLayer(name)) { + name = "skin"; + } + return this.coloring && name in this.coloring ? this.coloring[name] : undefined; + } + + /** + * Retrieves signature identifying this outfit. + */ + public getSignature(): string { + const lsig: string[] = []; + const csig: string[] = []; + for (const layer of this.getLayers()) { + lsig.push(layer.first + "=" + layer.second); + } + if (this.coloring) { + for (const name of Object.keys(this.coloring)) { + csig.push(name + "=" + this.coloring[name]); + } + } + return "outfit(" + lsig.join(",") + ")" + (csig ? " colors(" + csig.join(",") + ")" : "") + + (stendhal.config.getBoolean("effect.no-nude") ? " no-nude" : ""); + } + + /** + * Compares outfit signatures for equality. + * + * @param {any} obj + * The object to compare against this outfit. + */ + public equals(obj: any): boolean { + if (!(obj instanceof Outfit)) { + return false; + } + return obj.getSignature() === this.getSignature(); + } + + /** + * Creates a sprite sheet from layer information. + * + * TODO: + * - use dress layer based on body type + */ + async toImage(): Promise { + let parts: ImageMetadata[] = []; + let outfitLayers = this.getLayers(); + const detailIndex = this.getLayerIndex("detail") || 0; + if (OutfitStore.get().detailHasRearLayer(detailIndex)) { + outfitLayers = [new Pair("detail-rear", detailIndex), ...outfitLayers]; + } + for (const l of outfitLayers) { + let layerName = l.first; + let suffix = ""; + if (layerName.endsWith("-rear")) { + suffix = "-rear"; + layerName = layerName.replace(/-rear$/, ""); + } + const layerIndex = l.second; + let layerPath = Paths.sprites + "/outfit/" + layerName + "/" + + StringUtil.padLeft(""+layerIndex, "0", 3) + suffix; + if (layerName === "body" && stendhal.config.getBoolean("effect.no-nude")) { + // FIXME: some non-player pickable bodies don't have "no-nude" variant + layerPath += "-nonude"; + } + layerPath += ".png"; + let filter = undefined; + let coloring = this.getLayerColor(layerName); + if (coloring) { + filter = "trueColor"; + } + parts.push(new ImageMetadata(layerPath, filter, coloring)); + } + + return await new MultilayerImageLoader(parts).load(); + } +} diff --git a/src/js/stendhal/data/OutfitStore.ts b/src/js/stendhal/data/OutfitStore.ts index 10fbe6abbf..aaa22018f6 100644 --- a/src/js/stendhal/data/OutfitStore.ts +++ b/src/js/stendhal/data/OutfitStore.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2023 - Arianne * + * Copyright © 2003-2024 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -15,11 +15,17 @@ import { JSONLoader } from "../util/JSONLoader"; export class OutfitStore { + // layer names in draw order + private static readonly layers: string[] = ["body", "dress", "head", "mouth", "eyes", "mask", "hair", "hat", "detail"]; + private static readonly layersLegacy: string[] = ["body", "dress", "head", "hair", "detail"]; + // layers using skin coloring + private static readonly skinLayers: string[] = ["body", "head"]; + private detailRearLayers: number[] = []; // player pickable layers - private count: {[key: string]: number} = { + public readonly count: {[key: string]: number} = { "hat": 19, "hair": 57, "mask": 9, @@ -70,6 +76,31 @@ export class OutfitStore { loader.load(Paths.sprites + "/outfit/outfits.json"); } + /** + * Retrieves names of applicable outfit layers. + * + * @returns {string[]} + * Layer names. + */ + getLayerNames(legacy=false): string[] { + if (legacy) { + return [...OutfitStore.layersLegacy]; + } + return [...OutfitStore.layers]; + } + + /** + * Determines if a layer uses skin coloring. + * + * @param name {string} + * Layer name. + * @return {boolean} + * `true` if the layer is considered to be a "skin" layer. + */ + isSkinLayer(name: string): boolean { + return OutfitStore.skinLayers.indexOf(name) > -1; + } + /** * Determines if hair should be drawn under a determinted hat index. * diff --git a/src/js/stendhal/data/Paths.ts b/src/js/stendhal/data/Paths.ts index 946c63b11e..294c3736b5 100644 --- a/src/js/stendhal/data/Paths.ts +++ b/src/js/stendhal/data/Paths.ts @@ -24,6 +24,7 @@ export class Paths { public static readonly achievements = Paths.sprites + "/achievements"; public static readonly maps = Paths.data + "/maps"; public static readonly tileset = Paths.maps + "/tileset"; + public static readonly parallax = Paths.maps + "/parallax"; public static readonly ws = Paths.extractPath("data-ws"); /** diff --git a/src/js/stendhal/data/SpriteStore.ts b/src/js/stendhal/data/SpriteStore.ts deleted file mode 100644 index 0bc953556c..0000000000 --- a/src/js/stendhal/data/SpriteStore.ts +++ /dev/null @@ -1,603 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -declare var stendhal: any; - -import { Paths } from "./Paths"; - - -class SpriteImage extends Image { - // number of times the image has been accessed after initial creation - counter = 0; -} - -export class SpriteStore { - - private knownBrokenUrls: {[url: string]: boolean} = {}; - private images: {[filename: string]: SpriteImage} = {}; - - private knownShadows: {[key: string]: boolean} = { - "24x32": true, - "32x32": true, - "32x48": true, - "32x48_long": true, - "48x64": true, - "48x64_float": true, - "64x48": true, - "64x64": true, - "64x85": true, - "64x96": true, - "76x64": true, - "81x96": true, - "96x96": true, - "96x128": true, - "128x96": true, - "128x170": true, - "144x128": true, - "168x224": true, - "192x192": true, - "192x192_float": true, - "192x256": true, - "320x440": true, - "ent": true - }; - - // alternatives for known images that may be considered violent or mature - private knownSafeSprites: {[filename: string]: boolean} = { - [Paths.sprites + "/monsters/huge_animal/thing"]: true, - [Paths.sprites + "/monsters/mutant/imperial_mutant"]: true, - [Paths.sprites + "/monsters/undead/bloody_zombie"]: true, - [Paths.sprites + "/npc/deadmannpc"]: true - }; - - // TODO: move to animation.json - private animations: {[key: string]: any} = { - idea: { - "love": {delay: 100, offsetX: 24, offsetY: -8} - } - }; - - - /** - * Hidden singleton constructor. - */ - protected constructor() { - // do nothing - } - - get(filename: string): any { - if (!filename) { - return {}; - } - if (filename.indexOf("undefined") > -1) { - if (!this.knownBrokenUrls[filename]) { - console.log("Broken image path: ", filename, new Error()); - } - this.knownBrokenUrls[filename] = true; - return {}; - } - if (this.images[filename]) { - this.images[filename].counter++; - return this.images[filename]; - } - var temp = new Image() as SpriteImage; - // TypeError: Image constructor: 'new' is required - //~ var temp = new SpriteImage(); - temp.counter = 0; - temp.onerror = (function(t: SpriteImage, store: SpriteStore) { - return function() { - if (t.src && !store.knownBrokenUrls[t.src]) { - console.log("Broken image path:", t.src, new Error()); - store.knownBrokenUrls[t.src] = true; - } - const failsafe = store.getFailsafe(); - if (failsafe.src && t.src !== failsafe.src) { - t.src = failsafe.src; - } - }; - })(temp, this); - temp.src = filename; - this.images[filename] = temp; - return temp; - } - - getWithPromise(filename: string): any { - return new Promise((resolve) => { - if (typeof(this.images[filename]) != "undefined") { - this.images[filename].counter++; - resolve(this.images[filename]); - } - - const image = new Image() as SpriteImage; - // TypeError: Image constructor: 'new' is required - //~ const image = new SpriteImage(); - image.counter = 0; - this.images[filename] = image; - image.onload = () => resolve(image); - image.src = filename; - }); - } - - /** - * Rotates an image. - * - * @param img - * Image to be rotated. - * @param angle - * Angle of rotation. - */ - private rotate(img: HTMLImageElement, angle: number) { - const canvas = document.getElementById("drawing-stage")!; - const ctx = canvas.getContext("2d")!; - // make sure working with blank canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - canvas.width = img.width; - canvas.height = img.height; - - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(angle * Math.PI / 180); - ctx.translate(-canvas.width / 2, -canvas.height / 2); - ctx.drawImage(img, 0, 0); - - img.src = canvas.toDataURL("image/png"); - } - - /** - * Retrieves a rotated image. - * - * @param filename - * Path to target image file. - * @param angle - * Angle of rotation. - * @return - * HTMLImageElement. - */ - getRotated(filename: string, angle: number): any { - if (angle == 0) { - return this.get(filename); - } - const id = filename + "-rot" + angle; - if (this.images[id]) { - return this.get(id); - } - // NOTE: cannot use HTMLImageElement.cloneNode here, must get base image then transfer - // `src` property when ready - const img = new Image() as SpriteImage; - img.counter = 0; - img.onload = () => { - img.onload = null; - this.rotate(img, angle); - } - const baseImg = this.get(filename); - if (baseImg.complete) { - img.src = baseImg.src; - } else { - baseImg.onload = () => { - baseImg.onload = null; - img.src = baseImg.src; - } - } - this.images[id] = img; - return img; - } - - /** - * Used when we only want an image if it was previously cached. - * - * @param filename - * Full file path. - * @return - * HTMLImageElement or undefined. - */ - getCached(filename: string): any { - return this.images[filename]; - } - - /** - * Retrieves the failsafe sprite. - * - * @return - * HTMLImageElement with failsafe image data. - */ - getFailsafe(): HTMLImageElement { - const filename = Paths.sprites + "/failsafe.png"; - let failsafe = this.images[filename]; - if (failsafe) { - failsafe.counter++; - } else { - failsafe = new Image() as SpriteImage; - // TypeError: Image constructor: 'new' is required - //~ failsafe = new SpriteImage(); - failsafe.counter = 0; - failsafe.src = filename; - this.images[filename] = failsafe; - } - return failsafe; - } - - /** - * Checks cached images for a valid filename. - * - * @param filename - * Image filename to be checked. - * @return - * Path to image or failsafe image file. - */ - checkPath(filename: string): string { - return this.get(filename).src; - } - - /** deletes all objects that have not been accessed since this method was called last time */ - // TODO: call clean on map change - clean() { - for (var i in this.images) { - console.log(typeof(this.images[i])); - if (this.images[i] instanceof SpriteImage) { - if (this.images[i].counter > 0) { - this.images[i].counter--; - } else { - delete(this.images[i]); - } - } - } - } - - /** - * Get an image element whose image data is an area of a specified image. - * If the area matches the original image, the image itself is returned. - * Otherwise a copy of the image data is returned. This is meant - * to be used for obtaining the drag image for drag and drop. - * - * @param image original image - * @param width width of the area - * @param height height of the area - * @param {number=} offsetX optional. left x coordinate of the area - * @param {number=} offsetY optional. top y coordinate of the area - */ - getAreaOf(image: HTMLImageElement, width: number, height: number, - offsetX?: number, offsetY?: number): any { - try { - offsetX = offsetX || 0; - offsetY = offsetY || 0; - if ((image.width === width) && (image.height === height) - && (offsetX === 0) && (offsetY === 0)) { - return image; - } - var canvas = document.createElement("canvas") as HTMLCanvasElement; - canvas.width = width; - canvas.height = height; - var ctx = canvas.getContext("2d")!; - ctx.drawImage(image, offsetX, offsetY, width, height, 0, 0, width, height); - // Firefox would be able to use the canvas directly as a drag image, but - // Chrome does not. This should work in any standards compliant browser. - var newImage = new Image(); - newImage.src = canvas.toDataURL("image/png"); - return newImage; - } catch (err) { - if (err instanceof DOMException) { - return this.getFailsafe(); - } else { - // don't ignore other errors - throw err; - } - } - - return {}; - } - - /** - * @param {string} fileName - * @param {string} filter - * @param {number=} param - */ - getFiltered(fileName: string, filter: string, param?: number) { - const img = this.get(fileName); - let filterFn; - if (typeof(filter) === "undefined" - || !(filterFn = this.filter[filter]) - || !img.complete || img.width === 0 || img.height === 0) { - return img; - } - const filteredName = fileName + " " + filter + " " + param; - let filtered: SpriteImage = this.images[filteredName]; - if (typeof(filtered) === "undefined") { - const canvas = document.createElement("canvas") as any; - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d")!; - ctx.drawImage(img, 0, 0); - const imgData = ctx.getImageData(0, 0, img.width, img.height); - const data = imgData.data; - filterFn(data, param); - ctx.putImageData(imgData, 0, 0); - canvas.complete = true; - this.images[filteredName] = filtered = canvas as SpriteImage; - } - - return filtered; - } - - /** - * @param {string} fileName - * @param {string} filter - * @param {number=} param - */ - getFilteredWithPromise(fileName: string, filter: string, param?: number) { - const imgPromise = this.getWithPromise(fileName); - return imgPromise.then(function (img: HTMLImageElement) { - let filterFn: Function; - if (typeof(filter) === "undefined" - || !(filterFn = stendhal.data.sprites.filter[filter]) - || !img.complete || img.width === 0 || img.height === 0) { - return img; - } - const filteredName = fileName + " " + filter + " " + param; - let filtered: SpriteImage = stendhal.data.sprites.images[filteredName]; - if (typeof(filtered) === "undefined") { - const canvas = document.createElement("canvas") as any; - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d")!; - ctx.drawImage(img, 0, 0); - const imgData = ctx.getImageData(0, 0, img.width, img.height); - const data = imgData.data; - filterFn(data, param); - ctx.putImageData(imgData, 0, 0); - canvas.complete = true; - stendhal.data.sprites.images[filteredName] = filtered = canvas as SpriteImage; - } - - return filtered; - }); - } - - - /** Image filters */ - filter: {[key: string]: Function} = { - // Helper functions - /** - * @param {Number} rgb - * @return {Array} - */ - splitrgb: function(rgb: number): number[] { - rgb &= 0xffffff; - var b = rgb & 0xff; - rgb >>>= 8; - var g = rgb & 0xff; - rgb >>>= 8; - return [rgb, g, b]; - }, - - mergergb: function(rgbArray: number[]): number { - const r = rgbArray[0] << 16; - const g = rgbArray[1] << 8; - return 0xffffff & (r | g | rgbArray[2]); - }, - - /** - * @param {Array} rgb - * @return {Array} - */ - rgb2hsl: function(rgb: number[]): number[] { - var r = rgb[0] / 255; - var g = rgb[1] / 255; - var b = rgb[2] / 255; - - var max, min, maxVar; - // Find the max and minimum colors, and remember which one it was - if (r > g) { - max = r; - min = g; - maxVar = 0; - } else { - max = g; - min = r; - maxVar = 1; - } - if (b > max) { - max = b; - maxVar = 2; - } else if (b < min) { - min = b; - } - - // lightness - var l = (max + min) / 2; - var s, h; - - // saturation - var diff = max - min; - if (diff < 0.000001) { - s = 0; - // hue not really defined, but set it to something reasonable - h = 0; - } else { - if (l < 0.5) { - s = diff / (max + min); - } else { - s = diff / (2 - max - min); - } - - // hue - if (maxVar === 0) { - h = (g - b) / diff; - } else if (maxVar === 1) { - h = 2 + (b - r) / diff; - } else { - h = 4 + (r - g) / diff; - } - // Normalize to range [0, 1]. It's more useful than the usual 360 - h /= 6; - } - - return [h, s, l]; - }, - - /** - * @param {Array} hsl - * @return {Array} - */ - hsl2rgb: function(hsl: number[]): number[] { - var r, g, b; - var h = hsl[0]; - var s = hsl[1]; - var l = hsl[2]; - - if (s < 0.0000001) { - r = g = b = Math.floor(255 * l); - } else { - var tmp1, tmp2; - if (l < 0.5) { - tmp1 = l * (1 + s); - } else { - tmp1 = l + s - l * s; - } - tmp2 = 2 * l - tmp1; - - var rf = stendhal.data.sprites.filter.hue2rgb(this.limitHue(h + 1/3), tmp2, tmp1); - var gf = stendhal.data.sprites.filter.hue2rgb(this.limitHue(h), tmp2, tmp1); - var bf = stendhal.data.sprites.filter.hue2rgb(this.limitHue(h - 1/3), tmp2, tmp1); - - r = Math.floor(255 * rf) & 0xff; - g = Math.floor(255 * gf) & 0xff; - b = Math.floor(255 * bf) & 0xff; - } - - return [r, g, b]; - }, - - /** - * @param {Number} hue - * @param {Number} val1 - * @param {Number} val2 - */ - hue2rgb: function(hue: number, val1: number, val2: number): number { - var res = hue; - if (6 * hue < 1) { - res = val1 + (val2 - val1) * 6 * hue; - } else if (2 * hue < 1) { - res = val2; - } else if (3 * hue < 2) { - res = val1 + (val2 - val1) * (2/3 - hue) * 6; - } else { - res = val1; - } - - return res; - }, - - /** - * @param {Number} hue - */ - limitHue: function(hue: number): number { - var res = hue; - if (res < 0) { - res += 1; - } else if (res > 1) { - res -= 1; - } - return res; - } - } - - /** - * Retrieves a shadow sprite if the style is available. - * - * @param shadowStyle - * Style of shadow to get from cache. - * @return - * Image sprite or undefined. - */ - getShadow(shadowStyle: string): any { - if (this.knownShadows[shadowStyle]) { - const img = new Image(); - img.src = Paths.sprites + "/shadow/" + shadowStyle + ".png"; - return img; - } - return undefined; - } - - /** - * Checks if there is a "safe" image available for sprite. - * - * @param filename - * The sprite image base file path. - * @return - * true if a known safe image is available. - */ - hasSafeImage(filename: string): boolean { - return this.knownSafeSprites[filename] == true; - } - - /** - * Called at startup to pre-cache certain images. - */ - startupCache() { - // tutorial profile - this.get(Paths.sprites + "/npc/floattingladynpc.png"); - // achievement assets - this.get(Paths.gui + "/banner_background.png"); - for (const cat of ["commerce", "deathmatch", "experience", "fighting", "friend", - "interior_zone", "item", "obtain", "outside_zone", "production", "quest", - "quest_ados_items", "quest_kill_blordroughs", "quest_kirdneh_item", - "quest_mithrilbourgh_enemy_army", "quest_semos_monster", "special", - "underground_zone"]) { - this.get(Paths.achievements + "/" + cat + ".png"); - } - // weather - for (const weather of ["fog", "fog_heavy", "rain", "rain_heavy", - "rain_light", "snow", "snow_heavy", "snow_light", "wave"]) { - this.get(Paths.weather + "/" + weather + ".png"); - } - } -} - -/** - * Hidden class to create the singleton instance internally. - */ -class SpriteStoreInternal extends SpriteStore { - - /** Singleton instance. */ - private static instance: SpriteStore; - - /** - * Retrieves singleton instance. - */ - static get(): SpriteStore { - if (!SpriteStoreInternal.instance) { - SpriteStoreInternal.instance = new SpriteStore(); - } - return SpriteStoreInternal.instance; - } -} - -// SpriteStore singleton instance -export const store = SpriteStoreInternal.get(); -// *** Image filters. Prevent the closure compiler from mangling the names. *** -store.filter['trueColor'] = function(data: any, color: number) { - var hslColor = stendhal.data.sprites.filter.rgb2hsl(stendhal.data.sprites.filter.splitrgb(color)); - var end = data.length; - for (var i = 0; i < end; i += 4) { - var rgb = [data[i], data[i + 1], data[i + 2]]; - var hsl = stendhal.data.sprites.filter.rgb2hsl(rgb); - // Adjust the brightness - var adj = hslColor[2] - 0.5; // [-0.5, 0.5] - var tmp = hsl[2] - 0.5; // [-0.5, 0.5] - // tweaks the middle lights either upward or downward, depending - // on if source lightness is high or low - var l = hsl[2] - 2.0 * adj * ((tmp * tmp) - 0.25); - var resultHsl = [hslColor[0], hslColor[1], l]; - var resultRgb = stendhal.data.sprites.filter.hsl2rgb(resultHsl); - data[i] = resultRgb[0]; - data[i+1] = resultRgb[1]; - data[i+2] = resultRgb[2]; - } -}; diff --git a/src/js/stendhal/data/Map.ts b/src/js/stendhal/data/TileMap.ts similarity index 62% rename from src/js/stendhal/data/Map.ts rename to src/js/stendhal/data/TileMap.ts index 2e493b768d..ebd62a6154 100644 --- a/src/js/stendhal/data/Map.ts +++ b/src/js/stendhal/data/TileMap.ts @@ -9,53 +9,56 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { Deserializer } from "marauroa" +import { stendhal } from "../stendhal"; import { Paths } from "./Paths"; -import { LandscapeRenderingStrategy, CombinedTilesetRenderingStrategy } from "../landscape/LandscapeRenderingStrategy"; +import { LandscapeRenderingStrategy } from "../landscape/LandscapeRenderingStrategy"; +import { CombinedTilesetRenderingStrategy } from "../landscape/CombinedTilesetRenderingStrategy"; import { IndividualTilesetRenderingStrategy } from "../landscape/IndividualTilesetRenderingStrategy"; +import { ParallaxBackground } from "../landscape/ParallaxBackground"; -export class Map { +export class TileMap { - private currentZoneName = ""; + public currentZoneName = ""; - private offsetX = 0; - private offsetY = 0; - private zoneSizeX = -1; - private zoneSizeY = -1; - private sizeX = 20; - private sizeY = 15; + public offsetX = 0; + public offsetY = 0; + public zoneSizeX = -1; + public zoneSizeY = -1; + public sizeX = 20; + public sizeY = 15; - private tileWidth = 32; - private tileHeight = 32; - private zoom = 100; + public tileWidth = 32; + public tileHeight = 32; + public zoom = 100; - private tilesetFilenames: string[] = []; - private aImages = -1; - private layerNames: any = -1; - private layers: any = -1; - private firstgids: any = -1; - private gidsindex: any = []; + public tilesetFilenames: string[] = []; + public aImages = -1; + public layerNames: any = -1; + public layers: any = -1; + public firstgids: any = -1; + public gidsindex: any = []; - private drawingError = false; - private targetTileWidth = 0; - private targetTileHeight = 0; + public drawingError = false; + public targetTileWidth = 0; + public targetTileHeight = 0; - private readonly layerGroups: any = [ + public readonly layerGroups = [ ["0_floor", "1_terrain", "2_object"], ["3_roof", "4_roof_add"] ]; - private layerGroupIndexes: any; + public readonly blendLayers = ["blend_ground", "blend_roof"] + public layerGroupIndexes: any; - private strategy: LandscapeRenderingStrategy; - private protection: any; - private collisionData: any; + public strategy: LandscapeRenderingStrategy; + public protection: any; + public collisionData: any; // alternatives for known images that may be considered violent or mature - private knownSafeTilesets: string[] = [ + public knownSafeTilesets: string[] = [ Paths.tileset + "/item/armor/bloodied_small_axe", Paths.tileset + "/item/blood/floor_stain", Paths.tileset + "/item/blood/floor_stains_2", @@ -63,18 +66,22 @@ export class Map { Paths.tileset + "/item/blood/small_stains" ]; + public parallax: ParallaxBackground; + public parallaxImage?: string; + public ignoredTiles: string[]; + /** Singleton instance. */ - private static instance: Map; + private static instance: TileMap; /** * Retrieves singleton instance. */ - static get(): Map { - if (!Map.instance) { - Map.instance = new Map(); + static get(): TileMap { + if (!TileMap.instance) { + TileMap.instance = new TileMap(); } - return Map.instance; + return TileMap.instance; } /** @@ -86,6 +93,8 @@ export class Map { } else { this.strategy = new CombinedTilesetRenderingStrategy(); } + this.parallax = ParallaxBackground.get(); + this.ignoredTiles = []; } /** @@ -117,16 +126,23 @@ export class Map { this.decodeMapLayer(content, "2_object"); this.decodeMapLayer(content, "3_roof"); this.decodeMapLayer(content, "4_roof_add"); + this.decodeMapLayer(content, "blend_ground"); + this.decodeMapLayer(content, "blend_roof"); this.protection = this.decodeMapLayer(content, "protection"); this.collisionData = this.decodeMapLayer(content, "collision"); this.layerGroupIndexes = this.mapLayerGroup(); this.strategy.onMapLoaded(this); + + if (this.parallaxImage) { + this.parallax.setImage(this.parallaxImage, this.zoneSizeX * this.tileWidth, + this.zoneSizeY * this.tileHeight); + } } decodeTileset(content: any, name: string) { var layerData = content[name]; - var deserializer = marauroa.Deserializer.fromBase64(layerData); + var deserializer = Deserializer.fromBase64(layerData); var amount = deserializer.readInt() as number; this.tilesetFilenames = []; @@ -139,12 +155,17 @@ export class Map { // This is not input validation. It just rewrites a path used by the // Java client to a path matching the webserver directory layout. var filename = "/" + source.replace(/\.\.\/\.\.\//g, ""); + const basename = filename.replace(/\.png$/, "").replace(/^\/tileset\//, ""); + const targetname = Paths.tileset + "/" + basename; - let baseFilename = filename.replace(/\.png$/, "").replace("/tileset", Paths.tileset); - if (!stendhal.config.getBoolean("effect.blood") && this.hasSafeTileset(baseFilename)) { - this.tilesetFilenames.push(baseFilename + "-safe.png"); + if (this.ignoredTiles.indexOf(basename) > -1) { + // server has specified that certain tiles should not be drawn on this map + continue; + } + if (!stendhal.config.getBoolean("effect.blood") && this.hasSafeTileset(targetname)) { + this.tilesetFilenames.push(targetname + "-safe.png"); } else { - this.tilesetFilenames.push(baseFilename + ".png"); + this.tilesetFilenames.push(targetname + ".png"); } this.firstgids.push(firstgid); @@ -164,11 +185,18 @@ export class Map { } decodeMapLayer(content: any, name: string): number[]|undefined { - var layerData = content[name]; + var layerData: any; + if (name === "0_floor" && stendhal.config.getBoolean("effect.parallax")) { + // check for parallax-supporive floor layer + layerData = content["0_floor_parallax"]; + } + if (!layerData) { + layerData = content[name]; + } if (!layerData) { return; } - var deserializer = marauroa.Deserializer.fromDeflatedBase64(layerData); + var deserializer = Deserializer.fromDeflatedBase64(layerData); deserializer.readString(); // zone name this.zoneSizeX = deserializer.readInt(); this.zoneSizeY = deserializer.readInt(); @@ -222,4 +250,36 @@ export class Map { hasSafeTileset(filename: string) { return this.knownSafeTilesets.indexOf(filename) > -1; } + + /** + * Sets or unsets parallax image. + * + * @param {string=} name + * Background image filename. + */ + setParallax(name?: string) { + this.parallaxImage = name; + if (typeof(name) === "undefined") { + this.parallax.reset(); + } + } + + /** + * Adds tilesets to be ignored when drawing. + * + * @param {string=} tilesets + * Comma-delimited string. If empty or `undefined`, ignored tilesets list will be reset. + */ + setIgnoredTiles(tilesets?: string) { + if (!tilesets) { + this.ignoredTiles = []; + return; + } + for (let t of tilesets.split(",")) { + t = t.trim(); + if (t !== "") { + this.ignoredTiles.push(t); + } + } + } } diff --git a/src/js/stendhal/data/TileStore.ts b/src/js/stendhal/data/TileStore.ts index 17e20db3e9..1a3c2f2e73 100644 --- a/src/js/stendhal/data/TileStore.ts +++ b/src/js/stendhal/data/TileStore.ts @@ -12,9 +12,6 @@ import { Paths } from "./Paths"; import { JSONLoader } from "../util/JSONLoader"; -declare var stendhal: any; - - type AnimationMap = { [key: string]: any; } @@ -70,7 +67,7 @@ export class TileStore { this.landscapeMap = this.loadAnimations( loader.data["landscape"], Paths.tileset + "/"); this.weatherMap = this.loadAnimations( - loader.data["weather"], stendhal.paths.weather + "/"); + loader.data["weather"], Paths.weather + "/"); }; loader.load(Paths.tileset + "/animation.json"); } diff --git a/src/js/stendhal/ui/LoopedSoundSourceManager.ts b/src/js/stendhal/data/sound/LoopedSoundSourceManager.ts similarity index 87% rename from src/js/stendhal/ui/LoopedSoundSourceManager.ts rename to src/js/stendhal/data/sound/LoopedSoundSourceManager.ts index 84821e7486..3eb6fe7791 100644 --- a/src/js/stendhal/ui/LoopedSoundSourceManager.ts +++ b/src/js/stendhal/data/sound/LoopedSoundSourceManager.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2024 - Faiumoni e. V. * + * Copyright © 2003-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,12 +9,11 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; -import { SoundManager } from "./SoundManager"; - -import { LoopedSoundSource } from "../entity/LoopedSoundSource"; +import { LoopedSoundSource } from "../../entity/LoopedSoundSource"; +import { Zone } from "entity/Zone"; // server doesn't distinguish between music & looped sound effects so we @@ -30,8 +29,6 @@ const sfxLoops: {[name: string]: boolean} = { */ export class LoopedSoundSourceManager { - /** Sound manager instance. */ - private readonly sndMan = SoundManager.get(); /** Detected sound sources in current zone. */ private sources: {[id: string]: any} = {}; @@ -103,10 +100,10 @@ export class LoopedSoundSourceManager { let snd: any; const layer = source["layer"]; if (this.isMusic(source["sound"])) { - snd = this.sndMan.playLocalizedMusic(source["x"], source["y"], source["radius"], layer, + snd = stendhal.sound.playLocalizedMusic(source["x"], source["y"], source["radius"], layer, source["sound"], source["volume"]); } else { - snd = this.sndMan.playLocalizedLoop(source["x"], source["y"], source["radius"], layer, + snd = stendhal.sound.playLocalizedLoop(source["x"], source["y"], source["radius"], layer, source["sound"], source["volume"]); } @@ -137,7 +134,7 @@ export class LoopedSoundSourceManager { // FIXME: doesn't always delete reference delete this.sources[id]; const errmsg = []; - if (!this.sndMan.stop(source.layer, source.sound)) { + if (!stendhal.sound.stop(source.layer, source.sound)) { errmsg.push("failed to stop looped sound source with ID '" + id + "' (" + source.sound.src + ")"); } @@ -183,8 +180,8 @@ export class LoopedSoundSourceManager { */ private getZoneEntities(): LoopedSoundSource[] { const ents: LoopedSoundSource[] = []; - if (stendhal.zone.entities) { - for (const ent of stendhal.zone.entities) { + if (Zone.get().entities) { + for (const ent of Zone.get().entities) { if (ent instanceof LoopedSoundSource) { ents.push(ent); } @@ -215,9 +212,9 @@ export class LoopedSoundSourceManager { onDistanceChanged(x: number, y: number) { for (const ent of this.getZoneEntities()) { if (ent.isLoaded()) { - const layerName = this.sndMan.checkLayer(ent["layer"]); + const layerName = stendhal.sound.checkLayer(ent["layer"]); const snd = this.sources[ent["id"]].sound; - this.sndMan.adjustForDistance(layerName, snd, ent["radius"], + stendhal.sound.adjustForDistance(layerName, snd, ent["radius"], ent["x"], ent["y"], x, y); } } diff --git a/src/js/stendhal/data/SoundFactory.ts b/src/js/stendhal/data/sound/SoundFactory.ts similarity index 97% rename from src/js/stendhal/data/SoundFactory.ts rename to src/js/stendhal/data/sound/SoundFactory.ts index 58ef504340..d635204a74 100644 --- a/src/js/stendhal/data/SoundFactory.ts +++ b/src/js/stendhal/data/sound/SoundFactory.ts @@ -13,8 +13,6 @@ /** * A playable sound. - * - * TODO: move file to `data/sound` directory */ export interface SoundObject extends HTMLAudioElement { /** Base volume level unique to this sound. */ diff --git a/src/js/stendhal/data/enum/SoundLayer.ts b/src/js/stendhal/data/sound/SoundLayer.ts similarity index 92% rename from src/js/stendhal/data/enum/SoundLayer.ts rename to src/js/stendhal/data/sound/SoundLayer.ts index 138db35491..667b704ce3 100644 --- a/src/js/stendhal/data/enum/SoundLayer.ts +++ b/src/js/stendhal/data/sound/SoundLayer.ts @@ -16,7 +16,7 @@ import { AbstractEnum } from "../enum/AbstractEnum"; /** * Available layers of sound. * - * NOTE: string names should match `games.stendhal.common.constants.SoundLayer` + * string names should match `games.stendhal.common.constants.SoundLayer` */ export class SoundLayer extends AbstractEnum { @@ -29,9 +29,6 @@ export class SoundLayer extends AbstractEnum { static readonly SFX = new SoundLayer("sfx"); static readonly GUI = new SoundLayer("gui"); - /** Value is required. */ - override readonly value!: string; - private constructor(value: string) { super(value); @@ -47,7 +44,7 @@ export class SoundLayer extends AbstractEnum { static names(): string[] { const names: string[] = []; for (const layer of SoundLayer.layers) { - names.push(layer.value); + names.push(layer.value!); } return names; } diff --git a/src/js/stendhal/ui/SoundManager.ts b/src/js/stendhal/data/sound/SoundManager.ts similarity index 91% rename from src/js/stendhal/ui/SoundManager.ts rename to src/js/stendhal/data/sound/SoundManager.ts index 83bdfa3f19..100899de53 100644 --- a/src/js/stendhal/ui/SoundManager.ts +++ b/src/js/stendhal/data/sound/SoundManager.ts @@ -9,24 +9,29 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; -import { ui } from "./UI"; +import { SoundFactory } from "./SoundFactory"; +import { SoundObject } from "./SoundFactory"; +import { SoundLayer } from "./SoundLayer"; -import { singletons } from "../SingletonRepo"; - -import { SoundFactory } from "../data/SoundFactory"; -import { SoundObject } from "../data/SoundFactory"; - -import { SoundLayer } from "../data/enum/SoundLayer"; +import { ui } from "../../ui/UI"; +import { Paths } from "../Paths"; /** * Manages playing sounds & music. + * + * TODO: + * - rework to handle looped sounds/music better (without gap) + * - move "/data/sound" directory to "/sound" */ export class SoundManager { + /** Distance at which entity sounds can be heard. */ + public static readonly DEFAULT_RADIUS = 23; + /** Layer names & ordering. */ readonly layers: string[]; /** Session cache. */ @@ -238,7 +243,7 @@ export class SoundManager { let snd = this.cache[soundName] || this.cacheGlobal[soundName]; if (!snd) { // add new sound to cache - snd = this.load(soundName, stendhal.paths.sounds + "/" + soundName + ".ogg"); + snd = this.load(soundName, Paths.sounds + "/" + soundName + ".ogg"); } if (!this.cache[soundName]) { @@ -400,7 +405,7 @@ export class SoundManager { volume=1.0): SoundObject|undefined { // load into cache so playEffect doesn't look in "data/sounds" if (!this.cache[musicName]) { - this.load(musicName, stendhal.paths.music + "/" + musicName + ".ogg"); + this.load(musicName, Paths.music + "/" + musicName + ".ogg"); } return this.playLocalizedLoop(x, y, radius, layer, musicName, volume); } @@ -418,7 +423,7 @@ export class SoundManager { playGlobalizedMusic(musicName: string, volume=1.0): SoundObject|undefined { // load into cache so playEffect doesn't look in "data/sounds" if (!this.cache[musicName]) { - this.load(musicName, stendhal.paths.music + "/" + musicName + ".ogg"); + this.load(musicName, Paths.music + "/" + musicName + ".ogg"); } return this.playGlobalizedLoop(musicName, "music", volume); } @@ -616,6 +621,10 @@ export class SoundManager { * Normalized volume level between 0.0 and 1.0. */ private normVolume(vol: number): number { + if (isNaN(vol) || !isFinite(vol)) { + console.warn("Tried to set invalid volume level: " + vol, new Error()); + vol = 1; + } return vol < 0 ? 0 : vol > 1 ? 1 : vol; } @@ -630,9 +639,18 @@ export class SoundManager { * Volume level adjusted with "master" & associated layer. */ private getAdjustedVolume(layerName: string, volBase: number): number { - let volActual = volBase * stendhal.config.getFloat("sound.master.volume"); - const lvol = stendhal.config.getFloat("sound." + layerName + ".volume"); - if (typeof(lvol) !== "number") { + let volActual = stendhal.config.getInt("sound.master.volume"); + if (typeof(volActual) === "number") { + // convert to float in range between 0-1 + volActual /= 100; + } else { + volActual = 1; + } + let lvol = stendhal.config.getInt("sound." + layerName + ".volume"); + if (typeof(lvol) === "number") { + // convert to float in range between 0-1 + lvol /= 100; + } else { console.warn("cannot adjust volume for layer \"" + layerName + "\""); return volActual; } @@ -664,12 +682,15 @@ export class SoundManager { * `true` if volume level was set. */ setVolume(layerName: string, vol: number): boolean { - const volOld = stendhal.config.getFloat("sound." + layerName + ".volume"); - if (typeof(volOld) === "undefined" || volOld === "") { + let volOld = stendhal.config.getInt("sound." + layerName + ".volume"); + if (typeof(volOld) === "number") { + // convert to float in range between 0-1 + volOld /= 100; + } else { return false; } - stendhal.config.set("sound." + layerName + ".volume", this.normVolume(vol)); + stendhal.config.set("sound." + layerName + ".volume", Math.floor(this.normVolume(vol) * 100)); const layerSet = layerName === "master" ? this.layers : [layerName]; for (const l of layerSet) { @@ -704,12 +725,13 @@ export class SoundManager { * Current volume level of layer (returns 1 on error). */ getVolume(layerName="master"): number { - let vol = stendhal.config.getFloat("sound." + layerName + ".volume"); + // NOTE: config value is integer in range between 0-100 + let vol = stendhal.config.getInt("sound." + layerName + ".volume"); if (typeof(vol) === "undefined" || isNaN(vol) || !isFinite(vol)) { console.warn("could not get volume for channel \"" + layerName + "\""); return 1; } - return this.normVolume(vol); + return this.normVolume(vol / 100); } /** @@ -754,8 +776,11 @@ export class SoundManager { */ onConfigUpdate() { for (const layerName of ["master", ...this.layers]) { - let vol = stendhal.config.getFloat("sound." + layerName + ".volume"); - if (typeof(vol) !== "number") { + let vol = stendhal.config.getInt("sound." + layerName + ".volume"); + if (typeof(vol) === "number") { + // convert to float in range between 0-1 + vol /= 100; + } else { console.warn("Unrecognized volume value for layer \"" + layerName + "\":", vol); // default to 100% vol = 1.0; @@ -770,6 +795,6 @@ export class SoundManager { startupCache() { // login sound this.load("ui/login", - stendhal.paths.sounds + "/ui/login.ogg", true); + Paths.sounds + "/ui/login.ogg", true); } } diff --git a/src/js/stendhal/entity/ActiveEntity.ts b/src/js/stendhal/entity/ActiveEntity.ts index 3006783576..80152deba7 100644 --- a/src/js/stendhal/entity/ActiveEntity.ts +++ b/src/js/stendhal/entity/ActiveEntity.ts @@ -13,7 +13,9 @@ import { Entity } from "./Entity"; import { Direction } from "../util/Direction"; import { MathUtil } from "../util/MathUtil"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { TileMap } from "data/TileMap"; +import { Zone } from "./Zone"; export class ActiveEntity extends Entity { @@ -230,9 +232,10 @@ export class ActiveEntity extends Entity { var startY = Math.floor(this["_y"]); var endX = Math.ceil(this["_x"] + this["width"]); var endY = Math.ceil(this["_y"] + this["height"]); + let map = TileMap.get(); for (var y = startY; y < endY; y++) { for (var x = startX; x < endX; x++) { - if (stendhal.data.map.collision(x, y)) { + if (map.collision(x, y)) { return true; } } @@ -258,8 +261,8 @@ export class ActiveEntity extends Entity { var thisEndY = Math.ceil(this["_y"] + this["height"]); var i; - for (i in stendhal.zone.entities) { - var other = stendhal.zone.entities[i]; + for (i in Zone.get().entities) { + var other = Zone.get().entities[i]; if (!this.isObstacle(other)) { continue; } @@ -280,10 +283,34 @@ export class ActiveEntity extends Entity { /** * Retrieves entities direction of movement. * - * @return {util.Direction.Direction} + * Default is `Direction.STOP`. + * + * @returns {Direction} * Current direction entity is moving or stopped. */ getWalkDirection(): Direction { - return Direction.VALUES[parseInt(this["dir"], 10)]; + const val = parseInt(this["dir"], 10); + if (Number.isNaN(val) || !Number.isFinite(val)) { + // assume stopped if cannot determine walking direction + return Direction.STOP; + } + return Direction.VALUES[Math.max(Direction.STOP.val, Math.min(Direction.LEFT.val, val))]; + } + + /** + * Retrieves direction entity is facing. + * + * If direction cannot be determined (e.g. `Direction.STOP`) defaults to `Direction.DOWN`. + * + * @returns {Direction} + * Entity's facing direction. + */ + getFaceDirection(): Direction { + const dir = this.getWalkDirection(); + if (dir === Direction.STOP) { + // assume down if cannot determine facing direction + return Direction.DOWN; + } + return dir; } } diff --git a/src/js/stendhal/entity/Blood.ts b/src/js/stendhal/entity/Blood.ts index 680b8a26c5..6fed890f72 100644 --- a/src/js/stendhal/entity/Blood.ts +++ b/src/js/stendhal/entity/Blood.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,9 +9,13 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { Entity } from "./Entity"; +import { Paths } from "../data/Paths"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class Blood extends Entity { @@ -20,27 +24,30 @@ export class Blood extends Entity { constructor() { super(); - this.sprite = { - height: 32, - width: 32, - filename: stendhal.paths.sprites + "/combat/blood_red.png" - }; + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/combat/blood_red.png"), + 0, 0, 32, 32 + ); } override set(key: string, value: any) { super.set(key, value); if (key === "amount") { - this.sprite.offsetY = parseInt(value, 10) * 32; + this.imageSprite!.offsetY = parseInt(value, 10) * 32; } else if (key === "class") { - this.sprite.filename = stendhal.paths.sprites + "/combat/blood_" + value + ".png"; + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/combat/blood_" + value + ".png"), + 0, parseInt(this["amount"], 10) * 32, 32, 32 + ); } } - override getCursor(x: number, y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/walk.png) 1 3, auto"; + override getCursor(_x: number, _y: number) { + return "url(" + Paths.sprites + "/cursor/walk.png) 1 3, auto"; } - override drawSpriteAt(ctx: CanvasRenderingContext2D, x: number, y: number) { + override drawSpriteAt(ctx: RenderingContext2D, x: number, y: number) { if (!stendhal.config.getBoolean("effect.blood")) { return; } diff --git a/src/js/stendhal/entity/Chest.ts b/src/js/stendhal/entity/Chest.ts index f6a9b7f63b..59b4438682 100644 --- a/src/js/stendhal/entity/Chest.ts +++ b/src/js/stendhal/entity/Chest.ts @@ -19,23 +19,13 @@ import { FloatingWindow } from "../ui/toolkit/FloatingWindow"; import { ItemInventoryComponent } from "../ui/component/ItemInventoryComponent"; import { Chat } from "../util/Chat"; +import { Paths } from "../data/Paths"; -declare var marauroa: any; -declare var stendhal: any; - - -let OPEN_SPRITE = { - filename: stendhal.paths.sprites + "/chest.png", - height: 32, - width: 32, - offsetY: 32 -}; - -let CLOSED_SPRITE = { - filename: stendhal.paths.sprites + "/chest.png", - height: 32, - width: 32 -}; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; +import { Entity } from "./Entity"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class Chest extends PopupInventory { @@ -43,16 +33,22 @@ export class Chest extends PopupInventory { override minimapStyle = Color.CHEST; override zIndex = 5000; - sprite = CLOSED_SPRITE; open = false; + constructor() { + super(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/chest.png"), + 0, 0, 32, 32); + } + override set(key: string, value: any) { super.set(key, value); if (key === "open") { - this.sprite = OPEN_SPRITE; + this.imageSprite!.offsetY = 32; this.open = true; } - if (this.isNextTo(marauroa.me)) { + if (this.isNextTo(marauroa.me as Entity)) { this.openInventoryWindow(); } } @@ -60,7 +56,7 @@ export class Chest extends PopupInventory { override unset(key: string) { super.unset(key); if (key === "open") { - this.sprite = CLOSED_SPRITE; + this.imageSprite!.offsetY = 0; this.open = false; if (this.inventory && this.inventory.isOpen()) { this.inventory.close(); @@ -151,7 +147,7 @@ export class Chest extends PopupInventory { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/bag.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/bag.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Corpse.ts b/src/js/stendhal/entity/Corpse.ts index bf6477904b..30d44aa9f0 100644 --- a/src/js/stendhal/entity/Corpse.ts +++ b/src/js/stendhal/entity/Corpse.ts @@ -9,6 +9,8 @@ * * ***************************************************************************/ +import { CorpseSlot } from "./CorpseSlot"; +import { RenderingContext2D } from "util/Types"; import { ActivityIndicatorSprite } from "../sprite/ActivityIndicatorSprite"; import { ItemInventoryComponent } from "../ui/component/ItemInventoryComponent"; @@ -17,9 +19,14 @@ import { FloatingWindow } from "../ui/toolkit/FloatingWindow"; import { Chat } from "../util/Chat"; import { PopupInventory } from "./PopupInventory"; +import { Paths } from "../data/Paths"; +import { singletons } from "../SingletonRepo"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa, RPObject, RPZone } from "marauroa" +import { stendhal } from "../stendhal"; +import { images } from "sprite/image/ImageManager"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { ViewPort } from "ui/ViewPort"; export class Corpse extends PopupInventory { @@ -27,72 +34,55 @@ export class Corpse extends PopupInventory { override zIndex = 5500; autoOpenedAlready = false; - private readonly indicator: ActivityIndicatorSprite; + private readonly indicator?: ActivityIndicatorSprite; constructor() { super(); - this.indicator = new ActivityIndicatorSprite(); + if (stendhal.config.getBoolean("activity-indicator")) { + this.indicator = new ActivityIndicatorSprite(); + } } override set(key: string, value: any) { super.set(key, value); - this.sprite = this.sprite || {}; - const bloodEnabled = stendhal.config.getBoolean("effect.blood"); - + let bloodEnabled = stendhal.config.getBoolean("effect.blood"); if (bloodEnabled && (key === "image")) { - this.sprite.filename = stendhal.paths.sprites + "/corpse/" + value + ".png"; + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/corpse/" + value + ".png")); } else if (!bloodEnabled && (key === "harmless_image")) { - this.sprite.filename = stendhal.paths.sprites + "/corpse/" + value + ".png"; + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/corpse/" + value + ".png")); } } - override draw(ctx: CanvasRenderingContext2D) { - if (!this.sprite) { + override draw(ctx: RenderingContext2D) { + let image = this.imageSprite?.imageRef?.image; + if (!image) { return; } super.draw(ctx); - if (stendhal.config.getBoolean("activity-indicator") && !this.isEmpty()) { - const tileW = stendhal.ui.gamewindow.targetTileWidth; - const tileH = stendhal.ui.gamewindow.targetTileHeight; - if (this.sprite.width == undefined || this.sprite.height == undefined) { - const image = stendhal.data.sprites.get(this.sprite.filename); - if (image.complete) { - this.sprite.width = image.width < tileW ? tileW : image.width; - this.sprite.height = image.height < tileH ? tileH : image.height; - } - return; - } - - const offsetX = Math.floor((this["width"] * tileW - this.sprite.width) / 2); - const offsetY = Math.floor((this["height"] * tileH - this.sprite.height) / 2); - const dx = this["x"] * tileW + offsetX; - const dy = this["y"] * tileH + offsetY; - - this.indicator.draw(ctx, dx, dy, this.sprite.width); + if (this.indicator && !this.isEmpty()) { + let viewPort = ViewPort.get(); + const tileW = viewPort.targetTileWidth; + const tileH = viewPort.targetTileHeight; + let width = image.width < tileW ? tileW : image.width; + let height = image.height < tileH ? tileH : image.height; + let offsetX = Math.floor((this["width"] * tileW - width) / 2); + let offsetY = Math.floor((this["height"] * tileH - height) / 2); + let dx = this["x"] * tileW + offsetX; + let dy = this["y"] * tileH + offsetY; + this.indicator.draw(ctx, dx, dy, width); } } override createSlot(name: string) { - var slot = marauroa.util.fromProto(marauroa.rpslotFactory["_default"], { - add: function(object: any) { - marauroa.rpslotFactory["_default"].add.apply(this, arguments); - if (this._objects.length > 0) { - this._parent.autoOpenIfDesired(); - } - }, - - del: function(key: any) { - marauroa.rpslotFactory["_default"].del.apply(this, arguments); - if (this._objects.length == 0) { - this._parent.closeCorpseInventory(); - } - } - }); + let slot = new CorpseSlot(); slot._name = name; - slot._objects = []; slot._parent = this; return slot; } @@ -166,18 +156,18 @@ export class Corpse extends PopupInventory { override getCursor(_x: number, _y: number) { if (this.isEmpty()) { - return "url(" + stendhal.paths.sprites + "/cursor/emptybag.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/emptybag.png) 1 3, auto"; } // owner if (!this["corpse_owner"] || (this["corpse_owner"] == marauroa.me["_name"])) { - return "url(" + stendhal.paths.sprites + "/cursor/bag.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/bag.png) 1 3, auto"; } if ((stendhal.data.group.lootmode === "shared") && (stendhal.data.group.members[this["corpse_owner"]])) { - return "url(" + stendhal.paths.sprites + "/cursor/bag.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/bag.png) 1 3, auto"; } - return "url(" + stendhal.paths.sprites + "/cursor/lockedbag.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/lockedbag.png) 1 3, auto"; } public override isDraggable(): boolean { @@ -187,4 +177,9 @@ export class Corpse extends PopupInventory { private isEmpty() { return !this["content"] || this["content"]._objects.length === 0; } + + override destroy(parent: RPObject|RPZone): void { + this.indicator?.free(); + super.destroy(parent); + } } diff --git a/src/js/stendhal/entity/CorpseSlot.ts b/src/js/stendhal/entity/CorpseSlot.ts new file mode 100644 index 0000000000..23a1f91df8 --- /dev/null +++ b/src/js/stendhal/entity/CorpseSlot.ts @@ -0,0 +1,34 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Corpse } from "./Corpse"; +import { RPSlot } from "marauroa"; + +export class CorpseSlot extends RPSlot { + + public _name?: string; + public _parent?: Corpse; + + override add(object: any) { + super.add(object); + if (this._objects.length > 0) { + this._parent?.autoOpenIfDesired(); + } + } + + override del(key: any) { + super.del(key); + if (this._objects.length == 0) { + this._parent?.closeCorpseInventory(); + } + } + +} diff --git a/src/js/stendhal/entity/Creature.ts b/src/js/stendhal/entity/Creature.ts index 79d3d55cb2..9eee4b1661 100644 --- a/src/js/stendhal/entity/Creature.ts +++ b/src/js/stendhal/entity/Creature.ts @@ -11,10 +11,15 @@ import { RPEntity } from "./RPEntity"; +import { EntityOverlayRegistry } from "../data/EntityOverlayRegistry"; + import { Color } from "../data/color/Color"; -declare var marauroa: any; -declare var stendhal: any; +import { SkillEffect } from "../sprite/action/SkillEffect"; +import { Paths } from "../data/Paths"; + +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; export class Creature extends RPEntity { @@ -22,6 +27,16 @@ export class Creature extends RPEntity { override spritePath = "monsters"; override titleStyle = "#ffc8c8"; + + override set(key: string, value: any) { + super.set(key, value); + + if (key === "name") { + // overlay animation + this.overlay = EntityOverlayRegistry.get("creature", this); + } + } + override onclick(_x: number, _y: number) { var action = { "type": "attack", @@ -38,7 +53,20 @@ export class Creature extends RPEntity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/attack.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/attack.png) 1 3, auto"; } + /** + * Shows a temporary animation overlay for certain entities. + * + * FIXME: does not restore previous overlay + */ + protected override onTransformed() { + if (!this["name"].startsWith("vampire")) { + return; + } + const delay = 100; + const frames = 5; + this.overlay = new SkillEffect("transform", delay, delay * frames); + } } diff --git a/src/js/stendhal/entity/DomesticAnimal.ts b/src/js/stendhal/entity/DomesticAnimal.ts index 137bdc52fe..cecb7e3a3e 100644 --- a/src/js/stendhal/entity/DomesticAnimal.ts +++ b/src/js/stendhal/entity/DomesticAnimal.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,53 +9,62 @@ * * ***************************************************************************/ +import { MenuItem } from "../action/MenuItem"; import { RPEntity } from "./RPEntity"; -import { MenuItem} from "../action/MenuItem"; +import { RenderingContext2D } from "util/Types"; import { Color } from "../data/color/Color"; +import { Paths } from "../data/Paths"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa"; +import { images } from "sprite/image/ImageManager"; +import { ImageSprite } from "sprite/image/ImageSprite"; export class DomesticAnimal extends RPEntity { override minimapStyle = Color.DOMESTICANIMAL; - override drawMain(ctx: CanvasRenderingContext2D) { - if (!this.imagePath && this["_rpclass"]) { - this["largeWeight"] = this["largeWeight"] | 20; - if (this["_rpclass"] == "sheep") { - this["largeWeight"] = 60; - } - this.imagePath = stendhal.paths.sprites + "/" + this["_rpclass"] + ".png"; - } - - var localX = this["_x"] * 32; - var localY = this["_y"] * 32; - var image = stendhal.data.sprites.get(this.imagePath); - if (image.height) { - var nFrames = 3; - var nDirections = 4; - var yRow = this["dir"] - 1; - if (this["weight"] >= this["largeWeight"]) { - yRow += 4; - } - this["drawHeight"] = image.height / nDirections / 2; - this["drawWidth"] = image.width / nFrames; - var drawX = ((this["width"] * 32) - this["drawWidth"]) / 2; - var frame = 0; - if (this["speed"] > 0) { - // % Works normally with *floats* (just whose bright idea was - // that?), so use floor() as a workaround - frame = Math.floor(Date.now() / 100) % nFrames; - } - var drawY = (this["height"] * 32) - this["drawHeight"]; - ctx.drawImage(image, frame * this["drawWidth"], yRow * this["drawHeight"], this["drawWidth"], this["drawHeight"], localX + drawX, localY + drawY, this["drawWidth"], this["drawHeight"]); + override init() { + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/" + this["_rpclass"] + ".png")); + this["largeWeight"] = this["largeWeight"] || 20; + if (this["_rpclass"] == "sheep") { + this["largeWeight"] = 60; } } + + override drawMain(ctx: RenderingContext2D) { + + let image = this.imageSprite?.imageRef?.image + if (!image) { + return; + } + + let localX = this["_x"] * 32; + let localY = this["_y"] * 32; + + let nFrames = 3; + let nDirections = 4; + let yRow = this["dir"] - 1; + if (this["weight"] >= this["largeWeight"]) { + yRow += 4; + } + this["drawHeight"] = image.height / nDirections / 2; + this["drawWidth"] = image.width / nFrames; + let drawX = ((this["width"] * 32) - this["drawWidth"]) / 2; + let frame = 0; + if (this["speed"] > 0) { + // % Works normally with *floats* (just whose bright idea was + // that?), so use floor() as a workaround + frame = Math.floor(Date.now() / 100) % nFrames; + } + let drawY = (this["height"] * 32) - this["drawHeight"]; + ctx.drawImage(image, frame * this["drawWidth"], yRow * this["drawHeight"], this["drawWidth"], this["drawHeight"], localX + drawX, localY + drawY, this["drawWidth"], this["drawHeight"]); + } + override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; } override buildActions(list: MenuItem[]) { diff --git a/src/js/stendhal/entity/Door.ts b/src/js/stendhal/entity/Door.ts index 3258d3aa41..90f98337d9 100644 --- a/src/js/stendhal/entity/Door.ts +++ b/src/js/stendhal/entity/Door.ts @@ -9,32 +9,45 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { MenuItem } from "../action/MenuItem"; import { Portal } from "./Portal"; - -declare var marauroa: any; -declare var stendhal: any; +import { Paths } from "../data/Paths"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class Door extends Portal { override zIndex = 5000; - override draw(ctx: CanvasRenderingContext2D) { - let imagePath = stendhal.paths.sprites + "/doors/" + this["class"] + ".png"; - let image = stendhal.data.sprites.get(imagePath); - if (image.height) { - let height = image.height / 2; - let x = (this["x"] * 32) - ((image.width - 32) / 2); - let y = (this["y"] * 32) - ((height - 32) / 2); - - var offsetY = height; - if (this["open"] === "") { - offsetY = 0; + override set(key: string, value: any) { + super.set(key, value); + if (key === "class") { + if (this.imageSprite) { + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/doors/" + value + ".png"), + 0, 0, 0, 0); } - ctx.drawImage(image, 0, offsetY, image.width, height, x, y, image.width, height); } } + override draw(ctx: RenderingContext2D) { + let image = this.imageSprite?.imageRef?.image; + if (!image) { + return; + } + let height = image.height / 2; + let x = (this["x"] * 32) - ((image.width - 32) / 2); + let y = (this["y"] * 32) - ((height - 32) / 2); + + let offsetY = height; + if (this["open"] === "") { + offsetY = 0; + } + ctx.drawImage(image, 0, offsetY, image.width, height, x, y, image.width, height); + } + override buildActions(list: MenuItem[]) { list.push({ title: "Look", @@ -51,7 +64,7 @@ export class Door extends Portal { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/portal.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/portal.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Entity.ts b/src/js/stendhal/entity/Entity.ts index ee52a43a29..1e32a0b2d8 100644 --- a/src/js/stendhal/entity/Entity.ts +++ b/src/js/stendhal/entity/Entity.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,12 +9,17 @@ * * ***************************************************************************/ +import { RPObject, RPZone } from "marauroa"; +import { RenderingContext2D } from "util/Types"; import { MenuItem } from "../action/MenuItem"; +import { Paths } from "../data/Paths"; import { Chat } from "../util/Chat"; -import { RPObject } from "./RPObject"; -declare var marauroa: any; -declare var stendhal: any; +import { htmlImageStore } from "data/HTMLImageStore"; +import { marauroa } from "marauroa"; +import { HTMLUtil } from "ui/HTMLUtil"; +import { ViewPort } from "ui/ViewPort"; +import { ImageSprite } from "../sprite/image/ImageSprite"; /** * General entity @@ -25,6 +30,7 @@ export class Entity extends RPObject { minimapShow = false; minimapStyle = "rgb(200,255,200)"; zIndex = 10000; + imageSprite?: ImageSprite; override set(key: string, value: any) { super.set(key, value); @@ -137,10 +143,10 @@ export class Entity extends RPObject { /** * is this entity visible to a specific action * - * @param filter 0: short left click + * @param _filter 0: short left click * @return true, if the entity is visible, false otherwise */ - isVisibleToAction(filter: boolean) { + isVisibleToAction(_filter?: boolean) { return false; } @@ -179,7 +185,7 @@ export class Entity extends RPObject { // action replaces "Look" command (e. g. "look closely" on signs) if (this["action"]) { list.push({ - title: stendhal.ui.html.niceName(this["action"]), + title: HTMLUtil.niceName(this["action"]), type: this.actionAliasToAction(this["action"]) }); } else { @@ -204,8 +210,8 @@ export class Entity extends RPObject { this["_x"] = this["x"]; } - draw(ctx: CanvasRenderingContext2D) { - if (this.sprite) { + draw(ctx: RenderingContext2D) { + if (this.sprite || this.imageSprite) { this.drawSprite(ctx); } } @@ -213,23 +219,27 @@ export class Entity extends RPObject { /** * draws a standard sprite */ - drawSprite(ctx: CanvasRenderingContext2D) { + drawSprite(ctx: RenderingContext2D) { this.drawSpriteAt(ctx, this["x"] * 32, this["y"] * 32); } - drawSpriteAt(ctx: CanvasRenderingContext2D, x: number, y: number) { - var image = stendhal.data.sprites.get(this.sprite.filename); - if (image.height) { - var offsetX = this.sprite.offsetX || 0; - var offsetY = this.sprite.offsetY || 0; - var width = this.sprite.width || image.width; - var height = this.sprite.height || image.height; + drawSpriteAt(ctx: RenderingContext2D, x: number, y: number) { + if (this.imageSprite) { + this.imageSprite.drawOnto(ctx, x, y, this.getWidth() * 32, this.getHeight() * 32); + } else { + let image = htmlImageStore.get(this.sprite.filename); + if (image.height) { + let offsetX = this.sprite.offsetX || 0; + let offsetY = this.sprite.offsetY || 0; + let width = this.sprite.width || image.width; + let height = this.sprite.height || image.height; - // use entity dimensions to center sprite - x += Math.floor((this.getWidth() * 32 - width) / 2); - y += Math.floor((this.getHeight() * 32 - height) / 2); - - ctx.drawImage(image, offsetX, offsetY, width, height, x, y, width, height); + // use entity dimensions to center sprite + x += Math.floor((this.getWidth() * 32 - width) / 2); + y += Math.floor((this.getHeight() * 32 - height) / 2); + + ctx.drawImage(image, offsetX, offsetY, width, height, x, y, width, height); + } } } @@ -274,7 +284,7 @@ export class Entity extends RPObject { cursor = this["cursor"]; } - return "url(" + stendhal.paths.sprites + "/cursor/" + cursor.toLowerCase().replace("_", "") + ".png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/" + cursor.toLowerCase().replace("_", "") + ".png) 1 3, auto"; } /** @@ -320,16 +330,17 @@ export class Entity extends RPObject { * Checks if within vieport area. */ public inView(): boolean { + let viewPort = ViewPort.get(); const view_rect: any = {}; - view_rect.left = stendhal.ui.gamewindow.offsetX; - view_rect.right = view_rect.left + stendhal.ui.gamewindow.width; - view_rect.top = stendhal.ui.gamewindow.offsetY; - view_rect.bottom = view_rect.top + stendhal.ui.gamewindow.height; + view_rect.left = viewPort.offsetX; + view_rect.right = view_rect.left + viewPort.width; + view_rect.top = viewPort.offsetY; + view_rect.bottom = view_rect.top + viewPort.height; - const pixelX = this["x"] * stendhal.ui.gamewindow.targetTileWidth - + Math.floor(stendhal.ui.gamewindow.targetTileWidth / 2); - const pixelY = this["y"] * stendhal.ui.gamewindow.targetTileHeight - + Math.floor(stendhal.ui.gamewindow.targetTileHeight / 2); + const pixelX = this["x"] * viewPort.targetTileWidth + + Math.floor(viewPort.targetTileWidth / 2); + const pixelY = this["y"] * viewPort.targetTileHeight + + Math.floor(viewPort.targetTileHeight / 2); const ent_rect: any = {}; // horizontal orientation is centered @@ -342,4 +353,8 @@ export class Entity extends RPObject { return ent_rect.right > view_rect.left && ent_rect.left < view_rect.right && ent_rect.bottom > view_rect.top && ent_rect.top < view_rect.bottom } + + override destroy(_parent: RPObject|RPZone) { + this.imageSprite?.free(); + } } diff --git a/src/js/stendhal/entity/EntityRegistry.ts b/src/js/stendhal/entity/EntityRegistry.ts index 535792b188..aef519c5bd 100644 --- a/src/js/stendhal/entity/EntityRegistry.ts +++ b/src/js/stendhal/entity/EntityRegistry.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var marauroa: any; +import { marauroa } from "marauroa" import { Blood } from "./Blood"; import { Chest} from "./Chest"; @@ -35,41 +35,46 @@ import { User } from "./User"; import { VisibleEntity } from "./VisibleEntity"; import { WalkBlocker } from "./WalkBlocker"; -marauroa.rpobjectFactory["area"] = InvisibleEntity; -marauroa.rpobjectFactory["baby_dragon"] = DomesticAnimal; -marauroa.rpobjectFactory["blackboard"] = Sign; -marauroa.rpobjectFactory["blocktarget"] = InvisibleEntity; -marauroa.rpobjectFactory["block"] = VisibleEntity; -marauroa.rpobjectFactory["blood"] = Blood; -marauroa.rpobjectFactory["cat"] = DomesticAnimal; -marauroa.rpobjectFactory["chest"] = Chest; -marauroa.rpobjectFactory["corpse"] = Corpse; -marauroa.rpobjectFactory["creature"] = Creature; -marauroa.rpobjectFactory["_default"] = UnknownEntity; -marauroa.rpobjectFactory["domesticanimal"] = DomesticAnimal; -marauroa.rpobjectFactory["door"] = Door; -marauroa.rpobjectFactory["flyover"] = InvisibleEntity; -marauroa.rpobjectFactory["food"] = Food; -marauroa.rpobjectFactory["game_board"] = GameBoard; -marauroa.rpobjectFactory["gate"] = Gate -marauroa.rpobjectFactory["growing_entity_spawner"] = GrowingEntitySpawner; -marauroa.rpobjectFactory["house_portal"] = Portal; -marauroa.rpobjectFactory["invisible_entity"] = InvisibleEntity; -marauroa.rpobjectFactory["item"] = Item; -marauroa.rpobjectFactory["looped_sound_source"] = LoopedSoundSource; -marauroa.rpobjectFactory["npc"] = NPC; -marauroa.rpobjectFactory["plant_grower"] = VisibleEntity; -marauroa.rpobjectFactory["player"] = Player; -marauroa.rpobjectFactory["portal"] = Portal; -marauroa.rpobjectFactory["rented_sign"] = Sign; -marauroa.rpobjectFactory["sheep"] = DomesticAnimal; -marauroa.rpobjectFactory["sign"] = Sign; -marauroa.rpobjectFactory["tiled_entity"] = InvisibleEntity; -marauroa.rpobjectFactory["training_dummy"] = TrainingDummy; -marauroa.rpobjectFactory["unknown"] = UnknownEntity; -marauroa.rpobjectFactory["useable_entity"] = UseableEntity; -marauroa.rpobjectFactory["user"] = User; -marauroa.rpobjectFactory["visible_entity"] = VisibleEntity; -marauroa.rpobjectFactory["walkblocker"] = WalkBlocker; -marauroa.rpobjectFactory["wall"] = InvisibleEntity; -marauroa.rpobjectFactory["weather_entity"] = InvisibleEntity; +export class EntityRegistry { + + public init() { + marauroa.rpobjectFactory.register("area", InvisibleEntity); + marauroa.rpobjectFactory.register("baby_dragon", DomesticAnimal); + marauroa.rpobjectFactory.register("blackboard", Sign); + marauroa.rpobjectFactory.register("blocktarget", InvisibleEntity); + marauroa.rpobjectFactory.register("block", VisibleEntity); + marauroa.rpobjectFactory.register("blood", Blood); + marauroa.rpobjectFactory.register("cat", DomesticAnimal); + marauroa.rpobjectFactory.register("chest", Chest); + marauroa.rpobjectFactory.register("corpse", Corpse); + marauroa.rpobjectFactory.register("creature", Creature); + marauroa.rpobjectFactory.register("_default", UnknownEntity); + marauroa.rpobjectFactory.register("domesticanimal", DomesticAnimal); + marauroa.rpobjectFactory.register("door", Door); + marauroa.rpobjectFactory.register("flyover", InvisibleEntity); + marauroa.rpobjectFactory.register("food", Food); + marauroa.rpobjectFactory.register("game_board", GameBoard); + marauroa.rpobjectFactory.register("gate", Gate); + marauroa.rpobjectFactory.register("growing_entity_spawner", GrowingEntitySpawner); + marauroa.rpobjectFactory.register("house_portal", Portal); + marauroa.rpobjectFactory.register("invisible_entity", InvisibleEntity); + marauroa.rpobjectFactory.register("item", Item); + marauroa.rpobjectFactory.register("looped_sound_source", LoopedSoundSource); + marauroa.rpobjectFactory.register("npc", NPC); + marauroa.rpobjectFactory.register("plant_grower", VisibleEntity); + marauroa.rpobjectFactory.register("player", Player); + marauroa.rpobjectFactory.register("portal", Portal); + marauroa.rpobjectFactory.register("rented_sign", Sign); + marauroa.rpobjectFactory.register("sheep", DomesticAnimal); + marauroa.rpobjectFactory.register("sign", Sign); + marauroa.rpobjectFactory.register("tiled_entity", InvisibleEntity); + marauroa.rpobjectFactory.register("training_dummy", TrainingDummy); + marauroa.rpobjectFactory.register("unknown", UnknownEntity); + marauroa.rpobjectFactory.register("useable_entity", UseableEntity); + marauroa.rpobjectFactory.register("user", User); + marauroa.rpobjectFactory.register("visible_entity", VisibleEntity); + marauroa.rpobjectFactory.register("walkblocker", WalkBlocker); + marauroa.rpobjectFactory.register("wall", InvisibleEntity); + marauroa.rpobjectFactory.register("weather_entity", InvisibleEntity); + } +} diff --git a/src/js/stendhal/entity/Food.ts b/src/js/stendhal/entity/Food.ts index 31effe663c..0936841396 100644 --- a/src/js/stendhal/entity/Food.ts +++ b/src/js/stendhal/entity/Food.ts @@ -10,33 +10,33 @@ ***************************************************************************/ import { Entity } from "./Entity"; +import { Paths } from "../data/Paths"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class Food extends Entity { override zIndex = 5000; + override init() { + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/food.png"), + 0, this["amount"] * 32, 32, 32); + } + override set(key: string, value: any) { super.set(key, value); if (key === "amount") { - this._amount = parseInt(value, 10); + if (this.imageSprite) { + this.imageSprite.offsetY = this["amount"] * 32; + } } // TODO: play sound effect } - override draw(ctx: CanvasRenderingContext2D) { - var image = stendhal.data.sprites.get(stendhal.paths.sprites + "/food.png"); - if (image.height) { - var localX = this["x"] * 32; - var localY = this["y"] * 32; - var offset = this._amount * 32; - ctx.drawImage(image, 0, offset, 32, 32, localX, localY, 32, 32); - } - } - override onclick(_x: number, _y: number) { var action = { "type": "look", @@ -50,7 +50,7 @@ export class Food extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/GameBoard.ts b/src/js/stendhal/entity/GameBoard.ts index 412e11fbbb..487678f1e3 100644 --- a/src/js/stendhal/entity/GameBoard.ts +++ b/src/js/stendhal/entity/GameBoard.ts @@ -9,28 +9,24 @@ * * ***************************************************************************/ +import { Paths } from "../data/Paths"; import { Entity } from "./Entity"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class GameBoard extends Entity { override minimapShow = false; override zIndex = 100; - constructor() { - super(); - this.sprite = { - height: 32 * 3, - width: 32 * 3 - }; - } - override set(key: string, value: any) { super.set(key, value); if (key === "class") { - this.sprite.filename = stendhal.paths.sprites + "/gameboard/" - + this["class"] + ".png"; + let filename = Paths.sprites + "/gameboard/" + this["class"] + ".png"; + this.imageSprite?.free(); + this.imageSprite = new ImageSprite(images.load(filename), 0, 0, 3*32, 3*32); } } @@ -39,7 +35,7 @@ export class GameBoard extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/walk.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/walk.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Gate.ts b/src/js/stendhal/entity/Gate.ts index 881e9659e5..e151c10c6a 100644 --- a/src/js/stendhal/entity/Gate.ts +++ b/src/js/stendhal/entity/Gate.ts @@ -9,30 +9,45 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { MenuItem } from "../action/MenuItem"; import { Entity } from "./Entity"; +import { Paths } from "../data/Paths"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { images } from "sprite/image/ImageManager"; +import { ImageSprite } from "sprite/image/ImageSprite"; export class Gate extends Entity { override zIndex = 5000; + private locked = false; + + override init() { + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/doors/" + this["image"] + "_" + this["orientation"] + ".png"), + 0, 0, 0, 0); + } override set(key: string, value: any) { super.set(key, value); if (key === "resistance") { - this["locked"] = parseInt(value, 10) !== 0; + this.locked = parseInt(value, 10) !== 0; } else if (key === "image" || key === "orientation") { - // Force re-evaluation of the sprite - delete this["_image"]; + if (this.imageSprite) { + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/doors/" + this["image"] + "_" + this["orientation"] + ".png"), + 0, 0, 0, 0); + } } } override buildActions(list: MenuItem[]) { var id = this["id"]; list.push({ - title: (this["locked"]) ? "Open" : "Close", + title: (this.locked) ? "Open" : "Close", action: function(_entity: Entity) { var action = { "type": "use", @@ -44,20 +59,18 @@ export class Gate extends Entity { }); } - override draw(ctx: CanvasRenderingContext2D) { - if (this._image == undefined) { - var filename = stendhal.paths.sprites + "/doors/" + this["image"] + "_" + this["orientation"] + ".png"; - this._image = stendhal.data.sprites.get(filename); - } - if (this._image.height) { - var xOffset = -32 * Math.floor(this._image.width / 32 / 2); - var height = this._image.height / 2; - var yOffset = -32 * Math.floor(height / 32 / 2); - var localX = this["_x"] * 32 + xOffset; - var localY = this["_y"] * 32 + yOffset; - var yStart = (this["locked"]) ? height : 0; - ctx.drawImage(this._image, 0, yStart, this._image.width, height, localX, localY, this._image.width, height); + override draw(ctx: RenderingContext2D) { + let image = this.imageSprite?.imageRef?.image; + if (!image) { + return; } + let xOffset = -32 * Math.floor(image.width / 32 / 2); + let height = image.height / 2; + let yOffset = -32 * Math.floor(height / 32 / 2); + let localX = this["_x"] * 32 + xOffset; + let localY = this["_y"] * 32 + yOffset; + let yStart = (this.locked) ? height : 0; + ctx.drawImage(image, 0, yStart, image.width, height, localX, localY, image.width, height); } override isVisibleToAction(_filter: boolean) { @@ -74,7 +87,7 @@ export class Gate extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/activity.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/activity.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Ground.ts b/src/js/stendhal/entity/Ground.ts index 68c69f360e..14159c5455 100644 --- a/src/js/stendhal/entity/Ground.ts +++ b/src/js/stendhal/entity/Ground.ts @@ -9,8 +9,12 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { Paths } from "../data/Paths"; + +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; +import { TileMap } from "data/TileMap"; +import { ViewPort } from "ui/ViewPort"; /** @@ -31,39 +35,42 @@ export class Ground { * @return Direction of the zone to change to, null if no zone change should happen */ private calculateZoneChangeDirection(x: number, y: number): string|null { + let map = TileMap.get(); if (x < 15) { return "4"; // LEFT } - if (x > stendhal.data.map.zoneSizeX * 32 - 15) { + if (x > map.zoneSizeX * 32 - 15) { return "2"; // RIGHT } if (y < 15) { return "1"; // UP } - if (y > stendhal.data.map.zoneSizeY * 32 - 15) { + if (y > map.zoneSizeY * 32 - 15) { return "3"; // DOWN } return null; } getCursor(x: number, y: number): string { - if ((x < 15) || (y < 15) || (x > stendhal.data.map.zoneSizeX * 32 - 15) || (y > stendhal.data.map.zoneSizeY * 32 - 15)) { - return "url(" + stendhal.paths.sprites + "/cursor/walkborder.png) 1 3, auto" + let map = TileMap.get(); + if ((x < 15) || (y < 15) || (x > map.zoneSizeX * 32 - 15) || (y > map.zoneSizeY * 32 - 15)) { + return "url(" + Paths.sprites + "/cursor/walkborder.png) 1 3, auto" } var worldX = Math.floor(x / 32); var worldY = Math.floor(y / 32); - if (stendhal.data.map.collision(worldX, worldY)) { - return "url(" + stendhal.paths.sprites + "/cursor/stop.png) 1 3, auto"; + if (map.collision(worldX, worldY)) { + return "url(" + Paths.sprites + "/cursor/stop.png) 1 3, auto"; } - return "url(" + stendhal.paths.sprites + "/cursor/walk.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/walk.png) 1 3, auto"; } onclick(x: number, y: number, dblclick: boolean) { if (!stendhal.config.getBoolean("pathfinding")) { return; } - var gameX = x + stendhal.ui.gamewindow.offsetX; - var gameY = y + stendhal.ui.gamewindow.offsetY; + let viewPort = ViewPort.get(); + var gameX = x + viewPort.offsetX; + var gameY = y + viewPort.offsetY; var action = { "type": "moveto", "x": "" + Math.floor(gameX / 32), diff --git a/src/js/stendhal/entity/GrowingEntitySpawner.ts b/src/js/stendhal/entity/GrowingEntitySpawner.ts index 3aa64e390a..4f5acf2ce4 100644 --- a/src/js/stendhal/entity/GrowingEntitySpawner.ts +++ b/src/js/stendhal/entity/GrowingEntitySpawner.ts @@ -9,16 +9,33 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { MenuItem } from "../action/MenuItem"; import { Entity } from "./Entity"; +import { Paths } from "../data/Paths"; +import { singletons } from "../SingletonRepo"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class GrowingEntitySpawner extends Entity { override zIndex = 3000; + + override set(key: string, value: any) { + super.set(key, value); + if (key === "class") { + this.imageSprite?.free(); + let className = value.replace(" ", "_"); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/" + className + ".png"), + 0, 0, 0, 0); + } + } + + /** * is this entity visible to a specific action * @@ -48,34 +65,23 @@ export class GrowingEntitySpawner extends Entity { marauroa.clientFramework.sendAction(action); } - /** - * draw RPEntities - */ - override draw(ctx: CanvasRenderingContext2D) { - var localX = this["x"] * 32; - var localY = this["y"] * 32; - - // FIXME: - // temporary fix, problem lies higher up - // appears to only affect button_mushroom_grower - // could be issue in marauroa - let class_name = this["class"]; - if (class_name.includes(" ")) { - class_name = class_name.replace(" ", "_"); + override draw(ctx: RenderingContext2D) { + let localX = this["x"] * 32; + let localY = this["y"] * 32; + let image = this.imageSprite?.imageRef?.image; + if (!image) { + return; } - var image = stendhal.data.sprites.get(stendhal.paths.sprites + "/" + class_name + ".png"); - if (image.height) { // image.complete is true on missing image files - var count = parseInt(this["max_ripeness"], 10) + 1; - var drawHeight = image.height / count; - var yRow = this["ripeness"]; - ctx.drawImage(image, 0, yRow * drawHeight, image.width, drawHeight, + let count = parseInt(this["max_ripeness"], 10) + 1; + let drawHeight = image.height / count; + let yRow = this["ripeness"]; + ctx.drawImage(image, 0, yRow * drawHeight, image.width, drawHeight, localX, localY - drawHeight + 32, image.width, drawHeight); - } } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/harvest.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/harvest.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/InvisibleEntity.ts b/src/js/stendhal/entity/InvisibleEntity.ts index 36afdfe64c..9b5b27d361 100644 --- a/src/js/stendhal/entity/InvisibleEntity.ts +++ b/src/js/stendhal/entity/InvisibleEntity.ts @@ -9,10 +9,9 @@ * * ***************************************************************************/ +import { Paths } from "../data/Paths"; import { Entity } from "./Entity"; -declare var stendhal: any; - export class InvisibleEntity extends Entity { override isVisibleToAction(_filter: boolean) { @@ -20,7 +19,7 @@ export class InvisibleEntity extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/walk.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/walk.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Item.ts b/src/js/stendhal/entity/Item.ts index 791bc533db..87116c2bea 100644 --- a/src/js/stendhal/entity/Item.ts +++ b/src/js/stendhal/entity/Item.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,14 +9,16 @@ * * ***************************************************************************/ -import { ItemMap } from "./ItemMap"; +import { RenderingContext2D } from "util/Types"; import { MenuItem } from "../action/MenuItem"; -import { Entity } from "./Entity"; +import { Paths } from "../data/Paths"; import { TextSprite } from "../sprite/TextSprite"; +import { Entity } from "./Entity"; +import { ItemMap } from "./ItemMap"; -declare var marauroa: any; -declare var stendhal: any; - +import { marauroa } from "marauroa"; +import { images } from "sprite/image/ImageManager"; +import { ImageSprite } from "sprite/image/ImageSprite"; export class Item extends Entity { @@ -28,15 +30,10 @@ export class Item extends Entity { // animation private frameTimeStamp = 0; private animated: boolean|null = null; - private xFrames: number|null = null; - private yFrames: number|null = null; + constructor() { super(); - this.sprite = { - height: 32, - width: 32 - }; this.quantityTextSprite = new TextSprite("", "white", "10px sans-serif"); } @@ -72,36 +69,50 @@ export class Item extends Entity { override set(key: string, value: any) { super.set(key, value); if (key === "class" || key === "subclass") { - this.sprite.filename = stendhal.paths.sprites + "/items/" - + this["class"] + "/" + this["subclass"] + ".png"; - } - if (key === "quantity") { + if (this["class"] && this["subclass"]) { + this.imageSprite?.free(); + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/items/" + this["class"] + "/" + this["subclass"] + ".png"), + 0, (this["state"] || 0) * 32, 32, 32); + } + } else if (key === "state") { + if (this.imageSprite) { + this.imageSprite.offsetY = (this["state"] || 0) * 32 + } + } else if (key === "quantity") { this.quantityTextSprite = new TextSprite(this.formatQuantity(), "white", "10px sans-serif"); } } - override draw(ctx: CanvasRenderingContext2D) { - this.sprite.offsetY = (this["state"] || 0) * 32 + override draw(ctx: RenderingContext2D) { this.stepAnimation(); - this.drawAt(ctx, this["x"] * 32, this["y"] * 32); } - drawAt(ctx: CanvasRenderingContext2D, x: number, y: number) { - if (this.sprite) { - this.drawSpriteAt(ctx, x, y); - let textMetrics = this.quantityTextSprite.getTextMetrics(ctx); - this.quantityTextSprite.draw(ctx, x + (32 - textMetrics.width), y + 6); + drawAt(ctx: RenderingContext2D, x: number, y: number) { + let image = this.imageSprite?.imageRef?.image; + if (!image) { + return; } + this.drawSpriteAt(ctx, x, y); + let textMetrics = this.quantityTextSprite.getTextMetrics(ctx); + if (!textMetrics) { + throw new Error("textMetrics is undefined"); + } + this.quantityTextSprite.draw(ctx, x + (32 - textMetrics.width), y + 6); } public stepAnimation() { + let image = this.imageSprite?.imageRef?.image; + if (!image) { + return; + } const currentTimeStamp = +new Date(); - if (this.frameTimeStamp == 0) { - this.frameTimeStamp = currentTimeStamp; - this.sprite.offsetX = 0; - this.sprite.offsetY = 0; - } else if (currentTimeStamp - this.frameTimeStamp >= 100) { +// if (this.frameTimeStamp == 0) { +// this.frameTimeStamp = currentTimeStamp; +// this.imageSprite?.offsetX = 0; +// } + if (currentTimeStamp - this.frameTimeStamp >= 100) { // FIXME: need proper FPS limit this.setXFrameIndex(this.getXFrameIndex() + 1); this.frameTimeStamp = currentTimeStamp; @@ -128,7 +139,7 @@ export class Item extends Entity { } else { cursor = ItemMap.getCursor(this["class"], this["name"]); } - return "url(" + stendhal.paths.sprites + "/cursor/" + cursor + ".png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/" + cursor + ".png) 1 3, auto"; } public getToolTip(): string { @@ -142,51 +153,31 @@ export class Item extends Entity { } public isAnimated(): boolean { - if (!stendhal.data.sprites.get(this.sprite.filename).height) { - return false; + if (this.animated === null) { + let image = this.imageSprite?.imageRef?.image + if (image) { + this.animated = (image.width / 32) > 1; + } } - if (this.animated == null) { - // store animation state - this.animated = (stendhal.data.sprites.get(this.sprite.filename).width / 32) > 1; - } - - return this.animated; + return this.animated || false; } private setXFrameIndex(idx: number) { - if (this.xFrames == null) { - const img = stendhal.data.sprites.get(this.sprite.filename); - this.xFrames = img.width / 32; + let image = this.imageSprite?.imageRef.image + if (!image) { + return; } - - if (idx >= this.xFrames) { + let frames = image.width / 32; + if (idx >= frames) { // restart idx = 0; } - this.sprite.offsetX = idx * 32; - } - - private setYFrameIndex(idx: number) { - if (this.yFrames == null) { - const img = stendhal.data.sprites.get(this.sprite.filename); - this.yFrames = img.height / 32; - } - - if (idx >= this.yFrames) { - // restart - idx = 0; - } - - this.sprite.offsetY = idx * 32; + this.imageSprite!.offsetX = idx * 32; } public getXFrameIndex(): number { - return (this.sprite.offsetX || 0) / 32; - } - - public getYFrameIndex(): number { - return (this.sprite.offsetY || 0) / 32; + return (this.imageSprite?.offsetX || 0) / 32; } public override isDraggable(): boolean { diff --git a/src/js/stendhal/entity/ItemMap.ts b/src/js/stendhal/entity/ItemMap.ts index 23f49b9988..b352055586 100644 --- a/src/js/stendhal/entity/ItemMap.ts +++ b/src/js/stendhal/entity/ItemMap.ts @@ -15,7 +15,7 @@ import { MenuItem } from "../action/MenuItem"; const config = singletons.getConfigManager(); -declare var marauroa: any; +import { marauroa } from "marauroa" const defaultUse = { title: "Use", diff --git a/src/js/stendhal/entity/LoopedSoundSource.ts b/src/js/stendhal/entity/LoopedSoundSource.ts index 10b247f7de..7f03faec0b 100644 --- a/src/js/stendhal/entity/LoopedSoundSource.ts +++ b/src/js/stendhal/entity/LoopedSoundSource.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2023 - Stendhal * + * Copyright © 2003-2024 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -11,14 +11,11 @@ import { InvisibleEntity } from "./InvisibleEntity"; -import { LoopedSoundSourceManager } from "../ui/LoopedSoundSourceManager"; +import { LoopedSoundSourceManager } from "../data/sound/LoopedSoundSourceManager"; export class LoopedSoundSource extends InvisibleEntity { - private readonly lssMan = LoopedSoundSourceManager.get(); - - /** * Checks if a looped sound source has been loaded with this * entity's ID. @@ -27,6 +24,6 @@ export class LoopedSoundSource extends InvisibleEntity { * true if the ID is found in the sources list. */ isLoaded(): boolean { - return typeof(this.lssMan.getSources()[this["id"]]) !== "undefined"; + return typeof(LoopedSoundSourceManager.get().getSources()[this["id"]]) !== "undefined"; } } diff --git a/src/js/stendhal/entity/NPC.ts b/src/js/stendhal/entity/NPC.ts index c614db4f97..d4c1b42cb8 100644 --- a/src/js/stendhal/entity/NPC.ts +++ b/src/js/stendhal/entity/NPC.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2024 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -11,9 +11,11 @@ import { RPEntity } from "./RPEntity"; -import { Color } from "../data/color/Color"; +import { EntityOverlayRegistry } from "../data/EntityOverlayRegistry"; -declare var stendhal: any; +import { Color } from "../data/color/Color"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; export class NPC extends RPEntity { override minimapStyle = Color.NPC; @@ -26,16 +28,21 @@ export class NPC extends RPEntity { this["base_hp"] = 100; } - override set(key: string, value: string) { + override set(key: string, value: any) { super.set(key, value); - if (key === "name" && value.startsWith("Zekiel")) { - // Zekiel uses transparentnpc sprite but he is taller - this.titleDrawYOffset = -32; + if (key === "name") { + // overlay animation + this.overlay = EntityOverlayRegistry.get("NPC", this); + + if (value.startsWith("Zekiel")) { + // Zekiel uses transparentnpc sprite but he is taller + this.titleDrawYOffset = -32; + } } } - override drawTop(ctx: CanvasRenderingContext2D) { + override drawTop(ctx: RenderingContext2D) { var localX = this["_x"] * 32; var localY = this["_y"] * 32; if (typeof(this["no_hpbar"]) == "undefined") { @@ -47,7 +54,7 @@ export class NPC extends RPEntity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Player.ts b/src/js/stendhal/entity/Player.ts index 2a07d86520..80df9f55f9 100644 --- a/src/js/stendhal/entity/Player.ts +++ b/src/js/stendhal/entity/Player.ts @@ -9,8 +9,8 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; import { RPEntity } from "./RPEntity"; @@ -22,6 +22,8 @@ import { UIComponentEnum } from "../ui/UIComponentEnum"; import { GroupPanelComponent } from "../ui/component/GroupPanelComponent"; import { Color } from "../data/color/Color"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; export class Player extends RPEntity { @@ -169,7 +171,7 @@ export class Player extends RPEntity { } } - override draw(ctx: CanvasRenderingContext2D) { + override draw(ctx: RenderingContext2D) { if (this.isIgnored()) { return; } @@ -236,8 +238,8 @@ export class Player extends RPEntity { override getCursor(_x: number, _y: number) { if (this.isVisibleToAction(false)) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; } - return "url(" + stendhal.paths.sprites + "/cursor/walk.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/walk.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/PopupInventory.ts b/src/js/stendhal/entity/PopupInventory.ts index fb995da53e..5af5364106 100644 --- a/src/js/stendhal/entity/PopupInventory.ts +++ b/src/js/stendhal/entity/PopupInventory.ts @@ -9,10 +9,10 @@ * * ***************************************************************************/ -import { Entity } from "./Entity"; -import { RPObject } from "./RPObject"; +import { marauroa, RPObject, RPZone } from "marauroa"; -declare var marauroa: any; +import { RenderingContext2D } from "util/Types"; +import { Entity } from "./Entity"; export class PopupInventory extends Entity { @@ -20,7 +20,7 @@ export class PopupInventory extends Entity { protected maxDistToView = 4; - override draw(ctx: CanvasRenderingContext2D) { + override draw(ctx: RenderingContext2D) { super.draw(ctx); if (this.inventory && this.inventory.isOpen() && !this.canViewContents()) { // player has moved too far away @@ -32,9 +32,9 @@ export class PopupInventory extends Entity { // inheriting classes can override } - override destroy(_parent: RPObject) { + override destroy(parent: RPObject|RPZone) { this.closeInventoryWindow(); - super.destroy(_parent); + super.destroy(parent); } /** diff --git a/src/js/stendhal/entity/Portal.ts b/src/js/stendhal/entity/Portal.ts index 27a62d3ea7..de6e562436 100644 --- a/src/js/stendhal/entity/Portal.ts +++ b/src/js/stendhal/entity/Portal.ts @@ -13,9 +13,10 @@ import { Entity } from "./Entity"; import { MenuItem } from "../action/MenuItem"; import { Color } from "../data/color/Color"; +import { Paths } from "../data/Paths"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { TileMap } from "data/TileMap"; export class Portal extends Entity { @@ -59,7 +60,8 @@ export class Portal extends Entity { */ override getDefaultAction() { // don't try to walk to if on collision tile - if (stendhal.data.map.collision(this["x"], this["y"])) { + let map = TileMap.get(); + if (map.collision(this["x"], this["y"])) { return { "type": "use", "target": "#" + this["id"], @@ -75,7 +77,7 @@ export class Portal extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/portal.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/portal.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/RPEntity.ts b/src/js/stendhal/entity/RPEntity.ts index cc82e86f91..7ccc19d564 100644 --- a/src/js/stendhal/entity/RPEntity.ts +++ b/src/js/stendhal/entity/RPEntity.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,44 +9,47 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa"; +import { stendhal } from "../stendhal"; -import { ActiveEntity } from "./ActiveEntity"; -import { Entity } from "./Entity"; import { singletons } from "../SingletonRepo"; import { MenuItem } from "../action/MenuItem"; +import { ActiveEntity } from "./ActiveEntity"; +import { Entity } from "./Entity"; import { Color } from "../data/color/Color"; import { Chat } from "../util/Chat"; import { Nature } from "../util/Nature"; -import { Floater } from "../sprite/Floater"; import { EmojiSprite } from "../sprite/EmojiSprite"; +import { Floater } from "../sprite/Floater"; +import { OverlaySpriteImpl } from "../sprite/OverlaySpriteImpl"; import { SpeechBubble } from "../sprite/SpeechBubble"; import { TextSprite } from "../sprite/TextSprite"; import { BarehandAttackSprite } from "../sprite/action/BarehandAttackSprite"; -//import { MeleeAttackSprite } from "../sprite/action/MeleeAttackSprite"; import { RangedAttackSprite } from "../sprite/action/RangedAttackSprite"; -import { SoundManager } from "../ui/SoundManager"; +import { htmlImageStore } from "data/HTMLImageStore"; import { ImageWithDimensions } from "data/ImageWithDimensions"; +import { OutfitStore } from "data/OutfitStore"; +import { ViewPort } from "ui/ViewPort"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; +import { statusIconDrawer } from "sprite/indicator/StatusIconDrawer"; var HEALTH_BAR_HEIGHT = 6; export class RPEntity extends ActiveEntity { - private static readonly soundManager = SoundManager.get(); - override zIndex = 8000; drawY = 0; spritePath = ""; titleStyle = "#FFFFFF"; _target?: RPEntity; - attackSprite: any = undefined; // TODO + protected attackSprite?: OverlaySpriteImpl; attackResult: any = undefined; // TODO dir = 3; titleTextSprite?: TextSprite; @@ -56,10 +59,17 @@ export class RPEntity extends ActiveEntity { // for adjusting entity hp bar & title vertical position protected titleDrawYOffset: number = 0; // canvas for merging outfit layers to be drawn - private octx?: CanvasRenderingContext2D; + private octx?: RenderingContext2D; private attackers: {[key: string]: any} = { size: 0 }; + /** + * Animation drawn over entity sprite. + * + * TODO: maybe support multiple overlays + */ + protected overlay?: OverlaySpriteImpl; + override set(key: string, value: any) { // Ugly hack to detect changes. The old value is no @@ -97,6 +107,8 @@ export class RPEntity extends ActiveEntity { this.onLevelChanged(key, value, oldValue); } else if (["title", "name", "class", "type"].indexOf(key) >-1) { this.createTitleTextSprite(); + } else if (key === "subclass" && typeof(oldValue) !== "undefined" && value !== oldValue) { + this.onTransformed(); } } @@ -197,11 +209,11 @@ export class RPEntity extends ActiveEntity { * Text to display. */ addSpeechBubble(text: string) { - stendhal.ui.gamewindow.addTextSprite(new SpeechBubble(text, this)); + ViewPort.get().addTextSprite(new SpeechBubble(text, this)); } addEmoji(emoji: HTMLImageElement) { - stendhal.ui.gamewindow.addEmojiSprite(new EmojiSprite(emoji, this)); + ViewPort.get().addEmojiSprite(new EmojiSprite(emoji, this)); } /** @@ -211,14 +223,13 @@ export class RPEntity extends ActiveEntity { return typeof(this["no_shadow"]) === "undefined"; } - drawMultipartOutfit(ctx: CanvasRenderingContext2D) { + drawMultipartOutfit(ctx: RenderingContext2D) { + const store = OutfitStore.get(); // layers in draw order - var layers: string[] = []; + var layers = store.getLayerNames(); var outfit: {[key: string]: number} = {}; if ("outfit_ext" in this) { - layers = ["body", "dress", "head", "mouth", "eyes", "mask", "hair", "hat", "detail"]; - for (const part of this["outfit_ext"].split(",")) { if (part.includes("=")) { var tmp = part.split("="); @@ -226,7 +237,7 @@ export class RPEntity extends ActiveEntity { } } } else { - layers = ["body", "dress", "head", "hair", "detail"]; + layers = store.getLayerNames(true); outfit["body"] = this["outfit"] % 100; outfit["dress"] = Math.floor(this["outfit"]/100) % 100; @@ -238,7 +249,7 @@ export class RPEntity extends ActiveEntity { if (stendhal.config.getBoolean("effect.shadows") && this.castsShadow()) { // dressed entities should use 48x64 sprites // FIXME: this will not display correctly for horse outfit - const shadow = stendhal.data.sprites.getShadow("48x64"); + const shadow = htmlImageStore.getShadow("48x64"); if (shadow && shadow.complete && shadow.height) { // draw shadow below other layers @@ -249,23 +260,21 @@ export class RPEntity extends ActiveEntity { if (this.octx) { this.octx.clearRect(0, 0, this.octx.canvas.width, this.octx.canvas.height); } - if (stendhal.data.outfit.detailHasRearLayer(outfit["detail"])) { + if (store.detailHasRearLayer(outfit["detail"])) { layers.splice(0, 0, "detail-rear"); outfit["detail-rear"] = outfit["detail"]; } for (const layer of layers) { // hair is not drawn under certain hats/helmets - if (layer == "hair" && !stendhal.data.outfit.drawHair(outfit["hat"])) { + if (layer == "hair" && !store.drawHair(outfit["hat"])) { continue; } const lsprite = this.getOutfitPart(layer, outfit[layer], outfit["body"]); if (lsprite && lsprite.complete && lsprite.height) { if (!this.octx) { - let ocanvas = document.createElement("canvas"); + let ocanvas = new OffscreenCanvas(lsprite.width, lsprite.height); this.octx = ocanvas.getContext("2d")!; - ocanvas.width = lsprite.width; - ocanvas.height = lsprite.height; } this.octx!.drawImage(lsprite, 0, 0); } @@ -273,6 +282,7 @@ export class RPEntity extends ActiveEntity { if (this.octx) { this.drawSpriteImage(ctx, this.octx.canvas); + this.drawOverlayAnimation(ctx); } } @@ -295,27 +305,28 @@ export class RPEntity extends ActiveEntity { n = "0" + index; } + let store = OutfitStore.get(); if (part === "body" && index < 3 && stendhal.config.getBoolean("effect.no-nude")) { n += "-nonude"; - } else if (part === "dress" && stendhal.data.outfit.drawBustyDress(index, body)) { + } else if (part === "dress" && store.drawBustyDress(index, body)) { n += "b"; } else if (part.endsWith("-rear")) { n += "-rear"; part = part.replace(/-rear$/, ""); } - const filename = stendhal.paths.sprites + "/outfit/" + part + "/" + n + ".png"; + const filename = Paths.sprites + "/outfit/" + part + "/" + n + ".png"; const colors = this["outfit_colors"]; let colorname; - if (part === "body" || part === "head") { + if (store.isSkinLayer(part)) { colorname = "skin"; } else { colorname = part; } if (typeof(colors) !== "undefined" && (typeof(colors[colorname]) !== "undefined")) { - return stendhal.data.sprites.getFiltered(filename, "trueColor", colors[colorname]); + return htmlImageStore.getFiltered(filename, "trueColor", colors[colorname]); } else { - return stendhal.data.sprites.get(filename); + return htmlImageStore.get(filename); } } @@ -324,7 +335,7 @@ export class RPEntity extends ActiveEntity { * extends past top edge. */ public setStatusBarOffset() { - const screenOffsetY = stendhal.ui.gamewindow.offsetY; + const screenOffsetY = ViewPort.get().offsetY; const entityBottom = (this["_y"] * 32) + (this["height"] * 32); // FIXME: how to get text height dynamically? const entityTop = entityBottom - this["drawHeight"] @@ -340,137 +351,70 @@ export class RPEntity extends ActiveEntity { /** * draw RPEntities */ - override draw(ctx: CanvasRenderingContext2D) { + override draw(ctx: RenderingContext2D) { if (typeof(this["ghostmode"]) != "undefined" && marauroa.me && !marauroa.me.isAdmin()) { return; } this.drawCombat(ctx); this.drawMain(ctx); this.drawAttack(ctx); - this.drawStatusIcons(ctx); + statusIconDrawer.drawStatusIcons(this, ctx); } - drawMain(ctx: CanvasRenderingContext2D) { + drawMain(ctx: RenderingContext2D) { if (typeof(this["outfit"]) != "undefined" || typeof(this["outfit_ext"]) != "undefined") { this.drawMultipartOutfit(ctx); } else { - let filename = stendhal.paths.sprites + "/" + this.spritePath + "/" + this["class"]; - if (typeof(this["subclass"]) != "undefined") { - filename = filename + "/" + this["subclass"]; - } - - // check for safe image - if (!stendhal.config.getBoolean("effect.blood") && stendhal.data.sprites.hasSafeImage(filename)) { - filename = filename + "-safe.png"; - } else { - filename = filename + ".png"; - } - - let image = stendhal.data.sprites.get(filename); - - if (stendhal.config.getBoolean("effect.shadows") && this.castsShadow()) { - // check for configured shadow style - let shadow_style = this["shadow_style"]; - if (typeof(shadow_style) === "undefined") { - // default to sprite dimensions - shadow_style = (image.width / 3) + "x" + (image.height / 4); - } - - const shadow = stendhal.data.sprites.getShadow(shadow_style); - - // draw shadow first - if (typeof(shadow) !== "undefined") { - this.drawSpriteImage(ctx, shadow); - } - } - - this.drawSpriteImage(ctx, image); + this.drawSingleOutfit(ctx); } } - drawStatusIcons(ctx: CanvasRenderingContext2D) { - - function _drawAnimatedIcon(icon: CanvasImageSource, delay: number, nFrames: number, xdim: number, ydim: number, x: number, y: number) { - var frame = Math.floor(Date.now() / delay) % nFrames; - ctx.drawImage(icon, frame * xdim, 0, xdim, ydim, x, y, xdim, ydim); - } - function drawAnimatedIcon(iconPath: string, delay: number, x: number, y: number, fWidth?: number) { - var icon = stendhal.data.sprites.get(iconPath); - var dimH = icon.height; - var dimW = typeof(fWidth) !== "undefined" ? fWidth : dimH; - var nFrames = icon.width / dimW; - _drawAnimatedIcon(icon, delay, nFrames, dimW, dimH, x, y); - } - function drawAnimatedIconWithFrames(iconPath: string, nFrames: number, delay: number, x: number, y: number) { - var icon = stendhal.data.sprites.get(iconPath); - var ydim = icon.height; - var xdim = icon.width / nFrames; - _drawAnimatedIcon(icon, delay, nFrames, xdim, ydim, x, y); + drawSingleOutfit(ctx: RenderingContext2D) { + let filename = Paths.sprites + "/" + this.spritePath + "/" + this["class"]; + if (typeof(this["subclass"]) != "undefined") { + filename = filename + "/" + this["subclass"]; } - var x = this["_x"] * 32 - 10; - var y = (this["_y"] + 1) * 32; - if (this.hasOwnProperty("choking")) { - ctx.drawImage(stendhal.data.sprites.get(stendhal.paths.sprites + "/ideas/choking.png"), x, y - 10); - } else if (this.hasOwnProperty("eating")) { - ctx.drawImage(stendhal.data.sprites.get(stendhal.paths.sprites + "/ideas/eat.png"), x, y - 10); + // check for safe image + if (!stendhal.config.getBoolean("effect.blood") && htmlImageStore.hasSafeImage(filename)) { + filename = filename + "-safe.png"; + } else { + filename = filename + ".png"; } - // NPC and pet idea icons - if (this.hasOwnProperty("idea")) { - const idea = stendhal.paths.sprites + "/ideas/" + this["idea"] + ".png"; - const ani = stendhal.data.sprites.animations.idea[this["idea"]]; - if (ani) { - drawAnimatedIcon(idea, ani.delay, x + ani.offsetX * this["width"], - y - this["drawHeight"] + ani.offsetY); - } else { - ctx.drawImage(stendhal.data.sprites.get(idea), x + 32 * this["width"], - y - this["drawHeight"]); + + let image = htmlImageStore.get(filename); + + if (stendhal.config.getBoolean("effect.shadows") && this.castsShadow()) { + // check for configured shadow style + let shadow_style = this["shadow_style"]; + if (typeof(shadow_style) === "undefined") { + // default to sprite dimensions + shadow_style = (image.width / 3) + "x" + (image.height / 4); + } + + const shadow = htmlImageStore.getShadow(shadow_style); + + // draw shadow first + if (typeof(shadow) !== "undefined") { + this.drawSpriteImage(ctx, shadow); } } - if (this.hasOwnProperty("away")) { - drawAnimatedIcon(stendhal.paths.sprites + "/ideas/away.png", 1500, x + 32 * this["width"], y - this["drawHeight"]); - } - if (this.hasOwnProperty("grumpy")) { - drawAnimatedIcon(stendhal.paths.sprites + "/ideas/grumpy.png", 1000, x + 5, y - this["drawHeight"]); - } - if (this.hasOwnProperty("last_player_kill_time")) { - drawAnimatedIconWithFrames(stendhal.paths.sprites + "/ideas/pk.png", 12, 300, x, y - this["drawHeight"]); - } - // status affects - if (this.hasOwnProperty("poisoned")) { - drawAnimatedIcon(stendhal.paths.sprites + "/status/poison.png", 100, x + 32 * this["width"] - 10, y - this["drawHeight"]); - } - if (this.hasOwnProperty("status_confuse")) { - drawAnimatedIcon(stendhal.paths.sprites + "/status/confuse.png", 200, x + 32 * this["width"] - 14, y - this["drawHeight"] + 16); - } - if (this.hasOwnProperty("status_shock")) { - drawAnimatedIcon(stendhal.paths.sprites + "/status/shock.png", 200, x + 32 * this["width"] - 25, y - 32, 38); - } - // NPC job icons - let nextX = x; - if (this.hasOwnProperty("job_healer")) { - ctx.drawImage(stendhal.data.sprites.get(stendhal.paths.sprites + "/status/healer.png"), nextX, y - 10); - nextX += 12; - } - if (this.hasOwnProperty("job_merchant")) { - ctx.drawImage(stendhal.data.sprites.get(stendhal.paths.sprites + "/status/merchant.png"), nextX, y - 10); - nextX += 12; - } - if (this.hasOwnProperty("job_producer")) { - ctx.drawImage(stendhal.data.sprites.get(stendhal.paths.sprites + "/status/producer.png"), nextX, y - 16); - nextX += 16; - } + + this.drawSpriteImage(ctx, image); + this.drawOverlayAnimation(ctx); } + + /** * Draw colored ellipses (or rectangles on browsers that do not support * ellipses) when the entity is being attacked, or is attacking the user. */ - drawCombat(ctx: CanvasRenderingContext2D) { + drawCombat(ctx: RenderingContext2D) { if (this.attackers.size > 0) { ctx.lineWidth = 1; /* - * As of 2015-9-15 CanvasRenderingContext2D.ellipse() is not + * As of 2015-9-15 RenderingContext2D.ellipse() is not * supported in most browsers. Fall back to rectangles on these. * Also on Chrome 45.0.2454.85 ellipse() does not seem to support * the begin angle parameter correctly, nor does the stroke @@ -531,7 +475,7 @@ export class RPEntity extends ActiveEntity { * the coordinates where to start floating. The method should return true * when the floater should be removed. */ - drawFloaters(ctx: CanvasRenderingContext2D) { + drawFloaters(ctx: RenderingContext2D) { var centerX = (this["_x"] + this["width"] / 2) * 32; var topY = (this["_y"] + 1) * 32 - this["drawHeight"]; // Grab an unchanging copy @@ -547,22 +491,23 @@ export class RPEntity extends ActiveEntity { } /** - * @param {CanvasRenderingContext2D} ctx + * @param {RenderingContext2D} ctx * @param {Image} image */ - drawSpriteImage(ctx: CanvasRenderingContext2D, image: CanvasImageSource & ImageWithDimensions) { + drawSpriteImage(ctx: RenderingContext2D, image: CanvasImageSource & ImageWithDimensions) { var localX = this["_x"] * 32; var localY = this["_y"] * 32; if (image.height) { // image.complete is true on missing image files var nFrames = 3; var nDirections = 4; - var yRow = this["dir"] - 1; + const facing = this.getFaceDirection().val; + var yRow = facing - 1; var frame = 1; // draw center column when idle // Ents are a hack in Java client too if (this["class"] == "ent") { nFrames = 1; nDirections = 2; - yRow = Math.floor((this["dir"] - 1) / 2); + yRow = Math.floor((facing - 1) / 2); frame = 0; } this["drawHeight"] = image.height as number / nDirections; @@ -589,13 +534,19 @@ export class RPEntity extends ActiveEntity { ctx.globalAlpha = opacity * 0.01; } - ctx.drawImage(image, frame * this["drawWidth"], yRow * this["drawHeight"], this["drawWidth"], this["drawHeight"], localX + drawX, localY + drawY, this["drawWidth"], this["drawHeight"]); + // store offset for use by other drawing methods + this["drawOffsetX"] = localX + drawX; + this["drawOffsetY"] = localY + drawY; + + ctx.drawImage(image, frame * this["drawWidth"], yRow * this["drawHeight"], this["drawWidth"], + this["drawHeight"], this["drawOffsetX"], this["drawOffsetY"], this["drawWidth"], + this["drawHeight"]); // restore opacity ctx.globalAlpha = opacity_orig; } } - drawTop(ctx: CanvasRenderingContext2D) { + drawTop(ctx: RenderingContext2D) { var localX = this["_x"] * 32; var localY = this["_y"] * 32; this.drawFloaters(ctx); @@ -603,7 +554,7 @@ export class RPEntity extends ActiveEntity { this.drawTitle(ctx, localX, localY + this.statusBarYOffset); } - drawHealthBar(ctx: CanvasRenderingContext2D, x: number, y: number) { + drawHealthBar(ctx: RenderingContext2D, x: number, y: number) { var drawX = x + ((this["width"] * 32) - this["drawWidth"]) / 2; var drawY = y + (this["height"] * 32) - this["drawHeight"] - HEALTH_BAR_HEIGHT + this.titleDrawYOffset; @@ -640,30 +591,52 @@ export class RPEntity extends ActiveEntity { } } - drawTitle(ctx: CanvasRenderingContext2D, x: number, y: number) { + drawTitle(ctx: RenderingContext2D, x: number, y: number) { if (this.titleTextSprite) { let textMetrics = this.titleTextSprite.getTextMetrics(ctx); + if (!textMetrics) { + throw new Error("textMetrics is undefined"); + } var drawY = y + (this["height"] * 32) - this["drawHeight"] - HEALTH_BAR_HEIGHT + this.titleDrawYOffset; this.titleTextSprite.draw(ctx, x + (this["width"] * 32 - textMetrics.width) / 2, drawY - 5 - HEALTH_BAR_HEIGHT); } } - drawAttack(ctx: CanvasRenderingContext2D) { - if (this.attackSprite == null) { + drawAttack(ctx: RenderingContext2D) { + if (!this.attackSprite) { return; } if (this.attackSprite.expired()) { - this.attackSprite = null; + // sprite expired & should be removed + // NOTE: if this is done after `this.attackSprite.draw` animation doesn't look right + this.attackSprite.free(); + this.attackSprite = undefined; return; } + let viewPort = ViewPort.get(); var localX = this["_x"] * 32; var localY = this["_y"] * 32; - var localW = this["width"] * stendhal.ui.gamewindow.targetTileWidth; - var localH = this["height"] * stendhal.ui.gamewindow.targetTileHeight; + var localW = this["width"] * viewPort.targetTileWidth; + var localH = this["height"] * viewPort.targetTileHeight; this.attackSprite.draw(ctx, localX, localY, localW, localH); } + /** + * Draws an animation over entity sprite. + * + * @param {RenderingContext2D} ctx + * Canvas context to draw on. + */ + private drawOverlayAnimation(ctx: RenderingContext2D) { + if (this.overlay && this.overlay.draw(ctx, this["drawOffsetX"], this["drawOffsetY"], + this["drawWidth"], this["drawHeight"])) { + // overlay sprite expired + this.overlay.free(); + this.overlay = undefined; + } + } + // attack handling getAttackTarget() { // If the attack target id was read before the target was available, @@ -679,21 +652,21 @@ export class RPEntity extends ActiveEntity { } onDamaged(_source: Entity, damage: number) { - this.attackResult = this.createResultIcon(stendhal.paths.sprites + "/combat/hitted.png"); + this.attackResult = this.createResultIcon(Paths.sprites + "/combat/hitted.png"); var sounds = ["attack-melee-01", "attack-melee-02", "attack-melee-03", "attack-melee-04", "attack-melee-05", "attack-melee-06", "attack-melee-07"]; var index = Math.floor(Math.random() * Math.floor(sounds.length)); - RPEntity.soundManager.playLocalizedEffect(this["_x"], this["_y"], 20, 3, sounds[index], 1); + stendhal.sound.playLocalizedEffect(this["_x"], this["_y"], 20, 3, sounds[index], 1); } onBlocked(_source: Entity) { - this.attackResult = this.createResultIcon(stendhal.paths.sprites + "/combat/blocked.png"); + this.attackResult = this.createResultIcon(Paths.sprites + "/combat/blocked.png"); var sounds = ["clang-metallic-1", "clang-dull-1"]; var index = Math.floor(Math.random() * Math.floor(sounds.length)); - RPEntity.soundManager.playLocalizedEffect(this["_x"], this["_y"], 20, 3, sounds[index], 1); + stendhal.sound.playLocalizedEffect(this["_x"], this["_y"], 20, 3, sounds[index], 1); } onMissed(_source: Entity) { - this.attackResult = this.createResultIcon(stendhal.paths.sprites + "/combat/missed.png"); + this.attackResult = this.createResultIcon(Paths.sprites + "/combat/missed.png"); } protected onHPChanged(change: number) { @@ -748,8 +721,8 @@ export class RPEntity extends ActiveEntity { createResultIcon(imagePath: string) { return { initTime: Date.now(), - image: stendhal.data.sprites.get(imagePath), - draw: function(ctx: CanvasRenderingContext2D, x: number, y: number) { + image: htmlImageStore.get(imagePath), + draw: function(ctx: RenderingContext2D, x: number, y: number) { ctx.drawImage(this.image, x, y); return (Date.now() - this.initTime > 1200); } @@ -762,6 +735,7 @@ export class RPEntity extends ActiveEntity { weapon = undefined; } + this.attackSprite?.free(); if (ranged) { this.attackSprite = new RangedAttackSprite(this, this.getAttackTarget()!, Nature.VALUES[nature].color, weapon); @@ -779,12 +753,12 @@ export class RPEntity extends ActiveEntity { const imagePath = Nature.VALUES[nature].getWeaponPath(weapon); /* if (weapon.startsWith("blade_strike")) { - this.attackSprite = new BarehandAttackSprite(this, stendhal.data.sprites.get(imagePath)); + this.attackSprite = new BarehandAttackSprite(this, imagePath); } else { this.attackSprite = new MeleeAttackSprite(this, imagePath); } */ - this.attackSprite = new BarehandAttackSprite(this, stendhal.data.sprites.get(imagePath)); + this.attackSprite = new BarehandAttackSprite(this, imagePath); } } @@ -813,9 +787,18 @@ export class RPEntity extends ActiveEntity { } } - override destroy(_obj: Entity) { - if (this._target) { - this._target.onAttackStopped(this); - } + override destroy(obj: Entity) { + this._target?.onAttackStopped(this); + this.overlay?.free(); + super.destroy(obj); + } + + /** + * Called when entity's subclass is changed. + * + * Does nothing in this implementation. + */ + protected onTransformed() { + // do nothing } } diff --git a/src/js/stendhal/entity/RPObject.ts b/src/js/stendhal/entity/RPObject.ts deleted file mode 100644 index bac2ac041d..0000000000 --- a/src/js/stendhal/entity/RPObject.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -declare var marauroa: any; - - -/** - * base class for RPObjects - */ -export class RPObject { - [key: string]: any; - - onEvent(e: any) { - var event = marauroa.rpeventFactory.create(e["c"]); - for (var i in e["a"]) { - if (e["a"].hasOwnProperty(i)) { - event[i] = e["a"][i]; - } - event["_rpclass"] = e["c"]; - } - - // Event slots - for (var slot in e["s"]) { - if (e["s"].hasOwnProperty(slot)) { - event[slot] = e["s"][slot]; - } - } - event.execute(this); - } - - set(key: string, value: object) { - this[key] = value; - } - - setMapEntry(map: string, key: string, value: object) { - this[map][key] = value; - } - - unset(key: string) { - delete this[key]; - } - - unsetMapEntry(map: string, key: string) { - delete this[map][key]; - } - - destroy(_parent: RPObject) { - // do nothing - } - - createSlot(name: string) { - var slot = marauroa.rpslotFactory.create(name); - slot._parent = this; - return slot; - } - - init() { - // do nothing - } - - /** - * Retrieves object width. - */ - public getWidth(): number { - return this["width"] || 1; - } - - /** - * Retrieves object height. - */ - public getHeight(): number { - return this["height"] || 1; - } - - /** - * Checks if this object is nested. - */ - public isContained(): boolean { - return this._parent !== undefined; - } - - /** - * Retrieves the parent object. - */ - public getContainer(): RPObject { - return this._parent; - } -} diff --git a/src/js/stendhal/entity/Sign.ts b/src/js/stendhal/entity/Sign.ts index f2df46cc6a..f9429c12ba 100644 --- a/src/js/stendhal/entity/Sign.ts +++ b/src/js/stendhal/entity/Sign.ts @@ -9,39 +9,51 @@ * * ***************************************************************************/ +import { RPObject, RPZone} from "marauroa"; import { Entity } from "./Entity"; import { ActivityIndicatorSprite } from "../sprite/ActivityIndicatorSprite"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; +import { singletons } from "../SingletonRepo"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { images } from "sprite/image/ImageManager"; +import { ImageSprite } from "sprite/image/ImageSprite"; export class Sign extends Entity { override zIndex = 5000; private indicator?: ActivityIndicatorSprite; - - constructor() { - super(); - this["class"] = "default"; + override init() { + super.init(); + if (!this.imageSprite) { + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/signs/" + "default" + ".png"), + 0, 0, 32, 32 + ); + } } override set(key: string, value: object) { super.set(key, value); - if (key === "activity-indicator") { + if (key === "activity-indicator" && stendhal.config.getBoolean("activity-indicator")) { this.indicator = new ActivityIndicatorSprite(); + } else if (key === "class") { + this.imageSprite = new ImageSprite( + images.load(Paths.sprites + "/signs/" + this["class"] + ".png"), + 0, 0, 32, 32 + ); } } - override draw(ctx: CanvasRenderingContext2D) { - if (!this.imagePath) { - this.imagePath = stendhal.paths.sprites + "/signs/" + this["class"] + ".png"; - } - var image = stendhal.data.sprites.get(this.imagePath); - if (image.height) { - var localX = this["x"] * 32; - var localY = this["y"] * 32; + override draw(ctx: RenderingContext2D) { + let image = this.imageSprite?.imageRef?.image; + let localX = this["x"] * 32; + let localY = this["y"] * 32; + if (image) { ctx.drawImage(image, localX, localY); - if (this.indicator && stendhal.config.getBoolean("activity-indicator")) { + if (this.indicator) { this.indicator.draw(ctx, localX, localY, image.width); } } @@ -52,7 +64,12 @@ export class Sign extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; + } + + override destroy(parent: RPObject|RPZone): void { + this.indicator?.free(); + super.destroy(parent); } } diff --git a/src/js/stendhal/entity/TrainingDummy.ts b/src/js/stendhal/entity/TrainingDummy.ts index d2984622ce..776227e83d 100644 --- a/src/js/stendhal/entity/TrainingDummy.ts +++ b/src/js/stendhal/entity/TrainingDummy.ts @@ -9,10 +9,10 @@ * * ***************************************************************************/ +import { Paths } from "../data/Paths"; import { NPC } from "./NPC"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" export class TrainingDummy extends NPC { @@ -29,6 +29,6 @@ export class TrainingDummy extends NPC { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/attack.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/attack.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/UnknownEntity.ts b/src/js/stendhal/entity/UnknownEntity.ts index df40496262..1a9381ff08 100644 --- a/src/js/stendhal/entity/UnknownEntity.ts +++ b/src/js/stendhal/entity/UnknownEntity.ts @@ -11,7 +11,7 @@ import { Entity } from "./Entity"; -declare var marauroa: any; +import { marauroa } from "marauroa" export class UnknownEntity extends Entity { diff --git a/src/js/stendhal/entity/UseableEntity.ts b/src/js/stendhal/entity/UseableEntity.ts index 4ec2317aa8..96883af230 100644 --- a/src/js/stendhal/entity/UseableEntity.ts +++ b/src/js/stendhal/entity/UseableEntity.ts @@ -9,32 +9,26 @@ * * ***************************************************************************/ -declare var stendhal: any; - import { Entity } from "./Entity"; import { MenuItem } from "../action/MenuItem"; +import { Paths } from "../data/Paths"; +import { ImageSprite } from "sprite/image/ImageSprite"; +import { images } from "sprite/image/ImageManager"; export class UseableEntity extends Entity { override zIndex = 3000 action = "use"; - constructor() { - super(); - this.sprite = { - height: 32, - width: 32 - }; - } - override set(key: string, value: any) { super.set(key, value); if (key === "class" || key === "name") { - this.sprite.filename = stendhal.paths.sprites + "/" - + this["class"] + "/" + this["_name"] + ".png"; - } - if (key === "state") { - this.sprite.offsetY = this["state"] * 32; + let filename = Paths.sprites + "/" + this["class"] + "/" + this["_name"] + ".png"; + let state = this["state"] * 32 || 0; + this.imageSprite?.free(); + this.imageSprite = new ImageSprite(images.load(filename), 0, state, 32, 32); + } else if (key === "state" && this.imageSprite) { + this.imageSprite.offsetY = this["state"] * 32; } } diff --git a/src/js/stendhal/entity/User.ts b/src/js/stendhal/entity/User.ts index 5715cab6ff..c2c55844c7 100644 --- a/src/js/stendhal/entity/User.ts +++ b/src/js/stendhal/entity/User.ts @@ -9,8 +9,8 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; import { Entity } from "./Entity"; import { Player } from "./Player"; @@ -33,6 +33,7 @@ import { OutfitDialog } from "../ui/dialog/outfit/OutfitDialog"; import { FloatingWindow } from "../ui/toolkit/FloatingWindow"; import { Direction } from "../util/Direction"; +import { ViewPort } from "ui/ViewPort"; /** @@ -40,9 +41,6 @@ import { Direction } from "../util/Direction"; */ export class User extends Player { - private readonly soundMan = singletons.getSoundManager(); - private readonly lssMan = singletons.getLoopedSoundSourceManager(); - override minimapStyle = Color.USER; @@ -58,7 +56,7 @@ export class User extends Player { if ((key === "x" || key === "y") && this["x"] && this["y"] && (this["x"] !== oldX || this["y"] !== oldY)) { - this.lssMan.onDistanceChanged(this["x"], this["y"]); + singletons.getLoopedSoundSourceManager().onDistanceChanged(this["x"], this["y"]); } queueMicrotask( () => { @@ -139,27 +137,49 @@ export class User extends Player { && (Math.abs(this["y"] - entity["y"]) < hearingRange)); } + /** + * Checks if player is within range to hear a sound event. + * + * @param {number} radius + * Distance at which sound can be heard. + * @param {Entity} entity + * Entity emitting sound event. + * @returns {boolean} + * `true` if sound should be loaded (when event radius is more than -1 & user position is within + * radial distance from event origin). + */ + isInSoundRange(radius: number, entity: Entity): boolean { + if (entity === this) { + return true; + } + if (radius < 0) { + return false; + } + return Math.abs(this["x"] - entity["x"]) + Math.abs(this["y"] - entity["y"]) <= radius; + } + /** * Actions when player leaves a zone. */ override onExitZone() { super.onExitZone(); // speech bubbles & emojis from viewport - stendhal.ui.gamewindow.onExitZone(); + ViewPort.get().onExitZone(); // stop sounds & clear map sounds cache on zone change const msgs: string[] = []; - if (!this.lssMan.removeAll()) { + const lssm = singletons.getLoopedSoundSourceManager(); + if (!lssm.removeAll()) { let tmp = "LoopedSoundSourceManager reported not all sources stopped on zone change:"; - const loopSources = this.lssMan.getSources(); + const loopSources = lssm.getSources(); for (const id in loopSources) { const snd = loopSources[id].sound; tmp += "\n- ID: " + id + " (" + snd.src + ")"; } msgs.push(tmp); } - if (!this.soundMan.stopAll()) { + if (!stendhal.sound.stopAll()) { let tmp = "SoundManager reported not all sounds stopped on zone change:"; - for (const snd of this.soundMan.getActive()) { + for (const snd of stendhal.sound.getActive()) { tmp += "\n- " + snd.src; if (snd.loop) { tmp += " (loop)"; @@ -179,7 +199,7 @@ export class User extends Player { override onEnterZone() { super.onEnterZone(); // play looped sound sources - this.lssMan.onZoneReady(); + singletons.getLoopedSoundSourceManager().onZoneReady(); } /** @@ -206,7 +226,7 @@ export class User extends Player { * @param dir {util.Direction.Direction} * New direction to face. */ - public setFacing(dir: Direction) { + public faceTo(dir: Direction) { marauroa.clientFramework.sendAction({type: "face", dir: ""+dir.val}); } @@ -225,7 +245,7 @@ export class User extends Player { return; } // in case viewport frozen from view change event - stendhal.ui.viewport.freeze = false; + ViewPort.get().freeze = false; if (cancelAutoWalk && this.autoWalkEnabled() && this.getWalkDirection() == dir) { // cancel auto-walk if enabled & new direction is same as current direction of movement marauroa.clientFramework.sendAction({type: "walk"}); @@ -261,7 +281,7 @@ export class User extends Player { public moveTo(x: number, y: number, zone?: string): void; public moveTo(p1: object|number, p2?: number, p3?:string) { // in case viewport frozen from view change event - stendhal.ui.viewport.freeze = false; + ViewPort.get().freeze = false; let action: any = {}; if (typeof(p1) === "object") { action = p1; diff --git a/src/js/stendhal/entity/VisibleEntity.ts b/src/js/stendhal/entity/VisibleEntity.ts index 12cea77ae8..dff542145a 100644 --- a/src/js/stendhal/entity/VisibleEntity.ts +++ b/src/js/stendhal/entity/VisibleEntity.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,9 +9,10 @@ * * ***************************************************************************/ +import { images } from "sprite/image/ImageManager"; +import { Paths } from "../data/Paths"; import { Entity } from "./Entity"; - -declare var stendhal: any; +import { ImageSprite } from "sprite/image/ImageSprite"; export class VisibleEntity extends Entity { @@ -19,21 +20,29 @@ export class VisibleEntity extends Entity { constructor() { super(); - this.sprite = { - height: 32, - width: 32 - }; + } + + override init(): void { + let filename = Paths.sprites + "/" + + (this["class"] || "") + "/" + + (this["subclass"] || "") + "/" + + (this["_name"] || "") + ".png"; + let state = this["state"] * 32 || 0; + this.imageSprite = new ImageSprite(images.load(filename), 0, state, 32, 32); } override set(key: string, value: any) { super.set(key, value); if (key === "class" || key === "subclass" || key === "_name") { - this.sprite.filename = stendhal.paths.sprites + "/" - + (this["class"] || "") + "/" - + (this["subclass"] || "") + "/" - + (this["_name"] || "") + ".png"; + // if this was already initialized, create a new ImageSprite + if (this.imageSprite) { + this.imageSprite.free(); + this.init(); + } } else if (key === "state") { - this.sprite.offsetY = value * 32; + if (this.imageSprite) { + this.imageSprite.offsetY = value * 32; + } } } @@ -42,7 +51,7 @@ export class VisibleEntity extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/look.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/look.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/WalkBlocker.ts b/src/js/stendhal/entity/WalkBlocker.ts index 7d8ccd650d..72654ecc64 100644 --- a/src/js/stendhal/entity/WalkBlocker.ts +++ b/src/js/stendhal/entity/WalkBlocker.ts @@ -9,10 +9,9 @@ * * ***************************************************************************/ +import { Paths } from "../data/Paths"; import { Entity } from "./Entity"; -declare var stendhal: any; - export class WalkBlocker extends Entity { override zIndex = 3000; @@ -22,7 +21,7 @@ export class WalkBlocker extends Entity { } override getCursor(_x: number, _y: number) { - return "url(" + stendhal.paths.sprites + "/cursor/stop.png) 1 3, auto"; + return "url(" + Paths.sprites + "/cursor/stop.png) 1 3, auto"; } } diff --git a/src/js/stendhal/entity/Zone.ts b/src/js/stendhal/entity/Zone.ts index 6afb996d9b..0bcdbcefa3 100644 --- a/src/js/stendhal/entity/Zone.ts +++ b/src/js/stendhal/entity/Zone.ts @@ -9,11 +9,12 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; import { Entity } from "./Entity"; import { Player } from "./Player"; +import { Ground } from "./Ground"; /** @@ -22,11 +23,12 @@ import { Player } from "./Player"; export class Zone { /** Entities found in this zone. */ - private entities: Entity[] = []; + public entities: Entity[] = []; /** Singleton instance. */ private static instance: Zone; + public ground = new Ground(); /** * Retrieves singleton instance. @@ -45,7 +47,7 @@ export class Zone { // do nothing } - getEntitiesAt(x: number, y: number, filter: boolean): Entity[] { + getEntitiesAt(x: number, y: number, filter?: boolean): Entity[] { const xGrid = x / 32; const yGrid = y / 32; const entities = [] as Entity[]; @@ -63,14 +65,14 @@ export class Zone { return entities; } - entityAt(x: number, y: number, filter: boolean): Entity { - let res = stendhal.zone.ground; + entityAt(x: number, y: number, filter?: boolean): Entity { + let res = Zone.get().ground as any; for (const obj of this.getEntitiesAt(x, y, filter)) { res = obj; } // If we found an entity, return it - if (res != stendhal.zone.ground) { + if (res != Zone.get().ground) { return res; } diff --git a/src/js/stendhal/event/AttackEvent.ts b/src/js/stendhal/event/AttackEvent.ts new file mode 100644 index 0000000000..4d01c198e3 --- /dev/null +++ b/src/js/stendhal/event/AttackEvent.ts @@ -0,0 +1,38 @@ +/*************************************************************************** + * (C) Copyright 2005-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +export class AttackEvent extends RPEvent { + + public damage!: string; + public type!: string; + public weapon!: string; + + public execute(entity: any): void { + let target = entity.getAttackTarget(); + if (!target) { + return; + } + if (this.hasOwnProperty("hit")) { + var damage = parseInt(this["damage"], 10); + if (damage !== 0) { + target.onDamaged(entity, damage); + } else { + target.onBlocked(entity); + } + } else { + target.onMissed(entity); + } + entity.onAttackPerformed(parseInt(this["type"], 10), this.hasOwnProperty("ranged"), this["weapon"]); + } + +}; diff --git a/src/js/stendhal/event/BestiaryEvent.ts b/src/js/stendhal/event/BestiaryEvent.ts new file mode 100644 index 0000000000..df1eea3ca4 --- /dev/null +++ b/src/js/stendhal/event/BestiaryEvent.ts @@ -0,0 +1,121 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +import { ui } from "../ui/UI"; + +import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent"; + +import { stendhal } from "../stendhal"; + + +export class BestiaryEvent extends RPEvent { + + enemies!: string; + + + execute(_entity: any) { + if (!this.hasOwnProperty("enemies")) { + console.error("Event does not have \"enemies\" attribute"); + return; + } + + // --- title & headers --- // + + const header = ["Bestiary:", "\"???\" = unknown"]; + const hasRare = this["enemies"].includes("(rare)"); + const hasAbnormal = this["enemies"].includes("(abnormal)"); + + // show explanation of "rare" & "abnormal" creatures in header + if (hasRare || hasAbnormal) { + let subheader = ""; + if (!hasRare) { + subheader += "\"abnormal\""; + } else { + subheader += "\"rare\""; + if (hasAbnormal) { + subheader += " and \"abnormal\""; + } + } + header[1] = subheader + " creatures not required for achievements"; + } + + // --- contents --- // + + const content = new class extends DialogContentComponent {} ("empty-div-template"); + content.setConfigId("bestiary"); + content.componentElement.classList.add("bestiary"); + + const layout = document.createElement("div"); + layout.className = "horizontalgroup stretchgroup"; + const col1 = document.createElement("div"); + const col2 = document.createElement("div"); + const col3 = document.createElement("div"); + for (const col of [col1, col2, col3]) { + col.classList.add("verticalgroup", "stretchgroup"); + } + + const t1 = document.createElement("div"); + const t2 = document.createElement("div"); + const t3 = document.createElement("div"); + for (const t of [t1, t2, t3]) { + t.classList.add("shopcol", "bestiary-cell"); + } + t2.classList.add("center-text"); + t3.classList.add("center-text"); + t1.textContent = "Name"; + t2.textContent = "Solo"; + t3.textContent = "Shared"; + + col1.appendChild(t1); + col2.appendChild(t2); + col3.appendChild(t3); + + for (const e of this["enemies"].split(";")) { + const info = e.split(","); + let solo = ""; + let shared = ""; + if (info[1] == "true") { + solo = "✔"; + } + if (info[2] == "true") { + shared = "✔"; + } + + const l1 = document.createElement("div"); + const l2 = document.createElement("div"); + const l3 = document.createElement("div"); + for (const li of [l1, l2, l3]) { + li.classList.add("shopcol", "bestiary-cell"); + } + l2.classList.add("center-text"); + l3.classList.add("center-text"); + + l1.textContent = info[0]; + l2.textContent = solo; + l3.textContent = shared; + + col1.appendChild(l1); + col2.appendChild(l2); + col3.appendChild(l3); + } + + layout.appendChild(col1); + layout.appendChild(col2); + layout.appendChild(col3); + content.componentElement.appendChild(layout); + + stendhal.ui.globalInternalWindow.set(ui.createSingletonFloatingWindow(header.join(" "), + content, 20, 20)); + } +} diff --git a/src/js/stendhal/event/ChatOptionsEvent.ts b/src/js/stendhal/event/ChatOptionsEvent.ts index f16cc023a4..919e02a38e 100644 --- a/src/js/stendhal/event/ChatOptionsEvent.ts +++ b/src/js/stendhal/event/ChatOptionsEvent.ts @@ -11,9 +11,9 @@ import { Chat } from "../util/Chat"; import { ChatOptionsDialog } from "../ui/dialog/ChatOptionsDialog"; -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" -declare var marauroa: any +import { marauroa } from "marauroa" /** * shows an image for a detail view diff --git a/src/js/stendhal/event/EventRegistry.ts b/src/js/stendhal/event/EventRegistry.ts new file mode 100644 index 0000000000..76da3507c5 --- /dev/null +++ b/src/js/stendhal/event/EventRegistry.ts @@ -0,0 +1,93 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { marauroa } from "marauroa" + +import { AttackEvent } from "event/AttackEvent"; +import { BestiaryEvent } from "./BestiaryEvent"; +import { ChatOptionsEvent } from "./ChatOptionsEvent"; +import { ExamineEvent } from "./ExamineEvent"; +import { GlobalVisualEffectEvent } from "event/GlobalVisualEffectEvent"; +import { GroupChangeEvent } from "./GroupChangeEvent"; +import { GroupInviteEvent } from "./GroupInviteEvent"; +import { ImageEvent } from "event/ImageEvent"; +import { PlayerLoggedOnEvent } from "./PlayerLoggedOnEvent"; +import { PlayerLoggedOutEvent } from "./PlayerLoggedOutEvent"; +import { PrivateTextEvent } from "event/PrivateTextEvent"; +import { ProgressStatusEvent } from "./ProgressStatusEvent"; +import { ReachedAchievementEvent } from "event/ReachedAchievementEvent"; +import { ShowItemListEvent } from "./ShowItemListEvent"; +import { ShowOutfitListEvent } from "./ShowOutfitListEvent"; +import { SoundEvent } from "./SoundEvent"; +import { TextEvent } from "event/TextEvent"; +import { TradeEvent } from "./TradeEvent"; +import { TransitionGraphEvent } from "event/TransitionGraphEvent"; +import { ViewChangeEvent } from "./ViewChangeEvent"; + + +export class EventRegistry { + + private initialized = false; + + /** Singleton instance. */ + private static instance: EventRegistry; + + + /** + * Retrieves singleton instance. + */ + static get(): EventRegistry { + if (!EventRegistry.instance) { + EventRegistry.instance = new EventRegistry(); + } + return EventRegistry.instance; + } + + /** + * Hidden singleton constructor. + */ + private constructor() { + // do nothing + } + + /** + * Registers standard events. + */ + init() { + if (this.initialized) { + console.warn("tried to re-initialize EventRegistry"); + return; + } + this.initialized = true; + + marauroa.rpeventFactory.register("attack", AttackEvent); + marauroa.rpeventFactory.register("bestiary", BestiaryEvent); + marauroa.rpeventFactory.register("chat_options", ChatOptionsEvent); + marauroa.rpeventFactory.register("examine", ExamineEvent); + marauroa.rpeventFactory.register("global_visual_effect", GlobalVisualEffectEvent); + marauroa.rpeventFactory.register("group_change_event", GroupChangeEvent); + marauroa.rpeventFactory.register("group_invite_event", GroupInviteEvent); + marauroa.rpeventFactory.register("image_event", ImageEvent); + marauroa.rpeventFactory.register("player_logged_on", PlayerLoggedOnEvent); + marauroa.rpeventFactory.register("player_logged_out", PlayerLoggedOutEvent); + marauroa.rpeventFactory.register("private_text", PrivateTextEvent); + marauroa.rpeventFactory.register("progress_status_event", ProgressStatusEvent); + marauroa.rpeventFactory.register("reached_achievement", ReachedAchievementEvent); + marauroa.rpeventFactory.register("show_item_list", ShowItemListEvent); + marauroa.rpeventFactory.register("show_outfit_list", ShowOutfitListEvent); + marauroa.rpeventFactory.register("sound_event", SoundEvent); + marauroa.rpeventFactory.register("text", TextEvent); + marauroa.rpeventFactory.register("trade_state_change_event", TradeEvent); + marauroa.rpeventFactory.register("transition_graph", TransitionGraphEvent); + marauroa.rpeventFactory.register("view_change", ViewChangeEvent); + } + +} diff --git a/src/js/stendhal/event/ExamineEvent.ts b/src/js/stendhal/event/ExamineEvent.ts index 3a79d3f649..108fbc7911 100644 --- a/src/js/stendhal/event/ExamineEvent.ts +++ b/src/js/stendhal/event/ExamineEvent.ts @@ -11,9 +11,9 @@ import { ImageViewerDialog } from "../ui/dialog/ImageViewerDialog"; import { ui } from "../ui/UI"; -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" -declare var marauroa: any +import { marauroa } from "marauroa" /** * shows an image for a detail view diff --git a/src/js/stendhal/event/GlobalVisualEffectEvent.ts b/src/js/stendhal/event/GlobalVisualEffectEvent.ts new file mode 100644 index 0000000000..989ceb2cb9 --- /dev/null +++ b/src/js/stendhal/event/GlobalVisualEffectEvent.ts @@ -0,0 +1,20 @@ +/*************************************************************************** + * (C) Copyright 2005-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +export class GlobalVisualEffectEvent extends RPEvent { + + public execute(_entity: any): void { + // not implemented + } + +}; diff --git a/src/js/stendhal/event/GroupChangeEvent.ts b/src/js/stendhal/event/GroupChangeEvent.ts index 713a26f7b8..372edb65fe 100644 --- a/src/js/stendhal/event/GroupChangeEvent.ts +++ b/src/js/stendhal/event/GroupChangeEvent.ts @@ -9,13 +9,13 @@ * * ***************************************************************************/ -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" import { ui } from "../ui/UI"; import { UIComponentEnum } from "../ui/UIComponentEnum"; import { GroupPanelComponent } from "../ui/component/GroupPanelComponent"; -declare var stendhal: any; -declare var marauroa: any; +import { stendhal } from "../stendhal"; +import { marauroa } from "marauroa" /** * handles player trade events diff --git a/src/js/stendhal/event/GroupInviteEvent.ts b/src/js/stendhal/event/GroupInviteEvent.ts index dda3f7af48..455f457a41 100644 --- a/src/js/stendhal/event/GroupInviteEvent.ts +++ b/src/js/stendhal/event/GroupInviteEvent.ts @@ -10,12 +10,12 @@ ***************************************************************************/ -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" import { ui } from "../ui/UI"; import { UIComponentEnum } from "../ui/UIComponentEnum"; import { GroupPanelComponent } from "../ui/component/GroupPanelComponent"; -declare var marauroa: any; +import { marauroa } from "marauroa" /** * handles player trade events diff --git a/src/js/stendhal/event/ImageEvent.ts b/src/js/stendhal/event/ImageEvent.ts new file mode 100644 index 0000000000..8c744e5305 --- /dev/null +++ b/src/js/stendhal/event/ImageEvent.ts @@ -0,0 +1,20 @@ +/*************************************************************************** + * (C) Copyright 2005-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +export class ImageEvent extends RPEvent { + + public execute(_entity: any): void { + // not implemented + } + +}; diff --git a/src/js/stendhal/event/PlayerLoggedOnEvent.ts b/src/js/stendhal/event/PlayerLoggedOnEvent.ts index d24042385d..ca409baa91 100644 --- a/src/js/stendhal/event/PlayerLoggedOnEvent.ts +++ b/src/js/stendhal/event/PlayerLoggedOnEvent.ts @@ -10,9 +10,9 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" /** diff --git a/src/js/stendhal/event/PlayerLoggedOutEvent.ts b/src/js/stendhal/event/PlayerLoggedOutEvent.ts index f976a8cd64..df741f4973 100644 --- a/src/js/stendhal/event/PlayerLoggedOutEvent.ts +++ b/src/js/stendhal/event/PlayerLoggedOutEvent.ts @@ -10,9 +10,9 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" export class PlayerLoggedOutEvent extends RPEvent { diff --git a/src/js/stendhal/event/PrivateTextEvent.ts b/src/js/stendhal/event/PrivateTextEvent.ts new file mode 100644 index 0000000000..5066b5d65d --- /dev/null +++ b/src/js/stendhal/event/PrivateTextEvent.ts @@ -0,0 +1,53 @@ +/*************************************************************************** + * (C) Copyright 2005-2023 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Chat } from "util/Chat"; +import { RPEvent } from "marauroa" +import { stendhal } from "stendhal"; + +/** + * handles private text messages + */ +export class PrivateTextEvent extends RPEvent { + + public texttype!: string; + public text!: string; + public profile!: string; + + private soundTextEvents = ["privmsg", "support", "tutorial"]; + + public execute(_entity: any): void { + const ttype = this["texttype"].toLowerCase(); + const msg = this["text"].replace(/\\r\\n/g, "\n").replace(/\\r/g, "\n"); + + let profile; + if (this.hasOwnProperty("profile")) { + profile = this["profile"]; + } else if (ttype === "tutorial") { + profile = "floattingladynpc"; + } + + if (ttype === "server" && msg.includes("\n")) { + Chat.log(ttype, msg.split("\n"), undefined, profile); + } else { + // scene settings messages should not disturb playing, just create some atmosphere + const headed = ttype !== "scene_setting"; + Chat.log(ttype, msg, undefined, profile, headed); + } + + // play notification sound + const notif = stendhal.config.get("chat.private.sound"); + if (notif && this.soundTextEvents.indexOf(ttype) > -1) { + stendhal.sound.playGlobalizedEffect(notif); + } + } + +}; diff --git a/src/js/stendhal/event/ProgressStatusEvent.ts b/src/js/stendhal/event/ProgressStatusEvent.ts index 16b36e78f8..8e09564274 100644 --- a/src/js/stendhal/event/ProgressStatusEvent.ts +++ b/src/js/stendhal/event/ProgressStatusEvent.ts @@ -13,9 +13,8 @@ import { FloatingWindow } from "../ui/toolkit/FloatingWindow"; import { TravelLogDialog } from "../ui/dialog/TravelLogDialog"; import { ui } from "../ui/UI"; import { UIComponentEnum } from "../ui/UIComponentEnum"; -import { RPEvent } from "./RPEvent"; - -declare let stendhal: any; +import { RPEvent } from "marauroa" +import { stendhal } from "stendhal"; /** diff --git a/src/js/stendhal/event/ReachedAchievementEvent.ts b/src/js/stendhal/event/ReachedAchievementEvent.ts new file mode 100644 index 0000000000..c47a431275 --- /dev/null +++ b/src/js/stendhal/event/ReachedAchievementEvent.ts @@ -0,0 +1,27 @@ +/*************************************************************************** + * (C) Copyright 2005-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" +import { ViewPort } from "ui/ViewPort"; + +/** + * displays an achievement banner + */ +export class ReachedAchievementEvent extends RPEvent { + + public category!: string; + public title!: string; + public description!: string; + + public execute(_entity: any): void { + ViewPort.get().addAchievementNotif(this["category"], this["title"], this["description"]); + } +}; diff --git a/src/js/stendhal/event/ShowItemListEvent.ts b/src/js/stendhal/event/ShowItemListEvent.ts new file mode 100644 index 0000000000..345a5f7620 --- /dev/null +++ b/src/js/stendhal/event/ShowItemListEvent.ts @@ -0,0 +1,124 @@ +/*************************************************************************** + * Copyright © 2024 - 2026 Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa"; + +import { Paths } from "../data/Paths"; +import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent"; +import { ui } from "../ui/UI"; +import { Chat } from "../util/Chat"; + +import { htmlImageStore } from "data/HTMLImageStore"; +import { stendhal } from "../stendhal"; + + +export class ShowItemListEvent extends RPEvent { + + title!: string; + caption!: string; + content!: any; + + execute(_entity: any) { + let title = "Items"; + let caption = ""; + let items = []; + + if (this.hasOwnProperty("title")) { + title = this["title"]; + } + if (this.hasOwnProperty("caption")) { + caption = this["caption"]; + } + if (this.hasOwnProperty("content")) { + for (let obj in this["content"]) { + if (this["content"].hasOwnProperty(obj)) { + let data = this["content"][obj]["a"]; + let item = { + clazz: data["class"], + subclass: data["subclass"], + img: data["class"] + "/" + data["subclass"] + ".png", + price: data["price"], + desc: data["description_info"] + } + + // seller shops prefix prices with "-" + if (item.price.startsWith("-")) { + item.price = item.price.substr(1); + } + items.push(item); + } + } + } + + const content = new class extends DialogContentComponent { }("empty-div-template"); + content.componentElement.classList.add("shopsign"); + // Create table + const table = document.createElement("table"); + table.className = "shoptable"; + content.componentElement.appendChild(table); + + // Caption + const tableCaption = document.createElement("caption"); + tableCaption.className = "shopcaption"; + tableCaption.textContent = caption; + table.appendChild(tableCaption); + + // Header + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + + const headers = ["Item", "Price", "Description"]; + for (const text of headers) { + const th = document.createElement("th"); + th.textContent = text; + headerRow.appendChild(th); + } + + thead.appendChild(headerRow); + table.appendChild(thead); + + // Body + const tbody = document.createElement("tbody"); + + for (const i of items) { + const row = document.createElement("tr"); + + // Item (image) + const itemCell = document.createElement("td"); + itemCell.appendChild( + htmlImageStore.get( + Paths.sprites + "/items/" + i.img + ) + ); + row.appendChild(itemCell); + + // Price + const priceCell = document.createElement("td"); + priceCell.className = "number"; + priceCell.textContent = i.price; + row.appendChild(priceCell); + + // Description + const descCell = document.createElement("td"); + descCell.innerHTML = Chat.formatLogEntry(i.desc); + row.appendChild(descCell); + + tbody.appendChild(row); + } + + table.appendChild(tbody); + + stendhal.ui.globalInternalWindow.set( + ui.createSingletonFloatingWindow(title, content, 20, 20)); + } + +} diff --git a/src/js/stendhal/event/ShowOutfitListEvent.ts b/src/js/stendhal/event/ShowOutfitListEvent.ts new file mode 100644 index 0000000000..a0c8dc5a4c --- /dev/null +++ b/src/js/stendhal/event/ShowOutfitListEvent.ts @@ -0,0 +1,146 @@ +/*************************************************************************** + * Copyright © 2024 - 2025 Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +import { ui } from "../ui/UI"; +import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent"; + +import { stendhal } from "../stendhal"; + + +export class ShowOutfitListEvent extends RPEvent { + + title!: string; + caption!: string; + outfits!: string; + + execute(_entity: any) { + let title = "Outfits"; + let caption = ""; + let outfits = []; + + if (this.hasOwnProperty("title")) { + title = this["title"]; + } + if (this.hasOwnProperty("caption")) { + caption = this["caption"]; + } + if (this.hasOwnProperty("outfits")) { + for (let o of this["outfits"].split(":")) { + let parts = o.split(";"); + if (parts.length > 2) { + outfits.push({ + name: parts[0], + outfit: parts[1], + price: parts[2] + }); + } + } + } + if (this.hasOwnProperty("show_base")) { + //Chat.log("normal", this["show_base"]); + } + + const content = new class extends DialogContentComponent { }("empty-div-template"); + content.componentElement.classList.add("shopsign"); + + const table = document.createElement("table"); + table.className = "shoptable"; + content.componentElement.appendChild(table); + + // Caption + const tableCaption = document.createElement("caption"); + tableCaption.className = "shopcaption"; + tableCaption.textContent = caption; + table.appendChild(tableCaption); + + // Header + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + + const headers = ["Name", "Outfit", "Price"]; + for (const text of headers) { + const th = document.createElement("th"); + th.textContent = text; + headerRow.appendChild(th); + } + + thead.appendChild(headerRow); + table.appendChild(thead); + + // Body + const tbody = document.createElement("tbody"); + + for (const outfit of outfits) { + const row = document.createElement("tr"); + + // Name + const nameCell = document.createElement("td"); + nameCell.textContent = outfit.name; + row.appendChild(nameCell); + + // Image + const imageCell = document.createElement("td"); + let url = ShowOutfitListEvent.outfitToUrl(outfit.outfit); + const img1 = document.createElement("img"); + const img2 = document.createElement("img"); + img1.src = url; + img2.src = url + "&offset=0"; + imageCell.append(img1); + imageCell.append(img2); + row.appendChild(imageCell); + + // Price + const priceCell = document.createElement("td"); + priceCell.className = "number"; + priceCell.textContent = outfit.price; + row.appendChild(priceCell); + + tbody.appendChild(row); + } + + table.appendChild(tbody); + + stendhal.ui.globalInternalWindow.set( + ui.createSingletonFloatingWindow(title, content, 20, 20)); + } + + private static outfitToUrl(outfit?: string): string { + if (!outfit) { + return ""; + } + + // Required order + const order = ["body", "dress", "head", "mouth", "eyes", "mask", "hair", "hat", "detail"]; + + // Parse input into a map + let values: Record = {}; + + for (let part of outfit.split(",")) { + let [key, value] = part.split("="); + values[key] = Number(value); + } + + // Build path + let parts = order.map(key => { + let value = values[key] ?? 0; + return `${key}-${value}-0`; + }); + + let url = + "https://stendhalgame.org/createoutfit.php?rewritten=true&dialog=shop&outfit=" + + parts.join("_"); + + return url; + } +} diff --git a/src/js/stendhal/event/SoundEvent.ts b/src/js/stendhal/event/SoundEvent.ts new file mode 100644 index 0000000000..55382bad48 --- /dev/null +++ b/src/js/stendhal/event/SoundEvent.ts @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RPEvent } from "marauroa" + +import { SoundID } from "../data/sound/SoundID"; +import { SoundManager } from "../data/sound/SoundManager"; + +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; + + +export class SoundEvent extends RPEvent { + + sound?: string; + sound_id?: string; + volume!: number; + radius?: number; + layer!: string; + + + execute(entity: any) { + if (!marauroa.me) { + return; + } + let radius = SoundManager.DEFAULT_RADIUS; + if (typeof(this["radius"]) === "number") { + radius = this["radius"]; + } + if (!marauroa.me.isInSoundRange(radius, entity)) { + // too far away to hear so don't load + return; + } + + let volume = 1; + // Adjust by the server specified volume, if any + if (this.hasOwnProperty("volume")) { + // NOTE: server uses int in range 1-100 while HTMLAudioElement uses float in range 0-1 + volume *= this["volume"] / 100; + } + + let sound = this["sound"]; + // get sound from ID + if (this["sound_id"]) { + sound = SoundID[this["sound_id"]]; + } + + stendhal.sound.playLocalizedEffect(entity["_x"], entity["_y"], radius, this["layer"], sound, + volume); + } +} diff --git a/src/js/stendhal/event/TextEvent.ts b/src/js/stendhal/event/TextEvent.ts new file mode 100644 index 0000000000..5436fe1cc2 --- /dev/null +++ b/src/js/stendhal/event/TextEvent.ts @@ -0,0 +1,31 @@ +/*************************************************************************** + * (C) Copyright 2005-2023 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Entity } from "entity/Entity"; +import { RPEvent } from "marauroa" + +/** + * text events + */ +export class TextEvent extends RPEvent { + + public range!: string; + public text!: string; + + public execute(entity: Entity): void { + if (this["range"]) { + entity.say(this["text"], parseInt(this["range"], 10)); + } else { + entity.say(this["text"]); + } + } + +}; diff --git a/src/js/stendhal/event/TradeEvent.ts b/src/js/stendhal/event/TradeEvent.ts index 76eac52690..09bf9bac94 100644 --- a/src/js/stendhal/event/TradeEvent.ts +++ b/src/js/stendhal/event/TradeEvent.ts @@ -15,10 +15,11 @@ import { TradeDialog } from "../ui/dialog/TradeDialog"; import { UIComponentEnum } from "../ui/UIComponentEnum"; import { ui } from "../ui/UI"; -import { RPEvent } from "./RPEvent"; +import { RPEvent } from "marauroa" -declare var stendhal: any; -declare var marauroa: any; +import { stendhal } from "../stendhal"; +import { marauroa } from "marauroa" +import { Player } from "../entity/Player"; /** * handles player trade events @@ -39,7 +40,7 @@ export class TradeEvent extends RPEvent { if (dialog) { let partner = marauroa.currentZone[this["partner_id"]]; - dialog.updateTradeState(marauroa.me, partner, this["user_trade_state"], this["partner_trade_state"]); + dialog.updateTradeState(marauroa.me as Player, partner, this["user_trade_state"], this["partner_trade_state"]); } } diff --git a/src/js/stendhal/event/RPEvent.ts b/src/js/stendhal/event/TransitionGraphEvent.ts similarity index 83% rename from src/js/stendhal/event/RPEvent.ts rename to src/js/stendhal/event/TransitionGraphEvent.ts index 209ee6e5bb..fc4807c532 100644 --- a/src/js/stendhal/event/RPEvent.ts +++ b/src/js/stendhal/event/TransitionGraphEvent.ts @@ -9,11 +9,13 @@ * * ***************************************************************************/ -/** - * abstract base class for Events from the server - */ -export abstract class RPEvent { +import { RPEvent } from "marauroa" - abstract execute(entity: any): void; + +export class TransitionGraphEvent extends RPEvent { + + public execute(_entity: any): void { + // not implemented + } }; diff --git a/src/js/stendhal/event/ViewChangeEvent.ts b/src/js/stendhal/event/ViewChangeEvent.ts index f86330fd4b..8d08276148 100644 --- a/src/js/stendhal/event/ViewChangeEvent.ts +++ b/src/js/stendhal/event/ViewChangeEvent.ts @@ -10,9 +10,11 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; -import { RPEvent } from "./RPEvent"; +import { Canvas } from "util/Types"; +import { RPEvent } from "marauroa" +import { ViewPort } from "ui/ViewPort"; /** @@ -24,13 +26,14 @@ export class ViewChangeEvent extends RPEvent { public y!: number; - execute(entity: any) { - const canvas = stendhal.ui.viewport.getElement() as HTMLCanvasElement; + execute(_entity: any) { + let viewport = ViewPort.get(); + const canvas = viewport.getElement(); queueMicrotask(() => { - stendhal.ui.viewport.freeze = true; - stendhal.ui.viewport.offsetX = this.x * stendhal.ui.viewport.targetTileWidth + viewport.freeze = true; + viewport.offsetX = this.x * viewport.targetTileWidth - Math.round(canvas.width / 2); - stendhal.ui.viewport.offsetY = this.y * stendhal.ui.viewport.targetTileHeight + viewport.offsetY = this.y * viewport.targetTileHeight - Math.round(canvas.height / 2); }); } diff --git a/src/js/stendhal/landscape/CombinedTileset.ts b/src/js/stendhal/landscape/CombinedTileset.ts index 2731927447..1689e586fd 100644 --- a/src/js/stendhal/landscape/CombinedTileset.ts +++ b/src/js/stendhal/landscape/CombinedTileset.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { Canvas, RenderingContext2D } from "util/Types"; /** * a combined tileset with group layers using one single tileset image. @@ -21,20 +21,19 @@ declare var stendhal: any; * We use one large tileset for the combined tiles in order to be compatible with WebGL. */ export class CombinedTileset { - public readonly canvas: HTMLCanvasElement; - public readonly ctx: CanvasRenderingContext2D; + public readonly canvas: Canvas; + public readonly ctx: RenderingContext2D; public readonly tilesPerRow: number; - constructor(numberOfTiles: number, public readonly combinedLayers: number[][]) { + constructor(numberOfTiles: number, public readonly combinedLayers: number[][], tileWidth: number, tileHeight: number) { // The original approach used an a very wide image of 32 pixel height. // But both Firefox and Chrome are limit the dimension of an image to 2^15 pixels. this.tilesPerRow = Math.ceil(Math.sqrt(numberOfTiles)); - this.canvas = document.createElement("canvas"); - this.canvas.width = stendhal.data.map.tileWidth * this.tilesPerRow; - this.canvas.height = stendhal.data.map.tileHeight * this.tilesPerRow; + this.canvas = new OffscreenCanvas( + tileWidth * this.tilesPerRow, + tileHeight * this.tilesPerRow); this.ctx = this.canvas.getContext("2d")!; } - } diff --git a/src/js/stendhal/landscape/CombinedTilesetFactory.ts b/src/js/stendhal/landscape/CombinedTilesetFactory.ts index 245266fdf0..9060aa6862 100644 --- a/src/js/stendhal/landscape/CombinedTilesetFactory.ts +++ b/src/js/stendhal/landscape/CombinedTilesetFactory.ts @@ -9,12 +9,13 @@ * * ***************************************************************************/ +import { TileMap } from "../data/TileMap"; import { CombinedTileset } from "./CombinedTileset"; import { CombinedTilesetImageLoader } from "./CombinedTilesetImageLoader"; export class CombinedTilesetFactory { - constructor(private map: any) {} + constructor(private map: TileMap) {} combine(): CombinedTileset { let combinedTilesToIndex: Map = new Map(); @@ -33,7 +34,15 @@ export class CombinedTilesetFactory { for (let group = 0; group < this.map.layerGroupIndexes.length; group++) { let layers = this.map.layerGroupIndexes[group]; + let blendLayer = this.map.layerNames.indexOf(this.map.blendLayers[group]); let combinedTile = [] + + if (blendLayer && this.map.layers[blendLayer]) { + combinedTile.push(this.map.layers[blendLayer][y * this.map.zoneSizeX + x]); + } else { + combinedTile.push(0); + } + for (let layer of layers) { let gid = this.map.layers[layer][y * this.map.zoneSizeX + x]; if (gid > 0) { @@ -43,7 +52,8 @@ export class CombinedTilesetFactory { // most of the tiles on the roof layer are empty, so we add a special case here // to skip drawing completely transparent tiles later. - if (combinedTile.length === 0) { + // In this case, we ignore the entry on the blend layer + if (combinedTile.length <= 1) { combinedLayers[group][y * this.map.zoneSizeX + x] = -1 continue; } @@ -61,7 +71,7 @@ export class CombinedTilesetFactory { } } } - let combinedTileset = new CombinedTileset(combinedTilesToIndex.size, combinedLayers); + let combinedTileset = new CombinedTileset(combinedTilesToIndex.size, combinedLayers, this.map.tileWidth, this.map.tileHeight); // console.log(this.map, combinedTilesToIndex, combinedLayers); // document.getElementsByTagName("body")[0].append(combinedTileset.canvas); diff --git a/src/js/stendhal/landscape/CombinedTilesetImageLoader.ts b/src/js/stendhal/landscape/CombinedTilesetImageLoader.ts index 8c792338bc..548ace3331 100644 --- a/src/js/stendhal/landscape/CombinedTilesetImageLoader.ts +++ b/src/js/stendhal/landscape/CombinedTilesetImageLoader.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2022-2023 - Faiumoni e. V. * + * (C) Copyright 2022-2025 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,25 +9,42 @@ * * ***************************************************************************/ -import { TileStore } from "../data/TileStore"; +import { Canvas, RenderingContext2D } from "util/Types"; +import { TileMap } from "../data/TileMap"; import { MapOfSets } from "../util/MapOfSets"; import { CombinedTileset } from "./CombinedTileset"; +import { Debug } from "../util/Debug"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { images } from "sprite/image/ImageManager"; +import { ImageRef } from "sprite/image/ImageRef"; +import { ViewPort } from "ui/ViewPort"; export class CombinedTilesetImageLoader { private tileUsedAtIndex!: MapOfSets - private tilesetImages: HTMLImageElement[] = []; - private animations: any = {}; - private landscapeAnimationMap: any; + private tilesetImages: ImageRef[] = []; + private drawCanvas: Canvas; + private drawCtx: RenderingContext2D; + private blendCanvas: Canvas; + private blendCtx: RenderingContext2D; + private blendFixCanvas: Canvas; + private blendFixCtx: RenderingContext2D; + private loadedTilesetCount = 0; + private usedTilesetCount = 0; constructor( - private map: any, + private map: TileMap, private indexToCombinedTiles: Map, private combinedTileset: CombinedTileset) { - this.landscapeAnimationMap = TileStore.get().getLandscapeMap(); - } + + this.drawCanvas = new OffscreenCanvas(this.map.tileWidth, this.map.tileHeight); + this.blendCanvas = new OffscreenCanvas(this.map.tileWidth, this.map.tileHeight); + this.blendFixCanvas = new OffscreenCanvas(this.map.tileWidth, this.map.tileHeight); + this.drawCtx = this.drawCanvas.getContext("2d")!; + this.blendCtx = this.blendCanvas.getContext("2d")!; + this.blendFixCtx = this.blendFixCanvas.getContext("2d")!; + } private calculateTileUsedAtIndex(): void { @@ -50,29 +67,34 @@ export class CombinedTilesetImageLoader { } - private loadTileset(tileset: number) { - let img = document.createElement("img"); - img.onload = () => { - this.drawTileset(tileset); - } - + private async loadTileset(tileset: number) { const tsname = this.map.tilesetFilenames[tileset]; - img.src = tsname + "?v=" + stendhal.data.build.version; - - if (this.landscapeAnimationMap) { - const animation = this.landscapeAnimationMap[tsname]; - if (animation) { - this.animations[tileset] = animation; - } + if (!tsname) { + return; } + let imageRef = images.load(tsname); + await imageRef.waitFor(); + this.tilesetImages[tileset] = imageRef; + this.loadedTilesetCount++; + this.drawTileset(tileset); + if (this.loadedTilesetCount >= this.usedTilesetCount) { + this.finish(); + } + } - this.tilesetImages[tileset] = img; + private finish() { + for (let imageRef of this.tilesetImages) { + imageRef.free(); + } } private drawTileset(tileset: number): void { let firstGid = this.map.firstgids[tileset]; - let image = this.tilesetImages[tileset]; + let image = this.tilesetImages[tileset].image; + if (!image) { + return; + } let numberOfTiles = image.width / this.map.tileWidth * image.height / this.map.tileHeight; let lastGid = firstGid + numberOfTiles; @@ -96,36 +118,85 @@ export class CombinedTilesetImageLoader { const pixelX = x * this.map.tileWidth; const pixelY = y * this.map.tileHeight; - this.combinedTileset.ctx.clearRect(pixelX, pixelY, this.map.tileWidth, this.map.tileHeight); - for (let tile of tiles) { - let flip = tile & 0xE0000000; - let gid = tile & 0x1FFFFFFF; - let tileset = this.map.getTilesetForGid(gid); - let image = this.tilesetImages[tileset]; - if (!image || !image.height) { - continue; - } - - let base = this.map.firstgids[tileset]; - let tileIndexInTileset = gid - base; - this.drawTile(pixelX, pixelY, image, tileIndexInTileset, flip); - + if (Debug.isActive("light") && ViewPort.get().HSLFilter) { + this.drawCombinedTileWithBlend(tiles, pixelX, pixelY); + } else { + this.drawCombinedTileWithoutBlend(this.combinedTileset.ctx, tiles, pixelX, pixelY); } } - private drawTile(pixelX: number, pixelY: number, tilesetImage: HTMLImageElement, tileIndexInTileset: number, flip: number) { + private drawCombinedTileWithoutBlend(ctx: RenderingContext2D, tiles: number[], pixelX: number, pixelY: number) { + ctx.clearRect(pixelX, pixelY, this.map.tileWidth, this.map.tileHeight); + for (let [i, tile] of tiles.entries()) { + if (i === 0) { + continue; // skip blend layer + } + this.drawTile(ctx, tile, pixelX, pixelY); + } + } + + private drawCombinedTileWithBlend(tiles: number[], pixelX: number, pixelY: number) { + + this.drawCombinedTileWithoutBlend(this.drawCtx, tiles, 0, 0); + + // copy the image to the blend canvas + this.blendCtx.globalCompositeOperation = "source-over"; + this.blendCtx.clearRect(0, 0, this.map.tileWidth, this.map.tileHeight); + this.blendCtx.drawImage(this.drawCanvas, 0, 0); + + this.blendCtx.globalCompositeOperation = "multiply"; + this.blendCtx.fillStyle = ViewPort.get().HSLFilter!; + this.blendCtx.fillRect(0, 0, this.map.tileWidth, this.map.tileHeight); + + let tile = tiles[0]; + this.blendCtx.globalCompositeOperation = "soft-light"; + if (tile > 0) { + // turn transparency into black for the blend layer + this.blendFixCtx.fillStyle = "black"; + this.blendFixCtx.fillRect(0, 0, this.map.tileWidth, this.map.tileHeight); + this.drawTile(this.blendFixCtx, tile, 0, 0); + + // draw the blend layer onto the blend canvas + this.blendCtx.drawImage(this.blendFixCanvas, 0, 0); + } else { + this.blendCtx.fillStyle = "black"; + this.blendCtx.fillRect(0, 0, this.map.tileWidth, this.map.tileHeight); + } + + // restore transparency which was lost during multiply and soft-light composition + this.blendCtx.globalCompositeOperation = "destination-in"; + this.blendCtx.drawImage(this.drawCanvas, 0, 0); + + // draw the blend canvas at the correct position onto the combined tileset + this.combinedTileset.ctx.clearRect(pixelX, pixelY, this.map.tileWidth, this.map.tileHeight); + this.combinedTileset.ctx.drawImage(this.blendCanvas, pixelX, pixelY); + } + + private drawTile(ctx: RenderingContext2D, tile: number, pixelX: number, pixelY: number) { + let flip = tile & 0xE0000000; + let gid = tile & 0x1FFFFFFF; + let tileset = this.map.getTilesetForGid(gid); + let image = this.tilesetImages[tileset]?.image; + if (!image) { + return; + } + + let base = this.map.firstgids[tileset]; + let tileIndexInTileset = gid - base; + this.drawImageTile(ctx, pixelX, pixelY, image, tileIndexInTileset, flip); + } + + private drawImageTile(ctx: RenderingContext2D, pixelX: number, pixelY: number, tilesetImage: ImageBitmap, tileIndexInTileset: number, flip: number) { const tilesetWidth = tilesetImage.width; const tilesPerRow = Math.floor(tilesetWidth / this.map.tileWidth); - const ctx = this.combinedTileset.ctx; - if (flip === 0) { ctx.drawImage(tilesetImage, - (tileIndexInTileset % tilesPerRow) * this.map.tileWidth, - Math.floor(tileIndexInTileset / tilesPerRow) * this.map.tileHeight, - this.map.tileWidth, this.map.tileHeight, - pixelX, pixelY, - this.map.tileWidth, this.map.tileHeight); + (tileIndexInTileset % tilesPerRow) * this.map.tileWidth, + Math.floor(tileIndexInTileset / tilesPerRow) * this.map.tileHeight, + this.map.tileWidth, this.map.tileHeight, + pixelX, pixelY, + this.map.tileWidth, this.map.tileHeight); } else { ctx.translate(pixelX, pixelY); @@ -154,7 +225,12 @@ export class CombinedTilesetImageLoader { restore.push([0, 1, 1, 0, 0, 0]); } - this.drawTile(0, 0, tilesetImage, tileIndexInTileset, 0); + ctx.drawImage(tilesetImage, + (tileIndexInTileset % tilesPerRow) * this.map.tileWidth, + Math.floor(tileIndexInTileset / tilesPerRow) * this.map.tileHeight, + this.map.tileWidth, this.map.tileHeight, + 0, 0, + this.map.tileWidth, this.map.tileHeight); restore.reverse(); for (const args of restore) { @@ -168,9 +244,9 @@ export class CombinedTilesetImageLoader { console.log("CombinedTilesetImageLoader.load()"); this.calculateTileUsedAtIndex(); let usedTilesets = this.calculateUsedTilesets(this.tileUsedAtIndex.keys()); + this.usedTilesetCount = usedTilesets.size; for (let tileset of usedTilesets) { this.loadTileset(tileset); } - } } diff --git a/src/js/stendhal/landscape/CombinedTilesetRenderingStrategy.ts b/src/js/stendhal/landscape/CombinedTilesetRenderingStrategy.ts new file mode 100644 index 0000000000..0ccf32b68b --- /dev/null +++ b/src/js/stendhal/landscape/CombinedTilesetRenderingStrategy.ts @@ -0,0 +1,90 @@ +/*************************************************************************** + * (C) Copyright 2022-2026 - Faiumoni e. V. * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { LandscapeRenderingStrategy } from "./LandscapeRenderingStrategy"; +import { Canvas, RenderingContext2D } from "util/Types"; +import { TileMap } from "../data/TileMap"; +import { CombinedTilesetFactory } from "./CombinedTilesetFactory"; +import { CombinedTileset } from "./CombinedTileset"; + +export class CombinedTilesetRenderingStrategy extends LandscapeRenderingStrategy { + private combinedTileset?: CombinedTileset; + private map!: TileMap; + + public onMapLoaded(map: TileMap): void { + this.map = map; + let combinedTilesetFactory = new CombinedTilesetFactory(map); + this.combinedTileset = combinedTilesetFactory.combine(); + } + + public onTilesetLoaded(): void { + let body = document.getElementById("body")!; + body.style.cursor = "auto"; + } + + public render( + canvas: Canvas, gamewindow: any, + tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void { + + this.drawLayer( + canvas, + this.combinedTileset, + 0, + tileOffsetX, tileOffsetY, targetTileWidth, targetTileHeight); + + gamewindow.drawEntities(); + + this.drawLayer( + canvas, + this.combinedTileset, + 1, + tileOffsetX, tileOffsetY, targetTileWidth, targetTileHeight); + } + + drawLayer( + canvas: Canvas, + combinedTileset: CombinedTileset|undefined, layerNo: number, + tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void { + if (!combinedTileset) { + return; + } + let ctx = canvas.getContext("2d")! as RenderingContext2D; + + const layer = combinedTileset.combinedLayers[layerNo]; + const yMax = Math.min(tileOffsetY + canvas.height / targetTileHeight + 1, this.map.zoneSizeY); + const xMax = Math.min(tileOffsetX + canvas.width / targetTileWidth + 1, this.map.zoneSizeX); + + for (let y = tileOffsetY; y < yMax; y++) { + for (let x = tileOffsetX; x < xMax; x++) { + let index = layer[y * this.map.zoneSizeX + x]; + if (index > -1) { + + try { + const pixelX = x * targetTileWidth; + const pixelY = y * targetTileHeight; + + ctx.drawImage(combinedTileset.canvas, + + (index % combinedTileset.tilesPerRow) * this.map.tileWidth, + Math.floor(index / combinedTileset.tilesPerRow) * this.map.tileHeight, + + this.map.tileWidth, this.map.tileHeight, + pixelX, pixelY, + targetTileWidth, targetTileHeight); + } catch (e) { + console.error(e); + } + } + } + } + } + +} diff --git a/src/js/stendhal/landscape/IndividualTilesetRenderingStrategy.ts b/src/js/stendhal/landscape/IndividualTilesetRenderingStrategy.ts index 60ed83c047..8de9384643 100644 --- a/src/js/stendhal/landscape/IndividualTilesetRenderingStrategy.ts +++ b/src/js/stendhal/landscape/IndividualTilesetRenderingStrategy.ts @@ -9,17 +9,21 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { LandscapeRenderingStrategy } from "./LandscapeRenderingStrategy"; -import { ImagePreloader } from "../data/ImagePreloader"; +import { ImageCache } from "../sprite/image/ImageCache"; import { Chat } from "../util/Chat"; +import { TileMap } from "../data/TileMap"; +import { Canvas, RenderingContext2D } from "util/Types"; export class IndividualTilesetRenderingStrategy extends LandscapeRenderingStrategy { private targetTileWidth = 32; private targetTileHeight = 32; + private imageCache = new ImageCache(); + private map!: TileMap; constructor() { super(); @@ -28,27 +32,29 @@ export class IndividualTilesetRenderingStrategy extends LandscapeRenderingStrate }, 1000); } - public onMapLoaded(_map: any): void { + public onMapLoaded(map: TileMap): void { // do nothing console.log("Using IndividualTilesetRenderingStrategy.") + this.map = map; + this.imageCache.close(); + this.imageCache = new ImageCache(); + this.imageCache.load(map.tilesetFilenames); } public onTilesetLoaded(): void { - new ImagePreloader(stendhal.data.map.tilesetFilenames, function() { - let body = document.getElementById("body")!; - body.style.cursor = "auto"; - }); + let body = document.getElementById("body")!; + body.style.cursor = "auto"; } public render( - canvas: HTMLCanvasElement, gamewindow: any, + canvas: Canvas, gamewindow: any, tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void { this.targetTileWidth = targetTileWidth; this.targetTileHeight = targetTileHeight; - for (var drawingLayer=0; drawingLayer < stendhal.data.map.layers.length; drawingLayer++) { - var name = stendhal.data.map.layerNames[drawingLayer]; + for (var drawingLayer=0; drawingLayer < this.map.layers.length; drawingLayer++) { + var name = this.map.layerNames[drawingLayer]; if (name !== "protection" && name !== "collision" && name !== "objects" && name !== "blend_ground" && name !== "blend_roof") { this.paintLayer(canvas, drawingLayer, tileOffsetX, tileOffsetY); @@ -59,27 +65,28 @@ export class IndividualTilesetRenderingStrategy extends LandscapeRenderingStrate } } - private paintLayer(canvas: HTMLCanvasElement, drawingLayer: number, + private paintLayer(canvas: Canvas, drawingLayer: number, tileOffsetX: number, tileOffsetY: number) { - const layer = stendhal.data.map.layers[drawingLayer]; - const yMax = Math.min(tileOffsetY + canvas.height / this.targetTileHeight + 1, stendhal.data.map.zoneSizeY); - const xMax = Math.min(tileOffsetX + canvas.width / this.targetTileWidth + 1, stendhal.data.map.zoneSizeX); - let ctx = canvas.getContext("2d")!; + const layer = this.map.layers[drawingLayer]; + const yMax = Math.min(tileOffsetY + canvas.height / this.targetTileHeight + 1, this.map.zoneSizeY); + const xMax = Math.min(tileOffsetX + canvas.width / this.targetTileWidth + 1, this.map.zoneSizeX); + let ctx = canvas.getContext("2d")! as RenderingContext2D; for (let y = tileOffsetY; y < yMax; y++) { for (let x = tileOffsetX; x < xMax; x++) { - let gid = layer[y * stendhal.data.map.zoneSizeX + x]; + let gid = layer[y * this.map.zoneSizeX + x]; const flip = gid & 0xE0000000; gid &= 0x1FFFFFFF; if (gid > 0) { - const tileset = stendhal.data.map.getTilesetForGid(gid); - const base = stendhal.data.map.firstgids[tileset]; + const tileset = this.map.getTilesetForGid(gid); + const base = this.map.firstgids[tileset]; const idx = gid - base; try { - if (stendhal.data.map.aImages[tileset].height > 0) { - this.drawTile(ctx, stendhal.data.map.aImages[tileset], idx, x, y, flip); + let image = this.imageCache.images[this.map.tilesetFilenames[tileset]]; + if (image) { + this.drawTile(ctx, image, idx, x, y, flip); } } catch (e) { console.error(e); @@ -89,17 +96,17 @@ export class IndividualTilesetRenderingStrategy extends LandscapeRenderingStrate } } - private drawTile(ctx: CanvasRenderingContext2D, tileset: HTMLImageElement, idx: number, x: number, y: number, flip = 0) { + private drawTile(ctx: RenderingContext2D, tileset: ImageBitmap, idx: number, x: number, y: number, flip = 0) { const tilesetWidth = tileset.width; - const tilesPerRow = Math.floor(tilesetWidth / stendhal.data.map.tileWidth); + const tilesPerRow = Math.floor(tilesetWidth / this.map.tileWidth); const pixelX = x * this.targetTileWidth; const pixelY = y * this.targetTileHeight; if (flip === 0) { ctx.drawImage(tileset, - (idx % tilesPerRow) * stendhal.data.map.tileWidth, - Math.floor(idx / tilesPerRow) * stendhal.data.map.tileHeight, - stendhal.data.map.tileWidth, stendhal.data.map.tileHeight, + (idx % tilesPerRow) * this.map.tileWidth, + Math.floor(idx / tilesPerRow) * this.map.tileHeight, + this.map.tileWidth, this.map.tileHeight, pixelX, pixelY, this.targetTileWidth, this.targetTileHeight); } else { diff --git a/src/js/stendhal/landscape/LandscapeRenderer.ts b/src/js/stendhal/landscape/LandscapeRenderer.ts deleted file mode 100644 index be00690247..0000000000 --- a/src/js/stendhal/landscape/LandscapeRenderer.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2022-2023 - Faiumoni e. V. * - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -import { CombinedTileset } from "./CombinedTileset"; - -declare var stendhal: any; - -export class LandscapeRenderer { - - drawLayer( - canvas: HTMLCanvasElement, - combinedTileset: CombinedTileset, layerNo: number, - tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void { - if (!combinedTileset) { - return; - } - let ctx = canvas.getContext("2d")!; - - const layer = combinedTileset.combinedLayers[layerNo]; - const yMax = Math.min(tileOffsetY + canvas.height / targetTileHeight + 1, stendhal.data.map.zoneSizeY); - const xMax = Math.min(tileOffsetX + canvas.width / targetTileWidth + 1, stendhal.data.map.zoneSizeX); - - for (let y = tileOffsetY; y < yMax; y++) { - for (let x = tileOffsetX; x < xMax; x++) { - let index = layer[y * stendhal.data.map.zoneSizeX + x]; - if (index > -1) { - - try { - const pixelX = x * targetTileWidth; - const pixelY = y * targetTileHeight; - - ctx.drawImage(combinedTileset.canvas, - - (index % combinedTileset.tilesPerRow) * stendhal.data.map.tileWidth, - Math.floor(index / combinedTileset.tilesPerRow) * stendhal.data.map.tileHeight, - - stendhal.data.map.tileWidth, stendhal.data.map.tileHeight, - pixelX, pixelY, - targetTileWidth, targetTileHeight); - } catch (e) { - console.error(e); - } - } - } - } - } - -} diff --git a/src/js/stendhal/landscape/LandscapeRenderingStrategy.ts b/src/js/stendhal/landscape/LandscapeRenderingStrategy.ts index 61db62bf4c..2d6502edbd 100644 --- a/src/js/stendhal/landscape/LandscapeRenderingStrategy.ts +++ b/src/js/stendhal/landscape/LandscapeRenderingStrategy.ts @@ -9,54 +9,21 @@ * * ***************************************************************************/ +import { Canvas } from "util/Types"; +import { TileMap } from "../data/TileMap"; import { CombinedTilesetFactory } from "./CombinedTilesetFactory"; -import { LandscapeRenderer } from "./LandscapeRenderer"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; export abstract class LandscapeRenderingStrategy { - public abstract onMapLoaded(map: any): void; + public abstract onMapLoaded(map: TileMap): void; public abstract onTilesetLoaded(): void; public abstract render( - canvas: HTMLCanvasElement, gamewindow: any, + canvas: Canvas, gamewindow: any, tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void; } - -export class CombinedTilesetRenderingStrategy extends LandscapeRenderingStrategy { - - public onMapLoaded(map: any): void { - let combinedTilesetFactory = new CombinedTilesetFactory(map); - stendhal.data.map.combinedTileset = combinedTilesetFactory.combine(); - } - - public onTilesetLoaded(): void { - let body = document.getElementById("body")!; - body.style.cursor = "auto"; - } - - public render( - canvas: HTMLCanvasElement, gamewindow: any, - tileOffsetX: number, tileOffsetY: number, targetTileWidth: number, targetTileHeight: number): void { - - let landscapeRenderder = new LandscapeRenderer(); - landscapeRenderder.drawLayer( - canvas, - stendhal.data.map.combinedTileset, - 0, - tileOffsetX, tileOffsetY, targetTileWidth, targetTileHeight); - - gamewindow.drawEntities(); - - landscapeRenderder.drawLayer( - canvas, - stendhal.data.map.combinedTileset, - 1, - tileOffsetX, tileOffsetY, targetTileWidth, targetTileHeight); - } - -} diff --git a/src/js/stendhal/landscape/ParallaxBackground.ts b/src/js/stendhal/landscape/ParallaxBackground.ts new file mode 100644 index 0000000000..574132be25 --- /dev/null +++ b/src/js/stendhal/landscape/ParallaxBackground.ts @@ -0,0 +1,102 @@ +/*************************************************************************** + * Copyright © 2024-2026 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RenderingContext2D } from "util/Types"; +import { singletons } from "../SingletonRepo"; +import { ImageRef } from "sprite/image/ImageRef"; +import { images } from "sprite/image/ImageManager"; + +export class ParallaxBackground { + + /** Default scrol rate of parallax background (1/4). */ + public static readonly SCROLL = 0.25; + + /** Tiled image to be drawn. */ + private imageRef?: ImageRef; + + /** Singleton instance. */ + private static instance: ParallaxBackground; + + + /** + * Retrieves singleton instance. + */ + static get(): ParallaxBackground { + if (!ParallaxBackground.instance) { + ParallaxBackground.instance = new ParallaxBackground(); + } + return ParallaxBackground.instance; + } + + /** + * Hidden singleton constructor. + */ + private constructor() { + // do nothing + } + + /** + * Sets image to be drawn. + * + * @param {string} name + * Relative path (exluding .png filename suffix) to image inside "data/maps/parallax" directory + * or `undefined` to unset. + * @param {number} width + * Map pixel width. + * @param {number} height + * Map pixel height. + */ + setImage(name: string, width: number, height: number) { + let fullPath = singletons.getPaths().parallax + "/" + name + ".png"; + this.imageRef = images.load(fullPath); + + /* FIXME: + //this.image = singletons.getTileStore().getParallax(name, ParallaxBackground.SCROLL, width, height); + singletons.getTileStore().getParallaxPromise(name, ParallaxBackground.SCROLL, width, height) + .then(image => { + this.image = image; + }).catch(error => { + console.error("Error setting parallax background \"" + name + "\"\n", error); + }); + */ + } + + /** + * Unsets parallax background image. + */ + reset() { + this.imageRef?.free(); + this.imageRef = undefined; + } + + draw(ctx: RenderingContext2D, offsetX: number, offsetY: number) { + if (!this.imageRef?.image) { + return; + } + let image = this.imageRef.image; + + // FIXME: seams are visible when walking + let dy = offsetY - ((offsetY / 4) % image.height); + for (dy; dy < image.height * 100; dy += image.height) { + let dx = offsetX - ((offsetX / 4) % image.width); + for (dx; dx < image.width * 100; dx += image.width) { + ctx.drawImage(image, dx, dy); + } + } + + /* TODO: use the following when `setImage` fixed + const tileLeft = offsetX - (offsetX * ParallaxBackground.SCROLL); + const tileTop = offsetY - (offsetY * ParallaxBackground.SCROLL); + ctx.drawImage(this.image, tileLeft, tileTop); + */ + } +} diff --git a/src/js/stendhal/license.js b/src/js/stendhal/license.js index 998fb61401..86e2af4ccb 100644 --- a/src/js/stendhal/license.js +++ b/src/js/stendhal/license.js @@ -1,7 +1,7 @@ /** * @license *************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * diff --git a/src/js/stendhal/main.ts b/src/js/stendhal/main.ts new file mode 100644 index 0000000000..55aa254ebf --- /dev/null +++ b/src/js/stendhal/main.ts @@ -0,0 +1,39 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { marauroa } from "marauroa"; +import { stendhal } from "./stendhal"; +import { Client } from "./Client"; + +declare var require: any; +(window as any)["Zlib"] = require("marauroa/inflate").Zlib; +let build = require("marauroa/build"); +require("./data/sha3"); + + + +/** + * Initializes "stendhal" object. + */ +function initGlobals() { + build.version(marauroa, stendhal); + stendhal.main = Client.get(); +} + +// entry point +(function() { + initGlobals(); + + stendhal.main.init(); + + document.addEventListener('DOMContentLoaded', () => stendhal.main.startup()); + window.addEventListener('error', (error: ErrorEvent) => stendhal.main.onError(error)); +})(); diff --git a/src/js/stendhal/sprite/AchievementBanner.ts b/src/js/stendhal/sprite/AchievementBanner.ts index 3ba28b0d5c..55d8f85b41 100644 --- a/src/js/stendhal/sprite/AchievementBanner.ts +++ b/src/js/stendhal/sprite/AchievementBanner.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2024 - Faiumoni e. V. * + * Copyright © 2003-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,11 +9,13 @@ * * ***************************************************************************/ -import { TextBubble } from "./TextBubble"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { ViewPort } from "ui/ViewPort"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; +import { stendhal } from "../stendhal"; import { BackgroundPainter } from "../util/BackgroundPainter"; - -declare const stendhal: any; - +import { TextBubble } from "./TextBubble"; export class AchievementBanner extends TextBubble { @@ -32,9 +34,9 @@ export class AchievementBanner extends TextBubble { constructor(cat: string, title: string, desc: string) { super(desc); this.title = title; - const bg = stendhal.data.sprites.get(stendhal.paths.gui + "/banner_background.png"); + const bg = htmlImageStore.get(Paths.gui + "/banner_background.png"); this.banner = new BackgroundPainter(bg); - this.icon = stendhal.data.sprites.get(stendhal.paths.achievements + this.icon = htmlImageStore.get(Paths.achievements + "/" + cat.toLowerCase() + ".png"); /* keep achievements on the screen a bit longer since they @@ -42,8 +44,7 @@ export class AchievementBanner extends TextBubble { */ this.duration = TextBubble.STANDARD_DUR * 4; - const gamewindow = - document.getElementById("viewport")!; + const gamewindow = document.getElementById("viewport")! as HTMLCanvasElement; const td = this.getTextDimensions(gamewindow.getContext("2d")!); this.innerWidth = td.width + this.padding; // add padding between icon & text @@ -55,9 +56,10 @@ export class AchievementBanner extends TextBubble { this.y = gamewindow.height - this.height; } - override draw(ctx: CanvasRenderingContext2D): boolean { - const targetX = stendhal.ui.gamewindow.offsetX + this.x; - const targetY = stendhal.ui.gamewindow.offsetY + this.y; + override draw(ctx: RenderingContext2D): boolean { + let viewPort = ViewPort.get(); + const targetX = viewPort.offsetX + this.x; + const targetY = viewPort.offsetY + this.y; const iconX = targetX + (this.width / 2) - (this.innerWidth / 2); const iconY = targetY + (this.height / 2) - (this.icon.height * 0.75); @@ -81,7 +83,7 @@ export class AchievementBanner extends TextBubble { return this.expired(); } - private getTextDimensions(ctx: CanvasRenderingContext2D): any { + private getTextDimensions(ctx: RenderingContext2D): any { const ret = {} as any; ctx.font = this.font; let m = ctx.measureText(this.text); @@ -95,10 +97,10 @@ export class AchievementBanner extends TextBubble { } override getX(): number { - return stendhal.ui.gamewindow.offsetX + this.x; + return ViewPort.get().offsetX + this.x; } override getY(): number { - return stendhal.ui.gamewindow.offsetY + this.y; + return ViewPort.get().offsetY + this.y; } } diff --git a/src/js/stendhal/sprite/ActivityIndicatorSprite.ts b/src/js/stendhal/sprite/ActivityIndicatorSprite.ts index 7434bfb6ce..0f1a2bf67f 100644 --- a/src/js/stendhal/sprite/ActivityIndicatorSprite.ts +++ b/src/js/stendhal/sprite/ActivityIndicatorSprite.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2024 - Faiumoni e. V. * + * Copyright © 2024-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,9 +9,12 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { Paths } from "../data/Paths"; -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { ImageRef } from "./image/ImageRef"; +import { images } from "./image/ImageManager"; /** @@ -19,10 +22,17 @@ declare var stendhal: any; */ export class ActivityIndicatorSprite { - private static readonly img = stendhal.data.sprites.get(Paths.sprites + "/ideas/activity.png"); + private imageRef: ImageRef; private frameIdx = 0; private lastFrameUpdate = 0; + private readonly animate: boolean; + + + constructor() { + this.animate = stendhal.config.getBoolean("activity-indicator.animate"); + this.imageRef = images.load(Paths.sprites + "/ideas/activity.png"); + } /** * Draws an indicator. @@ -34,27 +44,35 @@ export class ActivityIndicatorSprite { * @param width * Pixel width of parent object. */ - public draw(ctx: CanvasRenderingContext2D, dx: number, dy: number, width: number) { - if (!ActivityIndicatorSprite.img.complete) { + public draw(ctx: RenderingContext2D, dx: number, dy: number, width: number) { + let image = this.imageRef.image; + if (!image) { return; } - const cycleStart = Date.now(); - if (this.lastFrameUpdate == 0) { - this.lastFrameUpdate = cycleStart; - } - // NOTE: indicator image should be square - const dim = ActivityIndicatorSprite.img.height; - if (cycleStart - this.lastFrameUpdate > 150) { - this.frameIdx++; - if ((this.frameIdx + 1) * dim >= ActivityIndicatorSprite.img.width) { - this.frameIdx = 0; + let dim = image.height; + + if (this.animate) { + let cycleStart = Date.now(); + if (this.lastFrameUpdate == 0) { + this.lastFrameUpdate = cycleStart; + } + + if (cycleStart - this.lastFrameUpdate > 150) { + this.frameIdx++; + if ((this.frameIdx + 1) * dim >= image.width) { + this.frameIdx = 0; + } + this.lastFrameUpdate = cycleStart; } - this.lastFrameUpdate = cycleStart; } // draw in upper-right of target area - ctx.drawImage(ActivityIndicatorSprite.img, dim * this.frameIdx, 0, dim, dim, dx+width-dim, dy, dim, dim); + ctx.drawImage(image, dim * this.frameIdx, 0, dim, dim, dx+width-dim, dy, dim, dim); + } + + free() { + this.imageRef.free(); } } diff --git a/src/js/stendhal/sprite/EmojiSprite.ts b/src/js/stendhal/sprite/EmojiSprite.ts index d3abc01580..e81c22dd28 100644 --- a/src/js/stendhal/sprite/EmojiSprite.ts +++ b/src/js/stendhal/sprite/EmojiSprite.ts @@ -9,6 +9,7 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { RPEntity } from "../entity/RPEntity"; @@ -24,7 +25,7 @@ export class EmojiSprite { this.sprite = sprite; } - draw(ctx: CanvasRenderingContext2D): boolean { + draw(ctx: RenderingContext2D): boolean { let x = this.owner["_x"] * 32 - 16; let y = this.owner["_y"] * 32 - 32; if (x < 0) { diff --git a/src/js/stendhal/sprite/EmptySprite.ts b/src/js/stendhal/sprite/EmptySprite.ts deleted file mode 100644 index 8ceac7cfdb..0000000000 --- a/src/js/stendhal/sprite/EmptySprite.ts +++ /dev/null @@ -1,113 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -import { Sprite } from "./Sprite"; - - -/** - * An empty (non-drawing) sprite. - */ -export class EmptySprite { - - public constructor(private width: number, private height: number, private reference?: object) { - - } - - /** - * Create a sub-region of this sprite. NOTE: This does not use - * caching. - * - * @param x - * The starting X coordinate. - * @param y - * The starting Y coordinate. - * @param width - * The region width. - * @param height - * The region height. - * @param ref - * The sprite reference. - * - * @return A new sprite. - */ - createRegion(x: number, y: number, width: number, height: number, ref: object): Sprite { - return new EmptySprite(width, height, ref); - } - - /** - * Draw the sprite onto the graphics context provided. - * - * @param g - * The graphics context on which to draw the sprite - * @param x - * The x location at which to draw the sprite - * @param y - * The y location at which to draw the sprite - */ - draw(g: CanvasRenderingContext2D, x: number, y: number): void; - - /** - * Draws the image. - * - * @param g - * the graphics context where to draw to - * @param destx - * destination x - * @param desty - * destination y - * @param x - * the source x - * @param y - * the source y - * @param w - * the width - * @param h - * the height - */ - public draw(g: CanvasRenderingContext2D, destx: number, desty: number, x: number, y: number, w: number, h: number): void; - public draw(g: CanvasRenderingContext2D, destx: number, desty: number, x?: number, y?: number, w?: number, h?: number): void { - } - - /** - * Get the height of the drawn sprite. - * - * @return The height in pixels of this sprite - */ - public getHeight(): number { - return this.height; - } - - /** - * Get the sprite reference. This identifier is an externally opaque object - * that implements equals() and hashCode() to uniquely/repeatably reference - * a keyed sprite. - * - * @return The reference identifier, or null if not - * referencable. - */ - public getReference(): object | undefined { - return this.reference; - } - - /** - * Get the width of the drawn sprite. - * - * @return The width in pixels of this sprite - */ - public getWidth(): number { - return this.width;; - } - - public isConstant(): boolean { - return true; - } -} diff --git a/src/js/stendhal/sprite/Floater.ts b/src/js/stendhal/sprite/Floater.ts index f529137473..08a4f1e925 100644 --- a/src/js/stendhal/sprite/Floater.ts +++ b/src/js/stendhal/sprite/Floater.ts @@ -9,6 +9,7 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; import { TextSprite } from "./TextSprite"; /** @@ -23,7 +24,7 @@ export class Floater { this.textSprite = new TextSprite(this.message, this.color, "12px sans-serif") } - draw(ctx: CanvasRenderingContext2D, x: number, y: number): boolean { + draw(ctx: RenderingContext2D, x: number, y: number): boolean { let textOffset = this.textSprite.getTextMetrics(ctx).width / 2; var timeDiff = Date.now() - this.initTime; this.textSprite.draw(ctx, x - textOffset, y - timeDiff / 50); diff --git a/src/js/stendhal/sprite/ImageSprite.ts b/src/js/stendhal/sprite/ImageSprite.ts deleted file mode 100644 index 3e25d125f2..0000000000 --- a/src/js/stendhal/sprite/ImageSprite.ts +++ /dev/null @@ -1,194 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - -import { ImageWithDimensions } from "data/ImageWithDimensions"; -import { EmptySprite } from "./EmptySprite"; -import { Sprite } from "./Sprite"; - -/** - * A sprite to be displayed on the screen. Note that a sprite contains no state - * information, i.e. its just the image and not the location. This allows us to - * use a single sprite in lots of different places without having to store - * multiple copies of the image. - * - * @author Kevin Glass - */ -export class ImageSprite implements Sprite { - - /** The image to be drawn for this sprite. */ - private image: CanvasImageSource & ImageWithDimensions; - - /** - * The identifier reference. - */ - private reference?: object; - - /** - * Create a new sprite based on an image. - * - * @param image - * The image that is this sprite. - * @param reference - * The sprite reference, or null. - */ - public constructor (image: CanvasImageSource & ImageWithDimensions | Sprite, reference?: object) { - if (image instanceof Sprite) { - this.image = ImageSprite.createCompatibleImage(image); - } else { - this.image = image; - } - this.reference = reference; - } - - private static createCompatibleImage(sprite: Sprite): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - const g = canvas.getContext('2d'); - canvas.width = sprite.getWidth(); - canvas.height = sprite.getHeight(); - sprite.draw(g!, 0, 0); - return canvas; - } - - - /** - * Create a sub-region of this sprite. NOTE: This does not use - * caching. - * - * @param x - * The starting X coordinate. - * @param y - * The starting Y coordinate. - * @param width - * The region width. - * @param height - * The region height. - * @param ref - * The sprite reference. - * - * @return A new sprite. - */ - public createRegion(x: number, y: number, width: number, height: number, ref?: object): Sprite { - const iwidth = this.getWidth(); - const iheight = this.getHeight(); - - if ((x >= iwidth) || (y >= iheight)) { - // Outside of image (nothing to draw) - return new EmptySprite(width, height, ref); - } - - // Exclude regions outside the original image - width = Math.min(width, iwidth); - height = Math.min(height, iheight); - - // Full copy method (the memory hog) - const canvas = document.createElement('canvas'); - const g = canvas.getContext('2d'); - canvas.width = width; - canvas.height = height; - - this.draw(g!, 0, 0, x, y, width, height); - - return new ImageSprite(canvas, ref); - } - - /** - * Draw the sprite onto the graphics context provided. - * - * @param g - * The graphics context on which to draw the sprite - * @param x - * The x location at which to draw the sprite - * @param y - * The y location at which to draw the sprite - */ - public draw(g: CanvasRenderingContext2D, destx: number, desty: number): void; - - /** - * Draws the image. - * - * @param g - * the graphics context where to draw to - * @param destx - * destination x - * @param desty - * destination y - * @param x - * the source x - * @param y - * the source y - * @param w - * the width - * @param h - * the height - */ - public draw(g: CanvasRenderingContext2D, destx: number, desty: number, x: number, y: number, w: number, h: number): void; - public draw(g: CanvasRenderingContext2D, destx: number, desty: number, x?: number, y?: number, w?: number, h?: number): void { - if (arguments.length === 7) { - g.drawImage(this.image, destx, desty, destx + w!, desty + h!, x!, y!, x! + w!, y! + h!); - } else { - g.drawImage(this.image, destx, desty); - } - } - - /** - * Get the height of the drawn sprite. - * - * @return The height in pixels of this sprite - */ - public getHeight(): number { - return this.image.height as number; - } - - /** - * Get the sprite reference. This identifier is an externally opaque object - * that implements equals() and hashCode() to uniquely/repeatably reference - * a keyed sprite. - * - * @return The reference identifier, or null if not - * referencable. - */ - public getReference(): object | undefined { - return this.reference; - } - - /** - * Get the width of the drawn sprite. - * - * @return The width in pixels of this sprite - */ - public getWidth(): number { - return this.image.width as number; - } - - public isConstant(): boolean { - return true; - } - - /** - * Retrieves a single frame from the image. - * - * @param xIndex - * Horizontal index. - * @param yIndex - * Vertical index. - * @return - * Cropped Sprite. - */ - public getFrame(xIndex: number, yIndex: number): Sprite { - const w = this.getWidth() / 3; - const h = this.getHeight() / 4; - const x = w * xIndex; - const y = h * yIndex; - - return this.createRegion(x, y, w, h, undefined); - } -} diff --git a/src/js/stendhal/sprite/NotificationBubble.ts b/src/js/stendhal/sprite/NotificationBubble.ts index df6d04b6ca..83e1f8a2d3 100644 --- a/src/js/stendhal/sprite/NotificationBubble.ts +++ b/src/js/stendhal/sprite/NotificationBubble.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2003-2024 - Faiumoni e. V. * + * Copyright © 2003-2026 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -10,14 +10,18 @@ * * ***************************************************************************/ -declare var stendhal: any; import { TextBubble } from "./TextBubble"; import { Color } from "../data/color/Color"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { ViewPort } from "ui/ViewPort"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../data/Paths"; import { NotificationType } from "../util/NotificationType"; import { Speech } from "../util/Speech"; +import { HTMLImageElementUtil } from "./image/HTMLImageElementUtil"; export class NotificationBubble extends TextBubble { @@ -70,10 +74,11 @@ export class NotificationBubble extends TextBubble { } } - override draw(ctx: CanvasRenderingContext2D): boolean { - const screenTop = stendhal.ui.gamewindow.offsetY; + override draw(ctx: RenderingContext2D): boolean { + let viewPort = ViewPort.get() + const screenTop = viewPort.offsetY; const screenBottom = screenTop + ctx.canvas.height; - const screenLeft = stendhal.ui.gamewindow.offsetX; + const screenLeft = viewPort.offsetX; const screenCenterX = screenLeft + (ctx.canvas.width / 2); @@ -132,10 +137,10 @@ export class NotificationBubble extends TextBubble { * Loads a profile image to be drawn with text. */ private loadProfileSprite() { - const img = stendhal.data.sprites.get(stendhal.paths.sprites + const img = htmlImageStore.get(Paths.sprites + "/npc/" + this.profileName + ".png"); if (img.complete && img.height) { - this.profile = stendhal.data.sprites.getAreaOf(img, 48, 48, 48, 128); + this.profile = HTMLImageElementUtil.getAreaOf(img, 48, 48, 48, 128); } } } diff --git a/src/js/stendhal/sprite/OverlaySpriteImpl.ts b/src/js/stendhal/sprite/OverlaySpriteImpl.ts new file mode 100644 index 0000000000..cd12104f8f --- /dev/null +++ b/src/js/stendhal/sprite/OverlaySpriteImpl.ts @@ -0,0 +1,46 @@ +/*************************************************************************** + * Copyright © 20242-2026 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RenderingContext2D } from "util/Types"; + + +export interface OverlaySpriteImpl { + + /** + * Instructions when sprite should be drawn on viewport. + * + * @param {RenderingContext2D} ctx + * The viewport canvas drawing context. + * @param {number} x + * Horizonal pixel position of where to draw on canvas. + * @param {number} y + * Vertical pixel position of where to draw on canvas. + * @param {number} drawWidth + * Width of each frame to draw. + * @param {number} drawHeight + * Height of each frame to draw. + * @returns {boolean} + * `true` to denote sprite is expired & should be removed. + */ + draw(ctx: RenderingContext2D, x: number, y: number, drawWidth: number, drawHeight: number) + : boolean; + + /** + * Checks if sprite is expired. + * + * @returns {boolean} + * `true` if sprite should be removed. + */ + expired(): boolean; + + free(): void; +} diff --git a/src/js/stendhal/sprite/SpeechBubble.ts b/src/js/stendhal/sprite/SpeechBubble.ts index 2e8b71fc75..521d15eb3c 100644 --- a/src/js/stendhal/sprite/SpeechBubble.ts +++ b/src/js/stendhal/sprite/SpeechBubble.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { TextBubble } from "./TextBubble"; @@ -19,6 +19,8 @@ import { RPEntity } from "../entity/RPEntity"; import { Pair } from "../util/Pair"; import { Speech } from "../util/Speech"; +import { RenderingContext2D } from "util/Types"; +import { ViewPort } from "ui/ViewPort"; export class SpeechBubble extends TextBubble { @@ -31,6 +33,7 @@ export class SpeechBubble extends TextBubble { constructor(text: string, entity: RPEntity) { + let viewPort = ViewPort.get(); text = text.replace(/\\\\/g, "\\"); super((text.length > 30) ? (text.substring(0, 30) + "...") : text); this.entity = entity; @@ -41,9 +44,9 @@ export class SpeechBubble extends TextBubble { this.offsetY = 0; // find free text bubble position const x = this.getX(), y = this.getY(); - for (const bubble of stendhal.ui.gamewindow.textSprites) { + for (const bubble of viewPort.textSprites) { if (x == bubble.getX() && y + this.offsetY == bubble.getY()) { - this.offsetY += stendhal.ui.gamewindow.targetTileHeight / 2; + this.offsetY += viewPort.targetTileHeight / 2; } } @@ -52,7 +55,7 @@ export class SpeechBubble extends TextBubble { this.text.length * TextBubble.STANDARD_DUR / 50); } - override draw(ctx: CanvasRenderingContext2D): boolean { + override draw(ctx: RenderingContext2D): boolean { ctx.lineWidth = 2; ctx.font = "14px Arial"; ctx.fillStyle = Color.WHITE; @@ -82,8 +85,9 @@ export class SpeechBubble extends TextBubble { override getX(): number { let x = this.entity["_x"] * 32 + (32 * this.entity["width"]); if (this.entity.inView()) { + let viewPort = ViewPort.get(); // keep on screen while entity is visible (border is 1 pixel) - const overdraw = x + this.width - (stendhal.ui.gamewindow.offsetX + stendhal.ui.gamewindow.width) + 1; + const overdraw = x + this.width - (viewPort.offsetX + viewPort.width) + 1; x = overdraw > 0 ? x - overdraw : x; } return x; @@ -92,8 +96,9 @@ export class SpeechBubble extends TextBubble { override getY(): number { let y = this.entity["_y"] * 32 - 16 - (32 * (this.entity["height"] - 1)) + this.offsetY - TextBubble.adjustY; if (this.entity.inView()) { + let viewPort = ViewPort.get(); // keep on screen while entity is visible (border is 1 pixel) - y = y < stendhal.ui.gamewindow.offsetY + 1 ? stendhal.ui.gamewindow.offsetY + 1 : y; + y = y < viewPort.offsetY + 1 ? viewPort.offsetY + 1 : y; } return y; } diff --git a/src/js/stendhal/sprite/Sprite.ts b/src/js/stendhal/sprite/Sprite.ts deleted file mode 100644 index acfb065ba5..0000000000 --- a/src/js/stendhal/sprite/Sprite.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * - *************************************************************************** - *************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU Affero General Public License as * - * published by the Free Software Foundation; either version 3 of the * - * License, or (at your option) any later version. * - * * - ***************************************************************************/ - - /** - * A sprite to be displayed on the screen. Note that a sprite contains no state - * information, i.e. its just the image and not the location. This allows us to - * use a single sprite in lots of different places without having to store - * multiple copies of the image. - */ -export abstract class Sprite { - - /** - * Create a sub-region of this sprite. NOTE: This does not use - * caching. - * - * @param x - * The starting X coordinate. - * @param y - * The starting Y coordinate. - * @param width - * The region width. - * @param height - * The region height. - * @param ref - * The sprite reference. - * - * @return A new sprite. - */ - abstract createRegion(x: number, y: number, width: number, height: number, ref: object): Sprite; - - /** - * Draw the sprite onto the graphics context provided. - * - * @param g - * The graphics context on which to draw the sprite - * @param x - * The x location at which to draw the sprite - * @param y - * The y location at which to draw the sprite - */ - abstract draw(g: CanvasRenderingContext2D, x: number, y: number): void; - - /** - * Draws the image. - * - * @param g - * the graphics context where to draw to - * @param destx - * destination x - * @param desty - * destination y - * @param x - * the source x - * @param y - * the source y - * @param w - * the width - * @param h - * the height - */ - abstract draw(g: CanvasRenderingContext2D, destx: number, desty: number, x: number, y: number, w: number, h: number): void; - - /** - * Get the height of the drawn sprite. - * - * @return The height in pixels of this sprite - */ - abstract getHeight(): number; - - /** - * Get the sprite reference. This identifier is an externally opaque object - * that implements equals() and hashCode() to uniquely/repeatably reference - * a keyed sprite. - * - * @return The reference identifier, or null if not - * referencable. - */ - abstract getReference(): object | undefined; - - /** - * Get the width of the drawn sprite. - * - * @return The width in pixels of this sprite - */ - abstract getWidth(): number; - - /** - * Check whether the sprite won't change between draws. - * - * @return true if different draws of the sprite always - * have the same result. - */ - abstract isConstant(): boolean; -} diff --git a/src/js/stendhal/sprite/TextBubble.ts b/src/js/stendhal/sprite/TextBubble.ts index 97f4c1cc0f..b15e7060db 100644 --- a/src/js/stendhal/sprite/TextBubble.ts +++ b/src/js/stendhal/sprite/TextBubble.ts @@ -9,10 +9,13 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; +import { RenderingContext2D } from "util/Types"; import { Color } from "../data/color/Color"; import { Pair } from "../util/Pair"; +import { HTMLUtil } from "ui/HTMLUtil"; +import { ViewPort } from "ui/ViewPort"; export abstract class TextBubble { @@ -45,7 +48,7 @@ export abstract class TextBubble { * true if the sprites duration time has expired * & should be removed from the screen. */ - abstract draw(ctx: CanvasRenderingContext2D): boolean; + abstract draw(ctx: RenderingContext2D): boolean; getX(): number { return this.x; @@ -109,14 +112,15 @@ export abstract class TextBubble { * Mouse or touch event executed on canvas context. */ onClick(evt: MouseEvent|TouchEvent) { - const pos = stendhal.ui.html.extractPosition(evt); + let viewPort = ViewPort.get(); + const pos = HTMLUtil.extractPosition(evt); const screenRect = document.getElementById("viewport")!.getBoundingClientRect(); - const pointX = pos.clientX - screenRect.x + stendhal.ui.gamewindow.offsetX; - const pointY = pos.clientY - screenRect.y + stendhal.ui.gamewindow.offsetY + TextBubble.adjustY; + const pointX = pos.clientX - screenRect.x + viewPort.offsetX; + const pointY = pos.clientY - screenRect.y + viewPort.offsetY + TextBubble.adjustY; if (this.clipsPoint(pointX, pointY)) { evt.stopPropagation(); // remove when clicked or tapped - stendhal.ui.gamewindow.removeTextBubble(this, pointX, pointY); + viewPort.removeTextBubble(this, pointX, pointY); } } @@ -128,7 +132,13 @@ export abstract class TextBubble { * @param ctx * Canvas context on which text bubble is drawn. */ - onAdded(ctx: CanvasRenderingContext2D) { + onAdded(ctx: RenderingContext2D) { + // TODO: Reference the Viewport directly instead of deriving the canvas from the context + if (!(ctx instanceof CanvasRenderingContext2D)) { + console.error("Cannot add event listeners because HTMLCanvasElement is required."); + return; + } + // prevent multiple listeners from being added if (typeof(this.onRemovedAction) === "undefined") { // add click listener to remove chat bubble diff --git a/src/js/stendhal/sprite/TextSprite.ts b/src/js/stendhal/sprite/TextSprite.ts index 458ad09207..9ec21c69b2 100644 --- a/src/js/stendhal/sprite/TextSprite.ts +++ b/src/js/stendhal/sprite/TextSprite.ts @@ -9,6 +9,8 @@ * * ***************************************************************************/ +import { RenderingContext2D } from "util/Types"; + export class TextSprite { private textMetrics?: TextMetrics; @@ -25,7 +27,7 @@ export class TextSprite { * @param x x coordinate * @param y y coordinate */ - public draw(ctx: CanvasRenderingContext2D, x: number, y: number) { + public draw(ctx: RenderingContext2D, x: number, y: number) { ctx.font = this.font; ctx.lineWidth = 2; ctx.strokeStyle = "black"; @@ -35,7 +37,7 @@ export class TextSprite { ctx.fillText(this.text, x, y); } - getTextMetrics(ctx: CanvasRenderingContext2D) { + getTextMetrics(ctx: RenderingContext2D) { if (!this.textMetrics) { ctx.font = this.font; this.textMetrics = ctx.measureText(this.text); diff --git a/src/js/stendhal/sprite/action/AttackSprite.ts b/src/js/stendhal/sprite/action/AttackSprite.ts new file mode 100644 index 0000000000..6740ceaa55 --- /dev/null +++ b/src/js/stendhal/sprite/action/AttackSprite.ts @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright © 2023-2024 - Stendhal * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RenderingContext2D } from "util/Types"; +import { OverlaySpriteImpl } from "../OverlaySpriteImpl"; + + +/** + * Sprite representing an attack animation. + */ +export abstract class AttackSprite implements OverlaySpriteImpl { + + protected readonly initTime: number; + + + constructor() { + this.initTime = Date.now(); + } + + abstract draw(ctx: RenderingContext2D, x: number, y: number, entityWidth: number, + entityHeight: number): boolean; + + expired(): boolean { + return Date.now() - this.initTime > 180; + } + + abstract free() :void; +} diff --git a/src/js/stendhal/sprite/action/BarehandAttackSprite.ts b/src/js/stendhal/sprite/action/BarehandAttackSprite.ts index fd6b561ba0..441191ddd1 100644 --- a/src/js/stendhal/sprite/action/BarehandAttackSprite.ts +++ b/src/js/stendhal/sprite/action/BarehandAttackSprite.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2023-2024 - Faiumoni e. V. * + * Copyright © 2023-2026 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -10,34 +10,40 @@ * * ***************************************************************************/ -import { ActionSprite } from "./ActionSprite"; +import { RenderingContext2D } from "util/Types"; import { RPEntity } from "../../entity/RPEntity"; +import { AttackSprite } from "./AttackSprite"; -declare var stendhal: any; +import { images } from "sprite/image/ImageManager"; +import { ImageRef } from "sprite/image/ImageRef"; +import { ViewPort } from "ui/ViewPort"; -export class BarehandAttackSprite extends ActionSprite { +export class BarehandAttackSprite extends AttackSprite { private readonly dir: number; - private readonly image: HTMLImageElement; + private readonly imageRef: ImageRef; - constructor(source: RPEntity, image: HTMLImageElement) { + constructor(source: RPEntity, imagePath: string) { super(); this.dir = source["dir"]; - this.image = image; + this.imageRef = images.load(imagePath); } - public override draw(ctx: CanvasRenderingContext2D, x: number, y: number, entityWidth: number, - entityHeight: number) { - if (!this.image || !this.image.height) { - return; + override draw(ctx: RenderingContext2D, x: number, y: number, + entityWidth: number, entityHeight: number): boolean { + + let image = this.imageRef.image; + if (!image) { + return this.expired(); } - + + let viewPort = ViewPort.get(); const dtime = Date.now() - this.initTime; const frameIndex = Math.floor(Math.min(dtime / 60, 2)); - const drawWidth = this.image.width / 3; - const drawHeight = this.image.height / 4; + const drawWidth = image.width / 3; + const drawHeight = image.height / 4; const centerX = x + (entityWidth - drawWidth) / 2; const centerY = y + (entityHeight - drawHeight) / 2; @@ -45,19 +51,19 @@ export class BarehandAttackSprite extends ActionSprite { let sx, sy; switch (this.dir+"") { case "1": // UP - sx = centerX + (stendhal.ui.gamewindow.targetTileWidth / 2); - sy = y - (stendhal.ui.gamewindow.targetTileHeight * 1.5); + sx = centerX + (viewPort.targetTileWidth / 2); + sy = y - (viewPort.targetTileHeight * 1.5); break; case "3": // DOWN sx = centerX; - sy = y + entityHeight - drawHeight + (stendhal.ui.gamewindow.targetTileHeight / 2); + sy = y + entityHeight - drawHeight + (viewPort.targetTileHeight / 2); break; case "4": // LEFT - sx = x - (stendhal.ui.gamewindow.targetTileWidth / 2); - sy = centerY - (stendhal.ui.gamewindow.targetTileHeight / 2); + sx = x - (viewPort.targetTileWidth / 2); + sy = centerY - (viewPort.targetTileHeight / 2); break; case "2": // RIGHT - sx = x + entityWidth - drawWidth + (stendhal.ui.gamewindow.targetTileWidth / 2); + sx = x + entityWidth - drawWidth + (viewPort.targetTileWidth / 2); sy = centerY; // - ICON_OFFSET; // ICON_OFFSET = 8 in Java client break; default: @@ -65,7 +71,16 @@ export class BarehandAttackSprite extends ActionSprite { sy = centerY; } - ctx.drawImage(this.image, frameIndex * drawWidth, (this.dir - 1) * drawHeight, - drawWidth, drawHeight, sx, sy, drawWidth, drawHeight); + ctx.drawImage(image, + frameIndex * drawWidth, (this.dir - 1) * drawHeight, + drawWidth, drawHeight, + sx, sy, + drawWidth, drawHeight); + return this.expired(); } + + override free(): void { + this.imageRef?.free(); + } + } diff --git a/src/js/stendhal/sprite/action/DirectionalSkillEffect.ts b/src/js/stendhal/sprite/action/DirectionalSkillEffect.ts new file mode 100644 index 0000000000..e2a7d6880f --- /dev/null +++ b/src/js/stendhal/sprite/action/DirectionalSkillEffect.ts @@ -0,0 +1,58 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { SkillEffect } from "./SkillEffect"; + +import { Entity } from "../../entity/Entity"; + +import { Direction } from "../../util/Direction"; +import { RenderingContext2D } from "util/Types"; + + +/** + * Represents an entity animation overlay with variations for facing direction. + */ +export class DirectionalSkillEffect extends SkillEffect { + + private readonly entity: Entity; + + + /** + * Creates a new skill effect sprite. + * + * @param {Entity} entity + * Entity associated with this effect. + * @param {string} name + * Image filename (excluding .png suffix). + * @param {number} [delay=100] + * Time, in milliseconds, for which each frame should be displayed. Any value less than 1 + * results in using a default of 100ms. + * @param {number} [duration=0] + * Time, in milliseconds, sprite drawing should persist. Any value less than 1 signifies that + * sprite should not expire. + */ + constructor(entity: Entity, name: string, delay=100, duration=0) { + super(name, delay, duration); + this.entity = entity; + } + + protected override drawInternal(ctx: RenderingContext2D, colIdx: number, x: number, + y: number, drawWidth: number, drawHeight: number) { + const dir = Direction.VALUES[parseInt(this.entity["dir"], 10)]; + let rowIdx = dir.val - 1; + if (rowIdx < 0) { + rowIdx = Direction.DOWN.val - 1; // default is third row (down) + } + ctx.drawImage(this.imageRef.image!, colIdx*drawWidth, rowIdx*drawHeight, drawWidth, drawHeight, + x, y, drawWidth, drawHeight); + } +} diff --git a/src/js/stendhal/sprite/action/MeleeAttackSprite.ts b/src/js/stendhal/sprite/action/MeleeAttackSprite.ts index caa2f89d46..9dfe8beb60 100644 --- a/src/js/stendhal/sprite/action/MeleeAttackSprite.ts +++ b/src/js/stendhal/sprite/action/MeleeAttackSprite.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2023-2024 - Faiumoni e. V. * + * Copyright © 2023-2026 - Faiumoni e. V. * *************************************************************************** *************************************************************************** * * @@ -10,14 +10,14 @@ * * ***************************************************************************/ -declare var stendhal: any; - -import { ActionSprite } from "./ActionSprite"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { RenderingContext2D } from "util/Types"; import { RPEntity } from "../../entity/RPEntity"; import { Direction } from "../../util/Direction"; +import { AttackSprite } from "./AttackSprite"; -export class MeleeAttackSprite extends ActionSprite { +export class MeleeAttackSprite extends AttackSprite { /** Direction entity is facing. */ private readonly dir: Direction; @@ -45,10 +45,10 @@ export class MeleeAttackSprite extends ActionSprite { const rot = 90 * (this.dir.val - 1); imagePath = imagePath.replace(/\.png$/, ""); this.frames = [ - //~ stendhal.data.sprites.getRotated(imagePath + "_rot45.png", rot-90), + //~ singletons.getSpriteStore().getRotated(imagePath + "_rot45.png", rot-90), undefined, - stendhal.data.sprites.getRotated(imagePath + ".png", rot), - //~ stendhal.data.sprites.getRotated(imagePath + "_rot45.png", rot) + htmlImageStore.getRotated(imagePath + ".png", rot), + //~ singletons.getSpriteStore().getRotated(imagePath + "_rot45.png", rot) undefined ]; if (this.frames[1].complete) { @@ -108,15 +108,15 @@ export class MeleeAttackSprite extends ActionSprite { } } - public override draw(ctx: CanvasRenderingContext2D, x: number, y: number, entityWidth: number, - entityHeight: number) { + override draw(ctx: RenderingContext2D, x: number, y: number, entityWidth: number, + entityHeight: number): boolean { const dtime = Date.now() - this.initTime; const frameIndex = Math.floor(Math.min(dtime / 60, 2)); // TODO: add rotated images for each attack type //~ const frame = this.frames[frameIndex]; const frame = this.frames[1]; if (!frame || !frame.height) { - return; + return this.expired(); } const cx = x + Math.floor(entityWidth / 2); @@ -128,5 +128,10 @@ export class MeleeAttackSprite extends ActionSprite { const dy = cy + this.offsetY[1]; ctx.drawImage(frame, 0, 0, frame.width, frame.height, dx, dy, frame.width, frame.height); + return this.expired(); + } + + override free(): void { + // do nothing } } diff --git a/src/js/stendhal/sprite/action/RangedAttackSprite.ts b/src/js/stendhal/sprite/action/RangedAttackSprite.ts index 1aa4ca221a..2a32346cc9 100644 --- a/src/js/stendhal/sprite/action/RangedAttackSprite.ts +++ b/src/js/stendhal/sprite/action/RangedAttackSprite.ts @@ -1,5 +1,6 @@ /*************************************************************************** - * Copyright © 2023 - Stendhal * + * Copyright © 2023-2026 - Stendhal * + *************************************************************************** *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,24 +10,29 @@ * * ***************************************************************************/ -import { ActionSprite } from "./ActionSprite"; +import { RenderingContext2D } from "util/Types"; +import { Paths } from "../../data/Paths"; import { RPEntity } from "../../entity/RPEntity"; +import { AttackSprite } from "./AttackSprite"; -declare var stendhal: any; +import { images } from "sprite/image/ImageManager"; +import { ImageRef } from "sprite/image/ImageRef"; +import { ViewPort } from "ui/ViewPort"; -export class RangedAttackSprite extends ActionSprite { +export class RangedAttackSprite extends AttackSprite { private readonly dir: number; private readonly targetX: number; private readonly targetY: number; private readonly color: string; - private readonly image = stendhal.data.sprites.get(stendhal.paths.sprites + "/combat/ranged.png"); + private readonly imageRef: ImageRef; private readonly weapon?: string; constructor(source: RPEntity, target: RPEntity, color: string, weapon?: string) { super(); + this.imageRef = images.load(Paths.sprites + "/combat/ranged.png"); this.dir = source["dir"]; this.targetX = (target.x + target.width / 2) * 32; this.targetY = (target.y + target.height / 2) * 32; @@ -34,9 +40,10 @@ export class RangedAttackSprite extends ActionSprite { this.weapon = weapon; } - public override draw(ctx: CanvasRenderingContext2D, x: number, y: number, entityWidth: number, - entityHeight: number) { + override draw(ctx: RenderingContext2D, x: number, y: number, entityWidth: number, + entityHeight: number): boolean { // FIXME: alignment with entity is not correct + let viewPort = ViewPort.get(); var dtime = Date.now() - this.initTime; // We can use fractional "frame" for the lines. Just @@ -60,12 +67,14 @@ export class RangedAttackSprite extends ActionSprite { ctx.lineTo(endX, endY); ctx.stroke(); + let image = this.imageRef.image; + // draw bow - if (this.weapon === "ranged" && this.image.height) { + if (this.weapon === "ranged" && image) { frame = Math.floor(Math.min(dtime / 60, 3)); const yRow = this.dir - 1; - const drawWidth = this.image.width / 3; - const drawHeight = this.image.height / 4; + const drawWidth = image.width / 3; + const drawHeight = image.height / 4; const centerX = x + (entityWidth - drawWidth) / 2; const centerY = y + (entityHeight - drawHeight) / 2; @@ -74,19 +83,19 @@ export class RangedAttackSprite extends ActionSprite { let sx, sy; switch (this.dir+"") { case "1": // UP - sx = centerX + (stendhal.ui.gamewindow.targetTileWidth / 2); - sy = y - (stendhal.ui.gamewindow.targetTileHeight * 1.5); + sx = centerX + (viewPort.targetTileWidth / 2); + sy = y - (viewPort.targetTileHeight * 1.5); break; case "3": // DOWN sx = centerX; - sy = y + entityHeight - drawHeight + (stendhal.ui.gamewindow.targetTileHeight / 2); + sy = y + entityHeight - drawHeight + (viewPort.targetTileHeight / 2); break; case "4": // LEFT - sx = x - (stendhal.ui.gamewindow.targetTileWidth / 2); - sy = centerY - (stendhal.ui.gamewindow.targetTileHeight / 2); + sx = x - (viewPort.targetTileWidth / 2); + sy = centerY - (viewPort.targetTileHeight / 2); break; case "2": // RIGHT - sx = x + entityWidth - drawWidth + (stendhal.ui.gamewindow.targetTileWidth / 2); + sx = x + entityWidth - drawWidth + (viewPort.targetTileWidth / 2); sy = centerY; // - ICON_OFFSET; // ICON_OFFSET = 8 in Java client break; default: @@ -94,8 +103,16 @@ export class RangedAttackSprite extends ActionSprite { sy = centerY; } - ctx.drawImage(this.image, frame * drawWidth, yRow * drawHeight, - drawWidth, drawHeight, sx, sy, drawWidth, drawHeight); + ctx.drawImage(image, + frame * drawWidth, yRow * drawHeight, + drawWidth, drawHeight, + sx, sy, + drawWidth, drawHeight); } + return this.expired(); + } + + override free(): void { + this.imageRef.free(); } } diff --git a/src/js/stendhal/sprite/action/SkillEffect.ts b/src/js/stendhal/sprite/action/SkillEffect.ts new file mode 100644 index 0000000000..f42be3aec6 --- /dev/null +++ b/src/js/stendhal/sprite/action/SkillEffect.ts @@ -0,0 +1,107 @@ +/*************************************************************************** + * Copyright © 2024-2026 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { OverlaySpriteImpl } from "../OverlaySpriteImpl"; + +import { images } from "sprite/image/ImageManager"; +import { ImageRef } from "sprite/image/ImageRef"; +import { RenderingContext2D } from "util/Types"; +import { singletons } from "../../SingletonRepo"; + + +/** + * Represents an entity animation overlay. + */ +export class SkillEffect implements OverlaySpriteImpl { + + /** Image to be drawn on canvas. */ + protected readonly imageRef: ImageRef; + /** Time for which each frame should be displayed. */ + private readonly delay: number; + /** Timestamp at which this sprite should expire. */ + private readonly expires: number; + /** Timestamp at which frame cycling begins. */ + private cycleTime: number; + /** Timestamp of most recent draw. */ + private drawTime: number; + + + /** + * Creates a new skill effect sprite. + * + * @param {string} name + * Image filename (excluding .png suffix). + * @param {number} [delay=100] + * Time, in milliseconds, for which each frame should be displayed. Any value less than 1 + * results in using a default of 100ms. + * @param {number} [duration=0] + * Time, in milliseconds, sprite drawing should persist. Any value less than 1 signifies that + * sprite should not expire. + */ + constructor(name: string, delay=100, duration=0) { + this.imageRef = images.load(singletons.getPaths().sprites + "/entity/overlay/" + name + ".png"); + this.cycleTime = Date.now(); + this.drawTime = 0; + this.delay = delay > 0 ? delay : 100; + this.expires = duration > 0 ? this.cycleTime + duration : 0; + } + + /** + * Draws sprite to canvas. + * + * @param {RenderingContext2D} ctx + * Canvas drawing context. + * @param {number} colIdx + * Horizontal frame index. + * @param {number} x + * Horizonal pixel position of where to draw on canvas. + * @param {number} y + * Vertical pixel position of where to draw on canvas. + * @param {number} drawWidth + * Width of each frame to draw. + * @param {number} drawHeight + * Height of each frame to draw. + */ + protected drawInternal(ctx: RenderingContext2D, colIdx: number, x: number, y: number, + drawWidth: number, drawHeight: number) { + ctx.drawImage(this.imageRef.image!, colIdx*drawWidth, 0, drawWidth, drawHeight, + x, y, drawWidth, drawHeight); + } + + draw(ctx: RenderingContext2D, x=0, y=0, drawWidth=48, drawHeight=64): boolean { + this.drawTime = Date.now(); + if (!this.imageRef.image) { + return this.expired(); + } + + const timeDiff = this.drawTime - this.cycleTime; + const fcount = Math.floor(this.imageRef.image?.width / drawWidth); + let frame = Math.floor(timeDiff / this.delay); + if (frame >= fcount) { + // start new frame cycle + frame = 0; + this.cycleTime = this.drawTime; + } + + this.drawInternal(ctx, frame, x, y, drawWidth, drawHeight); + return this.expired(); + } + + expired(): boolean { + return this.expires > 0 && this.drawTime > this.expires; + } + + free(): void { + this.imageRef.free(); + } + +} diff --git a/src/js/stendhal/sprite/image/FilteredImageRefImpl.ts b/src/js/stendhal/sprite/image/FilteredImageRefImpl.ts new file mode 100644 index 0000000000..806610a1c1 --- /dev/null +++ b/src/js/stendhal/sprite/image/FilteredImageRefImpl.ts @@ -0,0 +1,31 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { images } from "./ImageManager"; +import { ImageFilter } from "./ImageFilter"; +import { ImageRefImpl } from "./ImageRefImpl"; + +export class FilteredImageRefImpl extends ImageRefImpl { + + constructor(filename: string, private filter: string, private param?: number) { + super(filename); + } + + override async load() { + let originalImage = images.load(this.filename); + await originalImage.waitFor(); + if (originalImage.image) { + this.image = new ImageFilter().filterImage(originalImage.image, this.filter, this.param); + } + images.free(this.filename); + this.promiseResolve(); + } +} diff --git a/src/js/stendhal/sprite/image/HTMLImageElementUtil.ts b/src/js/stendhal/sprite/image/HTMLImageElementUtil.ts new file mode 100644 index 0000000000..4bbf76fff8 --- /dev/null +++ b/src/js/stendhal/sprite/image/HTMLImageElementUtil.ts @@ -0,0 +1,55 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { ImageWithDimensions } from "data/ImageWithDimensions"; + +export class HTMLImageElementUtil { + + /** + * Get an image element whose image data is an area of a specified image. + * If the area matches the original image, the image itself is returned. + * Otherwise a copy of the image data is returned. This is meant + * to be used for obtaining the drag image for drag and drop. + * + * This method intentionally returns an HTMLImageElement because Chrome is + * not able to use an HTMLCanvasElement as drag image. Firefox does support + * this. But neither browser is able to use an ImageBitmap. + * + * @param image original image + * @param width width of the area + * @param height height of the area + * @param offsetX optional. left x coordinate of the area + * @param offsetY optional. top y coordinate of the area + */ + static getAreaOf(image: CanvasImageSource & ImageWithDimensions, width: number, height: number, + offsetX?: number, offsetY?: number): any { + + offsetX = offsetX || 0; + offsetY = offsetY || 0; + if (image instanceof HTMLImageElement + && (image.width === width) && (image.height === height) + && (offsetX === 0) && (offsetY === 0)) { + return image; + } + let canvas = document.createElement("canvas") as HTMLCanvasElement; + canvas.width = width; + canvas.height = height; + let ctx = canvas.getContext("2d", {willReadFrequently: true})!; + try { + ctx.drawImage(image, offsetX, offsetY, width, height, 0, 0, width, height); + } catch (err) { + console.log(err); + } + let newImage = new Image(); + newImage.src = canvas.toDataURL("image/png"); + return newImage; + } +} diff --git a/src/js/stendhal/sprite/image/ImageCache.ts b/src/js/stendhal/sprite/image/ImageCache.ts new file mode 100644 index 0000000000..49e7968566 --- /dev/null +++ b/src/js/stendhal/sprite/image/ImageCache.ts @@ -0,0 +1,48 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { stendhal } from "stendhal" + +export class ImageCache { + images: Record = {}; + closed = false; + + load(filenames: string[]) { + for (let filename of filenames) { + this.loadImage(filename); + } + } + + private async loadImage(filename: string) { + let url = filename + "?v=" + stendhal.data.build.version; + let response = await fetch(url); + if (!response.ok || this.closed) { + return; + } + let blob = await response.blob(); + if (this.closed) { + return; + } + let bitmap = await createImageBitmap(blob); + if (this.closed) { + bitmap.close(); + return; + } + this.images[filename] = bitmap; + } + + close() { + this.closed = true; + for (let image of Object.values(this.images)) { + image?.close(); + } + } +} \ No newline at end of file diff --git a/src/js/stendhal/sprite/image/ImageFilter.ts b/src/js/stendhal/sprite/image/ImageFilter.ts new file mode 100644 index 0000000000..d23a3acb1f --- /dev/null +++ b/src/js/stendhal/sprite/image/ImageFilter.ts @@ -0,0 +1,199 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { ImageWithDimensions } from "data/ImageWithDimensions"; + +export class ImageFilter { + + /** + * @param rgb + */ + splitrgb(rgb: number): number[] { + rgb &= 0xffffff; + let b = rgb & 0xff; + rgb >>>= 8; + let g = rgb & 0xff; + rgb >>>= 8; + return [rgb, g, b]; + } + + mergergb(rgbArray: number[]): number { + const r = rgbArray[0] << 16; + const g = rgbArray[1] << 8; + return 0xffffff & (r | g | rgbArray[2]); + } + + /** + * @param rgb + */ + rgb2hsl(rgb: number[]): number[] { + let r = rgb[0] / 255; + let g = rgb[1] / 255; + let b = rgb[2] / 255; + + let max, min, maxVar; + // Find the max and minimum colors, and remember which one it was + if (r > g) { + max = r; + min = g; + maxVar = 0; + } else { + max = g; + min = r; + maxVar = 1; + } + if (b > max) { + max = b; + maxVar = 2; + } else if (b < min) { + min = b; + } + + // lightness + let l = (max + min) / 2; + let s, h; + + // saturation + let diff = max - min; + if (diff < 0.000001) { + s = 0; + // hue not really defined, but set it to something reasonable + h = 0; + } else { + if (l < 0.5) { + s = diff / (max + min); + } else { + s = diff / (2 - max - min); + } + + // hue + if (maxVar === 0) { + h = (g - b) / diff; + } else if (maxVar === 1) { + h = 2 + (b - r) / diff; + } else { + h = 4 + (r - g) / diff; + } + // Normalize to range [0, 1]. It's more useful than the usual 360 + h /= 6; + } + + return [h, s, l]; + } + + /** + * @param {Array} hsl + * @return {Array} + */ + hsl2rgb(hsl: number[]): number[] { + let r, g, b; + let h = hsl[0]; + let s = hsl[1]; + let l = hsl[2]; + + if (s < 0.0000001) { + r = g = b = Math.floor(255 * l); + } else { + let tmp1, tmp2; + if (l < 0.5) { + tmp1 = l * (1 + s); + } else { + tmp1 = l + s - l * s; + } + tmp2 = 2 * l - tmp1; + + let rf = this.hue2rgb(this.limitHue(h + 1 / 3), tmp2, tmp1); + let gf = this.hue2rgb(this.limitHue(h), tmp2, tmp1); + let bf = this.hue2rgb(this.limitHue(h - 1 / 3), tmp2, tmp1); + + r = Math.floor(255 * rf) & 0xff; + g = Math.floor(255 * gf) & 0xff; + b = Math.floor(255 * bf) & 0xff; + } + + return [r, g, b]; + } + + /** + * @param hue + * @param val1 + * @param val2 + */ + private hue2rgb(hue: number, val1: number, val2: number): number { + let res = hue; + if (6 * hue < 1) { + res = val1 + (val2 - val1) * 6 * hue; + } else if (2 * hue < 1) { + res = val2; + } else if (3 * hue < 2) { + res = val1 + (val2 - val1) * (2 / 3 - hue) * 6; + } else { + res = val1; + } + + return res; + } + + /** + * @param hue + */ + private limitHue(hue: number): number { + let res = hue; + if (res < 0) { + res += 1; + } else if (res > 1) { + res -= 1; + } + return res; + } + + private trueColor(data: any, color: number) { + let hslColor = this.rgb2hsl(this.splitrgb(color)); + let end = data.length; + for (let i = 0; i < end; i += 4) { + let rgb = [data[i], data[i + 1], data[i + 2]]; + let hsl = this.rgb2hsl(rgb); + // Adjust the brightness + let adj = hslColor[2] - 0.5; // [-0.5, 0.5] + let tmp = hsl[2] - 0.5; // [-0.5, 0.5] + // tweaks the middle lights either upward or downward, depending + // on if source lightness is high or low + let l = hsl[2] - 2.0 * adj * ((tmp * tmp) - 0.25); + let resultHsl = [hslColor[0], hslColor[1], l]; + let resultRgb = this.hsl2rgb(resultHsl); + data[i] = resultRgb[0]; + data[i + 1] = resultRgb[1]; + data[i + 2] = resultRgb[2]; + } + } + + + public filter(data: ImageData, filter: string, param?: number): void { + if (filter ==='trueColor') { + this.trueColor(data.data, param!); + } else { + console.log("Unknown filter name " + filter); + } + } + + + public filterImage(image: CanvasImageSource & ImageWithDimensions, filter: string, param?: number): ImageBitmap { + const canvas = new OffscreenCanvas(image.width, image.height); + const ctx = canvas.getContext("2d", {willReadFrequently: true})!; + ctx.drawImage(image, 0, 0); + let imageData = ctx.getImageData(0, 0, image.width, image.height); + new ImageFilter().filter(imageData, filter, param); + this.filter(imageData, filter, param); + ctx.putImageData(imageData, 0, 0); + return canvas.transferToImageBitmap(); + } +} + diff --git a/src/js/stendhal/sprite/image/ImageManager.ts b/src/js/stendhal/sprite/image/ImageManager.ts new file mode 100644 index 0000000000..8c8ec26348 --- /dev/null +++ b/src/js/stendhal/sprite/image/ImageManager.ts @@ -0,0 +1,60 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { FilteredImageRefImpl } from "./FilteredImageRefImpl"; +import { ImageRef } from "./ImageRef"; +import { ImageRefImpl } from "./ImageRefImpl"; + +export class ImageManager { + + images = new Map(); + + load(filename: string, filter?: string, param?: number): ImageRef { + if (filename.indexOf("..") > -1) { + throw new Error("Invalid filename: " + filename); + } + let key = filename + "!" + filter + "!" + param; + let imageRef = this.images.get(key); + if (!imageRef) { + if (filter) { + imageRef = new FilteredImageRefImpl(filename, filter, param); + } else { + imageRef = new ImageRefImpl(filename); + } + imageRef.load(); + this.images.set(key, imageRef); + } + imageRef.use(); + return imageRef; + } + + free(filename: string, filter?: string, param?: number) { + let key = filename + "!" + filter + "!" + param; + let imageRef = this.images.get(key); + if (!imageRef) { + console.error("freeing unknown image: " + key); + return; + } + imageRef.free(); + } + + cleanup(olderThan: Date) { + for (let [key, imageRef] of this.images) { + if (imageRef.shouldCleanup(olderThan)) { + imageRef.close(); + this.images.delete(key); + } + } + } + +} + +export const images = /* @__PURE__ */ new ImageManager(); diff --git a/src/js/stendhal/sprite/action/ActionSprite.ts b/src/js/stendhal/sprite/image/ImageMetadata.ts similarity index 64% rename from src/js/stendhal/sprite/action/ActionSprite.ts rename to src/js/stendhal/sprite/image/ImageMetadata.ts index 590c5ce75b..fd04ad9500 100644 --- a/src/js/stendhal/sprite/action/ActionSprite.ts +++ b/src/js/stendhal/sprite/image/ImageMetadata.ts @@ -1,5 +1,6 @@ /*************************************************************************** - * Copyright © 2023 - Stendhal * + * Copyright © 2026-2026 - Faiumoni e. V. * + *************************************************************************** *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,20 +10,7 @@ * * ***************************************************************************/ +export class ImageMetadata { -export abstract class ActionSprite { - - protected readonly initTime: number; - - - constructor() { - this.initTime = Date.now(); - } - - public abstract draw(ctx: CanvasRenderingContext2D, x: number, y: number, entityWidth: number, - entityHeight: number): void; - - public expired(): boolean { - return Date.now() - this.initTime > 180; - } -} + constructor(public filename: string, public filter?: string, public color?: number) {} +} \ No newline at end of file diff --git a/src/js/stendhal/main.js b/src/js/stendhal/sprite/image/ImageRef.ts similarity index 63% rename from src/js/stendhal/main.js rename to src/js/stendhal/sprite/image/ImageRef.ts index bf0ee35123..ef41b2a7fe 100644 --- a/src/js/stendhal/main.js +++ b/src/js/stendhal/sprite/image/ImageRef.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2023 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,14 +9,20 @@ * * ***************************************************************************/ -"use strict"; +import { RenderingContext2D } from "util/Types"; -var marauroa = window.marauroa = window.marauroa || {}; -var stendhal = window.stendhal = window.stendhal || {}; +export abstract class ImageRef { + image?: ImageBitmap; + async /*abstract */ waitFor(): Promise { + return new Promise(() => {}); + } -stendhal.main = require("../../../build/ts/Client").Client.get(); -stendhal.main.init(); + drawOnto(ctx: RenderingContext2D, x: number, y: number) { + if (this.image) { + ctx.drawImage(this.image, x, y); + } + } -document.addEventListener('DOMContentLoaded', stendhal.main.startup); -window.addEventListener('error', stendhal.main.onerror); + abstract free(): void; +} diff --git a/src/js/stendhal/sprite/image/ImageRefImpl.ts b/src/js/stendhal/sprite/image/ImageRefImpl.ts new file mode 100644 index 0000000000..a39b7599ea --- /dev/null +++ b/src/js/stendhal/sprite/image/ImageRefImpl.ts @@ -0,0 +1,91 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { stendhal } from "stendhal"; +import { ImageRef } from "./ImageRef"; + +export class ImageRefImpl extends ImageRef { + private refCount = 0; + private lastFreed?: Date; + private closed = false; + private loaded: Promise; + protected promiseResolve!: Function; + + constructor(protected filename: string) { + super(); + this.loaded = new Promise((resolve) => { + this.promiseResolve = resolve; + }) + } + + async load() { + let url = this.filename + "?v=" + stendhal.data.build.version; + let response = await fetch(url); + if (!response.ok || this.closed) { + this.promiseResolve(undefined) + return; + } + let blob = await response.blob(); + if (this.closed) { + this.promiseResolve(undefined) + return; + } + let bitmap = await createImageBitmap(blob); + if (this.closed) { + bitmap.close(); + this.promiseResolve(undefined) + return; + } + this.image = bitmap; + this.promiseResolve(bitmap); + } + + /** + * called internally by ImageManager + */ + use() { + this.refCount++; + this.lastFreed = undefined; + } + + /** + * called internally by ImageManager + */ + override free() { + this.refCount--; + if (this.refCount < 0) { + console.error("Negative reference count", this); + } + if (this.refCount <= 0) { + this.lastFreed = new Date(); + } + } + + shouldCleanup(olderThan: Date) { + if (this.refCount > 0 || !this.lastFreed) { + return false; + } + return (this.lastFreed < olderThan); + } + + /** + * called internally by ImageManager + */ + close() { + this.closed = true; + this.image?.close(); + this.image = undefined; + } + + override async waitFor(): Promise { + return this.loaded + } +} diff --git a/src/js/stendhal/sprite/image/ImageSprite.ts b/src/js/stendhal/sprite/image/ImageSprite.ts new file mode 100644 index 0000000000..0b79292b26 --- /dev/null +++ b/src/js/stendhal/sprite/image/ImageSprite.ts @@ -0,0 +1,60 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { RenderingContext2D } from "util/Types"; +import { ImageRef } from "./ImageRef"; + + +export class ImageSprite { + + constructor( + public imageRef: ImageRef, + public offsetX?: number, + public offsetY?: number, + public width?: number, + public height?: number + ) { } + + drawOnto(ctx: RenderingContext2D, x: number, y: number, entityWidth: number, entityHeight: number) { + let image = this.imageRef.image; + if (!image) { + return; + } + let offsetX = this.offsetX || 0; + let offsetY = this.offsetY || 0; + let width = this.width || image.width; + let height = this.height || image.height; + + // use entity dimensions to center sprite + x += Math.floor((entityWidth - width) / 2); + y += Math.floor((entityHeight - height) / 2); + + ctx.drawImage(image, offsetX, offsetY, width, height, x, y, width, height); + } + + drawOntoWithZoom(ctx: RenderingContext2D, targetX: number, targetY: number, targetWidth: number, targetHeight: number) { + let image = this.imageRef.image; + if (!image) { + return; + } + let offsetX = this.offsetX || 0; + let offsetY = this.offsetY || 0; + let width = this.width || image.width; + let height = this.height || image.height; + + ctx.drawImage(image, offsetX, offsetY, width, height, targetX, targetY, targetWidth, targetHeight); + } + + public free() { + this.imageRef.free(); + } + +} diff --git a/src/js/stendhal/sprite/image/MultilayerImageLoader.ts b/src/js/stendhal/sprite/image/MultilayerImageLoader.ts new file mode 100644 index 0000000000..54de7cc0fa --- /dev/null +++ b/src/js/stendhal/sprite/image/MultilayerImageLoader.ts @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright © 2024-2026 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { ImageMetadata } from "./ImageMetadata"; +import { images } from "./ImageManager"; +import { ImageRef } from "./ImageRef"; + +export class MultilayerImageLoader { + private imageRefs: ImageRef[] = []; + + constructor(private imageMetadatas: ImageMetadata[]) { + } + + public async load() { + for (let imageMetadata of this.imageMetadatas) { + this.imageRefs.push(images.load(imageMetadata.filename, imageMetadata.filter, imageMetadata.color)); + } + + for (let imageRef of this.imageRefs) { + await imageRef.waitFor(); + } + + let canvas: OffscreenCanvas|undefined = undefined; + let ctx: OffscreenCanvasRenderingContext2D|undefined = undefined; + for (let imageRef of this.imageRefs) { + if (imageRef.image) { + if (!canvas) { + canvas = new OffscreenCanvas(imageRef.image.width, imageRef.image.height); + ctx = canvas.getContext("2d")!; + } + ctx?.drawImage(imageRef.image, 0, 0); + imageRef.free(); + } + } + if (!canvas) { + canvas = new OffscreenCanvas(0, 0); + } + return canvas.transferToImageBitmap(); + } + +} diff --git a/src/js/stendhal/sprite/indicator/StatusIconDrawer.ts b/src/js/stendhal/sprite/indicator/StatusIconDrawer.ts new file mode 100644 index 0000000000..03e0dee4f6 --- /dev/null +++ b/src/js/stendhal/sprite/indicator/StatusIconDrawer.ts @@ -0,0 +1,110 @@ +/*************************************************************************** + * (C) Copyright 2003-2026 - Stendhal * + *************************************************************************** + * * + * entity program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { htmlImageStore } from "data/HTMLImageStore"; +import { Paths } from "data/Paths"; +import { RPEntity } from "entity/RPEntity"; +import { images } from "sprite/image/ImageManager"; +import { ImageRef } from "sprite/image/ImageRef"; +import { RenderingContext2D } from "util/Types"; + +export class StatusIconDrawer { + + private icons: Record = {}; + + private load(filename: string): ImageRef { + let imageRef = this.icons[filename]; + if (!imageRef) { + imageRef = images.load(filename); + this.icons[filename] = imageRef; + } + return imageRef; + } + + private drawAnimatedIcon(ctx: RenderingContext2D, iconPath: string, delay: number, x: number, y: number, fWidth?: number) { + let icon = this.load(iconPath).image; + if (!icon) { + return; + } + let dimH = icon.height; + let dimW = fWidth || dimH; + let nFrames = icon.width / dimW; + let frame = Math.floor(Date.now() / delay) % nFrames; + ctx.drawImage(icon, frame * dimW, 0, dimW, dimH, x, y, dimW, dimH); + } + + private drawAnimatedIconWithFrames(ctx: RenderingContext2D, iconPath: string, nFrames: number, delay: number, x: number, y: number) { + let icon = this.load(iconPath).image; + if (!icon) { + return; + } + let ydim = icon.height; + let xdim = icon.width / nFrames; + let frame = Math.floor(Date.now() / delay) % nFrames; + ctx.drawImage(icon, frame * xdim, 0, xdim, ydim, x, y, xdim, ydim); + } + + public drawStatusIcons(entity: RPEntity, ctx: RenderingContext2D) { + let x = entity["_x"] * 32 - 10; + let y = (entity["_y"] + 1) * 32; + if (entity.hasOwnProperty("choking")) { + this.load(Paths.sprites + "/ideas/choking.png").drawOnto(ctx, x, y - 10); + } else if (entity.hasOwnProperty("eating")) { + this.load(Paths.sprites + "/ideas/eat.png").drawOnto(ctx, x, y - 10); + } + // NPC and pet idea icons + if (entity.hasOwnProperty("idea")) { + const idea = Paths.sprites + "/ideas/" + entity["idea"] + ".png"; + const ani = htmlImageStore.animations.idea[entity["idea"]]; + if (ani) { + this.drawAnimatedIcon(ctx, idea, ani.delay, x + ani.offsetX * entity["width"], + y - entity["drawHeight"] + ani.offsetY); + } else { + this.load(idea).drawOnto(ctx, x + 32 * entity["width"], y - entity["drawHeight"]); + } + } + if (entity.hasOwnProperty("away")) { + this.drawAnimatedIcon(ctx, Paths.sprites + "/ideas/away.png", 1500, x + 32 * entity["width"], y - entity["drawHeight"]); + } + if (entity.hasOwnProperty("grumpy")) { + this.drawAnimatedIcon(ctx, Paths.sprites + "/ideas/grumpy.png", 1000, x + 5, y - entity["drawHeight"]); + } + if (entity.hasOwnProperty("last_player_kill_time")) { + this.drawAnimatedIconWithFrames(ctx, Paths.sprites + "/ideas/pk.png", 12, 300, x, y - entity["drawHeight"]); + } + // status affects + if (entity.hasOwnProperty("poisoned")) { + this.drawAnimatedIcon(ctx, Paths.sprites + "/status/poison.png", 100, x + 32 * entity["width"] - 10, y - entity["drawHeight"]); + } + if (entity.hasOwnProperty("status_confuse")) { + this.drawAnimatedIcon(ctx, Paths.sprites + "/status/confuse.png", 200, x + 32 * entity["width"] - 14, y - entity["drawHeight"] + 16); + } + if (entity.hasOwnProperty("status_shock")) { + this.drawAnimatedIcon(ctx, Paths.sprites + "/status/shock.png", 200, x + 32 * entity["width"] - 25, y - 32, 38); + } + // NPC job icons + let nextX = x; + if (entity.hasOwnProperty("job_healer")) { + this.load(Paths.sprites + "/status/healer.png").drawOnto(ctx, nextX, y - 10); + nextX += 12; + } + if (entity.hasOwnProperty("job_merchant")) { + this.load(Paths.sprites + "/status/merchant.png").drawOnto(ctx, nextX, y - 10); + nextX += 12; + } + if (entity.hasOwnProperty("job_producer")) { + this.load(Paths.sprites + "/status/producer.png").drawOnto(ctx, nextX, y - 16); + nextX += 16; + } + } +} + +export const statusIconDrawer = /* @__PURE__ */ new StatusIconDrawer(); \ No newline at end of file diff --git a/src/js/stendhal/stendhal.ts b/src/js/stendhal/stendhal.ts new file mode 100644 index 0000000000..743cc4d6f3 --- /dev/null +++ b/src/js/stendhal/stendhal.ts @@ -0,0 +1,14 @@ +/** + * this class helps with refactoring from JavaScript to TypeScript. + */ +export class stendhal { + static config: any; + static ui: any; + static sound: any; + static zone: any; + static data: any; + static session: any; + static main: any; + static actions: any; + static players: any +} diff --git a/src/js/stendhal/tsconfig.json b/src/js/stendhal/tsconfig.json index 03ff0b466b..df8edfa5e4 100644 --- a/src/js/stendhal/tsconfig.json +++ b/src/js/stendhal/tsconfig.json @@ -1,14 +1,15 @@ { "compilerOptions": { "baseUrl": ".", - "lib": ["dom", "es2015"], - "module": "commonjs", + "lib": ["dom", "es2022"], + "module": "es2020", + "moduleResolution": "bundler", "noImplicitOverride": true, "outDir": "../../../build/ts", "rootDir": ".", "sourceMap": true, "strict": true, - "target": "es6" + "target": "es2022" }, "typedocOptions": { "entryPoints": ["."], diff --git a/src/js/stendhal/ui/ChatPanel.ts b/src/js/stendhal/ui/ChatPanel.ts index 147ebd3d50..1b06fb7ed7 100644 --- a/src/js/stendhal/ui/ChatPanel.ts +++ b/src/js/stendhal/ui/ChatPanel.ts @@ -16,6 +16,7 @@ import { QuickMenu } from "./quickmenu/QuickMenu"; import { QuickMenuButton } from "./quickmenu/QuickMenuButton"; import { Panel } from "./toolkit/Panel"; import { singletons } from "../SingletonRepo"; +import { ViewPort } from "./ViewPort"; /** @@ -68,7 +69,7 @@ export class ChatPanel extends Panel { public override refresh() { const floating = this.isFloating(); if (floating) { - const rect = singletons.getViewPort().getElement().getBoundingClientRect(); + const rect = ViewPort.get().getElement().getBoundingClientRect(); const halfHeight = Math.abs(rect.height / 2); this.componentElement.style["width"] = rect.width + "px"; this.componentElement.style["height"] = halfHeight + "px"; @@ -88,7 +89,7 @@ export class ChatPanel extends Panel { QuickMenu.setButtonEnabled(UIComponentEnum.QMChat, floating); // adapt viewport layout - singletons.getViewPort().onChatPanelRefresh(floating); + ViewPort.get().onChatPanelRefresh(floating); } /** diff --git a/src/js/stendhal/ui/HTMLManager.ts b/src/js/stendhal/ui/HTMLUtil.ts similarity index 86% rename from src/js/stendhal/ui/HTMLManager.ts rename to src/js/stendhal/ui/HTMLUtil.ts index 18b08acc0f..d421b00278 100644 --- a/src/js/stendhal/ui/HTMLManager.ts +++ b/src/js/stendhal/ui/HTMLUtil.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,27 +9,11 @@ * * ***************************************************************************/ -declare var stendhal: any; - /** * HTML code manipulation. */ -export class HTMLManager { - - /** Singleton instance. */ - private static instance: HTMLManager; - - - /** - * Retrieves singleton instance. - */ - static get(): HTMLManager { - if (!HTMLManager.instance) { - HTMLManager.instance = new HTMLManager(); - } - return HTMLManager.instance; - } +export class HTMLUtil { /** * Hidden singleton constructor. @@ -38,7 +22,7 @@ export class HTMLManager { // do nothing } - esc(msg: string, filter=[]) { + public static esc(msg: string, filter: string[]=[]) { msg = msg.replace(//g, '>').replace(/"/g, '"').replace(/\n/g, "
"); // restore filtered tags for (const tag of filter) { @@ -49,7 +33,7 @@ export class HTMLManager { return msg; } - niceName(s: string): string { + public static niceName(s: string): string { if (!s) { return ""; } @@ -65,7 +49,7 @@ export class HTMLManager { * @return {EventTarget} * Translated event target. */ - extractTarget(event: any): EventTarget { + public static extractTarget(event: any): EventTarget { if (event.changedTouches) { // FIXME: Always uses last index. Any way to detect which touch index was engaged? const tidx = event.changedTouches.length - 1; @@ -90,9 +74,9 @@ export class HTMLManager { * @return {any} * Normalized event. */ - extractPosition(event: any): any { + public static extractPosition(event: any): any { let pos = event; - const target = this.extractTarget(event); + const target = HTMLUtil.extractTarget(event); const canvas = target as HTMLCanvasElement; if (event.changedTouches) { // FIXME: Always uses last index. Any way to detect which touch index was engaged? @@ -119,7 +103,7 @@ export class HTMLManager { return pos; } - formatTallyMarks(line: string): any { + public static formatTallyMarks(line: string): any { let tmp = line.split(""); const pre = tmp[0]; tmp = tmp[1].split(""); @@ -159,7 +143,7 @@ export class HTMLManager { * @return {string} * Slot name. */ - parseSlotName(id: string): string { + public static parseSlotName(id: string): string { if (id.includes("-")) { return id.split("-")[0]; } diff --git a/src/js/stendhal/ui/HeldObject.ts b/src/js/stendhal/ui/HeldObject.ts index f2d521946d..158741b7f5 100644 --- a/src/js/stendhal/ui/HeldObject.ts +++ b/src/js/stendhal/ui/HeldObject.ts @@ -10,9 +10,12 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { Entity } from "entity/Entity"; +import { stendhal } from "../stendhal"; import { Point } from "../util/Point"; +import { HTMLUtil } from "./HTMLUtil"; +import { ViewPort } from "./ViewPort"; /** @@ -34,12 +37,14 @@ export interface HeldObject { */ export class HeldObjectManager { - /** Image displayed when an object is "held". */ - private readonly image: HTMLImageElement; - /** Singleton instance. */ private static instance: HeldObjectManager; + /** Image displayed when an object is "held". */ + private image: HTMLCanvasElement|HTMLImageElement; + + public heldObject?: HeldObject; + /** * Retrieves singleton instance. @@ -58,6 +63,28 @@ export class HeldObjectManager { this.image = document.getElementById("held-object")! as HTMLImageElement; } + public prepare(draggedEntity: Entity, e: Event) { + let image = draggedEntity?.imageSprite?.imageRef.image; + if (!draggedEntity || !(draggedEntity.type === "item" || draggedEntity.type === "corpse") || !image) { + e.preventDefault(); + return false; + } + let canvas = document.getElementById("held-object") as HTMLCanvasElement; + let sprite = draggedEntity.imageSprite!; + canvas.style.display = "block"; + this.setPosition(-1000, -1000); + canvas.width = sprite.width!; + canvas.height = sprite.height!; + let ctx = canvas.getContext("2d")!; + ctx.clearRect(0, 0, sprite.width!, sprite.height!); + sprite.drawOntoWithZoom(ctx, 0, 0, 32, 32); + + if (e instanceof DragEvent) { + e.dataTransfer?.setDragImage(canvas, 0, 0); + } + return true; + } + /** * Sets the image's visibility. * @@ -80,9 +107,8 @@ export class HeldObjectManager { * @param pos {util.Point.Point} * Initial position of displayed image. */ - public set(obj: HeldObject, img: HTMLImageElement|string, pos?: Point) { - stendhal.ui.heldObject = obj; - this.setImage(img); + public set(obj: HeldObject, pos?: Point) { + this.heldObject = obj; this.onSet(pos); } @@ -96,7 +122,7 @@ export class HeldObjectManager { if (pos) { this.setPosition(pos.x, pos.y); } else { - const rect = stendhal.ui.gamewindow.getElement().getBoundingClientRect(); + const rect = ViewPort.get().getElement().getBoundingClientRect(); this.setPosition(rect.left, rect.top); } this.setVisible(true); @@ -109,26 +135,10 @@ export class HeldObjectManager { * Hides object image. */ public onRelease() { - // NOTE: should we unset `stendhal.ui.heldObject` here? + // NOTE: should we unset `heldObject` here? this.setVisible(false); } - /** - * Sets or unsets object image. - * - * @param img {HTMLImageElement|string} - * Image element or path to image. - */ - private setImage(img?: HTMLImageElement|string) { - if (!img) { - this.image.src = ""; - } else if (typeof(img) === "string") { - this.image.src = img; - } else { - this.image.src = img.src; - } - } - /** * Updates position of displayed image. * @@ -146,7 +156,7 @@ export class HeldObjectManager { * Handles event to update held object drawing position. */ private onDragWhileHeld(e: Event) { - const pos = stendhal.ui.html.extractPosition(e); + const pos = HTMLUtil.extractPosition(e); HeldObjectManager.get().setPosition(pos.pageX, pos.pageY); } diff --git a/src/js/stendhal/ui/Inventory.ts b/src/js/stendhal/ui/Inventory.ts index 90163cf7e0..4ea876aadd 100644 --- a/src/js/stendhal/ui/Inventory.ts +++ b/src/js/stendhal/ui/Inventory.ts @@ -9,7 +9,7 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { ItemContainerImplementation } from "./component/ItemContainerImplementation"; diff --git a/src/js/stendhal/ui/TouchHandler.ts b/src/js/stendhal/ui/TouchHandler.ts index b64a7177fa..87c6f3db56 100644 --- a/src/js/stendhal/ui/TouchHandler.ts +++ b/src/js/stendhal/ui/TouchHandler.ts @@ -9,9 +9,10 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../stendhal"; import { Point } from "../util/Point"; +import { HTMLUtil } from "./HTMLUtil"; /** @@ -36,9 +37,6 @@ export class TouchHandler { /** Number of pixels touch can move before being vetoed as a long touch or double tap. */ private readonly moveThreshold = 16; - /** Property for debugging. */ - private debugging = false; - /** Singleton instance. */ private static instance: TouchHandler; @@ -120,7 +118,7 @@ export class TouchHandler { const durationMatch = (this.timestampTouchEnd - this.timestampTouchStart > this.longTouchDuration); let positionMatch = true; if (evt && this.origin) { - const pos = stendhal.ui.html.extractPosition(evt); + const pos = HTMLUtil.extractPosition(evt); // if position has moved too much it's not a long touch positionMatch = (Math.abs(pos.pageX - this.origin.x) <= this.moveThreshold) && (Math.abs(pos.pageY - this.origin.y) <= this.moveThreshold); @@ -148,18 +146,4 @@ export class TouchHandler { holding(): boolean { return this.held; } - - /** - * Sets debugging property for touch events. - */ - setDebuggingEnabled(enable: boolean) { - this.debugging = enable; - } - - /** - * Checks if debugging is enabled for touch events. - */ - isDebuggingEnabled(): boolean { - return this.debugging; - } } diff --git a/src/js/stendhal/ui/UI.ts b/src/js/stendhal/ui/UI.ts index 391fd42162..a0e77d5260 100644 --- a/src/js/stendhal/ui/UI.ts +++ b/src/js/stendhal/ui/UI.ts @@ -9,8 +9,8 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../stendhal"; import { UIComponentEnum } from "./UIComponentEnum"; import { SoftwareJoystickController } from "./SoftwareJoystickController"; @@ -166,7 +166,7 @@ class UI { * Instructions to execute when menu style changes. */ public onMenuUpdate() { - switch (stendhal.config.get("menu.style")) { + switch (stendhal.ui.getMenuStyle()) { case "traditional": document.getElementById("menupanel")!.style["display"] = ""; QuickMenu.setVisible(false); diff --git a/src/js/stendhal/ui/UIComponentEnum.ts b/src/js/stendhal/ui/UIComponentEnum.ts index 454b1a14bd..af97f7765b 100644 --- a/src/js/stendhal/ui/UIComponentEnum.ts +++ b/src/js/stendhal/ui/UIComponentEnum.ts @@ -46,5 +46,6 @@ export class UIComponentEnum { // Quick menu public static readonly QMChat = 301; public static readonly QMJoystick = 302; - public static readonly QMSound = 303; + public static readonly QMPathFinding = 303; + public static readonly QMSound = 304; } diff --git a/src/js/stendhal/ui/ViewPort.ts b/src/js/stendhal/ui/ViewPort.ts index 7a65ed129d..fba92cd20d 100644 --- a/src/js/stendhal/ui/ViewPort.ts +++ b/src/js/stendhal/ui/ViewPort.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Stendhal * + * (C) Copyright 2003-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,10 +9,10 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa"; +import { stendhal } from "../stendhal"; -import { HeldObject } from "./HeldObject"; +import { HeldObject, HeldObjectManager } from "./HeldObject"; import { ui } from "./UI"; import { UIComponentEnum } from "./UIComponentEnum"; @@ -22,7 +22,6 @@ import { ActionContextMenu } from "./dialog/ActionContextMenu"; import { DropQuantitySelectorDialog } from "./dialog/DropQuantitySelectorDialog"; import { Client } from "../Client"; -import { singletons } from "../SingletonRepo"; import { AchievementBanner } from "../sprite/AchievementBanner"; import { EmojiSprite } from "../sprite/EmojiSprite"; @@ -30,7 +29,14 @@ import { NotificationBubble } from "../sprite/NotificationBubble"; import { SpeechBubble } from "../sprite/SpeechBubble"; import { TextBubble } from "../sprite/TextBubble"; +import { TileMap } from "data/TileMap"; +import { Zone } from "entity/Zone"; +import { Canvas, RenderingContext2D } from "util/Types"; +import { WeatherRenderer } from "util/WeatherRenderer"; +import { Debug } from "../util/Debug"; import { Point } from "../util/Point"; +import { HTMLUtil } from "./HTMLUtil"; +import { TouchHandler } from "./TouchHandler"; /** @@ -39,41 +45,42 @@ import { Point } from "../util/Point"; export class ViewPort { /** Horizontal screen offset in pixels. */ - private offsetX = 0; + public offsetX = 0; /** Vertical screen offset in pixels. */ - private offsetY = 0; + public offsetY = 0; /** Prevents adjusting offset based on player position. */ - private freeze = false; + public freeze = false; /** Time of most recent redraw. */ private timeStamp = Date.now(); // dimensions // TODO: remove & use CSS style instead - private readonly width: number; - private readonly height: number; + public readonly width: number; + public readonly height: number; /** Drawing context. */ - private ctx: CanvasRenderingContext2D; + private ctx: RenderingContext2D; /** Map tile pixel width. */ - private readonly targetTileWidth = 32; + public readonly targetTileWidth = 32; /** Map tile pixel height. */ - private readonly targetTileHeight = 32; + public readonly targetTileHeight = 32; private drawingError = false; /** Active speech bubbles to draw. */ - private textSprites: SpeechBubble[] = []; + public textSprites: SpeechBubble[] = []; /** Active notification bubbles/achievement banners to draw. */ private notifSprites: TextBubble[] = []; /** Active emoji sprites to draw. */ private emojiSprites: EmojiSprite[] = []; /** Handles drawing weather in viewport. */ - private weatherRenderer = singletons.getWeatherRenderer(); + private weatherRenderer = WeatherRenderer.get(); /** Coloring method of current zone. */ - private filter?: string; // deprecated, use `HSLFilter` + public filter?: string; // deprecated, use `HSLFilter` /** Coloring filter of current zone. */ - private HSLFilter?: string; + public HSLFilter?: string; private colorMethod = ""; private blendMethod = ""; // FIXME: unused + private map: TileMap; /** Styles to be applied when chat panel is not floating. */ private readonly initialStyle: {[prop: string]: string}; @@ -81,7 +88,6 @@ export class ViewPort { /** Singleton instance. */ private static instance: ViewPort; - /** * Retrieves singleton instance. */ @@ -96,6 +102,7 @@ export class ViewPort { * Hidden singleton constructor. */ private constructor() { + this.map = TileMap.get(); const element = this.getElement() as HTMLCanvasElement; this.ctx = element.getContext("2d")!; this.width = element.width; @@ -114,11 +121,11 @@ export class ViewPort { /** * Retrieves the viewport element. * - * @return {HTMLElement} + * @return {HTMLCanvasElement} * Viewport `HTMLElement`. */ - public getElement(): HTMLElement { - return document.getElementById("viewport")!; + public getElement(): HTMLCanvasElement { + return document.getElementById("viewport")! as HTMLCanvasElement; } /** @@ -128,10 +135,10 @@ export class ViewPort { var startTime = new Date().getTime(); if (marauroa.me && document.visibilityState === "visible") { - if (marauroa.currentZoneName === stendhal.data.map.currentZoneName - || stendhal.data.map.currentZoneName === "int_vault" - || stendhal.data.map.currentZoneName === "int_adventure_island" - || stendhal.data.map.currentZoneName === "tutorial_island") { + if (marauroa.currentZoneName === this.map.currentZoneName + || this.map.currentZoneName === "int_vault" + || this.map.currentZoneName === "int_adventure_island" + || this.map.currentZoneName === "tutorial_island") { this.drawingError = false; this.ctx.globalAlpha = 1.0; @@ -144,11 +151,14 @@ export class ViewPort { // FIXME: filter should not be applied to "blend" layers //this.applyFilter(); - stendhal.data.map.strategy.render(this.ctx.canvas, this, tileOffsetX, tileOffsetY, this.targetTileWidth, this.targetTileHeight); + this.map.parallax.draw(this.ctx, this.offsetX, this.offsetY); + this.map.strategy.render(this.ctx.canvas, this, tileOffsetX, tileOffsetY, this.targetTileWidth, this.targetTileHeight); this.weatherRenderer.draw(this.ctx); //this.removeFilter(); - this.applyHSLFilter(); + if (!Debug.isActive("light")) { + this.applyHSLFilter(); + } this.drawEntitiesTop(); this.drawEmojiSprites(); this.drawTextSprites(); @@ -159,8 +169,8 @@ export class ViewPort { (ui.get(UIComponentEnum.PlayerEquipment) as PlayerEquipmentComponent).update(); } } - window.setTimeout(function() { - stendhal.ui.gamewindow.draw.apply(stendhal.ui.gamewindow, arguments); + window.setTimeout(() => { + this.draw(); }, Math.max((1000/20) - (new Date().getTime()-startTime), 1)); } @@ -193,13 +203,10 @@ export class ViewPort { * Add coloring filter to viewport. */ private applyHSLFilter() { - if (!this.HSLFilter || !stendhal.config.getBoolean("effect.lighting")) { + if (!this.HSLFilter) { return; } this.ctx.save(); - // FIXME: is this the appropriate alpha level to use? "color_method" value from server doesn't - // appear to include alpha information - this.ctx.globalAlpha = 0.5; this.ctx.globalCompositeOperation = (this.colorMethod || this.ctx.globalCompositeOperation) as GlobalCompositeOperation; this.ctx.fillStyle = this.HSLFilter; this.ctx.fillRect(this.offsetX, this.offsetY, this.ctx.canvas.width, this.ctx.canvas.height); @@ -261,11 +268,10 @@ export class ViewPort { * Draws overall entity sprites. */ drawEntities() { - var currentTime = new Date().getTime(); - var time = currentTime - this.timeStamp; + let currentTime = new Date().getTime(); + let time = currentTime - this.timeStamp; this.timeStamp = currentTime; - for (var i in stendhal.zone.entities) { - var entity = stendhal.zone.entities[i]; + for (let entity of Zone.get().entities) { if (typeof(entity.draw) != "undefined") { entity.updatePosition(time); entity.draw(this.ctx); @@ -277,9 +283,7 @@ export class ViewPort { * Draws titles & HP bars associated with entities. */ drawEntitiesTop() { - var i; - for (i in stendhal.zone.entities) { - const entity = stendhal.zone.entities[i]; + for (let entity of Zone.get().entities) { if (typeof(entity.setStatusBarOffset) !== "undefined") { entity.setStatusBarOffset(); } @@ -334,10 +338,10 @@ export class ViewPort { /** * Updates viewport drawing position of map based on player position. * - * @param canvas {HTMLCanvasElement} + * @param canvas {Canvas} * Viewport canvas element. */ - adjustView(canvas: HTMLCanvasElement) { + adjustView(canvas: Canvas) { // IE does not support ctx.resetTransform(), so use the following workaround: this.ctx.setTransform(1, 0, 0, 1, 0, 0); @@ -352,10 +356,10 @@ export class ViewPort { } // Keep the world within the screen view - centerX = Math.min(centerX, stendhal.data.map.zoneSizeX * this.targetTileWidth - canvas.width); + centerX = Math.min(centerX, this.map.zoneSizeX * this.targetTileWidth - canvas.width); centerX = Math.max(centerX, 0); - centerY = Math.min(centerY, stendhal.data.map.zoneSizeY * this.targetTileHeight - canvas.height); + centerY = Math.min(centerY, this.map.zoneSizeY * this.targetTileHeight - canvas.height); centerY = Math.max(centerY, 0); if (this.freeze) { @@ -491,13 +495,14 @@ export class ViewPort { const mHandle: any = {}; mHandle._onMouseDown = function(e: MouseEvent|TouchEvent) { - var pos = stendhal.ui.html.extractPosition(e); - if (stendhal.ui.touch.isTouchEvent(e)) { - if (stendhal.ui.touch.holding()) { + let touchHandler = TouchHandler.get(); + var pos = HTMLUtil.extractPosition(e); + if (touchHandler.isTouchEvent(e)) { + if (touchHandler.holding()) { // prevent default viewport action when item is "held" return; } - stendhal.ui.touch.onTouchStart(pos.pageX, pos.pageY); + touchHandler.onTouchStart(pos.pageX, pos.pageY); } if (stendhal.ui.globalpopup) { stendhal.ui.globalpopup.close(); @@ -505,24 +510,26 @@ export class ViewPort { startX = pos.canvasRelativeX; startY = pos.canvasRelativeY; + let viewPort = ViewPort.get(); - var x = pos.canvasRelativeX + stendhal.ui.gamewindow.offsetX; - var y = pos.canvasRelativeY + stendhal.ui.gamewindow.offsetY; + var x = pos.canvasRelativeX + viewPort.offsetX; + var y = pos.canvasRelativeY + viewPort.offsetY; // override ground/entity action if there is a text bubble - if (stendhal.ui.gamewindow.textBubbleAt(x, y+15)) { + if (viewPort.textBubbleAt(x, y+15)) { return; } - entity = stendhal.zone.entityAt(x, y); + let zone = Zone.get(); + entity = zone.entityAt(x, y); stendhal.ui.timestampMouseDown = +new Date(); if (e.type !== "dblclick" && e.target) { - e.target.addEventListener("mousemove", mHandle.onDrag); + e.target.addEventListener("mousemove", mHandle.onDrag, {passive: true}); e.target.addEventListener("mouseup", mHandle.onMouseUp); - e.target.addEventListener("touchmove", mHandle.onDrag); + e.target.addEventListener("touchmove", mHandle.onDrag, {passive: true}); e.target.addEventListener("touchend", mHandle.onMouseUp); - } else if (entity == stendhal.zone.ground) { + } else if (entity == zone.ground) { entity.onclick(pos.canvasRelativeX, pos.canvasRelativeY, true); } } @@ -539,14 +546,15 @@ export class ViewPort { } mHandle.onMouseUp = function(e: MouseEvent|TouchEvent) { - const is_touch = stendhal.ui.touch.isTouchEvent(e); + let touchHandler = TouchHandler.get(); + const is_touch = touchHandler.isTouchEvent(e); if (is_touch) { - stendhal.ui.touch.onTouchEnd(e); + touchHandler.onTouchEnd(); } - var pos = stendhal.ui.html.extractPosition(e); - const long_touch = is_touch && stendhal.ui.touch.isLongTouch(e); + var pos = HTMLUtil.extractPosition(e); + const long_touch = is_touch && touchHandler.isLongTouch(e); if ((e instanceof MouseEvent && mHandle.isRightClick(e)) || long_touch) { - if (entity != stendhal.zone.ground) { + if (entity != Zone.get().ground) { const append: any[] = []; /* if (long_touch) { @@ -565,11 +573,11 @@ export class ViewPort { } mHandle.onDrag = function(e: MouseEvent) { - if (stendhal.ui.touch.isTouchEvent(e)) { - stendhal.ui.gamewindow.onDragStart(e); + if (TouchHandler.get().isTouchEvent(e)) { + ViewPort.get().onDragStart(e); } - var pos = stendhal.ui.html.extractPosition(e); + var pos = HTMLUtil.extractPosition(e); var xDiff = startX - pos.offsetX; var yDiff = startY - pos.offsetY; // It's not really a click if the mouse has moved too much. @@ -596,11 +604,12 @@ export class ViewPort { * Updates cursor style when positioned over an element or entity. */ onMouseMove(e: MouseEvent) { - var pos = stendhal.ui.html.extractPosition(e); - var x = pos.canvasRelativeX + stendhal.ui.gamewindow.offsetX; - var y = pos.canvasRelativeY + stendhal.ui.gamewindow.offsetY; - var entity = stendhal.zone.entityAt(x, y); - stendhal.ui.gamewindow.getElement().style.cursor = entity.getCursor(x, y); + let viewPort = ViewPort.get(); + var pos = HTMLUtil.extractPosition(e); + var x = pos.canvasRelativeX + viewPort.offsetX; + var y = pos.canvasRelativeY + viewPort.offsetY; + var entity = Zone.get().entityAt(x, y); + viewPort.getElement().style.cursor = entity.getCursor(x, y); } /** @@ -639,47 +648,37 @@ export class ViewPort { /** * Handles engaging an item or corpse to be dragged. */ - onDragStart(e: DragEvent) { - var pos = stendhal.ui.html.extractPosition(e); + onDragStart(e: DragEvent|MouseEvent) { + var pos = HTMLUtil.extractPosition(e); let draggedEntity; - for (const obj of stendhal.zone.getEntitiesAt(pos.canvasRelativeX + stendhal.ui.gamewindow.offsetX, - pos.canvasRelativeY + stendhal.ui.gamewindow.offsetY)) { + let viewPort = ViewPort.get(); + let entries = Zone.get().getEntitiesAt( + pos.canvasRelativeX + viewPort.offsetX, + pos.canvasRelativeY + viewPort.offsetY); + for (const obj of entries) { if (obj.isDraggable()) { draggedEntity = obj; } } - var img = undefined; - let heldObject: HeldObject; - if (draggedEntity && draggedEntity.type === "item") { - img = stendhal.data.sprites.getAreaOf(stendhal.data.sprites.get(draggedEntity.sprite.filename), 32, 32); - heldObject = { - path: draggedEntity.getIdPath(), - zone: marauroa.currentZoneName, - quantity: draggedEntity.hasOwnProperty("quantity") ? draggedEntity["quantity"] : 1 - } - } else if (draggedEntity && draggedEntity.type === "corpse") { - img = stendhal.data.sprites.get(draggedEntity.sprite.filename); - heldObject = { - path: draggedEntity.getIdPath(), - zone: marauroa.currentZoneName, - quantity: 1 - } - } else { + if (!draggedEntity || !HeldObjectManager.get().prepare(draggedEntity, e)) { e.preventDefault(); return; } - if (stendhal.ui.touch.isTouchEvent(e)) { - singletons.getHeldObjectManager().set(heldObject, img, new Point(pos.pageX, pos.pageY)); - stendhal.ui.touch.setHolding(true); - } else { - stendhal.ui.heldObject = heldObject; + + let heldObject: HeldObject = { + path: draggedEntity.getIdPath(), + zone: marauroa.currentZoneName, + quantity: draggedEntity.hasOwnProperty("quantity") ? draggedEntity["quantity"] : 1 } - if (e.dataTransfer) { - window.event = e; // required by setDragImage polyfil - e.dataTransfer.setDragImage(img, 0, 0); + let touchHandler = TouchHandler.get(); + if (touchHandler.isTouchEvent(e)) { + HeldObjectManager.get().set(heldObject, new Point(pos.pageX, pos.pageY)); + touchHandler.setHolding(true); + } else { + HeldObjectManager.get().heldObject = heldObject; } } @@ -697,22 +696,24 @@ export class ViewPort { /** * Handles releasing an item or corpse from drag event. */ - onDrop(e: DragEvent) { - if (stendhal.ui.heldObject) { - var pos = stendhal.ui.html.extractPosition(e); - const targetSlot = stendhal.ui.html.parseSlotName((pos.target as HTMLElement).id); + onDrop(e: DragEvent|TouchEvent) { + let heldObject = HeldObjectManager.get().heldObject; + if (heldObject) { + var pos = HTMLUtil.extractPosition(e); + const targetSlot = HTMLUtil.parseSlotName((pos.target as HTMLElement).id); const action: any = { - "zone": stendhal.ui.heldObject.zone + "zone": heldObject.zone }; if (targetSlot === "viewport") { - action.x = Math.floor((pos.canvasRelativeX + stendhal.ui.gamewindow.offsetX) / 32).toString(); - action.y = Math.floor((pos.canvasRelativeY + stendhal.ui.gamewindow.offsetY) / 32).toString(); + let viewPort = ViewPort.get(); + action.x = Math.floor((pos.canvasRelativeX + viewPort.offsetX) / 32).toString(); + action.y = Math.floor((pos.canvasRelativeY + viewPort.offsetY) / 32).toString(); - var id = stendhal.ui.heldObject.path.substr(1, stendhal.ui.heldObject.path.length - 2); + var id = heldObject.path.substring(1, heldObject.path.length - 1); var drop = /\t/.test(id); if (drop) { action["type"] = "drop"; - action["source_path"] = stendhal.ui.heldObject.path; + action["source_path"] = heldObject.path; } else { action["type"] = "displace"; action["baseitem"] = id; @@ -721,28 +722,28 @@ export class ViewPort { let objectId = marauroa.me["id"]; if (e.type === "touchend" && targetSlot === "content") { // find the actual target ID for touch events - const container = stendhal.ui.equip.getByElement(stendhal.ui.html.extractTarget(event).parentElement!); + const container = stendhal.ui.equip.getByElement((HTMLUtil.extractTarget(event) as HTMLElement).parentElement!); if (container && container.object) { objectId = container.object.id; } } action["type"] = "equip"; - action["source_path"] = stendhal.ui.heldObject.path; + action["source_path"] = heldObject.path; action["target_path"] = "[" + objectId + "\t" + targetSlot + "]"; } - const quantity = stendhal.ui.heldObject.quantity; - const sourceSlot = stendhal.ui.heldObject.slot || "viewport"; + const quantity = heldObject.quantity; + const sourceSlot = heldObject.slot || "viewport"; // item was dropped - stendhal.ui.heldObject = undefined; + heldObject = undefined; - const touch_held = stendhal.ui.touch.holding() && quantity > 1; + const touch_held = TouchHandler.get().holding() && (quantity||1) > 1; // if ctrl is pressed or holding stackable item from touch event, we ask for the quantity // NOTE: don't create selector if touch source is ground if (e.ctrlKey || (touch_held && sourceSlot !== targetSlot)) { ui.createSingletonFloatingWindow("Quantity", new DropQuantitySelectorDialog(action, touch_held), pos.pageX - 50, pos.pageY - 25); } else { - singletons.getHeldObjectManager().onRelease(); + HeldObjectManager.get().onRelease(); marauroa.clientFramework.sendAction(action); } } @@ -755,11 +756,12 @@ export class ViewPort { * This is a workaround until it's figured out how to make it work using the same methods as mouse event. */ onTouchEnd(e: TouchEvent) { - stendhal.ui.touch.onTouchEnd(); - stendhal.ui.gamewindow.onDrop(e); - if (stendhal.ui.touch.holding()) { - stendhal.ui.touch.setHolding(false); - stendhal.ui.touch.unsetOrigin(); + let touchHandler = TouchHandler.get(); + touchHandler.onTouchEnd(); + ViewPort.get().onDrop(e); + if (touchHandler.holding()) { + touchHandler.setHolding(false); + touchHandler.unsetOrigin(); } // execute here because "touchend" event propagation is cancelled on the veiwport Client.handleClickIndicator(e); diff --git a/src/js/stendhal/ui/component/BagComponent.ts b/src/js/stendhal/ui/component/BagComponent.ts index fd1d9ff89d..2a42fd48d0 100644 --- a/src/js/stendhal/ui/component/BagComponent.ts +++ b/src/js/stendhal/ui/component/BagComponent.ts @@ -11,7 +11,7 @@ import { ItemInventoryComponent } from "./ItemInventoryComponent"; -declare var marauroa: any; +import { marauroa } from "marauroa" export class BagComponent extends ItemInventoryComponent { diff --git a/src/js/stendhal/ui/component/BuddyListComponent.ts b/src/js/stendhal/ui/component/BuddyListComponent.ts index 18ec0bd96e..bf6a20446b 100644 --- a/src/js/stendhal/ui/component/BuddyListComponent.ts +++ b/src/js/stendhal/ui/component/BuddyListComponent.ts @@ -16,10 +16,12 @@ import { Component } from "../toolkit/Component"; import { ActionContextMenu } from "../dialog/ActionContextMenu"; import { ChatInputComponent } from "./ChatInputComponent"; +import { Paths } from "../../data/Paths"; +import { stendhal } from "stendhal"; -declare let marauroa: any; -declare let stendhal: any; +import { marauroa } from "marauroa" +import { HTMLUtil } from "ui/HTMLUtil"; /** * displays the player stats @@ -66,11 +68,11 @@ export class BuddyListComponent extends Component { for (let i = 0; i < buddies.length; i++) { html += "
  • " + stendhal.ui.html.esc(buddies[i].name) + "
  • "; + html += "\"> " + HTMLUtil.esc(buddies[i].name) + ""; } if (this.lastHtml !== html) { diff --git a/src/js/stendhal/ui/component/ChatInputComponent.ts b/src/js/stendhal/ui/component/ChatInputComponent.ts index 22f2fd898d..83a23dbd19 100644 --- a/src/js/stendhal/ui/component/ChatInputComponent.ts +++ b/src/js/stendhal/ui/component/ChatInputComponent.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Faiumoni e. V. * + * (C) Copyright 2003-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,18 +9,19 @@ * * ***************************************************************************/ -declare let marauroa: any; -declare let stendhal: any; +import { marauroa } from "marauroa"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { stendhal } from "stendhal"; +import { Paths } from "../../data/Paths"; +import { ChatCompletionHelper } from "../../util/ChatCompletionHelper"; +import { KeyCode } from "../../util/KeyCode"; import { ChatPanel } from "../ChatPanel"; import { ui } from "../UI"; import { UIComponentEnum } from "../UIComponentEnum"; import { ChatOptionsDialog } from "../dialog/ChatOptionsDialog"; import { EmojiMapDialog } from "../dialog/EmojiMapDialog"; import { Component } from "../toolkit/Component"; -import { singletons } from "../../SingletonRepo"; -import { ChatCompletionHelper } from "../../util/ChatCompletionHelper"; -import { KeyCode } from "../../util/KeyCode"; /** @@ -86,7 +87,7 @@ export class ChatInputComponent extends Component { btn_emoji.innerText = "☺"; } else { // set image for emoji button - btn_emoji.appendChild(stendhal.data.sprites.get(stendhal.paths.sprites + "/emoji/smile.png").cloneNode()); + btn_emoji.appendChild(htmlImageStore.get(Paths.sprites + "/emoji/smile.png").cloneNode()); } } diff --git a/src/js/stendhal/ui/component/ChatLogComponent.ts b/src/js/stendhal/ui/component/ChatLogComponent.ts index 6f069570f4..94f661d1fc 100644 --- a/src/js/stendhal/ui/component/ChatLogComponent.ts +++ b/src/js/stendhal/ui/component/ChatLogComponent.ts @@ -14,8 +14,11 @@ import { Component } from "../toolkit/Component"; import { MenuItem } from "../../action/MenuItem"; import { UIComponentEnum } from "../UIComponentEnum"; import { singletons } from "../../SingletonRepo"; +import { Chat } from "../../util/Chat"; -declare var stendhal: any; +import { stendhal } from "../../stendhal"; +import { TouchHandler } from "ui/TouchHandler"; +import { HTMLUtil } from "ui/HTMLUtil"; /** @@ -33,19 +36,20 @@ export class ChatLogComponent extends Component { super("chat"); this.refresh(); this.scrollStateBottom = this.isAtBottom(); + let touchHandler = TouchHandler.get(); this.componentElement.addEventListener("mouseup", (evt: MouseEvent) => { this.onContextMenu(evt) }); this.componentElement.addEventListener("touchstart", (evt: TouchEvent) => { - const pos = stendhal.ui.html.extractPosition(evt); - stendhal.ui.touch.onTouchStart(pos.pageX, pos.pageY); - }); + const pos = HTMLUtil.extractPosition(evt); + touchHandler.onTouchStart(pos.pageX, pos.pageY); + }, {passive: true}); this.componentElement.addEventListener("touchend", (evt: TouchEvent) => { - stendhal.ui.touch.onTouchEnd(); + touchHandler.onTouchEnd(); this.onContextMenu(evt) // clean up - stendhal.ui.touch.unsetOrigin(); + touchHandler.unsetOrigin(); }); this.scrollListener = (evt: Event) => { @@ -106,13 +110,13 @@ export class ChatLogComponent extends Component { /** * Adds a line of text. * - * @param type {string} + * @param type * Message type. - * @param message {string} + * @param message * Text to be added. - * @param orator {string} + * @param orator * Name of entity making the expression (default: `undefined`). - * @param timestamp {boolean} + * @param timestamp * If `false`, suppresses prepending message with timestamp (default: `true`). */ public addLine(type: string, message: string, orator?: string, timestamp=true) { @@ -131,7 +135,7 @@ export class ChatLogComponent extends Component { const rcol = document.createElement("div"); rcol.className = "logcolR log" + type; - rcol.innerHTML += this.formatLogEntry(message); + rcol.innerHTML += Chat.formatLogEntry(message); const row = document.createElement("div"); row.className = "logrow"; @@ -192,122 +196,7 @@ export class ChatLogComponent extends Component { this.add(row); } - /** - * Formats displayed text. - * - * @param message {string} - * Text to parse for special characters. - * @return {string} - * Text formatted with keywords & item names highlighted. - */ - private formatLogEntry(message: string): string { - if (!message) { - return ""; - } - let res = ""; - let delims = [" ", ",", ".", "!", "?", ":", ";"]; - let length = message.length; - let inHighlight = false, inUnderline = false, - inHighlightQuote = false, inUnderlineQuote = false; - for (let i = 0; i < length; i++) { - let c = message[i]; - if (c === "\\") { - let n = message[i + 1]; - res += n; - i++; - - // Highlight Start? - } else if (c === "#") { - if (inHighlight) { - res += c; - continue; - } - let n = message[i + 1]; - if (n === "#") { - res += c; - i++; - continue; - } - if (n === "'") { - inHighlightQuote = true; - i++; - } - inHighlight = true; - res += ""; - - // Underline start? - } else if (c === "§") { - if (inUnderline) { - res += c; - continue; - } - let n = message[i + 1]; - if (n === "§") { - res += c; - i++; - continue; - } - if (n === "'") { - inUnderlineQuote = true; - i++; - } - inUnderline = true; - res += ""; - - // End Highlight and Underline? - } else if (c === "'") { - if (inUnderlineQuote) { - inUnderline = false; - inUnderlineQuote = false; - res += ""; - continue; - } - if (inHighlightQuote) { - inHighlight = false; - inHighlightQuote = false; - res += ""; - continue; - } - res += c; - - // HTML escape - } else if (c === "<") { - res += "<"; - - // End of word - } else if (delims.indexOf(c) > -1) { - let n = message[i + 1]; - if (c === " " || n === " " || n == undefined) { - if (inUnderline && !inUnderlineQuote && !inHighlightQuote) { - inUnderline = false; - res += "" + c; - continue; - } - if (inHighlight && !inUnderlineQuote && !inHighlightQuote) { - inHighlight = false; - res += "" + c; - continue; - } - } - res += c; - - // Normal characters - } else { - res += c; - } - } - - // Close opened formattings - if (inUnderline) { - res += ""; - } - if (inHighlight) { - res += ""; - } - - return res; - } /** * Removes all text from log. @@ -464,7 +353,7 @@ export class ChatLogComponent extends Component { if (evt.type === "mouseup" && evt.button != 2) { return; } - if (evt.type === "touchend" && !stendhal.ui.touch.isLongTouch(evt)) { + if (evt.type === "touchend" && !TouchHandler.get().isLongTouch(evt)) { evt.preventDefault(); return; } @@ -480,7 +369,7 @@ export class ChatLogComponent extends Component { options.unshift(new MenuItem("Copy", function() { log.exportContents(); })); } - const pos = stendhal.ui.html.extractPosition(evt); + const pos = HTMLUtil.extractPosition(evt); stendhal.ui.actionContextMenu.set(ui.createSingletonFloatingWindow("Action", new LogContextMenu(options), pos.pageX - 50, pos.pageY - 5)); @@ -503,9 +392,9 @@ class LogContextMenu extends Component { super("contextmenu-template", true); this.options = options; - let content = "
    "; + let content = "
    "; for (let i = 0; i < this.options.length; i++) { - content += "
    "; + content += ""; } content += "
    "; this.componentElement.innerHTML = content; diff --git a/src/js/stendhal/ui/component/GroupMemberComponent.ts b/src/js/stendhal/ui/component/GroupMemberComponent.ts index 6981457a2f..944f9bb4bc 100644 --- a/src/js/stendhal/ui/component/GroupMemberComponent.ts +++ b/src/js/stendhal/ui/component/GroupMemberComponent.ts @@ -9,8 +9,7 @@ * * ***************************************************************************/ -declare let marauroa: any; -declare let stendhal: any; +import { marauroa } from "marauroa" import { ChatInputComponent } from "./ChatInputComponent"; @@ -24,6 +23,8 @@ import { StatBarComponent } from "../component/StatBarComponent"; import { ActionContextMenu } from "../dialog/ActionContextMenu"; import { singletons } from "../../SingletonRepo"; +import { stendhal } from "stendhal"; +import { Zone } from "entity/Zone"; /** @@ -45,7 +46,7 @@ export class GroupMemberComponent extends Component { this.hpBar = new StatBarComponent(); this.hpBar.componentElement.classList.add("group-member-hpbar"); // check for player to initialize HP bar value - const player = singletons.getZone().findPlayer(memberName); + const player = Zone.get().findPlayer(memberName); if (player) { // component hasn't been created yet so `Player.updateGroupStatus` will fail here this.hpBar.draw(player["hp"] / player["base_hp"]); diff --git a/src/js/stendhal/ui/component/GroupPanelComponent.ts b/src/js/stendhal/ui/component/GroupPanelComponent.ts index 64cb587ff8..84d7dec43d 100644 --- a/src/js/stendhal/ui/component/GroupPanelComponent.ts +++ b/src/js/stendhal/ui/component/GroupPanelComponent.ts @@ -9,8 +9,7 @@ * * ***************************************************************************/ -declare let marauroa: any; -declare let stendhal: any; +import { marauroa } from "marauroa" import { ChatInputComponent } from "./ChatInputComponent"; import { GroupMemberComponent } from "./GroupMemberComponent"; @@ -23,6 +22,7 @@ import { Panel } from "../toolkit/Panel"; import { TabPanelComponent } from "../toolkit/TabPanelComponent"; import { Chat } from "../../util/Chat"; +import { stendhal } from "stendhal"; /** diff --git a/src/js/stendhal/ui/component/ItemContainerImplementation.ts b/src/js/stendhal/ui/component/ItemContainerImplementation.ts index 1d024dc8f8..86c02e666c 100644 --- a/src/js/stendhal/ui/component/ItemContainerImplementation.ts +++ b/src/js/stendhal/ui/component/ItemContainerImplementation.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * (C) Copyright 2003-2024 - Faiumoni e. V. * + * (C) Copyright 2003-2026 - Faiumoni e. V. * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,17 +9,23 @@ * * ***************************************************************************/ -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa"; +import { stendhal } from "../../stendhal"; import { ui } from "../UI"; +import { Item } from "../../entity/Item"; import { ActionContextMenu } from "../dialog/ActionContextMenu"; import { DropQuantitySelectorDialog } from "../dialog/DropQuantitySelectorDialog"; -import { Item } from "../../entity/Item"; import { singletons } from "../../SingletonRepo"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { HeldObjectManager } from "ui/HeldObject"; +import { HTMLUtil } from "ui/HTMLUtil"; +import { TouchHandler } from "ui/TouchHandler"; +import { ViewPort } from "ui/ViewPort"; +import { Paths } from "../../data/Paths"; import { Point } from "../../util/Point"; @@ -35,6 +41,7 @@ export class ItemContainerImplementation { // marked for updating certain attributes private dirty = false; + private touchHandler = TouchHandler.get(); // TODO: replace usage of global document.getElementById() @@ -60,7 +67,7 @@ export class ItemContainerImplementation { }); e.addEventListener("touchmove", (event: TouchEvent) => { this.onTouchMove(event); - }); + }, {passive: true}); e.addEventListener("drop", (event: DragEvent) => { this.onDrop(event) }); @@ -72,7 +79,7 @@ export class ItemContainerImplementation { }); e.addEventListener("touchstart", (event: TouchEvent) => { this.onTouchStart(event) - }); + }, {passive: true}); e.addEventListener("touchend", (event: TouchEvent) => { this.onTouchEnd(event) }); @@ -131,7 +138,7 @@ export class ItemContainerImplementation { } e.style.backgroundImage = "url(" - + stendhal.data.sprites.checkPath(stendhal.paths.sprites + + htmlImageStore.checkPath(Paths.sprites + "/items/" + o["class"] + "/" + o["subclass"] + ".png") + ")"; e.style.backgroundPosition = (xOffset+1) + "px " + (yOffset+1) + "px"; @@ -148,7 +155,7 @@ export class ItemContainerImplementation { for (let i = cnt; i < this.size; i++) { let e = this.parentElement.querySelector("#" + this.slot +this. suffix + i) as HTMLElement; if (this.defaultImage) { - e.style.backgroundImage = "url(" + stendhal.paths.gui + "/" + this.defaultImage + ")"; + e.style.backgroundImage = "url(" + Paths.gui + "/" + this.defaultImage + ")"; } else { e.style.backgroundImage = "none"; } @@ -166,7 +173,7 @@ export class ItemContainerImplementation { private onDragStart(event: DragEvent|TouchEvent) { let myobject = this.object || marauroa.me; // some mobile browsers such as Chrome call "dragstart" via long touch - if (!myobject[this.slot] || (event.type === "dragstart" && stendhal.ui.touch.isTouchEngaged())) { + if (!myobject[this.slot] || (event.type === "dragstart" && this.touchHandler.isTouchEngaged())) { event.preventDefault(); return; } @@ -183,6 +190,10 @@ export class ItemContainerImplementation { const slotNumber = target.id.slice(this.slot.length + this.suffix.length); let item = myobject[this.slot].getByIndex(slotNumber); if (item) { + if (!HeldObjectManager.get().prepare(item, event)) { + return; + } + const heldObject = { path: item.getIdPath(), zone: marauroa.currentZoneName, @@ -190,15 +201,13 @@ export class ItemContainerImplementation { quantity: item.hasOwnProperty("quantity") ? item["quantity"] : 1 }; - const img = stendhal.data.sprites.getAreaOf(stendhal.data.sprites.get(item.sprite.filename), 32, 32); if (event instanceof DragEvent && event.dataTransfer) { - stendhal.ui.heldObject = heldObject; - event.dataTransfer.setDragImage(img, 0, 0); - } else if (stendhal.ui.touch.isTouchEvent(event)) { - stendhal.ui.touch.setHolding(true); + HeldObjectManager.get().heldObject = heldObject; + } else if (this.touchHandler.isTouchEvent(event)) { + this.touchHandler.setHolding(true); // TODO: move when supported by mouse events - const pos = stendhal.ui.html.extractPosition(event); - singletons.getHeldObjectManager().set(heldObject, img, new Point(pos.pageX, pos.pageY)); + const pos = HTMLUtil.extractPosition(event); + HeldObjectManager.get().set(heldObject, new Point(pos.pageX, pos.pageY)); } } else { event.preventDefault(); @@ -209,7 +218,7 @@ export class ItemContainerImplementation { * Handles displaying an icon for objects dragged with touch. */ private onTouchMove(event: TouchEvent) { - if (stendhal.ui.heldObject) { + if (HeldObjectManager.get().heldObject) { return; } this.onDragStart(event); @@ -242,13 +251,14 @@ export class ItemContainerImplementation { } private onDrop(event: DragEvent|TouchEvent) { + let heldObject = HeldObjectManager.get().heldObject; const myobject = this.object || marauroa.me; - if (stendhal.ui.heldObject) { - const pos = stendhal.ui.html.extractPosition(event); + if (heldObject) { + const pos = HTMLUtil.extractPosition(event); const id = (pos.target as HTMLElement).id; - const targetSlot = stendhal.ui.html.parseSlotName(id); + const targetSlot = HTMLUtil.parseSlotName(id); if (event.type === "touchend" && id === "viewport") { - stendhal.ui.gamewindow.onDrop(event); + ViewPort.get().onDrop(event); event.stopPropagation(); event.preventDefault(); return; @@ -258,7 +268,7 @@ export class ItemContainerImplementation { if (event.type === "touchend") { // find the actual target ID for touch events if (targetSlot === "content") { - const container = stendhal.ui.equip.getByElement(stendhal.ui.html.extractTarget(event).parentElement!); + const container = stendhal.ui.equip.getByElement((HTMLUtil.extractTarget(event) as HTMLElement).parentElement!); if (container && container.object) { objectId = container.object.id; } @@ -269,31 +279,31 @@ export class ItemContainerImplementation { } const action = { - "source_path": stendhal.ui.heldObject.path + "source_path": heldObject.path } as any; - const sameSlot = stendhal.ui.heldObject.slot === targetSlot; + const sameSlot = heldObject.slot === targetSlot; if (sameSlot) { action["type"] = "reorder"; action["new_position"] = this.parseIndex(id) || "" + (this.size - 1); } else { action["type"] = "equip"; action["target_path"] = "[" + objectId + "\t" + targetSlot + "]"; - action["zone"] = stendhal.ui.heldObject.zone; + action["zone"] = heldObject.zone; } - const quantity = stendhal.ui.heldObject.quantity; - stendhal.ui.heldObject = undefined; + const quantity = heldObject.quantity || 1; + HeldObjectManager.get().heldObject = undefined; // if ctrl is pressed or holding stackable item from touch event, we ask for the quantity - const touch_held = stendhal.ui.touch.holding() && quantity > 1; + const touch_held = this.touchHandler.holding() && quantity > 1; const split_action = !sameSlot && ((event instanceof DragEvent && event.ctrlKey) || touch_held); if (split_action) { - const pos = stendhal.ui.html.extractPosition(event); + const pos = HTMLUtil.extractPosition(event); ui.createSingletonFloatingWindow("Quantity", new DropQuantitySelectorDialog(action, touch_held), pos.pageX - 50, pos.pageY - 25); } else { - singletons.getHeldObjectManager().onRelease(); + HeldObjectManager.get().onRelease(); marauroa.clientFramework.sendAction(action); } } @@ -318,9 +328,9 @@ export class ItemContainerImplementation { if (this.timestampMouseDown - this.timestampMouseDownPrev <= this.rightClickDuration) { // reset so subsequent single clicks/taps aren't counted this.timestampMouseDown = 0; - return (stendhal.ui.html.extractTarget(evt) as HTMLElement).id === this.lastClickedId; + return (HTMLUtil.extractTarget(evt) as HTMLElement).id === this.lastClickedId; } - this.lastClickedId = (stendhal.ui.html.extractTarget(evt) as HTMLElement).id; + this.lastClickedId = (HTMLUtil.extractTarget(evt) as HTMLElement).id; return false; } @@ -340,9 +350,9 @@ export class ItemContainerImplementation { return; } - let event = stendhal.ui.html.extractPosition(evt); + let event = HTMLUtil.extractPosition(evt); if ((event.target as any).dataItem) { - const long_touch = stendhal.ui.touch.isLongTouch(evt); + const long_touch = this.touchHandler.isLongTouch(evt); const context_action = (evt instanceof MouseEvent && this.isRightClick(evt)) || long_touch; if (this.quickPickup && !context_action) { marauroa.clientFramework.sendAction({ @@ -372,7 +382,7 @@ export class ItemContainerImplementation { stendhal.ui.actionContextMenu.set(ui.createSingletonFloatingWindow("Action", new ActionContextMenu((event.target as any).dataItem, append), event.pageX - 50, event.pageY - 5)); - } else if (!stendhal.ui.heldObject) { + } else if (!HeldObjectManager.get().heldObject) { if (!stendhal.config.getBoolean("inventory.double-click") || this.isDoubleClick(event)) { marauroa.clientFramework.sendAction({ type: "use", @@ -384,7 +394,7 @@ export class ItemContainerImplementation { } // clean up item held via touch - stendhal.ui.touch.setHolding(false); + this.touchHandler.setHolding(false); document.getElementById("viewport")!.focus(); } @@ -398,22 +408,22 @@ export class ItemContainerImplementation { } private onTouchStart(evt: TouchEvent) { - const pos = stendhal.ui.html.extractPosition(evt); - stendhal.ui.touch.onTouchStart(pos.pageX, pos.pageY); + const pos = HTMLUtil.extractPosition(evt); + this.touchHandler.onTouchStart(pos.pageX, pos.pageY); } private onTouchEnd(evt: TouchEvent) { - stendhal.ui.touch.onTouchEnd(); - if (stendhal.ui.touch.isLongTouch(evt) && !stendhal.ui.touch.holding()) { + this.touchHandler.onTouchEnd(); + if (this.touchHandler.isLongTouch(evt) && !this.touchHandler.holding()) { this.onMouseUp(evt); - } else if (stendhal.ui.touch.holding()) { + } else if (this.touchHandler.holding()) { evt.preventDefault(); this.onDrop(evt); - stendhal.ui.touch.setHolding(false); + this.touchHandler.setHolding(false); } // clean up touch handler - stendhal.ui.touch.unsetOrigin(); + this.touchHandler.unsetOrigin(); } /** @@ -427,14 +437,14 @@ export class ItemContainerImplementation { private updateCursor(target: HTMLElement, item?: Item) { if (item) { if (this.slot === "content" && stendhal.config.getBoolean("inventory.quick-pickup")) { - target.style.cursor = "url(" + stendhal.paths.sprites + target.style.cursor = "url(" + Paths.sprites + "/cursor/itempickupfromslot.png) 1 3, auto"; return; } target.style.cursor = item.getCursor(0, 0); return; } - target.style.cursor = "url(" + stendhal.paths.sprites + target.style.cursor = "url(" + Paths.sprites + "/cursor/normal.png) 1 3, auto"; } diff --git a/src/js/stendhal/ui/component/ItemInventoryComponent.ts b/src/js/stendhal/ui/component/ItemInventoryComponent.ts index 6f2662879f..7ad2dea094 100644 --- a/src/js/stendhal/ui/component/ItemInventoryComponent.ts +++ b/src/js/stendhal/ui/component/ItemInventoryComponent.ts @@ -12,7 +12,7 @@ import { Component } from "../toolkit/Component"; import { ItemContainerImplementation } from "./ItemContainerImplementation"; -declare var stendhal: any; +import { stendhal } from "../../stendhal"; /** * handles an item inventory @@ -68,7 +68,6 @@ export class ItemInventoryComponent extends Component { public override onParentClose() { let idx = stendhal.ui.equip.indexOf(this.itemContainerImplementation); - console.log(stendhal.ui.equip.getInventory(), idx); if (idx < 0) { console.log("Cannot cleanup unknown itemContainerImplementation"); return; diff --git a/src/js/stendhal/ui/component/KeyringComponent.ts b/src/js/stendhal/ui/component/KeyringComponent.ts index a5da2fb9bc..7c808267e4 100644 --- a/src/js/stendhal/ui/component/KeyringComponent.ts +++ b/src/js/stendhal/ui/component/KeyringComponent.ts @@ -11,7 +11,7 @@ import { ItemInventoryComponent } from "./ItemInventoryComponent"; -declare var marauroa: any; +import { marauroa } from "marauroa" export class KeyringComponent extends ItemInventoryComponent { diff --git a/src/js/stendhal/ui/component/MiniMapComponent.ts b/src/js/stendhal/ui/component/MiniMapComponent.ts index ea0a5d464e..5019f175f9 100644 --- a/src/js/stendhal/ui/component/MiniMapComponent.ts +++ b/src/js/stendhal/ui/component/MiniMapComponent.ts @@ -9,14 +9,17 @@ * * ***************************************************************************/ -declare let marauroa: any; -declare let stendhal: any; +import { marauroa } from "marauroa" import { Component } from "../toolkit/Component"; import { Player } from "../../entity/Player"; import { Color } from "../../data/color/Color"; +import { RenderingContext2D } from "util/Types"; +import { stendhal } from "stendhal"; +import { TileMap } from "data/TileMap"; +import { HTMLUtil } from "ui/HTMLUtil"; /** @@ -24,6 +27,7 @@ import { Color } from "../../data/color/Color"; */ export class MiniMapComponent extends Component { + private map: TileMap; private width = 128; private height = 128; private minimumScale = 2; @@ -32,7 +36,7 @@ export class MiniMapComponent extends Component { private mapWidth = 1; private mapHeight = 1; private scale = 1; - private bgImage!: HTMLCanvasElement; + private bgImage?: ImageBitmap; private lastZone?: number[]; // ground/collision colors @@ -43,6 +47,7 @@ export class MiniMapComponent extends Component { constructor() { super("minimap"); + this.map = TileMap.get(); this.componentElement.addEventListener("click", (event) => { this.onClick(event); }); @@ -53,8 +58,8 @@ export class MiniMapComponent extends Component { private zoneChange() { - this.mapWidth = stendhal.data.map.zoneSizeX; - this.mapHeight = stendhal.data.map.zoneSizeY; + this.mapWidth = this.map.zoneSizeX; + this.mapHeight = this.map.zoneSizeY; this.scale = Math.max(this.minimumScale, Math.min(this.height / this.mapHeight, this.width / this.mapWidth)); this.createBackgroundImage(); }; @@ -95,10 +100,10 @@ export class MiniMapComponent extends Component { } public draw() { - if (marauroa.currentZoneName === stendhal.data.map.currentZoneName - || stendhal.data.map.currentZoneName === "int_vault" - || stendhal.data.map.currentZoneName === "int_adventure_island" - || stendhal.data.map.currentZoneName === "tutorial_island") { + if (marauroa.currentZoneName === this.map.currentZoneName + || this.map.currentZoneName === "int_vault" + || this.map.currentZoneName === "int_adventure_island" + || this.map.currentZoneName === "tutorial_island") { this.scale = 10; @@ -119,7 +124,7 @@ export class MiniMapComponent extends Component { } } - drawBackground(ctx: CanvasRenderingContext2D) { + drawBackground(ctx: RenderingContext2D) { ctx.save(); ctx.imageSmoothingEnabled = false; @@ -137,10 +142,10 @@ export class MiniMapComponent extends Component { return; } - if (stendhal.data.map.collisionData !== this.lastZone) { - this.lastZone = stendhal.data.map.collisionData; - this.bgImage = document.createElement("canvas"); - let ctx = this.bgImage.getContext("2d")!; + if (this.map.collisionData !== this.lastZone) { + this.lastZone = this.map.collisionData; + let canvas = new OffscreenCanvas(width, height); + let ctx = canvas.getContext("2d")!; let imgData = ctx.createImageData(width, height); for (let y = 0; y < height; y++) { @@ -148,10 +153,10 @@ export class MiniMapComponent extends Component { let color = MiniMapComponent.COLOR_GROUND; // RGBA array. Find the actual position let pos = 4 * (y * width + x); - if (stendhal.data.map.collision(x, y)) { + if (this.map.collision(x, y)) { // red collision color = MiniMapComponent.COLOR_COLLISION; - } else if (stendhal.data.map.isProtected(x, y)) { + } else if (this.map.isProtected(x, y)) { // light green for protection color = MiniMapComponent.COLOR_PROTECTION; } @@ -161,14 +166,15 @@ export class MiniMapComponent extends Component { imgData.data[pos + 3] = 255; // opacity } } - this.bgImage.width = width; - this.bgImage.height = height; - ctx.putImageData(imgData, 0, 0); + if (this.bgImage) { + this.bgImage.close(); + } + this.bgImage = canvas.transferToImageBitmap(); } } - drawEntities(ctx: CanvasRenderingContext2D) { + drawEntities(ctx: RenderingContext2D) { ctx.fillStyle = Color.RED; ctx.strokeStyle = Color.BLACK; let isAdmin = marauroa.me["adminlevel"] && marauroa.me["adminlevel"] >= 600; @@ -215,10 +221,10 @@ export class MiniMapComponent extends Component { if (!stendhal.config.getBoolean("pathfinding.minimap")) { return; } - let pos = stendhal.ui.html.extractPosition(event); + let pos = HTMLUtil.extractPosition(event); let x = Math.floor((pos.canvasRelativeX + this.xOffset) / this.scale); let y = Math.floor((pos.canvasRelativeY + this.yOffset) / this.scale); - if (!stendhal.data.map.collision(x, y)) { + if (!this.map.collision(x, y)) { let action: any = { type: "moveto", x: x.toString(), diff --git a/src/js/stendhal/ui/component/OutfitPreviewComponent.ts b/src/js/stendhal/ui/component/OutfitPreviewComponent.ts new file mode 100644 index 0000000000..9fc10a3892 --- /dev/null +++ b/src/js/stendhal/ui/component/OutfitPreviewComponent.ts @@ -0,0 +1,146 @@ +/*************************************************************************** + * Copyright © 2024 - Faiumoni e. V. * + *************************************************************************** + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation; either version 3 of the * + * License, or (at your option) any later version. * + * * + ***************************************************************************/ + +import { Component } from "../toolkit/Component"; + +import { Outfit } from "../../data/Outfit"; + +import { Direction } from "../../util/Direction"; +import { StringUtil } from "../../util/StringUtil"; + + +/** + * Component to preview an entity's outfit sprite. + * + * FIXME: + * - character select previews don't draw after page refresh + */ +export class OutfitPreviewComponent extends Component { + + private dir: Direction; + private index: number; + private image?: ImageBitmap; + private bgColor?: string; + + + constructor() { + super("outfit-preview-template"); + this.dir = Direction.DOWN; + this.index = 1; + } + + /** + * Sets current outfit for this preview & updates drawing. + * + * @param {string} outfit + * Outfit formatted string ("body=0,head=2,eyes=1,...). + * @param {string=} coloring + * Outfit coloring. + */ + setOutfit(outfit: string, coloring?: string) { + const otemp = Outfit.build(outfit, coloring); + otemp.toImage().then((image) => { + this.image?.close(); + this.image = image; + this.update(); + }); + } + + /** + * Sets facing direction to draw. + * + * Default is down. + * + * @param {Direction} dir + * Facing direction. + */ + setDirection(dir: Direction) { + this.dir = dir.val < 1 ? Direction.VALUES[1] : dir.val > 4 ? Direction.VALUES[4] : dir; + this.update(); + } + + /** + * Increments facing direction to draw clockwise. + */ + nextDirection() { + let dir = Direction.VALUES[this.dir.val + 1]; + if (typeof(dir) === "undefined") { + dir = Direction.VALUES[1]; + } + this.setDirection(dir); + } + + /** + * Increments facing direction to draw counter-clockwise. + */ + prevDirection() { + let dir = Direction.VALUES[this.dir.val - 1]; + if (typeof(dir) === "undefined" || dir.val === 0) { + dir = Direction.VALUES[4]; + } + this.setDirection(dir); + } + + /** + * Sets frame index to draw. + * + * Default is 1 (center frame). + * + * @param {number} index + * Frame index. + */ + setFrame(index: number) { + this.index = index < 0 ? 0 : index > 2 ? 2 : index; + this.update(); + } + + /** + * Sets or unsets background color to fill before drawing outfit. + * + * @param {string|undefined|null} color + * Background color or `undefined|null` or empty string to unset. + */ + setBGColor(color: string|undefined|null) { + if (!color || StringUtil.isEmpty(color)) { + color = undefined; + } + this.bgColor = color; + } + + /** + * Draws outfit in preview area. + */ + update() { + if (!this.image || !this.image.height) { + console.warn("outfit preview not ready"); + return; + } + const w = this.image.width / 3; + const h = this.image.height / 4; + const ctx = (this.componentElement as HTMLCanvasElement).getContext("2d")!; + if (w !== ctx.canvas.width || h !== ctx.canvas.height) { + console.warn("Image dimensions do not match preview canvas: " + w + "x" + h + " != " + + ctx.canvas.width + "x" + ctx.canvas.height); + } + if (this.bgColor) { + ctx.fillStyle = this.bgColor; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + } else { + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + } + ctx.drawImage(this.image, this.index * w, (this.dir.val - 1) * h, w, h, 0, 0, w, h); + } + + close() { + this.image?.close(); + } +} diff --git a/src/js/stendhal/ui/component/PlayerEquipmentComponent.ts b/src/js/stendhal/ui/component/PlayerEquipmentComponent.ts index 50bb097baa..c4e6e913e8 100644 --- a/src/js/stendhal/ui/component/PlayerEquipmentComponent.ts +++ b/src/js/stendhal/ui/component/PlayerEquipmentComponent.ts @@ -12,7 +12,7 @@ import { Component } from "../toolkit/Component"; import { ItemContainerImplementation } from "./ItemContainerImplementation"; -declare var marauroa: any; +import { marauroa } from "marauroa" /** * manage the equipment which is used by the player diff --git a/src/js/stendhal/ui/component/PlayerStatsComponent.ts b/src/js/stendhal/ui/component/PlayerStatsComponent.ts index 74e44041ec..3428f77d75 100644 --- a/src/js/stendhal/ui/component/PlayerStatsComponent.ts +++ b/src/js/stendhal/ui/component/PlayerStatsComponent.ts @@ -22,8 +22,8 @@ import { singletons } from "../../SingletonRepo"; import { Item } from "../../entity/Item"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; /** diff --git a/src/js/stendhal/ui/component/StatBarComponent.ts b/src/js/stendhal/ui/component/StatBarComponent.ts index b25b384ca1..2f13f329a0 100644 --- a/src/js/stendhal/ui/component/StatBarComponent.ts +++ b/src/js/stendhal/ui/component/StatBarComponent.ts @@ -11,6 +11,7 @@ import { Component } from "../toolkit/Component"; import { Color } from "../../data/color/Color"; +import { RenderingContext2D } from "util/Types"; /** * TODO: move this to `ui.toolkit`. @@ -18,7 +19,7 @@ import { Color } from "../../data/color/Color"; export class StatBarComponent extends Component { readonly canvas: HTMLCanvasElement; - protected readonly ctx: CanvasRenderingContext2D; + protected readonly ctx: RenderingContext2D; constructor(id?: string) { @@ -26,7 +27,7 @@ export class StatBarComponent extends Component { id = "statbar-template"; } super(id); - this.canvas = this.componentElement; + this.canvas = this.componentElement as HTMLCanvasElement; this.ctx = this.canvas.getContext("2d")!; } diff --git a/src/js/stendhal/ui/component/StatusesListComponent.ts b/src/js/stendhal/ui/component/StatusesListComponent.ts index b0c30a985d..6428451445 100644 --- a/src/js/stendhal/ui/component/StatusesListComponent.ts +++ b/src/js/stendhal/ui/component/StatusesListComponent.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2023 - Arianne * + * Copyright © 2023-2026 - Arianne * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -11,10 +11,10 @@ import { Component } from "../toolkit/Component"; +import { htmlImageStore } from "data/HTMLImageStore"; +import { Paths } from "../../data/Paths"; import { User } from "../../entity/User"; -declare var stendhal: any; - export class StatusesListComponent extends Component { @@ -65,11 +65,11 @@ export class StatusesListComponent extends Component { } let iconPath: string; if (Object.keys(StatusesListComponent.iconPaths).indexOf(id) !== -1) { - iconPath = stendhal.paths.sprites + StatusesListComponent.iconPaths[id]; + iconPath = Paths.sprites + StatusesListComponent.iconPaths[id]; } else { - iconPath = stendhal.paths.sprites + "/status/panel/" + id + ".png"; + iconPath = Paths.sprites + "/status/panel/" + id + ".png"; } - const icon = stendhal.data.sprites.get(iconPath); + const icon = htmlImageStore.get(iconPath); icon.className = "status-icon"; this.componentElement.appendChild(icon); } diff --git a/src/js/stendhal/ui/component/ZoneInfoComponent.ts b/src/js/stendhal/ui/component/ZoneInfoComponent.ts index d4490a2395..7222a7642c 100644 --- a/src/js/stendhal/ui/component/ZoneInfoComponent.ts +++ b/src/js/stendhal/ui/component/ZoneInfoComponent.ts @@ -11,7 +11,7 @@ import { Component } from "../toolkit/Component"; -declare let marauroa: any; +import { marauroa } from "marauroa" /** * zone info diff --git a/src/js/stendhal/ui/dialog/ActionContextMenu.ts b/src/js/stendhal/ui/dialog/ActionContextMenu.ts index 46ee1b0061..d4465734ac 100644 --- a/src/js/stendhal/ui/dialog/ActionContextMenu.ts +++ b/src/js/stendhal/ui/dialog/ActionContextMenu.ts @@ -14,8 +14,9 @@ import { UIComponentEnum } from "../UIComponentEnum"; import { Component } from "../toolkit/Component"; import { ChatInputComponent } from "../component/ChatInputComponent"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; +import { HTMLUtil } from "ui/HTMLUtil"; export class ActionContextMenu extends Component { private entity: any; @@ -30,7 +31,8 @@ export class ActionContextMenu extends Component { var content = "
    "; for (var i = 0; i < this.actions.length; i++) { - content += ""; + // FIXME: the addition of "sub-actionbutton" class should no longer be needed + content += ""; } content += "
    "; this.componentElement.innerHTML = content; diff --git a/src/js/stendhal/ui/dialog/ApplicationMenuDialog.ts b/src/js/stendhal/ui/dialog/ApplicationMenuDialog.ts index 01b04379c6..bde75e9c0d 100644 --- a/src/js/stendhal/ui/dialog/ApplicationMenuDialog.ts +++ b/src/js/stendhal/ui/dialog/ApplicationMenuDialog.ts @@ -9,11 +9,22 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../../stendhal"; import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { singletons } from "../../SingletonRepo"; +import { Debug } from "../../util/Debug"; +import { ScreenCapture } from "../../util/ScreenCapture"; +import { HTMLUtil } from "ui/HTMLUtil"; + + +interface MenuAction { + title: string, + action: string, + alt?: string, + condition?: Function +} export class ApplicationMenuDialog extends DialogContentComponent { @@ -37,7 +48,7 @@ export class ApplicationMenuDialog extends DialogContentComponent { title: "Logout", action: "logout" } - ] + ] as MenuAction[] }, { title: "Tools", @@ -46,11 +57,19 @@ export class ApplicationMenuDialog extends DialogContentComponent { title: "Take Screenshot", action: "screenshot", }, + /* + { + title: "Capture Video", + alt: "Stop Capture", + condition: ScreenCapture.isActive, + action: "screencap" + }, + */ { title: "Settings", action: "settings", } - ] + ] as MenuAction[] }, { title: "Commands", @@ -71,7 +90,7 @@ export class ApplicationMenuDialog extends DialogContentComponent { title: "Travel Log", action: "progressstatus", } - ] + ] as MenuAction[] }, { title: "Help", @@ -100,18 +119,32 @@ export class ApplicationMenuDialog extends DialogContentComponent { title: "About", action: "about", } - ] + ] as MenuAction[] }, ] constructor() { super("applicationmenudialog-template"); + if (Debug.isActive("screencap")) { + this.actions[1].children.push({ + title: "Capture Video", + alt: "Stop Capture", + condition: ScreenCapture.isActive, + action: "screencap" + }); + } + var content = ""; for (var i = 0; i < this.actions.length; i++) { - content += "

    " + stendhal.ui.html.esc(this.actions[i].title) + "

    " + content += "

    " + HTMLUtil.esc(this.actions[i].title) + "

    " for (var j = 0; j < this.actions[i].children.length; j++) { - content += "
    "; + const action = this.actions[i].children[j]; + let title = action.title; + if (action.alt && action.condition && action.condition()) { + title = action.alt; + } + content += "
    "; } content += "
    "; } diff --git a/src/js/stendhal/ui/dialog/ChatOptionsDialog.ts b/src/js/stendhal/ui/dialog/ChatOptionsDialog.ts index 81a667faf5..caa3315b6a 100644 --- a/src/js/stendhal/ui/dialog/ChatOptionsDialog.ts +++ b/src/js/stendhal/ui/dialog/ChatOptionsDialog.ts @@ -14,8 +14,8 @@ import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { Chat } from "../../util/Chat"; import { singletons } from "../../SingletonRepo"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; /** diff --git a/src/js/stendhal/ui/dialog/ChooseCharacterDialog.ts b/src/js/stendhal/ui/dialog/ChooseCharacterDialog.ts index 26d842c7dd..b724896a9f 100644 --- a/src/js/stendhal/ui/dialog/ChooseCharacterDialog.ts +++ b/src/js/stendhal/ui/dialog/ChooseCharacterDialog.ts @@ -9,9 +9,12 @@ * * ***************************************************************************/ -import { Client } from "../../Client"; import { DialogContentComponent } from "../toolkit/DialogContentComponent"; +import { Client } from "../../Client"; +import { SlashActionRepo } from "../../SlashActionRepo"; + + /** * a dialog to select your character from */ @@ -20,6 +23,7 @@ export class ChooseCharacterDialog extends DialogContentComponent { constructor(characters: any) { super("choose-character-template"); + const characterList = this.child("#characters")!; for (var i in characters) { if (characters.hasOwnProperty(i)) { let name = characters[i]["a"]["name"]; @@ -30,9 +34,20 @@ export class ChooseCharacterDialog extends DialogContentComponent { this.componentElement.dispatchEvent(new Event("close")); Client.get().chooseCharacter(name); }); - this.componentElement.append(button); + characterList.append(button); } } + + this.child("#logout")!.addEventListener("click", (e: Event) => { + this.componentElement.dispatchEvent(new Event("close")); + this.onLogout(); + }); } + onLogout() { + queueMicrotask(() => { + // TODO: detect if not logged in via website & simply re-display login dialog + SlashActionRepo.get().execute("/logout"); + }); + } } diff --git a/src/js/stendhal/ui/dialog/CreateAccountDialog.ts b/src/js/stendhal/ui/dialog/CreateAccountDialog.ts index 0909bd725b..a81113dfe0 100644 --- a/src/js/stendhal/ui/dialog/CreateAccountDialog.ts +++ b/src/js/stendhal/ui/dialog/CreateAccountDialog.ts @@ -13,7 +13,7 @@ import { ui } from "../UI"; import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { LoginDialog } from "./LoginDialog"; -declare var marauroa: any; +import { marauroa } from "marauroa" /** * a dialog to enter username and password diff --git a/src/js/stendhal/ui/dialog/DropQuantitySelectorDialog.ts b/src/js/stendhal/ui/dialog/DropQuantitySelectorDialog.ts index 5c2da2930c..b4291550a0 100644 --- a/src/js/stendhal/ui/dialog/DropQuantitySelectorDialog.ts +++ b/src/js/stendhal/ui/dialog/DropQuantitySelectorDialog.ts @@ -9,10 +9,10 @@ * * ***************************************************************************/ -declare var marauroa: any; +import { marauroa } from "marauroa"; +import { HeldObjectManager } from "ui/HeldObject"; import { Component } from "../toolkit/Component"; -import { singletons } from "../../SingletonRepo"; export class DropQuantitySelectorDialog extends Component { @@ -49,7 +49,7 @@ export class DropQuantitySelectorDialog extends Component { * Drops selected quantity from stack. */ private onDrop(event: Event) { - singletons.getHeldObjectManager().onRelease(); + HeldObjectManager.get().onRelease(); let quantityStr = (this.child(".quantityselectorvalue") as HTMLInputElement).value; let quantity = parseInt(quantityStr); if (quantity > 0) { @@ -64,7 +64,7 @@ export class DropQuantitySelectorDialog extends Component { * Drops entire stack. */ private onDropAll(event: Event) { - singletons.getHeldObjectManager().onRelease(); + HeldObjectManager.get().onRelease(); marauroa.clientFramework.sendAction(this.action); this.componentElement.dispatchEvent(new Event("close")); event.preventDefault(); diff --git a/src/js/stendhal/ui/dialog/EmojiMapDialog.ts b/src/js/stendhal/ui/dialog/EmojiMapDialog.ts index 9fab015c4d..b3812f4302 100644 --- a/src/js/stendhal/ui/dialog/EmojiMapDialog.ts +++ b/src/js/stendhal/ui/dialog/EmojiMapDialog.ts @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright © 2023-2024 - Stendhal * + * Copyright © 2023-2026 - Stendhal * *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -9,11 +9,13 @@ * * ***************************************************************************/ -import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { singletons } from "../../SingletonRepo"; +import { Paths } from "../../data/Paths"; +import { DialogContentComponent } from "../toolkit/DialogContentComponent"; -declare var marauroa: any; -declare var stendhal: any; +import { htmlImageStore } from "data/HTMLImageStore"; +import { marauroa } from "marauroa"; +import { stendhal } from "../../stendhal"; export class EmojiMapDialog extends DialogContentComponent { @@ -83,7 +85,7 @@ export class EmojiMapDialog extends DialogContentComponent { const button = document.createElement("button"); button.classList.add("shortcut-button", "emoji-text"); if (!this.sysEmojis) { - button.appendChild(stendhal.data.sprites.get(stendhal.paths.sprites + "/emoji/" + emoji + ".png").cloneNode()); + button.appendChild(htmlImageStore.get(Paths.sprites + "/emoji/" + emoji + ".png").cloneNode()); } else { button.innerText = emoji; } diff --git a/src/js/stendhal/ui/dialog/ImageViewerDialog.ts b/src/js/stendhal/ui/dialog/ImageViewerDialog.ts index a72c518f88..6b6b9ca3f4 100644 --- a/src/js/stendhal/ui/dialog/ImageViewerDialog.ts +++ b/src/js/stendhal/ui/dialog/ImageViewerDialog.ts @@ -11,8 +11,8 @@ import { Component } from "../toolkit/Component"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; /** * a dialog to display images diff --git a/src/js/stendhal/ui/dialog/LoginDialog.ts b/src/js/stendhal/ui/dialog/LoginDialog.ts index b546b05e81..3412b681f4 100644 --- a/src/js/stendhal/ui/dialog/LoginDialog.ts +++ b/src/js/stendhal/ui/dialog/LoginDialog.ts @@ -14,7 +14,7 @@ import { ui } from "../UI"; import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { CreateAccountDialog } from "./CreateAccountDialog"; -declare var marauroa: any; +import { marauroa } from "marauroa" /** * a dialog to enter username and password @@ -24,6 +24,14 @@ export class LoginDialog extends DialogContentComponent { constructor() { super("logindialog-template"); + // prevent KeyHandler's overrides of keypress events + this.componentElement.addEventListener("keydown", (e: Event) => { + e.stopPropagation(); + }); + this.componentElement.addEventListener("keyup", (e: Event) => { + e.stopPropagation(); + }); + this.child("button")!.addEventListener("click", (event: Event) => { event.preventDefault(); let username = (this.child("#username") as HTMLInputElement).value; diff --git a/src/js/stendhal/ui/dialog/SettingsDialog.ts b/src/js/stendhal/ui/dialog/SettingsDialog.ts index b2a6531567..783f999577 100644 --- a/src/js/stendhal/ui/dialog/SettingsDialog.ts +++ b/src/js/stendhal/ui/dialog/SettingsDialog.ts @@ -9,21 +9,23 @@ * * ***************************************************************************/ -declare var stendhal: any; +import { stendhal } from "../../stendhal"; import { GeneralTab } from "./settings/GeneralTab"; import { InputTab } from "./settings/InputTab"; import { SoundTab } from "./settings/SoundTab"; +import { VisualsTab } from "./settings/VisualsTab"; import { TabDialogContentComponent } from "../toolkit/TabDialogContentComponent"; +import { Tooltip } from "../toolkit/Tooltip"; import { Layout } from "../../data/enum/Layout"; +import { Debug } from "../../util/Debug"; + export class SettingsDialog extends TabDialogContentComponent { - public static debugging = false; - public readonly storedStates: {[index: string]: string}; private readonly initialStates: {[index: string]: string}; @@ -39,11 +41,17 @@ export class SettingsDialog extends TabDialogContentComponent { }; this.initialStates = { + "activity-indicator": stendhal.config.get("activity-indicator"), + "activity-indicator.animate": stendhal.config.get("activity-indicator.animate"), "effect.blood": stendhal.config.get("effect.blood"), + "effect.entity-overlay": stendhal.config.get("effect.entity-overlay"), + "effect.lighting": stendhal.config.get("effect.lighting"), + "effect.parallax": stendhal.config.get("effect.parallax"), "effect.weather": stendhal.config.get("effect.weather") }; this.addTab("General", new GeneralTab(this, this.child("#settings-general")!)); + this.addTab("Visuals", new VisualsTab(this, this.child("#settings-visuals")!)); this.addTab("Sound", new SoundTab(this, this.child("#settings-sound")!)); this.addTab("Input", new InputTab(this, this.child("#settings-input")!)); @@ -102,7 +110,7 @@ export class SettingsDialog extends TabDialogContentComponent { label.innerHTML = ""; label.appendChild(checkbox); label.appendChild(text); - if (!SettingsDialog.debugging) { + if (!Debug.isActive("settings")) { // hide if settings debugging is not enabled checkbox.disabled = true; checkbox.style.setProperty("display", "none"); @@ -140,9 +148,13 @@ export class SettingsDialog extends TabDialogContentComponent { const chk = this.createCheckBoxSkel(id)!; chk.checked = stendhal.config.getBoolean(setid); - const tt = new CheckTooltip(ttpos, ttneg); - if (chk.parentElement) { - chk.parentElement.title = tt.getValue(chk.checked); + let tt: Tooltip; + if (ttpos !== "") { + let element: any = chk; + if (chk.parentElement) { + element = chk.parentElement; + } + tt = new Tooltip(element, ttpos, ttneg !== "" ? ttneg : undefined, chk.checked); } if (!chk.disabled) { chk.addEventListener("change", (e) => { @@ -153,7 +165,9 @@ export class SettingsDialog extends TabDialogContentComponent { } else { stendhal.config.set(setid, chk.checked); } - chk.parentElement!.title = tt.getValue(chk.checked); + if (tt) { + tt.setState(chk.checked); + } if (action) { action(); } @@ -238,16 +252,16 @@ export class SettingsDialog extends TabDialogContentComponent { /** * Creates a select element. * - * @param id + * @param {string} id * Identifier of element to retrieve. - * @param cid + * @param {string} ckey * Configuration key associated with element. - * @param tooltip + * @param {string=} tooltip * Optional popup tooltip text. - * @param action + * @param {Function=} action * Action to execute when state changed. - * @return - * `HTMLSelectElement`. + * @returns {HTMLSelectElement} + * Select element. */ createSelectFromConfig(id: string, ckey: string, tooltip?: string, action?: Function): HTMLSelectElement { const cvalue = stendhal.config.get(ckey); @@ -344,19 +358,3 @@ export class SettingsDialog extends TabDialogContentComponent { return input; } } - - -class CheckTooltip { - private valueEnabled: string; - private valueDisabled: string; - constructor(e: string, d: string) { - this.valueEnabled = e; - this.valueDisabled = d; - } - public getValue(enabled: boolean): string { - if (enabled) { - return this.valueEnabled; - } - return this.valueDisabled; - } -} diff --git a/src/js/stendhal/ui/dialog/TradeDialog.ts b/src/js/stendhal/ui/dialog/TradeDialog.ts index 691866241d..2deb045534 100644 --- a/src/js/stendhal/ui/dialog/TradeDialog.ts +++ b/src/js/stendhal/ui/dialog/TradeDialog.ts @@ -15,7 +15,7 @@ import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { ui } from "../UI"; import { UIComponentEnum } from "../UIComponentEnum"; -declare var marauroa: any; +import { marauroa } from "marauroa" /** * a dialog to display images diff --git a/src/js/stendhal/ui/dialog/TravelLogDialog.ts b/src/js/stendhal/ui/dialog/TravelLogDialog.ts index 54e035f2c4..cec17d26d6 100644 --- a/src/js/stendhal/ui/dialog/TravelLogDialog.ts +++ b/src/js/stendhal/ui/dialog/TravelLogDialog.ts @@ -9,12 +9,14 @@ * * ***************************************************************************/ +import { Paths } from "../../data/Paths"; import { DialogContentComponent } from "../toolkit/DialogContentComponent"; import { ui } from "../UI"; import { UIComponentEnum } from "../UIComponentEnum"; -declare var marauroa: any; -declare var stendhal: any; +import { marauroa } from "marauroa" +import { stendhal } from "../../stendhal"; +import { HTMLUtil } from "ui/HTMLUtil"; /** * a dialog to display images @@ -57,9 +59,9 @@ export class TravelLogDialog extends DialogContentComponent { private createHtml(dataItems: string[]) { let buttons = ""; for (var i = 0; i < dataItems.length; i++) { - buttons = buttons + ""; + + HTMLUtil.esc(dataItems[i]) + ""; } this.child(".tavellogtabpanel")!.innerHTML = buttons; @@ -135,8 +137,8 @@ export class TravelLogDialog extends DialogContentComponent { var html = ""; for (var i = 0; i < dataItems.length; i++) { const currentItem = dataItems[i]; - html += "