10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/domain/utils/arrayUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function pickRandom(arr: T[]): T { 4 | return arr[~~(Math.random() * arr.length)] 5 | } 6 | 7 | export function updateIn( 8 | arr: T[], 9 | matcher: T => boolean, 10 | replacer: T => T 11 | ): T[] { 12 | return arr.map(i => (matcher(i) ? replacer(i) : i)) 13 | } 14 | -------------------------------------------------------------------------------- /src/client/reducers/setup.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { SetupAction as Action } from '../actions/setupActions' 3 | 4 | export type State = {} 5 | const initialState: State = {} 6 | 7 | export default (state: State = initialState, action: Action) => { 8 | switch (action.type) { 9 | default: 10 | return state 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/__mocks/resourceMock.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Resource } from '../entities/Resource' 3 | 4 | export const resourcesMock0: Resource[] = [ 5 | { 6 | resourceId: '$gold', 7 | resourceName: 'gold', 8 | amount: 0 9 | }, 10 | { 11 | resourceId: '$energy', 12 | resourceName: 'energy', 13 | amount: 0 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/domain/battle/Command.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { BattleSession } from './BattleSession' 3 | import type { CommandResult } from './CommandResult' 4 | 5 | export type Command = BattleSession => CommandApplicationProgress 6 | 7 | export type CommandApplicationProgress = { 8 | session: BattleSession, 9 | commandResults: CommandResult[] 10 | } 11 | -------------------------------------------------------------------------------- /src/domain/entities/Actor.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { SkillId } from 'domain/master' 3 | 4 | export type Actor = { 5 | id: string, 6 | displayName: string, 7 | controllable: boolean, 8 | lifeValue: number, 9 | acquiredSkills: AcquiredSkill[] 10 | } 11 | 12 | export type AcquiredSkill = { 13 | skillId: SkillId, 14 | lv: number 15 | } 16 | -------------------------------------------------------------------------------- /src/client/sagas/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import battleSaga from './battleSaga' 3 | import adventureSaga from './adventureSaga' 4 | import playingSaga from './playingSaga' 5 | import { fork } from 'redux-saga/effects' 6 | 7 | export default function* rootSaga(): any { 8 | yield fork(battleSaga) 9 | yield fork(adventureSaga) 10 | yield fork(playingSaga) 11 | } 12 | -------------------------------------------------------------------------------- /masterdata/schema/troop-schema.yml: -------------------------------------------------------------------------------- 1 | required: 2 | - id 3 | - monsters 4 | properties: 5 | id: 6 | type: string 7 | monsters: 8 | type: array 9 | item: 10 | required: 11 | - monster_id 12 | - displayName 13 | properties: 14 | monster_id: 15 | type: 'string' 16 | displayName: 17 | type: 'string' 18 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // window.addEventListener('error', (event: any) => { 4 | // debugger 5 | // console.error(event) 6 | // }) 7 | // 8 | // window.addEventListener('unhandledrejection', (event: any) => { 9 | // debugger 10 | // console.error( 11 | // `Unhandled rejection (promise: ${event.promise}, reason: ${event.reason}.` 12 | // ) 13 | // }) 14 | -------------------------------------------------------------------------------- /src/domain/__mocks/playingSessionMock.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import type { PlayingSession } from '../sessions/PlayingSession' 4 | import { resourcesMock0 } from './resourceMock' 5 | 6 | export const playingSessionMock0: PlayingSession = { 7 | id: uuid(), 8 | savedataId: uuid(), 9 | playerName: 'Player1$playingSessionMock', 10 | resources: resourcesMock0 11 | } 12 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/flow.* 3 | .*/node_modules/babel-plugin-flow.* 4 | .*/node_modules/stylelint.* 5 | .*/node_modules/styled-components.* 6 | 7 | [include] 8 | 9 | [libs] 10 | ./flow-typed 11 | 12 | [options] 13 | module.ignore_non_literal_requires=true 14 | esproposal.decorators=ignore 15 | module.system.node.resolve_dirname=node_modules 16 | module.system.node.resolve_dirname=src 17 | -------------------------------------------------------------------------------- /src/domain/__mocks/savedataMock.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Savedata } from '../entities/Savedata' 3 | 4 | export const savedataListForTest: Savedata[] = [ 5 | { 6 | id: '$save1', 7 | playerName: 'Player1', 8 | ownedShells: [], 9 | resources: [] 10 | }, 11 | { 12 | id: '$save2', 13 | playerName: 'Player2', 14 | ownedShells: [], 15 | resources: [] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - mizchi 3 | plugins: 4 | - mutation 5 | settings: 6 | import/resolver: 7 | node: 8 | moduleDirectory: 9 | - node_modules # defaults to 'node_modules', but... 10 | - src 11 | rules: 12 | import/prefer-default-export: 0 13 | # mutation 14 | mutation/no-mutation: 15 | - 2 16 | - 17 | execptions: 18 | this 19 | exports 20 | -------------------------------------------------------------------------------- /src/domain/battle/BattleSessionResult.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Resource } from 'domain/entities/Resource' 3 | 4 | export type BattleSessionReward = { 5 | resources: Resource[] 6 | } 7 | 8 | export type BattleSessionResult = 9 | | { 10 | winner: 'ally', 11 | rewards: BattleSessionReward 12 | } 13 | | { 14 | winner: 'enemy' 15 | } 16 | | { 17 | winner: 'escaped' 18 | } 19 | -------------------------------------------------------------------------------- /src/client/reducers/battle/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { combineReducers } from 'redux' 3 | import runner from './runner' 4 | import log from './log' 5 | 6 | import type { State as RunnerState } from './runner' 7 | import type { State as LogState } from './log' 8 | 9 | export type State = { 10 | runner: RunnerState, 11 | log: LogState 12 | } 13 | 14 | export default combineReducers({ 15 | runner, 16 | log 17 | }) 18 | -------------------------------------------------------------------------------- /src/client/components/organisms/SetupScene.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { lifecycle } from 'recompose' 4 | import type { SetupContainerProps } from '../../containers/SetupContainer' 5 | 6 | export default lifecycle({ 7 | componentDidMount() { 8 | // console.log('didmount') 9 | } 10 | })(function SetupScene(_props: SetupContainerProps) { 11 | return ( 12 |
13 |

Setup

14 |
15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/client/components/atoms/Button.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | // import { StyleSheet, css } from 'aphrodite' 4 | import { Button as SemanticButton } from 'semantic-ui-react' 5 | 6 | export default function Button(props: { label: string, onClick: Function }) { 7 | return ( 8 | { 10 | props.onClick(ev) 11 | }} 12 | > 13 | {props.label} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /masterdata/data/monster-data.yml: -------------------------------------------------------------------------------- 1 | - 2 | id: $goblin 3 | displayName: Goblin 4 | displayImage: assets/EnemyGraphic/GD_Goblin(Green).png 5 | life: 30 6 | skills: 7 | - 8 | skillId: $attack 9 | lv: 1 10 | - 11 | id: $hob-goblin 12 | displayName: Hob Goblin 13 | displayImage: assets/EnemyGraphic/GD_Goblin(Red).png 14 | life: 50 15 | skills: 16 | - 17 | skillId: $attack 18 | lv: 1 19 | - 20 | skillId: $power-attack 21 | lv: 1 22 | -------------------------------------------------------------------------------- /src/client/actions/battleSagaActions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { BattleSession } from '../../domain/battle' 3 | 4 | // Constants 5 | export const SYNC = 'battel-saga/sync' 6 | 7 | // Action 8 | export type SyncAction = { 9 | type: typeof SYNC, 10 | payload: BattleSession 11 | } 12 | 13 | export type BattleSagaAction = SyncAction 14 | 15 | // Action creator 16 | export const sync = (session: BattleSession): SyncAction => ({ 17 | type: SYNC, 18 | payload: session 19 | }) 20 | -------------------------------------------------------------------------------- /docs/SkillExecTypeSpecs.md: -------------------------------------------------------------------------------- 1 | # スキル実行タイプ: SkillExecType 2 | 3 | ## 即時: Instant 4 | 5 | クールダウンで1以上なら、スキルが選択された時、即時に発動する。最大3周し、実行係数がかかる。 6 | 7 | 8 | 例: 攻撃 9 | 10 | ## パッシブ: Passive 11 | 12 | クールダウンはなく、常時切り替え可能。 13 | 発動に応じて、他のインスタントスキルに対して、クールダウン係数がかかる。 14 | 15 | 例: 挑発 16 | 17 | ## チャージ: Charged 18 | 19 | スキルが選択された時、実行までのカウントダウンが始まる。 20 | カウントダウン中は他のスキルの実行ができない。 21 | 22 | 例: 呪文詠唱からの発動 23 | 24 | ## チェイス: Chase 25 | 26 | 条件が満たされた時、他のプレーヤーのスキル発動に応じて、自動で発動する。 27 | クールダウンがある。 28 | 29 | 例: 追撃 30 | -------------------------------------------------------------------------------- /src/domain/battle/Input.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | 4 | export const NO_TARGETED_SKILL = 'NO_TARGETED_SKILL' 5 | 6 | export type Input = {| 7 | type: typeof NO_TARGETED_SKILL, 8 | id: string, 9 | battlerId: string, 10 | skillId: string 11 | |} 12 | 13 | export const createNoTargetedSkillInput = ( 14 | battlerId: string, 15 | skillId: string 16 | ) => 17 | Object.freeze({ 18 | id: uuid(), 19 | type: typeof NO_TARGETED_SKILL, 20 | battlerId, 21 | skillId 22 | }) 23 | -------------------------------------------------------------------------------- /masterdata/schema/monster-schema.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | id: 3 | type: string 4 | displayName: 5 | type: string 6 | displayImage: 7 | type: string 8 | life: 9 | type: integer 10 | skills: 11 | type: array 12 | items: 13 | - 14 | required: 15 | - skillId 16 | - lv 17 | properties: 18 | skillId: 19 | type: string 20 | lv: 21 | type: integer 22 | 23 | required: 24 | - id 25 | - displayName 26 | - displayImage 27 | - life 28 | - skills 29 | -------------------------------------------------------------------------------- /masterdata/schema/skill-schema.yml: -------------------------------------------------------------------------------- 1 | required: 2 | - id 3 | - displayName 4 | - cooldownCount 5 | - skillType 6 | - altIconText 7 | properties: 8 | id: 9 | type: string 10 | displayName: 11 | type: string 12 | displayIcon: 13 | type: string 14 | cooldownCount: 15 | type: number 16 | skillType: 17 | type: 'string' 18 | enum: 19 | - DAMAGE_OPONENT_SINGLE 20 | - DAMAGE_OPONENT_ALL 21 | - HEAL_SELF 22 | - HEAL_ALLY_SINGLE 23 | - HEAL_ALLY_ALL 24 | # Instant name instead of icon 25 | altIconText: 26 | type: string 27 | -------------------------------------------------------------------------------- /src/client/reducers/battle/log.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { LOG, RESET } from '../../actions/battleActions' 3 | import type { BattleAction } from '../../actions/battleActions' 4 | 5 | // State 6 | export type State = string[] 7 | 8 | // Reducer 9 | export default (log: State = [], action: BattleAction) => { 10 | switch (action.type) { 11 | case LOG: 12 | return log.length < 6 13 | ? [].concat([action.payload], log) 14 | : [].concat([action.payload], log.slice(0, -1)) 15 | case RESET: 16 | return [] 17 | default: 18 | return log 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "Chorme": 58 6 | }, 7 | "exclude": [ 8 | "transform-regenerator" 9 | ] 10 | }], 11 | "react", 12 | "flow" 13 | ], 14 | "plugins": [ 15 | "transform-decorators-legacy", 16 | "transform-class-properties", 17 | "transform-object-rest-spread", 18 | "react-hot-loader/babel", 19 | "dynamic-import-webpack", 20 | ["module-resolver", { 21 | "root": ["./src"], 22 | "alias": { 23 | "domain": "./domain" 24 | } 25 | }] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/client/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { connect } from 'react-redux' 3 | import App from '../components/App' 4 | import type { State as RootState } from '../reducers' 5 | import type { State as AppState } from '../reducers/app' 6 | import type { AppAction } from '../actions/appActions' 7 | 8 | export type AppContainerProps = AppState & Redux$Dispatcher 9 | 10 | const mapStateToProps: RootState => AppState = root => root.app 11 | 12 | const connector: Redux$Connector<{}, AppState, AppAction> = connect( 13 | mapStateToProps 14 | ) 15 | export default connector(App) 16 | -------------------------------------------------------------------------------- /src/domain/battle/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export type { BattleSession } from './BattleSession' 3 | export type { Battler, AllyBattler, EnemyBattler } from './Battler' 4 | export type { Command } from './Command' 5 | export type { CommandResult } from './CommandResult' 6 | export type { BattleSessionResult } from './BattleSessionResult' 7 | export type { Input } from './Input' 8 | export type { Skill } from './Skill' 9 | 10 | export { createNoTargetedSkillInput } from './Input' 11 | export { 12 | processTurn, 13 | createBattleSession, 14 | isBattleFinished, 15 | buildBattleSession 16 | } from './BattleSession' 17 | -------------------------------------------------------------------------------- /src/client/containers/DebugModeContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { connect } from 'react-redux' 3 | import DebugMode from '../components/organisms/DebugMode' 4 | import type { State as RootState } from '../reducers' 5 | import type { AppAction } from '../actions/appActions' 6 | import type { AdventureAction } from '../actions/adventureActions' 7 | 8 | export type DebugModeContainerProps = RootState & 9 | Redux$Dispatcher 10 | 11 | const mapStateToProps: RootState => RootState = root => root 12 | 13 | const connector: Redux$Connector<{}, RootState, any> = connect(mapStateToProps) 14 | export default connector(DebugMode) 15 | -------------------------------------------------------------------------------- /src/client/containers/GlobalHeader.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { connect } from 'react-redux' 3 | import GlobalHeader from '../components/molecules/GlobalHeader' 4 | import type { State as RootState } from '../reducers' 5 | import type { AppAction } from '../actions/appActions' 6 | import type { AdventureAction } from '../actions/adventureActions' 7 | 8 | export type GlobalHeaderContainerProps = RootState & 9 | Redux$Dispatcher 10 | 11 | const mapStateToProps: RootState => RootState = root => root 12 | 13 | const connector: Redux$Connector<{}, RootState, any> = connect(mapStateToProps) 14 | export default connector(GlobalHeader) 15 | -------------------------------------------------------------------------------- /src/client/store/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { applyMiddleware, createStore } from 'redux' 3 | // import promiseMiddleware from 'redux-promise' 4 | // import logger from 'redux-logger' 5 | import createSagaMiddleware from 'redux-saga' 6 | import reducer from '../reducers' 7 | import mySaga from '../sagas' 8 | 9 | const sagaMiddleware = createSagaMiddleware() 10 | 11 | const store = createStore( 12 | reducer, 13 | global.__REDUX_DEVTOOLS_EXTENSION__ && global.__REDUX_DEVTOOLS_EXTENSION__(), 14 | applyMiddleware( 15 | sagaMiddleware 16 | // promiseMiddleware 17 | // logger, 18 | ) 19 | ) 20 | sagaMiddleware.run(mySaga) 21 | 22 | export default store 23 | -------------------------------------------------------------------------------- /src/client/containers/SetupContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { connect } from 'react-redux' 3 | import SetupScene from '../components/organisms/SetupScene' 4 | import type { AppAction } from '../actions/appActions' 5 | import type { State as RootState } from '../reducers' 6 | import type { State as SetupState } from '../reducers/setup' 7 | import type { SetupAction } from '../actions/setupActions' 8 | 9 | export type SetupContainerProps = SetupState & 10 | Redux$Dispatcher 11 | 12 | const mapStateToProps: RootState => SetupState = root => root.setup 13 | 14 | const connector: Redux$Connector<{}, SetupState, any> = connect(mapStateToProps) 15 | export default connector(SetupScene) 16 | -------------------------------------------------------------------------------- /src/client/components/Layout.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import GlobalHeader from '../containers/GlobalHeader' 4 | 5 | export default function Layout(props: any) { 6 | return ( 7 |
19 |
20 | 21 |
22 | 23 |
24 | {props.children} 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/client/reducers/playing.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as PlayingSessionActions from '../actions/playingActions' 3 | import type { PlayingAction } from '../actions/playingActions' 4 | import type { PlayingSession } from 'domain/sessions/PlayingSession' 5 | 6 | export type State = { 7 | playingSession: ?PlayingSession 8 | } 9 | 10 | const initialState: State = { 11 | playingSession: undefined 12 | } 13 | 14 | export default (state: State = initialState, action: PlayingAction) => { 15 | switch (action.type) { 16 | case PlayingSessionActions.PLAYING_SESSION_LOADED: 17 | return { 18 | ...state, 19 | playingSession: action.payload 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/components/molecules/InputQueueDisplay.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import type { Input, BattleSession } from 'domain/battle' 4 | 5 | export default function InputQueueDisplay({ 6 | inputQueue, 7 | battleSession 8 | }: { 9 | inputQueue: Input[], 10 | battleSession: BattleSession 11 | }) { 12 | return ( 13 |
14 | {inputQueue.map((input, index) => { 15 | const actorToAction = battleSession.battlers.find( 16 | b => b.id === input.battlerId 17 | ) 18 | return ( 19 |
20 | {actorToAction && actorToAction.displayName}: Ready {input.skillId} 21 |
22 | ) 23 | })} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/domain/adventure/AdventureSession.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Actor } from '../entities/Actor' 3 | import type { Resource } from '../entities/Resource' 4 | import { adventureSessionMock0 } from '../__mocks/adventureSessionMock' 5 | import * as ResourceActions from '../entities/Resource' 6 | 7 | export type AdventureSession = { 8 | id: string, 9 | resources: Resource[], 10 | actors: Actor[] 11 | } 12 | 13 | export function load() { 14 | return adventureSessionMock0 15 | } 16 | 17 | export function addResources( 18 | session: AdventureSession, 19 | resources: Resource[] 20 | ): AdventureSession { 21 | return { 22 | ...session, 23 | resources: ResourceActions.mergeResources(session.resources, resources) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /masterdata/data/skill-data.yml: -------------------------------------------------------------------------------- 1 | - 2 | id: $attack 3 | cooldownCount: 5 4 | displayName: Attack 5 | displayIcon: assets/icons/icon000.png 6 | altIconText: At 7 | skillType: DAMAGE_OPONENT_SINGLE 8 | - 9 | id: $power-attack 10 | cooldownCount: 12 11 | displayName: Power Attack 12 | altIconText: PA 13 | skillType: DAMAGE_OPONENT_SINGLE 14 | - 15 | id: $fire-wave 16 | cooldownCount: 18 17 | displayName: Fire Wave 18 | altIconText: FW 19 | skillType: DAMAGE_OPONENT_ALL 20 | - 21 | id: $heal-self 22 | cooldownCount: 20 23 | displayName: Heal Self 24 | altIconText: HS 25 | skillType: HEAL_SELF 26 | - 27 | id: $heal-all 28 | cooldownCount: 20 29 | displayName: Heal All 30 | altIconText: HA 31 | skillType: HEAL_ALLY_ALL 32 | -------------------------------------------------------------------------------- /src/client/containers/AdventureContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { connect } from 'react-redux' 3 | import AdventureScene from '../components/organisms/AdventureScene' 4 | import type { State as RootState } from '../reducers' 5 | import type { AppAction } from '../actions/appActions' 6 | import type { AdventureAction } from '../actions/adventureActions' 7 | import type { State as AdventureState } from '../reducers/adventure' 8 | 9 | export type AdventureContainerProps = AdventureState & 10 | Redux$Dispatcher 11 | 12 | const mapStateToProps: RootState => AdventureState = root => root.adventure 13 | 14 | const connector: Redux$Connector<{}, RootState, any> = connect(mapStateToProps) 15 | export default connector(AdventureScene) 16 | -------------------------------------------------------------------------------- /src/client/components/molecules/LogBoard.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { StyleSheet, css } from 'aphrodite' 4 | 5 | // export default 6 | function Message(props: { message: string }) { 7 | return ( 8 |

9 | {props.message} 10 |

11 | ) 12 | } 13 | 14 | export default function LogBoard(props: { 15 | messages: string[], 16 | direction: 'upper' | 'bottom' 17 | }) { 18 | return ( 19 |
20 | {props.messages.map((message, index) => ( // Animate a list of items as they are added 21 | 22 | ))} 23 |
24 | ) 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | logBoard: {}, 29 | message: {} 30 | }) 31 | -------------------------------------------------------------------------------- /src/domain/entities/Resource.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { updateIn } from '../utils/arrayUtils' 3 | 4 | export type Resource = { 5 | resourceId: string, 6 | resourceName: string, 7 | amount: number 8 | } 9 | 10 | export function addResource(resources: Resource[], res: Resource): Resource[] { 11 | const existedResource = resources.find(r => r.resourceId === res.resourceId) 12 | 13 | if (existedResource) { 14 | return updateIn( 15 | resources, 16 | r => r.resourceId === res.resourceId, 17 | r => Object.assign({ ...r, amount: r.amount + res.amount }) 18 | ) 19 | } else { 20 | return resources.concat([res]) 21 | } 22 | } 23 | 24 | export function mergeResources(a: Resource[], b: Resource[]): Resource[] { 25 | return b.reduce((acc, res) => addResource(acc, res), a) 26 | } 27 | -------------------------------------------------------------------------------- /src/domain/entities/Savedata.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { savedataListForTest } from '../__mocks/savedataMock' 3 | import type { Resource } from './Resource' 4 | 5 | export type Savedata = { 6 | id: string, 7 | playerName: string, 8 | resources: Resource[], 9 | ownedShells: [] 10 | } 11 | 12 | // TODO: Mocked 13 | export function load(savedataId: string): Savedata { 14 | const save = savedataListForTest.find(s => s.id === savedataId) 15 | if (save) { 16 | return save 17 | } else { 18 | throw `${savedataId} is not savedata id` 19 | } 20 | } 21 | 22 | // TODO: Mocked 23 | export function save(savedata: Savedata) { 24 | const index = savedataListForTest.findIndex(s => s.id === savedata.id) 25 | if (index > -1) { 26 | // eslint-disable-next-line 27 | savedataListForTest[index] = savedata 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/reducers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { combineReducers } from 'redux' 3 | import battle from './battle' 4 | import app from './app' 5 | import adventure from './adventure' 6 | import playing from './playing' 7 | import setup from './setup' 8 | import type { State as BattleSession } from './battle' 9 | import type { State as AppState } from './app' 10 | import type { State as AdventureState } from './adventure' 11 | import type { State as PlayingState } from './playing' 12 | import type { State as SetupState } from './setup' 13 | 14 | export type State = { 15 | battle: BattleSession, 16 | app: AppState, 17 | playing: PlayingState, 18 | adventure: AdventureState, 19 | setup: SetupState 20 | } 21 | 22 | export default combineReducers({ 23 | app, 24 | battle, 25 | playing, 26 | adventure, 27 | setup 28 | }) 29 | -------------------------------------------------------------------------------- /src/client/components/helpers/GlobalKeyListener.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | 4 | // Usage: 5 | //
6 | // console.log('Enter')}/> 7 | //
8 | export default class GlobalKeyListner extends React.Component { 9 | props: { 10 | keyCode: string | number, 11 | handler: SyntheticEvent => void 12 | } 13 | _bound: any = null 14 | type = 'keydown' 15 | componentDidMount() { 16 | const keyCode = this.props.keyCode 17 | this._bound = (ev: SyntheticEvent) => { 18 | if (ev.keyCode === keyCode) { 19 | ev.preventDefault() 20 | this.props.handler(ev) 21 | } 22 | } 23 | window.addEventListener(this.type, this._bound) 24 | } 25 | 26 | componentWillUnmount() { 27 | window.removeEventListener(this.type, this._bound) 28 | this._bound = null 29 | } 30 | render() { 31 | return null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/client/reducers/adventure.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as AdventureActions from '../actions/adventureActions' 3 | import type { AdventureAction } from '../actions/adventureActions' 4 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 5 | 6 | export type State = { 7 | adventureSession: ?AdventureSession, 8 | log: string[] 9 | } 10 | 11 | const initialState: State = { 12 | adventureSession: undefined, 13 | log: [] 14 | } 15 | 16 | export default (state: State = initialState, action: AdventureAction) => { 17 | switch (action.type) { 18 | case AdventureActions.PLAYING_SESSION_LOADED: 19 | return { 20 | ...state, 21 | adventureSession: action.payload 22 | } 23 | case AdventureActions.ADD_LOG: 24 | return { 25 | ...state, 26 | log: [action.payload].concat(state.log).slice(0, 4) 27 | } 28 | default: 29 | return state 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/domain/sessions/PlayingSession.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import type { Resource } from 'domain/entities/Resource' 4 | import * as SavedataActions from 'domain/entities/Savedata' 5 | import * as ResourceActions from 'domain/entities/Resource' 6 | 7 | export type PlayingSession = { 8 | id: string, 9 | savedataId: string, 10 | resources: Resource[] 11 | } 12 | 13 | // TODO: Mocked 14 | export function loadBySavedataId(savedataId: string): PlayingSession { 15 | const savedata = SavedataActions.load(savedataId) 16 | return { 17 | id: uuid(), 18 | savedataId: savedata.id, 19 | playerName: savedata.playerName, 20 | resources: savedata.resources 21 | } 22 | } 23 | 24 | export function collectResources( 25 | session: PlayingSession, 26 | resources: Resource[] 27 | ): PlayingSession { 28 | return { 29 | ...session, 30 | resources: ResourceActions.mergeResources(session.resources, resources) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/components/molecules/EnemyBattlersDisplay.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import type { Battler } from 'domain/battle' 4 | 5 | export default function EnemyBattlersDisplay({ 6 | enemies 7 | }: { 8 | enemies: Battler[] 9 | }) { 10 | return ( 11 |
12 | {enemies.map((enemy, index) => ( 13 |
14 | 0 ? 'grayscale(0)' : 'grayscale(1)' 18 | }} 19 | /> 20 |
21 | {enemy.monsterData && enemy.monsterData.displayName} 22 |   23 | {enemy.life.val} 24 | / 25 | {enemy.life.max} 26 |
27 |
28 | ))} 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/client/containers/BattleContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import BattleScene from '../components/organisms/BattleScene' 5 | import type { State as BattleState } from '../reducers/battle' 6 | import type { State as RootReducerState } from '../reducers' 7 | import type { BattleAction } from '../actions/battleActions' 8 | import type { AppAction } from '../actions/appActions' 9 | 10 | export type BattleContainerAction = BattleAction | AppAction 11 | export type BattleContainerProps = BattleState & 12 | Redux$Dispatcher 13 | 14 | function BattleContainer(props: BattleContainerProps) { 15 | return 16 | } 17 | 18 | const mapStateToProps: RootReducerState => BattleState = root => root.battle 19 | 20 | const connector: Redux$Connector< 21 | {}, 22 | BattleContainerProps, 23 | BattleContainerAction 24 | > = connect(mapStateToProps) 25 | export default connector(BattleContainer) 26 | -------------------------------------------------------------------------------- /src/client/components/App.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import BattleContainer from '../containers/BattleContainer' 4 | import DebugModeContainer from '../containers/DebugModeContainer' 5 | import AdventureContainer from '../containers/AdventureContainer' 6 | import SetupContainer from '../containers/SetupContainer' 7 | import type { AppContainerProps } from '../containers/AppContainer' 8 | import Layout from './Layout' 9 | 10 | export default function App(props: AppContainerProps) { 11 | const frontScene = props.sceneStack[props.sceneStack.length - 1] 12 | return ( 13 | 14 | {(() => { 15 | switch (frontScene.sceneId) { 16 | case 'debug-mode': 17 | return 18 | case 'adventure': 19 | return 20 | case 'battle': 21 | return 22 | case 'setup': 23 | return 24 | } 25 | return

App

26 | })()} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/client/reducers/app.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as AppActions from '../actions/appActions' 3 | import type { AppAction } from '../actions/appActions' 4 | 5 | export type Action = AppAction 6 | 7 | export type Scene = 8 | | { 9 | sceneId: 'debug-mode', 10 | sceneData: {} 11 | } 12 | | { 13 | sceneId: 'battle', 14 | sceneData: {} 15 | } 16 | | { 17 | sceneId: 'adventure', 18 | sceneData: {} 19 | } 20 | | { 21 | sceneId: 'setup', 22 | sceneData: {} 23 | } 24 | 25 | export type State = { 26 | sceneStack: Scene[] 27 | } 28 | 29 | const initialState: State = { 30 | sceneStack: [{ sceneId: 'debug-mode', sceneData: {} }] 31 | } 32 | 33 | export default (state: State = initialState, action: AppAction) => { 34 | switch (action.type) { 35 | case AppActions.PUSH_SCENE: 36 | return { ...state, sceneStack: state.sceneStack.concat([action.payload]) } 37 | case AppActions.POP_SCENE: 38 | return { ...state, sceneStack: state.sceneStack.slice(0, -1) } 39 | default: 40 | return state 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/160d27e2bebf784c4f4a1e070df057f3868b62bc/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | .DS_Store 52 | 53 | # app 54 | 55 | public/bundle.js 56 | .fusebox 57 | 58 | src/domain/master/**/*.js 59 | -------------------------------------------------------------------------------- /src/domain/battle/__mock/battleStateMock.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as BattlerFactory from '../BattlerFactory' 3 | import type { BattleSession } from '../BattleSession' 4 | 5 | export const battleStateMock0: BattleSession = Object.freeze({ 6 | turn: 0, 7 | battlers: BattlerFactory.buildAllyBattlers([ 8 | { 9 | id: '$player1', 10 | controllable: true, 11 | displayName: 'Player1', 12 | lifeValue: 150, 13 | acquiredSkills: [ 14 | { skillId: '$attack', lv: 1 }, 15 | { skillId: '$power-attack', lv: 1 }, 16 | { skillId: '$fire-wave', lv: 1 }, 17 | { skillId: '$heal-self', lv: 1 } 18 | ] 19 | }, 20 | { 21 | id: '$bot1', 22 | controllable: false, 23 | displayName: 'BOT1', 24 | lifeValue: 50, 25 | acquiredSkills: [ 26 | { skillId: '$attack', lv: 1 }, 27 | { skillId: '$heal-self', lv: 1 } 28 | ] 29 | } 30 | ]).concat( 31 | BattlerFactory.buildEnemyBattlers([ 32 | { 33 | monsterId: '$goblin' 34 | }, 35 | { 36 | monsterId: '$hob-goblin' 37 | } 38 | ]) 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /docs/FormationSpecs.md: -------------------------------------------------------------------------------- 1 | # 隊列: Formation 2 | 3 | ## 狙われ率: Presence 4 | 5 | 基礎ヘイト値が上から N / N -1 / N -2 / ... 1 と振られる。 6 | 基礎ヘイト値に対して、 挑発などのスキルによって、ヘイト値に倍率が掛かる。 7 | 8 | 単体スキルで狙われる確率 = PT内の自身のヘイト値 / 味方の合計 Presence 値 9 | 列指指定スキルで狙われる確率 = その列の Presence の合計 / 味方の合計 Presence 10 | 11 | 例: 12 | 4列での1列目の命中率は 4/(4+3+2+1) = 40% 13 | 4列で、挑発状態(x2)の1列目の命中率は 8/(8+3+2+1) = 57% 14 | 15 | ## 味方の隊列 16 | 17 | 一列。 18 | 19 | ## 敵の隊列 20 | 21 | 前・中・後の3列。列ごとに3枠。3x3。 22 | 23 | 例: `[ ['goblin', 'goblin', 'goblin'], ['goblin-leader'], ['goblin-mage', 'goblin-archer'] ]` 24 | 25 | ## ターゲットタイプ 26 | 27 | - 敵単体: SINGLE_OPONENT 28 | - 敵単体指定: SINGLE_OPONENT_TARGETED 29 | - 敵単体低体力: SINGLE_OPONENT_LOW_LIFE 30 | - 敵単体高体力: SINGLE_OPONENT_HIGH_LIFE 31 | - 敵単体プレゼンス無視: SINGLE_OPONENT_NO_PRESENCE 32 | - 敵単体プレゼンス反転: SINGLE_OPONENT_REVERSED_PRESENCE 33 | - 敵一列: ROW_OPONENT 34 | - 敵一列プレゼンス無視: ROW_OPONENT_NO_PRESENCE 35 | - 敵一列プレゼンス反転: ROW_OPONENT_REVERSED_PRESENCE 36 | - 敵全体: ALL_OPONENT 37 | - 敵全体減衰: ALL_OPONENT_DIMINISHING 38 | - 自身: SELF 39 | - 味方単体: SINGLE_ALLY 40 | - 味方単体指定: SINGLE_ALLY_TARGETED 41 | - 味方全体: SINGLE_ALLY 42 | - 味方単体低体力: SINGLE_ALLY_LOW_LIFE 43 | - 味方単体高体力: SINGLE_ALLY_HIGH_LIFE 44 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { AppContainer } from 'react-hot-loader' 5 | import App from './containers/AppContainer' 6 | import { Provider } from 'react-redux' 7 | import store from './store/index' 8 | 9 | export default async () => { 10 | if (process.env.NODE_ENV === 'production') { 11 | ReactDOM.render( 12 | , 13 | document.querySelector('main') 14 | ) 15 | } else { 16 | const render = async () => { 17 | const { default: App } = await import('./containers/AppContainer') 18 | ReactDOM.render( 19 | 20 | 21 | 22 | 23 | , 24 | document.querySelector('main') 25 | ) 26 | } 27 | render() 28 | if (module.hot) { 29 | const { default: store } = await import('./store') 30 | const { default: nextRootReducer } = await import('./reducers') 31 | store.replaceReducer(nextRootReducer) 32 | module.hot.accept('./components/App', render) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/components/organisms/__stories/BattleScene.stories.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { action, storiesOf } from '@storybook/react' 4 | import BattleScene from '../BattleScene' 5 | import { createBattleSession } from 'domain/battle/BattleSession' 6 | 7 | storiesOf('BattleScene', module) 8 | .add('Loading', () => { 9 | return ( 10 | { 21 | action('clicked') 22 | }} 23 | /> 24 | ) 25 | }) 26 | .add('Show', () => { 27 | return ( 28 | { 39 | action('clicked') 40 | }} 41 | /> 42 | ) 43 | }) 44 | -------------------------------------------------------------------------------- /src/domain/values/RangedValue.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export type RangedValue = {| 3 | val: number, // int 4 | max: number 5 | |} 6 | 7 | export const set: (RangedValue, number) => RangedValue = (cv, n) => 8 | Object.freeze({ 9 | val: (() => { 10 | if (n > cv.max) { 11 | return cv.max 12 | } else if (n < 0) { 13 | return 0 14 | } else { 15 | return n 16 | } 17 | })() | 0, 18 | max: cv.max 19 | }) 20 | 21 | export const add: (RangedValue, number) => RangedValue = (cv, n) => 22 | Object.freeze({ 23 | val: Math.min(cv.val + n, cv.max) | 0, 24 | max: cv.max 25 | }) 26 | 27 | export const sub: (RangedValue, number) => RangedValue = (cv, n) => 28 | Object.freeze({ 29 | val: Math.max(cv.val - n, 0) | 0, 30 | max: cv.max 31 | }) 32 | 33 | export const increment: RangedValue => RangedValue = cv => 34 | Object.freeze({ 35 | val: Math.min(cv.val + 1, cv.max) | 0, 36 | max: cv.max 37 | }) 38 | 39 | export const decrement: RangedValue => RangedValue = cv => 40 | Object.freeze({ 41 | val: Math.max(cv.val - 1, 0) | 0, 42 | max: cv.max 43 | }) 44 | 45 | export const create: number => RangedValue = max => 46 | Object.freeze({ 47 | val: max, 48 | max 49 | }) 50 | -------------------------------------------------------------------------------- /src/domain/battle/Skill.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import type { RangedValue } from 'domain/values/RangedValue' 4 | import type { SkillData, SkillId } from 'domain/master' 5 | import { increment } from 'domain/values/RangedValue' 6 | import { loadSkillData } from 'domain/master' 7 | 8 | export type Skill = { 9 | id: string, // FIXIT: can not seriarize to send 10 | data: SkillData, 11 | lv: number, 12 | cooldown: RangedValue 13 | } 14 | 15 | export function buildSkill(id: SkillId, lv: number): Skill { 16 | const data = loadSkillData(id) 17 | return Object.freeze({ 18 | data, 19 | lv, 20 | id: uuid(), 21 | cooldown: { 22 | val: 0, 23 | max: data.cooldownCount 24 | } 25 | }) 26 | } 27 | 28 | export function updateCooldownCount(skill: Skill): Skill { 29 | return Object.freeze({ 30 | ...skill, 31 | cooldown: increment(skill.cooldown) 32 | }) 33 | } 34 | 35 | export function resetCooldownCount(skill: Skill): Skill { 36 | return Object.freeze({ 37 | ...skill, 38 | cooldown: { 39 | val: 0, 40 | max: skill.data.cooldownCount 41 | } 42 | }) 43 | } 44 | 45 | export function isExecutable(skill: Skill): boolean { 46 | return skill.cooldown.val >= skill.cooldown.max 47 | } 48 | -------------------------------------------------------------------------------- /src/client/sagas/playingSaga.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as PlayingActions from '../actions/playingActions' 3 | import * as PlayingSession from 'domain/sessions/PlayingSession' 4 | import { takeEvery, put } from 'redux-saga/effects' 5 | import type { AdventureResult } from 'domain/adventure/AdventureResult' 6 | 7 | let currentSession = null 8 | 9 | export function* load(action: { 10 | type: typeof PlayingActions.REQUEST_FINISH_ADVENTURE, 11 | payload: { savedataId: string } 12 | }): any { 13 | currentSession = PlayingSession.loadBySavedataId(action.payload.savedataId) 14 | yield put(PlayingActions.loaded(currentSession)) 15 | } 16 | 17 | export function* finishAdventureSession(action: { 18 | payload: AdventureResult 19 | }): any { 20 | if (currentSession) { 21 | currentSession = PlayingSession.collectResources( 22 | currentSession, 23 | action.payload.session.resources 24 | ) 25 | yield put(PlayingActions.loaded(currentSession)) 26 | } else { 27 | console.warn('no playing session') 28 | } 29 | } 30 | 31 | export default function* playingSaga(): any { 32 | yield takeEvery(PlayingActions.REQUEST_TO_START_PLYAING_SESSION, load) 33 | yield takeEvery( 34 | PlayingActions.REQUEST_FINISH_ADVENTURE, 35 | finishAdventureSession 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/client/actions/AppActions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Scene } from '../reducers/app' 3 | 4 | export const PUSH_SCENE = 'app/PUSH_SCENE' 5 | export const POP_SCENE = 'app/POP_SCENE' 6 | export const TOGGLE_RESET_ON_RELOAD = 'app/TOGGLE_RESET_ON_RELOAD' 7 | export type AppAction = 8 | | { 9 | type: typeof PUSH_SCENE, 10 | payload: Scene 11 | } 12 | | { 13 | type: typeof POP_SCENE 14 | } 15 | | { 16 | type: typeof TOGGLE_RESET_ON_RELOAD 17 | } 18 | 19 | export function pushBattleScene(sceneData: {}) { 20 | return { 21 | type: PUSH_SCENE, 22 | payload: { 23 | sceneId: 'battle', 24 | sceneData 25 | } 26 | } 27 | } 28 | 29 | export function pushAdventureScene(sceneData: {}) { 30 | return { 31 | type: PUSH_SCENE, 32 | payload: { 33 | sceneId: 'adventure', 34 | sceneData 35 | } 36 | } 37 | } 38 | 39 | export function pushSetupScene(sceneData: {}) { 40 | return { 41 | type: PUSH_SCENE, 42 | payload: { 43 | sceneId: 'setup', 44 | sceneData 45 | } 46 | } 47 | } 48 | 49 | export function popScene() { 50 | return { 51 | type: POP_SCENE 52 | } 53 | } 54 | 55 | export function toggleResetOnReload() { 56 | return { 57 | type: TOGGLE_RESET_ON_RELOAD 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/client/components/molecules/GlobalHeader.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import * as AppActions from '../../actions/appActions' 4 | import type { GlobalHeaderContainerProps } from '../../containers/GlobalHeader' 5 | 6 | export default function GlobalHeader(props: GlobalHeaderContainerProps) { 7 | return ( 8 |
9 | 10 | Playing: 11 | {props.playing.playingSession && 12 | props.playing.playingSession.savedataId} 13 | 14 | / 15 |
16 | {props.app.sceneStack.map(scene => `[${scene.sceneId}]`).join(' > ')} 17 |
18 | {props.app.sceneStack.length > 1 && 19 | } 25 |   26 |
27 | Resources/ 28 | {props.playing.playingSession && 29 | props.playing.playingSession.resources.map((r, index) => ( 30 | {r.resourceName}: {r.amount} 31 | ))} 32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/domain/__mocks/adventureSessionMock.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import { resourcesMock0 } from './resourceMock' 4 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 5 | 6 | export const adventureSessionMockMinimum: AdventureSession = { 7 | id: uuid(), 8 | resources: resourcesMock0, 9 | actors: [ 10 | { 11 | id: '$minimum-example-player1', 12 | controllable: true, 13 | displayName: 'Player1', 14 | lifeValue: 100, 15 | acquiredSkills: [{ skillId: '$attack', lv: 1 }] 16 | } 17 | ] 18 | } 19 | 20 | export const adventureSessionMock0: AdventureSession = { 21 | id: uuid(), 22 | resources: resourcesMock0, 23 | actors: [ 24 | { 25 | id: '$player1', 26 | controllable: true, 27 | displayName: 'Player1', 28 | lifeValue: 150, 29 | acquiredSkills: [ 30 | { skillId: '$attack', lv: 1 }, 31 | { skillId: '$power-attack', lv: 1 }, 32 | { skillId: '$fire-wave', lv: 1 }, 33 | { skillId: '$heal-self', lv: 1 } 34 | ] 35 | }, 36 | { 37 | id: '$bot1', 38 | controllable: false, 39 | displayName: 'BOT1', 40 | lifeValue: 50, 41 | acquiredSkills: [ 42 | { skillId: '$attack', lv: 1 }, 43 | { skillId: '$heal-self', lv: 1 } 44 | ] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/client/actions/playingActions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { PlayingSession } from 'domain/sessions/PlayingSession' 3 | // import type { AdventureSession } from 'domain/sessions/AdventureSession' 4 | import type { AdventureResult } from 'domain/adventure/AdventureResult' 5 | 6 | export const REQUEST_TO_START_PLYAING_SESSION = 7 | 'playingSession/REQUEST_TO_START_PLYAING_SESSION' 8 | export const PLAYING_SESSION_LOADED = 'playingSession/PLAYING_SESSION_LOADED' 9 | export const REQUEST_FINISH_ADVENTURE = 10 | 'playingSession/REQUEST_FINISH_ADVENTURE' 11 | 12 | // TODO: Fix 13 | export type PlayingAction = any 14 | // | { 15 | // type: typeof REQUEST_TO_START_PLYAING_SESSION, 16 | // payload: { 17 | // savedataId: string 18 | // } 19 | // } 20 | // | { 21 | // type: typeof PLAYING_SESSION_LOADED, 22 | // payload: PlayingSession 23 | // } 24 | 25 | export function requestToStartPlayingSession(savedataId: string) { 26 | return { 27 | type: REQUEST_TO_START_PLYAING_SESSION, 28 | payload: { savedataId } 29 | } 30 | } 31 | 32 | export function loaded(payload: PlayingSession) { 33 | return { 34 | type: PLAYING_SESSION_LOADED, 35 | payload 36 | } 37 | } 38 | 39 | export function finishAdventureSession(adventureResult: AdventureResult) { 40 | return { 41 | type: REQUEST_FINISH_ADVENTURE, 42 | payload: adventureResult 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/client/components/molecules/AllyBattlersDisplay.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import SkillBar from './SkillBar' 4 | import type { Battler, Skill } from 'domain/battle' 5 | 6 | export default function AllyBattlersDisplay({ 7 | skillSelectCursor, 8 | allies, 9 | onAllyAndSkillSelect, 10 | isSkillInQueue 11 | }: { 12 | skillSelectCursor: ?{ x: number, y: number }, 13 | allies: Battler[], 14 | onAllyAndSkillSelect: Battler => Skill => void, 15 | isSkillInQueue: Skill => boolean 16 | }) { 17 | return ( 18 |
19 | {allies.map((ally, index) => ( 20 |
26 |
27 |
{ally.displayName}
28 |
29 | {ally.life.val} 30 | / 31 | {ally.life.max} 32 |
33 |
34 |
35 | 42 |
43 |
44 | ))} 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/client/components/molecules/SkillBar.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import SkillIcon from '../atoms/SkillIcon' 5 | import type { Skill } from 'domain/battle' 6 | 7 | export default function SkillBar({ 8 | skillSelectCursor, 9 | y, 10 | skills, 11 | onSkillSelect, 12 | isSkillInQueue 13 | }: { 14 | skillSelectCursor: ?{ x: number, y: number }, 15 | y: number, 16 | skills: Skill[], 17 | onSkillSelect: Skill => void, 18 | isSkillInQueue: Skill => boolean 19 | }) { 20 | return ( 21 | 22 | {skills.map((skill, index) => { 23 | const x = index 24 | const focused: boolean = 25 | !!skillSelectCursor && 26 | skillSelectCursor.x === x && 27 | skillSelectCursor.y === y 28 | return ( 29 |
30 | 31 | { 36 | onSkillSelect(skill) 37 | }} 38 | /> 39 | 40 |
41 | ) 42 | })} 43 |
44 | ) 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | display: 'flex' 50 | }, 51 | skillSlot: {} 52 | }) 53 | -------------------------------------------------------------------------------- /src/client/sagas/adventureSaga.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as AppActions from '../actions/appActions' 3 | import * as PlayingActions from '../actions/playingActions' 4 | import * as AdventureActions from '../actions/adventureActions' 5 | import * as Session from 'domain/adventure/AdventureSession' 6 | import type { Resource } from 'domain/entities/Resource' 7 | import { take, takeEvery, put } from 'redux-saga/effects' 8 | 9 | export function* startAdventure(): any { 10 | let session = Session.load() 11 | yield put(AdventureActions.sync(session)) 12 | while (true) { 13 | const action = yield take(action => 14 | [ 15 | AdventureActions.REQUEST_ADD_RESOURCES, 16 | AdventureActions.REQUEST_EXIT 17 | ].includes(action.type) 18 | ) 19 | switch (action.type) { 20 | case AdventureActions.REQUEST_ADD_RESOURCES: 21 | const resources: Resource[] = action.payload.resources 22 | session = Session.addResources(session, resources) 23 | for (const res of resources) { 24 | yield put( 25 | AdventureActions.addLog( 26 | `Add resource ${res.resourceName}:${res.amount}` 27 | ) 28 | ) 29 | } 30 | yield put(AdventureActions.sync(session)) 31 | break 32 | case AdventureActions.REQUEST_EXIT: 33 | yield put(PlayingActions.finishAdventureSession({ session })) 34 | yield put(AppActions.popScene()) 35 | return 36 | } 37 | } 38 | } 39 | 40 | export default function* playingSessionSaga(): any { 41 | yield takeEvery(AdventureActions.REQUEST_START_ADVENTURE, startAdventure) 42 | } 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | const BASE_PLUGINS = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 9 | }) 10 | ] 11 | 12 | console.log('env >', process.env.NODE_ENV) 13 | module.exports = { 14 | entry: process.env.NODE_ENV === 'production' 15 | ? ['./src/index.js'] 16 | : [ 17 | 'react-hot-loader/patch', 18 | 'webpack-dev-server/client?http://localhost:4444', 19 | 'webpack/hot/only-dev-server', 20 | './src/index.js' 21 | ], 22 | output: { 23 | filename: 'bundle.js', 24 | path: path.resolve(__dirname, 'public'), 25 | publicPath: '/' 26 | }, 27 | devServer: { 28 | contentBase: 'public/', 29 | historyApiFallback: true, 30 | port: 4444, 31 | hot: true 32 | }, 33 | plugins: process.env.NODE_ENV === 'production' 34 | ? BASE_PLUGINS.concat( 35 | [ 36 | // new webpack.optimize.UglifyJsPlugin({ 37 | // minimize: true, 38 | // sourceMap: false, 39 | // compressor: { 40 | // warnings: false 41 | // }, 42 | // output: { 43 | // comments: false 44 | // } 45 | // }) 46 | ] 47 | ) 48 | : BASE_PLUGINS.concat([ 49 | new webpack.NamedModulesPlugin(), 50 | new webpack.NoEmitOnErrorsPlugin(), 51 | new webpack.HotModuleReplacementPlugin() 52 | ]), 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.js$/, 57 | use: 'babel-loader', 58 | exclude: /node_modules/ 59 | } 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/domain/battle/CommandPlanner.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import planDamageOponentSingleSkill from './plans/planDamageOponentSingleSkill' 3 | import planDamageOponentAllSkill from './plans/planDamageOponentAllSkill' 4 | import planHealSelfSkill from './plans/planHealSelfSkill' 5 | import type { BattleSession } from './BattleSession' 6 | import type { Command, CommandApplicationProgress } from './Command' 7 | 8 | type CommandPlan = (next: BattleSession) => CommandApplicationProgress 9 | 10 | export function createCommandPlan( 11 | prevEnv: BattleSession, 12 | skillId: string, 13 | actorId: string, 14 | plannedTargetId?: string 15 | ): ?CommandPlan { 16 | const actor = prevEnv.battlers.find(b => b.id === actorId) 17 | const skill = actor && actor.skills.find(s => s.id === skillId) 18 | if (actor && skill && skill.data) { 19 | switch (skill.data.skillType) { 20 | case 'DAMAGE_OPONENT_SINGLE': 21 | return planDamageOponentSingleSkill(prevEnv, { 22 | actor, 23 | skill, 24 | plannedTargetId 25 | }) 26 | case 'DAMAGE_OPONENT_ALL': 27 | return planDamageOponentAllSkill(prevEnv, { 28 | actor, 29 | skill 30 | }) 31 | case 'HEAL_SELF': 32 | return planHealSelfSkill(prevEnv, { 33 | actor, 34 | skill 35 | }) 36 | } 37 | } 38 | // TODO: Assert or throw 39 | return null 40 | } 41 | 42 | export function createCommand( 43 | prevEnv: BattleSession, 44 | skillId: string, 45 | actorId: string, 46 | plannedTargetId?: string 47 | ): Command { 48 | const plan = createCommandPlan(prevEnv, skillId, actorId, plannedTargetId) 49 | return nextEnv => { 50 | if (plan) { 51 | return plan(nextEnv) 52 | } else { 53 | return { 54 | session: nextEnv, 55 | commandResults: [] 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /script/gen-code.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('require-yaml') 3 | const cc = require('change-case') 4 | const path = require('path') 5 | const fs = require('fs') 6 | const tv4 = require('tv4') 7 | const { convertSchema, schemaToFlow } = require('json-schema-to-flow-type') 8 | 9 | let code = ` 10 | /* @flow */ 11 | /* eslint-disable */ 12 | import data from './data' 13 | ` 14 | const all = {} 15 | ;['material', 'skill', 'shell', 'monster', 'dungeon', 'troop'].forEach(t => { 16 | const data = require(path.resolve(__dirname, `../masterdata/data/${t}-data`)) 17 | const schema = require(path.resolve( 18 | __dirname, 19 | `../masterdata/schema/${t}-schema` 20 | )) 21 | for (const i of data) { 22 | const r = tv4.validateResult(i, schema) 23 | if (!r.valid) { 24 | console.log(t, r.error.toString()) 25 | throw r.error 26 | } 27 | } 28 | 29 | const pascalName = cc.pascalCase(t) 30 | // data 31 | all[t] = data 32 | 33 | // flow 34 | const schemaForGen = Object.assign({}, { id: pascalName + 'Data' }, schema) 35 | // type 36 | let flowCode = schemaToFlow(convertSchema(schemaForGen)) 37 | 38 | // ids 39 | const ids = data.filter(i => i.id).map(i => `'${i.id}'`) 40 | // TODO: Validate relational ids 41 | code += `\n// === ${pascalName} ===\n` 42 | code += `export type ${pascalName}Id = ${ids.join(' | ')}\n` 43 | code += flowCode + '\n' 44 | code += `export function load${pascalName}Data(id: ${pascalName}Id): ${pascalName}Data { return Object.freeze(data['${t}'].find(i => i.id === id)) }\n` 45 | }) 46 | 47 | fs.writeFileSync(path.resolve(__dirname, '../src/domain/master/index.js'), code) 48 | console.log('> src/domain/master/index.js') 49 | 50 | fs.writeFileSync( 51 | path.resolve(__dirname, '../src/domain/master/data.js'), 52 | '/* eslint-disable */\nmodule.exports =' + JSON.stringify(all) 53 | ) 54 | console.log('> src/domain/master/data.js') 55 | -------------------------------------------------------------------------------- /src/domain/battle/BattlerFactory.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import { loadMonsterData } from '../master' 4 | import { buildSkill } from './Skill' 5 | import type { Battler } from './Battler' 6 | import type { MonsterId } from 'domain/master' 7 | import type { Actor, AcquiredSkill } from 'domain/entities/Actor' 8 | 9 | export function buildAllyBattler(data: { 10 | formationOrder: number, 11 | displayName: string, 12 | controllable: boolean, 13 | lifeValue: number, 14 | acquiredSkills: AcquiredSkill[] 15 | }): Battler { 16 | return { 17 | id: uuid('ally'), 18 | side: 'ally', 19 | formationOrder: data.formationOrder, 20 | controllable: data.controllable, 21 | displayName: data.displayName, 22 | life: { val: data.lifeValue, max: data.lifeValue }, 23 | skills: data.acquiredSkills.map(as => buildSkill(as.skillId, as.lv)) 24 | } 25 | } 26 | 27 | export function buildAllyBattlers(actors: Actor[]): Battler[] { 28 | return actors.map((actor, index) => { 29 | return buildAllyBattler({ 30 | ...actor, 31 | formationOrder: index 32 | }) 33 | }) 34 | } 35 | 36 | export function buildEnemyBattler(data: { 37 | formationOrder: number, 38 | monsterId: MonsterId 39 | }): Battler { 40 | const monsterData = loadMonsterData(data.monsterId) 41 | return { 42 | monsterData, 43 | id: uuid('enemy'), 44 | side: 'enemy', 45 | formationOrder: data.formationOrder, 46 | controllable: false, 47 | displayName: monsterData.displayName, 48 | life: { val: monsterData.life, max: monsterData.life }, 49 | skills: monsterData.skills.map(as => buildSkill((as.skillId: any), as.lv)) 50 | } 51 | } 52 | 53 | export function buildEnemyBattlers( 54 | enemies: { 55 | monsterId: MonsterId 56 | }[] 57 | ): Battler[] { 58 | return enemies.map((enemy, index) => { 59 | return buildEnemyBattler({ 60 | ...enemy, 61 | formationOrder: index 62 | }) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /src/domain/battle/plans/planHealSelfSkill.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as CommandResult from '../CommandResult' 3 | import * as BattlerActions from '../Battler' 4 | import type { Battler } from '../Battler' 5 | import type { Skill } from '../Skill' 6 | import type { BattleSession } from '../BattleSession' 7 | import type { CommandApplicationProgress } from '../Command' 8 | import * as RangedValueAction from 'domain/values/RangedValue' 9 | import { updateIn } from 'domain/utils/arrayUtils' 10 | 11 | const handleHealSelfSkill = ( 12 | session: BattleSession, 13 | actor: Battler, 14 | skill: Skill 15 | ): CommandApplicationProgress => { 16 | // TODO: Calc damage by master 17 | const healAmmount = 5 18 | let results = [] 19 | const battlers = updateIn( 20 | session.battlers, 21 | b => b.id === actor.id, 22 | target => { 23 | results = results.concat({ 24 | type: CommandResult.LOG, 25 | message: `${actor.displayName} exec ${skill.data.displayName}: ${healAmmount} healed` 26 | }) 27 | return { 28 | ...target, 29 | life: RangedValueAction.add(target.life, healAmmount) 30 | } 31 | } 32 | ) 33 | const skillConsumedBattlers = updateIn( 34 | battlers, 35 | b => b.id === actor.id, 36 | b => BattlerActions.consumeSkillCooldown(b, skill.id) 37 | ) 38 | return { 39 | session: { ...session, battlers: skillConsumedBattlers }, 40 | commandResults: results 41 | } 42 | } 43 | 44 | const planHealSelfSkill: ( 45 | BattleSession, 46 | { 47 | actor: Battler, 48 | skill: Skill 49 | } 50 | ) => BattleSession => CommandApplicationProgress = (_env, plan) => { 51 | return (nextEnv: BattleSession) => { 52 | const targets = nextEnv.battlers.filter(b => { 53 | return b.side !== plan.actor.side && BattlerActions.isTargetable(b) 54 | }) 55 | return handleHealSelfSkill(nextEnv, plan.actor, plan.skill) 56 | } 57 | } 58 | 59 | export default planHealSelfSkill 60 | -------------------------------------------------------------------------------- /src/client/actions/adventureActions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 3 | import type { AdventureResult } from 'domain/adventure/AdventureResult' 4 | import type { Resource } from 'domain/entities/Resource' 5 | 6 | export const REQUEST_ADD_RESOURCES = 'adventure/request-add-resource' 7 | export const REQUEST_START_ADVENTURE = 'adventure/request-loading' 8 | export const REQUEST_EXIT = 'adventure/request-exit' 9 | export const PLAYING_SESSION_LOADED = 'adventure/loaded' 10 | export const ADD_LOG = 'adventure/add-log' 11 | export const EXIT = 'adventure/exit' 12 | 13 | export type AdventureAction = 14 | | { 15 | type: typeof REQUEST_START_ADVENTURE 16 | } 17 | | { 18 | type: typeof REQUEST_EXIT, 19 | payload: AdventureResult 20 | } 21 | | { 22 | type: typeof EXIT 23 | } 24 | | { 25 | type: typeof ADD_LOG, 26 | payload: string 27 | } 28 | | { 29 | type: typeof PLAYING_SESSION_LOADED, 30 | payload: AdventureSession 31 | } 32 | | { 33 | type: typeof REQUEST_ADD_RESOURCES, 34 | payload: { 35 | resources: Resource[] 36 | } 37 | } 38 | 39 | export function requestLoadAdventureSession(): AdventureAction { 40 | return { 41 | type: REQUEST_START_ADVENTURE 42 | } 43 | } 44 | 45 | export function exit(): AdventureAction { 46 | return { 47 | type: EXIT 48 | } 49 | } 50 | 51 | export function requestExit(result: AdventureResult): AdventureAction { 52 | return { 53 | type: REQUEST_EXIT, 54 | payload: result 55 | } 56 | } 57 | 58 | // TODO 59 | export function requestAddResources(resources: Resource[]): AdventureAction { 60 | return { 61 | type: REQUEST_ADD_RESOURCES, 62 | payload: { resources } 63 | } 64 | } 65 | 66 | export function sync(payload: AdventureSession) { 67 | return { 68 | type: PLAYING_SESSION_LOADED, 69 | payload 70 | } 71 | } 72 | 73 | export function addLog(message: string): AdventureAction { 74 | return { 75 | type: ADD_LOG, 76 | payload: message 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/domain/battle/plans/planDamageOponentAllSkill.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as CommandResult from '../CommandResult' 3 | import * as BattlerActions from '../Battler' 4 | import type { Battler } from '../Battler' 5 | import type { Skill } from '../Skill' 6 | import type { BattleSession } from '../BattleSession' 7 | import type { CommandApplicationProgress } from '../Command' 8 | import * as RangedValueAction from 'domain/values/RangedValue' 9 | import { updateIn } from 'domain/utils/arrayUtils' 10 | 11 | const handleDamageOponentAllSkill = ( 12 | session: BattleSession, 13 | actor: Battler, 14 | skill: Skill, 15 | targets: Battler[] 16 | ): CommandApplicationProgress => { 17 | // TODO: Calc damage by master 18 | const damageAmmount = 5 19 | const targetIds = targets.map(t => t.id) 20 | let results = [] 21 | const battlers = updateIn( 22 | session.battlers, 23 | b => targetIds.includes(b.id), 24 | target => { 25 | results = results.concat({ 26 | type: CommandResult.LOG, 27 | message: `${actor.displayName} exec ${skill.data.displayName} to ${target.displayName} : ${damageAmmount} damage` 28 | }) 29 | return { 30 | ...target, 31 | life: RangedValueAction.sub(target.life, damageAmmount) 32 | } 33 | } 34 | ) 35 | const skillConsumedBattlers = updateIn( 36 | battlers, 37 | b => b.id === actor.id, 38 | b => BattlerActions.consumeSkillCooldown(b, skill.id) 39 | ) 40 | return { 41 | session: { ...session, battlers: skillConsumedBattlers }, 42 | commandResults: results 43 | } 44 | } 45 | 46 | const planDamageOponentAllSkill: ( 47 | BattleSession, 48 | { 49 | actor: Battler, 50 | skill: Skill 51 | } 52 | ) => BattleSession => CommandApplicationProgress = (_env, plan) => { 53 | return (nextEnv: BattleSession) => { 54 | const targets = nextEnv.battlers.filter(b => { 55 | return b.side !== plan.actor.side && BattlerActions.isTargetable(b) 56 | }) 57 | return handleDamageOponentAllSkill(nextEnv, plan.actor, plan.skill, targets) 58 | } 59 | } 60 | 61 | export default planDamageOponentAllSkill 62 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7f1a115f75043c44385071ea3f33c586 2 | // flow-typed version: 358375125e/redux_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'redux' { 5 | 6 | /* 7 | 8 | S = State 9 | A = Action 10 | 11 | */ 12 | 13 | declare type Dispatch }> = (action: A) => A; 14 | 15 | declare type MiddlewareAPI = { 16 | dispatch: Dispatch; 17 | getState(): S; 18 | }; 19 | 20 | declare type Store = { 21 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 22 | dispatch: Dispatch; 23 | getState(): S; 24 | subscribe(listener: () => void): () => void; 25 | replaceReducer(nextReducer: Reducer): void 26 | }; 27 | 28 | declare type Reducer = (session: S, action: A) => S; 29 | 30 | declare type CombinedReducer = (session: $Shape & {} | void, action: A) => S; 31 | 32 | declare type Middleware = 33 | (api: MiddlewareAPI) => 34 | (next: Dispatch) => Dispatch; 35 | 36 | declare type StoreCreator = { 37 | (reducer: Reducer, enhancer?: StoreEnhancer): Store; 38 | (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 39 | }; 40 | 41 | declare type StoreEnhancer = (next: StoreCreator) => StoreCreator; 42 | 43 | declare function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store; 44 | declare function createStore(reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store; 45 | 46 | declare function applyMiddleware(...middlewares: Array>): StoreEnhancer; 47 | 48 | declare type ActionCreator = (...args: Array) => A; 49 | declare type ActionCreators = { [key: K]: ActionCreator }; 50 | 51 | declare function bindActionCreators>(actionCreator: C, dispatch: Dispatch): C; 52 | declare function bindActionCreators>(actionCreators: C, dispatch: Dispatch): C; 53 | 54 | declare function combineReducers(reducers: O): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 55 | 56 | declare function compose(...fns: Array>): Function; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/client/components/organisms/DebugMode.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { lifecycle } from 'recompose' 4 | import type { 5 | DebugModeContainerProps 6 | } from '../../containers/DebugModeContainer' 7 | import * as appActions from '../../actions/appActions' 8 | // import * as adventureActions from '../../actions/adventureActions' 9 | import * as playingActions from '../../actions/playingActions' 10 | 11 | export default lifecycle({ 12 | componentDidMount() { 13 | // console.log('didmount') 14 | } 15 | })(function DebugMode(props: DebugModeContainerProps) { 16 | return ( 17 |
18 |

DebugMode

19 |
20 | debug: 21 | 28 | 35 |   36 | 43 | 50 |
51 | Menu: 52 | 59 | 66 | 73 |
74 |
75 |

Actors

76 | {props.adventure.adventureSession && 77 | props.adventure.adventureSession.actors.map((a, index) => ( 78 |
{a.displayName}
79 | ))} 80 |
81 |
82 | ) 83 | }) 84 | -------------------------------------------------------------------------------- /src/client/components/organisms/AdventureScene.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import { lifecycle } from 'recompose' 4 | import * as appActions from '../../actions/appActions' 5 | import * as adventureActions from '../../actions/adventureActions' 6 | import type { 7 | AdventureContainerProps 8 | } from '../../containers/AdventureContainer' 9 | import LogBoard from '../molecules/LogBoard' 10 | 11 | export default lifecycle({ 12 | componentDidMount() { 13 | this.props.dispatch(adventureActions.requestLoadAdventureSession()) 14 | } 15 | })(function AdventureScene(props: AdventureContainerProps) { 16 | return ( 17 |
18 |

AdventureScene

19 |
20 | 27 | 42 | 56 |
57 | 58 |
59 |
60 |

Actors

61 | {props.adventureSession && 62 | props.adventureSession.actors.map((a, index) => ( 63 |
{a.displayName}
64 | ))} 65 |
66 |
67 | Resources/ 68 | {props.adventureSession && 69 | props.adventureSession.resources.map((r, index) => ( 70 | {r.resourceName}: {r.amount} 71 | ))} 72 |
73 |
74 | ) 75 | }) 76 | -------------------------------------------------------------------------------- /src/client/components/atoms/__stories/SkillIcon.stories.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import React from 'react' 4 | import { action, storiesOf } from '@storybook/react' 5 | import SkillIcon from '../SkillIcon' 6 | import { loadSkillData } from 'domain/master' 7 | 8 | storiesOf('SkillIcon', module) 9 | .add('0/15', () => { 10 | const mockSkill = { 11 | data: loadSkillData('$power-attack'), 12 | id: uuid(), 13 | cooldown: { val: 0, max: 15 }, 14 | lv: 1 15 | } 16 | 17 | return ( 18 | 24 | ) 25 | }) 26 | .add('5/15', () => { 27 | const mockSkill = { 28 | data: loadSkillData('$power-attack'), 29 | id: uuid(), 30 | cooldown: { val: 5, max: 15 }, 31 | lv: 1 32 | } 33 | 34 | return ( 35 | 41 | ) 42 | }) 43 | .add('10/15', () => { 44 | const mockSkill = { 45 | data: loadSkillData('$power-attack'), 46 | id: uuid(), 47 | cooldown: { val: 9, max: 15 }, 48 | lv: 1 49 | } 50 | 51 | return ( 52 | 58 | ) 59 | }) 60 | .add('15/15', () => { 61 | const mockSkill = { 62 | data: loadSkillData('$power-attack'), 63 | id: uuid(), 64 | cooldown: { val: 15, max: 15 }, 65 | lv: 1 66 | } 67 | 68 | return ( 69 | 75 | ) 76 | }) 77 | .add('15/15 inQueue', () => { 78 | const mockSkill = { 79 | data: loadSkillData('$power-attack'), 80 | id: uuid(), 81 | cooldown: { val: 15, max: 15 }, 82 | lv: 1 83 | } 84 | 85 | return ( 86 | 92 | ) 93 | }) 94 | .add('15/15 onFocus', () => { 95 | const mockSkill = { 96 | data: loadSkillData('$power-attack'), 97 | id: uuid(), 98 | cooldown: { val: 15, max: 15 }, 99 | lv: 1 100 | } 101 | 102 | return ( 103 | 109 | ) 110 | }) 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rpg-prototype", 3 | "version": "0.0.1", 4 | "private": true, 5 | "license": "MIT", 6 | "ava": { 7 | "babel": "inherit", 8 | "files": [ 9 | "src/**/*.test.js" 10 | ], 11 | "require": [ 12 | "babel-register" 13 | ] 14 | }, 15 | "scripts": { 16 | "clean": "rimraf src/master/*.js public/bundle.js", 17 | "prepare": "yarn run clean; yarn build:master", 18 | "deploy": "yarn prepare; yarn build:src:prod; yarn gh-pages -- -d public/", 19 | "build:master": "node script/gen-code.js", 20 | "watch:master": "chokidar masterdata/**/* -c 'yarn build:master' --polling", 21 | "build:src": "webpack", 22 | "build:src:prod": "NODE_ENV=production webpack", 23 | "watch:src": "webpack-dev-server", 24 | "watch": "yarn prepare; run-p watch:*", 25 | "test": "ava", 26 | "typecheck": "flow", 27 | "lint": "eslint src", 28 | "lint:fix": "eslint src --fix", 29 | "storybook": "start-storybook -p 9001 -c .storybook -s public" 30 | }, 31 | "dependencies": { 32 | "aphrodite": "^1.2.1", 33 | "babel-runtime": "^6.23.0", 34 | "prop-types": "^15.5.10", 35 | "react": "15", 36 | "react-dom": "15", 37 | "react-modal": "^1.7.7", 38 | "react-redux": "^5.0.5", 39 | "react-tooltip": "^3.3.0", 40 | "recompose": "^0.23.4", 41 | "redux": "^3.6.0", 42 | "redux-logger": "^3.0.6", 43 | "redux-saga": "^0.15.3", 44 | "semantic-ui-css": "^2.2.10", 45 | "semantic-ui-react": "^0.68.5", 46 | "styled-components": "^2.0.0", 47 | "uuid": "^3.0.1" 48 | }, 49 | "devDependencies": { 50 | "@mizchi/babel-preset": "^1.1.1", 51 | "@storybook/react": "^3.0.0", 52 | "ava": "^0.19.1", 53 | "babel-core": "^6.24.1", 54 | "babel-loader": "^7.0.0", 55 | "babel-plugin-dynamic-import-webpack": "^1.0.1", 56 | "babel-plugin-flow-runtime": "^0.11.1", 57 | "babel-plugin-module-resolver": "^2.7.1", 58 | "babel-plugin-transform-class-properties": "^6.24.1", 59 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 60 | "babel-plugin-transform-export-extensions": "^6.22.0", 61 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 62 | "babel-preset-env": "^1.5.1", 63 | "babel-preset-react": "^6.24.1", 64 | "change-case": "^3.0.1", 65 | "chokidar-cli": "^1.2.0", 66 | "eslint": "^3.19.0", 67 | "eslint-config-mizchi": "^1.1.0", 68 | "eslint-import-resolver-node": "^0.3.0", 69 | "eslint-plugin-mutation": "^1.0.0", 70 | "flow-runtime": "^0.12.0", 71 | "gh-pages": "^1.0.0", 72 | "json-schema-to-flow-type": "^0.2.6", 73 | "npm-run-all": "^4.0.2", 74 | "react-hot-loader": "next", 75 | "require-yaml": "^0.0.1", 76 | "rimraf": "^2.6.1", 77 | "tv4": "^1.3.0", 78 | "webpack": "^2.6.1", 79 | "webpack-dev-server": "^2.4.5" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/domain/battle/plans/planDamageOponentSingleSkill.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as CommandResult from '../CommandResult' 3 | import * as BattlerActions from '../Battler' 4 | import type { Battler } from '../Battler' 5 | import type { Skill } from '../Skill' 6 | import type { BattleSession } from '../BattleSession' 7 | import type { CommandApplicationProgress } from '../Command' 8 | import * as RangedValueAction from 'domain/values/RangedValue' 9 | import { pickRandom, updateIn } from 'domain/utils/arrayUtils' 10 | 11 | const handleDamageOponentSingleSkill = ( 12 | session: BattleSession, 13 | actor: Battler, 14 | skill: Skill, 15 | target: Battler 16 | ): CommandApplicationProgress => { 17 | // TODO: Calc damage by master 18 | const damageAmmount = 5 19 | const damaged: Battler = { 20 | ...target, 21 | life: RangedValueAction.sub(target.life, damageAmmount) 22 | } 23 | const targetId = target && target.id 24 | const battlers = updateIn( 25 | session.battlers, 26 | b => b.id === targetId, 27 | () => damaged 28 | ) 29 | const skillConsumedBattlers = updateIn( 30 | battlers, 31 | b => b.id === actor.id, 32 | b => BattlerActions.consumeSkillCooldown(b, skill.id) 33 | ) 34 | return { 35 | session: { ...session, battlers: skillConsumedBattlers }, 36 | commandResults: [ 37 | { 38 | type: CommandResult.LOG, 39 | message: `${actor.displayName} attacked ${target.displayName} : ${damageAmmount} damage` 40 | } 41 | ] 42 | } 43 | } 44 | 45 | const planDamageOponentSingleSkill: ( 46 | BattleSession, 47 | { 48 | actor: Battler, 49 | skill: Skill, 50 | plannedTargetId?: string 51 | } 52 | ) => BattleSession => CommandApplicationProgress = (env, plan) => { 53 | let plannedTarget: ?Battler = null 54 | if (plan.plannedTargetId) { 55 | plannedTarget = env.battlers.find(b => b.id === plan.plannedTargetId) 56 | } 57 | 58 | const defineRealTarget = (env: BattleSession): ?Battler => { 59 | if (plannedTarget && BattlerActions.isAlive(plannedTarget)) { 60 | return plannedTarget 61 | } else { 62 | // Pick random oponent 63 | return pickRandom( 64 | env.battlers.filter(b => { 65 | return b.side !== plan.actor.side && BattlerActions.isTargetable(b) 66 | }) 67 | ) 68 | } 69 | } 70 | 71 | return (nextEnv: BattleSession) => { 72 | const target = defineRealTarget(nextEnv) 73 | if (target) { 74 | return handleDamageOponentSingleSkill( 75 | nextEnv, 76 | plan.actor, 77 | plan.skill, 78 | target 79 | ) 80 | } else { 81 | return Object.freeze({ 82 | session: nextEnv, 83 | commandResults: [ 84 | { 85 | type: CommandResult.LOG, 86 | message: `${plan.actor.displayName} failed to attack` 87 | } 88 | ] 89 | }) 90 | } 91 | } 92 | } 93 | 94 | export default planDamageOponentSingleSkill 95 | -------------------------------------------------------------------------------- /src/domain/battle/Battler.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as SkillAction from './Skill' 3 | import * as CommandPlanner from './CommandPlanner' 4 | import type { Skill } from './Skill' 5 | import type { BattleSession } from './BattleSession' 6 | import type { Command, Input } from './index' 7 | import type { RangedValue } from 'domain/values/RangedValue' 8 | import type { MonsterData } from 'domain/master' 9 | 10 | export type Battler = { 11 | side: 'ally' | 'enemy', 12 | controllable: boolean, 13 | formationOrder: number, 14 | id: string, 15 | displayName: string, 16 | life: RangedValue, 17 | monsterData?: MonsterData, 18 | skills: Skill[] 19 | } 20 | 21 | export type AllyBattler = Battler & { 22 | monsterData: void 23 | } 24 | 25 | export type EnemyBattler = Battler & { 26 | monsterData: MonsterData 27 | } 28 | 29 | export const isAlive = (battler: Battler): boolean => { 30 | return battler.life.val > 0 31 | } 32 | 33 | export const isTargetable = (battler: Battler): boolean => { 34 | return isAlive(battler) 35 | } 36 | 37 | export const consumeSkillCooldown: (Battler, string) => Battler = ( 38 | battler, 39 | skillId 40 | ) => { 41 | return { 42 | ...battler, 43 | skills: battler.skills.map(s => { 44 | if (s.id === skillId) { 45 | return SkillAction.resetCooldownCount(s) 46 | } else { 47 | return s 48 | } 49 | }) 50 | } 51 | } 52 | 53 | export function updateBattlerState(battler: Battler): Battler { 54 | // update cooldown 55 | const updatedSkills = isAlive(battler) 56 | ? battler.skills.map(s => SkillAction.updateCooldownCount(s)) 57 | : battler.skills 58 | return { ...battler, skills: updatedSkills } 59 | } 60 | 61 | export function planNextCommand( 62 | battler: Battler, 63 | inputs: Input[], 64 | env: BattleSession 65 | ): Command[] { 66 | let commands: Command[] = [] 67 | 68 | if (battler.controllable) { 69 | // Player 70 | if (inputs.length) { 71 | for (const input of inputs) { 72 | const cs = battler.skills.reduce((commands, skill) => { 73 | if (skill.id === input.skillId && SkillAction.isExecutable(skill)) { 74 | return commands.concat([ 75 | CommandPlanner.createCommand(env, input.skillId, battler.id) 76 | ]) 77 | } else { 78 | return commands 79 | } 80 | }, []) 81 | commands = commands.concat(cs) 82 | } 83 | } 84 | } else { 85 | // AI or BOT 86 | // Search executable skill 87 | const executableSkill = battler.skills.find(s => 88 | SkillAction.isExecutable(s) 89 | ) 90 | if (executableSkill) { 91 | commands = battler.skills.reduce((commands, skill) => { 92 | if (skill.id === executableSkill.id) { 93 | return commands.concat([ 94 | CommandPlanner.createCommand(env, skill.id, battler.id) 95 | ]) 96 | } else { 97 | return commands 98 | } 99 | }, []) 100 | } 101 | } 102 | return commands 103 | } 104 | -------------------------------------------------------------------------------- /src/client/components/atoms/SkillIcon.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | // import { StyleSheet, css } from 'aphrodite' 4 | import Tooltip from 'react-tooltip' 5 | import type { Skill } from 'domain/battle' 6 | 7 | const { sin, cos, PI } = Math 8 | const arcPath = ( 9 | percent, 10 | cx: number, 11 | cy: number, 12 | r: number, 13 | reversed: 0 | 1 = 1 14 | ) => { 15 | const axisRotation = percent > 0.5 ? 1 : 0 16 | return ` 17 | M ${cx} ${cy - r} 18 | A ${r} ${r}, ${axisRotation}, ${axisRotation}, ${reversed}, ${cx + sin(percent * PI * 2) * r} ${cy - cos(percent * PI * 2) * r} 19 | ` 20 | } 21 | 22 | export default function SkillIcon({ 23 | focused, 24 | skill, 25 | inQueue, 26 | onClick 27 | }: { 28 | focused: boolean, 29 | skill: Skill, 30 | inQueue: boolean, 31 | onClick: Function 32 | }) { 33 | const rad = skill.cooldown.val / skill.cooldown.max 34 | const filled = rad >= 1 35 | const tooltipId = `skillName${skill.data.displayName}` 36 | const size = 40 37 | return ( 38 | 45 | 46 | {skill.data.displayName} 47 | 48 | 60 | {filled 61 | ? 70 | : [ 71 | , 78 | 87 | ]} 88 | {skill.data.displayIcon 89 | ? 99 | : 105 | {skill.data.altIconText} 106 | } 107 | 108 | 109 | ) 110 | } 111 | 112 | // const styles = StyleSheet.create({ 113 | // red: { 114 | // padding: '3px' 115 | // // outline: '1px solid black' 116 | // } 117 | // }) 118 | -------------------------------------------------------------------------------- /flow-typed/npm/react-redux_v5.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 0ed284c5a2e97a9e3c0e87af3dedc09d 2 | // flow-typed version: bdf1e66252/react-redux_v5.x.x/flow_>=v0.30.x 3 | 4 | import type { Dispatch, Store } from 'redux' 5 | 6 | declare module 'react-redux' { 7 | /* 8 | 9 | S = State 10 | A = Action 11 | OP = OwnProps 12 | SP = StateProps 13 | DP = DispatchProps 14 | 15 | */ 16 | 17 | declare type MapStateToProps = ( 18 | session: S, 19 | ownProps: OP 20 | ) => SP | MapStateToProps 21 | 22 | declare type MapDispatchToProps = 23 | | ((dispatch: Dispatch
, ownProps: OP) => DP) 24 | | DP 25 | 26 | declare type MergeProps = ( 27 | sessionProps: SP, 28 | dispatchProps: DP, 29 | ownProps: OP 30 | ) => P 31 | 32 | declare type StatelessComponent

= (props: P) => ?React$Element 33 | 34 | declare class ConnectedComponent 35 | extends React$Component { 36 | static WrappedComponent: Class>, 37 | getWrappedInstance(): React$Component, 38 | static defaultProps: void, 39 | props: OP, 40 | session: void 41 | } 42 | 43 | declare type ConnectedComponentClass = Class< 44 | ConnectedComponent 45 | > 46 | 47 | declare type Connector = { 48 | ( 49 | component: StatelessComponent

50 | ): ConnectedComponentClass, 51 | ( 52 | component: Class> 53 | ): ConnectedComponentClass 54 | } 55 | 56 | declare class Provider 57 | extends React$Component< 58 | void, 59 | { store: Store, children?: any }, 60 | void 61 | > {} 62 | 63 | declare type ConnectOptions = { 64 | pure?: boolean, 65 | withRef?: boolean 66 | } 67 | 68 | declare type Null = null | void 69 | 70 | declare function connect( 71 | ...rest: Array // <= workaround for https://github.com/facebook/flow/issues/2360 72 | ): Connector } & OP>> 73 | 74 | declare function connect( 75 | mapStateToProps: Null, 76 | mapDispatchToProps: Null, 77 | mergeProps: Null, 78 | options: ConnectOptions 79 | ): Connector } & OP>> 80 | 81 | declare function connect( 82 | mapStateToProps: MapStateToProps, 83 | mapDispatchToProps: Null, 84 | mergeProps: Null, 85 | options?: ConnectOptions 86 | ): Connector } & OP>> 87 | 88 | declare function connect( 89 | mapStateToProps: Null, 90 | mapDispatchToProps: MapDispatchToProps, 91 | mergeProps: Null, 92 | options?: ConnectOptions 93 | ): Connector> 94 | 95 | declare function connect( 96 | mapStateToProps: MapStateToProps, 97 | mapDispatchToProps: MapDispatchToProps, 98 | mergeProps: Null, 99 | options?: ConnectOptions 100 | ): Connector> 101 | 102 | declare function connect( 103 | mapStateToProps: MapStateToProps, 104 | mapDispatchToProps: MapDispatchToProps, 105 | mergeProps: MergeProps, 106 | options?: ConnectOptions 107 | ): Connector 108 | } 109 | -------------------------------------------------------------------------------- /src/domain/battle/BattleSession.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as BattlerActions from './Battler' 3 | import * as BattlerFactory from './BattlerFactory' 4 | import type { Battler } from './Battler' 5 | import type { Command, CommandApplicationProgress } from './Command' 6 | import type { CommandResult } from './CommandResult' 7 | import type { Input } from './Input' 8 | import type { BattleSessionResult } from './BattleSessionResult' 9 | import { battleStateMock0 } from './__mock/battleStateMock' 10 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 11 | 12 | // State 13 | export type BattleSession = { 14 | battlers: Battler[], 15 | turn: number 16 | } 17 | 18 | export function buildBattleSession(ps: AdventureSession): BattleSession { 19 | const allies = BattlerFactory.buildAllyBattlers(ps.actors) 20 | const enemies = BattlerFactory.buildEnemyBattlers([ 21 | { 22 | monsterId: '$goblin' 23 | }, 24 | { 25 | monsterId: '$hob-goblin' 26 | } 27 | ]) 28 | return { 29 | turn: 0, 30 | battlers: allies.concat(enemies) 31 | } 32 | } 33 | 34 | export function processPreUpdatePhase(session: BattleSession): BattleSession { 35 | return { 36 | ...session, 37 | battlers: session.battlers.map(BattlerActions.updateBattlerState) 38 | } 39 | } 40 | 41 | export function processPlanningPhase( 42 | session: BattleSession, 43 | inputQueue: Input[] 44 | ): Command[] { 45 | return Object.freeze( 46 | session.battlers.reduce((commands, battler) => { 47 | const inputs = inputQueue.filter(input => input.battlerId === battler.id) 48 | return commands.concat( 49 | BattlerActions.planNextCommand(battler, inputs, session) 50 | ) 51 | }, []) 52 | ) 53 | } 54 | 55 | export function processCommandExecPhase( 56 | session: BattleSession, 57 | commandQueue: Command[] 58 | ): CommandApplicationProgress { 59 | return Object.freeze( 60 | commandQueue.reduce( 61 | (next: CommandApplicationProgress, nextCmd: Command) => { 62 | const { session: nextState, commandResults } = nextCmd(next.session) 63 | return { 64 | session: nextState, 65 | commandResults: next.commandResults.concat(commandResults) 66 | } 67 | }, 68 | { session, commandResults: [] } 69 | ) 70 | ) 71 | } 72 | 73 | export function isBattleFinished(session: BattleSession): ?BattleSessionResult { 74 | if ( 75 | session.battlers.filter(b => b.side === 'enemy').every(b => b.life.val <= 0) 76 | ) { 77 | return { 78 | winner: 'ally', 79 | rewards: { 80 | resources: [ 81 | { 82 | resourceId: '$gold', 83 | resourceName: 'gold', 84 | amount: 30 85 | } 86 | ] 87 | } 88 | } 89 | } 90 | 91 | if ( 92 | session.battlers.filter(b => b.side === 'ally').every(b => b.life.val <= 0) 93 | ) { 94 | return { winner: 'enemy' } 95 | } 96 | 97 | return null 98 | } 99 | 100 | export function processTurn( 101 | session: BattleSession, 102 | inputQueue: Input[] 103 | ): { session: BattleSession, commandResults: CommandResult[] } { 104 | // update pre-actions 105 | const preUpdatedState = processPreUpdatePhase(session) 106 | 107 | // create commands 108 | const commandQueue = processPlanningPhase(preUpdatedState, inputQueue) 109 | 110 | // exec commands 111 | return processCommandExecPhase(preUpdatedState, commandQueue) 112 | } 113 | 114 | export function createBattleSession(): BattleSession { 115 | // TODO: Return real session 116 | return battleStateMock0 117 | } 118 | -------------------------------------------------------------------------------- /src/client/actions/battleActions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 3 | import type { Input, BattleSessionResult } from 'domain/battle' 4 | import { createNoTargetedSkillInput } from 'domain/battle' 5 | 6 | // Constants 7 | export const REQUEST_START = 'battle:request-start' 8 | export const REQUEST_PAUSE = 'battle:request-pause' 9 | export const REQUEST_RESTART = 'battle:request-restart' 10 | export const PAUSED = 'battle:paused' 11 | export const RESTARTED = 'battle:restarted' 12 | export const OPEN_BATTLE_SESSION_RESULT = 'battle:open-battle-session-result' 13 | export const EXIT_BATTLE_SESSION = 'battle:exit-battle-session' 14 | export const ADD_INPUT_TO_QUEUE = 'battle:add-input-to-queue' 15 | export const UPDATE_INPUT_QUEUE = 'battle:update-input-queue' 16 | export const MOVE_SKILL_SELECTOR = 'battle:move-skill-selector' 17 | export const SET_SKILL_SELECTOR = 'battle:set-skill-selector' 18 | export const UNSET_SKILL_SELECTOR = 'battle:unset-skill-selector' 19 | export const RESET = 'battle:reset' 20 | export const LOG = 'battle:log' 21 | 22 | // Actions 23 | export type BattleAction = 24 | | { 25 | type: typeof REQUEST_START, 26 | payload: { adventureSession: AdventureSession } 27 | } 28 | | { type: typeof REQUEST_PAUSE } 29 | | { type: typeof REQUEST_RESTART } 30 | | { type: typeof PAUSED } 31 | | { type: typeof RESTARTED } 32 | | { type: typeof RESET } 33 | | { type: typeof MOVE_SKILL_SELECTOR, payload: { dx: number, dy: number } } 34 | | { type: typeof SET_SKILL_SELECTOR, payload: { x: number, y: number } } 35 | | { type: typeof UNSET_SKILL_SELECTOR } 36 | | { 37 | type: typeof OPEN_BATTLE_SESSION_RESULT, 38 | payload: BattleSessionResult 39 | } 40 | | { 41 | type: typeof EXIT_BATTLE_SESSION 42 | } 43 | | { type: typeof LOG, payload: string } 44 | | { 45 | type: typeof ADD_INPUT_TO_QUEUE, 46 | payload: { 47 | battlerId: string, 48 | skillId: string 49 | } 50 | } 51 | | { 52 | type: typeof UPDATE_INPUT_QUEUE, 53 | payload: { 54 | inputQueue: Input[] 55 | } 56 | } 57 | 58 | // Action creator 59 | export const requestStart = (adventureSession: AdventureSession) => ({ 60 | type: REQUEST_START, 61 | payload: { 62 | adventureSession 63 | } 64 | }) 65 | export const requestPause = () => ({ type: REQUEST_PAUSE }) 66 | export const requestRestart = () => ({ type: REQUEST_RESTART }) 67 | export const paused = () => ({ type: PAUSED }) 68 | export const restarted = () => ({ type: RESTARTED }) 69 | export const reset = () => ({ type: RESET }) 70 | 71 | // Skill Selector 72 | export const moveSkillSelector = (dx: number, dy: number) => ({ 73 | type: MOVE_SKILL_SELECTOR, 74 | payload: { dx, dy } 75 | }) 76 | export const setSkillSelector = (x: number, y: number) => ({ 77 | type: SET_SKILL_SELECTOR, 78 | payload: { x, y } 79 | }) 80 | export const unsetSkillSelector = () => ({ 81 | type: UNSET_SKILL_SELECTOR 82 | }) 83 | 84 | export const openBattleSessionResult = (result: BattleSessionResult) => ({ 85 | type: OPEN_BATTLE_SESSION_RESULT, 86 | payload: result 87 | }) 88 | export const closeBattleSessionResult = () => ({ type: EXIT_BATTLE_SESSION }) 89 | export const log = (message: string) => ({ type: LOG, payload: message }) 90 | export const addInputToQueue = (battlerId: string, skillId: string) => { 91 | return { 92 | type: ADD_INPUT_TO_QUEUE, 93 | payload: createNoTargetedSkillInput(battlerId, skillId) 94 | } 95 | } 96 | export const updateInputQueue = (inputQueue: Input[]) => ({ 97 | type: UPDATE_INPUT_QUEUE, 98 | payload: { inputQueue } 99 | }) 100 | -------------------------------------------------------------------------------- /src/client/reducers/battle/runner.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { 3 | PAUSED, 4 | REQUEST_START, 5 | RESET, 6 | RESTARTED, 7 | UPDATE_INPUT_QUEUE, 8 | MOVE_SKILL_SELECTOR, 9 | SET_SKILL_SELECTOR, 10 | UNSET_SKILL_SELECTOR, 11 | OPEN_BATTLE_SESSION_RESULT, 12 | EXIT_BATTLE_SESSION 13 | } from '../../actions/battleActions' 14 | import type { BattleSagaAction } from '../../actions/battleSagaActions' 15 | import { SYNC } from '../../actions/battleSagaActions' 16 | import type { BattleAction } from '../../actions/battleActions' 17 | import type { 18 | BattleSession, 19 | Input, 20 | BattleSessionResult, 21 | Battler 22 | } from 'domain/battle' 23 | 24 | // State 25 | export type SkillSelector = { 26 | x: number, 27 | y: number 28 | } 29 | export type State = { 30 | battleSession: ?BattleSession, 31 | inputQueue: Input[], 32 | loading: boolean, 33 | paused: boolean, 34 | skillSelectCursor: ?SkillSelector, 35 | battleSessionResult: ?BattleSessionResult 36 | } 37 | 38 | const initialState: State = Object.freeze({ 39 | loading: true, 40 | paused: false, 41 | battleSession: null, 42 | inputQueue: [], 43 | skillSelectCursor: { x: 0, y: 0 }, 44 | battleSessionResult: null 45 | }) 46 | 47 | // Reducer 48 | export default ( 49 | state: State = initialState, 50 | action: BattleAction | BattleSagaAction 51 | ) => { 52 | switch (action.type) { 53 | case REQUEST_START: 54 | return { 55 | ...state, 56 | battleSession: null, 57 | loading: false 58 | } 59 | case PAUSED: 60 | return { 61 | ...state, 62 | paused: true 63 | } 64 | case RESTARTED: 65 | return { 66 | ...state, 67 | paused: false 68 | } 69 | case SYNC: 70 | return { 71 | ...state, 72 | battleSession: action.payload, 73 | loading: true 74 | } 75 | case OPEN_BATTLE_SESSION_RESULT: 76 | return { 77 | ...state, 78 | battleSessionResult: action.payload 79 | } 80 | case EXIT_BATTLE_SESSION: 81 | return initialState 82 | case UPDATE_INPUT_QUEUE: 83 | return { 84 | ...state, 85 | inputQueue: action.payload.inputQueue 86 | } 87 | case SET_SKILL_SELECTOR: 88 | return { 89 | ...state, 90 | skillSelectCursor: action.payload 91 | } 92 | case UNSET_SKILL_SELECTOR: 93 | return { 94 | ...state, 95 | skillSelectCursor: null 96 | } 97 | case MOVE_SKILL_SELECTOR: 98 | if (state.skillSelectCursor && state.battleSession) { 99 | const { skillSelectCursor, battleSession } = state 100 | const allies = battleSession.battlers.filter(b => b.side === 'ally') 101 | const { x, y } = skillSelectCursor 102 | const { dx, dy } = action.payload 103 | return { 104 | ...state, 105 | skillSelectCursor: moveSkillCursorWithOverflow(allies, x + dx, y + dy) 106 | } 107 | } else { 108 | return { 109 | ...state, 110 | skillSelectCursor: { x: 0, y: 0 } 111 | } 112 | } 113 | case RESET: 114 | return initialState 115 | default: 116 | return state 117 | } 118 | } 119 | 120 | const { abs } = Math 121 | function moveSkillCursorWithOverflow( 122 | battlers: Battler[], 123 | x: number, 124 | y: number 125 | ): { x: number, y: number } { 126 | // select battler as y axis 127 | const my = battlers.length 128 | let ry = y < my ? y : abs(y % my) 129 | ry = ry < 0 ? my - 1 : ry 130 | const b = battlers[ry] 131 | 132 | // select skill as x axis 133 | const mx = b.skills.length 134 | let rx = x < mx ? x : abs(x % mx) 135 | rx = rx < 0 ? mx - 1 : rx 136 | console.log(x, y, rx, ry) 137 | return { 138 | x: rx, 139 | y: ry 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/client/sagas/battleSaga.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import uuid from 'uuid' 3 | import { delay } from 'redux-saga' 4 | import { sync } from '../actions/battleSagaActions' 5 | import * as battleActions from '../actions/battleActions' 6 | import type { BattleSession } from 'domain/battle' 7 | import type { AdventureSession } from 'domain/adventure/AdventureSession' 8 | import { 9 | isBattleFinished, 10 | processTurn, 11 | buildBattleSession 12 | } from 'domain/battle' 13 | import { call, put, race, take, takeEvery } from 'redux-saga/effects' 14 | import * as CommandResultActions from 'domain/battle/CommandResult' 15 | import type { Input, Battler, Skill } from 'domain/battle' 16 | 17 | let _inputQueue: Input[] = [] 18 | 19 | function hydrateInputQueue() { 20 | const iq = _inputQueue 21 | _inputQueue = [] 22 | return iq 23 | } 24 | 25 | function* addInputToQueue(action: any) { 26 | if (waitMode) { 27 | // Can't intercept 28 | return 29 | } 30 | _inputQueue = _inputQueue.concat([{ ...action.payload, id: uuid('input') }]) 31 | yield put(battleActions.updateInputQueue(_inputQueue)) 32 | } 33 | 34 | export function findActiveSkill(battlers: Battler[]): ?Skill { 35 | for (const b of battlers) { 36 | if (b.controllable) { 37 | const executableSkill = b.skills.find( 38 | sk => sk.cooldown.val >= sk.cooldown.max 39 | ) 40 | if (executableSkill) { 41 | return executableSkill 42 | } 43 | } 44 | } 45 | return 46 | } 47 | 48 | let waitMode = false 49 | function* start(action: { payload: { adventureSession: AdventureSession } }) { 50 | // Use wait mode 51 | waitMode = location.search.indexOf('wait') > -1 52 | 53 | // let session: BattleSession = createBattleSession() 54 | // debugger 55 | let session: BattleSession = buildBattleSession( 56 | action.payload.adventureSession 57 | ) 58 | 59 | // Sync first 60 | yield put(sync(session)) 61 | 62 | // Start loop 63 | while (true) { 64 | // InputQueue buffer 65 | let takenInputQueue: Input[] = [] 66 | 67 | // WaitMode: check executableSkill 68 | if (waitMode) { 69 | // Wait input on wait mode 70 | const executableSkill = findActiveSkill(session.battlers) 71 | if (executableSkill) { 72 | yield put(sync(session)) 73 | yield put(battleActions.paused()) 74 | const takenInputAction: { payload: Input } = yield take( 75 | battleActions.ADD_INPUT_TO_QUEUE 76 | ) 77 | takenInputQueue = [takenInputAction.payload] 78 | yield put(battleActions.restarted()) 79 | yield call(delay, 100) 80 | } 81 | } 82 | 83 | // ActiveMode: wait interval or intercept by pause request 84 | if (!waitMode) { 85 | // const { paused, waited } = yield race({ 86 | const { paused } = yield race({ 87 | waited: call(delay, 300), 88 | paused: take(battleActions.REQUEST_PAUSE) 89 | }) 90 | // if user request pausing, wait for restart 91 | if (paused) { 92 | yield put(battleActions.paused()) 93 | yield take(battleActions.REQUEST_RESTART) 94 | yield put(battleActions.restarted()) 95 | } 96 | // Get input 97 | takenInputQueue = hydrateInputQueue() 98 | yield put(battleActions.updateInputQueue([])) 99 | } 100 | 101 | // Update session 102 | const processed = processTurn(session, takenInputQueue) 103 | session = processed.session 104 | for (const result of processed.commandResults) { 105 | switch (result.type) { 106 | case CommandResultActions.LOG: 107 | yield put(battleActions.log(result.message)) 108 | if (waitMode) { 109 | yield put(sync(session)) 110 | yield call(delay, 100) 111 | } 112 | break 113 | } 114 | } 115 | 116 | // Check finished flag 117 | const result = isBattleFinished(session) 118 | if (result) { 119 | yield put(sync(session)) 120 | yield put(battleActions.openBattleSessionResult(result)) 121 | // yield put(battleActions.openBattleSessionResult(`${finshed.winner} win.`)) 122 | yield take(battleActions.EXIT_BATTLE_SESSION) 123 | return 124 | // break 125 | } 126 | 127 | // Sync session by each frame on active 128 | if (!waitMode) { 129 | yield put(sync(session)) 130 | } 131 | 132 | // Clear inputQueue 133 | takenInputQueue = [] 134 | } 135 | } 136 | 137 | export default function* battleSaga(): any { 138 | yield takeEvery(battleActions.REQUEST_START, start) 139 | yield takeEvery(battleActions.ADD_INPUT_TO_QUEUE, addInputToQueue) 140 | } 141 | -------------------------------------------------------------------------------- /flow-typed/npm/recompose_v0.21.x.js: -------------------------------------------------------------------------------- 1 | declare module 'recompose' { 2 | 3 | /* 4 | Fn1 = Function with arity of 1 5 | HOC = Higher order component 6 | SCU = Should component update function 7 | */ 8 | 9 | declare type FunctionComponent = (props: A) => ?React$Element; 10 | 11 | declare type ClassComponent = Class>; 12 | 13 | declare type Component = FunctionComponent | ClassComponent; 14 | 15 | declare type Fn1 = (a: A) => B; 16 | 17 | declare type HOC = Fn1, Component>; 18 | 19 | declare type SCU = (props: A, nextProps: A) => boolean; 20 | 21 | declare function id(a: A): A; 22 | declare function compose(pq: Fn1, op: Fn1, mo: Fn1, lm: Fn1, kl: Fn1, jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 23 | declare function compose(op: Fn1, mo: Fn1, lm: Fn1, kl: Fn1, jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 24 | declare function compose(mo: Fn1, lm: Fn1, kl: Fn1, jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 25 | declare function compose(lm: Fn1, kl: Fn1, jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 26 | declare function compose(kl: Fn1, jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 27 | declare function compose(jk: Fn1, ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 28 | declare function compose(ij: Fn1, hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 29 | declare function compose(hi: Fn1, gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 30 | declare function compose(gh: Fn1, fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 31 | declare function compose(fg: Fn1, ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 32 | declare function compose(ef: Fn1, de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 33 | declare function compose(de: Fn1, cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 34 | declare function compose(cd: Fn1, bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 35 | declare function compose(bc: Fn1, ab: Fn1, ...rest: Array): Fn1; 36 | declare function compose(ab: Fn1, ...rest: Array): Fn1; 37 | declare function compose(...rest: Array): id; 38 | 39 | declare function mapProps( 40 | propsMapper: (ownerProps: B) => A 41 | ): HOC; 42 | 43 | declare function withProps( 44 | createProps: (ownerProps: B) => A 45 | ): HOC; 46 | declare function withProps( 47 | createProps: A 48 | ): HOC; 49 | 50 | declare function withPropsOnChange( 51 | shouldMapOrKeys: Array<$Keys> | SCU, 52 | createProps: (ownerProps: B) => A 53 | ): HOC; 54 | 55 | declare function withHandlers Function }>( 56 | handlerCreators: A 57 | ): HOC 58 | 59 | declare function defaultProps, B: $Diff>( 60 | props: D 61 | ): HOC; 62 | 63 | declare function renameProp( 64 | oldName: $Keys, 65 | newName: $Keys 66 | ): HOC; 67 | 68 | declare function renameProps( 69 | nameMap: { [key: $Keys]: $Keys } 70 | ): HOC; 71 | 72 | declare function flattenProp( 73 | propName: $Keys 74 | ): HOC; 75 | 76 | declare function withState( 77 | stateName: string, 78 | stateUpdaterName: string, 79 | initialState: T | (props: B) => T 80 | ): HOC; 81 | 82 | declare function withReducer( 83 | stateName: string, 84 | dispatchName: string, 85 | reducer: (state: State, action: Action) => State, 86 | initialState: State 87 | ): HOC; 88 | 89 | declare function branch( 90 | test: (ownerProps: B) => boolean, 91 | left: HOC, 92 | right?: HOC 93 | ): HOC; 94 | 95 | declare function renderComponent(C: Component | string): HOC; 96 | 97 | declare function renderNothing(): HOC; 98 | 99 | declare function shouldUpdate( 100 | test: SCU 101 | ): HOC; 102 | 103 | declare function pure(C: Component): FunctionComponent; 104 | 105 | declare function onlyUpdateForKeys(propKeys: Array<$Keys>): HOC; 106 | 107 | declare function withContext( 108 | childContextTypes: Object, 109 | getChildContext: (props: Object) => Object 110 | ): HOC; 111 | 112 | declare function getContext( 113 | contextTypes: C 114 | ): HOC ]: any }, B>; 115 | 116 | declare function lifecycle( 117 | spec: Object, 118 | ): HOC; 119 | 120 | declare function toClass(): HOC; 121 | 122 | declare function setStatic( 123 | key: string, 124 | value: any 125 | ): HOC; 126 | 127 | declare function setDisplayName( 128 | displayName: string 129 | ): HOC; 130 | 131 | declare function getDisplayName(C: Component): string; 132 | 133 | declare function wrapDisplayName(C: Component, wrapperName: string): string; 134 | 135 | declare function shallowEqual(a: Object, b: Object): boolean; 136 | 137 | declare function isClassComponent(value: any): boolean; 138 | 139 | declare type ReactNode = React$Element | Array>; 140 | 141 | declare function createEagerElement( 142 | type: Component | string, 143 | props: ?A, 144 | children?: ?ReactNode 145 | ): React$Element; 146 | 147 | declare function createEagerFactory( 148 | type: Component | string, 149 | ): ( 150 | props: ?A, 151 | children?: ?ReactNode 152 | ) => React$Element; 153 | 154 | declare function createSink(callback: (props: A) => void): Component; 155 | 156 | declare function componentFromProp(propName: string): Component; 157 | 158 | declare function nest( 159 | ...Components: Array | string> 160 | ): Component 161 | 162 | declare function hoistStatics>(hoc: H): H; 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/client/components/organisms/BattleScene.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react' 3 | import Modal from 'react-modal' 4 | import PropTypes from 'prop-types' 5 | import { StyleSheet, css } from 'aphrodite' 6 | import { 7 | addInputToQueue, 8 | requestPause, 9 | requestRestart, 10 | requestStart, 11 | // setSkillSelector, 12 | // unsetSkillSelector, 13 | moveSkillSelector, 14 | closeBattleSessionResult 15 | } from '../../actions/battleActions' 16 | import { popScene } from '../../actions/appActions' 17 | import Button from '../atoms/Button' 18 | import LogBoard from '../molecules/LogBoard' 19 | import AllyBattlersDisplay from '../molecules/AllyBattlersDisplay' 20 | import EnemyBattlersDisplay from '../molecules/EnemyBattlersDisplay' 21 | import InputQueueDisplay from '../molecules/InputQueueDisplay' 22 | import GlobalKeyListner from '../helpers/GlobalKeyListener' 23 | import type { BattleContainerProps } from '../../containers/BattleContainer' 24 | import type { BattleSessionResult } from 'domain/battle' 25 | 26 | export function BattleSessionResultModal(props: { 27 | isOpen: boolean, 28 | onClickClose: Function, 29 | result: ?BattleSessionResult 30 | }) { 31 | return ( 32 | 47 |

Reward

48 | {props.result && 49 | props.result.winner === 'ally' && 50 | props.result.rewards.resources.map((r, index) => ( 51 |

{r.resourceName}: {r.amount}

52 | ))} 53 | {props.result && 54 |
55 |

{props.result.winner} win.

56 | 57 |
} 58 | 59 | ) 60 | } 61 | 62 | export default class BattleScene extends React.Component { 63 | props: BattleContainerProps 64 | static contextTypes = { 65 | store: PropTypes.any 66 | } 67 | 68 | componentDidMount() { 69 | const { adventure: { adventureSession } } = this.context.store.getState() 70 | if (adventureSession) { 71 | this.props.dispatch(requestStart(adventureSession)) 72 | } 73 | } 74 | 75 | render() { 76 | const { runner, log, dispatch } = this.props 77 | if (!runner.battleSession) { 78 | return

Loading

79 | } else { 80 | const { battleSession, inputQueue, paused, skillSelectCursor } = runner 81 | return ( 82 |
88 | { 92 | dispatch(closeBattleSessionResult()) 93 | dispatch(popScene()) 94 | }} 95 | /> 96 | { 99 | if (paused) { 100 | dispatch(requestRestart()) 101 | } else { 102 | dispatch(requestPause()) 103 | } 104 | }} 105 | /> 106 | { 109 | dispatch(moveSkillSelector(0, -1)) 110 | }} 111 | /> 112 | { 115 | dispatch(moveSkillSelector(0, +1)) 116 | }} 117 | /> 118 | { 121 | dispatch(moveSkillSelector(-1, 0)) 122 | }} 123 | /> 124 | { 127 | dispatch(moveSkillSelector(+1, 0)) 128 | }} 129 | /> 130 | { 133 | if (skillSelectCursor) { 134 | const { x, y } = skillSelectCursor 135 | const ally = battleSession.battlers[y] 136 | if (ally) { 137 | const skill = ally.skills[x] 138 | if ( 139 | skill.cooldown.val >= skill.cooldown.max && 140 | !inputQueue.map(iq => iq.skillId).includes(skill.id) 141 | ) { 142 | dispatch(addInputToQueue(ally.id, skill.id)) 143 | } 144 | } 145 | } 146 | }} 147 | /> 148 |
149 | { 152 | dispatch(requestPause()) 153 | }} 154 | onClickRestart={_ => { 155 | dispatch(requestRestart()) 156 | }} 157 | /> 158 |
159 |
160 | b.side === 'enemy')} 162 | /> 163 |
164 |
165 | b.side === 'ally')} 168 | isSkillInQueue={skill => 169 | inputQueue.map(input => input.skillId).includes(skill.id)} 170 | onAllyAndSkillSelect={ally => skill => { 171 | // Check skill is executable with queue 172 | if ( 173 | skill.cooldown.val >= skill.cooldown.max && 174 | !inputQueue.map(iq => iq.skillId).includes(skill.id) 175 | ) { 176 | dispatch(addInputToQueue(ally.id, skill.id)) 177 | } 178 | }} 179 | /> 180 |
181 |
182 | 186 | {inputQueue.length > 0 &&
} 187 | 188 |
189 |
190 | ) 191 | } 192 | } 193 | } 194 | 195 | export function BattleSessionController({ 196 | paused, 197 | onClickPause, 198 | onClickRestart 199 | }: { 200 | paused: boolean, 201 | onClickPause: Function, 202 | onClickRestart: Function 203 | }) { 204 | return paused 205 | ?