Compare commits

..

158 commits

Author SHA1 Message Date
Hendrik Brummermann
fcd8330db0
Merge pull request #769 from arianne/dependabot/npm_and_yarn/npm_and_yarn-53cbaf2a5b
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Java CI / build (push) Has been cancelled
Bump esbuild from 0.27.2 to 0.28.1 in the npm_and_yarn group across 1 directory
2026-07-12 06:51:38 +02:00
dependabot[bot]
98864f3802
Bump esbuild in the npm_and_yarn group across 1 directory
Some checks failed
Java CI / build (push) Has been cancelled
Bumps the npm_and_yarn group with 1 update in the / directory: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.27.2 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2025.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.2...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-14 11:54:18 +00:00
dimitris
0dff60f98e Guard external link launch against ActivityNotFoundException 2026-06-04 15:04:24 -07:00
Hendrik Brummermann
246d9ce288 converted BarehandAttackSprite and RangedAttackSprite to ImageBitmap 2026-03-01 23:04:12 +01:00
Hendrik Brummermann
31d417e31f code cleanup 2026-03-01 23:04:12 +01:00
Hendrik Brummermann
60d20f593b use ImageBitmap for SkillEffect 2026-03-01 23:04:11 +01:00
Hendrik Brummermann
f44e519104
Merge pull request #762 from arianne/dependabot/npm_and_yarn/npm_and_yarn-fb4fae9948
Bump minimatch from 9.0.5 to 9.0.9 in the npm_and_yarn group across 1 directory
2026-03-01 12:54:22 +01:00
dependabot[bot]
70dc2ded31
Bump minimatch in the npm_and_yarn group across 1 directory
Some checks failed
Java CI / build (push) Has been cancelled
Bumps the npm_and_yarn group with 1 update in the / directory: [minimatch](https://github.com/isaacs/minimatch).


Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 10:34:11 +00:00
Hendrik Brummermann
542f4524db converted StatusIconDrawer to ImageBitmap 2026-02-28 18:55:14 +01:00
Hendrik Brummermann
0d25beefa8 refactored singleton class StatusIconDrawer out of RPEntity 2026-02-28 03:14:24 +01:00
Hendrik Brummermann
3a478f341d use HeldObjectManager.get() 2026-02-28 02:30:16 +01:00
Hendrik Brummermann
c9f865a894 slightly reduce size of drag icon to so players focus cursor position 2026-02-23 22:26:32 +01:00
Hendrik Brummermann
afd5329fcc Item uses ImageBitmap instead of HTMLImageElement 2026-02-23 22:08:35 +01:00
Hendrik Brummermann
b8edcfb252 code cleanup 2026-02-22 19:48:41 +01:00
Hendrik Brummermann
bebce66517 use ImageBitmap in DomesticAnimal 2026-02-22 19:32:51 +01:00
Hendrik Brummermann
6f58eef4f5 updated marauroa.jar to set _rpclass before init() 2026-02-22 19:29:26 +01:00
Hendrik Brummermann
4d2925aa4b cleaned up HTMLImageStorage usage 2026-02-22 10:53:57 +01:00
Hendrik Brummermann
fb2ae76fc5 renamed SpriteStore to HTMLImageStore and access it directly 2026-02-22 09:59:37 +01:00
Hendrik Brummermann
aadec09b5d clean up factory class 2026-02-22 09:16:33 +01:00
Hendrik Brummermann
9fd4316d0f removed stendhal.data.sprites variable 2026-02-22 09:09:49 +01:00
Hendrik Brummermann
e1d01dea79 replaced stendhal.ui.gamewindow and stendhal.ui.viewport 2026-02-12 23:52:30 +01:00
Hendrik Brummermann
dd500a36e5 replaced stendhal.zone with Zone.get() 2026-02-12 22:29:45 +01:00
Hendrik Brummermann
8b2727f392 converted stendhal.ui.html into static class HTMLUtil 2026-02-12 21:59:12 +01:00
Hendrik Brummermann
d350c2b494 invoke singleton get-methods instead of SingletonRepo 2026-02-12 21:27:15 +01:00
Hendrik Brummermann
2022b28b76 access OutfitStore singleton directly 2026-02-12 01:42:32 +01:00
Hendrik Brummermann
1f9242ae6f converted to image bitmap 2026-02-10 23:24:15 +01:00
Hendrik Brummermann
03654e4459 prevent path traversal 2026-02-09 22:32:04 +01:00
Hendrik Brummermann
fcadfcdbd3 use ImageBitmap 2026-02-09 22:28:39 +01:00
Hendrik Brummermann
581ffc886a use ImageBitmap and ImageManager 2026-02-09 00:32:54 +01:00
Hendrik Brummermann
e2146721d1 use ImageBitmap in ActivityIndicatorSprite 2026-02-08 22:02:29 +01:00
Hendrik Brummermann
c6a51ffcf3 converted Blood from SpriteStore to ImageManager 2026-02-08 18:45:22 +01:00
Hendrik Brummermann
d846fa5818 created an ImageSprite class for ImageManager ImageRefs and use it 2026-02-08 18:45:05 +01:00
Hendrik Brummermann
13fc7f6b1a deleted unused classes EmptySprite, ImageSprite, Sprite, Tileset 2026-02-08 16:45:36 +01:00
Hendrik Brummermann
86cf9c0959 deleted class ImageSprite 2026-02-08 15:19:16 +01:00
Hendrik Brummermann
7c5cef1181 fixed ctrl direction change 2026-02-08 00:27:36 +01:00
Hendrik Brummermann
72f23a7bc1 call super.destroy() 2026-02-07 22:24:24 +01:00
Hendrik Brummermann
404263a1ef code cleanup 2026-02-07 22:23:33 +01:00
Hendrik Brummermann
b479ec3d52 use ImageManager instead of SpriteStore for weather images 2026-02-07 02:04:40 +01:00
Hendrik Brummermann
b796ba36b0 use ImageManager instead of SpriteStore 2026-02-07 02:01:55 +01:00
Hendrik Brummermann
07a41fddfe cache tileset-images to speed up zone changes 2026-02-07 02:01:21 +01:00
Hendrik Brummermann
97ea5ab442 use ImageManager 2026-02-05 22:09:59 +01:00
Hendrik Brummermann
659c155a36 apply preferences to own outfit 2026-02-05 22:09:39 +01:00
Hendrik Brummermann
0f3ecbd3dd code cleanup 2026-02-05 22:08:12 +01:00
Hendrik Brummermann
535e7f5727 use ImageManager images for preview of outfit parts into Outfit Dialog 2026-02-04 22:57:48 +01:00
Hendrik Brummermann
1bf34316fb support ImageRef.free() without going through the ImageManager 2026-02-04 22:57:01 +01:00
Hendrik Brummermann
c7befd2fe1 support cleanup of ImageManager 2026-02-04 22:15:01 +01:00
Hendrik Brummermann
230b25e5fa added support for filtered images 2026-02-02 22:10:18 +01:00
Hendrik Brummermann
3b6d652aa8 dont return the image in waitFor because it might have expired 2026-02-02 22:04:07 +01:00
Hendrik Brummermann
41604ca00e code cleanup 2026-02-02 21:59:34 +01:00
Hendrik Brummermann
7e0fbe0f16 split ImageRef and ImageRefImpl to hide management methods 2026-02-02 20:54:03 +01:00
Hendrik Brummermann
2bd16f0307 implemented ImageRef.waitFor() 2026-02-01 22:43:01 +01:00
Hendrik Brummermann
afb06b8f97 started to work on ImageManager and ImageRef 2026-02-01 22:25:24 +01:00
Hendrik Brummermann
95f373bd1c removed reference to global stendhal object 2026-02-01 22:25:06 +01:00
Hendrik Brummermann
e58e6a54cb do not use failsave sprite for drag image 2026-01-31 23:11:37 +01:00
Hendrik Brummermann
f109b551f5 optimized image creation 2026-01-31 23:10:42 +01:00
Hendrik Brummermann
5f4d9d9460 moved SpriteStore.getAreaOf() to HTMLImageElementUtil 2026-01-31 21:28:43 +01:00
Hendrik Brummermann
61f047a955 prevent negative positions of dialogs 2026-01-31 17:18:58 +01:00
Hendrik Brummermann
3534bf384f close outfit dialog preview ImageBitmap 2026-01-31 17:06:49 +01:00
Hendrik Brummermann
6b1296e059 renamed MultilayerImage to MultilayerImageLoader 2026-01-31 15:50:39 +01:00
Hendrik Brummermann
c19e4b0627 added filter support to MultilayerImage 2026-01-31 15:50:19 +01:00
Hendrik Brummermann
72d569343a implemented MultilayerImage loader 2026-01-31 14:55:56 +01:00
Hendrik Brummermann
a6708ee0c2 removed references to stendhal.data.map and SingletonRepo.getMap() 2026-01-25 23:43:17 +01:00
Hendrik Brummermann
e0dce1a242 access TileMap directly without referencing stendhal.data.map 2026-01-25 23:33:25 +01:00
Hendrik Brummermann
55e802b988 changed module and moduleResolution for better compatibility 2026-01-25 23:09:08 +01:00
Hendrik Brummermann
d24e452f59 inlined LandscapeRender.ts into CombinedTilesetRenderingStrategy.ts 2026-01-25 23:08:26 +01:00
Hendrik Brummermann
8fc173d923 replaced tileset loader "ImagePreloader" as ImageCache using ImageBitmap 2026-01-25 20:01:31 +01:00
Hendrik Brummermann
1484f872da code cleanup 2026-01-25 19:59:25 +01:00
Hendrik Brummermann
93a99c9c10 moved CombinedTilesetRenderingStrategy out of LandscapeRenderingStrategy 2026-01-25 01:53:13 +01:00
Hendrik Brummermann
79ba39a030 fixed moduleResolution setting 2026-01-25 01:41:35 +01:00
Hendrik Brummermann
1c50a2a454 moved sprite/image/ImageFilter out of data/SpriteStore 2026-01-25 01:40:28 +01:00
Hendrik Brummermann
4feb1e628a use OffscreenCanvas to create outfit sprite 2026-01-24 23:13:03 +01:00
Hendrik Brummermann
662aab6ded use ImageBitmap for minimap static collisions 2026-01-24 18:39:35 +01:00
Hendrik Brummermann
f9fc0230f5 use OffscreenCanvas 2026-01-24 15:13:40 +01:00
Hendrik Brummermann
ba6d507924 inlined DrawingStage into Outfit 2026-01-23 23:00:04 +01:00
Hendrik Brummermann
e121d44356 added TODOs and fixed compiler warnings 2026-01-23 22:47:34 +01:00
Hendrik Brummermann
8b61de02e6 converted Singleton DrawingStage to instance and use Offscreen 2026-01-23 22:47:17 +01:00
Hendrik Brummermann
1ca1f0e47e fixed undefined _hsMap() error 2026-01-23 21:31:57 +01:00
Jordan Irwin
3026afe931
Manually build code in CodeQL workflow as default target has changed 2026-01-20 21:52:22 -08:00
Jordan Irwin
f9a37f122e
Update instances of CodeQL Action to v3 as v2 is now deprecated 2026-01-20 21:39:33 -08:00
Jordan Irwin
fd2cff8618
Clean up README 2026-01-20 20:27:21 -08:00
Jordan Irwin
30494af57c
Fix running server with 'ant server_run' 2026-01-20 19:41:59 -08:00
Jordan Irwin
2ed431fd6a
Add Spanish translations for lemon, banana, & corn 2026-01-20 01:27:31 -08:00
Jordan Irwin
cc4918a5ed
Add Spanish translation for map description 2026-01-20 01:26:38 -08:00
Jordan Irwin
6608eb2b02
Add Spanish translations for some newer items 2026-01-20 01:26:09 -08:00
Jordan Irwin
79622ee9cf
Update Spanish translations for some renamed items 2026-01-20 01:12:57 -08:00
Jordan Irwin
c6f2718aec
Remove trailing periods from translated names 2026-01-20 01:12:06 -08:00
Jordan Irwin
1fd97009eb
Update Spanish translations of item & entity descriptions 2026-01-20 00:19:55 -08:00
Jordan Irwin
517b62c5b1
Update Spanash locale file header 2026-01-20 00:19:29 -08:00
Jordan Irwin
de36580350
Run 'ant locale_update' 2026-01-20 00:14:16 -08:00
Jordan Irwin
3d971f37b5
Update locale template 2026-01-20 00:13:41 -08:00
Jordan Irwin
4666911be6
Don't need English language file 2026-01-20 00:13:30 -08:00
AntumDeluge
c4723aba93 Merge pull request 'Clean Up Ant Build' (#32) from AntumDeluge/stendhal:build_cleanup into master
Reviewed-on: https://codeberg.org/arianne/stendhal/pulls/32
2026-01-20 03:47:18 +01:00
Jordan Irwin
8c93122d1e
Rename target locale_build_template to locale_template 2026-01-19 18:07:18 -08:00
Jordan Irwin
2bec3a151b
Remove unneeded aliases 2026-01-19 18:03:56 -08:00
Jordan Irwin
38f5c0124d
Remove build target descriptions
Descriptions now available in `ant help` target.
2026-01-19 18:03:56 -08:00
Jordan Irwin
2df3243f2d
Make 'help' default target 2026-01-19 18:03:55 -08:00
Jordan Irwin
08873546ba
Add help info to ant build 2026-01-19 18:03:47 -08:00
Jordan Irwin
26c4723749
Move build properties to sub-module 2026-01-19 17:38:39 -08:00
Jordan Irwin
6eeff4766d
Make compile_stendhaltools target alias of compile_tools 2026-01-19 17:38:39 -08:00
Jordan Irwin
737cac9495
Make compile_stendhaltextclient build target alias of compile_textclient 2026-01-19 17:38:33 -08:00
Hendrik Brummermann
5ae5d33438 updated marauroa.jar
Some checks failed
Java CI / build (push) Has been cancelled
2026-01-18 21:41:30 +01:00
Hendrik Brummermann
621a43579a fixed import 2026-01-18 21:05:29 +01:00
Hendrik Brummermann
45cd2ac30e moved onPerceptionEnd method from Client to StendhalPerceptionListener 2026-01-18 14:13:13 +01:00
Hendrik Brummermann
b328b1aedf moved RPObject, RPEvent and RPSlot from Stendhal to Marauroa 2026-01-17 23:50:58 +01:00
Hendrik Brummermann
d633ebf947 use instance of Marauroa class instead of global marauroa object 2026-01-17 15:15:33 +01:00
Hendrik Brummermann
f4935f4928 updated marauroa.jar 2026-01-16 22:07:55 +01:00
Hendrik Brummermann
b71a29f883 user register for RPObjects 2026-01-16 22:04:56 +01:00
Hendrik Brummermann
3424b898b8 created dedicated CorpseSlot class 2026-01-16 22:04:20 +01:00
Hendrik Brummermann
1f3abca39c use register for events 2026-01-16 22:03:57 +01:00
Hendrik Brummermann
d6ca574a20 fixed references to wrong "ui" variable 2026-01-15 21:42:09 +01:00
Hendrik Brummermann
612b4a6b88 code cleanup 2026-01-14 23:09:37 +01:00
Hendrik Brummermann
a13ee36fe8 converted all events to TypeScript 2026-01-14 22:56:50 +01:00
Jordan Irwin
2abc8df666
Run 'ant cleanwhitespace' 2026-01-13 19:12:13 -08:00
Jordan Irwin
14d7e046be
Process ant build files in 'cleanwhitespace' target 2026-01-13 19:09:24 -08:00
Jordan Irwin
6c64621496
Don't try to clean removed 'tiled' directory 2026-01-13 19:09:23 -08:00
Hendrik Brummermann
6a4009c790 fixed build 2026-01-13 23:07:55 +01:00
Hendrik Brummermann
38934a8c09 fixed local builds after esbuild migration 2026-01-13 09:13:54 +01:00
Hendrik Brummermann
738d3cd830 explicitly define marauroa global variable 2026-01-10 00:24:12 +01:00
Hendrik Brummermann
8cb1a7b050 converted stendhal object into a static class 2026-01-09 00:06:37 +01:00
Hendrik Brummermann
d1fc6947b7 use esbuild 2026-01-06 21:45:58 +01:00
Hendrik Brummermann
402bb84c43 fixed this-reference and redirect to character selection on disconnect 2026-01-06 21:44:41 +01:00
Hendrik Brummermann
819b80913f added esbuild as devDependencies 2026-01-06 21:43:57 +01:00
Hendrik Brummermann
da4378d30c updated marauroa 2026-01-06 21:43:35 +01:00
Hendrik Brummermann
37d3e1c84d fixed init order and wrong "this". 2026-01-02 04:11:34 +01:00
Hendrik Brummermann
f1d98fff56 access SpriteStore via singletons instead of stendhal-variable 2026-01-02 04:11:33 +01:00
Jordan Irwin
8f642ac446
Increment version to 1.49.5 2026-01-01 18:33:01 -08:00
Jordan Irwin
6224a0638a
Run 'ant init' to update copyright year 2026-01-01 18:31:27 -08:00
Jordan Irwin
c38740eaab
Use 'versionName' instead of 'versionCode' in Android output filename 2026-01-01 18:24:56 -08:00
Hendrik Brummermann
cbf971e632 replace stendhal.paths with Paths to ensure correct init order 2026-01-02 01:37:39 +01:00
Hendrik Brummermann
7546fbba80 removed unused library 2025-12-30 00:24:38 +01:00
Hendrik Brummermann
face9f3a67 use OffscreenCanvas and ImageBitmap instead of img- and canvas-elements 2025-12-28 18:59:53 +01:00
Hendrik Brummermann
703fa61329 cleaned up references to animations 2025-12-28 18:13:15 +01:00
Hendrik Brummermann
4dbe1f73fe implemented light rendering (to test "/debug light" and change zone) 2025-12-28 17:51:51 +01:00
Hendrik Brummermann
09371bff47 restore transparency after blending 2025-12-28 14:41:39 +01:00
Hendrik Brummermann
6ff59a0040 refactored drawCombinedTileAtIndex and drawTile in preparation for blend 2025-12-28 12:40:28 +01:00
Hendrik Brummermann
063d03c45f used Canvas and RenderingContext2D to allow OffscreenCanvas 2025-12-28 12:32:20 +01:00
Hendrik Brummermann
d191c14232 updated typescript and typedoc 2025-12-28 12:31:20 +01:00
Hendrik Brummermann
d9b51f0d15 disabled code to draw lights (alpha is broken) 2025-12-28 10:04:14 +01:00
Hendrik Brummermann
44f6a4a9fe marked event listener as passive to prevent scroll delay 2025-12-27 13:10:16 +01:00
Hendrik Brummermann
d1d4eb5c49 removed setting alpha in applyHSLFilter method 2025-12-27 13:00:05 +01:00
Hendrik Brummermann
0d56d2b63b renamed Map to TileMap and use type definition 2025-12-26 14:57:00 +01:00
Hendrik Brummermann
a000e5f788 made blend layer available 2025-12-26 14:41:21 +01:00
Jordan Irwin
cc5414894f
Use JDK version 17 in Android Build Test workflow
Current bundled Gradle Wrapper version is 7.5.1, which isn't
compatible with more recent Java releases.
2025-12-24 22:10:38 -08:00
Jordan Irwin
2e5dbc12c6
Validate Gradle Wrapper .jar in Android Build Test workflow 2025-12-24 22:01:09 -08:00
Jordan Irwin
f756534846
Validate Gradle Wrapper distribution checksum not .jar checksum 2025-12-24 21:59:21 -08:00
Jordan Irwin
7174c32521
Send Android Build error report to Discord dev channel 2025-12-24 18:21:32 -08:00
Jordan Irwin
049b91e98b
Don't send details to Discord 2025-12-24 18:21:01 -08:00
Jordan Irwin
a5db37305e
Fix 'run' line 2025-12-24 18:13:57 -08:00
Jordan Irwin
179bc4d5e7
Fix code alignment in Android Build Test GitHub workflow 2025-12-24 18:10:25 -08:00
Jordan Irwin
31c4f9cfb4
Discord webhook for Android Build Test failure 2025-12-24 18:07:00 -08:00
Jordan Irwin
c4491004d2
Parse job ID in Android Build Test GitHub workflow 2025-12-24 18:05:36 -08:00
Jordan Irwin
e910892a16
Build .apk packages using Ant in Android Build Test GitHub workflow 2025-12-24 17:31:39 -08:00
Jordan Irwin
7beb123117
Allow dispatching Android build workflow manually 2025-12-24 17:11:58 -08:00
Hendrik Brummermann
a141cc187d properly format item names in description text of shop windows 2025-12-22 19:21:51 +01:00
Hendrik Brummermann
c68f0a59d5 show outfits in shop sign dialog 2025-12-22 18:06:00 +01:00
Hendrik Brummermann
a3bd4b328a code cleanup 2025-12-22 18:05:49 +01:00
Hendrik Brummermann
9e56945faf use proper table for item shop signs 2025-12-22 17:31:24 +01:00
Hendrik Brummermann
6f783d3ef6 moved ShowItemListEvent and ShowOutfitListEvent out of EventRegistry 2025-12-22 17:02:31 +01:00
223 changed files with 7363 additions and 8003 deletions

View file

@ -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

View file

@ -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

1
.gitignore vendored
View file

@ -50,4 +50,3 @@ build_server_xmlconf
/node_modules/
/log/
/build-archive/
/bin/

View file

@ -20,4 +20,15 @@
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
<filteredResources>
<filter>
<id>1771748271774</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -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.
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](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)<br>
You can double click on stendhal-starter.jar and the client will run.<br>
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).<br>
You can double click on `stendhal-starter.jar` and the client will run.<br>
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<br>
Stendhal(c) is copyright of the Arianne Project, 2006-2025, 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).

