UNPKG

boardcast

Version:

Animation library for tabletop game rules on hex boards with CLI tools and game extensions

377 lines (376 loc) 15.9 kB
import { BoardcastHexBoard, ClearType } from 'boardcast'; import * as Lancer from 'boardcast-contrib/lancer'; // Initialize the boardcast demo when the DOM is loaded let board; let tutorialShouldStop = false; document.addEventListener('DOMContentLoaded', () => { board = new BoardcastHexBoard('#chart'); console.log('Boardcast hex board demo initialized!'); // Make board available globally for console access window.boardcast = board; setupDemoEventListeners(); }); function setupDemoEventListeners() { const showCoordBtn = document.getElementById('show-coordinates'); const hideCoordBtn = document.getElementById('hide-coordinates'); const gridSmallBtn = document.getElementById('grid-small'); const gridMediumBtn = document.getElementById('grid-medium'); const gridLargeBtn = document.getElementById('grid-large'); const demoHighlightBtn = document.getElementById('demo-highlight'); const demoBlinkBtn = document.getElementById('demo-blink'); const demoPulseBtn = document.getElementById('demo-pulse'); const demoPointBtn = document.getElementById('demo-point'); const demoCaptionBtn = document.getElementById('demo-caption'); const demoDiceBtn = document.getElementById('demo-dice'); const demoClearBtn = document.getElementById('demo-clear'); const demoTokensBtn = document.getElementById('demo-tokens'); const demoMovementBtn = document.getElementById('demo-movement'); const tutorialLancerMovementBtn = document.getElementById('tutorial-lancer-movement'); const tutorialLancerContribBtn = document.getElementById('tutorial-lancer-contrib'); const resetBtn = document.getElementById('reset-board'); showCoordBtn?.addEventListener('click', () => board.showCoordinates()); hideCoordBtn?.addEventListener('click', () => board.hideCoordinates()); gridSmallBtn?.addEventListener('click', () => board.setGridSizeWithScaling(3)); gridMediumBtn?.addEventListener('click', () => board.setGridSizeWithScaling(6)); gridLargeBtn?.addEventListener('click', () => board.setGridSizeWithScaling(10)); demoHighlightBtn?.addEventListener('click', () => { board.resetBoard(); board.highlight(1, 0, '#ff6b6b'); board.highlight(-1, 1, '#4ecdc4'); board.highlight(0, -1, '#feca57'); }); demoBlinkBtn?.addEventListener('click', () => { board.resetBoard(); board.blink(2, -1, '#ff6b6b'); board.blink(-2, 1, '#4ecdc4'); board.blink(1, 1, '#feca57'); }); demoPulseBtn?.addEventListener('click', () => { board.resetBoard(); board.pulse(1, -1, '#ff6b6b'); board.pulse(-1, 0, '#4ecdc4'); board.pulse(0, 1, '#feca57'); }); demoPointBtn?.addEventListener('click', () => { board.resetBoard(); board.point(2, -1, 'Target A'); board.point(-1, 2, 'Target B'); board.point(0, 0, 'Center'); board.point(-2, 0, 'Left'); }); demoCaptionBtn?.addEventListener('click', async () => { board.resetBoard(); await board.caption('Welcome to Boardcast!', 3000); await board.caption('This demonstrates caption overlays', 2500); await board.caption('Perfect for explaining game rules!', 2000); }); demoDiceBtn?.addEventListener('click', async () => { board.resetBoard(); await board.caption('Rolling multiple dice...', 1500); // Add dice one by one to show multiple dice feature board.dice('d6', 1, '#ff6b6b'); // Red d6 await sleep(800); board.dice('d20', 15, '#4ecdc4'); // Teal d20 await sleep(800); board.dice('d6', 6, '#feca57'); // Yellow d6 await sleep(800); board.dice('d20', 3, '#ff9ff3'); // Pink d20 await sleep(1000); await board.caption('Multiple dice display side by side!', 2000); await sleep(500); // Add more dice to show positioning board.dice('d6', 4, '#54a0ff'); // Blue d6 await sleep(800); board.dice('d20', 20, '#5f27cd'); // Purple d20 await sleep(1000); await board.caption('New dice appear on the right, old ones shift left', 2500); // Clear all and show mixed dice rapidly board.clear(); await board.caption('Rapid dice rolling...', 1000); const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']; for (let i = 0; i < 6; i++) { const dieType = Math.random() > 0.5 ? 'd6' : 'd20'; const number = dieType === 'd6' ? Math.ceil(Math.random() * 6) : Math.ceil(Math.random() * 20); board.dice(dieType, number, colors[i]); await sleep(600); } await sleep(1000); await board.caption('Each die keeps its own type, number, and color!', 2500); }); demoClearBtn?.addEventListener('click', async () => { // Create various artifacts to demonstrate clearing board.resetBoard(); board.highlight(0, 0, '#ff6b6b'); board.highlight(1, 0, '#4ecdc4'); board.blink(-1, 0, '#feca57'); board.pulse(0, 1, '#ff6b6b'); board.point(1, 1, 'Target'); board.token(0, -1, 'demo', 'star', '#4444ff', 'Token'); await board.caption('All artifacts created - watch selective clearing', 3000); // Demonstrate selective clearing board.clear(ClearType.POINT); await board.caption('Points cleared! Watch highlights clear next', 3000); board.clear(ClearType.HIGHLIGHT); await board.caption('Highlights cleared! Other artifacts remain', 2000); }); demoTokensBtn?.addEventListener('click', () => { board.resetBoard(); board.token(0, 0, 'center', 'circle', '#ff4444', 'Player'); board.token(1, 0, 'right', 'rect', '#44ff44', 'Guard'); board.token(-1, 1, 'left', 'triangle', '#4444ff', 'Enemy'); board.token(0, -1, 'top', 'star', '#ffff44', 'Treasure'); }); demoMovementBtn?.addEventListener('click', async () => { board.resetBoard(); board.token(0, 0, 'player', 'circle', '#ff4444', 'Hero'); // Demo movement sequence await board.move('player', 2, -1); await board.move('player', -1, 2); await board.move('player', 0, 0); }); resetBtn?.addEventListener('click', () => { tutorialShouldStop = true; board.resetBoard(); }); // Lancer Movement Tutorial tutorialLancerMovementBtn?.addEventListener('click', async () => { tutorialShouldStop = false; await lancerMovementTutorial(); }); // Lancer Contrib Demo tutorialLancerContribBtn?.addEventListener('click', async () => { tutorialShouldStop = false; await lancerContribDemo(); }); } // Lancer Movement Tutorial - Based on script async function lancerMovementTutorial() { board.resetBoard(); // CAPTION "In Lancer SPEED determines how far you can go" 3s if (tutorialShouldStop) return; await board.caption('In Lancer SPEED determines how far you can go', 3000); // show a circle token with label "mech" on the origin board.token(0, 0, 'mech', 'circle', '#4444FF', 'mech'); // CAPTION "This mech has speed 2" if (tutorialShouldStop) return; await board.caption('This mech has speed 2', 2000); // show pulse on all coordinates in distance 2 from the origin // Distance 1 hexes board.pulse(1, 0, '#4fc3f7'); board.pulse(-1, 0, '#4fc3f7'); board.pulse(0, 1, '#4fc3f7'); board.pulse(0, -1, '#4fc3f7'); board.pulse(1, -1, '#4fc3f7'); board.pulse(-1, 1, '#4fc3f7'); // Distance 2 hexes board.pulse(2, 0, '#4fc3f7'); board.pulse(-2, 0, '#4fc3f7'); board.pulse(0, 2, '#4fc3f7'); board.pulse(0, -2, '#4fc3f7'); board.pulse(2, -1, '#4fc3f7'); board.pulse(1, -2, '#4fc3f7'); board.pulse(-1, -1, '#4fc3f7'); board.pulse(-2, 1, '#4fc3f7'); board.pulse(-1, 2, '#4fc3f7'); board.pulse(1, 1, '#4fc3f7'); board.pulse(2, -2, '#4fc3f7'); board.pulse(-2, 2, '#4fc3f7'); // CAPTION "The mech will now move by 2" if (tutorialShouldStop) return; await board.caption('The mech will now move by 2', 2000); // now move the mech token two hexes away if (tutorialShouldStop) return; await board.move('mech', 2, 0); // clear the highlights board.clear(ClearType.PULSE); // CAPTION "It is out of move now..." if (tutorialShouldStop) return; await board.caption('It is out of move now...', 2000); // CAPTION "...but it can spend a quick action to move again" if (tutorialShouldStop) return; await board.caption('...but it can spend a quick action to move again', 2000); // move again two fields somewhere else if (tutorialShouldStop) return; await board.move('mech', 0, 2); // CAPTION "this is called a boost" if (tutorialShouldStop) return; await board.caption('this is called a boost', 2000); // CAPTION "Terrain can make it harder to move" if (tutorialShouldStop) return; await board.caption('Terrain can make it harder to move', 2000); // highlight some hexes next to the mech in yellow, point arrow at them with label "difficult terrain" board.highlight(1, 1, '#FFFF44'); board.highlight(0, 2, '#FFFF44'); board.highlight(-1, 2, '#FFFF44'); board.point(1, 1, 'difficult terrain'); // CAPTION "Moving through difficult terrain costs 2x speed" if (tutorialShouldStop) return; await board.caption('Moving through difficult terrain costs 2x speed', 2000); // clear the arrows board.clear(ClearType.POINT); // make the mech move into one of the difficult terrain hexes if (tutorialShouldStop) return; await board.move('mech', 1, 1); // CAPTION "With Speed 2 this mech can move through only 1 difficult terrain" if (tutorialShouldStop) return; await board.caption('With Speed 2 this mech can move through only 1 difficult terrain', 2000); // clear the highlights board.clear(ClearType.HIGHLIGHT); // CAPTION "Dangerous terrain does not slow you down ..." if (tutorialShouldStop) return; await board.caption('Dangerous terrain does not slow you down ...', 2000); // CAPTION "But may cause damage" if (tutorialShouldStop) return; await board.caption('But may cause damage', 2000); // highlight some hexes near the mech in red, point at them with label dangerous terrain board.highlight(2, 1, '#FF4444'); board.highlight(1, 2, '#FF4444'); board.highlight(0, 3, '#FF4444'); board.point(2, 1, 'dangerous terrain'); // Wait a moment to show the dangerous terrain if (tutorialShouldStop) return; await sleep(2000); // clear the arrows board.clear(ClearType.POINT); // move the mech into one of the dangerous hexes if (tutorialShouldStop) return; await board.move('mech', 2, 1); // CAPTION "When you move into dangerous terrain for the first time" if (tutorialShouldStop) return; await board.caption('When you move into dangerous terrain for the first time', 2000); // CAPTION "You must roll ENGINEERING, you get 5 damage if you fail" if (tutorialShouldStop) return; await board.caption('You must roll ENGINEERING, you get 5 damage if you fail', 2000); // CAPTION "The damage type depends on the terrain itself (up to GM)" if (tutorialShouldStop) return; await board.caption('The damage type depends on the terrain itself (up to GM)', 2000); } // Lancer Contrib Demo - Showcasing specialized Lancer methods async function lancerContribDemo() { board.resetBoard(); // Initialize Lancer contrib classes const movement = new Lancer.LancerMovement(board); const combat = new Lancer.LancerCombat(board); if (tutorialShouldStop) return; await board.caption('Lancer Contrib Library Demo', 3000); if (tutorialShouldStop) return; await board.caption('Specialized methods for Lancer RPG mechanics', 3000); // Place a mech at the center board.token(0, 0, 'mech', 'circle', Lancer.Colors.BLUE, 'Atlas'); // Demo 1: Movement Range Calculation if (tutorialShouldStop) return; await board.caption('Movement Range: Speed 4 Mech', 3000); movement.showMovementRange(0, 0, 4); await sleep(4000); board.clear(ClearType.PULSE); // Demo 2: Terrain Effects if (tutorialShouldStop) return; await board.caption('Terrain Types in Lancer', 3000); // Show difficult terrain const difficultTerrain = [ { q: 2, r: 0 }, { q: 3, r: 0 }, { q: 1, r: 2 }, { q: -2, r: 1 }, { q: -3, r: 2 } ]; movement.showDifficultTerrain(difficultTerrain, '#8B4513'); board.point(2, 0, 'Difficult Terrain'); await sleep(3000); board.clear(ClearType.POINT); // Show dangerous terrain const dangerousTerrain = [ { q: -1, r: -2 }, { q: 0, r: -3 }, { q: 2, r: -2 }, { q: 3, r: -1 }, { q: 1, r: 3 } ]; movement.showDangerousTerrain(dangerousTerrain); board.point(-1, -2, 'Dangerous Terrain'); await sleep(3000); board.clear(ClearType.POINT); await board.caption('Difficult terrain costs 2x movement', 2500); await board.caption('Dangerous terrain causes damage', 2500); // Demo 3: Combat Mechanics if (tutorialShouldStop) return; await board.caption('Combat: Engagement Zone', 3000); board.clear(ClearType.HIGHLIGHT); // Show engagement zone combat.showEngagementZone(0, 0); await sleep(3000); // Add an enemy mech board.token(3, 1, 'enemy', 'triangle', Lancer.Colors.RED, 'Minotaur'); await board.caption('Enemy mech enters the battlefield', 2500); // Demo 4: Weapon Range if (tutorialShouldStop) return; await board.caption('Weapon Range: Assault Rifle (Range 10)', 3000); board.clear(ClearType.BLINK); const weapon = { name: 'Assault Rifle', type: 'main', range: 5, damage: '1d6+2', tags: ['Reliable 3'] }; combat.showWeaponRange(0, 0, weapon, 3, 1); await sleep(4000); // Demo 5: Line of Sight and Cover if (tutorialShouldStop) return; await board.caption('Cover Mechanics', 3000); board.clear(ClearType.PULSE); board.clear(ClearType.POINT); combat.showCoverPositions(0, 0, 3, 1); await sleep(4000); // Demo 6: Area of Effect Attack if (tutorialShouldStop) return; await board.caption('Blast Template: Burst 2 Explosion', 3000); board.clear(ClearType.HIGHLIGHT); board.clear(ClearType.POINT); combat.showBlastTemplate(2, 0, 2, Lancer.Colors.ORANGE); await sleep(4000); // Final demonstration - Complex scenario if (tutorialShouldStop) return; await board.caption('Complex Scenario: Multiple Mechs', 3000); board.clear(ClearType.BLINK); // Add more mechs board.token(-2, 2, 'ally1', 'circle', Lancer.Colors.GREEN, 'Blackbeard'); board.token(1, -3, 'ally2', 'circle', Lancer.Colors.GREEN, 'Everest'); // Show multiple engagement zones combat.showEngagementZone(-2, 2, Lancer.Colors.ENGAGEMENT_YELLOW); combat.showEngagementZone(1, -3, Lancer.Colors.ENGAGEMENT_YELLOW); await board.caption('Multiple engagement zones create tactical complexity', 3000); await sleep(3000); // Show combined movement and combat await board.caption('Movement + Combat: Positioning is key!', 3000); movement.showMovementRange(-2, 2, 3); await sleep(4000); await board.caption('Lancer Contrib Demo Complete!', 3000); await board.caption('Use these methods to create your own Lancer tutorials', 3000); } // Utility function for async delays function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }