diff --git a/IWantToHeal-Thor-v1.0.36.apk b/IWantToHeal-Thor-v1.0.36.apk new file mode 100644 index 0000000..52e7188 Binary files /dev/null and b/IWantToHeal-Thor-v1.0.36.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 069197a..6dbda5f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 54 - versionName "1.0.35" + versionCode 55 + versionName "1.0.36" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/server/game-api.mjs b/server/game-api.mjs index 256d563..30d414f 100644 --- a/server/game-api.mjs +++ b/server/game-api.mjs @@ -1866,6 +1866,13 @@ function talentEffectCapacity(level) { return Math.min(4, Math.max(0, Math.floor(level / 5))) } +function talentEffectSource(effectType) { + if (effectType.startsWith('mend_')) return 'Mend' + if (effectType.startsWith('radiance_')) return 'Radiance' + if (effectType.startsWith('shield_') || effectType.startsWith('shielded_')) return 'Shield' + return effectType +} + function allocateTalent(database, characterId, talentId) { const character = database.prepare(` SELECT class_id AS classId, level, talent_points AS talentPoints @@ -1880,7 +1887,8 @@ function allocateTalent(database, characterId, talentId) { max_rank AS maxRank, tier, prerequisite_talent_id AS prerequisiteTalentId, - prerequisite_rank AS prerequisiteRank + prerequisite_rank AS prerequisiteRank, + effect_type AS effectType FROM talents WHERE id = ? `).get(talentId) @@ -1905,14 +1913,25 @@ function allocateTalent(database, characterId, talentId) { } else { const capacity = talentEffectCapacity(character.level) if (capacity <= 0) throw new Error('Spell effects unlock at level 5.') - const activeCount = database.prepare(` - SELECT COUNT(*) AS count + const activeTalents = database.prepare(` + SELECT + talents.id, + talents.name, + talents.effect_type AS effectType FROM character_talents JOIN talents ON talents.id = character_talents.talent_id WHERE character_talents.character_id = ? AND talents.class_id = ? AND character_talents.rank > 0 - `).get(characterId, character.classId).count + `).all(characterId, character.classId) + const source = talentEffectSource(talent.effectType) + const sourceConflict = activeTalents.find( + (candidate) => candidate.id !== talentId && talentEffectSource(candidate.effectType) === source, + ) + if (sourceConflict) { + throw new Error(`Only one ${source} spell effect can be active.`) + } + const activeCount = activeTalents.length if (activeCount >= capacity) { throw new Error(`Level ${character.level} allows ${capacity} active spell effect${capacity === 1 ? '' : 's'}.`) } diff --git a/src/App.css b/src/App.css index afca1e5..0b4084f 100644 --- a/src/App.css +++ b/src/App.css @@ -3465,14 +3465,13 @@ h2 { .spell-effect-layout { display: grid; gap: 14px; - grid-template-columns: 260px minmax(0, 1fr) 260px; + grid-template-columns: 220px minmax(0, 1fr); margin-top: 17px; min-height: 0; } .effect-slots-panel, -.effect-pool-panel, -.effect-detail-panel { +.effect-pool-panel { background: #191b25; border: 2px solid #090a0d; min-height: 0; @@ -3544,10 +3543,47 @@ h2 { font-size: 9px; } +.selected-effect-strip { + align-items: center; + background: #20222d; + border: 2px solid #090a0d; + display: grid; + gap: 12px; + grid-template-columns: minmax(0, 1fr) auto; + margin-top: 12px; + outline: 2px solid #3a3944; + padding: 10px; +} + +.selected-effect-strip strong, +.selected-effect-strip small { + display: block; +} + +.selected-effect-strip strong { + color: var(--gold); + font-family: 'Press Start 2P', monospace; + font-size: 9px; + line-height: 1.35; + margin-top: 5px; +} + +.selected-effect-strip small { + color: var(--muted); + font-size: 15px; + line-height: 1; + margin-top: 5px; +} + +.selected-effect-strip .primary-button { + min-width: 120px; + padding: 9px 12px; +} + .effect-pool { display: grid; gap: 10px; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 12px; } @@ -3609,17 +3645,22 @@ h2 { margin-top: 5px; } -.effect-detail-panel h2 { - color: var(--gold); - font-size: 22px; - margin-top: 10px; -} +@media (max-width: 800px) { + .spell-effect-layout { + grid-template-columns: 1fr; + } -.effect-detail-panel p { - color: var(--muted); - font-size: 17px; - line-height: 1.1; - margin: 12px 0; + .effect-slots-panel { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .effect-pool { + grid-template-columns: 1fr; + } + + .selected-effect-strip { + grid-template-columns: 1fr; + } } .talent-tier { diff --git a/src/components/TalentScreen.tsx b/src/components/TalentScreen.tsx index 8b85bde..9a7a5db 100644 --- a/src/components/TalentScreen.tsx +++ b/src/components/TalentScreen.tsx @@ -16,6 +16,18 @@ type Props = { const EFFECT_SLOT_LEVELS = [5, 10, 15, 20] as const const EFFECT_CLASS_ID = 1 +const EFFECT_SOURCE_LABELS: Record = { + mend: 'Mend', + radiance: 'Radiance', + shield: 'Shield', +} + +function effectSource(effectType: string) { + if (effectType.startsWith('mend_')) return 'mend' + if (effectType.startsWith('radiance_')) return 'radiance' + if (effectType.startsWith('shield_') || effectType.startsWith('shielded_')) return 'shield' + return effectType +} function effectCapacity(level: number) { return EFFECT_SLOT_LEVELS.filter((slotLevel) => level >= slotLevel).length @@ -59,6 +71,9 @@ export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: P function lockReason(talent: Talent) { if (!isEffectClass) return 'Coming soon' if (talent.rank > 0) return '' + const source = effectSource(talent.effectType) + const sourceConflict = selectedEffects.find((effect) => effectSource(effect.effectType) === source) + if (sourceConflict) return `${EFFECT_SOURCE_LABELS[source] ?? source} already selected` if (capacity <= 0) return 'Unlocks at level 5' if (selectedEffects.length >= capacity) { return `Active slots full (${capacity}/${capacity})` @@ -182,6 +197,33 @@ export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: P {selectedEffects.length}/{capacity} active +
+
+

Selected Effect

+ {selectedTalent ? ( + <> + {selectedTalent.name} + {selectedTalent.description} + + ) : ( + No effect selected. + )} +
+ {selectedTalent && ( + + )} +
{gameClass.talents.map((talent) => { const reason = lockReason(talent) @@ -210,30 +252,6 @@ export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: P })}
- - )} diff --git a/src/gameRepository.ts b/src/gameRepository.ts index cb7bc94..08bad4b 100644 --- a/src/gameRepository.ts +++ b/src/gameRepository.ts @@ -432,6 +432,13 @@ function talentEffectCapacity(level: number) { return Math.min(4, Math.max(0, Math.floor(level / 5))) } +function talentEffectSource(effectType: string) { + if (effectType.startsWith('mend_')) return 'Mend' + if (effectType.startsWith('radiance_')) return 'Radiance' + if (effectType.startsWith('shield_') || effectType.startsWith('shielded_')) return 'Shield' + return effectType +} + type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string } const COMPONENT_ITEMS: Record = { 1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' }, @@ -1112,6 +1119,16 @@ function createLocalRepository(store: LocalSaveStore): GameRepository { } else { const capacity = talentEffectCapacity(cd.level) if (capacity <= 0) throw new Error('Spell effects unlock at level 5.') + const source = talentEffectSource(talent.effectType) + const sourceConflict = gameClass.talents.find( + (candidate) => + candidate.id !== talentId + && (cd.talentRanks[String(candidate.id)] ?? 0) > 0 + && talentEffectSource(candidate.effectType) === source, + ) + if (sourceConflict) { + throw new Error(`Only one ${source} spell effect can be active.`) + } const activeCount = gameClass.talents.reduce( (total, candidate) => total + ((cd.talentRanks[String(candidate.id)] ?? 0) > 0 ? 1 : 0), 0,