0.2.3 • Published 5 years ago

dynamic-story v0.2.3

Weekly downloads
23
License
-
Repository
github
Last release
5 years ago

React Dynamic Story β

Beta version of a minimalist react game framework for dynamic story telling.

npm install dynamic-story -S

Get started

RDS works with redux. First, we need to combined the module reducer with the main reducer:

// './reducer.js'

import { combineReducers } from 'redux';
import { reducer as dynamicStory } from 'dynamic-story';

export default combineReducers({
  dynamicStory, // name is important, don't change
  // ...yourOtherReducers
});

Then we provide store to our new App as described in react-redux documentation.

// './index.js'

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';

import reducer from './reducer';
// import MyStory from './components/MyStory';

const middlewares = [thunk];
const store = createStore(reducer, compose(applyMiddleware(...middlewares)));

const App = () => (
  <Provider store={store}>
    {/* <MyStory /> */}
  </Provider>
);

ReactDOM.render(<App />, document.getElementById('root'));

We are ready to create our <Story /> component and display our first story Element: a <Card />

// './components/MyStory.js'

import React from 'react';
import { Story, Card } from 'dynamic-story';

const MyStory = () => (
  <Story id="my-story">
    <Card
      text="
        When Mr. and Mrs. Dursley woke up on the dull, gray Tuesday our story
        starts, there was nothing about the cloudy sky outside to suggest that
        strange and mysterious things would soon be happening all over the country.
      "
    />
  </Story>
);

export default MyStory;

Wow, that's pretty ugly, is it normal ? Yes, there's no style in this module, but RDS works well with bootstrap and animate.css. See how to implements styles. See also the Card Component(#Card component).

Then, we could connect() our Story component in order to dispatch actions and go forward in the story.

// './components/MyStory.js'

import React from 'react';
import { connect } from 'react-redux';
import { Story, Card, goForward, goTo } from 'dynamic-story';

const MyStory = ({ dispatch }) => (
  <Story id="my-story">
    <Card text="Text n°0" onTimeout={() => dispatch(goForward())} timeout={5000} />
    <Card text="Text n°1" onTimeout={() => dispatch(goTo(3))} timeout={2000} />
    <Card text="Text n°2" />
    <Card text="Text n°3" />
  </Story>
);

export default connect()(MyStory);

With this example, Card with text n°0 will be revealed first and after a 5 seconds delay, the Card coming right after is going to be shown. Then, after 2 seconds, history will go forward directly to text n°3 skipping n°2.

Element's order is important ! Indexes are used as unique ids in the history state.

Fragments

Your story may required a lot of elements like <Card /> and more. To preserve understanding of your schema, you can divide parts of the story into fragments and use them directly in <Story > ...your fragments </ Story>

// './components/fragments/Intro.js'

import React from 'react';
import PropTypes from 'prop-types';
import readingTime from 'reading-time';
import { Card, goForward } from 'dynamic-story';

import image from '../assets/image/test.jpg';

const Fragment = ({ dispatch }) => (
  <>
    <Card
      isSkipAble
      onTimeout={() => dispatch(goForward())}
      text="Lorem Ipsum"
      timeout={({ text }) => readingTime(text).time}
    />
    <Card
      isSkipAble
      onTimeout={() => dispatch(goForward())}
      text="Dolor sit amet"
      timeout={({ text }) => readingTime(text).time}
    />
  </>
);

Fragment.propTypes = { dispatch: PropTypes.func.isRequired };

export default Fragment;
);
// './components/MyStory.js'

import React from 'react';
import { connect } from 'react-redux';
import { Story, Card, actions } from 'dynamic-story';

import Intro from './components/fragments/Intro';

const MyStory = (props) => (
  <Story id="my-story">
    {Intro(props)}
    <Card text="Some text coming after intro" />
  </Story>
);


export default connect()(MyStory);

API

Story component

<Story /> is the main component where you can load all your story elements.