View file

@ -10,8 +10,8 @@ android {
applicationId namespace
minSdk 21
targetSdk 32
versionCode 1049000
versionName "1.49"
versionCode 1049005
versionName "1.49.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 32
minSdkVersion 21
@ -26,7 +26,7 @@ android {
}
// 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

View file

@ -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()));

View file

@ -1,5 +1,3 @@
The outfit merchants display images of their products now. In order not to fall behind, item sellers have polished their shop signs, to highlight item names.
This in turn prompted Gwen at Deniarn Accessories to cut her prices in half. All in all, it is a good time for some late Christmas shopping.
PUT RELEASE ANNOUNCEMENT LINES HERE
Please see the complete change log for further details: https://arianne-project.org/game/stendhal.html#changes

View file

@ -5,6 +5,6 @@
<string name="app_name">Stendhal</string>
<string name="intent_url_scheme">stendhalprod8gps5y99pu</string>
<string name="build_type">prod</string>
<string name="build_version">1.49</string>
<string name="build_version">1.49.5</string>
</resources>

View file

@ -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

View file

@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
"applicationId": "org.stendhalgame.client",
"version": "1.49",
"version": "1.49.5",
"defaultMode": "window",
"port": 0,
"documentRoot": "/resources/",

View file

@ -1,6 +1,6 @@
@echo off
set STENDHAL_VERSION=1.49
set STENDHAL_VERSION=1.49.5
set SERVER_JAR=stendhal-server-%STENDHAL_VERSION%.jar
:: change to server directory

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
STENDHAL_VERSION="1.49"
STENDHAL_VERSION="1.49.5"
SERVER_JAR="stendhal-server-${STENDHAL_VERSION}.jar"
# change to server directory

