Android build v1.0.36
This commit is contained in:
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 54
|
versionCode 55
|
||||||
versionName "1.0.35"
|
versionName "1.0.36"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
+23
-4
@@ -1866,6 +1866,13 @@ function talentEffectCapacity(level) {
|
|||||||
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
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) {
|
function allocateTalent(database, characterId, talentId) {
|
||||||
const character = database.prepare(`
|
const character = database.prepare(`
|
||||||
SELECT class_id AS classId, level, talent_points AS talentPoints
|
SELECT class_id AS classId, level, talent_points AS talentPoints
|
||||||
@@ -1880,7 +1887,8 @@ function allocateTalent(database, characterId, talentId) {
|
|||||||
max_rank AS maxRank,
|
max_rank AS maxRank,
|
||||||
tier,
|
tier,
|
||||||
prerequisite_talent_id AS prerequisiteTalentId,
|
prerequisite_talent_id AS prerequisiteTalentId,
|
||||||
prerequisite_rank AS prerequisiteRank
|
prerequisite_rank AS prerequisiteRank,
|
||||||
|
effect_type AS effectType
|
||||||
FROM talents
|
FROM talents
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).get(talentId)
|
`).get(talentId)
|
||||||
@@ -1905,14 +1913,25 @@ function allocateTalent(database, characterId, talentId) {
|
|||||||
} else {
|
} else {
|
||||||
const capacity = talentEffectCapacity(character.level)
|
const capacity = talentEffectCapacity(character.level)
|
||||||
if (capacity <= 0) throw new Error('Spell effects unlock at level 5.')
|
if (capacity <= 0) throw new Error('Spell effects unlock at level 5.')
|
||||||
const activeCount = database.prepare(`
|
const activeTalents = database.prepare(`
|
||||||
SELECT COUNT(*) AS count
|
SELECT
|
||||||
|
talents.id,
|
||||||
|
talents.name,
|
||||||
|
talents.effect_type AS effectType
|
||||||
FROM character_talents
|
FROM character_talents
|
||||||
JOIN talents ON talents.id = character_talents.talent_id
|
JOIN talents ON talents.id = character_talents.talent_id
|
||||||
WHERE character_talents.character_id = ?
|
WHERE character_talents.character_id = ?
|
||||||
AND talents.class_id = ?
|
AND talents.class_id = ?
|
||||||
AND character_talents.rank > 0
|
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) {
|
if (activeCount >= capacity) {
|
||||||
throw new Error(`Level ${character.level} allows ${capacity} active spell effect${capacity === 1 ? '' : 's'}.`)
|
throw new Error(`Level ${character.level} allows ${capacity} active spell effect${capacity === 1 ? '' : 's'}.`)
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-14
@@ -3465,14 +3465,13 @@ h2 {
|
|||||||
.spell-effect-layout {
|
.spell-effect-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
grid-template-columns: 260px minmax(0, 1fr) 260px;
|
grid-template-columns: 220px minmax(0, 1fr);
|
||||||
margin-top: 17px;
|
margin-top: 17px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.effect-slots-panel,
|
.effect-slots-panel,
|
||||||
.effect-pool-panel,
|
.effect-pool-panel {
|
||||||
.effect-detail-panel {
|
|
||||||
background: #191b25;
|
background: #191b25;
|
||||||
border: 2px solid #090a0d;
|
border: 2px solid #090a0d;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -3544,10 +3543,47 @@ h2 {
|
|||||||
font-size: 9px;
|
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 {
|
.effect-pool {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3609,17 +3645,22 @@ h2 {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.effect-detail-panel h2 {
|
@media (max-width: 800px) {
|
||||||
color: var(--gold);
|
.spell-effect-layout {
|
||||||
font-size: 22px;
|
grid-template-columns: 1fr;
|
||||||
margin-top: 10px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.effect-detail-panel p {
|
.effect-slots-panel {
|
||||||
color: var(--muted);
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
font-size: 17px;
|
}
|
||||||
line-height: 1.1;
|
|
||||||
margin: 12px 0;
|
.effect-pool {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-effect-strip {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.talent-tier {
|
.talent-tier {
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ type Props = {
|
|||||||
|
|
||||||
const EFFECT_SLOT_LEVELS = [5, 10, 15, 20] as const
|
const EFFECT_SLOT_LEVELS = [5, 10, 15, 20] as const
|
||||||
const EFFECT_CLASS_ID = 1
|
const EFFECT_CLASS_ID = 1
|
||||||
|
const EFFECT_SOURCE_LABELS: Record<string, string> = {
|
||||||
|
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) {
|
function effectCapacity(level: number) {
|
||||||
return EFFECT_SLOT_LEVELS.filter((slotLevel) => level >= slotLevel).length
|
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) {
|
function lockReason(talent: Talent) {
|
||||||
if (!isEffectClass) return 'Coming soon'
|
if (!isEffectClass) return 'Coming soon'
|
||||||
if (talent.rank > 0) return ''
|
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 (capacity <= 0) return 'Unlocks at level 5'
|
||||||
if (selectedEffects.length >= capacity) {
|
if (selectedEffects.length >= capacity) {
|
||||||
return `Active slots full (${capacity}/${capacity})`
|
return `Active slots full (${capacity}/${capacity})`
|
||||||
@@ -182,6 +197,33 @@ export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: P
|
|||||||
</div>
|
</div>
|
||||||
<span>{selectedEffects.length}/{capacity} active</span>
|
<span>{selectedEffects.length}/{capacity} active</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="selected-effect-strip">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Selected Effect</p>
|
||||||
|
{selectedTalent ? (
|
||||||
|
<>
|
||||||
|
<strong>{selectedTalent.name}</strong>
|
||||||
|
<small>{selectedTalent.description}</small>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<small>No effect selected.</small>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedTalent && (
|
||||||
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
disabled={Boolean(lockReason(selectedTalent)) || busyTalentId === selectedTalent.id}
|
||||||
|
onClick={() => toggleEffect(selectedTalent)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{busyTalentId === selectedTalent.id
|
||||||
|
? 'Saving...'
|
||||||
|
: selectedTalent.rank > 0
|
||||||
|
? 'Remove'
|
||||||
|
: 'Activate'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="effect-pool">
|
<div className="effect-pool">
|
||||||
{gameClass.talents.map((talent) => {
|
{gameClass.talents.map((talent) => {
|
||||||
const reason = lockReason(talent)
|
const reason = lockReason(talent)
|
||||||
@@ -210,30 +252,6 @@ export function TalentScreen({ profile, onBack, onUpdated, embedded = false }: P
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside className="effect-detail-panel">
|
|
||||||
<p className="eyebrow">Selected Effect</p>
|
|
||||||
{selectedTalent ? (
|
|
||||||
<>
|
|
||||||
<h2>{selectedTalent.name}</h2>
|
|
||||||
<p>{selectedTalent.description}</p>
|
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
disabled={Boolean(lockReason(selectedTalent)) || busyTalentId === selectedTalent.id}
|
|
||||||
onClick={() => toggleEffect(selectedTalent)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{busyTalentId === selectedTalent.id
|
|
||||||
? 'Saving...'
|
|
||||||
: selectedTalent.rank > 0
|
|
||||||
? 'Remove Effect'
|
|
||||||
: 'Activate Effect'}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>No effect selected.</p>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -432,6 +432,13 @@ function talentEffectCapacity(level: number) {
|
|||||||
return Math.min(4, Math.max(0, Math.floor(level / 5)))
|
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 }
|
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
|
||||||
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
|
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
|
||||||
1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
|
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 {
|
} else {
|
||||||
const capacity = talentEffectCapacity(cd.level)
|
const capacity = talentEffectCapacity(cd.level)
|
||||||
if (capacity <= 0) throw new Error('Spell effects unlock at level 5.')
|
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(
|
const activeCount = gameClass.talents.reduce(
|
||||||
(total, candidate) => total + ((cd.talentRanks[String(candidate.id)] ?? 0) > 0 ? 1 : 0),
|
(total, candidate) => total + ((cd.talentRanks[String(candidate.id)] ?? 0) > 0 ? 1 : 0),
|
||||||
0,
|
0,
|
||||||
|
|||||||
Reference in New Issue
Block a user