Story.propTypes = {
  className: PropTypes.string,
  children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]).isRequired,
  current: PropTypes.number.isRequired,
  darkMode: PropTypes.bool,
  history: PropTypes.arrayOf(PropTypes.object).isRequired,
};
<Story id="myStory" className={classNames('styled', { 'bg-dark': context.isDark })}>
  {Intro(props)}
  <Card className="text-center" text="End" onClick={() => dispatch(resetStory())} />
</Story>

You can set Debug mode using <Debug /> component instead of <Story />.

Here the full demo example:

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connect } from 'react-redux';

import {
  Card,
  Debug,
  DebugButton,
  ErrorBoundary,
  getSchema,
  GoBackward,
  Header,
  resetStory,
  setContext,
  Story,
} from 'dynamic-story';

import Intro from './fragments/Intro';
import Choice1 from './fragments/Choice1';

import initialContext from './context';
import banner from './assets/image/logo.png';

const Content = props => (
  <>
    <Header
      className="border-0 bg-transparent"
      banner={{ src: banner, alt: 'React Dynamic Story Banner' }}
    />
    {Intro(props)}
    {Choice1(props)}
    <Card
      className="text-center"
      text="End"
      onClick={() => props.dispatch(resetStory())}
      title="End"
      category="End"
      comment="This is End"
      tags={['end', 'game over']}
    />
  </>
);

const MyStory = (props) => {
  const { context, dispatch, settings } = props;
  const { darkMode, debug } = settings;
  const schema = getSchema(Content(props));

  React.useEffect(() => { if (!context) dispatch(setContext(initialContext)); }, [context]);

  return context && (
    <>
      <ErrorBoundary>
        {React.createElement(
          debug ? Debug : Story,
          { id: 'myStory', className: classNames('styled', { 'bg-darker': darkMode }) },
          Content({ ...props, schema }),
        )}
      </ErrorBoundary>
      {(!debug && process.env.NODE_ENV === 'development') && (
        <div className="fixed-top m-2 text-right">
          <GoBackward className={classNames('btn-sm mr-2', { 'btn-dark': darkMode, 'btn-light': !darkMode })} />
          <DebugButton className={classNames('btn-sm mr-2', { 'btn-dark': darkMode, 'btn-light': !darkMode })} />
        </div>
      )}
    </>
  );
};

MyStory.propTypes = {
  dispatch: PropTypes.func.isRequired,
  context: PropTypes.objectOf(PropTypes.any),
  settings: PropTypes.shape({ debug: PropTypes.bool }),
};

MyStory.defaultProps = {
  context: null,
  settings: {},
};

export default connect(state => ({ ...state.dynamicStory }))(MyStory);

Card component

<Card /> is story element template implementing useful props and behaviors.

Card.propTypes = {
  animationEntrance: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
  children: PropTypes.oneOfType([
    PropTypes.array,
    PropTypes.element,
    PropTypes.number,
    PropTypes.string,
  ]),
  choices: PropTypes.objectOf(PropTypes.shape({
    className: PropTypes.string,
    text: PropTypes.string.isRequired,
    onClick: PropTypes.func.isRequired,
  })),
  className: PropTypes.string,
  darkMode: PropTypes.bool,
  debug: PropTypes.bool,
  disabled: PropTypes.bool,
  index: PropTypes.number,
  isSkipAble: PropTypes.bool,
  onClick: PropTypes.func,
  onKeyPress: PropTypes.func,
  onReveal: PropTypes.func,
  onTimeout: PropTypes.func,
  tabIndex: PropTypes.number,
  text: PropTypes.string,
  timeout: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
};

Card.defaultProps = {
  animationEntrance: 'animated fadeIn',
  children: undefined,
  choices: null,
  className: '',
  darkMode: false,
  debug: false,
  disabled: false,
  index: null,
  isSkipAble: false,
  onClick: null,
  onKeyPress: null,
  onReveal: null,
  onTimeout: null,
  tabIndex: null,
  text: undefined,
  timeout: 0,
};
import React from 'react';
import PropTypes from 'prop-types';
import { Card, goForward, updateContext, updateSettings } from '../lib';

