Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unique light sources on tokens, and macros for manipulating them. #4356

Merged

Conversation

kwvanderlinde
Copy link
Collaborator

@kwvanderlinde kwvanderlinde commented Nov 1, 2023

Identify the Bug or Feature request

Implements #331 and so also relates to #3087.

Description of the Change

This changes adds the concept of "unique light sources", which are lights sources defined on tokens rather than in the campaign as a whole. A token with such a unique light source can equipped or unequip it just like any other light sources in its right-click menu.

Macro functions are provided for defining and manipulating unique light sources. No UI is provided at this time for defining the lights (doing so would round out #3087).

Existing macro function changes

The following existing macro functions have been updated:

  • getLights([category, [delim, [token, [map]]]])
  • setLight(category, name, state, [token, [map]])
  • hasLightSource(category, name, [token, [map]])

The category parameter of each can now be set to the special "$unique" category to identify unique light sources. The wildcard "*" will likewise now include the token's unique light sources. I.e., these function treat the token's unique light sources as though they just belong to a specially named category.

New macro functions

The following new macro functions have been added:

  • createUniqueLightSource(lightSourceJson, [token, [map]]) to create a new unique light source.
  • updateUniqueLightSource(lightSourceJson, [token, [map]]) to modify a unique light source with the matching name.
  • deleteUniqueLightSource(name, [token, [map]]) to delete a unique light source by name.
  • getUniqueLightSource(name, [token, [map]]) to get a unique light source by name, as a JSON object.
  • getUniqueLightSources([token, [map]]) to get all of a token's unique light sources as a JSON array of JSON objects.
  • getUniqueLightSourceNames([token, [map]]) to get the names of all of a token's light sources, as a JSON array.

A consistent JSON format is used for light sources in the parameters and return values of these functions, and uses terminology from the Campaign Properties dialog. It looks like this:

{
	"name": "Torch - 20",
	"type": "NORMAL",
	"scale": false,
	"lights": [
		{"shape": "CIRCLE", "range": 20, "lumens": 100},
		{"shape": "CIRCLE", "range": 40, "color": "#000000", "lumens": 50}
	]
}

The exact fields are:

  • "name": string; required.
  • "type": ("NORMAL"|"AURA"); defaults to "NORMAL".
  • "scale": boolean; defaults to false.
  • "lights" array; defauls to [].

And for each light:

  • "range": number, required.
  • "shape": ("SQUARE"|"CIRCLE"|"CONE"|"HEX"|"GRID"), defaults to "CIRCLE".
  • "offset": number, defaults to 0. Only permitted if the shape is "CONE".
  • "arc": number, defaults to 0. Only permitted if the shape is "CONE".
  • "gmOnly": boolean, defaults to false. Only permitted if the light source type is "AURA".
  • "ownerOnly": boolean, defaults to false. Only permitted if the light source type is "AURA".
  • "color": string of the form "#rrggbb, defaults to null.
  • "lumens": number (integer), defaults to 100.

Refactoring

There was some refactoring done as groundwork.

LightSource is now immutable to avoid worries about cloning them along with tokens. In practice lights weren't been modified anyways - only CampaignPropertiesDialog and LightSourceCreator used the mutators, and that was only to build the light. Once built, the LightSource instances were never being modifed.

The existing token update messages for light sources (e.g., for attaching to a token) were reduced to passing around the light source ID instead of the entire light source definition. The ID was the only thing ever used, so there was no point serializing everything for one GUID. Now the only time a complete light source is sent in a token update is the new case of creating a unique light source.

AttachedLightSource has been encapsulated more and made responsible for the common case of looking up light sources by ID. Callers just need to provide the current campaign (and now also the associated token) and the AttachedLightSource will look itself up and return a LightSource.

Possible Drawbacks

There were some changes to the model. Should be benign, but need to watch for deserialization failures on existing campaigns.

If someone is already using $unique as a light source category, it won't be possible to find them with the macro functions now.

Documentation Notes

For the existing functions getLights(), setLight(), and hasLightSource(), the type parameter (which should be called "category") can now be set to "$unique" to access unique light sources on the token.

createUniqueLightSource() function

Trusted function

Creates a new unique light source on a token.

Usage

createUniqueLightSource(lightSourceJson)
createUniqueLightSource(lightSourceJson, token)
createUniqueLightSource(lightSourceJson, token, map)

Parameters

  • lightSourceJson - a JSON object defining the new unique light source. See below for details.
  • token - the name or ID of the token to add the unique light source to. Defaults to the current token.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

The lightSourceJson is a JSON object with these fields:

  • "name": The user-visible name of the unique light source. A unique light source with the same name must not already exist on the token. Required field.
  • "type": The type of the unique light source. Either "NORMAL" or "AURA"; defaults to "NORMAL".
  • "scale": Whether to add the token footprint (size) to the range, so that the light starts at the token's edge vs the token's center. Either json.true or json.false; defaults to json.false.
  • "lights": A JSON array of lights; defauls to [].

Each light has this JSON format:

  • "range": The distance that the light extends, in map units. Required field.
  • "shape": The shape of the boundary of the light. Must be one of "SQUARE", "CIRCLE", "CONE", "HEX", or "GRID"; defaults to "CIRCLE".
  • "offset": (for cones only) The counterclockwise angle (in degrees) to shift the cone relative to the token's facing. Defaults to 0.
  • "arc": (for cones only) The angle of the cone in degrees. Defaults to 0.
  • "gmOnly": (for auras only) Whether the aura should only be shown to GMs. Either json.true or json.false; defaults to json.false.
  • "ownerOnly": (for auras only) Whether the aura should only be shown to the tokens owners. Either json.true or json.false; defaults to json.false.
  • "color": The color of the light, in the form "#rrggbb"; defaults to no color (transparent).
  • "lumens": How bright the light should be treated, with negative values indicating darkness. Defaults to 100.

Return value

The name of the new unique light source.

Examples

To create a standard D&D torch, but with a blue flame:

createUniqueLightSource(json.set("{}",
	"name", "Blue Flame Torch",
	"type", "NORMAL",
	"scale", false,
	"lights", json.append("[]",
		json.set("{}", "shape", "CIRCLE", "range", 20, "color", "#0000ff", "lumens", 100),
		json.set("{}", "shape", "CIRCLE", "range", 40, "color", "#000077", "lumens", 50)
	)
))

And the same example, but using defaults where possible:

createUniqueLightSource(json.set("{}",
	"name", "Blue Flame Torch",
	"lights", json.append("[]",
		json.set("{}", "range", 20, "color", "#0000ff"),
		json.set("{}", "range", 40, "color", "#000077", "lumens", 50)
	)
))

updateUniqueLightSource() function

Trusted function

Modifies a unique light source on a token.

Usage

updateUniqueLightSource(lightSourceJson)
updateUniqueLightSource(lightSourceJson, token)
updateUniqueLightSource(lightSourceJson, token, map)

Parameters

  • lightSourceJson - a JSON object defining the new unique light source. The format is the same as for createUniqueLightSource().
  • token - the name or ID of the token to to find the unique light source on. Defaults to the current token.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

Return value

The name of the modified unique light source.

deleteUniqueLightSource() function

Removes a unique light source from a token.

Note: if you merely want to unequip or turn off the unique light source, but keep it around for later, use setLight() instead.

Usage

deleteUniqueLightSource(name)
deleteUniqueLightSource(name, token)
deleteUniqueLightSource(name, token, map)

Parameters

  • name - the name of the unique light source to delete.
  • token - the name or ID of the token to delete the unique light source from. Defaults to the current token. This parameter can only be used in a trusted macro.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

getUniqueLightSource() function

Looks up a unique light source by name on a token, returning the

Usage

getUniqueLightSource(name)
getUniqueLightSource(name, token)
getUniqueLightSource(name, token, map)

Parameters

  • name - the name of the unique light source to delete.
  • token - the name or ID of the token to find the unique light source on. Defaults to the current token. This parameter can only be used in a trusted macro.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

Return value

The matching unique light source, as a JSON object. See createUniqueLightSource() for the JSON format of a light source.

If there is no match unique light source, an empty string is returned.

Examples

[h: createUniqueLightSource(json.set("{}",
	"name", "Simple Torch",
	"lights", json.append("[]", json.set("{}", "range", 40))
))]

[r: getUniqueLightSource("Simple Torch")]

This will print {"name":"Simple Torch","type":"NORMAL","scale":false,"lights":[{"shape":"CIRCLE","range":40.0,"lumens":100}]}

getUniqueLightSources() function

Gets all the unique light sources defined on a token.

Usage

getUniqueLightSources()
getUniqueLightSources(token)
getUniqueLightSources(token, map)

Parameters

  • token - the name or ID of the token to get the unique light sources from. Defaults to the current token. This parameter can only be used in a trusted macro.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

** Return value**

A JSON array of all the token's unique light sources. As if getUniqueLightSource() was call for each unique light source on the token and the results put in an array.

getUniqueLightSourceNames() function

Get the names of all the unique light sources defined on a token.

Usage

getUniqueLightSourceNames()
getUniqueLightSourceNames(token)
getUniqueLightSourceNames(token, map)

Parameters

  • token - the name or ID of the token to get the unique light source from. Defaults to the current token. This parameter can only be used in a trusted macro.
  • map - the name or ID of the map on which to find token. Defaults to the current map.

** Return value**

A JSON array of all the names of the token's unique light sources.

Release Notes

  • Added macro functions for creating and manipulating unique light sources on tokens.

This change is Reviewable

Copy link
Collaborator

@thelsing thelsing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look much at how macros work so I can't say much about TokenLightFunctions but it looks good.
For the other stuff I added some comments.

@kwvanderlinde kwvanderlinde force-pushed the feature/331-unique-lights-on-tokens branch from eeedfaf to f156b59 Compare November 3, 2023 06:42
@cwisniew cwisniew added this pull request to the merge queue Nov 3, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2023
@cwisniew cwisniew added this pull request to the merge queue Nov 3, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2023
@cwisniew cwisniew added this pull request to the merge queue Nov 3, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2023
@cwisniew cwisniew added this pull request to the merge queue Nov 3, 2023
@cwisniew cwisniew added the feature Adding functionality that adds value label Nov 3, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2023
@cwisniew cwisniew added this pull request to the merge queue Nov 3, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 3, 2023
This involved reworking `CampaignPropertiesDialog` and `LightSourceCreator` as the only places the needed to mutate
`LightSource`. New static factories make it clearer which kind of light is being created while enforcing nullability
that doesn't agree with the "primary" constructor. The light's ID must be provided by the caller now instead of
`LightSource` deciding it for itself (avoids the need for `setId()`).

`Light` was already in practice immutable, but now both `Light` and `LightSource` are `final` classes with `final`
fields. `LightSource.lightList` remains a mutable list for serialization purposes, but is guarded to prevent any
mutations.
The protocol for adding and removing lights was streamlined: since the light source GUID is the only piece needed for
this functionality, that is now all that gets passed around via protobuf. Previously the entire light source definition
was passed around for these operations.

`AttachedLightSource` is now more encapsulated so that resolving `LightSource` from `GUID` is easier. Callers no longer
need to bother with grabbing the `GUID` from the `AttachedLightSource`, nor do they have to know where to use it.
Instead they can just ask the `AttachedLightSource` to do the lookup, providing only the `Campaign` for context. This
will enable future changes to how light sources can looked up. `AttachedLightSource` has also been made immutable by
finalizing the class and `lightSourceId` field.
@kwvanderlinde kwvanderlinde force-pushed the feature/331-unique-lights-on-tokens branch from f156b59 to 411ea18 Compare November 3, 2023 16:36
It is now possible for tokens to carry light source definitions, much like campaigns can. These new light sources are
called "unique lights" as they are only available to the token that defines them. If any unique light source are defined
on a token, they can be accessed like any other lights via the right-click menu, under the special category "Unique".

In order to support lookup of such light sources, `AttachedLightSource#resolve()` now also accepts a `Token` for
context, in addition to the `Campaign`. The token is checked first for a matching light source, then the campaign is
checked as a fallback.

For networking, the `TokenPropertyValueDto.lightSource` field was added back in because creating a unique light source
now requires the complete definition to be passed.

This commit does not add any way for a user to add any light sources to token. That will come later.
These functions have been updated:
- `getLights([category, [delim, [token, [map]]]])`
- `setLight(category, name, state, [token, [map]])`
- `hasLightSource(category, name, [token, [map]])`

They now support a special category `"$unique"` to identify unique light sources. Meanwhile the `"*"` category wildcard
will also find unique light sources in addition to campaign light sources.
The following new functions have been added:
- `createUniqueLightSource(lightSourceJson, [token, [map]])` to create a new unique light source.
- `updateUniqueLightSource(lightSourceJson, [token, [map]])` to modify the unique light source with the matching name.
- `deleteUniqueLightSource(name, [token, [map]])` to delete a unique light source by name.
- `getUniqueLightSource(name, [token, [map]])` to get a unique light source by name, as a JSON object.
- `getUniqueLightSources([token, [map]])` to get all of a token's light sources as a JSON array of JSON objects.
- `getUniqueLightSourceNames([token, [map]])` to get the names of all of a token's light sources, as a JSON array.

The JSON format for a unique light source look like:
```json
{
	"name": "Torch - 20",
	"type": "NORMAL",
	"scale": false,
	"lights": [
		{"shape": "CIRCLE", "range": 20, "lumens": 100},
		{"shape": "CIRCLE", "range": 40, "color": "#000000", "lumens": 50}
	]
}
```
Note that any boolean values are JSON booleans, not 0/1.

Light sources have the following fields:
- `"name"`: `string`; required.
- `"type"`: (`"NORMAL"`|`"AURA"`); defaults to `"NORMAL"`.
- `"scale"`: `boolean`; defaults to `false`.
- `"lights"` `array`; defauls to `[]`.

Each light has these fields:
- `"range"`: `number`, required.
- `"shape"`: (`"SQUARE"`|`"CIRCLE"`|`"CONE"`|`"HEX"`|`"GRID"`), defaults to `"CIRCLE"`.
- `"offset"`: `number`, defaults to `0`. Only permitted if the shape is `"CONE"`.
- `"arc"`: `number`, defaults to `0`. Only permitted if the shape is `"CONE"`.
- `"gmOnly"`: `boolean`, defaults to `false`. Only permitted if the light source type is `"AURA"`.
- `"ownerOnly"`: `boolean`, defaults to `false`. Only permitted if the light source is `"AURA"`.
- `"color"`: `string` of the form `"#rrggbb`, defaults to `null`.
- `"lumens"`: `number` (integer), defaults to 100.
It's possible for only some lights in an aura light source to be marked as GM-only (or OWNER-only). These were not being
shown in the right-click menu despite players being able to see them once equipped. Now these auras will show in the
menu, and will only be hidden if the entire auras is GM-only.

This also include a refactor to avoid duplicated logic between campaign light sources and unique light sources, while
also avoiding some of the complicated looping.
@kwvanderlinde kwvanderlinde force-pushed the feature/331-unique-lights-on-tokens branch from 411ea18 to 29e4660 Compare November 3, 2023 19:34
@kwvanderlinde
Copy link
Collaborator Author

Resolved the conflicts with the Cover VBL addition.

@cwisniew cwisniew added this pull request to the merge queue Nov 4, 2023
Merged via the queue into RPTools:develop with commit 7d89f48 Nov 4, 2023
4 checks passed
@bubblobill
Copy link
Collaborator

Umm.. hello? Can we undo this please? You know, until it works and I can edit tokens and stuff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Adding functionality that adds value ignore-for-release-note Wont be auto added to the release note
Projects
Development

Successfully merging this pull request may close these issues.

4 participants