Skip to content

Commit b8d4d6d

Browse files
AzurelolUnarekin
andauthored
Add pressure system visuals (by Unarekin) (#679)
* feat:Base implementation of pressure visuals * Add missing localization keys --------- Co-authored-by: Erica Phelps <erica@blackspork.com>
1 parent 6d9027d commit b8d4d6d

File tree

11 files changed

+696
-5
lines changed

11 files changed

+696
-5
lines changed

lang/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1741,11 +1741,19 @@
17411741
},
17421742
"OptionPressureSystem": "Enable Pressure System?",
17431743
"OptionPressureSystemHint": "Enables the Pressure and Stagger system.",
1744+
"OptionPressureGaugeShow": "Show Pressure Gauge",
1745+
"OptionPressureGaugeShowHint": "Whether to show the pressure gauge on tokens in the canvas.",
1746+
"OptionPressureGaugePosition": "Pressure Gauge Position",
1747+
"OptionPressureGaugePositionHint": "Where to place the pressure gauge relative to the token.",
1748+
"OptionPressureGaugePositionTop": "Top",
1749+
"OptionPressureGaugePositionBottom": "Bottom",
1750+
"OptionPressureGaugeTheme": "Pressure Gauge Theme",
1751+
"OptionPressureGaugeThemeHint": "What theme to use for the pressure gauge.",
1752+
"OptionPressureGaugeUseHUDTheme": "Use HUD Theme",
17441753
"Pressure": "Pressure",
17451754
"PressureClock": "Pressure Clock",
17461755
"PressurePoints": "Pressure Points",
17471756
"Stagger": "Stagger",
1748-
"Stagger": "Stagger",
17491757
"CompendiumBrowser": "Compendium Browser",
17501758
"CompendiumBrowserOpen": "Open Compendium Browser",
17511759
"Actors": "Actors",

module/helpers/config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,3 +1030,8 @@ FU.allIcon = {
10301030
...FU.attributeIcons,
10311031
...FU.weaponCategoryIcons,
10321032
};
1033+
1034+
/**
1035+
* Set of theme classes for pressure gauge
1036+
*/
1037+
FU.pressureGaugeThemes = {};

module/projectfu.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ import { InlineAction } from './helpers/inline-action.mjs';
110110
import { CompendiumBrowser } from './ui/compendium/compendium-browser.mjs';
111111
import { CompendiumIndex } from './ui/compendium/compendium-index.mjs';
112112
import { PressureSystem } from './systems/pressure-system.mjs';
113+
import { FUToken } from './ui/token.mjs';
114+
import { FUPressureGauge, FUModernPressureGauge, FUPixelPressureGauge } from './ui/pressureGauges/index.mjs';
113115