const Fragment = ({ dispatch, context }) => {
  const choices = {
    wait: {
      className: 'btn btn-link text-white',
      text: 'Wait',
      onClick: () => dispatch(goForward()),
    },
    turnOfTheLight: {
      className: 'btn btn-link text-white',
      text: 'Turn of the light',
      onClick: () => {
        dispatch(updateContext({ environment: { ...context.environment, isLightOn: false } }));
        dispatch(updateSettings({ darkMode: true }));
        dispatch(goForward());
      },
    },
  };

  return (
    <>
      <Card choices={choices} className="text-white bg-dark">
        <div className="card-body">
          <h5 className="card-title">Time to choose</h5>
          <p className="card-text">
            Which are the posibilities ?
            You can wait for someone to come
            or press the button and turn of the light.
          </p>
        </div>
      </Card>
    </>
  );
};

Fragment.propTypes = {
  dispatch: PropTypes.func.isRequired,
  context: PropTypes.objectOf(PropTypes.any).isRequired,
};

export default Fragment;

Header component

<Header /> is story element template extending <Card />.

Header.propTypes = {
  banner: PropTypes.shape({
    src: PropTypes.string.isRequired,
    alt: PropTypes.string.isRequired,
  }),
  children: PropTypes.arrayOf(PropTypes.element),
  className: PropTypes.string,
};

Header.defaultProps = {
  banner: null,
  className: '',
  children: undefined,
};
<Header banner={{ src: banner, alt: 'React Dynamic Story Banner' }} />

setContext action

ArgTypeExample
contextobject{ environment: { light: 'off' } }

Override context reducer.

updateContext action

ArgTypeExample
contextobject{ test: 'test' }

Merge with context reducer.

setCurrent action

ArgTypeExample
currentnumber6

Override current reducer.

setHistory action

ArgTypeExample
historyarray[{ from: 0, to: 1, context: {} }]

Override history reducer.

setSettings action

ArgTypeExample
settingsobject{ darkMode: true }

Override settings reducer.

updateSettings action

ArgTypeExample
settingsobject{ darkMode: true }

Merge settings reducer.

resetStory action

Reset all root (dynamicStory) reducer and delete storage if any (work great with redux-persist);

goTo action

ArgTypeExample
fromnumber0
tonumber1
contextobject{ light: 'off' }

Go from current element (from) to a specific story element index (to) and save current context.

goBackwardTo action

ArgTypeExample
indexnumber1
contextobjectundefined
tonumber4

Go backward to a specific history iteration index or a story element in the history to and reload context.

goForward action

ArgTypeExample
skipnumber0
contextobjectundefined

Go forward in story elements order and skip certain elements. Also save current context in history state.

goBackward action

ArgTypeExample
skipnumber0

Go backward in story elements order and skip certain elements.

Styling

RDS works well with bootstrap and animate.css.

npm install node-sass bootstrap animate.css -S

If your're in a create-react-app project, there are no configuration needed, otherwise, you might add a sass-loader to your bundle manager (Webpack).

Create a bootstrap Sass file and import only what's needed:

// './assets/style/bootstrap.scss'
$theme-colors: (
  "black": #000,
);

// Required
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";

// Optional
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/utilities";

Same with animate.css:

// './assets/style/animate.scss'

// Required
@import "~animate.css/source/_base.css";

// Optional
@import "~animate.css/source/fading_entrances/fadeIn.css";
@import "~animate.css/source/bouncing_entrances/bounceIn.css";

Then import Sass files in your JS, Webpack will do the rest:

// './index.js'

import React from 'react';
// ... Imports

import './assets/style/bootstrap.scss';
import './assets/style/animate.scss';
import './index.scss';

Troubleshooting

First, make sure to install all dependencies if npm has failed doing it:

npm install classnames classnamesprop-types react react-children-addons react-dom react-redux redux redux-thunk -S

TODO

  • Accessibility and focus
  • <Header /> template component
  • Mode debug for <Story />
  • Persit/Save demo
  • Quick Time Event plugin
  • X Box Controller support

License

This project is licensed under the MIT License - see the LICENSE file for details.