View file

@ -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.48
version = 1.49
version.old = 1.49
version = 1.49.5
# FIXME: dynamic method to set this property?
version.android = 1049000
version.android = 1049005
# javac options
javac.deprecation = true

View file

@ -1,81 +1,32 @@
<?xml version='1.0'?>
<project name="stendhal" default="compile" basedir=".">
<!--
Sets a property (by name and value), or set of properties (from file or resource) in the project. Properties are case sensitive.
Properties are immutable: whoever sets a property first freezes it for the rest of the build; they are most definitely not variables.
-->
<project name="stendhal" default="help" basedir=".">
<description>
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.
</description>
<property file="build.ant-private.properties"/>
<property file="build.ant.properties"/>
<property name="build_tests" value="${buildroot}/build_tests"/>
<property name="build_tests_report" value="${buildroot}/build_test_report"/>
<property name="build_client" value="${buildroot}/build_client"/>
<property name="build_client_data" value="${buildroot}/build_client_data"/>
<property name="build_client_sound_data" value="${buildroot}/build_client_sound_data"/>
<property name="build_client_music_data" value="${buildroot}/build_client_music_data"/>
<property name="build_webclient" value="${buildroot}/build_webclient"/>
<property name="build_docker" value="${buildroot}/build_docker"/>
<property name="buildtools_docker" value="buildtools/docker"/>
<property name="build_postman" value="${buildroot}/buildbot/"/>
<property name="build_server" value="${buildroot}/build_server"/>
<property name="build_server_maps" value="${buildroot}/build_server_maps"/>
<property name="build_server_script" value="${buildroot}/build_server_script"/>
<property name="build_server_mods" value="${buildroot}/build_server_mods"/>
<property name="build_server_xmlconf" value="${buildroot}/build_server_xmlconf"/>
<property name="build_stendhaltools" value="${buildroot}/build_stendhaltools"/>
<property name="build_stendhaltextclient" value="${buildroot}/build_stendhaltextclient"/>
<property name="client_data" value="stendhal-data-${version}.jar"/>
<property name="client_jarname" value="stendhal-${version}.jar"/>
<property name="client_sound_data" value="stendhal-sound-data-${version}.jar"/>
<property name="client_music_data" value="stendhal-music-data-${version}.jar"/>
<property name="client_starter_jarname" value="stendhal-starter-${version}.jar"/>
<property name="dist_server_binary" value="${buildroot}/stendhal-server-${version}.zip" />
<property name="maps_jarname" value="stendhal-maps-${version}.jar"/>
<property name="server_jarname" value="stendhal-server-${version}.jar"/>
<property name="xmlconf_jarname" value="stendhal-xmlconf-${version}.jar"/>
<property name="config_dir" value="data/conf"/>
<property name="schema_location" value="${config_dir}"/>
<property name="cobertura.dir" value="${libdir}/cobertura"/>
<condition property="exclude.signing" value="true">
<not>
<available file="keystore.ks"/>
</not>
</condition>
<condition property="exclude.jardiff" value="true">
<not>
<available file="${build-archive}/stendhal-${version.old}.zip"/>
</not>
</condition>
<!-- time & date -->
<tstamp>
<format property="year" pattern="yyyy"/>
<format property="date" pattern="yyyy-MM-dd"/>
<format property="time" pattern="HH:mm:ss"/>
</tstamp>
<property name="datetime" value="${date} ${time}"/>
<!-- Ant sub-modules -->
<import file="build-private.xml" optional="true"/>
<import file="${ant_modules}/properties.xml"/>
<import file="${ant_modules}/info.xml"/>
<import file="${ant_modules}/system.xml"/>
<import file="${ant_modules}/apps.xml"/>
<import file="${ant_modules}/main.xml"/>

View file

@ -161,7 +161,8 @@
</target> <!-- client_android_build -->
<target name="android" description="Create Android client package for release and distribution.">
<!-- Create Android client package for release and distribution. -->
<target name="android">
<echo
if:set="exclude.webview"
message="ANDROID_SDK_ROOT environment variable not set. Skipping Android WebView client build."/>

View file

@ -2,7 +2,8 @@
<project xmlns:if="ant:if" xmlns:unless="ant:unless">
<target name="clean" description="Delete all build files and directories.">
<!-- Delete all build files and directories. -->
<target name="clean">
<delete includeEmptyDirs="true" quiet="true">
<fileset dir="${buildroot}" excludes="eclipse/" defaultExcludes="false"/>
</delete>
@ -18,14 +19,16 @@
</target>
<target name="clean_client_data" description="Delete client data build files and directories.">
<!-- Delete client data build files and directories. -->
<target name="clean_client_data">
<delete dir="${build_client_data}"/>
<delete dir="${build_client_music_data}"/>
<delete dir="${build_client_sound_data}"/>
</target>
<target name="cleanjs" description="Delete web client build files and directories.">
<!-- Delete web client build files and directories. -->
<target name="cleanjs">
<delete dir="${buildroot}/js"/>
<delete dir="${buildroot}/ts"/>
</target>

View file

