diff --git a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java index 92bb63a4ca..b5c365eca0 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolExpressionParser.java @@ -43,6 +43,7 @@ public class MapToolExpressionParser extends ExpressionParser { ExecFunction.getInstance(), FindTokenFunctions.getInstance(), HasImpersonated.getInstance(), + IlluminationFunctions.getInstance(), InitiativeRoundFunction.getInstance(), InputFunction.getInstance(), IsTrustedFunction.getInstance(), diff --git a/src/main/java/net/rptools/maptool/client/functions/IlluminationFunctions.java b/src/main/java/net/rptools/maptool/client/functions/IlluminationFunctions.java new file mode 100644 index 0000000000..8bef6cc465 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/functions/IlluminationFunctions.java @@ -0,0 +1,128 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.functions; + +import com.google.gson.JsonNull; +import java.awt.geom.Point2D; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.functions.json.JSONMacroFunctions; +import net.rptools.maptool.client.ui.zone.PlayerView; +import net.rptools.maptool.client.ui.zone.ZoneRenderer; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.Token; +import net.rptools.maptool.util.FunctionUtil; +import net.rptools.parser.Parser; +import net.rptools.parser.ParserException; +import net.rptools.parser.VariableResolver; +import net.rptools.parser.function.AbstractFunction; +import net.rptools.parser.function.Function; + +public class IlluminationFunctions extends AbstractFunction { + private static final IlluminationFunctions instance = new IlluminationFunctions(); + + private IlluminationFunctions() { + super(0, Function.UNLIMITED_PARAMETERS, "getIllumination"); + } + + public static IlluminationFunctions getInstance() { + return instance; + } + + @Override + public Object childEvaluate( + Parser parser, VariableResolver resolver, String functionName, List parameters) + throws ParserException { + if (functionName.equalsIgnoreCase("getIllumination")) { + return BigDecimal.valueOf(getIllumination(functionName, parameters)); + } + + throw new ParserException(I18N.getText("macro.function.general.unknownFunction", functionName)); + } + + private int getIllumination(String functionName, List parameters) throws ParserException { + FunctionUtil.blockUntrustedMacro(functionName); + FunctionUtil.checkNumberParam(functionName, parameters, 2, 4); + + final var x = FunctionUtil.paramAsInteger(functionName, parameters, 0, false); + final var y = FunctionUtil.paramAsInteger(functionName, parameters, 1, false); + final var point = new Point2D.Double(x, y); + + final var renderer = FunctionUtil.getZoneRendererFromParam(functionName, parameters, 2); + final var playerView = getPlayerView(functionName, renderer, parameters, 3); + + for (final var lumensLevel : + renderer.getZoneView().getDisjointObscuredLumensLevels(playerView)) { + if (lumensLevel.darknessArea().contains(point)) { + return -lumensLevel.lumensStrength(); + } else if (lumensLevel.lightArea().contains(point)) { + return lumensLevel.lumensStrength(); + } + } + + return 0; + } + + /** + * Builds a player view for `getIllumination()` + * + * @param functionName + * @param renderer + * @param parameters + * @param tokenListIndex + * @return If the token list is not provided, the current view for the zone. If `json.null` is + * provided, a non-token view. If a token ID list is provided, a token view containing the + * identified tokens. is returned. If the token ID list is empty, a token view with no in it. + * @throws ParserException + */ + private PlayerView getPlayerView( + String functionName, ZoneRenderer renderer, List parameters, int tokenListIndex) + throws ParserException { + if (parameters.size() <= tokenListIndex) { + return renderer.getPlayerView(); + } + + final var parameter = parameters.get(tokenListIndex); + if (parameter instanceof JsonNull) { + // Explicitly requesting without token view. + return new PlayerView(MapTool.getPlayer().getEffectiveRole()); + } + + // Tokens for the view passed as a JSON array. + final var jsonList = + JSONMacroFunctions.getInstance().asJsonElement(parameters.get(3).toString()); + if (!jsonList.isJsonArray()) { + // Whoops, we need an array. + throw new ParserException(I18N.getText("macro.function.general.argumentTypeA", functionName)); + } + final var jsonArray = jsonList.getAsJsonArray(); + + final var tokens = new ArrayList(); + for (final var element : jsonArray) { + final var identifier = JSONMacroFunctions.getInstance().jsonToScriptString(element); + final var token = renderer.getZone().resolveToken(identifier); + if (token == null) { + throw new ParserException( + I18N.getText("macro.function.general.unknownToken", functionName, identifier)); + } + + tokens.add(token); + } + + return new PlayerView(MapTool.getPlayer().getEffectiveRole(), tokens); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java b/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java index f8a780deb1..b32d135b71 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/Illumination.java @@ -183,7 +183,7 @@ public Optional getObscuredLumensLevel(int lumensStrength) { *

This is useful for rendering the lumens levels, so that well-defined boundaries exist * between each lumens level. * - * @return The obscured lumens levels. + * @return The obscured lumens levels, ordered from strong to weak lumens. */ public @Nonnull List getDisjointObscuredLumensLevels() { if (disjointObscuredLumensLevels == null) { diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/PlayerView.java b/src/main/java/net/rptools/maptool/client/ui/zone/PlayerView.java index 91cbe979e4..1f3a6b6cb0 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/PlayerView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/PlayerView.java @@ -27,13 +27,31 @@ public class PlayerView { // Optimization private final String hash; + /** + * Creates a player view that does not use token views. + * + *

Calling `isUsingTokenView()` on the new player view will return {@code false} and {@link + * #getTokens()} should not be called. + * + * @param role The player role for the view. + */ public PlayerView(Player.Role role) { - this(role, null); + this.role = role; + this.tokens = null; + hash = calculateHashcode(); } + /** + * Creates a player view for a token view. + * + *

Calling `isUsingTokenView()` on the new player view will return {@code false} and {@link + * #getTokens()} can be called to retrieve the list of tokens. + * + * @param role The player role for the view. + */ public PlayerView(Player.Role role, List tokens) { this.role = role; - this.tokens = tokens != null && !tokens.isEmpty() ? tokens : null; + this.tokens = tokens; hash = calculateHashcode(); } @@ -45,6 +63,13 @@ public boolean isGMView() { return role == Player.Role.GM; } + /** + * Gets the tokens for this view. + * + *

This method should only be used when {@link #isUsingTokenView()} returns {@code true}. + * + * @return The tokens for this view. + */ public List getTokens() { return tokens; } @@ -74,6 +99,7 @@ private String calculateHashcode() { StringBuilder builder = new StringBuilder(); builder.append(role); if (tokens != null) { + builder.append('|'); // Distinguishes null and empty case. for (Token token : tokens) { builder.append(token.getId()); } diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index bc3691e25c..59e5706f02 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -880,6 +880,10 @@ public PlayerView getPlayerView(Player.Role role, boolean selected) { ? zone.getOwnedTokensWithSight(MapTool.getPlayer()) : zone.getPlayerTokensWithSight(); } + if (selectedTokens == null || selectedTokens.isEmpty()) { + return new PlayerView(role); + } + return new PlayerView(role, selectedTokens); } @@ -1512,9 +1516,6 @@ private void renderLumensOverlay( timer.stop("renderLumensOverlay:setTransform"); timer.start("renderLumensOverlay:drawLumens"); - // Lumens are ordered to be weak to strong. That works for us as we will draw the stronger - // areas overtop the weaker areas using `AlphaComposite.Src` to make sure the stronger one - // "wins". for (final var lumensLevel : disjointLumensLevels) { final var lumensStrength = lumensLevel.lumensStrength(); diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java index 2c84f3e29c..8b0bf21273 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneView.java @@ -549,7 +549,7 @@ public Illumination getIllumination(PlayerView view) { * * @param view * @return The various lumens levels, with any stronger lumens areas being subtracted from weaker - * lumens areas. + * lumens areas and ordered from strong to weak lumens. */ public List getDisjointObscuredLumensLevels(PlayerView view) { final var illumination = getIllumination(view); diff --git a/src/main/java/net/rptools/maptool/util/FunctionUtil.java b/src/main/java/net/rptools/maptool/util/FunctionUtil.java index 27e5bacab8..ea589ccee3 100644 --- a/src/main/java/net/rptools/maptool/util/FunctionUtil.java +++ b/src/main/java/net/rptools/maptool/util/FunctionUtil.java @@ -203,6 +203,9 @@ public static Token getTokenFromParam( ZoneRenderer zoneRenderer; if (map == null) { zoneRenderer = MapTool.getFrame().getCurrentZoneRenderer(); + if (zoneRenderer == null) { + throw new ParserException(I18N.getText("macro.function.map.none", functionName)); + } } else { zoneRenderer = getZoneRenderer(functionName, map); }