Story 4.1: Task 1 Complete - PlayerPrivacyManager Core Logic
- Created src/contracts/privacy-settings.js with: - PrivacySettings typedef - PRIVACY_SETTINGS_DEFAULT (both flags false) - PRIVACY_SETTING_KEYS and FEATURE_NAME_MAP constants - createPrivacySettings() factory - isValidPrivacySettings() validator - validateSettingKey(), validateSettingValue(), validateFeatureName() helpers - Created src/core/PlayerPrivacyManager.js with: - Constructor with FoundryAdapter DI validation - getSettings(userId) - retrieves settings from user flags - setSetting(userId, key, value) - async, validates, persists via user.setFlag - isOptedIn(userId, feature) - convenience method for feature checks - getAllSettings() - aggregates all users' settings (GM view) - onChange(callback) - subscription pattern for change events - teardown() - cleanup - Created tests/unit/contracts/privacy-settings.test.js - 44 tests - Created tests/unit/core/PlayerPrivacyManager.test.js - 35 tests - All tests passing, lint clean - Updated sprint-status.yaml: 4-1 from ready-for-dev to in-progress - Updated story file: Task 1 subtasks 1.1-1.8 marked complete Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Privacy Settings contract.
|
||||
*
|
||||
* Privacy settings control player opt-in/out for automation features that affect
|
||||
* their on-screen presence. Settings are stored as user flags on the user document.
|
||||
*
|
||||
* Storage key: game.user.setFlag('video-view-manager', key, value)
|
||||
* Shape: { reactionCamEnabled: boolean, hpReactiveCamStylingEnabled: boolean }
|
||||
*
|
||||
* @module contracts/privacy-settings
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PrivacySettings
|
||||
* @property {boolean} reactionCamEnabled - Whether Reaction Cam automation is enabled for this user.
|
||||
* @property {boolean} hpReactiveCamStylingEnabled - Whether HP-Reactive Cam Styling is enabled for this user.
|
||||
*/
|
||||
|
||||
export const PRIVACY_SETTINGS_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Default privacy settings - all features disabled by default (opt-in, not opt-out).
|
||||
* @type {PrivacySettings}
|
||||
*/
|
||||
export const PRIVACY_SETTINGS_DEFAULT = {
|
||||
reactionCamEnabled: false,
|
||||
hpReactiveCamStylingEnabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical list of privacy setting keys.
|
||||
* @type {string[]}
|
||||
*/
|
||||
export const PRIVACY_SETTING_KEYS = Object.freeze([
|
||||
"reactionCamEnabled",
|
||||
"hpReactiveCamStylingEnabled",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Feature name mapping for opt-in checks.
|
||||
* @type {Object.<string, string>}
|
||||
*/
|
||||
export const FEATURE_NAME_MAP = Object.freeze({
|
||||
reactionCam: "reactionCamEnabled",
|
||||
hpReactiveCamStyling: "hpReactiveCamStylingEnabled",
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new PrivacySettings object with defaults.
|
||||
* Only known keys from PRIVACY_SETTINGS_DEFAULT are included; extra properties are ignored.
|
||||
* @param {Partial<PrivacySettings>} [overrides={}] - Override default values.
|
||||
* @returns {PrivacySettings}
|
||||
*/
|
||||
export function createPrivacySettings(overrides = {}) {
|
||||
const result = { ...PRIVACY_SETTINGS_DEFAULT };
|
||||
for (const key of PRIVACY_SETTING_KEYS) {
|
||||
if (key in overrides) {
|
||||
result[key] = overrides[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a PrivacySettings DTO. Throws TypeError on any violation.
|
||||
* @param {unknown} data - Value to validate.
|
||||
* @returns {PrivacySettings} The validated settings.
|
||||
* @throws {TypeError} If data fails validation.
|
||||
*/
|
||||
export function isValidPrivacySettings(data) {
|
||||
if (data === null) {
|
||||
throw new TypeError("PrivacySettings: must be an object");
|
||||
}
|
||||
if (typeof data !== "object" || Array.isArray(data)) {
|
||||
throw new TypeError("PrivacySettings: must be an object");
|
||||
}
|
||||
const obj = /** @type {Record<string, unknown>} */ (data);
|
||||
const { reactionCamEnabled, hpReactiveCamStylingEnabled, ...rest } = obj;
|
||||
if (Object.keys(rest).length > 0) {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: unknown keys: ${Object.keys(rest).join(", ")}`
|
||||
);
|
||||
}
|
||||
if (typeof reactionCamEnabled !== "boolean") {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: reactionCamEnabled must be a boolean, got ${typeof reactionCamEnabled}`
|
||||
);
|
||||
}
|
||||
if (typeof hpReactiveCamStylingEnabled !== "boolean") {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: hpReactiveCamStylingEnabled must be a boolean, got ${typeof hpReactiveCamStylingEnabled}`
|
||||
);
|
||||
}
|
||||
return /** @type {PrivacySettings} */ (data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a single privacy setting key.
|
||||
* @param {string} key - The setting key to validate.
|
||||
* @returns {string} The validated key.
|
||||
* @throws {TypeError} If key is invalid.
|
||||
*/
|
||||
export function validateSettingKey(key) {
|
||||
if (typeof key !== "string" || key.length === 0) {
|
||||
throw new TypeError("Setting key must be a non-empty string");
|
||||
}
|
||||
if (!PRIVACY_SETTING_KEYS.includes(key)) {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: unknown key '${key}'. Valid keys: ${PRIVACY_SETTING_KEYS.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a single privacy setting value.
|
||||
* @param {unknown} value - The value to validate.
|
||||
* @returns {boolean} The validated value.
|
||||
* @throws {TypeError} If value is invalid.
|
||||
*/
|
||||
export function validateSettingValue(value) {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: value must be a boolean, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a feature name for opt-in checks.
|
||||
* @param {string} feature - The feature name to validate.
|
||||
* @returns {string} The validated feature name.
|
||||
* @throws {TypeError} If feature is invalid.
|
||||
*/
|
||||
export function validateFeatureName(feature) {
|
||||
if (typeof feature !== "string" || feature.length === 0) {
|
||||
throw new TypeError("Feature name must be a non-empty string");
|
||||
}
|
||||
const validFeatures = Object.keys(FEATURE_NAME_MAP);
|
||||
if (!validFeatures.includes(feature)) {
|
||||
throw new TypeError(
|
||||
`PrivacySettings: unknown feature '${feature}'. Valid features: ${validFeatures.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
Reference in New Issue
Block a user