@ -19,13 +19,15 @@
</condition>
<target name="jar" depends="client_build,server_build" description="Build server and clients.">
<!-- Build server and clients. -->
<target name="jar" depends="client_build,server_build">
<!-- XXX: client & server builds are not exclusive to NetBeans, remove this output? -->
<echo>Built client and server for NetBeans.</echo>
</target>
<target name="signjar" unless="exclude.signing" description="Sign client distribution.">
<!-- Sign client distribution. -->
<target name="signjar" unless="exclude.signing">
<signjar
alias="${keystore.alias}"
keystore="keystore.ks"
@ -34,20 +36,23 @@
</target>
<!-- Generate JAR files for both client and server. -->
<!-- XXX: why does this depend on "server_build" twice? -->
<target name="compile" depends="server_build,client_build,server_build" description="Generate JAR files for both client and server."/>
<target name="compile" depends="server_build,client_build,server_build" />
<target name="dist" depends="clean,dist_binary,dist_source,signupdates" description="Create all packages for Stendhal release."/>
<!-- Create all packages for Stendhal release. -->
<target name="dist" depends="clean,dist_binary,dist_source,signupdates" />
<target name="dist_binary" depends="clean,dist_server_binary,dist_client_binary,dist_client_starter,dist_webclient_binary"
description="Create all binary packages for Stendhal release."/>
<!-- Create all binary packages for Stendhal release. -->
<target name="dist_binary" depends="clean,dist_server_binary,dist_client_binary,dist_client_starter,dist_webclient_binary" />
<!-- START: source -->
<target name="dist_source" description="Create all source packages for Stendhal release.">
<!-- Create all source packages for Stendhal release. -->
<target name="dist_source">
<mkdir dir="${buildroot}/stendhal-${version}-src"/>
<copy todir="${buildroot}/stendhal-${version}-src/src">
@ -104,7 +109,8 @@
<!-- START: client data -->
<target name="build_client_data" description="Build client data files.">
<!-- Build client data files. -->
<target name="build_client_data">
<!-- base data -->
<mkdir dir="${build_client_data}"/>
<copy todir="${build_client_data}/data/sprites">
@ -179,7 +185,8 @@
<!-- START: server -->
<target name="server_build" depends="init,checkxml" description="Compile JAR file for Stendhal server.">
<!-- Compile JAR file for Stendhal server. -->
<target name="server_build" depends="init,checkxml">
<mkdir dir="${build_server}"/>
<copy todir="${build_server}/data/conf" file="data/conf/stendhalcreateaccount.properties"/>
@ -340,7 +347,8 @@
</target> <!-- server_build -->
<target name="dist_server_binary" depends="server_build" description="Create server package for release and distribution.">
<!-- Create server package for release and distribution. -->
<target name="dist_server_binary" depends="server_build">
<mkdir dir="${buildroot}/stendhal-server-${version}"/>
<copy todir="${buildroot}/stendhal-server-${version}/data/script">
@ -392,10 +400,12 @@
</target> <!-- dist_server_binary -->
<target name="dist_docker" depends="docker_build,docker_push" description="Build and push container image."/>
<!-- Build and push container image. -->
<target name="dist_docker" depends="docker_build,docker_push" />
<target name="docker_build_prepare" depends="dist_server_binary" description="Prepare container image build.">
<!-- Prepare container image build. -->
<target name="docker_build_prepare" depends="dist_server_binary">
<mkdir dir="${build_docker}" />
<copy file="${buildtools_docker}/Dockerfile" todir="${build_docker}" />
<copy file="${buildtools_docker}/.dockerignore" todir="${build_docker}" />
@ -404,7 +414,8 @@
</target> <!-- docker_build_prepare -->
<target name="docker_build" depends="docker_build_prepare" description="Build container image.">
<!-- Build container image. -->
<target name="docker_build" depends="docker_build_prepare">
<exec dir="${build_docker}" executable="docker">
<arg value="build" />
<arg value="-t" />
@ -414,28 +425,57 @@
</target> <!-- docker_build -->
<target name="docker_push" description="Push container image."/>
<!-- Push container image. -->
<target name="docker_push" />
<target name="server_run" depends="server_build,compilejs" description="Run server.">
<mkdir dir="run_srvr"/>
<!-- Run server. -->
<target name="server_run" depends="server_build,compilejs">
<mkdir dir="run_srvr/libs"/>
<mkdir dir="run_srvr/js"/>
<copy todir="run_srvr">
<fileset file="build/lib/${server_jarname}"/>
<fileset file="build/lib/${xmlconf_jarname}"/>
<fileset file="build/lib/${maps_jarname}"/>
<fileset file="libs/marauroa.jar"/>
<fileset file="libs/groovy.jar"/>
<fileset file="libs/guava.jar"/>
<fileset file="libs/log4j.jar"/>
<fileset file="libs/${mysqldriver_jarname}"/>
</copy>
<copy todir="run_srvr/libs">
<fileset file="${marauroa_jar}" />
<fileset file="${groovy_jar}" />
<fileset file="${guava_jar}" />
<fileset file="${log4j_jar}" />
<fileset file="${mysqldriver_jar}" />
<fileset file="${h2_jar}" />
<fileset file="${hamcrest_jar}" />
<fileset file="${tomcatcore_jar}" />
<fileset file="${tomcatwebsocket_jar}" />
<fileset file="${jakartaannotation_jar}" />
<fileset file="${luaj_jar}" />
<fileset file="${jsonsimple_jar}" />
</copy>
<copy todir="run_srvr/js">
<fileset dir="${srcjs}">
<exclude name="stendhal/" />
<exclude name="stendhal-compiled.js.map" />
<exclude name=".gitignore" />
</fileset>
</copy>
<chmod file="run_srvr/${server_jarname}" perm="+x"/>
<exec dir="." executable="java">
<arg line="-Xmx256m -cp run_srvr -jar run_srvr/${server_jarname} -c server.ini -l"/>
</exec>
<java jar="run_srvr/${server_jarname}"
fork="true"
failonerror="true"
maxmemory="256m">
<arg value="-c" />
<arg value="server.ini" />
<arg value="-l" />
<classpath>
<pathelement location="${basedir}" />
</classpath>
</java>
<!-- we've run the program, so delete the jars -->
<delete dir="run_srvr"/>
@ -446,7 +486,8 @@
<!-- START: java client -->
<target name="client_build" depends="init,build_client_data" description="Compile client JAR file.">
<!-- Compile client JAR file. -->
<target name="client_build" depends="init,build_client_data">
<mkdir dir="${build_client}"/>
<copy todir="${build_client}/games/stendhal/client">
@ -539,7 +580,8 @@
</target> <!-- client_build -->
<target name="compile_stendhaltextclient" description="Compile text client.">
<!-- Compile text client. -->
<target name="compile_textclient">
<mkdir dir="${build_stendhaltextclient}"/>
<javac srcdir="${src}" destdir="${build_stendhaltextclient}" source="1.8" target="1.8" debug="${javac.debug}" debuglevel="${javac.debuglevel}" deprecation="${javac.deprecation}" includeantruntime="false">
@ -560,8 +602,12 @@
</javac>
</target> <!-- compile_stendhaltextclient -->
<!-- Alias of `compile_textclient`. -->
<target name="compile_stendhaltextclient" depends="compile_textclient" />
<target name="dist_client_binary" depends="clean_client_data,client_build" description="Create Java client package for release and distribution.">
<!-- Create Java client package for release and distribution. -->
<target name="dist_client_binary" depends="clean_client_data,client_build">
<mkdir dir="${buildroot}/stendhal-${version}"/>
<mkdir dir="${buildroot}/stendhal-${version}/lib"/>
<mkdir dir="${buildroot}/stendhal-${version}/log"/>
@ -597,8 +643,8 @@
</target> <!-- dist_client_binary -->
<!-- XXX: is this still used? -->
<target name="dist_client_starter" depends="client_build" description="Create Java client starter package.">
<!-- Create Java client starter package (obsolete). -->
<target name="dist_client_starter" depends="client_build">
<mkdir dir="${buildroot}/stendhal-${version}"/>
<copy todir="${buildroot}/stendhal-${version}" file="${lib}/${client_starter_jarname}"/>
<copy todir="${buildroot}/stendhal-${version}/lib">
@ -620,7 +666,8 @@
</target> <!-- dist_client_starter -->
<target name="client_run" depends="client_build" description="Run Java client.">
<!-- Run Java client. -->
<target name="client_run" depends="client_build">
<mkdir dir="run_clnt"/>
<mkdir dir="run_clnt/lib"/>
@ -656,8 +703,9 @@
</target> <!-- client_run -->
<!-- Run client for NetBeans trick. -->
<!-- Netbeans won't let you choose ant options, so this does it for us-->
<target name="run" depends="client_run" description="Run client for NetBeans trick."/>
<target name="run" depends="client_run" />
<!-- END: java client -->
@ -665,30 +713,42 @@
<!-- START: web client -->
<target name="js-prepare-marauroa">
<mkdir dir="build/js"/>
<unzip dest="build" src="libs/marauroa.jar">
<mkdir dir="node_modules/marauroa"/>
<unzip dest="node_modules/marauroa" src="libs/marauroa.jar">
<mapper type="flatten"/>
<patternset>
<include name="js/**"/>
</patternset>
</unzip>
</target>
<target name="js-prepare-build">
<echo file="build/js/build.js">
var stendhal = window.stendhal = window.stendhal || {};
stendhal.data = stendhal.data || {};
<mkdir dir="node_modules/marauroa"/>
<echo file="node_modules/marauroa/build.js">
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
}
</echo>
<antcall target="js-prepare-build-ci" />
</target>
<target name="js-prepare-build-ci" if="ci.build.info">
<loadfile property="cibuildinfo" srcFile="${ci.build.info}" failonerror="false" quiet="true"/>
<replace file="node_modules/marauroa/build.js" token="//CI" value="${cibuildinfo}" />
</target>
<extension-point name="js-prepare" depends="js-prepare-marauroa,js-prepare-build,js-prepare-node,js-prepare-contribs"/>
<extension-point name="js-prepare" depends="js-prepare-node,js-prepare-contribs,js-prepare-marauroa,js-prepare-build"/>
<target name="js-prepare-node">
@ -713,72 +773,19 @@
<!-- TODO: Do not write stendhal-compiled.js to the source directory tree but directly to the compiled directory tree -->
<target name="js-invoke-compiler">
<exec executable="${executable.npx}" dir="${srcjs}/stendhal" failonerror="true">
<arg value="tsc"/>
</exec>
<property name="js-debug" value="--debug --formatting=PRETTY_PRINT --create_source_map ${srcjs}/stendhal-compiled.js.map --source_map_include_content"/>
<property name="js-files-license" value="${srcjs}/stendhal/license.js"/>
<property name="js-files-marauroa" value="build/js/marauroa.js build/js/client-framework.js build/js/message-factory.js build/js/perception.js build/js/rpfactory.js build/js/inflate.js build/js/deserializer.js"/>
<property name="js-files-config" value="build/js/build.js"/>
<property name="js-files-lib1" value="${srcjs}/jsxgraph-util.js"/>
<property name="js-files-lib2" value=""/>
<property name="js-files-data1" value="${srcjs}/stendhal/data/sha3.js"/>
<property name="js-files-data2" value=""/>
<property name="js-files-entity5" value=""/>
<property name="js-files-entity6" value=""/>
<property name="js-files-ui1" value=""/>
<property name="js-files-ui2" value=""/>
<property name="js-files-ui3" value=""/>
<property name="js-files-main1" value=""/>
<property name="js-files-ui4" value=""/>
<property name="js-files-main2" value=""/>
<property name="js-files-main3" value=""/>
<property name="js-files-ts" value="build/ts/*.js build/ts/**/*.js"/>
<property name="js-files-ts2" value=""/>
<java jar="buildtools/closure-compiler/compiler.jar"
fork="true"
failonerror="true"
maxmemory="128m">
<arg value="--language_in=ECMASCRIPT_2020"/>
<arg value="--language_out=STABLE"/>
<arg value="--compilation_level"/>
<arg value="ADVANCED"/>
<arg line="${js-debug}"/>
<arg value="--process_common_js_modules"/>
<arg value="--module_resolution=node"/>
<arg value="--jscomp_off=checkVars"/>
<!-- DEBUG: -->
<!--<arg value="-\-jscomp_off=globalThis"/>-->
<arg value="--js_output_file=${srcjs}/stendhal-compiled.js"/>
<arg line="${js-files-license}"/>
<arg line="${js-files-marauroa}"/>
<arg line="${js-files-config}"/>
<arg line="${js-files-lib1}"/>
<arg line="${js-files-lib2}"/>
<arg line="${js-files-data1}"/>
<arg line="${js-files-data2}"/>
<arg line="${js-files-entity5}"/>
<arg line="${js-files-entity6}"/>
<arg line="${js-files-ui1}"/>
<arg line="${js-files-ui2}"/>
<arg line="${js-files-ui3}"/>
<arg line="${js-files-main1}"/>
<arg line="${js-files-ui4}"/>
<arg line="${js-files-main2}"/>
<arg line="${js-files-main3}"/>
<arg line="${js-files-ts}"/>
<arg line="${js-files-ts2}"/>
</java>
<echo file="${srcjs}/stendhal-compiled.js" append="true">
//# sourceMappingURL=stendhal-compiled.js.map
</echo>
<exec executable="${executable.npx}" dir="${srcjs}/stendhal" failonerror="true">
<arg value="esbuild"/>
<arg value="--bundle"/>
<arg value="--outfile=../stendhal-compiled.js"/>
<arg value="--platform=browser"/>
<!-- <arg value="- -minify"/> -->
<arg value="main.ts"/>
</exec>
<antcall target="compilejs-hook" />
@ -795,7 +802,8 @@
</target>
<target name="compilejs" depends="cleanjs" description="Compile web client.">
<!-- Compile web client. -->
<target name="compilejs" depends="cleanjs">
<antcall target="js-prepare" unless:set="exclude.webclient"/>
<antcall target="js-invoke-compiler" unless:set="exclude.webclient"/>
@ -804,7 +812,8 @@
</target> <!-- compilejs -->
<target name="webclient_build" depends="compilejs,clean_client_data,build_client_data" description="Compile and stage web client files.">
<!-- Compile and stage web client files. -->
<target name="webclient_build" depends="compilejs,clean_client_data,build_client_data">
<mkdir dir="${build_webclient}"/>
<copy todir="${build_webclient}">
@ -826,10 +835,8 @@
</target> <!-- webclient_build -->
<target name="build_webclient" depends="webclient_build" description="Alias of webclient_build."/>
<target name="dist_webclient_binary" depends="webclient_build" description="Create web client package for release and distribution.">
<!-- Create web client package for release and distribution. -->
<target name="dist_webclient_binary" depends="webclient_build">
<zip destfile="${buildroot}/stendhal-webclient-${version}.zip">
<zipfileset dir="${build_client_data}"/>
<zipfileset dir="${build_client_music_data}"/>

