Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ jsvg = { group = "com.github.weisj", name = "jsvg", version = "1.4.0" }

handlebars = { group = "com.github.jknack", name = "handlebars", version.ref = "handlebars" }
handlebars-helpers = { group = "com.github.jknack", name = "handlebars-helpers", version.ref = "handlebars" }
handlebars-json = { group = "com.github.jknack", name = "handlebars-jackson2", version = "4.3.1" }

# Apache commons and other utilities
# parsing of configuration data
Expand Down Expand Up @@ -213,7 +214,7 @@ flatlaf = [
"flatlaf-extras",
"flatlaf-jide-oss",
]
handlebars = ["handlebars", "handlebars-helpers"]
handlebars = ["handlebars", "handlebars-helpers", "handlebars-json"]
junit = ["junit-api", "junit-engine", "junit-params"]
jai-imageio = ["jai-imageio-core", "jai-imageio-jpeg"]
graalvm-js = ["graalvm-js", "graalvm-js-scriptengine"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class DeveloperOptions {
new PreferenceStore(Preferences.userRoot().node(AppConstants.APP_NAME + "/dev"));

public static final class Toggle {
public static final Preference<Boolean> EnableHandlebarsDebugging =
store.defineBoolean(
"enableHandlebarsDebugging",
"Preferences.developer.enableHandlebarsDebugging.label",
"Preferences.developer.enableHandlebarsDebugging.tooltip",
false);
public static final Preference<Boolean> AutoSaveMeasuredInSeconds =
store.defineBoolean(
"autoSaveMeasuredInSeconds",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ private Object getProperties(
seg, coords[0], coords[1], coords[2], coords[3], coords[4], coords[5], coords[6]));
pi.next();
}

StringBuilder stringBuilder = new StringBuilder(sd.toNonLocalisedString());
stringBuilder.append("segments=").append(String.join(",", segments)).append(";");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,37 @@
*/
package net.rptools.maptool.client.ui.sheet.stats;

import com.github.jknack.handlebars.*;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import javafx.application.Platform;
import net.rptools.maptool.client.AppConstants;
import net.rptools.maptool.client.DeveloperOptions;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.ui.htmlframe.HTMLContent;
import net.rptools.maptool.model.Token;
import net.rptools.maptool.model.sheet.stats.StatSheetContext;
import net.rptools.maptool.model.sheet.stats.StatSheetLocation;
import net.rptools.maptool.util.HBDebugUtil;
import net.rptools.maptool.util.HandlebarsUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Class that represents a pop up stat sheet. */
public class StatSheet {

/** Object for logging messages. */
private static final Logger log = LogManager.getLogger(StatSheet.class);
private static final Logger log = LoggerFactory.getLogger(StatSheet.class);

private static final HBDebugUtil HBD;

static {
HBDebugUtil hbd = null;
if (DeveloperOptions.Toggle.EnableHandlebarsDebugging.get()) {
hbd = new HBDebugUtil();
}
HBD = hbd;
}

/**
* Sets the content for the stat sheet. The content is a HTML page that is rendered using the
Expand Down Expand Up @@ -67,6 +80,10 @@ public void setContent(Token token, String content, URL entry, StatSheetLocation
null);
}
});
if (HBD != null) {
Platform.runLater(
() -> HBD.publish(statSheetContext, token, content, entry, output.getHtmlString()));
}
} catch (IOException e) {
MapTool.showError("msg.error.renderingStatSheet", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,35 @@
*/
package net.rptools.maptool.model.sheet.stats;

import java.awt.Dimension;
import java.util.ArrayList;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.text.Collator;
import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import net.rptools.lib.AwtUtil;
import net.rptools.lib.MD5Key;
import net.rptools.maptool.client.AppPreferences;
import net.rptools.maptool.client.AppUtil;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.MapToolVariableResolver;
import net.rptools.maptool.client.ui.token.AbstractTokenOverlay;
import net.rptools.maptool.client.ui.token.BarTokenOverlay;
import net.rptools.maptool.model.Token;
import net.rptools.maptool.model.player.Player;
import net.rptools.maptool.util.HTMLUtil;
import net.rptools.maptool.util.ImageManager;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;

/** Class that extracts and provides the information needed to render a stat sheet. */
@SuppressWarnings("unused")
public class StatSheetContext {

/** Class that represents a token property on a stat sheet. */
static class Property {
public static class Property {
/** Name of the property. */
private final String name;

Expand All @@ -56,7 +65,7 @@ static class Property {
* @param displayName Display Name of the property.
* @param value Value of the property.
* @param gmOnly True if the property is GM only.
* @note GM only properties are only extracted if the player is a GM.
* @implNote GM only properties are only extracted if the player is a GM.
*/
Property(String name, String displayName, String shortName, Object value, boolean gmOnly) {
this.name = name;
Expand Down Expand Up @@ -139,6 +148,12 @@ public String getShortName() {
/** The properties of the token. */
private final List<Property> properties = new ArrayList<>();

/** The bars shown on the token. */
private final List<Map<String, Object>> bars = new ArrayList<>();

/** The states set on the token. */
private final List<Map<String, Object>> states = new ArrayList<>();

/** The notes of the token. */
private final String notes;

Expand Down Expand Up @@ -168,11 +183,27 @@ public String getShortName() {
* @param location The location of the stat sheet.
*/
public StatSheetContext(Token token, Player player, StatSheetLocation location) {
boolean playerOwns = AppUtil.playerOwns(token);
boolean playerIsGm = player.isGM();

name = token.getName();
tokenType = token.getType().name();

if (player.isGM()) {
/* Combined list of Bar and State names */
final List<String> OVERLAY_NAMES =
Stream.concat(
MapTool.getCampaign().getTokenBarsMap().keySet().stream(),
MapTool.getCampaign().getTokenStatesMap().keySet().stream())
.toList();

for (String stateName : OVERLAY_NAMES) {
Object stateValue = token.getState(stateName);
if (stateValue != null) {
addBarOrState(stateName, stateValue, playerOwns, playerIsGm);
}
}

if (playerIsGm) {
gmName = token.getGMName();
gmNotes = token.getGMNotes();
gmNotesType = token.getNotesType();
Expand All @@ -183,8 +214,8 @@ public StatSheetContext(Token token, Player player, StatSheetLocation location)
gmNotesType = null;
gm = false;
}
notes = AppUtil.playerOwns(token) ? token.getNotes() : null;
notesType = AppUtil.playerOwns(token) ? token.getNotesType() : null;
notes = playerOwns ? token.getNotes() : null;
notesType = playerOwns ? token.getNotesType() : null;
speechName = token.getSpeechName();

if (AppPreferences.showPortrait.get()) {
Expand All @@ -202,11 +233,11 @@ public StatSheetContext(Token token, Player player, StatSheetLocation location)
.forEach(
tp -> {
if (tp.isShowOnStatSheet()) {
if (tp.isGMOnly() && !MapTool.getPlayer().isGM()) {
if (tp.isGMOnly() && !playerIsGm) {
return;
}

if (tp.isOwnerOnly() && !AppUtil.playerOwns(token)) {
if (tp.isOwnerOnly() && !playerOwns) {
return;
}

Expand All @@ -215,10 +246,8 @@ public StatSheetContext(Token token, Player player, StatSheetLocation location)
return;
}

if (value instanceof String svalue) {
if (svalue.isBlank()) {
return;
}
if (value instanceof String sValue && sValue.isBlank()) {
return;
}
properties.add(
new Property(
Expand All @@ -232,11 +261,9 @@ public StatSheetContext(Token token, Player player, StatSheetLocation location)

Dimension dim;
if (token.getPortraitImage() != null) {
var image = ImageManager.getImage(token.getPortraitImage());
dim = new Dimension(image.getWidth(), image.getHeight());
dim = getImageDimensions.apply(token.getPortraitImage());
} else {
var image = ImageManager.getImage(token.getImageAssetId());
dim = new Dimension(image.getWidth(), image.getHeight());
dim = getImageDimensions.apply(token.getImageAssetId());
}
AwtUtil.constrainTo(dim, AppPreferences.portraitSize.get());
portraitWidth = dim.width;
Expand All @@ -255,6 +282,106 @@ public StatSheetContext(Token token, Player player, StatSheetLocation location)
};
}

private static final Function<MD5Key, Dimension> getImageDimensions =
md5Key -> {
BufferedImage image = ImageManager.getImage(md5Key);
return new Dimension(image.getWidth(), image.getHeight());
};

/** Comparator for sorting State Groups */
private static final Comparator<Map<String, Object>> stateComparator =
(o1, o2) -> {
String s1 = o1.get("group").toString();
String s2 = o2.get("group").toString();
// for different groups use natural order by group value
int result = Collator.getInstance().compare(s1, s2);
if (result != 0) {
return result;
}
// for the same group, use the "order" value - should always be present
if (Objects.equals(s1, s2)
&& o1.get("order") instanceof Integer i1
&& o2.get("order") instanceof Integer i2) {
return i1.compareTo(i2);
}
return 0; // should never reach this point
};

/**
* Method for filtering overlays and adding them to the appropriate data set
*
* @param overlayName Name of bar or state
* @param overlayValue Value attached to bar or state
* @param playerOwns Used for filtering what to display
* @param playerIsGm Used for filtering what to display
*/
private void addBarOrState(
String overlayName, Object overlayValue, boolean playerOwns, boolean playerIsGm) {

AbstractTokenOverlay ato;
if (MapTool.getCampaign().getTokenBarsMap().containsKey(overlayName)) {
ato = MapTool.getCampaign().getTokenBarsMap().get(overlayName);
} else {
ato = MapTool.getCampaign().getTokenStatesMap().get(overlayName);
}
if (ato == null) {
return;
}
if ((ato.isShowOthers() && !playerOwns)
|| (playerOwns && ato.isShowOwner())
|| (playerIsGm && ato.isShowGM())) {
Map<String, Object> featureMap = new HashMap<>();
featureMap.put(
"type",
ato.getClass()
.getSimpleName()
.replaceAll("BarTokenOverlay", "")
.replaceAll("TokenOverlay", ""));
try {
PropertyUtilsBean pub = BeanUtilsBean.getInstance().getPropertyUtils();
featureMap.putAll(pub.describe(ato));
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
String mName;
Map<String, Object> aspectRatioMap = new HashMap<>();
for (Map.Entry<String, Object> entry : featureMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof Color color) {
featureMap.put(
entry.getKey(),
String.format(
"rgba(%d,%d,%d,%#.3f)",
color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 255f));
} else if (value instanceof MD5Key id) {
featureMap.put(entry.getKey(), String.format("asset://%s", id));
Dimension dim = getImageDimensions.apply(id);
aspectRatioMap.put(entry.getKey() + "AspectRatio", dim.getWidth() / dim.getHeight());
} else if (value instanceof MD5Key[] idArray) {
String[] strOut = new String[idArray.length];
double[] arOut = new double[idArray.length];
for (int i = 0; i < idArray.length; i++) {
strOut[i] = String.format("asset://%s", idArray[i].toString());
Dimension dim = getImageDimensions.apply(idArray[i]);
arOut[i] = dim.getWidth() / dim.getHeight();
}
aspectRatioMap.put(entry.getKey() + "AspectRatio", arOut);
featureMap.put(entry.getKey(), strOut);
}
}
featureMap.putAll(aspectRatioMap);
if (ato instanceof BarTokenOverlay) {
featureMap.put(
"value", overlayValue instanceof BigDecimal bd ? bd.doubleValue() : overlayValue);
featureMap.remove("group"); // does not apply to bars
featureMap.remove("order"); // does not apply to bars
bars.add(featureMap);
} else {
states.add(featureMap);
}
}
}

/**
* Returns the name of the token.
*
Expand Down Expand Up @@ -328,9 +455,9 @@ public List<Property> getProperties() {
}

/**
* Returns the css class for the location of the stat sheet.
* Returns the CSS class for the location of the stat sheet.
*
* @return The css class for the location of the stat sheet.
* @return The CSS class for the location of the stat sheet.
*/
public String getStatSheetLocation() {
return statSheetLocation;
Expand Down Expand Up @@ -388,4 +515,19 @@ public String getTokenType() {
public boolean isGm() {
return gm;
}

/**
* @return States set on the token.
*/
public List<Map<String, Object>> getStates() {
states.sort(stateComparator);
return states;
}

/**
* @return Bars available on the token.
*/
public List<Map<String, Object>> getBars() {
return bars;
}
}
Loading
Loading