mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 17:26:47 +08:00
update: hooks, web
This commit is contained in:
@@ -13,9 +13,7 @@ export function parse(condition: string): ConditionTree {
|
||||
const catchString = (i: number): void => {
|
||||
const str = condition.substring(cursor, i).trim()
|
||||
cursor = i
|
||||
if (str) {
|
||||
stack[0]?.push(str)
|
||||
}
|
||||
if (str) stack[0]?.push(str)
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
@@ -118,13 +116,13 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
|
||||
switch (symbol) {
|
||||
case '>':
|
||||
return propData > conditionData
|
||||
return (propData ?? 0) > conditionData
|
||||
case '<':
|
||||
return propData < conditionData
|
||||
return (propData ?? 0) < conditionData
|
||||
case '>=':
|
||||
return propData >= conditionData
|
||||
return (propData ?? 0) >= conditionData
|
||||
case '<=':
|
||||
return propData <= conditionData
|
||||
return (propData ?? 0) <= conditionData
|
||||
case '=':
|
||||
if (propData instanceof Set) {
|
||||
return propData.has(conditionData)
|
||||
@@ -132,7 +130,7 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
if (Array.isArray(propData)) {
|
||||
return propData.includes(conditionData)
|
||||
}
|
||||
return propData == conditionData
|
||||
return (propData ?? 0) == conditionData
|
||||
case '!=':
|
||||
if (propData instanceof Set) {
|
||||
return !propData.has(conditionData)
|
||||
@@ -140,7 +138,7 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
if (Array.isArray(propData)) {
|
||||
return !propData.includes(conditionData)
|
||||
}
|
||||
return propData != conditionData
|
||||
return (propData ?? 0) != conditionData
|
||||
|
||||
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
|
||||
if (propData instanceof Set) {
|
||||
|
||||
@@ -1,29 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "bun test"
|
||||
@@ -15,5 +16,17 @@
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "../../package.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./src/*.ts",
|
||||
"default": "./src/*.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { createState } from './state'
|
||||
import { trigger } from './achievement'
|
||||
import { AchievementOpportunity } from '@remake/data'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
@@ -20,7 +21,11 @@ describe('Achievement', () => {
|
||||
|
||||
test('trigger [times:500]', () => {
|
||||
const p = { ...profile, times: 500 }
|
||||
const result = trigger('END', createState(allocation, []), p)
|
||||
const result = trigger(
|
||||
AchievementOpportunity.End,
|
||||
createState(allocation, []),
|
||||
p,
|
||||
)
|
||||
expect(result.state.achievements).toContain(101) // 既视感
|
||||
expect(result.state.achievements).toContain(102) // 孟婆愁
|
||||
expect(result.state.achievements).toContain(103) // 所有人都是我
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type {
|
||||
Achievement,
|
||||
AchievementOpportunity,
|
||||
} from '@remake/data/achievement'
|
||||
import achievements from '@remake/data/achievement'
|
||||
import type { GameState, ProfileState } from '@/state'
|
||||
import type { Achievement, AchievementOpportunity } from '@remake/data'
|
||||
import { achievements } from '@remake/data'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { createFlatState } from './state'
|
||||
import { check } from '@remake/condition'
|
||||
import { produce } from 'immer'
|
||||
import type { TriggerResult } from '@/game'
|
||||
import type { TriggerResult } from './game'
|
||||
|
||||
const OpportunityMap = Map.groupBy(
|
||||
achievements.keys(),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Event } from '@remake/data/event'
|
||||
import events from '@remake/data/event'
|
||||
import type { Properties } from '@/state'
|
||||
import type { GameState, ProfileState } from '@/state'
|
||||
import { propsEffect, createFlatState } from '@/state'
|
||||
import type { Properties } from './state'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { propsEffect, createFlatState } from './state'
|
||||
import { check as checkCondition } from '@remake/condition'
|
||||
import { produce } from 'immer'
|
||||
import type { TriggerResult } from '@/game'
|
||||
import type { TriggerResult } from './game'
|
||||
|
||||
export function check(
|
||||
event: Event['id'],
|
||||
@@ -30,7 +30,7 @@ export function trigger(
|
||||
draft.events.add(eventId)
|
||||
if (!effect) return
|
||||
if (effect.LIF) draft.life += effect.LIF
|
||||
const pe = {} as Partial<Properties>
|
||||
const pe: Partial<Properties> = {}
|
||||
if (effect.CHR) pe.charm = effect.CHR
|
||||
if (effect.INT) pe.intelligence = effect.INT
|
||||
if (effect.STR) pe.strength = effect.STR
|
||||
|
||||
+26
-39
@@ -1,26 +1,23 @@
|
||||
import ages from '@remake/data/age'
|
||||
import { propsEffect } from '@/state'
|
||||
import { enableMapSet, produce } from 'immer'
|
||||
import type { Achievement, Event, Talent } from '@remake/data'
|
||||
import { ages, AchievementOpportunity as Ao } from '@remake/data'
|
||||
import type { Allocation, GameState, ProfileState } from './state'
|
||||
import { createState, nextProfile, propsEffect } from './state'
|
||||
import { summary as stateSummary } from './state'
|
||||
import type { PullOptions, ReplacementResult } from './talent'
|
||||
import type { AdditionalPoint, AdditionalPoints } from './talent'
|
||||
import { pull, exclude, replacement, additionalPoints } from './talent'
|
||||
import { trigger as ttr } from './talent'
|
||||
import { trigger as atr } from './achievement'
|
||||
import { trigger as etr, check as ec } from './event'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
import { pickWeight } from '@remake/vitex'
|
||||
import { produce, enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
export type * as achievement from '@/achievement'
|
||||
export type * as event from '@/event'
|
||||
export type * as talent from '@/talent'
|
||||
export type * as state from '@/state'
|
||||
import type { GameState, ProfileState } from '@/state'
|
||||
export type { GameState, ProfileState }
|
||||
export interface TriggerResult<T> {
|
||||
state: GameState
|
||||
triggers: T[]
|
||||
}
|
||||
|
||||
export { pull } from '@/talent'
|
||||
// export { summary, nextProfile } from '@/state'
|
||||
|
||||
import { replacement, additionalPoints } from '@/talent'
|
||||
import type { ReplacementResult, AdditionalPoints } from '@/talent'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
|
||||
export interface TalentsPickedResult {
|
||||
talents: ReplacementResult
|
||||
additionalPoints: AdditionalPoints
|
||||
@@ -34,8 +31,6 @@ export function talentsPicked(
|
||||
return { talents: r, additionalPoints: ap }
|
||||
}
|
||||
|
||||
import { createState } from '@/state'
|
||||
|
||||
export interface StartResult {
|
||||
state: GameState
|
||||
achievements: Achievement['id'][]
|
||||
@@ -45,19 +40,9 @@ export function start(
|
||||
...args: Parameters<typeof createState>
|
||||
): StartResult {
|
||||
const state = createState(...args)
|
||||
const ar = achievementTrigger('START', state, profile)
|
||||
const ar = atr(Ao.Start, state, profile)
|
||||
return { state: ar.state, achievements: ar.triggers }
|
||||
}
|
||||
|
||||
import type { Achievement } from '@remake/data/achievement'
|
||||
import type { Event } from '@remake/data/event'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import { trigger as achievementTrigger } from '@/achievement'
|
||||
import { trigger as eventTrigger } from '@/event'
|
||||
import { trigger as talentTrigger } from '@/talent'
|
||||
import { check as eventCheck } from '@/event'
|
||||
import { pickWeight } from '@remake/vitex'
|
||||
|
||||
export interface NextResult {
|
||||
state: GameState
|
||||
age: number
|
||||
@@ -76,13 +61,13 @@ export function next(
|
||||
draft.props = propsEffect(state.props, { age: 1 })
|
||||
})
|
||||
const age = s.props.current.age
|
||||
const tr = talentTrigger(s, profile, rng)
|
||||
const tr = ttr(s, profile, rng)
|
||||
const events = ages
|
||||
.get(age)!
|
||||
.event.filter(([e]) => eventCheck(e, tr.state, profile))
|
||||
.event.filter(([e]) => ec(e, tr.state, profile))
|
||||
const event = pickWeight(events, rng)!
|
||||
const er = eventTrigger(event, tr.state, profile)
|
||||
const ar = achievementTrigger('TRAJECTORY', er.state, profile)
|
||||
const er = etr(event, tr.state, profile)
|
||||
const ar = atr(Ao.Trajectory, er.state, profile)
|
||||
const end = ar.state.life < 1
|
||||
return {
|
||||
state: ar.state,
|
||||
@@ -94,8 +79,6 @@ export function next(
|
||||
}
|
||||
}
|
||||
|
||||
import { summary as stateSummary } from '@/state'
|
||||
|
||||
export interface SummaryResult {
|
||||
state: GameState
|
||||
summary: number
|
||||
@@ -105,12 +88,11 @@ export function summary(
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
): SummaryResult {
|
||||
const ar = achievementTrigger('SUMMARY', state, profile)
|
||||
const ar = atr(Ao.Summary, state, profile)
|
||||
const s = stateSummary(ar.state)
|
||||
return { state: ar.state, summary: s, achievements: ar.triggers }
|
||||
}
|
||||
|
||||
import { nextProfile } from '@/state'
|
||||
export interface EndResult {
|
||||
profile: ProfileState
|
||||
achievements: Achievement['id'][]
|
||||
@@ -121,7 +103,12 @@ export function end(
|
||||
profile: ProfileState,
|
||||
lockedTalent?: Talent['id'],
|
||||
) {
|
||||
const ar = achievementTrigger('END', state, profile)
|
||||
const ar = atr(Ao.End, state, profile)
|
||||
const p = nextProfile(profile, ar.state, lockedTalent)
|
||||
return { profile: p, achievements: ar.triggers }
|
||||
}
|
||||
|
||||
export { pull, exclude }
|
||||
export type { GameState, ProfileState, Allocation, RNG }
|
||||
export type { PullOptions, ReplacementResult }
|
||||
export type { AdditionalPoint, AdditionalPoints }
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@/game'
|
||||
export * from './game'
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { parse, type ConditionNode } from '@remake/condition'
|
||||
|
||||
import achievements from '@remake/data/achievement'
|
||||
import events from '@remake/data/event'
|
||||
import talents from '@remake/data/talent'
|
||||
|
||||
import { achievements, events, talents } from '@remake/data'
|
||||
import type { GameState, ProfileState, Properties } from './state'
|
||||
import { createFlatState, createHLProperties, propsEffect } from './state'
|
||||
import { SupportedFlatStateKeys } from './state'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import type { Event } from '@remake/data/event'
|
||||
import type { Achievement } from '@remake/data/achievement'
|
||||
import type { Achievement, Event, Talent } from '@remake/data'
|
||||
import { produce } from 'immer'
|
||||
import { sum } from '@remake/vitex'
|
||||
import { sum, keys } from '@remake/vitex'
|
||||
|
||||
/** 基础的属性 */
|
||||
export interface Properties {
|
||||
@@ -112,7 +110,7 @@ type FlatMapper<Key extends FlatStateKey> = (
|
||||
state: FlatTarget,
|
||||
) => FlatState[Key]
|
||||
|
||||
const FlatMappers = {
|
||||
const FlatMappers: { [Key in FlatStateKey]: FlatMapper<Key> } = {
|
||||
AGE: state => state.game.props.current.age,
|
||||
CHR: state => state.game.props.current.charm,
|
||||
INT: state => state.game.props.current.intelligence,
|
||||
@@ -139,9 +137,9 @@ const FlatMappers = {
|
||||
ATLT: state => state.profile.talents,
|
||||
AACH: state => state.profile.achievements,
|
||||
SUM: ({ game }) => summary(game),
|
||||
} as { [Key in FlatStateKey]: FlatMapper<Key> }
|
||||
}
|
||||
|
||||
export const SupportedFlatStateKeys = new Set(Object.keys(FlatMappers))
|
||||
export const SupportedFlatStateKeys = new Set(keys(FlatMappers))
|
||||
|
||||
const flatStateHandle = {
|
||||
get<Key extends FlatStateKey>(target: FlatTarget, prop: Key) {
|
||||
@@ -175,7 +173,7 @@ export function propsEffect(hlp: HLProperties, effect: Partial<Properties>) {
|
||||
export function highestProperties(a: Properties, b?: Properties): Properties {
|
||||
if (!b) return { ...a }
|
||||
const result = {} as Properties
|
||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||
for (const key of keys(a)) {
|
||||
result[key] = Math.max(a[key], b[key])
|
||||
}
|
||||
return result
|
||||
@@ -184,7 +182,7 @@ export function highestProperties(a: Properties, b?: Properties): Properties {
|
||||
export function lowestProperties(a: Properties, b?: Properties): Properties {
|
||||
if (!b) return { ...a }
|
||||
const result = {} as Properties
|
||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||
for (const key of keys(a)) {
|
||||
result[key] = Math.min(a[key], b[key])
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -2,8 +2,7 @@ import { expect, test, describe } from 'bun:test'
|
||||
import type { ProfileState } from './state'
|
||||
import type { PullOptions } from './talent'
|
||||
import { pull, exclude, additionalPoints, replacement } from './talent'
|
||||
import { createState } from './state'
|
||||
import talents from '@remake/data/talent'
|
||||
import { talents } from '@remake/data'
|
||||
import { produce } from 'immer'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
import type { Talent, TalentGrade } from '@remake/data/talent'
|
||||
import talents from '@remake/data/talent'
|
||||
import type { Properties } from '@/state'
|
||||
import type { GameState, ProfileState } from '@/state'
|
||||
import type { Talent, TalentGrade } from '@remake/data'
|
||||
import { talents } from '@remake/data'
|
||||
import type { Properties } from './state'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { propsEffect, createFlatState } from './state'
|
||||
import { check } from '@remake/condition'
|
||||
import { pick, pickWeight, type RNG } from '@remake/vitex'
|
||||
import { produce } from 'immer'
|
||||
import type { TriggerResult } from '@/game'
|
||||
import type { TriggerResult } from './game'
|
||||
|
||||
const Grades: TalentGrade[] = [0, 1, 2, 3] as const
|
||||
const GradeMap = Map.groupBy(
|
||||
@@ -16,10 +16,6 @@ const GradeMap = Map.groupBy(
|
||||
|
||||
export const count = talents.size
|
||||
|
||||
export function get(talent: number) {
|
||||
return talents.get(talent)
|
||||
}
|
||||
|
||||
export type PullRate = Map<TalentGrade, number>
|
||||
export type PullRateAddition = {
|
||||
mode: 'add' | 'multiply'
|
||||
@@ -64,7 +60,11 @@ export interface PullOptions {
|
||||
rate: RateOptions
|
||||
}
|
||||
|
||||
export function pull(options: PullOptions, profile: ProfileState, rng?: RNG) {
|
||||
export function pull(
|
||||
options: PullOptions,
|
||||
profile: ProfileState,
|
||||
rng?: RNG,
|
||||
): Talent['id'][] {
|
||||
const rate = Array.from(
|
||||
talentRateWithAddition(options.rate, profile).entries(),
|
||||
)
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
"main": "src/index.ts",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"build": "bunx --bun v-transform@3.0.1 transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" ",
|
||||
"build": "bunx --bun v-transform transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" --map",
|
||||
"lint": "eslint .",
|
||||
"test": "bun test"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/*.ts",
|
||||
"import": "./dist/*.ts"
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
/** 成就稀有度 */
|
||||
export type AchievementGrade = 0 | 1 | 2 | 3
|
||||
export enum AchievementGrade {
|
||||
White = 0,
|
||||
Blue = 1,
|
||||
Purple = 2,
|
||||
Orange = 3,
|
||||
}
|
||||
|
||||
/** 成就触发时机 */
|
||||
export type AchievementOpportunity =
|
||||
| 'START' // 分配完成点数,点击开始新人生后
|
||||
| 'TRAJECTORY' // 每一年的人生经历中
|
||||
| 'SUMMARY' // 人生结束,点击人生总结后
|
||||
| 'END' // 游戏完成,点击重开 重开次数在这之后才会+1
|
||||
export enum AchievementOpportunity {
|
||||
Start = 'START', // 分配完成点数,点击开始新人生后
|
||||
Trajectory = 'TRAJECTORY', // 每一年的人生经历中
|
||||
Summary = 'SUMMARY', // 人生结束,点击人生总结后
|
||||
End = 'END', // 游戏完成,点击重开 重开次数在这之后才会+1
|
||||
}
|
||||
|
||||
/** 成就 */
|
||||
export type Achievement = {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export * from '../dist/achievement'
|
||||
export { default as achievements } from '../dist/achievement'
|
||||
export * from '../dist/age'
|
||||
export { default as ages } from '../dist/age'
|
||||
export * from '../dist/character'
|
||||
export { default as characters } from '../dist/character'
|
||||
export * from '../dist/event'
|
||||
export { default as events } from '../dist/event'
|
||||
export * from '../dist/specialthanks'
|
||||
export { default as specialThanks } from '../dist/specialthanks'
|
||||
export * from '../dist/talent'
|
||||
export { default as talents } from '../dist/talent'
|
||||
@@ -1,5 +1,10 @@
|
||||
/** 天赋稀有度 */
|
||||
export type TalentGrade = 0 | 1 | 2 | 3
|
||||
export enum TalentGrade {
|
||||
White = 0,
|
||||
Blue = 1,
|
||||
Purple = 2,
|
||||
Orange = 3,
|
||||
}
|
||||
|
||||
/** 天赋效果 */
|
||||
export type TalentEffect = {
|
||||
|
||||
@@ -1,29 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./setup-tests.ts"] # 在运行测试前加载环境
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@remake/hooks",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "bunx --bun eslint",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remake/core": "workspace:*",
|
||||
"@remake/data": "workspace:*",
|
||||
"@remake/vitex": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jotai": "^2.20.2",
|
||||
"react": "^19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^20.11.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.18",
|
||||
"eslint-config-react": "^1.1.7"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./src/*.ts",
|
||||
"types": "./src/*.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { GlobalRegistrator } from '@happy-dom/global-registrator'
|
||||
GlobalRegistrator.register()
|
||||
import { afterEach, expect } from 'bun:test'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
|
||||
expect.extend(matchers)
|
||||
|
||||
// Optional: cleans up `render` after each test
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useReplaced } from './talent'
|
||||
import type { Allocation } from '@remake/core'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
import { keys, shuffle, random as frandom } from '@remake/vitex'
|
||||
|
||||
const init: Allocation = { charm: 0, intelligence: 0, strength: 0, money: 0 }
|
||||
const allocAtom = atom<Allocation>(init)
|
||||
|
||||
const alloced = (alloc: Allocation) =>
|
||||
Object.values(alloc).reduce((a, b) => a + b, 0)
|
||||
|
||||
export const usePoints = () => {
|
||||
const { points } = useConfig()
|
||||
const { additionalPoints } = useReplaced()
|
||||
return Math.max(points + additionalPoints.points, 0)
|
||||
}
|
||||
|
||||
export const useLeftPoints = () => {
|
||||
const points = usePoints()
|
||||
const alloc = useAtomValue(allocAtom)
|
||||
return points - alloced(alloc)
|
||||
}
|
||||
|
||||
export const useAllocator = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const [alloc, setAlloc] = useAtom(allocAtom)
|
||||
const allocator = useCallback(
|
||||
(key: keyof Allocation, value: number) => {
|
||||
setAlloc(prev => {
|
||||
const left = total - alloced(prev) + prev[key]
|
||||
const max = Math.min(left, allocate)
|
||||
const final = Math.max(Math.min(max, value), 0)
|
||||
if (prev[key] === final) return prev
|
||||
return { ...prev, [key]: final }
|
||||
})
|
||||
},
|
||||
[allocate, total],
|
||||
)
|
||||
return [alloc, allocator] as const
|
||||
}
|
||||
|
||||
export const usePointRandomizer = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const setAlloc = useSetAtom(allocAtom)
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
const suffled = shuffle(keys(init), rng)
|
||||
let left = total
|
||||
const alloc = { ...init }
|
||||
while (suffled.length) {
|
||||
const key = suffled.pop()!
|
||||
const max = Math.min(allocate, left)
|
||||
const min = Math.max(0, left - suffled.length * allocate)
|
||||
const value = frandom(max, min, rng)
|
||||
alloc[key] = value
|
||||
left -= value
|
||||
}
|
||||
setAlloc(alloc)
|
||||
},
|
||||
[allocate, total],
|
||||
)
|
||||
}
|
||||
|
||||
export const useAllocSubmit = () => {}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions } from '@remake/core'
|
||||
|
||||
export interface Config {
|
||||
pick: number
|
||||
pull: PullOptions
|
||||
points: number
|
||||
allocate: number
|
||||
mode: number
|
||||
}
|
||||
|
||||
export const configAtom = atom<Config | null>(null)
|
||||
|
||||
export const useConfigInject = () => {
|
||||
const setConfig = useSetAtom(configAtom)
|
||||
return useCallback((config: Config) => setConfig(config), [setConfig])
|
||||
}
|
||||
|
||||
export const useConfig = () => {
|
||||
const config = useAtomValue(configAtom)
|
||||
if (!config) throw new Error('Game config is not set')
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './config'
|
||||
export * from './profile'
|
||||
export * from './talent'
|
||||
export * from './play'
|
||||
export * from './alloc'
|
||||
export type * from '@remake/core'
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTransition, useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue, useAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { end } from '@remake/core'
|
||||
import type { GameState } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
|
||||
export enum Step {
|
||||
Idle = 'idle',
|
||||
Mode = 'mode',
|
||||
Pick = 'pick',
|
||||
Alloc = 'alloc',
|
||||
Play = 'play',
|
||||
Summary = 'summary',
|
||||
}
|
||||
|
||||
const stepAtom = atom<Step>(Step.Idle)
|
||||
const gameStateAtom = atom<GameState | null>(null)
|
||||
|
||||
export const useStep = () => useAtomValue(stepAtom)
|
||||
export const useSetStep = () => useSetAtom(stepAtom)
|
||||
|
||||
export const useRemake = () => {
|
||||
const config = useConfig()
|
||||
const [profile] = useProfile()
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
return useCallback(() => {
|
||||
const step = profile.times < config.mode ? Step.Pick : Step.Mode
|
||||
setStep(step)
|
||||
}, [config, profile, setStep])
|
||||
}
|
||||
|
||||
export const useEnd = () => {
|
||||
const [profile, setProfile] = useProfile()
|
||||
const [step, setStep] = useAtom(stepAtom)
|
||||
const [state, setState] = useAtom(gameStateAtom)
|
||||
const [pending, startTransition] = useTransition()
|
||||
const ender = useCallback(
|
||||
(talent?: Talent['id']) => {
|
||||
startTransition(async () => {
|
||||
if (!state || step === Step.Summary)
|
||||
throw new Error(
|
||||
'Game state is not available or already ended.',
|
||||
)
|
||||
const { profile: next, achievements } = end(
|
||||
state,
|
||||
profile,
|
||||
talent,
|
||||
)
|
||||
setStep(Step.Idle)
|
||||
setState(null)
|
||||
setProfile(next)
|
||||
})
|
||||
},
|
||||
[state, step, profile, setStep, setState, setProfile],
|
||||
)
|
||||
return [pending, ender] as const
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { screen, renderHook } from '@testing-library/react'
|
||||
import { useProfile, useProfileInject } from './profile'
|
||||
|
||||
describe('profile', () => {
|
||||
test('inject', () => {
|
||||
expect(() => renderHook(() => useProfile())).toThrow()
|
||||
// const inject = renderHook(() => useProfileInject())
|
||||
})
|
||||
test('no inject throw', () => {
|
||||
expect(() => renderHook(() => useProfile())).toThrow()
|
||||
// const inject = renderHook(() => useProfileInject())
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useSetAtom } from 'jotai'
|
||||
import type { ProfileState } from '@remake/core'
|
||||
|
||||
const profileAtom = atom<ProfileState | null>(null)
|
||||
|
||||
export const useProfile = () => {
|
||||
const [profile, setProfile] = useAtom(profileAtom)
|
||||
if (!profile) throw new Error('Profile is not loaded')
|
||||
return [profile, setProfile] as const
|
||||
}
|
||||
|
||||
export const useProfileInject = () => {
|
||||
const setProfile = useSetAtom(profileAtom)
|
||||
return useCallback(
|
||||
(profile: ProfileState) => setProfile(profile),
|
||||
[setProfile],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { useSetStep, Step } from './play'
|
||||
import { pull, exclude, talentsPicked } from '@remake/core'
|
||||
import type { TalentsPickedResult, RNG } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
|
||||
const pickedAtom = atom(new Set<Talent['id']>())
|
||||
const replacedAtom = atom<TalentsPickedResult | null>(null)
|
||||
|
||||
export const usePicked = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
if (!picked) throw new Error('No talents picked')
|
||||
return picked
|
||||
}
|
||||
export const useReplaced = () => {
|
||||
const replaced = useAtomValue(replacedAtom)
|
||||
if (!replaced) throw new Error('No talents replaced')
|
||||
return replaced
|
||||
}
|
||||
|
||||
export const useTalentPuller = () => {
|
||||
const { pull: p } = useConfig()
|
||||
const [profile] = useProfile()
|
||||
const [pulled, setPulled] = useState<Talent['id'][] | null>(null)
|
||||
const puller = useCallback(
|
||||
(rng?: RNG) => setPulled(pull(p, profile, rng)),
|
||||
// (rng?: RNG) =>
|
||||
// setPulled([
|
||||
// 1142, 1143, 1144, 1145, 1146, 1086, 1122, 1111, 1130, 1048,
|
||||
// ]),
|
||||
[p, profile, setPulled],
|
||||
)
|
||||
return [pulled, puller] as const
|
||||
}
|
||||
|
||||
export type TalentPickerResult =
|
||||
| {
|
||||
type: 'ok'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'not-enough'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'exclude'
|
||||
talent: Talent['id']
|
||||
}
|
||||
|
||||
export const useTalentPicker = () => {
|
||||
const { pick } = useConfig()
|
||||
const [picked, setPicked] = useAtom(pickedAtom)
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']): TalentPickerResult => {
|
||||
if (!picked) {
|
||||
setPicked(new Set([talent]))
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.has(talent)) {
|
||||
const next = new Set(picked)
|
||||
next.delete(talent)
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.size >= pick) return { type: 'not-enough' }
|
||||
const e = exclude(talent, picked)
|
||||
if (e) return { type: 'exclude', talent: e }
|
||||
const next = new Set([...picked, talent])
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
},
|
||||
[pick, picked, setPicked],
|
||||
)
|
||||
return [picked, picker] as const
|
||||
}
|
||||
|
||||
export const useSubmitIsEnable = () => {
|
||||
const { pick } = useConfig()
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const enabled = picked.size >= pick
|
||||
return [enabled, pick] as const
|
||||
}
|
||||
|
||||
export const useTalentSubmit = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const setReplaced = useSetAtom(replacedAtom)
|
||||
const [enabled] = useSubmitIsEnable()
|
||||
const next = useSetStep()
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
if (!enabled) throw new Error('Not enough talents picked')
|
||||
setReplaced(talentsPicked(picked, rng))
|
||||
next(Step.Alloc)
|
||||
},
|
||||
[enabled, picked, setReplaced, next],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["react", "bun"],
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -22,3 +22,18 @@ export function pickWeight<T>(items: WeightItem<T>[], rng?: RNG) {
|
||||
export function sum(arr: number[]) {
|
||||
return arr.reduce((sum, v) => sum + v, 0)
|
||||
}
|
||||
|
||||
export function shuffle<T>(array: T[], rng?: RNG): T[] {
|
||||
const result = [...array]
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = random(i, 0, rng)
|
||||
const temp = result[i]
|
||||
result[i] = result[j]!
|
||||
result[j] = temp!
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function keys<T extends object>(obj: T): (keyof T)[] {
|
||||
return Object.keys(obj) as (keyof T)[]
|
||||
}
|
||||
|
||||
@@ -1,29 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user