diff --git a/.gitignore b/.gitignore index 7954a4b..6fe2ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,9 @@ assets/backgrounds/**/* assets/portraits/**/* assets/icons/**/* assets/tokens/* -assets/sounds/* +assets/sounds/**/* +!assets/sounds/**/ +!assets/sounds/**/.gitkeep !assets/**/.gitkeep # Keep application branding diff --git a/Build enCounter Linux.sh b/Build enCounter Linux.sh index 67dfae3..1f06eba 100644 --- a/Build enCounter Linux.sh +++ b/Build enCounter Linux.sh @@ -6,7 +6,7 @@ set -euo pipefail cd "$(dirname "$0")" -VERSION="0.1.0-alpha.1" +VERSION="0.2.0-alpha.1" ARCH="$(uname -m)" case "${ARCH}" in amd64) ARCH="x86_64" ;; @@ -75,7 +75,10 @@ for dir in \ icons/combat \ icons/systems \ tokens \ - sounds; do + sounds/music \ + sounds/ambience \ + sounds/sfx \ + sounds/custom; do mkdir -p "${RELEASE_DIR}/assets/${dir}" done diff --git a/Build enCounter Windows.bat b/Build enCounter Windows.bat index 2bc95b2..c988248 100644 --- a/Build enCounter Windows.bat +++ b/Build enCounter Windows.bat @@ -6,9 +6,9 @@ REM See LICENSE for the full license terms. setlocal EnableExtensions cd /d "%~dp0" -title enCounter 0.1.0-alpha.1 Windows Build +title enCounter 0.2.0-alpha.1 Windows Build -set "VERSION=0.1.0-alpha.1" +set "VERSION=0.2.0-alpha.1" set "RELEASE_ROOT=release" set "RELEASE_NAME=enCounter-v%VERSION%-Windows-Portable" set "RELEASE_DIR=%RELEASE_ROOT%\%RELEASE_NAME%" @@ -109,7 +109,10 @@ for %%D in ( "icons\combat" "icons\systems" "tokens" - "sounds" + "sounds\music" + "sounds\ambience" + "sounds\sfx" + "sounds\custom" ) do mkdir "%RELEASE_DIR%\assets\%%~D" >nul 2>nul mkdir "%RELEASE_DIR%\data\backups" >nul 2>nul diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d191c..60de100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,39 +2,225 @@ All notable changes to enCounter will be documented here. +## Unreleased + +No changes recorded yet. + +## 0.2.0-alpha.1 - 2026-09-13 + +### Added + +- Saved Encounter Library workflow with **Save**, **Save As**, **Load**, **Duplicate**, and **Delete** while keeping the loaded encounter as a working copy so game-session changes do not automatically overwrite the reusable Saved Encounter. + +- Encounter phases for **Prepared**, **Scene / Roleplay**, **Combat**, and **Complete** states. + +- Player Display **Scene mode** that shows encounter artwork without initiative or combat information while still allowing visual timers. + +- Separate **Scene Image** and **Combat Background** selections for prepared encounters. + +- Visual Timer system for Scene and Combat encounters. + +- Visual Timer controls for: + - Start + - Pause + - Resume + - Reset + - Stop + - Add 10 seconds + - Subtract 10 seconds + - Add 1 minute + - Subtract 1 minute + +- Three visual timer skins: + - Fantasy Hourglass + - Candle + - Sci-Fi Pixel Depletion + +- Timer display modes for different presentation needs, including overlay, featured, and focus-style presentation. + +- Independent options for showing the timer and numeric countdown on the DM Console and Player Display. + +- Synchronized Player Display timer state so the DM and player-facing displays remain aligned during a running countdown. + +- Encounter Soundscape system with: + - Scene music + - Layered scene ambience + - Combat music + - Combat ambience + - Master volume + - Fade transitions + - Scene-to-combat audio switching + - Optional return to scene audio after combat + +- Per-ambience combat transition behavior, allowing scene ambience to continue, lower in volume, or stop when combat begins. + +- Audio asset folders and import support for: + - MP3 + - OGG + - WAV + - FLAC + +- Prepared one-shot audio cues that can be configured as part of an encounter. + +- Live **Prepared Cue** buttons on the Encounter tab, populated automatically from the loaded encounter's Soundscape and usable during both Scene and Combat phases. + +- Player Display initiative auto-centering. The active visible combatant is automatically brought into view and centered as initiative advances. + +- DM initiative tracking and Auto-Follow. + +- The DM Console now follows the current initiative order and clearly identifies the combatant whose turn is currently active. + +- The DM Console automatically brings the active combatant into view when: + - Combat starts. + - **Next Turn** is selected. + - **Previous Turn** is selected. + - **Set Turn** is used. + - A delayed or readied combatant resumes and takes the current turn. + +- Current-turn highlighting on the DM Console remains synchronized with the Player Display so both screens reflect the same active combatant. + +### Changed + +- Player Display presentation is now modeled as **Stand By**, **Scene**, or **Combat** instead of only visible or hidden. + +- **Start Combat** can automatically transition scene audio to combat audio when configured. + +- **End Combat** can automatically restore the previous scene Soundscape when configured. + +- Encounter data normalization now preserves Soundscape, display, phase, scene/combat image, and Saved Encounter metadata. + +- Soundscape configuration is now encounter-bound rather than maintained as a separate Soundscape Library. + +- Loading a Saved Encounter restores its Soundscape configuration and Prepared Cue assignments. + +- Loading a Saved Encounter does **not** automatically begin audio playback. Scene or Combat audio remains under DM control. + +- Soundscape editor wording now makes clear that Soundscapes are stored with and loaded automatically from Saved Encounters. + +- Encounter remains the primary live game-control screen and now includes Saved Encounter controls, Scene/Combat phase controls, scene/combat images, audio controls, and Prepared Cue buttons. + +- Timer and Soundscape configuration are available as dedicated DM Console navigation sections. + +- Improved synchronization between Scene, Combat, timer, Soundscape, initiative, and Player Display state. + +- Initiative order on both the DM Console and Player Display now tracks the same current combatant as turns advance. + +- Player Display initiative cards automatically reposition so the current combatant remains visible and centered rather than moving off-screen during longer initiative orders. + +- DM initiative tracking now moves through the encounter order alongside the Player Display, making it easier for the DM to see: + - Who is currently acting. + - Where the current combatant is within the initiative order. + - Which combatant will act as initiative continues. + +- DM Auto-Follow occurs only when the active turn changes. Ordinary combatant edits do not force the DM screen back to the active combatant. + +- HP changes, damage/healing, status edits, Condition Track changes, resource changes, Hide/Reveal actions, and other non-turn edits leave the DM's current scroll position unchanged. + +- Improved initiative usability for larger encounters where the full combatant list cannot fit on the screen at once. + +### Notes + +- This is an **Alpha / pre-release** version of enCounter. + +- The Timer, Soundscape, Saved Encounter, Scene/Combat presentation, and initiative-follow systems remain Alpha features and may continue to evolve. + +- Player Display initiative centering and DM Auto-Follow use the same active encounter turn state, keeping DM and player-facing initiative presentation aligned. + +- DM Auto-Follow is intentionally triggered by turn changes rather than every encounter render so the DM can edit another combatant without the screen unexpectedly jumping away. + +- Audio configuration and media references are saved with Saved Encounters, but active playback state is not treated as permanent encounter state. + +- Imported audio files remain local assets and are not embedded directly into JSON backup/export files. + +- Export important Library and encounter data before upgrading between Alpha versions. + +## 0.1.0-alpha.2 - 2026-09-05 + +### Maintenance + +- Published the second enCounter Alpha release and updated release-facing documentation for `0.1.0-alpha.2`. + +- Standardized documentation for the single-button Player Display workflow: + - **Start Player Display** + - **Hide Player Display** + - **Show Player Display** + +- Clarified the **STAND BY** privacy-screen workflow for making private DM changes without exposing them to players. + +- Clarified that the Player Display and DM Console must use the same browser profile for local synchronization. + +- Expanded Player Display synchronization and troubleshooting documentation, including DM Sync connected/disconnected behavior. + +- Updated Alpha documentation while retaining the existing initiative, hidden-combatant, backup, Library, and portable-build functionality established during the initial Alpha hardening cycle. + +### Notes + +- This release remained part of the original `0.1.0` Alpha development line. + +- The subsequent Saved Encounter, Scene/Combat presentation, Timer, Soundscape, and enhanced initiative-follow work was developed after this Alpha milestone and is included in `0.2.0-alpha.1`. + ## 0.1.0-alpha.1 - 2026-08-20 ### Added + - Initial enCounter Alpha release baseline. + - DM encounter console and separate Player Display. + - Reusable Library for players, NPCs, enemies, and creatures. + - Generic, SWSE, 5E, and custom system profiles. + - Local asset handling, backup/export/import, and recovery features. + - Clean Windows portable build workflow using PyInstaller. + - Clean Linux x86_64 portable build workflow using PyInstaller. + - AI Assistance Disclosure for development transparency. ### Distribution + - Standardized Alpha version identifiers. + - Isolated Alpha IndexedDB name (`enCounterAlphaDB`). + - Build scripts exclude prior user data and personal campaign assets. + - Licensed the project under the MIT License. + - Added Windows/Linux release SHA-256 checksum generation. + - Hardened GitHub Actions permissions and cross-platform Python invocation. ### Pre-publication hardening + - Restricted the local HTTP server to required public application resources instead of serving the entire project root. + - Added local Host/Origin checks, response security headers, and image signature validation for uploads. + - Added graceful application shutdown from the DM Console. + - Added BroadcastChannel fallback synchronization for browsers where BroadcastChannel is unavailable. + - Added stricter backup/import validation and atomic full-database imports. + - Fixed single Library-entry imports and made **Export Everything** include recovery snapshots. + - Fixed Settings module changes so the encounter UI rerenders immediately. + - Normalized imported settings, encounters, combatants, and local asset references before use. + - Removed duplicate encounter broadcasts from UI-only renders; encounter changes now broadcast from save operations. + - Added Player Display DM Sync connected/disconnected status and DM-presence heartbeats. + - Added a documented single-DM-window workflow warning for the current Alpha. + - Applied the same local Origin policy to GET/HEAD requests as POST requests when an Origin header is present. + - Added dependency-free Node unit tests for normalization, initiative sorting, asset sanitization, and turn-transition behavior. + - Changed hidden combatants so they remain fully visible to the DM but are completely omitted from the Player Display until **Reveal to Players** is clicked. + - Added Player Display visibility tests for hidden combatants and next-turn selection. diff --git a/README.md b/README.md index 353e93d..7a0fb50 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # enCounter -**enCounter** is a free, open-source, local-first TTRPG encounter and initiative manager with separate DM and Player Displays, reusable player/NPC/enemy/creature libraries, encounter controls, asset support, and local backup/export features. +**enCounter** is a free, open-source, local-first TTRPG encounter, initiative, scene-presentation, timer, and soundscape manager with separate DM and Player Displays, reusable player/NPC/enemy/creature libraries, Saved Encounters, local media assets, and backup/export features. -**Current release:** `0.1.0-alpha.1` +**Current release:** `0.2.0-alpha.1` **Status:** Alpha / pre-release **License:** MIT **Platforms:** Windows and Linux @@ -24,28 +24,97 @@ - **Start Player Display** - **Hide Player Display** - **Show Player Display** +- Player Display presentation modes: + - **STAND BY** + - **Scene** + - **Combat** - Private DM editing while the Player Display shows **STAND BY** -- Initiative and turn management +- Initiative and turn management with synchronized DM and Player Display tracking +- **DM Initiative Auto-Follow** + - follows the active combatant as turns advance + - clearly highlights who is currently acting + - automatically brings the current combatant into view + - does not interrupt unrelated HP, status, resource, or combatant edits +- **Player Display initiative auto-centering** + - automatically centers the active visible combatant + - keeps the current turn visible during larger encounters +- Automatic round advancement +- Delay, Ready, Trigger / Resume, and Set Turn controls - HP, status, condition, and combatant controls - Hidden enemies with **Hide from Players / Reveal to Players** - Reusable Player, NPC, Enemy, and Creature Library -- Portrait and encounter background support +- Saved Encounter Library with: + - Save + - Save As + - Load + - Duplicate + - Delete + - working-copy loading for game-session changes +- Encounter phases for: + - Prepared + - Scene / Roleplay + - Combat + - Complete +- Separate **Scene Image** and **Combat Background** per encounter +- Scene / roleplay Player Display mode with full-screen encounter artwork +- Visual countdown timers for Scene and Combat +- Timer controls for Start, Pause, Resume, Reset, Stop, and time adjustments +- Visual timer skins including: + - Fantasy Hourglass + - Candle + - Sci-Fi Pixel Depletion +- Optional Player Display numeric countdown +- Encounter Soundscapes with: + - scene music + - layered scene ambience + - combat music + - combat ambience + - master volume + - fade transitions + - automatic Scene-to-Combat switching + - optional return to Scene audio after combat +- Prepared one-shot audio cues +- Live Prepared Cue buttons directly on the Encounter screen +- Local audio import support for MP3, OGG, WAV, and FLAC +- Portrait and encounter-background support - Generic TTRPG, SWSE, D&D 5E, and Custom system profiles - Local autosave and recovery snapshots - Backup, export, and import tools - Windows and Linux portable builds - Runs locally without requiring a cloud account +## What's new in 0.2.0-alpha.1 + +Version `0.2.0-alpha.1` is a major Alpha feature milestone focused on encounter presentation and live game management. + +Highlights include: + +- Visual Scene and Combat timers +- Fantasy Hourglass, Candle, and Sci-Fi Pixel timer presentations +- Encounter-bound Soundscapes +- Scene and Combat music and ambience +- Prepared one-shot audio cues +- Live cue buttons on the Encounter screen +- Saved Encounter audio and presentation configuration +- Stand By, Scene, and Combat Player Display modes +- Separate Scene and Combat artwork +- Player Display initiative auto-centering +- DM initiative Auto-Follow and current-turn tracking + ## Screenshots ### DM Console +The DM Console manages Saved Encounters, initiative, combatants, timers, scene/combat presentation, Soundscapes, and Prepared Cues. +     -### Player Display +### Player Display — Scene + +Scene mode displays encounter artwork and optional visual timers without revealing initiative or combat information.   @@ -54,6 +123,14 @@  +### Encounter Soundscape live cues + +Soundscapes are stored as part of Saved Encounters. Loading a Saved Encounter restores its Scene music, Scene ambience, Combat music, Combat ambience, transition settings, and Prepared Cue assignments. + +Configured one-shot cues appear as live buttons on the Encounter screen and can be triggered during either Scene or Combat play. + +Audio playback itself does not automatically begin when an encounter is loaded. The DM remains in control of when Scene or Combat audio starts. + ## Alpha notice This is pre-release software. Features, storage structures, and behavior may change before version 1.0. @@ -148,11 +225,19 @@ The Player Display should normally be opened from the DM Console using the same The Player Display button changes automatically depending on its current state: -| Button | Meaning | -| --- | --- | -| **Start Player Display** | Opens the Player Display | -| **Hide Player Display** | Replaces encounter information with a STAND BY screen | -| **Show Player Display** | Restores the Player Display using the latest encounter state | +| Button | Meaning | +| ------------------------ | ------------------------------------------------------------ | +| **Start Player Display** | Opens the Player Display | +| **Hide Player Display** | Replaces encounter information with a STAND BY screen | +| **Show Player Display** | Restores the Player Display using the latest encounter state | + +Player Display content is also controlled by the current encounter phase: + +| Encounter state | Player Display | +| --------------- | ---------------------------------------------------------------------- | +| **Stand By** | Privacy screen; encounter information is hidden | +| **Scene** | Scene artwork and optional timers without initiative | +| **Combat** | Combat artwork, initiative order, current turn, and combat information | ### Making private DM changes @@ -163,6 +248,8 @@ The DM Console remains fully functional. The DM can privately: - change initiative values - add or remove combatants - advance or correct turns +- use Set Turn or Trigger / Resume +- adjust initiative order - change HP or status information - add reinforcements - hide or reveal enemies @@ -180,6 +267,24 @@ When a combatant is marked **Hide from Players**, it remains fully visible in th Use **Reveal to Players** to return that combatant to the Player Display at its normal initiative position. +### Initiative tracking + +The DM Console and Player Display use the same active-turn state so both screens remain synchronized as initiative advances. + +On the **Player Display**, the current visible combatant is highlighted and automatically centered in the initiative track. + +On the **DM Console**, the current combatant is highlighted and the initiative list automatically follows that combatant when: + +- Combat starts +- **Next Turn** is selected +- **Previous Turn** is selected +- **Set Turn** is used +- A delayed or readied combatant resumes and takes the current turn + +DM Auto-Follow occurs only when the active turn changes. + +Editing HP, applying damage or healing, changing a status or condition, adjusting resources, hiding or revealing combatants, or making other non-turn changes does not force the DM screen back to the current combatant. + ### Display synchronization The Player Display includes a **DM Sync** connection indicator. @@ -190,7 +295,7 @@ Opening multiple DM Console windows against the same browser database is not sup ## Automated core tests -The repository includes dependency-free Node tests for normalization, initiative sorting, asset sanitization, and turn-transition behavior. Node.js is only needed to run these development tests; packaged enCounter users do not need Node.js. +The repository includes dependency-free Node tests for normalization, initiative sorting, asset sanitization, Player Display visibility, and turn-transition behavior. Node.js is only needed to run these development tests; packaged enCounter users do not need Node.js. Run them from the repository root with: @@ -205,6 +310,8 @@ node --test tests/*.test.js - Exports/imports and user-supplied assets remain local unless the user moves or shares them. - The current Alpha binds its local web server to `127.0.0.1` and does not intentionally send encounter or Library data to an enCounter-operated cloud service. - The local server exposes only the application files, public notices, and supported asset files needed by enCounter; runtime `data/` files and source/build files are not served through the browser. +- Imported audio files remain local under the enCounter `assets/sounds/` folders. +- Soundscape configuration stores references to those local audio assets; audio files are not embedded directly into JSON encounter backups or exports. See [`PRIVACY.md`](PRIVACY.md). diff --git a/Start enCounter.bat b/Start enCounter.bat index 1b5caf8..0e764a7 100644 --- a/Start enCounter.bat +++ b/Start enCounter.bat @@ -6,7 +6,7 @@ REM This file is part of enCounter. See LICENSE for the full license terms. setlocal EnableDelayedExpansion cd /d "%~dp0" -title enCounter 0.1.0-alpha.1 +title enCounter 0.2.0-alpha.1 REM ============================================================ REM Verify launcher exists diff --git a/assets/sounds/ambience/.gitkeep b/assets/sounds/ambience/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/sounds/custom/.gitkeep b/assets/sounds/custom/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/sounds/music/.gitkeep b/assets/sounds/music/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/sounds/sfx/.gitkeep b/assets/sounds/sfx/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/AUDIO_AND_ENCOUNTER_WORKFLOW.md b/docs/AUDIO_AND_ENCOUNTER_WORKFLOW.md new file mode 100644 index 0000000..f596d39 --- /dev/null +++ b/docs/AUDIO_AND_ENCOUNTER_WORKFLOW.md @@ -0,0 +1,175 @@ +# enCounter — Saved Encounters, Scene Display, Soundscapes, and Initiative Tracking + +This document describes the encounter presentation and live-session workflow for **enCounter v0.2.0-alpha.1**. + +A prepared encounter can move through Scene and Combat presentation while retaining its Saved Encounter configuration, visual timers, Soundscape, and initiative state. + +## Encounter workflow + +1. Build an encounter in the DM Console: name, system, participants, Scene Image, Combat Background, Soundscape, and timer settings. + +2. Use **Save** or **Save As…** in the Saved Encounter bar to store the prepared encounter. + +3. Load a Saved Encounter later. enCounter creates an active working copy; game-night changes do not overwrite the saved encounter unless the DM explicitly presses **Save**. + +4. Use **Show Scene** to display only the encounter's Scene Image on the Player Display. Initiative, combatants, round information, current turn, and next turn remain hidden. + +5. Use **Scene Audio** to start the roleplay/exploration Soundscape independently of Player Display visibility. + +6. Use **Start Combat** to: + - Change the encounter to Combat phase. + - Switch the Player Display to Combat mode. + - Establish the current combatant from the initiative order. + - Bring the current combatant into view on the DM Console. + - Center the current visible combatant on the Player Display. + - Transition the Soundscape to Combat audio when automatic Combat audio switching is enabled. + +7. During combat, use **Next Turn**, **Previous Turn**, **Set Turn**, and **Trigger / Resume** to manage the active combatant. The DM Console and Player Display use the same current-turn state and remain synchronized as initiative changes. + +8. Use **End Combat** to return the Player Display to Scene mode. If Scene audio was playing before combat and return-to-scene audio is enabled, the Scene Soundscape resumes automatically. + +9. Use **Hide Player Display** at any time to return the players to STAND BY without stopping DM-side audio. + +## Player Display modes + +- **Stand By** — enCounter logo / STAND BY only. Encounter information and Player-visible timers are hidden. +- **Scene** — full-screen Scene Image. No initiative or combat UI is displayed. Player-visible timers may still appear. +- **Combat** — initiative and combat information is displayed using the Combat Background. Player-visible timers may still appear. + +### Combat initiative presentation + +During Combat mode, the Player Display follows the active initiative state maintained by the DM Console. + +The current visible combatant is highlighted and automatically centered within the initiative display as turns advance. This keeps the active character or creature visible during encounters with initiative orders that are wider than the available Player Display area. + +Hidden combatants remain omitted from the Player Display until the DM uses **Reveal to Players**. + +## Initiative Tracking and DM Auto-Follow + +The DM Console and Player Display share the same active-turn state. When initiative changes, both displays identify the same current combatant. + +### DM Console behavior + +The current combatant is highlighted on the DM Console. + +The DM Console automatically follows and brings the active combatant into view when: + +- **Start Combat** establishes the first current combatant. +- **Next Turn** advances initiative. +- **Previous Turn** moves backward through initiative. +- **Set Turn** manually selects a combatant as the current turn. +- **Trigger / Resume** causes a Delayed or Readied combatant to resume and take the current turn. + +This makes it easier for the DM to follow the initiative order during encounters containing more combatants than can fit on the screen at once. + +### Player Display behavior + +When the active turn changes, the Player Display: + +- Updates the current-turn highlight. +- Keeps the visible initiative order synchronized with the DM Console. +- Automatically centers the current visible combatant. +- Continues to omit combatants that are hidden from players. + +### Auto-Follow only occurs on turn changes + +DM Auto-Follow is intentionally limited to actions that change the active turn. + +The DM Console does **not** automatically move back to the current combatant when the DM performs ordinary encounter edits such as: + +- Damage or healing. +- Direct HP changes. +- Status changes. +- Condition Track changes. +- Resource adjustments. +- Notes or other combatant edits. +- Hide or Reveal actions. +- Restoring a combatant without making that combatant the current turn. + +This allows the DM to work on another combatant's card without the screen unexpectedly jumping back to the active character. + +## Soundscape + +The Soundscape belongs to the loaded encounter and its configuration is saved with the Saved Encounter. + +### Scene audio + +- Optional Scene music. +- Up to three ambience layers in the current UI. +- Each Scene ambience layer can **Stop**, **Continue**, or **Lower Volume** when Combat begins. + +### Combat audio + +- Optional Combat music. +- Up to two additional Combat ambience layers in the current UI. +- Combat audio can begin automatically when **Start Combat** is selected. +- Scene audio can optionally return when Combat ends. + +### Prepared cues + +Three one-shot cue slots are available in the current Soundscape editor for alarms, explosions, doors, horns, creature sounds, transmissions, weapon effects, and similar events. + +After the Soundscape is applied to the encounter, any configured cue appears automatically as a live **Prepared Cues** button on the Encounter tab. + +These live cue buttons remain available during both Scene and Combat phases so the DM does not need to leave the Encounter screen during play. + +### Saving and loading Soundscapes + +Soundscapes are not stored or loaded separately. They are part of the Saved Encounter. + +Loading a Saved Encounter restores its: + +- Scene music configuration. +- Scene ambience configuration. +- Combat music configuration. +- Combat ambience configuration. +- Prepared Cue assignments. +- Volume settings. +- Fade settings. +- Scene-to-Combat transition behavior. +- Return-to-Scene behavior. + +Loading an encounter does **not** automatically start audio playback. + +The DM chooses when to start Scene Audio, Combat Audio, or a Prepared Cue. + +### Audio library + +Supported import formats: + +- MP3 +- OGG +- WAV +- FLAC + +Folders: + +```text +assets/sounds/music/ +assets/sounds/ambience/ +assets/sounds/sfx/ +assets/sounds/custom/ +``` + +Audio plays from the DM computer. The Player Display remains visual-only, avoiding duplicate playback and browser audio synchronization issues. + +Saved Encounters store references to local audio assets rather than embedding the audio files themselves. When moving enCounter to another computer, copy the complete `assets/` folder if Saved Encounters depend on imported audio or images. + +## Timers outside combat + +Player-visible timers are independent of initiative. + +A timer may be shown while the Player Display is in Scene mode, making it suitable for: + +- Puzzles. +- Arrivals. +- Evacuations. +- Security responses. +- Environmental hazards. +- Reinforcements. +- Countdown events. +- Other roleplaying sequences. + +Player-visible timers can also remain active during Combat mode. + +While the Player Display is on **STAND BY**, Player-visible timers remain hidden. diff --git a/docs/LEGAL.html b/docs/LEGAL.html index 2cbf63f..4d91652 100644 --- a/docs/LEGAL.html +++ b/docs/LEGAL.html @@ -13,7 +13,7 @@
Version: 0.1.0-alpha.1 (Alpha)
+Version: 0.2.0-alpha.1 (Alpha)
Copyright © 2026 Zygons.
diff --git a/docs/VERSION.txt b/docs/VERSION.txt index 9fc67c8..b523c76 100644 --- a/docs/VERSION.txt +++ b/docs/VERSION.txt @@ -1,5 +1,5 @@ enCounter -Version: 0.1.0-alpha.1 +Version: 0.2.0-alpha.1 Release stage: Alpha License: MIT -Platforms: Windows portable, Linux portable (architecture-specific) +Platforms: Windows portable, Linux portable (architecture-specific) \ No newline at end of file diff --git a/docs/enCounter User Guide.pdf b/docs/enCounter User Guide.pdf index 6a84ade..a6bb2a8 100644 Binary files a/docs/enCounter User Guide.pdf and b/docs/enCounter User Guide.pdf differ diff --git a/programs/app/css/base.css b/programs/app/css/base.css index 9af7077..f70ff67 100644 --- a/programs/app/css/base.css +++ b/programs/app/css/base.css @@ -680,3 +680,163 @@ hr { color: var(--text); text-decoration: underline; } + +/* ======================================== + SAVED ENCOUNTERS / LIVE ENCOUNTER CONTROL +======================================== */ + +.encounter-library-bar, +.encounter-live-bar { + display: flex; + align-items: center; + gap: 9px; + flex-wrap: wrap; + padding: 12px; + margin: 12px 0; + border: 1px solid var(--line); + border-radius: 9px; + background: rgba(7, 20, 28, 0.72); +} + +.encounter-library-bar > select { + width: min(380px, 100%); +} + +.encounter-phase-block { + min-width: 112px; + padding: 7px 10px; + border: 1px solid #345567; + border-radius: 7px; + background: #08141c; +} + +.encounter-phase-block strong { + display: block; + margin-top: 3px; + color: #d7eef7; + letter-spacing: 0.08em; +} + +.soundscape-status { + display: inline-block; + padding: 5px 9px; + border: 1px solid #3e6173; + border-radius: 999px; + color: #b9d3dd; + background: #0a1820; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.8px; +} + +.encounter-cue-panel { + display: grid; + gap: 10px; + padding: 12px; + margin: 12px 0; + border: 1px solid var(--line); + border-radius: 9px; + background: rgba(7, 20, 28, 0.72); +} + +.encounter-cue-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} + +.encounter-cue-heading strong { + display: block; + margin-top: 2px; + color: #d7eef7; + font-size: 14px; +} + +.encounter-cue-buttons { + display: flex; + gap: 9px; + flex-wrap: wrap; + min-height: 34px; + align-items: center; +} + +.encounter-cue-button { + min-width: 124px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; +} + +.encounter-cue-button::before { + content: "▶"; + font-size: 10px; +} + +.encounter-cue-button.is-playing { + border-color: #6fc6e8; + box-shadow: 0 0 0 1px rgba(111, 198, 232, 0.24); +} + +/* ======================================== + SOUNDSCAPE EDITOR +======================================== */ + +.soundscape-settings-grid { + display: grid; + grid-template-columns: minmax(180px, 1fr) 150px minmax(240px, 1fr) minmax(240px, 1fr); + gap: 12px; + align-items: end; +} + +.soundscape-columns { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 16px; +} + +.audio-track-row { + display: grid; + grid-template-columns: minmax(220px, 1fr) 105px; + gap: 10px; + align-items: end; + padding: 10px 0; + border-bottom: 1px solid rgba(55, 83, 99, 0.45); +} + +.audio-track-row.audio-track-wide { + grid-template-columns: minmax(210px, 1fr) 90px 145px 90px; +} + +.cue-grid { + display: grid; + gap: 8px; +} + +.cue-row { + display: grid; + grid-template-columns: minmax(130px, 0.5fr) minmax(260px, 1.5fr) 90px auto; + gap: 9px; + align-items: center; +} + +.asset-card audio { + width: 100%; + min-width: 180px; +} + +@media (max-width: 1000px) { + .soundscape-columns, + .soundscape-settings-grid { + grid-template-columns: 1fr; + } + + .audio-track-row, + .audio-track-row.audio-track-wide, + .cue-row { + grid-template-columns: 1fr; + } +} diff --git a/programs/app/css/display.css b/programs/app/css/display.css index a4fa3fc..bb7452f 100644 --- a/programs/app/css/display.css +++ b/programs/app/css/display.css @@ -479,3 +479,28 @@ color: #d7e7ee; text-decoration: underline; } + +/* ======================================== + SCENE / ROLEPLAY DISPLAY MODE +======================================== */ + +.player-body[data-display-mode="scene"] .player-shell { + display: none; +} + +.player-body[data-display-mode="scene"] .display-shade { + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.10), + rgba(0, 0, 0, 0.03) 55%, + rgba(0, 0, 0, 0.20) + ); +} + +.player-body[data-display-mode="scene"] .display-background { + transform: none; +} + +.player-body[data-display-mode="combat"] .player-shell { + display: flex; +} diff --git a/programs/app/css/timer.css b/programs/app/css/timer.css new file mode 100644 index 0000000..ca08a51 --- /dev/null +++ b/programs/app/css/timer.css @@ -0,0 +1,1717 @@ +/* + * Copyright (C) 2026 Zygons + * SPDX-License-Identifier: MIT + * This file is part of enCounter. See LICENSE for the full license terms. + */ + +/* ============================================================ + enCounter Visual Timer System + ============================================================ + + Timer state is controlled by JavaScript. + + Expected CSS custom properties: + + --timer-remaining + 1.0 = full time remaining + 0.0 = complete + + --timer-elapsed + 0.0 = just started + 1.0 = complete + + JavaScript is responsible for accurate timing. + CSS is responsible only for presentation and decorative + animation. + + ============================================================ */ + +/* ============================================================ + SHARED TIMER VARIABLES + ============================================================ */ + +:root { + --timer-remaining: 1; + --timer-elapsed: 0; + + --timer-width: 260px; + --timer-height: 320px; + + --timer-text: #d7e7ee; + --timer-muted: #7895a3; + + --timer-panel-bg: rgba(3, 10, 14, 0.94); + --timer-panel-border: rgba(83, 151, 180, 0.45); + + --timer-warning: #d7a447; + --timer-critical: #c85259; +} + +/* ============================================================ + SHARED TIMER CONTAINER + ============================================================ */ + +.timer-display { + position: relative; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: var(--timer-width); + min-width: 180px; + max-width: 100%; + + min-height: var(--timer-height); + + color: var(--timer-text); + + user-select: none; + pointer-events: none; +} + +/* ============================================================ + TIMER VISIBILITY + ============================================================ */ + +.timer-display.timer-hidden { + display: none; +} + +.timer-display.timer-visible { + display: flex; +} + +/* ============================================================ + TIMER LABEL + ============================================================ */ + +.timer-label { + position: relative; + z-index: 20; + + max-width: 100%; + + margin-bottom: 14px; + + color: var(--timer-text); + + font-size: clamp(14px, 1.5vw, 22px); + font-weight: 800; + + letter-spacing: 0.08em; + text-align: center; + text-transform: uppercase; + + text-shadow: + 0 2px 5px rgba(0, 0, 0, 0.9), + 0 0 12px rgba(0, 0, 0, 0.5); + + overflow-wrap: anywhere; +} + +/* ============================================================ + OPTIONAL NUMERIC TIME + ============================================================ */ + +.timer-numeric { + position: relative; + z-index: 20; + + margin-top: 12px; + + color: var(--timer-text); + + font-size: clamp(22px, 3vw, 42px); + font-weight: 900; + + font-variant-numeric: tabular-nums; + + letter-spacing: 0.08em; + text-align: center; + + text-shadow: + 0 2px 4px rgba(0, 0, 0, 0.95), + 0 0 14px rgba(60, 160, 200, 0.28); +} + +.timer-numeric.timer-numeric-hidden { + display: none; +} + +/* ============================================================ + TIMER STATUS STATES + ============================================================ */ + +.timer-display.timer-paused { + opacity: 0.78; +} + +.timer-display.timer-paused::after { + content: "PAUSED"; + + position: absolute; + z-index: 100; + + left: 50%; + bottom: 8px; + + transform: translateX(-50%); + + padding: 4px 9px; + + border: 1px solid rgba(170, 190, 200, 0.45); + border-radius: 999px; + + color: #c1d0d7; + background: rgba(0, 0, 0, 0.7); + + font-size: 9px; + font-weight: 900; + + letter-spacing: 0.16em; +} + +.timer-display.timer-complete { + --timer-remaining: 0; + --timer-elapsed: 1; +} + +/* ============================================================ + TIMER WARNING STATES + ============================================================ */ + +.timer-display.timer-warning .timer-label, +.timer-display.timer-warning .timer-numeric { + color: #f0d193; +} + +.timer-display.timer-critical .timer-label, +.timer-display.timer-critical .timer-numeric { + color: #ffd1d4; +} + +.timer-display.timer-critical { + animation: timer-critical-pulse 0.75s ease-in-out infinite alternate; +} + +@keyframes timer-critical-pulse { + from { + filter: brightness(1); + } + + to { + filter: brightness(1.2); + } +} + +/* ============================================================ + PLAYER DISPLAY TIMER OVERLAY + ============================================================ */ + +.player-timer-overlay { + position: absolute; + z-index: 50; + + top: 90px; + left: 50%; + + transform: translateX(-50%); + + display: flex; + align-items: center; + justify-content: center; + + max-width: calc(100% - 40px); + + pointer-events: none; +} + +.player-timer-overlay.timer-overlay-hidden { + display: none; +} + +/* ============================================================ + FEATURED TIMER MODE + ============================================================ */ + +.player-timer-overlay.timer-mode-featured { + top: 105px; + + padding: 18px 30px; + + border: 1px solid rgba(105, 165, 190, 0.35); + border-radius: 18px; + + background: rgba(2, 8, 12, 0.72); + + backdrop-filter: blur(7px); +} + +/* ============================================================ + FOCUS TIMER MODE + ============================================================ */ + +.player-timer-focus { + position: absolute; + inset: 0; + + z-index: 60; + + display: none; + + align-items: center; + justify-content: center; + + background: radial-gradient( + circle at center, + rgba(8, 21, 28, 0.86), + rgba(1, 4, 6, 0.98) + ); + + pointer-events: none; +} + +.player-timer-focus.show { + display: flex; +} + +.player-timer-focus .timer-display { + --timer-width: min(60vw, 520px); + --timer-height: min(70vh, 620px); +} + +/* ============================================================ + FANTASY HOURGLASS + ============================================================ */ + +.timer-hourglass { + position: relative; + + width: 200px; + height: 250px; + + display: flex; + align-items: center; + justify-content: center; + + filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.58)) + drop-shadow(0 0 10px rgba(184, 133, 55, 0.15)); +} + +/* ------------------------------------------------------------ + Wooden side supports + ------------------------------------------------------------ */ + +.timer-hourglass::before, +.timer-hourglass::after { + content: ""; + + position: absolute; + + top: 29px; + bottom: 29px; + + width: 13px; + + z-index: 10; + + border-radius: 7px; + + background: linear-gradient( + to right, + #392313 0%, + #6c4825 28%, + #a2753e 52%, + #69451f 76%, + #392313 100% + ); + + box-shadow: + inset 2px 0 2px rgba(255, 224, 159, 0.18), + inset -3px 0 3px rgba(0, 0, 0, 0.5), + 0 3px 5px rgba(0, 0, 0, 0.5); +} + +.timer-hourglass::before { + left: 21px; + transform: rotate(2deg); +} + +.timer-hourglass::after { + right: 21px; + transform: rotate(-2deg); +} + +/* ------------------------------------------------------------ + Top and bottom wooden rails + ------------------------------------------------------------ */ + +.timer-hourglass-frame { + position: absolute; + + inset: 0; + + z-index: 12; + + pointer-events: none; +} + +.timer-hourglass-frame::before, +.timer-hourglass-frame::after { + content: ""; + + position: absolute; + + left: 10px; + + width: 180px; + height: 20px; + + border-radius: 10px; + + background: linear-gradient( + to bottom, + #a77c43 0%, + #795027 28%, + #503017 70%, + #2e1a0d 100% + ); + + border: 1px solid rgba(198, 151, 82, 0.28); + + box-shadow: + inset 0 3px 2px rgba(255, 222, 156, 0.15), + inset 0 -4px 4px rgba(0, 0, 0, 0.48), + 0 5px 8px rgba(0, 0, 0, 0.52); +} + +.timer-hourglass-frame::before { + top: 12px; +} + +.timer-hourglass-frame::after { + bottom: 12px; +} + +/* ------------------------------------------------------------ + Glass body + ------------------------------------------------------------ */ + +.timer-hourglass-glass { + position: absolute; + + left: 44px; + top: 34px; + + width: 112px; + height: 182px; + + z-index: 5; + + overflow: hidden; + + /* + * Single continuous hourglass silhouette. + * + * Unlike the previous polygon, this shape does not cross over + * itself in the center. + */ + clip-path: polygon( + 8% 0%, + 92% 0%, + + 80% 24%, + 64% 42%, + 54% 49%, + + 54% 51%, + 64% 58%, + 80% 76%, + + 92% 100%, + 8% 100%, + + 20% 76%, + 36% 58%, + 46% 51%, + + 46% 49%, + 36% 42%, + 20% 24% + ); + + background: linear-gradient( + to right, + rgba(126, 183, 194, 0.08) 0%, + rgba(220, 245, 247, 0.21) 18%, + rgba(156, 210, 218, 0.08) 38%, + rgba(238, 251, 252, 0.17) 53%, + rgba(118, 181, 192, 0.09) 76%, + rgba(215, 241, 244, 0.16) 100% + ); + + box-shadow: + inset 0 0 9px rgba(213, 244, 247, 0.24), + inset 0 0 18px rgba(79, 130, 140, 0.12); +} + +/* ------------------------------------------------------------ + Glass highlight + ------------------------------------------------------------ */ + +.timer-hourglass-glass::after { + content: ""; + + position: absolute; + + top: 7px; + bottom: 7px; + left: 17px; + + width: 9px; + + z-index: 8; + + border-radius: 50%; + + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.08), + rgba(255, 255, 255, 0.36), + rgba(255, 255, 255, 0.05) + ); + + opacity: 0.55; + + pointer-events: none; +} + +/* ------------------------------------------------------------ + Upper sand + + IMPORTANT: + The bottom edge stays near the neck of the hourglass. + + As --timer-remaining decreases, the TOP surface moves downward. + This makes the sand look as though it is actually draining. + ------------------------------------------------------------ */ + +.timer-hourglass-sand-top { + position: absolute; + + left: 16px; + bottom: 93px; + + width: 80px; + + height: calc(72px * var(--timer-remaining)); + + max-height: 72px; + + z-index: 3; + + border-radius: 50% 50% 8px 8px / 6px 6px 4px 4px; + + background: linear-gradient( + to bottom, + #efd88e 0%, + #dcb863 34%, + #c4923c 72%, + #986622 100% + ); + + box-shadow: + inset 0 4px 5px rgba(255, 242, 189, 0.2), + inset 0 -5px 7px rgba(113, 68, 17, 0.34); + + transform-origin: bottom; + + transition: height 0.06s linear; +} + +/* ------------------------------------------------------------ + Slight surface highlight on upper sand + ------------------------------------------------------------ */ + +.timer-hourglass-sand-top::before { + content: ""; + + position: absolute; + + top: 0; + left: 7%; + + width: 86%; + height: 5px; + + border-radius: 50%; + + background: rgba(255, 229, 151, 0.42); +} + +/* ------------------------------------------------------------ + Lower accumulated sand + + This grows upward from the bottom as time elapses. + ------------------------------------------------------------ */ + +.timer-hourglass-sand-bottom { + position: absolute; + + left: 10px; + bottom: 8px; + + width: 92px; + + height: calc(74px * var(--timer-elapsed)); + + max-height: 74px; + + z-index: 3; + + border-radius: 50% 50% 8px 8px / 10px 10px 4px 4px; + + background: linear-gradient( + to top, + #996622 0%, + #bd8934 42%, + #deb55c 78%, + #efd28a 100% + ); + + box-shadow: + inset 0 5px 7px rgba(255, 230, 160, 0.21), + inset 0 -3px 5px rgba(99, 58, 15, 0.3); + + transform-origin: bottom; + + transition: height 0.06s linear; +} + +/* ------------------------------------------------------------ + Sand mound surface + ------------------------------------------------------------ */ + +.timer-hourglass-sand-bottom::before { + content: ""; + + position: absolute; + + top: -3px; + left: 8%; + + width: 84%; + height: 9px; + + border-radius: 50%; + + background: radial-gradient( + ellipse at center, + #e9c978 0%, + #d6a84e 55%, + rgba(188, 132, 45, 0) 75% + ); +} + +/* ------------------------------------------------------------ + Falling sand stream + ------------------------------------------------------------ */ + +.timer-hourglass-stream { + position: absolute; + + left: 54px; + top: 87px; + + width: 4px; + height: 29px; + + z-index: 6; + + border-radius: 3px; + + background: repeating-linear-gradient( + to bottom, + #f0d17d 0px, + #f0d17d 3px, + #bd8530 4px, + #bd8530 6px + ); + + background-size: 100% 12px; + + box-shadow: 0 0 4px rgba(225, 183, 85, 0.38); + + animation: timer-sand-stream 0.28s linear infinite; + + opacity: 0.92; +} + +/* ------------------------------------------------------------ + Stop the falling stream when appropriate + ------------------------------------------------------------ */ + +.timer-hourglass.timer-idle .timer-hourglass-stream, +.timer-hourglass.timer-paused .timer-hourglass-stream, +.timer-hourglass.timer-complete .timer-hourglass-stream { + animation-play-state: paused; + opacity: 0; +} + +/* ------------------------------------------------------------ + Falling grain animation + ------------------------------------------------------------ */ + +@keyframes timer-sand-stream { + from { + background-position-y: 0; + } + + to { + background-position-y: 12px; + } +} + +/* ------------------------------------------------------------ + Individual decorative grains + ------------------------------------------------------------ */ + +.timer-hourglass-particles { + position: absolute; + + left: 0; + top: 0; + + width: 100%; + height: 100%; + + z-index: 7; + + pointer-events: none; +} + +.timer-hourglass-particle { + position: absolute; + + width: 3px; + height: 3px; + + border-radius: 50%; + + background: #e6c36b; + + box-shadow: 0 0 3px rgba(226, 183, 84, 0.5); + + opacity: 0; + + animation: timer-sand-particle 0.75s linear infinite; +} + +.timer-hourglass-particle:nth-child(1) { + left: 51px; + top: 94px; + + animation-delay: 0s; +} + +.timer-hourglass-particle:nth-child(2) { + left: 58px; + top: 97px; + + animation-delay: 0.22s; +} + +.timer-hourglass-particle:nth-child(3) { + left: 54px; + top: 101px; + + animation-delay: 0.46s; +} + +@keyframes timer-sand-particle { + 0% { + transform: translateY(-5px); + opacity: 0; + } + + 15% { + opacity: 0.8; + } + + 80% { + opacity: 0.55; + } + + 100% { + transform: translateY(24px); + opacity: 0; + } +} + +/* ------------------------------------------------------------ + Pause decorative particles + ------------------------------------------------------------ */ + +.timer-hourglass.timer-idle .timer-hourglass-particle, +.timer-hourglass.timer-paused .timer-hourglass-particle, +.timer-hourglass.timer-complete .timer-hourglass-particle { + animation-play-state: paused; + opacity: 0; +} + +/* ------------------------------------------------------------ + Critical-time subtle shake + ------------------------------------------------------------ */ + +.timer-display.timer-critical .timer-hourglass { + animation: timer-hourglass-critical 0.24s ease-in-out infinite alternate; +} + +@keyframes timer-hourglass-critical { + from { + transform: rotate(-0.35deg); + } + + to { + transform: rotate(0.35deg); + } +} + +/* ------------------------------------------------------------ + Completed hourglass + ------------------------------------------------------------ */ + +.timer-display.timer-complete .timer-hourglass-sand-top { + height: 0; +} + +.timer-display.timer-complete .timer-hourglass-sand-bottom { + height: 74px; +} + +/* ------------------------------------------------------------ + Paused hourglass + ------------------------------------------------------------ */ + +.timer-display.timer-paused .timer-hourglass-stream, +.timer-display.timer-paused .timer-hourglass-particle { + animation-play-state: paused; +} + +/* ============================================================ + FANTASY / HORROR CANDLE + ============================================================ */ + +.timer-candle { + position: relative; + + width: 180px; + height: 245px; + + display: flex; + align-items: flex-end; + justify-content: center; +} + +/* ------------------------------------------------------------ + Candle base + ------------------------------------------------------------ */ + +.timer-candle-base { + position: absolute; + + left: 35px; + bottom: 8px; + + width: 110px; + height: 14px; + + border-radius: 50%; + + background: linear-gradient(to bottom, #776449, #3d3325); + + box-shadow: + 0 5px 12px rgba(0, 0, 0, 0.65), + inset 0 2px 2px rgba(255, 230, 190, 0.2); +} + +/* ------------------------------------------------------------ + Wax body + ------------------------------------------------------------ */ + +.timer-candle-body { + position: absolute; + + left: 59px; + bottom: 18px; + + width: 62px; + + height: calc(160px * var(--timer-remaining)); + + min-height: 7px; + max-height: 160px; + + border-radius: 7px 7px 4px 4px; + + background: linear-gradient( + to right, + #bba87b, + #e1d3aa 32%, + #c2ad7e 68%, + #927c57 + ); + + box-shadow: + inset 6px 0 8px rgba(255, 245, 215, 0.18), + inset -6px 0 9px rgba(79, 59, 30, 0.28), + 0 0 13px rgba(232, 189, 92, 0.15); + + transition: height 0.08s linear; +} + +/* ------------------------------------------------------------ + Wax drip details + ------------------------------------------------------------ */ + +.timer-candle-body::before, +.timer-candle-body::after { + content: ""; + + position: absolute; + + top: 2px; + + width: 9px; + + border-radius: 0 0 8px 8px; + + background: rgba(224, 208, 166, 0.86); +} + +.timer-candle-body::before { + left: 8px; + height: 19px; +} + +.timer-candle-body::after { + right: 12px; + height: 12px; +} + +/* ------------------------------------------------------------ + Wick + ------------------------------------------------------------ */ + +.timer-candle-wick { + position: absolute; + + left: 50%; + top: -13px; + + width: 3px; + height: 16px; + + transform: translateX(-50%); + + border-radius: 4px; + + background: #29221b; +} + +/* ------------------------------------------------------------ + Flame container + ------------------------------------------------------------ */ + +.timer-candle-flame-wrap { + position: absolute; + + left: 50%; + top: -60px; + + width: 42px; + height: 58px; + + transform: translateX(-50%); +} + +/* ------------------------------------------------------------ + Outer flame + ------------------------------------------------------------ */ + +.timer-candle-flame { + position: absolute; + + left: 7px; + bottom: 0; + + width: 28px; + height: 50px; + + border-radius: 50% 50% 46% 46% / 65% 65% 35% 35%; + + background: radial-gradient( + ellipse at center bottom, + #fff4b5 0%, + #f4c05c 35%, + #c86b27 65%, + rgba(170, 60, 20, 0) 78% + ); + + transform-origin: center bottom; + + filter: drop-shadow(0 0 8px rgba(245, 176, 63, 0.85)) + drop-shadow(0 0 20px rgba(226, 115, 35, 0.45)); + + animation: timer-flame-flicker 0.17s ease-in-out infinite alternate; +} + +/* ------------------------------------------------------------ + Inner flame + ------------------------------------------------------------ */ + +.timer-candle-flame::after { + content: ""; + + position: absolute; + + left: 8px; + bottom: 5px; + + width: 12px; + height: 25px; + + border-radius: 50%; + + background: linear-gradient( + to top, + #fffbd0, + #ffe784 65%, + rgba(255, 220, 80, 0) + ); +} + +@keyframes timer-flame-flicker { + 0% { + transform: rotate(-2deg) scaleX(0.94) scaleY(1); + } + + 50% { + transform: rotate(2deg) scaleX(1.04) scaleY(0.96); + } + + 100% { + transform: rotate(-1deg) scaleX(0.98) scaleY(1.05); + } +} + +/* ------------------------------------------------------------ + Candle glow + ------------------------------------------------------------ */ + +.timer-candle-glow { + position: absolute; + + left: 50%; + top: -92px; + + width: 120px; + height: 120px; + + transform: translateX(-50%); + + border-radius: 50%; + + background: radial-gradient( + circle, + rgba(246, 185, 73, 0.21), + rgba(246, 185, 73, 0.08) 42%, + rgba(246, 185, 73, 0) 72% + ); + + animation: timer-candle-glow-pulse 1.3s ease-in-out infinite alternate; +} + +@keyframes timer-candle-glow-pulse { + from { + opacity: 0.7; + transform: translateX(-50%) scale(0.93); + } + + to { + opacity: 1; + transform: translateX(-50%) scale(1.06); + } +} + +/* ------------------------------------------------------------ + Paused candle + ------------------------------------------------------------ */ + +.timer-display.timer-paused .timer-candle-flame, +.timer-display.timer-paused .timer-candle-glow { + animation-play-state: paused; +} + +/* ------------------------------------------------------------ + Completed candle + ------------------------------------------------------------ */ + +.timer-display.timer-complete .timer-candle-flame, +.timer-display.timer-complete .timer-candle-glow { + display: none; +} + +/* ------------------------------------------------------------ + Smoke + ------------------------------------------------------------ */ + +.timer-candle-smoke { + position: absolute; + + left: 50%; + top: -39px; + + width: 8px; + height: 45px; + + transform: translateX(-50%); + + opacity: 0; +} + +.timer-display.timer-complete .timer-candle-smoke { + opacity: 1; +} + +.timer-candle-smoke::before, +.timer-candle-smoke::after { + content: ""; + + position: absolute; + + width: 10px; + height: 28px; + + border-radius: 50%; + + border-left: 2px solid rgba(180, 190, 195, 0.5); + + animation: timer-smoke-rise 2s ease-out infinite; +} + +.timer-candle-smoke::after { + left: 5px; + animation-delay: 0.8s; +} + +@keyframes timer-smoke-rise { + from { + transform: translateY(10px) translateX(0) scale(0.8); + opacity: 0.55; + } + + to { + transform: translateY(-30px) translateX(12px) scale(1.3); + opacity: 0; + } +} + +/* ============================================================ + ORIGINAL SCI-FI PIXEL DEPLETION TIMER + ============================================================ */ + +.timer-scifi { + position: relative; + + width: 250px; + min-height: 220px; + + padding: 20px; + + border: 2px solid rgba(79, 181, 195, 0.62); + + clip-path: polygon( + 12px 0, + calc(100% - 12px) 0, + 100% 12px, + 100% calc(100% - 12px), + calc(100% - 12px) 100%, + 12px 100%, + 0 calc(100% - 12px), + 0 12px + ); + + background: linear-gradient( + 135deg, + rgba(7, 23, 27, 0.96), + rgba(2, 8, 11, 0.98) + ); + + box-shadow: + inset 0 0 18px rgba(59, 194, 212, 0.12), + 0 0 20px rgba(30, 145, 165, 0.12); +} + +/* ------------------------------------------------------------ + Sci-fi decorative corners + ------------------------------------------------------------ */ + +.timer-scifi::before, +.timer-scifi::after { + content: ""; + + position: absolute; + + width: 36px; + height: 3px; + + background: rgba(88, 205, 216, 0.72); + + box-shadow: 0 0 8px rgba(88, 205, 216, 0.36); +} + +.timer-scifi::before { + top: 8px; + left: 14px; +} + +.timer-scifi::after { + right: 14px; + bottom: 8px; +} + +/* ------------------------------------------------------------ + Sci-fi status heading + ------------------------------------------------------------ */ + +.timer-scifi-heading { + margin-bottom: 13px; + + color: #91d4da; + + font-size: 9px; + font-weight: 900; + + letter-spacing: 0.28em; + text-align: center; + text-transform: uppercase; + + opacity: 0.82; +} + +/* ------------------------------------------------------------ + Pixel grid + ------------------------------------------------------------ */ + +.timer-scifi-grid { + display: grid; + + grid-template-columns: repeat(8, 1fr); + + gap: 7px; + + width: 100%; + + padding: 10px; + + border: 1px solid rgba(89, 193, 203, 0.24); + + background: linear-gradient(rgba(11, 31, 36, 0.65), rgba(3, 11, 14, 0.74)); +} + +/* ------------------------------------------------------------ + Individual pixels + ------------------------------------------------------------ */ + +.timer-scifi-pixel { + aspect-ratio: 1; + + border-radius: 2px; + + border: 1px solid rgba(117, 230, 230, 0.35); + + background: linear-gradient(135deg, #91e0df, #3fadb2 70%, #24666c); + + box-shadow: + 0 0 5px rgba(95, 218, 220, 0.55), + inset 0 0 4px rgba(225, 255, 255, 0.25); + + opacity: 1; + + transition: + opacity 0.16s linear, + transform 0.16s ease, + filter 0.16s ease; +} + +.timer-scifi-pixel.off { + opacity: 0.06; + + transform: scale(0.78); + + filter: grayscale(1); + + box-shadow: none; + + background: #102326; +} + +/* ------------------------------------------------------------ + Pixels being removed + ------------------------------------------------------------ */ + +.timer-scifi-pixel.decaying { + animation: timer-pixel-decay 0.28s ease-out; +} + +@keyframes timer-pixel-decay { + 0% { + opacity: 1; + transform: scale(1); + filter: brightness(1); + } + + 45% { + opacity: 1; + transform: scale(1.3); + filter: brightness(2.4); + } + + 100% { + opacity: 0.06; + transform: scale(0.78); + filter: brightness(0.5); + } +} + +/* ------------------------------------------------------------ + Sci-fi scan line + ------------------------------------------------------------ */ + +.timer-scifi-scan { + position: absolute; + + left: 7px; + right: 7px; + top: 0; + + height: 2px; + + background: linear-gradient( + to right, + transparent, + rgba(117, 230, 230, 0.85), + transparent + ); + + opacity: 0.55; + + animation: timer-scifi-scan 2.7s linear infinite; +} + +@keyframes timer-scifi-scan { + from { + top: 5%; + } + + to { + top: 94%; + } +} + +/* ------------------------------------------------------------ + Sci-fi decorative microtext + ------------------------------------------------------------ */ + +.timer-scifi-footer { + display: flex; + + justify-content: space-between; + + gap: 8px; + + margin-top: 11px; + + color: #568b90; + + font-size: 7px; + font-weight: 800; + + letter-spacing: 0.14em; + text-transform: uppercase; +} + +/* ------------------------------------------------------------ + Sci-fi warning state + ------------------------------------------------------------ */ + +.timer-display.timer-warning .timer-scifi { + border-color: rgba(213, 169, 73, 0.68); + + box-shadow: + inset 0 0 18px rgba(205, 148, 54, 0.12), + 0 0 20px rgba(205, 148, 54, 0.16); +} + +.timer-display.timer-warning .timer-scifi-pixel:not(.off) { + background: linear-gradient(135deg, #e0c276, #aa782f); + + border-color: rgba(235, 197, 104, 0.42); + + box-shadow: 0 0 5px rgba(225, 174, 65, 0.55); +} + +/* ------------------------------------------------------------ + Sci-fi critical state + ------------------------------------------------------------ */ + +.timer-display.timer-critical .timer-scifi { + border-color: rgba(206, 70, 78, 0.82); + + box-shadow: + inset 0 0 20px rgba(195, 59, 68, 0.18), + 0 0 23px rgba(195, 59, 68, 0.22); + + animation: timer-scifi-critical-frame 0.38s steps(2) infinite; +} + +.timer-display.timer-critical .timer-scifi-pixel:not(.off) { + background: linear-gradient(135deg, #ef858a, #a42e37); + + border-color: rgba(255, 151, 157, 0.55); + + box-shadow: 0 0 7px rgba(220, 68, 76, 0.72); +} + +@keyframes timer-scifi-critical-frame { + 0% { + filter: brightness(0.88); + } + + 50% { + filter: brightness(1.25); + } + + 100% { + filter: brightness(0.92); + } +} + +/* ------------------------------------------------------------ + Sci-fi complete state + ------------------------------------------------------------ */ + +.timer-display.timer-complete .timer-scifi { + border-color: rgba(170, 48, 55, 0.38); + + box-shadow: inset 0 0 18px rgba(160, 40, 47, 0.08); + + filter: brightness(0.55); +} + +.timer-display.timer-complete .timer-scifi-pixel { + opacity: 0.04; + + transform: scale(0.72); + + background: #1b1516; + + box-shadow: none; +} + +/* ------------------------------------------------------------ + Paused sci-fi decorative animation + ------------------------------------------------------------ */ + +.timer-display.timer-paused .timer-scifi-scan, +.timer-display.timer-paused .timer-scifi, +.timer-display.timer-paused .timer-scifi-pixel { + animation-play-state: paused; +} + +/* ============================================================ + GENERIC TIMER PROGRESS RING + Optional future skin / fallback + ============================================================ */ + +.timer-progress-ring { + position: relative; + + width: 180px; + height: 180px; + + border-radius: 50%; + + display: flex; + align-items: center; + justify-content: center; + + background: conic-gradient( + #6bb5c8 calc(var(--timer-remaining) * 360deg), + rgba(91, 126, 139, 0.16) 0 + ); + + box-shadow: 0 0 20px rgba(70, 160, 185, 0.2); +} + +.timer-progress-ring::before { + content: ""; + + position: absolute; + + inset: 15px; + + border-radius: 50%; + + background: #071117; + + border: 1px solid rgba(93, 144, 163, 0.34); +} + +/* ============================================================ + TIMER PANEL - DM CONSOLE + ============================================================ */ + +.timer-panel { + display: grid; + + grid-template-columns: + minmax(280px, 0.8fr) + minmax(320px, 1.2fr); + + gap: 22px; + + align-items: start; +} + +.timer-controls, +.timer-preview-panel { + min-width: 0; +} + +/* ============================================================ + TIMER PREVIEW + ============================================================ */ + +.timer-preview-panel { + display: flex; + align-items: center; + justify-content: center; + + min-height: 430px; + + padding: 24px; + + border: 1px solid var(--timer-panel-border); + border-radius: 14px; + + background: linear-gradient( + 145deg, + rgba(8, 20, 27, 0.92), + rgba(3, 9, 13, 0.96) + ); +} + +/* ============================================================ + TIMER CONTROL GROUPS + ============================================================ */ + +.timer-control-group { + margin-bottom: 18px; +} + +.timer-control-group label { + display: block; + + margin-bottom: 6px; + + color: #a8c1cd; + + font-size: 10px; + font-weight: 800; + + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.timer-duration-row { + display: grid; + + grid-template-columns: + minmax(0, 1fr) + minmax(0, 1fr); + + gap: 12px; +} + +.timer-duration-field { + min-width: 0; +} + +.timer-duration-field input { + width: 100%; +} + +/* ============================================================ + TIMER ACTION BUTTONS + ============================================================ */ + +.timer-actions { + display: flex; + flex-wrap: wrap; + + gap: 9px; + + margin-top: 18px; +} + +.timer-adjust-actions { + display: flex; + flex-wrap: wrap; + + gap: 7px; + + margin-top: 12px; +} + +/* ============================================================ + TIMER ACTIVE SUMMARY + ============================================================ */ + +.timer-active-summary { + margin-top: 18px; + + padding: 12px 14px; + + border: 1px solid rgba(86, 136, 154, 0.34); + border-radius: 10px; + + background: rgba(4, 13, 18, 0.65); +} + +.timer-active-summary-title { + color: #d0e2e8; + + font-size: 14px; + font-weight: 800; +} + +.timer-active-summary-state { + margin-top: 5px; + + color: #849da8; + + font-size: 10px; + + letter-spacing: 0.08em; + text-transform: uppercase; +} + +/* ============================================================ + REDUCED MOTION + ============================================================ */ + +@media (prefers-reduced-motion: reduce) { + .timer-critical, + .timer-hourglass-stream, + .timer-hourglass-particle, + .timer-candle-flame, + .timer-candle-glow, + .timer-candle-smoke::before, + .timer-candle-smoke::after, + .timer-scifi-scan, + .timer-scifi, + .timer-scifi-pixel { + animation: none !important; + } + + .timer-hourglass-sand-top, + .timer-hourglass-sand-bottom, + .timer-candle-body, + .timer-scifi-pixel { + transition: none !important; + } +} + +/* ============================================================ + RESPONSIVE TIMER UI + ============================================================ */ + +@media (max-width: 900px) { + .timer-panel { + grid-template-columns: 1fr; + } + + .timer-preview-panel { + min-height: 350px; + } + + .player-timer-overlay { + top: 78px; + } +} + +@media (max-width: 600px) { + .timer-display { + --timer-width: 210px; + --timer-height: 270px; + } + + .timer-hourglass { + transform: scale(0.82); + } + + .timer-candle { + transform: scale(0.82); + } + + .timer-scifi { + width: 215px; + } + + .timer-scifi-grid { + gap: 5px; + } + + .timer-duration-row { + grid-template-columns: 1fr; + } + + .player-timer-overlay { + top: 68px; + + max-width: calc(100% - 16px); + } +} + +/* ============================================================ + PLAYER DISPLAY TIMER POSITIONING + ============================================================ */ + +/* + * Keep the normal overlay timer comfortably below + * the Player Display header. + */ +.player-timer-overlay { + top: 155px; + right: 36px; +} + +/* + * Featured mode can sit slightly lower because + * the timer is intentionally larger. + */ +.player-timer-overlay.timer-mode-featured { + top: 175px; +} +/* ============================================================ + PLAYER DISPLAY TIMER CLEANUP + ============================================================ */ + +/* Hide the timer name/label on the Player Display */ +#playerTimerLayer .timer-label { + display: none; +} diff --git a/programs/app/display.html b/programs/app/display.html index ecf927d..ec1e4c9 100644 --- a/programs/app/display.html +++ b/programs/app/display.html @@ -3,6 +3,7 @@ SPDX-License-Identifier: MIT This file is part of enCounter. See LICENSE for the full license terms. --> + @@ -11,13 +12,17 @@
New Encounter
Generic TTRPG
- ALPHA · v0.1.0-alpha.1
+
+
+ ALPHA · v0.1.0-alpha.1
+
+
DM SYNC: CONNECTING…
+ DM SYNC: CONNECTING…
+
+
MIT · License & Notices
+ MIT · License & Notices
+