View file

@ -12,7 +12,8 @@
<delete dir="${buildroot}/build_docs"/>
</target>
<target name="docs" description="Generate HTML documentation for Java, TypeScript, and Lua APIs." depends="luadocs,javadocs,tsdocs"/>
<!-- Generate HTML documentation for Java, TypeScript, and Lua APIs. -->
<target name="docs" depends="luadocs,javadocs,tsdocs"/>
<!-- Java documentation -->

View file

@ -0,0 +1,86 @@
<?xml version='1.0'?>
<project>
<target name="info" description="Print project build information.">
<echo level="info" message="Project:" />
<echo level="info" message=" Name: .......... ${game_name}" />
<echo level="info" message=" Version: ....... ${version}" />
<echo level="info" message=" Root: .......... ${basedir}" />
<echo level="info" />
<echo level="info" message="Build system:" />
<echo level="info" message=" Ant version: ... ${ant.version.number}" />
<echo level="info" message=" Ant home: ...... ${ant.location}" />
<echo level="info" message=" Java version: .. ${java.version}" />
<echo level="info" message=" Java home: ..... ${java.home}" />
</target>
<target name="help" description="Print project build and help information." depends="info">
<echo level="info" message="Usage:" />
<echo level="info" message=" ant &lt;target&gt; [&lt;target&gt;...]" />
<echo level="info" />
<echo level="info" />
<echo level="info" message="=== TARGETS ===" />
<echo level="info" />
<echo level="info" message="General:" />
<echo level="info" message=" info: ........................ Print project build information." />
<echo level="info" message=" help: ........................ Print project build and help information." />
<echo level="info" message=" init: ........................ Update version and resource info." />
<echo level="info" message=" checkxml: .................... Verify XML files are well-formed." />
<echo level="info" />
<echo level="info" message="Build:" />
<echo level="info" message=" server_build: ................ Compile JAR file for Stendhal server." />
<echo level="info" message=" client_build: ................ Compile client JAR file." />
<echo level="info" message=" webclient_build: ............. Compile and stage web client files." />
<echo level="info" message=" compile_textclient: .......... Compile text client." />
<echo level="info" message=" compile: ..................... Generate JAR files for both client and server." />
<echo level="info" message=" compilejs: ................... Compile web client." />
<echo level="info" message=" compile_tools: ............... Compile Stendhal build tools." />
<echo level="info" message=" server_run: .................. Run server." />
<echo level="info" message=" client_run: .................. Run Java client." />
<echo level="info" message=" postman: ..................... Build postman bot." />
<echo level="info" />
<echo level="info" message="Testing:" />
<echo level="info" message=" compile_tests: ............... Compile all tests." />
<echo level="info" message=" run_tests: ................... Run all tests." />
<echo level="info" message=" test: ........................ Compile and run all tests." />
<echo level="info" />
<echo level="info" message="Build cleaning:" />
<echo level="info" message=" clean: ....................... Delete all build files and directories." />
<echo level="info" message=" clean_client_data: ........... Delete client data build files and directories." />
<echo level="info" message=" cleanjs: ..................... Delete web client build files and directories." />
<echo level="info" />
<echo level="info" message="Source maintanence:" />
<echo level="info" message=" clean_whitespace: ............ Clean whitespace in source and other specified file types." />
<echo level="info" />
<echo level="info" message="Distribution:" />
<echo level="info" message=" dist_source: ................. Create all source packages for Stendhal release." />
<echo level="info" message=" dist_binary: ................. Create all binary packages for Stendhal release." />
<echo level="info" message=" dist_server_binary: .......... Create server package for release and distribution." />
<echo level="info" message=" dist_client_binary: .......... Create Java client package for release and distribution." />
<echo level="info" message=" dist_webclient_binary: ....... Create web client package for release and distribution." />
<echo level="info" message=" dist_client_starter: ......... Create Java client starter package (obsolete)." />
<echo level="info" message=" dist: ........................ Create all packages for Stendhal release." />
<echo level="info" message=" android: ..................... Create Android client package for release and distribution." />
<echo level="info" />
<echo level="info" message="Resources:" />
<echo level="info" message=" rendermaps: .................. Convert TMX maps to PNG images scaled 1:16." />
<echo level="info" message=" renderatlas: ................. Generate files for the Stendhal Atlas." />
<echo level="info" message=" renderworldmap: .............. Export world.tmx map to PNG image." />
<echo level="info" message=" updatemaps: .................. Load and save TMX maps to fix tilesets changes." />
<echo level="info" />
<echo level="info" message="Documentation:" />
<echo level="info" message=" docs: ........................ Generate HTML documentation for Java, TypeScript, and Lua APIs." />
<echo level="info" />
<echo level="info" message="Localization:" />
<echo level="info" message=" locale_template: ............. Creates a template for translating." />
<echo level="info" message=" locale_update: ............... Update translation files from template." />
<echo level="info" />
<echo level="info" message="Aliases:" />
<echo level="info" message=" compile_stendhaltextclient: .. Alias of `compile_textclient`." />
<echo level="info" message=" compile_stendhaltools: ....... Alias of `compile_tools`." />
<echo level="info" />
<echo level="info" message="Execute `ant -p -v` for a full list of build targets." />
</target>
</project>

View file

@ -2,7 +2,8 @@
<project xmlns:if="ant:if" xmlns:unless="ant:unless">
<target name="locale_build_template" description="Creates a template for translating.">
<!-- Creates a template for translating. -->
<target name="locale_template">
<echo unless:true="${python.present}" message="Python unavailable, cannot build locale template"/>
<exec executable="${executable.python}" if:true="${python.present}" failonerror="true">
@ -11,7 +12,8 @@
</exec>
</target>
<target name="locale_update" description="Update translation files from template.">
<!-- Update translation files from template. -->
<target name="locale_update">
<echo unless:true="${python.present}" message="Python unavailable, cannot update translations"/>
<exec executable="${executable.python}" if:true="${python.present}" failonerror="true">

View file

@ -2,7 +2,8 @@
<project xmlns:if="ant:if" xmlns:unless="ant:unless">
<target name="init" description="Update version and resource info.">
<!-- Update version and resource info. -->
<target name="init">
<mkdir dir="${lib}"/>
<!-- update version -->
@ -89,7 +90,8 @@
</target> <!-- init -->
<target name="checkxml" description="Verify XML files are well-formed.">
<!-- Verify XML files are well-formed. -->
<target name="checkxml">
<schemavalidate failonerror="yes" lenient="yes" warn="yes">
<schema namespace="stendhal" file="${config_dir}/zones.xsd"/>
<fileset dir="${config_dir}/zones">
@ -167,7 +169,8 @@
</target> <!-- checkxml -->
<target name="internalUpdatePrereleaseVersion" if="PRE_RELEASE" description="Add date to pre-release test client.">
<!-- Add date to pre-release test client. -->
<target name="internalUpdatePrereleaseVersion" if="PRE_RELEASE">
<tstamp prefix="date">
<format property="iso" pattern="yyyy-MM-dd"/>
</tstamp>

View file

@ -4,7 +4,8 @@
<import file="tools.xml" />
<target name="rendermaps" description="Convert TMX maps to PNG images scaled 1:16." depends="compile_stendhaltools">
<!-- Convert TMX maps to PNG images scaled 1:16. -->
<target name="rendermaps" depends="compile_stendhaltools">
<taskdef name="maprenderer" classname="games.stendhal.tools.MapRenderer">
<classpath>
<pathelement path="${build_stendhaltools}"/>
@ -23,7 +24,8 @@
</target> <!-- rendermaps -->
<target name="renderatlas" description="Generate files for the Stendhal Atlas." depends="compile_stendhaltools">
<!-- Generate files for the Stendhal Atlas. -->
<target name="renderatlas" depends="compile_stendhaltools">
<taskdef name="maprenderer" classname="games.stendhal.tools.MapRenderer">
<classpath>
<pathelement path="${build_stendhaltools}"/>
@ -58,7 +60,8 @@
</target> <!-- renderatlas -->
<target name="renderworldmap" description="Export world.tmx map to PNG image." depends="rendermaps">
<!-- Export world.tmx map to PNG image. -->
<target name="renderworldmap" depends="rendermaps">
<!-- embed area images with area name -->
<!-- FIXME: need to detect the Python "PIL" module to ensure script can run or
write Java class that can do this -->
@ -85,7 +88,8 @@
</target> <!-- renderworldmap -->
<target name="updatemaps" description="Load and save TMX maps to fix tilesets changes." depends="compile_stendhaltools">
<!-- Load and save TMX maps to fix tilesets changes. -->
<target name="updatemaps" depends="compile_stendhaltools">
<taskdef name="mapupdater" classname="games.stendhal.tools.MapUpdater">
<classpath>
<pathelement path="${build_stendhaltools}"/>

