feat: Add a new Foundry VTT module for tracking and displaying session numbers with GM controls and auto-increment functionality.

This commit is contained in:
CPTN Cosmo 2025-12-21 19:11:07 +01:00
parent ad704b5fa2
commit 44b80af3d0
5 changed files with 320 additions and 0 deletions

26
module.json Normal file
View file

@ -0,0 +1,26 @@
{
"id": "fvtt-session-tracker",
"title": "Session Tracker",
"description": "A lightweight, premium-looking session tracker for Foundry VTT V13.",
"version": "1.0.0",
"authors": [
{
"name": "CPTN Cosmo",
"email": "cptncosmo@gmail.com",
"url": "https://github.com/cptn-cosmo",
"discord": "cptn_cosmo"
}
],
"compatibility": {
"minimum": "13",
"verified": "13"
},
"esmodules": [
"scripts/main.js"
],
"styles": [
"styles/session-tracker.css"
],
"url": "https://github.com/cptn-cosmo/fvtt-session-tracker",
"license": "MIT"
}

90
scripts/main.js Normal file
View file

@ -0,0 +1,90 @@
import { SessionTrackerApp } from "./session-tracker-app.js";
Hooks.once("init", () => {
console.log("Session Tracker | Initializing");
// Register Session Count
game.settings.register("fvtt-session-tracker", "sessionCount", {
name: "Current Session Number",
hint: "The current session number displayed on the tracker.",
scope: "world",
config: true,
type: Number,
default: 1,
onChange: () => {
if (SessionTrackerApp.instance) SessionTrackerApp.instance.render(true);
}
});
// Register Auto-Increment Toggle
game.settings.register("fvtt-session-tracker", "autoIncrement", {
name: "Automate Session Tracking",
hint: "Automatically increment the session count when the selected user logs in.",
scope: "world",
config: true,
type: Boolean,
default: false
});
// Register Trigger User
game.settings.register("fvtt-session-tracker", "triggerUser", {
name: "Trigger User",
hint: "Selecting this user will trigger the session increment when they log in.",
scope: "world",
config: true,
type: String,
default: "",
choices: () => {
const users = game.users.reduce((acc, u) => {
acc[u.id] = u.name;
return acc;
}, {});
return users;
}
});
// Register tracker position (internal)
game.settings.register("fvtt-session-tracker", "position", {
scope: "client",
config: false,
type: Object,
default: { top: 10, left: 120 }
});
// Register visibility toggle for players
game.settings.register("fvtt-session-tracker", "showTracker", {
name: "Show Session Tracker",
hint: "Whether to display the session tracker on your canvas.",
scope: "client",
config: true,
type: Boolean,
default: true,
onChange: (value) => {
if (SessionTrackerApp.instance) {
if (value) SessionTrackerApp.instance.render(true);
else SessionTrackerApp.instance.close();
}
}
});
});
Hooks.once("ready", () => {
// Initialize the app
SessionTrackerApp.initialize();
// Logic for auto-incrementing
if (game.user.isGM) {
Hooks.on("userConnected", (user, connected) => {
if (!connected) return;
const isAuto = game.settings.get("fvtt-session-tracker", "autoIncrement");
const targetUser = game.settings.get("fvtt-session-tracker", "triggerUser");
if (isAuto && user.id === targetUser) {
const current = game.settings.get("fvtt-session-tracker", "sessionCount");
game.settings.set("fvtt-session-tracker", "sessionCount", current + 1);
ui.notifications.info(`Session Tracker | User ${user.name} logged in. Incrementing to session ${current + 1}.`);
}
});
}
});

View file

@ -0,0 +1,106 @@
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export class SessionTrackerApp extends HandlebarsApplicationMixin(ApplicationV2) {
static instance;
constructor(options = {}) {
super(options);
}
static DEFAULT_OPTIONS = {
id: "session-tracker-app",
tag: "aside",
classes: ["session-tracker"],
window: {
frame: false,
positioned: true,
},
position: {
width: "auto",
height: "auto",
},
actions: {
increment: SessionTrackerApp.#onIncrement,
decrement: SessionTrackerApp.#onDecrement,
}
};
static PARTS = {
content: {
template: "modules/fvtt-session-tracker/templates/session-tracker.hbs",
},
};
static initialize() {
this.instance = new SessionTrackerApp();
if (!game.settings.get("fvtt-session-tracker", "showTracker")) return;
const pos = game.settings.get("fvtt-session-tracker", "position");
this.instance.render(true, { position: pos });
}
async _prepareContext(options) {
return {
sessionNumber: game.settings.get("fvtt-session-tracker", "sessionCount"),
isGM: game.user.isGM
};
}
static async #onIncrement(event, target) {
if (!game.user.isGM) return;
const current = game.settings.get("fvtt-session-tracker", "sessionCount");
await game.settings.set("fvtt-session-tracker", "sessionCount", current + 1);
}
static async #onDecrement(event, target) {
if (!game.user.isGM) return;
const current = game.settings.get("fvtt-session-tracker", "sessionCount");
if (current <= 1) return;
await game.settings.set("fvtt-session-tracker", "sessionCount", current - 1);
}
// Drag and drop support for positioning
_onRender(context, options) {
if (!game.user.isGM) return;
const dragHandle = this.element;
let isDragging = false;
let startX, startY, startLeft, startTop;
dragHandle.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Only left click
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = this.element.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
this.element.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const newLeft = startLeft + dx;
const newTop = startTop + dy;
this.element.style.left = `${newLeft}px`;
this.element.style.top = `${newTop}px`;
});
window.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
this.element.style.cursor = 'move';
// Save position
const rect = this.element.getBoundingClientRect();
game.settings.set("fvtt-session-tracker", "position", {
top: rect.top,
left: rect.left
});
});
}
}

View file

@ -0,0 +1,83 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;700&display=swap');
.session-tracker {
position: absolute;
z-index: 100;
font-family: 'Outfit', sans-serif;
pointer-events: all;
user-select: none;
cursor: move;
}
.session-tracker-content {
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 10px 16px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.session-tracker-content:hover {
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.55);
}
.session-label {
text-transform: uppercase;
font-size: 10px;
letter-spacing: 2px;
color: rgba(255, 255, 255, 0.6);
font-weight: 700;
}
.session-number {
font-size: 32px;
font-weight: 700;
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
margin-top: -4px;
}
.session-controls {
display: flex;
gap: 8px;
margin-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 8px;
width: 100%;
justify-content: center;
}
.session-controls button {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
border-radius: 6px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.session-controls button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.session-controls button:active {
transform: translateY(0);
background: rgba(255, 255, 255, 0.3);
}
.session-controls button i {
font-size: 12px;
}

View file

@ -0,0 +1,15 @@
<div class="session-tracker-content">
<div class="session-label">Session</div>
<div class="session-number">{{sessionNumber}}</div>
{{#if isGM}}
<div class="session-controls">
<button type="button" data-action="decrement" title="Decrease Session Number">
<i class="fas fa-minus"></i>
</button>
<button type="button" data-action="increment" title="Increase Session Number">
<i class="fas fa-plus"></i>
</button>
</div>
{{/if}}
</div>