feat: enhance build process and testing workflow

- Updated package.json scripts to include separate unit and integration tests before building.
- Added a new script `test-build-test` to run tests before and after the build process.
- Introduced a new local CI/CD script for streamlined local development and testing.
- Refactored PlayerTab component to normalize skills data structure and added tooltips for Space Marine and Power Armour abilities.
- Added named export for Tooltip in DeathwatchRoller for reuse in other components.
This commit is contained in:
2025-08-16 15:14:28 +02:00
parent b6320ceff6
commit 671865d100
12 changed files with 2462 additions and 35 deletions
+3
View File
@@ -1122,3 +1122,6 @@ if (process.env.NODE_ENV === 'development') {
}
export default DeathwatchRoller;
// Named export for reuse in other components
export { Tooltip };
+91 -10
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import axios from 'axios';
import logger, { debug, info, warn, logApiCall, logApiError, logUserAction } from '../utils/logger';
import { Tooltip } from './DeathwatchRoller';
const STORAGE_SHOP_AUTHED = 'dw:shop:authedPlayer';
const STORAGE_SHOP_PLAYERS = 'dw:shop:players:v1';
@@ -29,6 +30,8 @@ const CHARACTERISTICS = [
{ key: 'fel', label: 'Fellowship (Fel)' },
]
// Updated default skills to Space Marine skills
const SKILLS = [
'Acrobatics (Ag)', 'Awareness (Per)', 'Charm (Fel)', 'Climb (S)', 'Command (Fel)',
@@ -67,7 +70,7 @@ function PlayerTab({
const [pastEvent, setPastEvent] = useState('');
const [personalDemeanour, setPersonalDemeanour] = useState('');
const [characteristics, setCharacteristics] = useState({});
const [skills, setSkills] = useState([]);
const [skills, setSkills] = useState({});
const [weapons, setWeapons] = useState([]);
const [armour, setArmour] = useState({});
const [talents, setTalents] = useState('');
@@ -89,6 +92,37 @@ function PlayerTab({
// Debug state
const [showLogs, setShowLogs] = useState(false);
const [logs, setLogs] = useState([]);
// Ability descriptions (used as hover tooltips)
const SPACE_MARINE_ABILITIES = [
{ name: 'Secondary Heart', desc: 'Once per long rest, when reduced to 0 Wounds you remain at 1 Wound instead of falling unconscious (stunned 1 round).' },
{ name: "Larraman's Organ", desc: 'When stabilised or treated with basic medicae, regain an extra 1–3 Wounds (GM roll) or gain +10 to recovery checks.' },
{ name: 'Catalepsean Node', desc: 'Ignore penalties from lack of sleep for ~48 hours and reduce exhaustion effects; can grant allies advantage on vigilance checks during extended watches.' },
{ name: 'Preomnor', desc: 'Strong bonus to toxin-resistance tests (GM: auto-success or +20) and halve duration/effect of ingested poisons.' },
{ name: 'Omophagea', desc: 'On consuming a biological sample (GM permission), gain a one-time clue or +10 to lore/identification checks related to that subject.' },
{ name: 'Multi-lung', desc: 'Ignore breathing penalties in toxic/low-oxygen environments and avoid short-term inhalation damage.' },
{ name: 'Sus-an Membrane', desc: 'Can enter suspended animation to avoid bleeding out when gravely wounded; buys time for evacuation/treatment.' },
{ name: 'Oolitic Kidney', desc: 'Reduce toxin damage/effects by half once per exposure or gain advantage on resisting chemical effects.' },
{ name: 'Neuroglottis', desc: 'Automatic/strong bonus to checks identifying substances by taste; +10 to Tracking vs a target you have tasted.' },
{ name: 'Mucranoid', desc: 'Small resistance to disease and contaminants; +10 on disease-resistance and recovery rolls.' },
{ name: "Betcher's Gland", desc: 'One-use close toxin/venom attack per rest or apply a minor toxin effect on a successful melee strike (GM adjudication).' },
{ name: 'Progenoid Glands', desc: 'Store genetic material for Chapter preservation; may enable long-term resurrection rituals (GM process).' },
{ name: 'Melanochrome', desc: '+10 or advantage to perception in varied lighting and camouflage detection.' },
{ name: 'Occulobe', desc: 'Ignore low-light penalties; see in darkness to short range.' },
{ name: "Lyman's Ear", desc: 'Advantage on hearing/perception checks and ability to detect faint sounds.' }
];
const POWER_ARMOUR_ABILITIES = [
{ name: 'Servo-Augmented Musculature', desc: '+20 Strength while wearing Power Armour.' },
{ name: 'Auto-senses', desc: 'Dark sight; immune to Photon Flash and Stun Grenades; Called Shots are Half Actions; +10 to sight and hearing Awareness Tests (stacks with Heightened Senses).' },
{ name: 'Built-in Vox Link', desc: 'Integrated communications — no external vox gear required.' },
{ name: 'Built-in Magboots', desc: 'Grip in zero-G or slippery conditions; avoid slipping/falling penalties.' },
{ name: 'Nutrient Recycling', desc: 'Operate up to two weeks without resupply.' },
{ name: 'Recoil Suppression', desc: 'May fire Basic weapons one-handed without penalty while in Power Armour.' },
{ name: 'Black Carapace / Size: Hulking', desc: 'Large frame, but Black Carapace prevents enemies gaining a size-based bonus to hit you while in Power Armour.' },
{ name: 'Poor Manual Dexterity', desc: 'Delicate tasks suffer a −10 penalty unless using Space Marine-specific tools.' },
{ name: 'Osmotic Gill Life Sustainer', desc: 'Extended underwater/contaminated atmosphere operation support.' }
];
// Initialize GM state is now handled in App.js
@@ -97,6 +131,10 @@ function PlayerTab({
if (showLogs) setLogs(logger.getLogs({ limit: 200 }));
}, [showLogs]);
// GM helpers (define early so hooks using them can call safely)
const isGMLoggedIn = useCallback(() => authedPlayer === 'gm', [authedPlayer]);
const isGMOrShopAuthed = useCallback(() => isGMLoggedIn() || !!authedPlayer, [isGMLoggedIn, authedPlayer]);
// Remove localStorage cache for players except after backend fetch
const currentPlayer = useMemo(() => {
@@ -129,7 +167,7 @@ function PlayerTab({
setPastEvent('');
setPersonalDemeanour('');
setCharacteristics({});
setSkills([]);
setSkills({});
setWeapons([]);
setArmour({});
setTalents('');
@@ -162,7 +200,7 @@ function PlayerTab({
setPastEvent(tabInfo.pastEvent || '');
setPersonalDemeanour(tabInfo.personalDemeanour || '');
setCharacteristics(tabInfo.characteristics || {});
setSkills(tabInfo.skills || []);
setSkills(normalizeIncomingSkills(tabInfo.skills || []));
setWeapons(tabInfo.weapons || []);
setArmour(tabInfo.armour || {});
setPicture(tabInfo.picture || tabInfo.avatar || '');
@@ -337,7 +375,7 @@ function PlayerTab({
pastEvent: updatedPlayer.pastEvent,
personalDemeanour: updatedPlayer.personalDemeanour,
characteristics: updatedPlayer.characteristics,
skills: updatedPlayer.skills,
skills: serializeSkillsForSave(updatedPlayer.skills),
weapons: updatedPlayer.weapons,
armour: updatedPlayer.armour,
talents: updatedPlayer.talents,
@@ -432,9 +470,7 @@ function PlayerTab({
// Define shopAuthed for use in the component
const shopAuthed = getShopAuthed();
// Stable callbacks so hooks depending on them don't change each render
const isGMLoggedIn = useCallback(() => authedPlayer === 'gm', [authedPlayer]);
const isGMOrShopAuthed = useCallback(() => isGMLoggedIn() || !!authedPlayer, [isGMLoggedIn, authedPlayer]);
// Stable callbacks defined earlier
// GM session is now managed in App.js
@@ -695,6 +731,33 @@ function PlayerTab({
return headers;
}
// Skills normalization helpers
function normalizeIncomingSkills(raw) {
// Backend stores skills as array of names; frontend expects an object map
if (Array.isArray(raw)) {
const obj = {};
for (const s of raw) {
if (!s) continue;
obj[s] = { trained: true, plus10: false, plus20: false };
}
return obj;
}
if (raw && typeof raw === 'object') return raw;
return {};
}
function serializeSkillsForSave(skillsObj) {
// Convert frontend skills object back to an array of skill names for backend
if (Array.isArray(skillsObj)) return skillsObj.filter(Boolean);
if (!skillsObj || typeof skillsObj !== 'object') return [];
const out = [];
for (const [k, v] of Object.entries(skillsObj)) {
if (!k) continue;
if (v && (v.trained || v.plus10 || v.plus20)) out.push(k);
}
return out;
}
function refreshLogs() { setLogs(logger.getLogs({ limit: 200 })); }
function clearLogs() { logger.clearLogs(); setLogs([]); }
@@ -901,11 +964,29 @@ function PlayerTab({
</div>
</div>
{/* Space Marine Abilities */}
{/* Space Marine Abilities - hover tooltips */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="font-semibold mb-2">Space Marine Abilities</div>
<div className="text-xs opacity-80">
Secondary Heart, Larraman’s Organ, Catalepsean Node, Preomnor, Omophagea, Multi-lung, Sus-an Membrane, Oolitic Kidney, Neuroglottis, Mucranoid, Betcher’s Gland, Progenoid Glands, Melanochrome, Occulobe, Lyman’s Ear
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
{SPACE_MARINE_ABILITIES.map(a => (
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
<span className="mr-2">{a.name}</span>
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
</div>
))}
</div>
</div>
{/* Power Armour Abilities - hover tooltips */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10 mt-4">
<div className="font-semibold mb-2">Power Armour Abilities (standard)</div>
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
{POWER_ARMOUR_ABILITIES.map(a => (
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
<span className="mr-2">{a.name}</span>
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
</div>
))}
</div>
</div>