Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
Hendrik Brummermann
cbe0825e9b updated release notes
Some checks failed
Android Build Test / build (push) Has been cancelled
Java CI / build (push) Has been cancelled
2025-12-23 08:49:29 +01:00
Hendrik Brummermann
05d1fb34af properly format item names in description text of shop windows 2025-12-22 19:22:11 +01:00
Hendrik Brummermann
39edb92f1c show outfits in shop sign dialog 2025-12-22 18:08:52 +01:00
Hendrik Brummermann
17003c457a code cleanup 2025-12-22 18:08:45 +01:00
Hendrik Brummermann
ef6c92bd74 use proper table for item shop signs 2025-12-22 18:08:37 +01:00
Hendrik Brummermann
59f2ba9fbd moved ShowItemListEvent and ShowOutfitListEvent out of EventRegistry 2025-12-22 18:08:17 +01:00
Hendrik Brummermann
e00e6b9b4c updated version number 2025-12-21 22:19:28 +01:00
18 changed files with 424 additions and 252 deletions

1
.gitignore vendored
View file

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

View file

@ -10,8 +10,8 @@ android {
applicationId namespace
minSdk 21
targetSdk 32
versionCode 1048005
versionName "1.48.5"
versionCode 1049000
versionName "1.49"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 32
minSdkVersion 21

View file

@ -1,3 +1,5 @@
PUT RELEASE ANNOUNCEMENT LINES HERE
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.
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.48.5</string>
<string name="build_version">1.49</string>
</resources>

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.48.5",
"version": "1.49",
"defaultMode": "window",
"port": 0,
"documentRoot": "/resources/",

View file

@ -1,6 +1,6 @@
@echo off
set STENDHAL_VERSION=1.48.5
set STENDHAL_VERSION=1.49
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.48.5"
STENDHAL_VERSION="1.49"
SERVER_JAR="stendhal-server-${STENDHAL_VERSION}.jar"
# change to server directory

View file

@ -92,9 +92,9 @@ version_server = http://arianne.sourceforge.net/stendhal.version
# current version of stendhal
version.old = 1.48
version = 1.48.5
version = 1.49
# FIXME: dynamic method to set this property?
version.android = 1048005
version.android = 1049000
# javac options
javac.deprecation = true

View file

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

View file

@ -12,7 +12,7 @@ Changelog
*bug fixes*
- fixed invalid head outfit index of Mr Ross
- fixed silent NPC fixed paths
- fixed silent NPC paths
- fixed desktop webview client window freezing on Windows when closed
- fixed nativehelper extension process not exiting cleanly on Windows
- set User-Agent string for Wikipedia API requests

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.48.5 (https://arianne-project.org/contact.html)");
connection.setRequestProperty("User-Agent", "StendhalClient/1.49 (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.48.5";
public static final String VERSION = "1.49";
/** 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

@ -708,3 +708,16 @@ img#click-indicator {
.pad-contents {
padding: 0.5em;
}
table.shoptable {
border-collapse: collapse;
}
table.shoptable th, table.shoptable td {
border: 1px solid var(--text-color);
padding: 2px;
}
td.number {
text-align: right;
}

View file

@ -24,14 +24,11 @@ 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 { ui } from "./ui/UI";
import { DialogContentComponent } from "./ui/toolkit/DialogContentComponent";
import { Chat } from "./util/Chat";
@ -155,120 +152,8 @@ export class EventRegistry {
}
}); // reached_achievement
this.register("show_item_list", {
execute: function(rpobject: RPObject) {
let title = "Items";
let caption = "";
let items = [];
if (this.hasOwnProperty("title")) {
title = this["title"];
}
if (this.hasOwnProperty("caption")) {
caption = this["caption"];
}
if (this.hasOwnProperty("content")) {
for (var obj in this["content"]) {
if (this["content"].hasOwnProperty(obj)) {
var slotObj = this["content"][obj];
var data = this["content"][obj]["a"];
const i = {
clazz: data["class"],
subclass: data["subclass"],
img: data["class"] + "/" + data["subclass"] + ".png",
price: data["price"],
desc: data["description_info"]
}
// seller shops prefix prices with "-"
if (i.price.startsWith("-")) {
i.price = i.price.substr(1);
}
items.push(i);
}
}
}
const content = new class extends DialogContentComponent {} ("empty-div-template");
content.componentElement.classList.add("shopsign");
const captionElement = document.createElement("div");
captionElement.className = "horizontalgroup shopcaption";
captionElement.textContent = caption + "\nItem\t-\tPrice\t-\tDescription";
content.componentElement.appendChild(captionElement);
const itemList = document.createElement("div");
itemList.className = "shoplist";
content.componentElement.appendChild(itemList);
// TODO: organize in columns & show item sprites
for (const i of items) {
const row = document.createElement("div");
row.className = "horizontalgroup shoprow";
const img = document.createElement("div");
img.className = "shopcol";
img.appendChild(stendhal.data.sprites.get(stendhal.paths.sprites + "/items/" + i.img));
row.appendChild(img);
const price = document.createElement("div");
price.className = "shopcol";
price.textContent = i.price;
row.appendChild(price);
const desc = document.createElement("div");
desc.className = "shopcol shopcolr";
desc.textContent = i.desc;
row.appendChild(desc);
itemList.appendChild(row);
}
stendhal.ui.globalInternalWindow.set(
ui.createSingletonFloatingWindow(title, content, 20, 20));
}
}); // show_item_list
this.register("show_outfit_list", {
execute: function(rpobject: RPObject) {
let title = "Outfits";
let caption = "";
let outfits = [];
if (this.hasOwnProperty("title")) {
title = this["title"];
}
if (this.hasOwnProperty("caption")) {
caption = this["caption"];
}
if (this.hasOwnProperty("outfits")) {
for (let o of this["outfits"].split(":")) {
o = o.split(";");
if (o.length > 2) {
outfits.push([o[0], o[1], o[2]]);
}
}
}
if (this.hasOwnProperty("show_base")) {
//Chat.log("normal", this["show_base"]);
}
const content = new class extends DialogContentComponent {} ("empty-div-template");
content.componentElement.classList.add("shopsign");
const captionElement = document.createElement("div");
captionElement.className = "horizontalgroup shopcaption";
captionElement.textContent = caption;
content.componentElement.appendChild(captionElement);
const itemList = document.createElement("div");
itemList.className = "shoplist";
content.componentElement.appendChild(itemList);
// TODO: organize in columns & show outfit sprites
for (const o of outfits) {
const row = document.createElement("div");
row.className = "horizontalgroup shoprow";
row.textContent = o[0] + ": " + o[2];
itemList.appendChild(row);
}
stendhal.ui.globalInternalWindow.set(
ui.createSingletonFloatingWindow(title, content, 20, 20));
}
}); // show_outfit_list
this.register("show_item_list", new ShowItemListEvent());
this.register("show_outfit_list", new ShowOutfitListEvent());
this.register("sound_event", new SoundEvent());

View file

@ -0,0 +1,122 @@
/***************************************************************************
* Copyright © 2024 - 2025 Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation; either version 3 of the *
* License, or (at your option) any later version. *
* *
***************************************************************************/
import { RPEvent } from "./RPEvent";
import { ui } from "../ui/UI";
import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent";
import { Chat } from "../util/Chat";
declare var stendhal: any;
export class ShowItemListEvent extends RPEvent {
title!: string;
caption!: string;
content!: any;
execute(_entity: any) {
let title = "Items";
let caption = "";
let items = [];
if (this.hasOwnProperty("title")) {
title = this["title"];
}
if (this.hasOwnProperty("caption")) {
caption = this["caption"];
}
if (this.hasOwnProperty("content")) {
for (let obj in this["content"]) {
if (this["content"].hasOwnProperty(obj)) {
let data = this["content"][obj]["a"];
let item = {
clazz: data["class"],
subclass: data["subclass"],
img: data["class"] + "/" + data["subclass"] + ".png",
price: data["price"],
desc: data["description_info"]
}
// seller shops prefix prices with "-"
if (item.price.startsWith("-")) {
item.price = item.price.substr(1);
}
items.push(item);
}
}
}
const content = new class extends DialogContentComponent { }("empty-div-template");
content.componentElement.classList.add("shopsign");
// Create table
const table = document.createElement("table");
table.className = "shoptable";
content.componentElement.appendChild(table);
// Caption
const tableCaption = document.createElement("caption");
tableCaption.className = "shopcaption";
tableCaption.textContent = caption;
table.appendChild(tableCaption);
// Header
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
const headers = ["Item", "Price", "Description"];
for (const text of headers) {
const th = document.createElement("th");
th.textContent = text;
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
const tbody = document.createElement("tbody");
for (const i of items) {
const row = document.createElement("tr");
// Item (image)
const itemCell = document.createElement("td");
itemCell.appendChild(
stendhal.data.sprites.get(
stendhal.paths.sprites + "/items/" + i.img
)
);
row.appendChild(itemCell);
// Price
const priceCell = document.createElement("td");
priceCell.className = "number";
priceCell.textContent = i.price;
row.appendChild(priceCell);
// Description
const descCell = document.createElement("td");
descCell.innerHTML = Chat.formatLogEntry(i.desc);
row.appendChild(descCell);
tbody.appendChild(row);
}
table.appendChild(tbody);
stendhal.ui.globalInternalWindow.set(
ui.createSingletonFloatingWindow(title, content, 20, 20));
}
}

View file

@ -0,0 +1,146 @@
/***************************************************************************
* Copyright © 2024 - 2025 Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation; either version 3 of the *
* License, or (at your option) any later version. *
* *
***************************************************************************/
import { RPEvent } from "./RPEvent";
import { ui } from "../ui/UI";
import { DialogContentComponent } from "../ui/toolkit/DialogContentComponent";
declare var stendhal: any;
export class ShowOutfitListEvent extends RPEvent {
title!: string;
caption!: string;
outfits!: string;
execute(_entity: any) {
let title = "Outfits";
let caption = "";
let outfits = [];
if (this.hasOwnProperty("title")) {
title = this["title"];
}
if (this.hasOwnProperty("caption")) {
caption = this["caption"];
}
if (this.hasOwnProperty("outfits")) {
for (let o of this["outfits"].split(":")) {
let parts = o.split(";");
if (parts.length > 2) {
outfits.push({
name: parts[0],
outfit: parts[1],
price: parts[2]
});
}
}
}
if (this.hasOwnProperty("show_base")) {
//Chat.log("normal", this["show_base"]);
}
const content = new class extends DialogContentComponent { }("empty-div-template");
content.componentElement.classList.add("shopsign");
const table = document.createElement("table");
table.className = "shoptable";
content.componentElement.appendChild(table);
// Caption
const tableCaption = document.createElement("caption");
tableCaption.className = "shopcaption";
tableCaption.textContent = caption;
table.appendChild(tableCaption);
// Header
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
const headers = ["Name", "Outfit", "Price"];
for (const text of headers) {
const th = document.createElement("th");
th.textContent = text;
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
const tbody = document.createElement("tbody");
for (const outfit of outfits) {
const row = document.createElement("tr");
// Name
const nameCell = document.createElement("td");
nameCell.textContent = outfit.name;
row.appendChild(nameCell);
// Image
const imageCell = document.createElement("td");
let url = ShowOutfitListEvent.outfitToUrl(outfit.outfit);
const img1 = document.createElement("img");
const img2 = document.createElement("img");
img1.src = url;
img2.src = url + "&offset=0";
imageCell.append(img1);
imageCell.append(img2);
row.appendChild(imageCell);
// Price
const priceCell = document.createElement("td");
priceCell.className = "number";
priceCell.textContent = outfit.price;
row.appendChild(priceCell);
tbody.appendChild(row);
}
table.appendChild(tbody);
stendhal.ui.globalInternalWindow.set(
ui.createSingletonFloatingWindow(title, content, 20, 20));
}
private static outfitToUrl(outfit?: string): string {
if (!outfit) {
return "";
}
// Required order
const order = ["body", "dress", "head", "mouth", "eyes", "mask", "hair", "hat", "detail"];
// Parse input into a map
let values: Record<string, number> = {};
for (let part of outfit.split(",")) {
let [key, value] = part.split("=");
values[key] = Number(value);
}
// Build path
let parts = order.map(key => {
let value = values[key] ?? 0;
return `${key}-${value}-0`;
});
let url =
"https://stendhalgame.org/createoutfit.php?rewritten=true&dialog=shop&outfit=" +
parts.join("_");
return url;
}
}

View file

@ -14,6 +14,7 @@ import { Component } from "../toolkit/Component";
import { MenuItem } from "../../action/MenuItem";
import { UIComponentEnum } from "../UIComponentEnum";
import { singletons } from "../../SingletonRepo";
import { Chat } from "../../util/Chat";
declare var stendhal: any;
@ -106,13 +107,13 @@ export class ChatLogComponent extends Component {
/**
* Adds a line of text.
*
* @param type {string}
* @param type
* Message type.
* @param message {string}
* @param message
* Text to be added.
* @param orator {string}
* @param orator
* Name of entity making the expression (default: `undefined`).
* @param timestamp {boolean}
* @param timestamp
* If `false`, suppresses prepending message with timestamp (default: `true`).
*/
public addLine(type: string, message: string, orator?: string, timestamp=true) {
@ -131,7 +132,7 @@ export class ChatLogComponent extends Component {
const rcol = document.createElement("div");
rcol.className = "logcolR log" + type;
rcol.innerHTML += this.formatLogEntry(message);
rcol.innerHTML += Chat.formatLogEntry(message);
const row = document.createElement("div");
row.className = "logrow";
@ -192,122 +193,7 @@ export class ChatLogComponent extends Component {
this.add(row);
}
/**
* Formats displayed text.
*
* @param message {string}
* Text to parse for special characters.
* @return {string}
* Text formatted with keywords & item names highlighted.
*/
private formatLogEntry(message: string): string {
if (!message) {
return "";
}
let res = "";
let delims = [" ", ",", ".", "!", "?", ":", ";"];
let length = message.length;
let inHighlight = false, inUnderline = false,
inHighlightQuote = false, inUnderlineQuote = false;
for (let i = 0; i < length; i++) {
let c = message[i];
if (c === "\\") {
let n = message[i + 1];
res += n;
i++;
// Highlight Start?
} else if (c === "#") {
if (inHighlight) {
res += c;
continue;
}
let n = message[i + 1];
if (n === "#") {
res += c;
i++;
continue;
}
if (n === "'") {
inHighlightQuote = true;
i++;
}
inHighlight = true;
res += "<span class=\"logh\">";
// Underline start?
} else if (c === "§") {
if (inUnderline) {
res += c;
continue;
}
let n = message[i + 1];
if (n === "§") {
res += c;
i++;
continue;
}
if (n === "'") {
inUnderlineQuote = true;
i++;
}
inUnderline = true;
res += "<span class=\"logi\">";
// End Highlight and Underline?
} else if (c === "'") {
if (inUnderlineQuote) {
inUnderline = false;
inUnderlineQuote = false;
res += "</span>";
continue;
}
if (inHighlightQuote) {
inHighlight = false;
inHighlightQuote = false;
res += "</span>";
continue;
}
res += c;
// HTML escape
} else if (c === "<") {
res += "&lt;";
// End of word
} else if (delims.indexOf(c) > -1) {
let n = message[i + 1];
if (c === " " || n === " " || n == undefined) {
if (inUnderline && !inUnderlineQuote && !inHighlightQuote) {
inUnderline = false;
res += "</span>" + c;
continue;
}
if (inHighlight && !inUnderlineQuote && !inHighlightQuote) {
inHighlight = false;
res += "</span>" + c;
continue;
}
}
res += c;
// Normal characters
} else {
res += c;
}
}
// Close opened formattings
if (inUnderline) {
res += "</span>";
}
if (inHighlight) {
res += "</span>";
}
return res;
}
/**
* Removes all text from log.

View file

@ -99,4 +99,121 @@ export class Chat {
Chat.log("client", message);
}
}
/**
* Formats displayed text.
*
* @param message {string}
* Text to parse for special characters.
* @return {string}
* Text formatted with keywords & item names highlighted.
*/
public static formatLogEntry(message: string): string {
if (!message) {
return "";
}
let res = "";
let delims = [" ", ",", ".", "!", "?", ":", ";"];
let length = message.length;
let inHighlight = false, inUnderline = false,
inHighlightQuote = false, inUnderlineQuote = false;
for (let i = 0; i < length; i++) {
let c = message[i];
if (c === "\\") {
let n = message[i + 1];
res += n;
i++;
// Highlight Start?
} else if (c === "#") {
if (inHighlight) {
res += c;
continue;
}
let n = message[i + 1];
if (n === "#") {
res += c;
i++;
continue;
}
if (n === "'") {
inHighlightQuote = true;
i++;
}
inHighlight = true;
res += "<span class=\"logh\">";
// Underline start?
} else if (c === "§") {
if (inUnderline) {
res += c;
continue;
}
let n = message[i + 1];
if (n === "§") {
res += c;
i++;
continue;
}
if (n === "'") {
inUnderlineQuote = true;
i++;
}
inUnderline = true;
res += "<span class=\"logi\">";
// End Highlight and Underline?
} else if (c === "'") {
if (inUnderlineQuote) {
inUnderline = false;
inUnderlineQuote = false;
res += "</span>";
continue;
}
if (inHighlightQuote) {
inHighlight = false;
inHighlightQuote = false;
res += "</span>";
continue;
}
res += c;
// HTML escape
} else if (c === "<") {
res += "&lt;";
// End of word
} else if (delims.indexOf(c) > -1) {
let n = message[i + 1];
if (c === " " || n === " " || n == undefined) {
if (inUnderline && !inUnderlineQuote && !inHighlightQuote) {
inUnderline = false;
res += "</span>" + c;
continue;
}
if (inHighlight && !inUnderlineQuote && !inHighlightQuote) {
inHighlight = false;
res += "</span>" + c;
continue;
}
}
res += c;
// Normal characters
} else {
res += c;
}
}
// Close opened formattings
if (inUnderline) {
res += "</span>";
}
if (inHighlight) {
res += "</span>";
}
return res;
}
}