yasshi2525/RushHour

View on GitHub
client/src/common/model.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import React, { createContext, useEffect, useReducer, useContext } from "react";
import { createAction } from "typesafe-actions";
import * as PIXI from "pixi.js";
import { config } from "interfaces/gamemap";
import { UserInfo } from "interfaces/user";
import { UnhandledError } from "interfaces/error";
import { ComponentProperty } from "interfaces/component";
import AuthContext from "./auth";
import { LoadingCircle } from "./loading";
import GameModel from "models";

const sheets = [
  "cursor",
  "anchor",
  "residence",
  "company",
  "rail_node",
  "rail_edge",
  "destroy"
];

const app = new PIXI.Application({
  width: window.innerWidth,
  height: window.innerHeight,
  backgroundColor: config.background,
  autoStart: true,
  antialias: true,
  resolution: window.devicePixelRatio,
  autoDensity: true
});
app.stage.sortableChildren = true;

const modelOptions = {
  app,
  cx: config.gamePos.default.x,
  cy: config.gamePos.default.y,
  scale: config.scale.default,
  zoom: 0
};

interface LoadingStatus {
  completed: boolean;
  error: UnhandledError | null;
  model: GameModel;
}

const ModelContext = createContext<GameModel>(
  new GameModel({ ...modelOptions, my: 0 })
);

const RESET = "RESET";
const ERR = "ERR";
const OK = "OK";

const reset = createAction(RESET)();
const err = createAction(ERR, (e: UnhandledError) => e)();
const ok = createAction(OK)();
type Actions =
  | ReturnType<typeof reset>
  | ReturnType<typeof err>
  | ReturnType<typeof ok>;

const reducer = (state: LoadingStatus, action: Actions) => {
  switch (action.type) {
    case RESET:
      return { ...state, completed: false };
    case ERR:
      return { ...state, error: action.payload };
    case OK:
      return { ...state, completed: true };
  }
};

const loadImages = (model: GameModel) =>
  new Promise<GameModel>((resolve, reject) => {
    sheets.forEach(key => {
      model.app.loader.add(
        key,
        `assets/bundle/spritesheet/${key}@${Math.floor(
          model.renderer.resolution
        )}x.json`
      );
    });
    model.app.loader.load(() => {
      model.init();
      return resolve(model);
    });
    model.app.loader.onError = () => reject(model);
  });

const useModel = (my?: UserInfo | null) => {
  const [state, dispatch] = useReducer(reducer, {
    completed: false,
    error: null,
    model: new GameModel({
      ...modelOptions,
      my: my ? my.id : 0
    })
  });
  useEffect(() => {
    console.info("effect useModel");
    (async () => {
      await loadImages(state.model).catch(e =>
        dispatch(err(new UnhandledError(e)))
      );
      dispatch(ok());
    })();
    return () => {
      console.info("cleanup useModel");
      state.model.app.loader.reset();
      dispatch(reset());
    };
  }, [my]);
  return [state.completed, state.error, state.model] as [
    boolean,
    UnhandledError | null,
    GameModel
  ];
};

export const ModelProvider = (props: ComponentProperty) => {
  const [[, my]] = useContext(AuthContext);
  const [completed, err, model] = useModel(my);

  if (!completed) {
    return <LoadingCircle />;
  } else if (err) {
    <>
      <div>画像データの読み込みに失敗しました。</div>
      <div>画面を更新してください</div>
      {err?.messages.map(msg => (
        <div>{msg}</div>
      ))}
    </>;
  }
  return (
    <ModelContext.Provider value={model}>
      {props.children}
    </ModelContext.Provider>
  );
};

export default ModelContext;