Open-source interactive fiction engine, powered by React, Redux and TypeScript. _ https://discoteque.pub/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

122 lines
3.9 KiB

import React, { FC, useState, useMemo } from 'react';
import * as R from 'ramda';
import * as styles from './SkillTree.styles';
import { useDispatch, useSelector } from 'react-redux';
import { stateSelector, setSkill, setSkillpoints, setState } from '../engine/lib/store';
import { EngineState } from '../engine/types';
import cn from 'classnames';
type Props = {
skillDescriptions?: Record<string, string>;
}
export const SkillTree: FC<Props> = <GS, MT, ST extends string>({ skillDescriptions = {} }: Props) => {
const dispatch = useDispatch();
const engineState = useSelector(stateSelector) as unknown as EngineState<GS, MT, ST>;
const pointsLeft = engineState.skillPoints;
const skills: ST[] = Object.keys(engineState.skills) as ST[];
const minSkills: Record<ST, number> = useMemo(() => {
if (engineState.protectSkills) {
return engineState.skills;
}
return {} as any;
}, [engineState.protectSkills]);
const [skillOpen, setSkillOpen] = useState<ST | null>(null);
const blockDecrement = (skill: ST) => {
const minSkill = R.propOr(0, skill, minSkills) as number;
return engineState.skills[skill] <= minSkill;
}
const incrementSkill = (skill: ST) => () => {
const oldValue = engineState.skills[skill];
const newValue = oldValue + 1;
if (pointsLeft) {
dispatch(setSkillpoints(pointsLeft - 1))
dispatch(setSkill(skill, newValue));
}
}
const decrementSkill = (skill: ST) => () => {
const oldValue = engineState.skills[skill];
const newValue = oldValue - 1;
const minSkill = R.propOr(0, skill, minSkills) as number;
if (newValue >= 1 && (newValue >= minSkill)) {
dispatch(setSkillpoints(pointsLeft + 1))
dispatch(setSkill(skill, newValue));
}
}
const confirmSkills = () => {
const confirm = pointsLeft ? window.confirm('You have unspent skill poinst. Really confirm?') : true;
if (confirm) {
dispatch(setState({ ui: { skillsOpen: false } }))
}
}
const toggleSkillOpen = (name: ST) => () => {
if (skillOpen === name) {
setSkillOpen(null);
} else {
setSkillOpen(name);
}
}
const getDescription = (name: ST): string | undefined => {
return skillDescriptions[name];
}
return (
<div className={styles.skillTree}>
<div className={styles.skillHeader}>
<h1>Skills <small>Allocate skill points</small></h1>
<div>Points left: {pointsLeft}</div>
</div>
<ul className={styles.skillList}>
{skills.map(skill => (
<li key={skill} className={styles.skillItem}>
<div className={styles.skillRow}>
<a
className={cn(styles.skillName, { [styles.skillNameClick]: !!getDescription(skill) })}
onClick={toggleSkillOpen(skill)}
>
{skill}
{!!getDescription(skill) && ' (?)'}
</a>
<div>
<button onClick={decrementSkill(skill)} className={cn(
styles.skillButton, {
[styles.disabled]: blockDecrement(skill) || (engineState.skills[skill] === 1),
}
)}>-</button>
<span className={styles.skillValue}>{engineState.skills[skill]}</span>
<button onClick={incrementSkill(skill)} className={cn(
styles.skillButton, {
[styles.disabled]: !pointsLeft,
}
)}>+</button>
</div>
</div>
{!!skillDescriptions[skill] && (
<div className={cn(styles.skillDesc, {
[styles.skillDescClosed]: skillOpen !== skill,
[styles.skillDescOpen]: skillOpen === skill,
})}>
{getDescription(skill)}
</div>
)}
</li>
))}
</ul>
<button className={styles.skillConfirm} onClick={confirmSkills}>
DECIDE
</button>
</div>
);
}
export default SkillTree;