View file

@ -16,7 +16,8 @@
<delete file="${build}/package/client/stendhal-starter.exe"/>
</target>
<target name="msiPackage" description="Creates MSI installer package from current build" depends="stagePackage">
<!-- Creates MSI installer package from current build. -->
<target name="msiPackage" depends="stagePackage">
<delete dir="${build}/package/msi-app-image"/>
<exec executable="jpackage" dir="." failonerror="true">
<arg line="--type app-image"/>
@ -46,7 +47,8 @@
</exec>
</target> <!-- msiPackage -->
<target name="dmgPackage" description="Creates DMG package from current build" depends="stagePackage">
<!-- Creates DMG package from current build. -->
<target name="dmgPackage" depends="stagePackage">
<delete dir="${build}/package/dmg-app-image"/>
<exec executable="jpackage" dir="." failonerror="true">
<arg line="--type app-image"/>
@ -74,7 +76,8 @@
</exec>
</target> <!-- dmgPackage -->
<target name="rpmPackage" description="Creates RPM package from current build" depends="stagePackage">
<!-- Creates RPM package from current build. -->
<target name="rpmPackage" depends="stagePackage">
<delete dir="${build}/package/rpm-app-image"/>
<exec executable="jpackage" dir="." failonerror="true">
<arg line="--type app-image"/>
@ -107,7 +110,8 @@
</exec>
</target> <!-- rpmPackage -->
<target name="debPackage" description="Creates DEB package from current build" depends="stagePackage">
<!-- Creates DEB package from current build. -->
<target name="debPackage" depends="stagePackage">
<delete dir="${build}/package/deb-app-image"/>
<exec executable="jpackage" dir="." failonerror="true">
<arg line="--type app-image"/>

View file

@ -0,0 +1,77 @@
<?xml version='1.0'?>
<project>
<!--
Sets a property (by name and value), or set of properties (from file or resource) in the project. Properties are case sensitive.
Properties are immutable: whoever sets a property first freezes it for the rest of the build; they are most definitely not variables.
-->
<!-- store ant version number without extra details -->
<antversion property="ant.version.number" />
<!-- store ant home directory with system delimiter -->
<property name="ant.location" location="${ant.home}" />
<property name="build_tests" value="${buildroot}/build_tests"/>
<property name="build_tests_report" value="${buildroot}/build_test_report"/>
<property name="build_client" value="${buildroot}/build_client"/>
<property name="build_client_data" value="${buildroot}/build_client_data"/>
<property name="build_client_sound_data" value="${buildroot}/build_client_sound_data"/>
<property name="build_client_music_data" value="${buildroot}/build_client_music_data"/>
<property name="build_webclient" value="${buildroot}/build_webclient"/>
<property name="build_docker" value="${buildroot}/build_docker"/>
<property name="buildtools_docker" value="buildtools/docker"/>
<property name="build_postman" value="${buildroot}/buildbot/"/>
<property name="build_server" value="${buildroot}/build_server"/>
<property name="build_server_maps" value="${buildroot}/build_server_maps"/>
<property name="build_server_script" value="${buildroot}/build_server_script"/>
<property name="build_server_mods" value="${buildroot}/build_server_mods"/>
<property name="build_server_xmlconf" value="${buildroot}/build_server_xmlconf"/>
<property name="build_stendhaltools" value="${buildroot}/build_stendhaltools"/>
<property name="build_stendhaltextclient" value="${buildroot}/build_stendhaltextclient"/>
<property name="client_data" value="stendhal-data-${version}.jar"/>
<property name="client_jarname" value="stendhal-${version}.jar"/>
<property name="client_sound_data" value="stendhal-sound-data-${version}.jar"/>
<property name="client_music_data" value="stendhal-music-data-${version}.jar"/>
<property name="client_starter_jarname" value="stendhal-starter-${version}.jar"/>
<property name="dist_server_binary" value="${buildroot}/stendhal-server-${version}.zip" />
<property name="maps_jarname" value="stendhal-maps-${version}.jar"/>
<property name="server_jarname" value="stendhal-server-${version}.jar"/>
<property name="xmlconf_jarname" value="stendhal-xmlconf-${version}.jar"/>
<property name="config_dir" value="data/conf"/>
<property name="schema_location" value="${config_dir}"/>
<property name="cobertura.dir" value="${libdir}/cobertura"/>
<condition property="exclude.signing" value="true">
<not>
<available file="keystore.ks"/>
</not>
</condition>
<condition property="exclude.jardiff" value="true">
<not>
<available file="${build-archive}/stendhal-${version.old}.zip"/>
</not>
</condition>
<!-- time & date -->
<tstamp>
<format property="year" pattern="yyyy"/>
<format property="date" pattern="yyyy-MM-dd"/>
<format property="time" pattern="HH:mm:ss"/>
</tstamp>
<property name="datetime" value="${date} ${time}"/>
</project>

View file

@ -7,7 +7,8 @@
<import file="dist.xml" />
<import file="tools.xml" />
<target name="test" description="Compile and run all tests." depends="compile_tests,prepare_serverini_for_tests,run_tests,report_test">
<!-- Compile and run all tests. -->
<target name="test" depends="compile_tests,prepare_serverini_for_tests,run_tests,report_test">
<exec executable="curl">
<arg value="-q"/>
<arg value="-F"/>
@ -21,7 +22,8 @@
</target> <!-- test -->
<target name="compile_tests" description="Compile all tests." depends="server_build,client_build,compile_stendhaltools,compile_stendhaltextclient,postman">
<!-- Compile all tests. -->
<target name="compile_tests" depends="server_build,client_build,compile_stendhaltools,compile_stendhaltextclient,postman">
<mkdir dir="${build_tests}"/>
<copy todir="${build_tests}">
@ -63,7 +65,8 @@
</target> <!-- compile_tests -->
<target name="prepare_serverini_for_tests" unless="server.ini" description="Generate server.ini for testing.">
<!-- Generate server.ini for testing. -->
<target name="prepare_serverini_for_tests" unless="server.ini">
<echo file="server.ini">
database_implementation=games.stendhal.server.core.engine.StendhalPlayerDatabase
factory_implementation=games.stendhal.server.core.engine.StendhalRPObjectFactory
@ -85,7 +88,8 @@ d = 9017093486007323270605414717455737376618092805104733236083857811315606869372
</target> <!-- prepare_serverini_for_tests -->
<target name="run_tests" description="Run all tests.">
<!-- Run all tests. -->
<target name="run_tests">
<mkdir dir="${build_tests_report}"/>
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="buildtools/jacoco/jacocoant.jar"/>
@ -147,7 +151,8 @@ d = 9017093486007323270605414717455737376618092805104733236083857811315606869372
</target> <!-- run_tests -->
<target name="report_test" description="Aggregate test results.">
<!-- Aggregate test results. -->
<target name="report_test">
<mkdir dir="${build_tests_report}"/>
<junitreport todir="${build_tests_report}" tofile="testresults.xml">
<fileset dir="${build_tests_report}">

View file

@ -4,7 +4,8 @@
<import file="dist.xml" />
<target name="compile_stendhaltools" description="Compile Stendhal build tools.">
<!-- Compile Stendhal build tools. -->
<target name="compile_tools">
<mkdir dir="${build_stendhaltools}"/>
<javac srcdir="${src}" destdir="${build_stendhaltools}" source="1.8" target="1.8" debug="${javac.debug}" debuglevel="${javac.debuglevel}" deprecation="${javac.deprecation}" includeantruntime="true">
@ -28,7 +29,11 @@
</javac>
</target> <!-- compile_stendhaltools -->
<target name="clean_whitespace" description="Clean whitespace in source and other specified file types." depends="compile_stendhaltools">
<!-- Alias of `compile_tools`. -->
<target name="compile_stendhaltools" depends="compile_tools" />
<!-- Clean whitespace in source and other specified file types. -->
<target name="clean_whitespace" depends="compile_stendhaltools">
<!-- process main Java source, config, & scripting files -->
<java classname="games.stendhal.tools.CleanWhitespace">
<classpath>
@ -55,16 +60,22 @@
<pathelement path="${build_stendhaltools}"/>
</classpath>
<arg value="data/"/>
<arg value="tiled/"/>
<arg value="xml,xsd,lua,py"/>
</java>
<!-- process Ant build configuration files -->
<java classname="games.stendhal.tools.CleanWhitespace">
<classpath>
<pathelement path="${build_stendhaltools}"/>
</classpath>
<arg value="${ant_modules}" />
<arg value="xml" />
</java>
</target> <!-- clean_whitespace -->
<target name="cleanwhitespace" depends="clean_whitespace" description="Alias of clean_whitespace."/>
<target name="postman" description="Build postman bot.">
<!-- Build postman bot. -->
<target name="postman">
<mkdir dir="${buildroot}/buildbot"/>
<javac srcdir="${src}" destdir="${buildroot}/buildbot" debug="${javac.debug}" debuglevel="${javac.debuglevel}" source="1.8" target="1.8" deprecation="${javac.deprecation}" includeantruntime="false">
<include name="games/stendhal/bot/postman/*.java"/>
@ -100,7 +111,8 @@
</target> <!-- postman -->
<target name="signupdates" description="Sign updates." depends="compile_stendhaltools,jardiff" unless="exclude.signing">
<!-- Sign updates. -->
<target name="signupdates" depends="compile_stendhaltools,jardiff" unless="exclude.signing">
<taskdef name="updatesigner" classname="games.stendhal.tools.updateprop.UpdateSigner">
<classpath>
<pathelement path="."/>
@ -116,7 +128,8 @@
</target> <!-- signupdates -->
<target name="jardiff" depends="client_build" unless="exclude.jardiff" description="Generate release changes info.">
<!-- Generate release changes info. -->
<target name="jardiff" depends="client_build" unless="exclude.jardiff">
<echo>Creating update from ${version.old} to ${version}</echo>
<delete>
<fileset dir="${build-archive}">
@ -160,8 +173,9 @@
</target> <!-- jardiff -->
<!-- Run enemy balancer. -->
<!-- USAGE: ant balance -Dcreature="<creature_name>" -->
<target name="balance" description="Run enemy balancer." depends="compile_stendhaltools,server_build">
<target name="balance" depends="compile_stendhaltools,server_build">
<java classname="games.stendhal.tools.BalanceRPGame">
<classpath>
<pathelement path="${build_stendhaltools}"/>

