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

  1. import React, { FC, useState, useMemo } from 'react';
  2. import * as R from 'ramda';
  3. import * as styles from './SkillTree.styles';
  4. import { useDispatch, useSelector } from 'react-redux';
  5. import { stateSelector, setSkill, setSkillpoints, setState } from '../engine/lib/store';
  6. import { EngineState } from '../engine/types';
  7. import cn from 'classnames';
  8. type Props = {
  9. skillDescriptions?: Record<string, string>;
  10. }
  11. export const SkillTree: FC<Props> = <GS, MT, ST extends string>({ skillDescriptions = {} }: Props) => {
  12. const dispatch = useDispatch();
  13. const engineState = useSelector(stateSelector) as unknown as EngineState<GS, MT, ST>;
  14. const pointsLeft = engineState.skillPoints;
  15. const skills: ST[] = Object.keys(engineState.skills) as ST[];
  16. const minSkills: Record<ST, number> = useMemo(() => {
  17. if (engineState.protectSkills) {
  18. return engineState.skills;
  19. }
  20. return {} as any;
  21. }, [engineState.protectSkills]);
  22. const [skillOpen, setSkillOpen] = useState<ST | null>(null);
  23. const blockDecrement = (skill: ST) => {
  24. const minSkill = R.propOr(0, skill, minSkills) as number;
  25. return engineState.skills[skill] <= minSkill;
  26. }
  27. const incrementSkill = (skill: ST) => () => {
  28. const oldValue = engineState.skills[skill];
  29. const newValue = oldValue + 1;
  30. if (pointsLeft) {
  31. dispatch(setSkillpoints(pointsLeft - 1))
  32. dispatch(setSkill(skill, newValue));
  33. }
  34. }
  35. const decrementSkill = (skill: ST) => () => {
  36. const oldValue = engineState.skills[skill];
  37. const newValue = oldValue - 1;
  38. const minSkill = R.propOr(0, skill, minSkills) as number;
  39. if (newValue >= 1 && (newValue >= minSkill)) {
  40. dispatch(setSkillpoints(pointsLeft + 1))
  41. dispatch(setSkill(skill, newValue));
  42. }
  43. }
  44. const confirmSkills = () => {
  45. const confirm = pointsLeft ? window.confirm('You have unspent skill poinst. Really confirm?') : true;
  46. if (confirm) {
  47. dispatch(setState({ ui: { skillsOpen: false } }))
  48. }
  49. }
  50. const toggleSkillOpen = (name: ST) => () => {
  51. if (skillOpen === name) {
  52. setSkillOpen(null);
  53. } else {
  54. setSkillOpen(name);
  55. }
  56. }
  57. const getDescription = (name: ST): string | undefined => {
  58. return skillDescriptions[name];
  59. }
  60. return (
  61. <div className={styles.skillTree}>
  62. <div className={styles.skillHeader}>
  63. <h1>Skills <small>Allocate skill points</small></h1>
  64. <div>Points left: {pointsLeft}</div>
  65. </div>
  66. <ul className={styles.skillList}>
  67. {skills.map(skill => (
  68. <li key={skill} className={styles.skillItem}>
  69. <div className={styles.skillRow}>
  70. <a
  71. className={cn(styles.skillName, { [styles.skillNameClick]: !!getDescription(skill) })}
  72. onClick={toggleSkillOpen(skill)}
  73. >
  74. {skill}
  75. {!!getDescription(skill) && ' (?)'}
  76. </a>
  77. <div>
  78. <button onClick={decrementSkill(skill)} className={cn(
  79. styles.skillButton, {
  80. [styles.disabled]: blockDecrement(skill) || (engineState.skills[skill] === 1),
  81. }
  82. )}>-</button>
  83. <span className={styles.skillValue}>{engineState.skills[skill]}</span>
  84. <button onClick={incrementSkill(skill)} className={cn(
  85. styles.skillButton, {
  86. [styles.disabled]: !pointsLeft,
  87. }
  88. )}>+</button>
  89. </div>
  90. </div>
  91. {!!skillDescriptions[skill] && (
  92. <div className={cn(styles.skillDesc, {
  93. [styles.skillDescClosed]: skillOpen !== skill,
  94. [styles.skillDescOpen]: skillOpen === skill,
  95. })}>
  96. {getDescription(skill)}
  97. </div>
  98. )}
  99. </li>
  100. ))}
  101. </ul>
  102. <button className={styles.skillConfirm} onClick={confirmSkills}>
  103. DECIDE
  104. </button>
  105. </div>
  106. );
  107. }
  108. export default SkillTree;