Skip to content

Commit

Permalink
Class to represent an entity's outfit
Browse files Browse the repository at this point in the history
  • Loading branch information
AntumDeluge committed Jul 29, 2024
1 parent decc527 commit 8ad155e
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 30 deletions.
149 changes: 149 additions & 0 deletions src/js/stendhal/data/Outfit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation; either version 3 of the *
* License, or (at your option) any later version. *
* *
***************************************************************************/

import { OutfitStore } from "./OutfitStore";

import { Pair } from "../util/Pair";


/**
* Represents an entity's sprite layers.
*/
export class Outfit {

/** Sprite layers. */
private readonly layers: {[name: string]: number};
/** Layer coloring values. */
private coloring?: {[name: string]: number};


/**
* Creates a new outfit.
*
* @param layers
* Sprite layers.
*/
constructor(layers: {[layer: string]: number}={}) {
this.layers = layers;
}

/**
* Sets a layer's sprite index.
*
* @param name
* Layer name.
* @param index
* Sprite image index.
*/
public setLayer(name: string, index: number) {
this.layers[name] = index;
}

/**
* Unsets a layer's sprite index.
*
* @param name
* Layer name.
*/
public unsetLayer(name: string) {
delete this.layers[name];
}

/**
* Retrieves the index value of a specified layer.
*
* @param name
* Layer name.
* @return
* Sprite image index.
*/
public getLayerIndex(name: string): number|undefined {
return this.layers[name];
}

/**
* Retrieves sorted layer info.
*
* @return {util.Pair.Pair<string, number>[]}
* Layers info.
*/
public getLayers(): Pair<string, number>[] {
const layers: Pair<string, number>[] = [];
// only include valid layers
for (const name of ["detail-rear", ...OutfitStore.get().getLayerNames()]) {
const index = this.layers[name];
if (index != undefined) {
layers.push(new Pair(name, index));
}
}
return layers;
}

/**
* Sets coloring info for this outfit.
*
* @param coloring {object}
* Color values indexed by layer name.
*/
public setColoring(coloring: {[name: string]: number}) {
this.coloring = coloring;
}

/**
* Retrieves coloring info for this outfit.
*
* @return {object}
* Color values indexed by layer name or `undefined`.
*/
public getColoring(): {[name: string]: number}|undefined {
return this.coloring;
}

/**
* Retrieves coloring for a single layer.
*
* @param name {string}
* Layer name.
* @return {number}
* Color value or `undefined`.
*/
public getLayerColor(name: string): number|undefined {
return this.coloring && name in this.coloring ? this.coloring[name] : undefined;
}

/**
* Retrieves signature identifying this outfit.
*/
public getSignature(): string {
const lsig: string[] = [];
const csig: string[] = [];
for (const layer of this.getLayers()) {
lsig.push(layer.join("="));
}
if (this.coloring) {
for (const name of Object.keys(this.coloring)) {
csig.push(name + "=" + this.coloring[name]);
}
}
return "outfit(" + lsig.join(",") + ")" + (csig ? " colors(" + csig.join(",") + ")" : "");
}

/**
* Compares outfit signatures for equality.
*
* @param other {data.Outfit.Outfit}
* The outfit to compare against this one.
*/
public equals(other: Outfit): boolean {
return other.getSignature() === this.getSignature();
}
}
82 changes: 52 additions & 30 deletions src/js/stendhal/entity/RPEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { Entity } from "./Entity";
import { singletons } from "../SingletonRepo";
import { MenuItem } from "../action/MenuItem";

import { Outfit } from "../data/Outfit";

import { Color } from "../data/color/Color";

import { Chat } from "../util/Chat";
Expand Down Expand Up @@ -55,6 +57,8 @@ export class RPEntity extends ActiveEntity {
protected titleDrawYOffset: number = 0;
// canvas for merging outfit layers to be drawn
private octx?: CanvasRenderingContext2D;
/** This entity's outfit. */
private outfit?: Outfit;

private attackers: {[key: string]: any} = { size: 0 };

Expand Down Expand Up @@ -104,6 +108,9 @@ export class RPEntity extends ActiveEntity {
this.createTitleTextSprite();
} else if (key === "subclass" && typeof(oldValue) !== "undefined" && value !== oldValue) {
this.onTransformed();
} else if ((key === "outfit_ext" || key === "outfit") && this.outfit) {
// update outfit if it has already been created
this.buildOutfit();
}
}