View file

@ -1,6 +1,6 @@
site_name: Stendhal 1.49 Reference Documentation
site_name: Stendhal 1.49.5 Reference Documentation
site_description: A fun, friendly, & free multiplayer online adventure game.
copyright: Copyright © 2003-2025 Stendhal
copyright: Copyright © 2003-2026 Stendhal
site_url: https://stendhalgame.org/
nav:
- Index: /reference/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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.=

Binary file not shown.

755
package-lock.json generated
View file

@ -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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -118,7 +118,7 @@ public class HttpClient {
try {
connection = (HttpURLConnection) url.openConnection();
// Wikipedia API now requires User-Agent string
connection.setRequestProperty("User-Agent", "StendhalClient/1.49 (https://arianne-project.org/contact.html)");
connection.setRequestProperty("User-Agent", "StendhalClient/1.49.5 (https://arianne-project.org/contact.html)");
connection.setConnectTimeout(myTimeout);
connection.setInstanceFollowRedirects(true);
connection.setUseCaches(false);

View file

@ -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.49";
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.

View file

@ -335,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;

File diff suppressed because it is too large Load diff

View file

@ -486,14 +486,11 @@
<div class="shortcuts-dialog"></div>
</template>
<img id="held-object" class="hidden unselectable notarget" draggable="false">
<canvas id="held-object" class="unselectable notarget" draggable="false"></canvas>
<!-- place at end of document so drawn above all other elements -->
<img id="click-indicator" class="unclickable notarget">
<!-- hidden canvas for manipulating images before being displayed -->
<canvas id="drawing-stage" class="hidden"></canvas>
<script type="text/javascript" src="stendhal-compiled.js"></script>
</body>

View file

@ -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";
@ -40,6 +36,12 @@ 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";
/**
@ -62,6 +64,7 @@ export class Client {
/** Singleton instance. */
private static instance: Client;
public loaded = false;
/**
@ -78,7 +81,7 @@ export class Client {
* Hidden singleton constructor.
*/
private constructor() {
// do nothing
// empty
}
/**
@ -91,19 +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);
}
/**
@ -117,9 +119,6 @@ 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 = [];
}
@ -141,22 +140,9 @@ export class Client {
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.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.
@ -177,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();
@ -204,11 +190,11 @@ export class Client {
stendhal.ui.globalInternalWindow = new DialogHandler();
// pre-cache images & sounds
stendhal.data.sprites.startupCache();
htmlImageStore.startupCache();
stendhal.sound.startupCache();
if (document.getElementById("viewport")) {
stendhal.ui.gamewindow.draw.apply(stendhal.ui.gamewindow, arguments);
ViewPort.get().draw();
}
}
@ -259,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<string, string>) {
if (config["client_login_url"]) {
@ -281,25 +270,25 @@ export class Client {
"Login",
new LoginDialog(),
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, {});
@ -331,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) {
@ -345,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();
}
}
@ -420,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();
@ -459,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";
}
/**
@ -469,8 +438,10 @@ 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);
@ -481,29 +452,29 @@ export class Client {
// parallax background
if (stendhal.config.getBoolean("effect.parallax")) {
stendhal.data.map.setParallax(zoneinfo["parallax"]);
stendhal.data.map.setIgnoredTiles(zoneinfo["parallax_ignore_tiles"]);
map.setParallax(zoneinfo["parallax"]);
map.setIgnoredTiles(zoneinfo["parallax_ignore_tiles"]);
}
// coloring information
if (zoneinfo["color"] && stendhal.config.getBoolean("effect.lighting")) {
if (zoneinfo["color_method"]) {
stendhal.ui.gamewindow.setColorMethod(zoneinfo["color_method"]);
viewPort.setColorMethod(zoneinfo["color_method"]);
}
if (zoneinfo["blend_method"]) {
stendhal.ui.gamewindow.setBlendMethod(zoneinfo["blend_method"]);
viewPort.setBlendMethod(zoneinfo["blend_method"]);
}
const hsl = Color.numToHSL(Number(zoneinfo["color"]));
stendhal.ui.gamewindow.HSLFilter = hsl.toString();
viewPort.HSLFilter = hsl.toString();
// deprecated
stendhal.ui.gamewindow.filter = "hue-rotate(" + hsl.H + "deg) saturate(" + hsl.S
viewPort.filter = "hue-rotate(" + hsl.H + "deg) saturate(" + hsl.S
+ ") brightness(" + hsl.L + ")";
} else {
stendhal.ui.gamewindow.HSLFilter = undefined;
stendhal.ui.gamewindow.filter = undefined;
viewPort.HSLFilter = undefined;
viewPort.filter = undefined;
}
singletons.getWeatherRenderer().update(zoneinfo["weather"]);
WeatherRenderer.get().update(zoneinfo["weather"]);
}
/**
@ -518,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";
}
/**

View file

@ -1,195 +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 { RPEntity } from "./entity/RPEntity";
import { RPObject } from "./entity/RPObject";
import { BestiaryEvent } from "./event/BestiaryEvent";
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 { ShowItemListEvent } from "./event/ShowItemListEvent";
import { ShowOutfitListEvent } from "./event/ShowOutfitListEvent";
import { SoundEvent } from "./event/SoundEvent";
import { TradeEvent } from "./event/TradeEvent";
import { ViewChangeEvent } from "./event/ViewChangeEvent";
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", new BestiaryEvent());
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.sound.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", new ShowItemListEvent());
this.register("show_outfit_list", new ShowOutfitListEvent());
this.register("sound_event", new SoundEvent());
this.register("text", {
execute: function(entity: RPEntity) {
if (this.hasOwnProperty("range")) {
entity.say(this["text"], this["range"]);
} else {
entity.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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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,45 +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 { DrawingStage } from "./util/DrawingStage";
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 { 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 { SoftwareJoystickController } from "./ui/SoftwareJoystickController";
import { TouchHandler } from "./ui/TouchHandler";
import { ui } from "./ui/UI";
import { UIComponentEnum } from "./ui/UIComponentEnum";
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 {
@ -75,10 +65,6 @@ export class SingletonRepo {
return DownloadUtil;
}
static getDrawingStage(): DrawingStage {
return DrawingStage.get();
}
static getEmojiStore(): EmojiStore {
return EmojiStore.get();
}
@ -95,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();
}
@ -119,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;
}
@ -143,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;

View file

@ -9,8 +9,8 @@
* *
***************************************************************************/
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa"
import { stendhal } from "./stendhal";
import { singletons } from "./SingletonRepo";
@ -766,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.");

View file

@ -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(

View file

@ -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,21 +9,20 @@
* *
***************************************************************************/
import { singletons } from "../SingletonRepo";
import { ui } from "../ui/UI";
import { UIComponentEnum } from "../ui/UIComponentEnum";
import { FloatingWindow } from "../ui/toolkit/FloatingWindow";
import { SlashAction } from "./SlashAction";
import { ShowFloatingWindowComponent } from "../ui/component/ShowFloatingWindowComponent";
import { Panel } from "ui/toolkit/Panel";
import { ShowFloatingWindowComponent } from "../ui/component/ShowFloatingWindowComponent";
import { SlashAction } from "./SlashAction";
import { Chat } from "../util/Chat";
import { Debug } from "../util/Debug";
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa";
import { WeatherRenderer } from "util/WeatherRenderer";
import { stendhal } from "../stendhal";
/**
* performances debugging actions
@ -55,6 +54,8 @@ export class DebugAction extends SlashAction {
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;
}
@ -66,7 +67,8 @@ export class DebugAction extends SlashAction {
" /debug ui [pop]",
" /debug weather [<name>]",
" /debug settings",
" /debug touch"
" /debug touch",
" /debug light"
];
Chat.log("client", usage);
}
@ -135,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);
}
}
};

View file

@ -14,7 +14,8 @@ import { SlashAction } from "./SlashAction";
import { ScreenCapture } from "../util/ScreenCapture";
declare var stendhal: any;
import { stendhal } from "../stendhal";
import { ViewPort } from "ui/ViewPort";
export class ScreenCaptureAction extends SlashAction {
@ -35,7 +36,7 @@ export class ScreenCaptureAction extends SlashAction {
return true;
}
this.recorder = new ScreenCapture();
this.recorder.start(stendhal.ui.viewport.getElement() as HTMLCanvasElement);
this.recorder.start(ViewPort.get().getElement());
return true;
}
}

View file

@ -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(

View file

@ -9,7 +9,7 @@
* *
***************************************************************************/
declare var marauroa: any;
import { marauroa } from "marauroa"
import { ParamList } from "./ParamList";
import { Pair } from "../util/Pair";

View file

@ -9,8 +9,8 @@
* *
***************************************************************************/
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa"
import { stendhal } from "../stendhal";
export class CStatus {

View file

@ -9,7 +9,7 @@
* *
***************************************************************************/
declare var stendhal: any;
import { stendhal } from "../stendhal";
/**

View file

@ -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);
}
/**

View file

@ -10,7 +10,7 @@
* *
***************************************************************************/
declare var stendhal: any;
import { stendhal } from "../stendhal";
import { Entity } from "../entity/Entity";

View file

@ -9,7 +9,7 @@
* *
***************************************************************************/
declare var stendhal: any;
import { stendhal } from "../stendhal";
import { SlashActionRepo } from "../SlashActionRepo";

View file

@ -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 <code>undefined</code>.
*/
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
* <code>true</code> 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();

View file

@ -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");
}
}

View file

@ -10,11 +10,14 @@
* *
***************************************************************************/
import { DrawingStage } from "../util/DrawingStage";
import { Pair } from "../util/Pair";
import { StringUtil } from "../util/StringUtil";
import { Paths } from "./Paths";
declare var stendhal: any;
import { stendhal } from "../stendhal";
import { ImageMetadata } from "sprite/image/ImageMetadata";
import { MultilayerImageLoader } from "sprite/image/MultilayerImageLoader";
import { OutfitStore } from "./OutfitStore";
/**
@ -113,7 +116,7 @@ export class Outfit {
public getLayers(): Pair<string, number>[] {
const layers: Pair<string, number>[] = [];
// only include valid layers
for (const name of stendhal.data.outfit.getLayerNames()) {
for (const name of OutfitStore.get().getLayerNames()) {
let index = this.layers[name];
if (index != undefined) {
layers.push(new Pair(name, index));
@ -151,7 +154,7 @@ export class Outfit {
* Color value or `undefined`.
*/
public getLayerColor(name: string): number|undefined {
if (stendhal.data.outfit.isSkinLayer(name)) {
if (OutfitStore.get().isSkinLayer(name)) {
name = "skin";
}
return this.coloring && name in this.coloring ? this.coloring[name] : undefined;
@ -193,40 +196,14 @@ export class Outfit {
*
* TODO:
* - use dress layer based on body type
*
* @param {Function} callback
* Function to pass image to when ready.
*/
toImage(callback: Function) {
const sig = this.getSignature();
// get directly from cache since we don't want to return failsafe image
let image: HTMLImageElement = stendhal.data.sprites.getCached(sig);
const onReady = function(e?: Event) {
image.removeEventListener("load", onReady);
callback(image);
};
if (image instanceof HTMLImageElement) {
if (image.height > 0) {
onReady();
} else {
image.addEventListener("load", onReady);
}
return;
}
const stage = DrawingStage.get();
stage.reset();
stage.setSize(48 * 3, 64 * 4);
async toImage(): Promise<ImageBitmap> {
let parts: ImageMetadata[] = [];
let outfitLayers = this.getLayers();
const detailIndex = this.getLayerIndex("detail") || 0;
if (stendhal.data.outfit.detailHasRearLayer(detailIndex)) {
if (OutfitStore.get().detailHasRearLayer(detailIndex)) {
outfitLayers = [new Pair("detail-rear", detailIndex), ...outfitLayers];
}
const layers: HTMLImageElement[] = [];
for (const l of outfitLayers) {
let layerName = l.first;
let suffix = "";
@ -235,58 +212,21 @@ export class Outfit {
layerName = layerName.replace(/-rear$/, "");
}
const layerIndex = l.second;
let layerPath = stendhal.paths.sprites + "/outfit/" + layerName + "/"
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";
const coloring = this.getLayerColor(layerName);
if (typeof(coloring) === "undefined") {
layers.push(stendhal.data.sprites.get(layerPath));
} else {
layers.push(stendhal.data.sprites.getFiltered(layerPath, "trueColor", coloring));
let filter = undefined;
let coloring = this.getLayerColor(layerName);
if (coloring) {
filter = "trueColor";
}
parts.push(new ImageMetadata(layerPath, filter, coloring));
}
if (layers.length == 0) {
image = stendhal.data.sprites.getFailsafe();
callback(image);
return;
}
let onAllLayersReady: Function;
const onLayerReady = function(e?: Event) {
for (const layer of layers) {
if (!layer.complete || layer.height === 0) {
return;
}
}
onAllLayersReady();
};
onAllLayersReady = function() {
for (const layer of layers) {
layer.removeEventListener("load", onLayerReady);
stage.drawImage(layer);
}
image = stage.toImage();
stage.reset();
stendhal.data.sprites.cache(sig, image);
if (image.height > 0) {
onReady();
} else {
image.addEventListener("load", onReady);
}
};
for (const layer of layers) {
if (layer.height > 0) {
onLayerReady();
} else {
layer.addEventListener("load", onLayerReady);
}
}
return await new MultilayerImageLoader(parts).load();
}
}

View file

@ -25,7 +25,7 @@ export class OutfitStore {
// player pickable layers
private count: {[key: string]: number} = {
public readonly count: {[key: string]: number} = {
"hat": 19,
"hair": 57,
"mask": 9,

View file

@ -1,618 +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 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 = <HTMLCanvasElement> 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;
}
/**
* Adds an image to cache.
*
* @param {string} id
* Cache identifier.
* @param {SpriteImage}
* Image to be cached.
*/
cache(id: string, image: SpriteImage) {
image.counter = 0;
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) {
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 <em>a copy</em> 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<Number>}
*/
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<Number>} rgb
* @return {Array<Number>}
*/
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<Number>} hsl
* @return {Array<Number>}
*/
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 <code>undefined</code>.
*/
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
* <code>true</code> 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");
}
// weather
for (const weather of ["clouds", "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];
}
};

View file

@ -9,54 +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",
@ -64,22 +66,22 @@ export class Map {
Paths.tileset + "/item/blood/small_stains"
];
private parallax: ParallaxBackground;
private parallaxImage?: string;
private ignoredTiles: string[];
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;
}
/**
@ -124,6 +126,8 @@ 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();
@ -138,7 +142,7 @@ export class Map {
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 = [];
@ -192,7 +196,7 @@ export class Map {
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();

View file

@ -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");
}

View file

@ -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,10 +9,11 @@
* *
***************************************************************************/
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa"
import { stendhal } from "../../stendhal";
import { LoopedSoundSource } from "../../entity/LoopedSoundSource";
import { Zone } from "entity/Zone";
// server doesn't distinguish between music & looped sound effects so we
@ -179,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);
}

View file

@ -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<string> {
@ -29,9 +29,6 @@ export class SoundLayer extends AbstractEnum<string> {
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<string> {
static names(): string[] {
const names: string[] = [];
for (const layer of SoundLayer.layers) {
names.push(layer.value);
names.push(layer.value!);
}
return names;
}

View file

@ -9,16 +9,15 @@
* *
***************************************************************************/
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa"
import { stendhal } from "../../stendhal";
import { SoundFactory } from "./SoundFactory";
import { SoundObject } from "./SoundFactory";
import { SoundLayer } from "./SoundLayer";
import { singletons } from "../../SingletonRepo";
import { ui } from "../../ui/UI";
import { Paths } from "../Paths";
/**
@ -244,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]) {
@ -406,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);
}
@ -424,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);
}
@ -796,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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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";
}
}

View file

@ -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 {
@ -40,61 +47,42 @@ export class Corpse extends PopupInventory {
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 (this.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);
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;
}
@ -168,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 {
@ -189,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);
}
}

View file

@ -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();
}
}
}

View file

@ -16,9 +16,10 @@ import { EntityOverlayRegistry } from "../data/EntityOverlayRegistry";
import { Color } from "../data/color/Color";
import { SkillEffect } from "../sprite/action/SkillEffect";
import { Paths } from "../data/Paths";
declare var marauroa: any;
declare var stendhal: any;
import { marauroa } from "marauroa"
import { stendhal } from "../stendhal";
export class Creature extends RPEntity {
@ -27,7 +28,7 @@ export class Creature extends RPEntity {
override titleStyle = "#ffc8c8";
override set(key: string, value: string) {
override set(key: string, value: any) {
super.set(key, value);
if (key === "name") {
@ -52,7 +53,7 @@ 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";
}
/**

View file

@ -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[]) {

View file

@ -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";
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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, <code>null</code> 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),

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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 {

View file

@ -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",

View file

@ -14,8 +14,8 @@ import { RPEntity } from "./RPEntity";
import { EntityOverlayRegistry } from "../data/EntityOverlayRegistry";
import { Color } from "../data/color/Color";
declare var stendhal: any;
import { RenderingContext2D } from "util/Types";
import { Paths } from "../data/Paths";
export class NPC extends RPEntity {
override minimapStyle = Color.NPC;
@ -28,7 +28,7 @@ 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") {
@ -42,7 +42,7 @@ export class NPC extends RPEntity {
}
}
override drawTop(ctx: CanvasRenderingContext2D) {
override drawTop(ctx: RenderingContext2D) {
var localX = this["_x"] * 32;
var localY = this["_y"] * 32;
if (typeof(this["no_hpbar"]) == "undefined") {
@ -54,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";
}
}

View file

@ -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";
}
}

View file

@ -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);
}
/**

View file

@ -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";
}
}

View file

@ -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,30 +9,35 @@
* *
***************************************************************************/
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 { 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;
@ -54,7 +59,7 @@ 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 };
@ -204,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));
}
/**
@ -218,8 +223,8 @@ export class RPEntity extends ActiveEntity {
return typeof(this["no_shadow"]) === "undefined";
}
drawMultipartOutfit(ctx: CanvasRenderingContext2D) {
const store = singletons.getOutfitStore();
drawMultipartOutfit(ctx: RenderingContext2D) {
const store = OutfitStore.get();
// layers in draw order
var layers = store.getLayerNames();
@ -244,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
@ -255,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);
}
@ -302,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 (stendhal.data.outfit.isSkinLayer(part)) {
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);
}
}
@ -331,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"]
@ -347,138 +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.drawOverlayAnimation(ctx);
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
@ -539,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
@ -555,10 +491,10 @@ 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
@ -610,7 +546,7 @@ export class RPEntity extends ActiveEntity {
}
}
drawTop(ctx: CanvasRenderingContext2D) {
drawTop(ctx: RenderingContext2D) {
var localX = this["_x"] * 32;
var localY = this["_y"] * 32;
this.drawFloaters(ctx);
@ -618,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;
@ -655,42 +591,48 @@ 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) {
drawAttack(ctx: RenderingContext2D) {
if (!this.attackSprite) {
return;
}
if (this.attackSprite.expired()) {
// 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 {CanvasRenderingContext2D} ctx
* @param {RenderingContext2D} ctx
* Canvas context to draw on.
*/
private drawOverlayAnimation(ctx: CanvasRenderingContext2D) {
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;
}
}
@ -710,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));
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));
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) {
@ -779,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);
}
@ -793,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);
@ -810,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);
}
}
@ -844,10 +787,10 @@ 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);
}
/**

View file

@ -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 <RPObject> this._parent;
}
}

View file

@ -9,37 +9,49 @@
* *
***************************************************************************/
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" && 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) {
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);
}
}

View file

@ -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";
}
}

View file

@ -11,7 +11,7 @@
import { Entity } from "./Entity";
declare var marauroa: any;
import { marauroa } from "marauroa"
export class UnknownEntity extends Entity {

View file

@ -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;
}
}

View file

@ -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";
/**
@ -163,7 +164,7 @@ export class User extends Player {
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[] = [];
const lssm = singletons.getLoopedSoundSourceManager();
@ -225,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});
}
@ -244,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"});
@ -280,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;

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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;
}

View file

@ -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"]);
}
};

View file

@ -10,13 +10,13 @@
* *
***************************************************************************/
import { RPEvent } from "./RPEvent";
import { RPEvent } from "marauroa"
import { ui } from "../ui/UI";
import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent";
declare var stendhal: any;
import { stendhal } from "../stendhal";
export class BestiaryEvent extends RPEvent {
@ -24,7 +24,7 @@ export class BestiaryEvent extends RPEvent {
enemies!: string;
execute(entity: any) {
execute(_entity: any) {
if (!this.hasOwnProperty("enemies")) {
console.error("Event does not have \"enemies\" attribute");
return;

View file

@ -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

View file

@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show more