Complete Phases 5-8: Adult tone tags, content validation, dashboard U… - #18
Conversation
…X, documentation - Phase 5: Added Brad and Marcus to ROMANCE_ARCS with tone tags, created adult-tone-guide.md and toneTags.test.js - Phase 6: Created validate-content.mjs script and 5 content structure tests - Phase 7: Added HomeStylePanel, HomeActivityPanel, NpcHomeReaction components to Dashboard - Phase 8: Created 6 documentation files (reputation, balance-scenarios, save-migrations, content-validation, npc-schedules) - Updated ROADMAP.md completion matrix (23/26 tasks complete) Co-authored-by: Earnest-Williams <Earnest-Williams@users.noreply.github.com>
There was a problem hiding this comment.
Code Review
This pull request updates the project roadmap, adds comprehensive documentation for adult tone guides, balance scenarios, content validation, NPC schedules, reputation, and save migrations, and introduces several new dashboard panels for home style and activities. It also adds romance arcs for Brad and Marcus, along with new schema validation tests. Feedback on these changes includes addressing a potential runtime crash in HomeActivityPanel by using the correct helper function for home style profiles, removing unused imports in validate-content.mjs and Dashboard.jsx, correcting pronoun inconsistencies in the new romance arcs, and updating the validation script to prevent false warnings on nested choice impacts and missing valid stats.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @@ -0,0 +1,97 @@ | |||
| import React from 'react'; | |||
| import { useGameStore } from '../../state/store'; | |||
| import { HOME_ACTIVITIES } from '../../data/furniture'; | |||
There was a problem hiding this comment.
| const gameState = useGameStore(state => state.gameState); | ||
|
|
||
| // Calculate home style profile | ||
| const profile = {}; | ||
| for (const furnitureId of placedFurniture) { | ||
| const furniture = useGameStore.getState().gameState.furniture[furnitureId]; | ||
| if (furniture?.tags) { | ||
| for (const tag of furniture.tags) { | ||
| profile[tag] = (profile[tag] || 0) + 1; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Accessing useGameStore.getState().gameState.furniture is highly likely to return undefined because furniture is static data defined in src/data/furniture.js and not part of the game state. This will cause a runtime TypeError and crash the dashboard. Additionally, calling getState() inside a render function is an anti-pattern as it bypasses React's reactive updates. Use the existing calculateHomeStyleProfile helper function instead.
| const gameState = useGameStore(state => state.gameState); | |
| // Calculate home style profile | |
| const profile = {}; | |
| for (const furnitureId of placedFurniture) { | |
| const furniture = useGameStore.getState().gameState.furniture[furnitureId]; | |
| if (furniture?.tags) { | |
| for (const tag of furniture.tags) { | |
| profile[tag] = (profile[tag] || 0) + 1; | |
| } | |
| } | |
| } | |
| // Calculate home style profile | |
| const profile = calculateHomeStyleProfile(placedFurniture); |
| import { NPCS } from '../src/data/npcs.js'; | ||
| import { DATE_TEMPLATES } from '../src/data/dates.js'; | ||
| import { ITEMS } from '../src/data/items.js'; | ||
| import { readFileSync } from 'node:fs'; |
| } | ||
|
|
||
| // Check for relationship/chemistry impact | ||
| const hasImpact = 'relationshipImpact' in choice || 'chemistryImpact' in choice || 'relationship' in choice || 'chemistry' in choice; |
There was a problem hiding this comment.
The hasImpact check only looks for direct relationship/chemistry impact properties on the choice object. However, choices that perform a stat check have their impacts nested inside onSuccess and onFail objects. This causes the script to output false warnings for valid stat-check choices. Update the check to also inspect onSuccess and onFail objects.
const hasImpact = 'relationshipImpact' in choice || 'chemistryImpact' in choice || 'relationship' in choice || 'chemistry' in choice || (choice.onSuccess && ('relationshipImpact' in choice.onSuccess || 'chemistryImpact' in choice.onSuccess)) || (choice.onFail && ('relationshipImpact' in choice.onFail || 'chemistryImpact' in choice.onFail));|
|
||
| // Validate checkStat if present | ||
| if (choice.checkStat) { | ||
| const validStats = ['intelligence', 'fitness', 'charisma', 'style', 'corporate', 'finance', 'culinary', 'confidence', 'empathy', 'negotiation', 'socialIq', 'programming', 'music', 'hygiene', 'money']; |
There was a problem hiding this comment.
The validStats array is missing several valid stats that are checked or used in the game, such as creativity, gaming, and marketing. This can lead to false validation failures when new content uses these stats. Add them to the list of valid stats.
const validStats = ['intelligence', 'fitness', 'charisma', 'style', 'corporate', 'finance', 'culinary', 'confidence', 'empathy', 'negotiation', 'socialIq', 'programming', 'music', 'hygiene', 'money', 'creativity', 'gaming', 'marketing'];| import TimeControls from './dashboard/TimeControls'; | ||
| import HomeStylePanel from './dashboard/HomeStylePanel'; | ||
| import HomeActivityPanel from './dashboard/HomeActivityPanel'; | ||
| import NpcHomeReaction from './dashboard/NpcHomeReaction'; |
| { id: "brad_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Brad gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day they are having before trying to impress them.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "brad_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | ||
| { id: "brad_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Brad lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match their pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | ||
| { id: "brad_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Brad admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank them for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "brad_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | ||
| { id: "brad_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Brad pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_owned_impact" }, { text: "Give them room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like they are overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, |
There was a problem hiding this comment.
Pronoun inconsistency in Brad's romance arc. Brad is a male NPC, but the choices in brad_intro, brad_early, brad_reveal, and brad_conflict use gender-neutral "they/them/their" pronouns (likely copied from the generic makePhase2RomanceArc template), while the rest of his arc correctly uses "he/him/his". Update the pronouns to "he/him/his" for consistency.
| { id: "brad_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Brad gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day they are having before trying to impress them.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "brad_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "brad_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Brad lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match their pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | |
| { id: "brad_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Brad admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank them for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "brad_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "brad_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Brad pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_owned_impact" }, { text: "Give them room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like they are overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, | |
| { id: "brad_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Brad gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day he is having before trying to impress him.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "brad_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "brad_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Brad lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match his pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | |
| { id: "brad_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Brad admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank him for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "brad_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "brad_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Brad pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "brad_owned_impact" }, { text: "Give him room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like he is overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, |
| { id: "marcus_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Marcus gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day they are having before trying to impress them.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "marcus_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | ||
| { id: "marcus_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Marcus lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match their pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | ||
| { id: "marcus_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Marcus admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank them for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "marcus_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | ||
| { id: "marcus_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Marcus pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_owned_impact" }, { text: "Give them room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like they are overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, |
There was a problem hiding this comment.
Pronoun inconsistency in Marcus's romance arc. Marcus is a male NPC, but the choices in marcus_intro, marcus_early, marcus_reveal, and marcus_conflict use gender-neutral "they/them/their" pronouns (likely copied from the generic makePhase2RomanceArc template), while the rest of his arc correctly uses "he/him/his". Update the pronouns to "he/him/his" for consistency.
| { id: "marcus_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Marcus gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day they are having before trying to impress them.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "marcus_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "marcus_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Marcus lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match their pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | |
| { id: "marcus_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Marcus admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank them for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "marcus_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "marcus_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Marcus pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_owned_impact" }, { text: "Give them room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like they are overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, | |
| { id: "marcus_intro", type: "introduction", minRelationship: 0, title: "First Real Conversation", prompt: "Marcus gives you a careful opening to show who you are.", emotionalBeat: "Curiosity and first-impression nerves.", choices: [{ text: "Ask what kind of day he is having before trying to impress him.", relationshipImpact: 6, chemistryImpact: 5, unlocksMemory: "marcus_honest_opening" }, { text: "Share a small honest detail about yourself.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Try too hard to sound impressive.", checkStat: "charisma", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 4 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "marcus_early", type: "early connection", minRelationship: 18, title: "A Shared Rhythm", prompt: "Marcus lets the conversation slow down enough for trust to start forming.", emotionalBeat: "Warmth, pacing, and guarded hope.", choices: [{ text: "Match his pace and listen for what matters.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_shared_rhythm" }, { text: "Suggest a low-pressure follow-up together.", relationshipImpact: 5, chemistryImpact: 6 }, { text: "Fill every silence.", relationshipImpact: 1, chemistryImpact: -1 }] }, | |
| { id: "marcus_reveal", type: "personal reveal", minRelationship: 35, title: "Something Underneath", prompt: "Marcus admits there is more going on than his usual public routine shows.", emotionalBeat: "Vulnerability balanced against self-protection.", choices: [{ text: "Thank him for trusting you with the truth.", relationshipImpact: 9, chemistryImpact: 6, unlocksMemory: "marcus_trusted_reveal" }, { text: "Ask what support would actually help.", relationshipImpact: 7, chemistryImpact: 6 }, { text: "Rush into advice.", checkStat: "empathy", threshold: 35, onSuccess: { relationshipImpact: 4, chemistryImpact: 3 }, onFail: { relationshipImpact: -2, chemistryImpact: -1 } }] }, | |
| { id: "marcus_conflict", type: "conflict", minRelationship: 52, title: "A Misread Moment", prompt: "Marcus pulls back after a choice lands differently than you intended.", emotionalBeat: "Hurt, uncertainty, and a chance to repair without defensiveness.", choices: [{ text: "Own the impact before explaining intent.", relationshipImpact: 7, chemistryImpact: 5, unlocksMemory: "marcus_owned_impact" }, { text: "Give him room, then follow up clearly.", relationshipImpact: 5, chemistryImpact: 4 }, { text: "Act like he is overreacting.", relationshipImpact: -6, chemistryImpact: -4 }] }, |
…X, documentation