Expand All @@ -115,6 +122,8 @@ export class RPEntity extends ActiveEntity {
this.addFloater("Back", "#ffff00");
} else if (key === "grumpy") {
this.addFloater("Receptive", "#ffff00");
} else if (key === "outfit_ext" || key === "outfit") {
this.outfit = undefined;
}
super.unset(key);
}
Expand Down Expand Up @@ -218,29 +227,42 @@ export class RPEntity extends ActiveEntity {
return typeof(this["no_shadow"]) === "undefined";
}

drawMultipartOutfit(ctx: CanvasRenderingContext2D) {
const store = singletons.getOutfitStore();
// layers in draw order
var layers = store.getLayerNames();

var outfit: {[key: string]: number} = {};
if ("outfit_ext" in this) {
for (const part of this["outfit_ext"].split(",")) {
/**
* Sets entity's outfit.
*/
private buildOutfit() {
const legacy = !("outfit_ext" in this);
const layerInfo = legacy ? this["outfit"] : this["outfit_ext"];
this.outfit = new Outfit();
if (!legacy) {
for (const part of layerInfo.split(",")) {
if (part.includes("=")) {
var tmp = part.split("=");
outfit[tmp[0]] = parseInt(tmp[1], 10);
this.outfit.setLayer(tmp[0], parseInt(tmp[1], 10));
}
}
const detailIndex = this.outfit.getLayerIndex("detail");
if (detailIndex != undefined && singletons.getOutfitStore().detailHasRearLayer(detailIndex)) {
this.outfit.setLayer("detail-rear", detailIndex);
}
} else {
layers = store.getLayerNames(true);
this.outfit.setLayer("body", layerInfo % 100);
this.outfit.setLayer("dress", Math.floor(layerInfo/100) % 100);
this.outfit.setLayer("head", Math.floor(layerInfo/10000) % 100);
this.outfit.setLayer("hair", Math.floor(layerInfo/1000000) % 100);
this.outfit.setLayer("detail", Math.floor(layerInfo/100000000) % 100);
}

outfit["body"] = this["outfit"] % 100;
outfit["dress"] = Math.floor(this["outfit"]/100) % 100;
outfit["head"] = Math.floor(this["outfit"]/10000) % 100;
outfit["hair"] = Math.floor(this["outfit"]/1000000) % 100;
outfit["detail"] = Math.floor(this["outfit"]/100000000) % 100;
if ("outfit_colors" in this) {
this.outfit.setColoring(this["outfit_colors"]);
}
}

drawMultipartOutfit(ctx: CanvasRenderingContext2D) {
if (!this.outfit) {
this.buildOutfit();
}
const outfit = this.outfit!;
if (stendhal.config.getBoolean("effect.shadows") && this.castsShadow()) {
// dressed entities should use 48x64 sprites
// FIXME: this will not display correctly for horse outfit
Expand All @@ -255,17 +277,16 @@ 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"])) {
layers.splice(0, 0, "detail-rear");
outfit["detail-rear"] = outfit["detail"];
}
for (const layer of layers) {
// TODO: cache outfit sprites
const bodyIndex = outfit.getLayerIndex("body") || 0;
const hatIndex = outfit.getLayerIndex("hat");
for (const p of outfit.getLayers()) {
// hair is not drawn under certain hats/helmets
if (layer == "hair" && !stendhal.data.outfit.drawHair(outfit["hat"])) {
if (p.first == "hair" && !stendhal.data.outfit.drawHair(hatIndex)) {
continue;
}

const lsprite = this.getOutfitPart(layer, outfit[layer], outfit["body"]);
const lsprite = this.getOutfitPart(p.first, p.second, bodyIndex);
if (lsprite && lsprite.complete && lsprite.height) {
if (!this.octx) {
let ocanvas = document.createElement("canvas");
Expand Down Expand Up @@ -312,15 +333,16 @@ export class RPEntity extends ActiveEntity {
}

const filename = stendhal.paths.sprites + "/outfit/" + part + "/" + n + ".png";
const colors = this["outfit_colors"];
let colorname;
if (part === "body" || part === "head") {
colorname = "skin";
} else {
colorname = part;
let color: any;
if (this.outfit) {
if (part === "body" || part === "head") {
color = this.outfit.getLayerColor("skin");
} else {
color = this.outfit.getLayerColor(part);
}
}
if (typeof(colors) !== "undefined" && (typeof(colors[colorname]) !== "undefined")) {
return stendhal.data.sprites.getFiltered(filename, "trueColor", colors[colorname]);
if (typeof(color) !== "undefined") {
return stendhal.data.sprites.getFiltered(filename, "trueColor", color);
} else {
return stendhal.data.sprites.get(filename);
}
Expand Down

0 comments on commit 8ad155e

Please sign in to comment.