From 394c4217c6067e003bda989b07da99bf883a5e99 Mon Sep 17 00:00:00 2001 From: Offirmo Date: Mon, 16 Sep 2024 20:39:25 +1000 Subject: [PATCH] +++ --- .../src/reducers/index.ts | 204 ++++++++++++++++++ .../state/src/reducers/create.ts | 4 +- .../the-boring-rpg/ui--rich-text/package.json | 5 +- .../ui--rich-text/src/adventure.ts | 18 +- .../ui--rich-text/src/adventure_spec.ts | 28 +-- 5 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 stack--current/A-apps--core/the-boring-rpg/logic--adventure--resolved/src/reducers/index.ts diff --git a/stack--current/A-apps--core/the-boring-rpg/logic--adventure--resolved/src/reducers/index.ts b/stack--current/A-apps--core/the-boring-rpg/logic--adventure--resolved/src/reducers/index.ts new file mode 100644 index 00000000..245580cf --- /dev/null +++ b/stack--current/A-apps--core/the-boring-rpg/logic--adventure--resolved/src/reducers/index.ts @@ -0,0 +1,204 @@ +import { type Immutable} from '@offirmo-private/ts-types' +import { Enum } from 'typescript-string-enums' + +import { generate_uuid } from '@offirmo-private/uuid' +import { getꓽrandom, RNGEngine } from '@offirmo/random' + +import { + CharacterAttribute, + CharacterClass, + State as CharacterState, +} from '@tbrpg/state--character' +import * as InventoryState from '@tbrpg/state--inventory' +import * as WalletState from '@tbrpg/state--wallet' +import { + create as create_weapon, + is_at_max_enhancement as is_weapon_at_max_enhancement, +} from '@tbrpg/logic--weapons' +import { + create as create_armor, +} from '@tbrpg/logic--armors' +import { + create as create_monster, +} from '@tbrpg/logic--monsters' +import { + OutcomeArchetype, + AdventureType, + AdventureArchetype, + generate_random_coin_gain_or_loss, +} from '@tbrpg/logic--adventures' + +import { + ResolvedAdventure, +} from '../types.js' + +import { LIB } from '../consts.js' + +///////////////////////////////////////////////// + +type AttributesArray = CharacterAttribute[] + +const ALL_ATTRIBUTES_X_LVL: AttributesArray = [ 'health', 'mana', 'strength', 'agility', 'charisma', 'wisdom', 'luck' ] + +const WARRIOR_LIKE_PRIMARY_ATTRIBUTES: AttributesArray = ['strength'] +const ROGUE_LIKE_PRIMARY_ATTRIBUTES: AttributesArray = ['agility'] +const MAGE_LIKE_PRIMARY_ATTRIBUTES: AttributesArray = ['mana'] +const HYBRID_PALADIN_LIKE_PRIMARY_ATTRIBUTES: AttributesArray = ['strength', 'mana'] + +const PRIMARY_STATS_BY_CLASS: { [k: string]: AttributesArray } = { + [CharacterClass.novice]: ALL_ATTRIBUTES_X_LVL, + + [CharacterClass.barbarian]: WARRIOR_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.druid]: ['wisdom', 'mana'], + [CharacterClass.warrior]: WARRIOR_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.paladin]: HYBRID_PALADIN_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.rogue]: ROGUE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.sorcerer]: MAGE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.warlock]: MAGE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.wizard]: MAGE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass['darkness hunter']]: HYBRID_PALADIN_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.hunter]: ['agility'], + [CharacterClass.priest]: ['charisma', 'mana'], + [CharacterClass['death knight']]: HYBRID_PALADIN_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.mage]: MAGE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.engineer]: ['wisdom'], + [CharacterClass.thief]: ROGUE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.assassin]: ROGUE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.illusionist]: ['charisma', 'agility'], + [CharacterClass.knight]: WARRIOR_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.pirate]: ['luck'], + [CharacterClass.ninja]: ROGUE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.corsair]: ROGUE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.necromancer]: MAGE_LIKE_PRIMARY_ATTRIBUTES, + [CharacterClass.sculptor]: ['agility'], + [CharacterClass.summoner]: MAGE_LIKE_PRIMARY_ATTRIBUTES, +} +if (Object.keys(PRIMARY_STATS_BY_CLASS).length !== Enum.keys(CharacterClass).length) + throw new Error(`${LIB}: PRIMARY_STATS_BY_CLASS is out of date!`) + + +const WARRIOR_LIKE_SECONDARY_ATTRIBUTES: AttributesArray = ['health'] +const ROGUE_LIKE_SECONDARY_ATTRIBUTES: AttributesArray = ['luck'] +const MAGE_LIKE_SECONDARY_ATTRIBUTES: AttributesArray = ['wisdom'] +const HYBRID_PALADIN_LIKE_SECONDARY_ATTRIBUTES: AttributesArray = ['health'] + + +const SECONDARY_STATS_BY_CLASS: { [k: string]: AttributesArray } = { + [CharacterClass.novice]: ALL_ATTRIBUTES_X_LVL, + + [CharacterClass.barbarian]: WARRIOR_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.druid]: ['strength', 'agility'], + [CharacterClass.warrior]: WARRIOR_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.paladin]: HYBRID_PALADIN_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.rogue]: ROGUE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.sorcerer]: MAGE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.warlock]: MAGE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.wizard]: MAGE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass['darkness hunter']]: HYBRID_PALADIN_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.hunter]: ['strength'], + [CharacterClass.priest]: ['wisdom'], + [CharacterClass['death knight']]: HYBRID_PALADIN_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.mage]: MAGE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.engineer]: ['agility', 'luck'], + [CharacterClass.thief]: ROGUE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.assassin]: ROGUE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.illusionist]: ['luck'], + [CharacterClass.knight]: WARRIOR_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.pirate]: ['charisma', 'agility'], + [CharacterClass.ninja]: ROGUE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.corsair]: ['charisma', 'luck'], + [CharacterClass.necromancer]: MAGE_LIKE_SECONDARY_ATTRIBUTES, + [CharacterClass.sculptor]: ['wisdom', 'charisma'], + [CharacterClass.summoner]: MAGE_LIKE_SECONDARY_ATTRIBUTES, +} +if (Object.keys(SECONDARY_STATS_BY_CLASS).length !== Enum.keys(CharacterClass).length) + throw new Error(`${LIB}: SECONDARY_STATS_BY_CLASS is out of date!`) + +///////////////////////////////////////////////// + +function create( + rng: RNGEngine, + aa: Immutable, + character: Immutable, + inventory: Immutable, + wallet: Immutable, +): ResolvedAdventure { + const { hid, good, type, outcome } = aa + + const should_gain: OutcomeArchetype = { + ...outcome, + } + + // instantiate the special gains + if (should_gain.random_attribute) { + const stat: keyof OutcomeArchetype = getꓽrandom.picker.of(ALL_ATTRIBUTES_X_LVL)(rng) as keyof OutcomeArchetype + (should_gain as any)[stat] = true + } + if (should_gain.lowest_attribute) { + const lowest_stat: keyof OutcomeArchetype = ALL_ATTRIBUTES_X_LVL.reduce((acc, val) => { + return (character.attributes as any)[acc] < (character.attributes as any)[val] ? acc : val + }, 'health') as keyof OutcomeArchetype + (should_gain as any)[lowest_stat] = true + } + if (should_gain.class_primary_attribute) { + const stat: keyof OutcomeArchetype = getꓽrandom.picker.of(PRIMARY_STATS_BY_CLASS[character.klass]!)(rng) as keyof OutcomeArchetype + (should_gain as any)[stat] = true + } + if (should_gain.class_secondary_attribute) { + const stat: keyof OutcomeArchetype = getꓽrandom.picker.of(SECONDARY_STATS_BY_CLASS[character.klass]!)(rng) as keyof OutcomeArchetype + (should_gain as any)[stat] = true + } + + if (should_gain.armor_or_weapon) { + // TODO take into account the existing inventory? + if (getꓽrandom.generator_of.bool()(rng)) + should_gain.armor = true + else + should_gain.weapon = true + } + if (should_gain.improvementⵧarmor_or_weapon) { + if (is_weapon_at_max_enhancement(InventoryState.get_slotted_weapon(inventory)!)) // most likely to happen + should_gain.improvementⵧarmor = true + else if (getꓽrandom.generator_of.bool()(rng)) + should_gain.improvementⵧarmor = true + else + should_gain.improvementⵧweapon = true + } + + // intermediate data + const new_player_level = character.attributes.level + (should_gain.level ? 1 : 0) + + // TODO check multiple charac gain (should not happen) + return { + uuid: generate_uuid(), + hid, + good, + encounter: type === AdventureType.fight ? create_monster(rng, {level: character.attributes.level}) : null, + gains: { + level: should_gain.level ? 1 : 0, + health: should_gain.health ? 1 : 0, + mana: should_gain.mana ? 1 : 0, + strength: should_gain.strength ? 1 : 0, + agility: should_gain.agility ? 1 : 0, + charisma: should_gain.charisma ? 1 : 0, + wisdom: should_gain.wisdom ? 1 : 0, + luck: should_gain.luck ? 1 : 0, + coin: generate_random_coin_gain_or_loss(rng, { + range: should_gain.coin, + player_level: new_player_level, + current_wallet_amount: wallet.coin_count, + }), + token: should_gain.token ? 1 : 0, + armor: should_gain.armor ? create_armor(rng) : null, + weapon: should_gain.weapon ? create_weapon(rng) : null, + improvementⵧarmor: should_gain.improvementⵧarmor, + improvementⵧweapon: should_gain.improvementⵧweapon, + }, + } +} + +///////////////////////////////////////////////// + +export { + create, +} diff --git a/stack--current/A-apps--core/the-boring-rpg/state/src/reducers/create.ts b/stack--current/A-apps--core/the-boring-rpg/state/src/reducers/create.ts index 831be29b..095e8ecf 100644 --- a/stack--current/A-apps--core/the-boring-rpg/state/src/reducers/create.ts +++ b/stack--current/A-apps--core/the-boring-rpg/state/src/reducers/create.ts @@ -73,8 +73,8 @@ function create(SXC?: TBRSoftExecutionContext, seed?: PRNGState.Seed): Immutable wallet: WalletState.add_amount(WalletState.create(), WalletState.Currency.coin, 1), // don't start empty so that a loss can happen prng: PRNGState.create(seed), energy: u_state_energy, - engagement: EngagementState.create(SXC), - codes: CodesState.create(SXC), + engagement: EngagementState.create(SXC as any), + codes: CodesState.create(SXC as any), progress: AchievementsState.create(SXC), meta: MetaState.create(), diff --git a/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/package.json b/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/package.json index 0d34db21..6fff0c73 100644 --- a/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/package.json +++ b/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/package.json @@ -26,14 +26,15 @@ "@offirmo-private/rich-text-format": "^0", "@oh-my-rpg/state--meta": "^0", "@tbrpg/definitions": "^0", - "@tbrpg/logic--shop": "^0", + "@tbrpg/logic--adventure--resolved": "*", "@tbrpg/logic--adventures": "^0", "@tbrpg/logic--armors": "^0", "@tbrpg/logic--monsters": "^0", + "@tbrpg/logic--shop": "^0", "@tbrpg/logic--weapons": "^0", + "@tbrpg/state--achievements": "^0", "@tbrpg/state--character": "^0", "@tbrpg/state--inventory": "^0", - "@tbrpg/state--achievements": "^0", "@tbrpg/state--wallet": "^0", "tiny-invariant": "^1", "typescript-string-enums": "^1" diff --git a/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/src/adventure.ts b/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/src/adventure.ts index 2002f479..eab0ac99 100644 --- a/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/src/adventure.ts +++ b/stack--current/A-apps--core/the-boring-rpg/ui--rich-text/src/adventure.ts @@ -1,9 +1,9 @@ import { type Immutable } from '@offirmo-private/ts-types' -import { InventorySlot, ITEM_SLOTS } from '@tbrpg/definitions' -import { CHARACTER_ATTRIBUTES, CharacterAttribute } from '@tbrpg/state--character' +import { type InventorySlot, ITEM_SLOTS } from '@tbrpg/definitions' +import { CHARACTER_ATTRIBUTES, type CharacterAttribute } from '@tbrpg/state--character' import { i18n_messages as I18N_ADVENTURES } from '@tbrpg/logic--adventures' -import { Adventure } from '@tbrpg/state' -import { ALL_CURRENCIES, Currency, get_currency_amount } from '@tbrpg/state--wallet' +import { type ResolvedAdventure } from '@tbrpg/logic--adventure--resolved' +import { ALL_CURRENCIES, type Currency } from '@tbrpg/state--wallet' import * as RichText from '@offirmo-private/rich-text-format' @@ -14,7 +14,7 @@ import { RenderItemOptions } from './types.js' import { DEFAULT_RENDER_ITEM_OPTIONS } from './consts.js' -function render_adventure(a: Immutable, options: Immutable = DEFAULT_RENDER_ITEM_OPTIONS): RichText.Document { +function renderꓽresolved_adventure(a: Immutable, options: Immutable = DEFAULT_RENDER_ITEM_OPTIONS): RichText.Document { const gains: any = a.gains // alias for typing // in this special function, we'll be: @@ -132,9 +132,9 @@ function render_adventure(a: Immutable, options: Immutable !!gains[prop]) const unhandled_adventure_outcomes = active_adventure_outcomes.filter(prop => !handled_adventure_outcomes_so_far.has(prop)) if (unhandled_adventure_outcomes.length) { - console.error(`render_adventure(): *UN*handled outcome properties: "${unhandled_adventure_outcomes}"!`) - console.info(`render_adventure(): handled outcome properties: "${Array.from(handled_adventure_outcomes_so_far.values())}"`) - throw new Error('render_adventure(): unhandled outcome properties!') + console.error(`renderꓽresolved_adventure(): *UN*handled outcome properties: "${unhandled_adventure_outcomes}"!`) + console.info(`renderꓽresolved_adventure(): handled outcome properties: "${Array.from(handled_adventure_outcomes_so_far.values())}"`) + throw new Error('renderꓽresolved_adventure(): unhandled outcome properties!') } /////// Final wrap-up ////// @@ -159,5 +159,5 @@ function render_adventure(a: Immutable, options: Immutable { - const $doc = render_adventure(DEMO_ADVENTURE_01) + const $doc = renderꓽresolved_adventure(DEMO_ADVENTURE_01) //console.log(prettifyꓽjson($doc)) const str = strip_terminal_escape_codes(rich_text_to_terminal($doc)) @@ -34,7 +35,7 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function }) it('should render properly - with gain of coins', () => { - const $doc = render_adventure(DEMO_ADVENTURE_02) + const $doc = renderꓽresolved_adventure(DEMO_ADVENTURE_02) //console.log(prettifyꓽjson($doc)) const str = strip_terminal_escape_codes(rich_text_to_terminal($doc)) @@ -46,7 +47,7 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function }) it('should render properly - with gain of item(s)', () => { - const $doc = render_adventure(DEMO_ADVENTURE_03) + const $doc = renderꓽresolved_adventure(DEMO_ADVENTURE_03) //console.log(prettifyꓽjson($doc)) const str = strip_terminal_escape_codes(rich_text_to_terminal($doc)) @@ -57,7 +58,7 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function }) it('should render properly - with gain of item improvement', () => { - const $doc = render_adventure(DEMO_ADVENTURE_04) + const $doc = renderꓽresolved_adventure(DEMO_ADVENTURE_04) //console.log(prettifyꓽjson($doc)) const str = strip_terminal_escape_codes(rich_text_to_terminal($doc)) @@ -74,11 +75,11 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function ALL_GOOD_ADVENTURE_ARCHETYPES .forEach(({hid, good}, index) => { describe(`✅ adventure #${index} "${hid}"`, function() { - it('should be playable', () => { + it('should be render-able', () => { let state = create() state = play(state, undefined, hid) - const $doc = render_adventure(state.u_state.last_adventure!) + const $doc = renderꓽresolved_adventure(state.u_state.last_adventure!) //console.log(prettifyꓽjson($doc)) // should just not throw @@ -91,7 +92,7 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function ALL_BAD_ADVENTURE_ARCHETYPES .forEach(({hid, good}, index) => { describe(`❎ adventure #${index} "${hid}"`, function() { - it('should be playable', () => { + it('should be render-able', () => { let state = create() state = play(state) @@ -104,11 +105,12 @@ describe('🔠 view to @offirmo-private/rich-text-format - adventure', function state = play(state, undefined, hid) - const $doc = render_adventure(state.u_state.last_adventure!) + const $doc = renderꓽresolved_adventure(state.u_state.last_adventure!) //console.log(prettifyꓽjson($doc)) + + // should just not throw const str = rich_text_to_terminal($doc) //console.log(str) - // should just not throw }) }) })