114116
globalThis.projectfu = {
115117
ClassFeatureDataModel,
@@ -176,6 +178,23 @@ Hooks.once('init', async () => {
176178
FU.ruleTriggerRegistry = RuleTriggerRegistry.instance;
177179
FU.rulePredicateRegistry = RulePredicateRegistry.instance;
178180

181+
// Set up pressure gauge things
182+
FU.pressureGaugeThemes = {
183+
default: FUPressureGauge,
184+
modern: FUModernPressureGauge,
185+
pixel: FUPixelPressureGauge,
186+
};
187+
188+
Hooks.on('canvasPan', () => {
189+
const start = performance.now();
190+
const tokens = canvas.scene.tokens.filter((token) => token.object instanceof FUToken);
191+
tokens.forEach((token) => token.object.pressureGauge.onCanvasScale());
192+
const duration = performance.now() - start;
193+
if (duration > 16) {
194+
console.warn(`Rescaling ${tokens.length} tokens with pressure gauges took ${duration}ms`);
195+
}
196+
});
197+
179198
/**
180199
* @type {Record<string, DataModelRegistry>}
181200
*/
@@ -392,6 +411,7 @@ Hooks.once('init', async () => {
392411

393412
// Override token ruler class
394413
CONFIG.Token.rulerClass = FUTokenRuler;
414+
CONFIG.Token.objectClass = FUToken;
395415

396416
// Preload Handlebars templates.
397417
return preloadHandlebarsTemplates();

module/settings.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WellspringDataModel } from './documents/items/classFeature/invoker/invo
66
import { CombatHudSettings } from './settings/combatHudSettings.js';
77
import { SettingsConfigurationApp } from './settings/settings-configuration-app.js';
88
import { PartyDataModel } from './documents/actors/party/party-data-model.mjs';
9+
import { FUToken } from './ui/token.mjs';
910

1011
/**
1112
* @description All system settings
@@ -65,6 +66,9 @@ export const SETTINGS = Object.freeze({
6566
useRevisedStudyRule: 'useRevisedStudyRule',
6667
technospheres: 'useTechnospheres',
6768
pressureSystem: 'pressureSystem',
69+
optionPressureGaugeShow: 'optionPressureGaugeShow',
70+
optionPressureGaugeTheme: 'optionPressureGaugeTheme',
71+
optionPressureGaugePosition: 'optionPressureGaugePosition',
6872
// Document Sheets
6973
sheetOptions: 'sheetOptions',
7074
showAssociatedTherioforms: 'showAssociatedTherioforms',
@@ -192,7 +196,17 @@ export const registerSystemSettings = async function () {
192196
label: game.i18n.localize('FU.OptionalRulesManage'),
193197
hint: game.i18n.localize('FU.OptionalRulesSettingsInstuction'),
194198
icon: 'fas fa-book',
195-
type: createConfigurationApp('FU.OptionalRules', [SETTINGS.optionQuirks, SETTINGS.optionZeroPower, SETTINGS.optionCampingRules, SETTINGS.useRevisedStudyRule, SETTINGS.technospheres, SETTINGS.pressureSystem]),
199+
type: createConfigurationApp('FU.OptionalRules', [
200+
SETTINGS.optionQuirks,
201+
SETTINGS.optionZeroPower,
202+
SETTINGS.optionCampingRules,
203+
SETTINGS.useRevisedStudyRule,
204+
SETTINGS.technospheres,
205+
SETTINGS.pressureSystem,
206+
SETTINGS.optionPressureGaugeShow,
207+
SETTINGS.optionPressureGaugePosition,
208+
SETTINGS.optionPressureGaugeTheme,
209+
]),
196210
restricted: true,
197211
});
198212

@@ -255,6 +269,59 @@ export const registerSystemSettings = async function () {
255269
requiresReload: true,
256270
});
257271

272+
game.settings.register(SYSTEM, SETTINGS.optionPressureGaugeShow, {
273+
name: game.i18n.localize('FU.OptionPressureGaugeShow'),
274+
hint: game.i18n.localize('FU.OptionPressureGaugeShowHint'),
275+
scope: 'world',
276+
config: false,
277+
type: Boolean,
278+
default: true,
279+
requiresReload: false,
280+
onChange() {
281+
canvas?.scene?.tokens.forEach((doc) => {
282+
if (doc.object instanceof FUToken) doc.object.pressureGauge.refresh();
283+
});
284+
},
285+
});
286+
287+
game.settings.register(SYSTEM, SETTINGS.optionPressureGaugePosition, {
288+
name: game.i18n.localize('FU.OptionPressureGaugePosition'),
289+
hint: game.i18n.localize('FU.OptionPressureGaugePositionHint'),
290+
scope: 'world',
291+
config: false,
292+
type: String,
293+
default: 'top',
294+
requiresReload: false,
295+
choices: {
296+
top: game.i18n.localize('FU.OptionPressureGaugePositionTop'),
297+
bottom: game.i18n.localize('FU.OptionPressureGaugePositionBottom'),
298+
},
299+
onChange() {
300+
canvas?.scene?.tokens.forEach((doc) => {
301+
if (doc.object instanceof FUToken) doc.object.pressureGauge.refresh();
302+
});
303+
},
304+
});
305+
306+
game.settings.register(SYSTEM, SETTINGS.optionPressureGaugeTheme, {
307+
name: game.i18n.localize('FU.OptionPressureGaugeTheme'),
308+
hint: game.i18n.localize('FU.OptionPressureGaugeThemeHint'),
309+
scope: 'world',
310+
config: false,
311+
type: String,
312+
requiresReload: false,
313+
default: 'hudTheme',
314+
choices: {
315+
hudTheme: 'FU.OptionPressureGaugeUseHUDTheme',
316+
...Object.fromEntries(Object.entries(FU.pressureGaugeThemes).map(([key, value]) => [key, value.name])),
317+
},
318+
onChange() {
319+
canvas?.scene?.tokens.forEach((doc) => {
320+
if (doc.object instanceof FUToken) doc.object._refreshPressureGauge(true);
321+
});
322+
},
323+
});
324+
258325
// BEHAVIOR ROLLS
259326
game.settings.register(SYSTEM, SETTINGS.optionBehaviorRoll, {
260327
name: game.i18n.localize('FU.BehaviorRollsSettings'),

module/ui/pressureGauges/index.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './pressure-gauge.mjs';
2+
export * from './modern-pressure-gauge.mjs';
3+
export * from './pixel-pressure-gauge.mjs';
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { FUPressureGauge } from './pressure-gauge.mjs';
2+
3+
export class FUModernPressureGauge extends FUPressureGauge {
4+
static get name() {
5+
return 'FU.CombatHudModern';
6+
}
7+
8+
static get combatHudTheme() {
9+
return 'fu-modern';
10+
}
11+
12+
bgColor = '#656565';
13+
borderColor = '#c7c7c7';
14+
barHeight = 12;
15+
16+
clipSize = 0.1;
17+
18+
_createMaskTexture(width, height) {
19+
const canvas = document.createElement('canvas');
20+
canvas.width = width;
21+
canvas.height = height;
22+
23+
const ctx = canvas.getContext('2d');
24+
25+
ctx.beginPath();
26+
ctx.moveTo(width * this.clipSize, 0);
27+
ctx.lineTo(width, 0);
28+
29+
ctx.lineTo(width - width * this.clipSize, height);
30+
ctx.lineTo(0, height);
31+
32+
ctx.closePath();
33+
34+
ctx.fillStyle = 'white';
35+
ctx.fill();
36+
37+
const texture = globalThis.PIXI.Texture.from(canvas);
38+
return texture;
39+
}
40+
41+
_createFGMaskTexture(width, height) {
42+
const canvas = document.createElement('canvas');
43+
canvas.width = width;
44+
canvas.height = height;
45+
const ctx = canvas.getContext('2d');
46+
47+
ctx.beginPath();
48+
ctx.moveTo(0, 0);
49+
ctx.lineTo(width, 0);
50+
ctx.lineTo(width - width * this.clipSize, height);
51+
ctx.lineTo(0, height);
52+
53+
ctx.closePath();
54+
ctx.fillStyle = 'white';
55+
ctx.fill();
56+
57+
return globalThis.PIXI.Texture.from(canvas);
58+
}
59+
60+
_getBorderThickness() {
61+
return canvas.stage.scale.y;
62+
}
63+
_getShadowThickness() {
64+
return 2 * canvas.stage.scale.y;
65+
}
66+
67+
_drawShadow(width, height, color = this.shadowColor) {
68+
const shadow = this._getChildElement('Shadow');
69+
if (!shadow) return;
70+
71+
shadow.clear();
72+
const thickness = this._getShadowThickness();
73+
shadow.lineStyle(thickness, color, 0.25, 0);
74+
shadow.moveTo(0, thickness);
75+
shadow.lineTo(width, thickness);
76+
77+
shadow.moveTo(0, thickness);
78+
shadow.lineTo(0, height);
79+
}
80+
81+
_drawBorder(width, height, color = this.border) {
82+
const border = this._getChildElement('Border');
83+
if (!border) return;
84+
85+
border.clear();
86+
border.lineStyle(this._getBorderThickness(), color, 1, 0);
87+
border.drawRect(0, 0, width, height);
88+
}
89+
90+
async _setFGPosition(suppressAnimation = false) {
91+
const fg = this._getChildElement('FG');
92+
if (!fg?.mask) return;
93+
94+
const { current = 0, max = 0 } = this.clock ?? {};
95+
const perc = current / max;
96+
const x = -(fg.mask.width - fg.mask.width * perc);
97+
98+
if (!this.token.isPreview && !suppressAnimation) {
99+
globalThis.gsap.killTweensOf(fg);
100+
await globalThis.gsap.to(fg.mask, { x, duration: this.animationDuration / 1000 });
101+
} else {
102+
fg.mask.x = x;
103+
}
104+
}
105+
106+
onCanvasScale() {
107+
if (!(this.lastCanvasScale.x === canvas.stage.scale.y && this.lastCanvasScale.y === canvas.stage.scale.y)) {
108+
super.onCanvasScale();
109+
const { width, height } = this._getScaledSize();
110+
const fg = this._getChildElement('FG');
111+
if (fg?.mask) fg.mask.texture = this._createFGMaskTexture(width, height);
112+
113+
if (this.mask) this.mask.texture = this._createMaskTexture(width, height);
114+
}
115+
}
116+
117+
/** @inheritdoc */
118+
_createElements(width = 100, height = this.barHeight) {
119+
const { current = 0, max = 0 } = this.clock ?? {};
120+
const bg = new globalThis.PIXI.Sprite(globalThis.PIXI.Texture.WHITE);
121+
bg.name = `${this.token.id}.Pressure.BG`;
122+
this.addChild(bg);
123+
124+
const fg = new globalThis.PIXI.Sprite(this._createProgressTexture(width, height));
125+
fg.name = `${this.token.id}.Pressure.FG`;
126+
this.addChild(fg);
127+
128+
const fgmask = new globalThis.PIXI.Sprite(this._createFGMaskTexture(width, height, current / max));
129+
fgmask.name = `${this.token.id}.Pressure.FGMask`;
130+
this.addChild(fgmask);
131+
fg.mask = fgmask;
132+
133+
const shadow = new globalThis.PIXI.Graphics();
134+
shadow.name = `${this.token.id}.Pressure.Shadow`;
135+
this.addChild(shadow);
136+
this._drawShadow(width, height, this.shadowColor);
137+
138+
const border = new globalThis.PIXI.Graphics();
139+
border.name = `${this.token.id}.Pressure.Border`;
140+
this.addChild(border);
141+
this._drawBorder(width, height, this.borderColor);
142+
143+
const mask = new globalThis.PIXI.Sprite(this._createMaskTexture(width, height));
144+
mask.name = `${this.token.id}.Pressure.Mask`;
145+
this.addChild(mask);
146+
this.mask = mask;
147+
148+
this._setFGPosition(true);
149+
}
150+
151+
/** @inheritdoc */
152+
refresh(force = false) {
153+
try {
154+
const { width, height } = this._getScaledSize();
155+
156+
if (force) {
157+
this._destroyChildren();
158+
const { width, height } = this._getScaledSize();
159+
this._createElements(width, height);
160+
}
161+
162+
if (!this.shouldDrawPressureGauge) {
163+
this.visible = false;
164+
return;
165+
} else {
166+
this.visible = true;
167+
}
168+
169+
const elems = ['BG', 'FG', 'Border', 'Mask'].map((name) => this._getChildElement(name));
170+
if (elems.some((elem) => !elem)) throw new Error(`Pressure gauge PIXI elements not created for ${this.id}!`);
171+
172+
const [bg, fg, border, mask] = elems;
173+
const { current } = this.clock;
174+
175+
this._drawBorder(width, height, this.borderColor);
176+
this._drawShadow(width, height, this.shadowColor);
177+
178+
bg.tint = this.bgColor;
179+
bg.width = fg.width = border.width;
180+
bg.height = fg.height = border.height;
181+
182+
bg.x = bg.y = fg.y = border.x = border.y = mask.x = mask.y = 0;
183+
184+
// fg.texture = this._createProgressTexture(width, this.barHeight);
185+
this._setFGPosition(current === this.lastValue);
186+
this.lastValue = current;
187+
this._positionGauge();
188+
} catch (err) {
189+
console.error(err);
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)