You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
12 KiB
367 lines
12 KiB
import { ErrorType, EventResult, IncomingEvent, IncomingEventType, OutgoingEventType } from './gameEvent'
|
|
import { GamePhase, GameState, oppositePlayer, PlayerSide, PlayerState, RoundStarter } from './gameState'
|
|
import {
|
|
DieFace,
|
|
DieResult,
|
|
DieState,
|
|
DieType,
|
|
isSelectedState,
|
|
setDeselected,
|
|
setSelected,
|
|
toggleSelected,
|
|
} from './dieState'
|
|
import { isValidIndex } from '../util/precondition'
|
|
import {
|
|
calculateBonuses,
|
|
calculateDamage,
|
|
calculateFails,
|
|
calculateRecovery,
|
|
calculateStops,
|
|
determineNextRoundStarter,
|
|
} from './calculateDamage'
|
|
|
|
export function handleEvent(state: GameState, event: IncomingEvent): EventResult<GameState> {
|
|
try {
|
|
switch (event.type) {
|
|
case IncomingEventType.SelectAction:
|
|
return handleSelectActionEvent<GameState>(state, event)
|
|
case IncomingEventType.ToggleSelectDie:
|
|
return handleToggleSelectIndexEvent<DieResult, GameState>(state, event)
|
|
case IncomingEventType.SelectAllDice:
|
|
return handleSelectAllEvent<DieResult, GameState>(state, true)
|
|
case IncomingEventType.DeselectAllDice:
|
|
return handleSelectAllEvent<DieResult, GameState>(state, false)
|
|
case IncomingEventType.HoldSelectedDice:
|
|
return handleHoldDiceEvent<DieResult, GameState>(state, true)
|
|
case IncomingEventType.ReleaseSelectedDice:
|
|
return handleHoldDiceEvent<DieResult, GameState>(state, false)
|
|
case IncomingEventType.EndTurn:
|
|
return handleEndTurnEvent<PlayerState, GameState>(state)
|
|
case IncomingEventType.Abort:
|
|
return handleAbortEvent<GameState>(state, event)
|
|
default:
|
|
return {
|
|
newState: state,
|
|
error: {
|
|
type: ErrorType.UnexpectedError,
|
|
message: 'Event type not yet implemented',
|
|
},
|
|
}
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
newState: state,
|
|
error: {
|
|
type: ErrorType.UnexpectedError,
|
|
message: `Failed handling event: ${e}`,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface SelectActionState<A> {
|
|
readonly gamePhase: GamePhase
|
|
readonly theme: {
|
|
readonly actions: readonly A[]
|
|
}
|
|
readonly action: A
|
|
}
|
|
|
|
export function handleSelectActionEvent<T extends SelectActionState<unknown>>(
|
|
state: T,
|
|
{ index }: { readonly index: number },
|
|
): EventResult<T> {
|
|
if (state.gamePhase !== GamePhase.ROUND_START) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `You can only change actions when the turn has not started yet.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
if (index < 0 || index >= state.theme.actions.length || index !== Math.floor(index)) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.InvalidIndex,
|
|
message: `Select an integer action index between 0 (inclusive) and ${state.theme.actions.length} (exclusive)`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
return {
|
|
newState: {
|
|
...state,
|
|
action: state.theme.actions[index],
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
|
|
export interface DiceStatesState<D extends { readonly state: DieState }> {
|
|
readonly gamePhase: GamePhase
|
|
readonly lastRoll: readonly D[]
|
|
}
|
|
|
|
export function handleToggleSelectIndexEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
|
|
state: T,
|
|
{ index }: { readonly index: number },
|
|
): EventResult<T> {
|
|
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `You can only toggle the selected state of a die when dice have been rolled.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
if (!isValidIndex(index, state.lastRoll.length)) {
|
|
return {
|
|
newState: state,
|
|
error: {
|
|
type: ErrorType.InvalidIndex,
|
|
message: `Select an integer action index between 0 (inclusive) and ${state.lastRoll.length} (exclusive)`,
|
|
},
|
|
}
|
|
}
|
|
return {
|
|
newState: {
|
|
...state,
|
|
lastRoll: [
|
|
...state.lastRoll.slice(0, index),
|
|
{
|
|
...state.lastRoll[index],
|
|
state: toggleSelected(state.lastRoll[index].state),
|
|
},
|
|
...state.lastRoll.slice(index + 1),
|
|
],
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
|
|
function handleSelectAllEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
|
|
state: T,
|
|
select: boolean,
|
|
): EventResult<T> {
|
|
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `You can only change the selected state of the dice when dice have been rolled.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
return {
|
|
newState: {
|
|
...state,
|
|
lastRoll: state.lastRoll.map<D>((x) => ({
|
|
...x,
|
|
state: select ? setSelected(x.state) : setDeselected(x.state),
|
|
})),
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
|
|
function handleHoldDiceEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
|
|
state: T,
|
|
hold: boolean,
|
|
): EventResult<T> {
|
|
if (state.gamePhase !== GamePhase.TURN_ROLLED) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `You can only toggle the hold state of dice when dice have been rolled.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
return {
|
|
newState: {
|
|
...state,
|
|
lastRoll: state.lastRoll.map<D>((v) => {
|
|
if (isSelectedState(v.state)) {
|
|
return {
|
|
...v,
|
|
state: hold ? DieState.HELD_SELECTED : DieState.SELECTED,
|
|
}
|
|
} else {
|
|
return v
|
|
}
|
|
}),
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
|
|
export interface EndTurnPlayer {
|
|
readonly damage: number
|
|
readonly damageMax: number
|
|
readonly damageTotal: number
|
|
readonly stopCount: number
|
|
readonly failCount: number
|
|
readonly damageBonuses: number
|
|
readonly nextRecoverAt: number
|
|
readonly timesRecovered: number
|
|
}
|
|
|
|
export interface EndTurnState<P extends EndTurnPlayer> {
|
|
readonly difficulty: {
|
|
readonly roundStarter?: PlayerSide | RoundStarter
|
|
readonly stats: {
|
|
[key in PlayerSide]: {
|
|
readonly maxDamageBonuses?: number
|
|
readonly minFailCount?: number
|
|
readonly maxStopCount?: number
|
|
readonly damageBonusBase?: number
|
|
readonly damageBonusIncrement?: number
|
|
readonly recoverBase?: number
|
|
readonly recoverIncrement?: number
|
|
readonly recoverPercentBase?: number
|
|
readonly recoverPercentIncrement?: number
|
|
}
|
|
}
|
|
}
|
|
readonly gamePhase: GamePhase
|
|
readonly phaseOwner: PlayerSide
|
|
readonly roundOwner: PlayerSide
|
|
readonly action: {
|
|
readonly dice: readonly DieType[]
|
|
readonly givesDamageBonus?: boolean
|
|
readonly givesStopCount?: boolean
|
|
readonly givesFailCount?: boolean
|
|
readonly canFinish?: boolean
|
|
}
|
|
readonly players: {
|
|
readonly [key in PlayerSide]: P
|
|
}
|
|
readonly lastRoll: readonly { readonly face: DieFace }[]
|
|
readonly lastTurnTotal: number
|
|
readonly currentTurnTotal: number
|
|
readonly selectedDiceValue: number
|
|
readonly currentDiceEndTurn: boolean
|
|
readonly countedFails: number
|
|
}
|
|
|
|
function handleEndTurnEvent<P extends EndTurnPlayer, T extends EndTurnState<P>>(state: T): EventResult<T> {
|
|
if (state.gamePhase !== GamePhase.TURN_ROLLED && state.gamePhase !== GamePhase.TURN_FAILED) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `You can only end your turn when it's in progress.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
const failed = state.gamePhase === GamePhase.TURN_FAILED || !state.currentDiceEndTurn
|
|
const delta = state.lastTurnTotal - (failed ? 0 : state.currentTurnTotal)
|
|
if (delta < 0) {
|
|
// The winner of this round has not yet been decided; this is better than the last turn this round.
|
|
// Moving on to the next turn...
|
|
return {
|
|
newState: {
|
|
...state,
|
|
lastTurnTotal: state.currentTurnTotal,
|
|
currentTurnTotal: 0,
|
|
selectedDiceValue: 0,
|
|
|
|
lastRoll: [],
|
|
phaseOwner: oppositePlayer(state.phaseOwner),
|
|
},
|
|
events: [
|
|
{
|
|
type: OutgoingEventType.PassTurn,
|
|
target: state.phaseOwner,
|
|
},
|
|
{
|
|
type: OutgoingEventType.StartTurn,
|
|
target: oppositePlayer(state.phaseOwner),
|
|
},
|
|
],
|
|
}
|
|
} else {
|
|
// That's the round.
|
|
// Time to calculate your punishment.
|
|
const player = state.players[state.phaseOwner]
|
|
const opponent = state.players[oppositePlayer(state.phaseOwner)]
|
|
const stats = state.difficulty.stats[state.phaseOwner]
|
|
const damageDelta = calculateDamage(delta, player, stats)
|
|
const newDamage = player.damage + damageDelta
|
|
const newTotal = player.damageTotal + damageDelta
|
|
const stops = state.action.givesStopCount ? calculateStops(player, stats) : player.stopCount
|
|
const fails = state.action.givesFailCount ? calculateFails(player, stats) : player.failCount
|
|
const bonuses = state.action.givesDamageBonus ? calculateBonuses(player, stats) : player.damageBonuses
|
|
const opponentDamage = calculateRecovery(
|
|
player,
|
|
opponent,
|
|
state.difficulty.stats[oppositePlayer(state.phaseOwner)],
|
|
)
|
|
const playerResult: P = {
|
|
...player,
|
|
damage: newDamage,
|
|
damageTotal: newTotal,
|
|
stopCount: stops,
|
|
failCount: fails,
|
|
damageBonuses: bonuses,
|
|
}
|
|
const opponentResult: P = {
|
|
...player,
|
|
damage: opponentDamage,
|
|
}
|
|
const players =
|
|
state.phaseOwner === PlayerSide.TOP
|
|
? { top: playerResult, bottom: opponentResult }
|
|
: { top: opponentResult, bottom: playerResult }
|
|
// Oh, no... That's game over for you, my friend...
|
|
const newPhase =
|
|
state.action.canFinish && playerResult.damage >= playerResult.damageMax
|
|
? GamePhase.VICTORY
|
|
: GamePhase.ROUND_START
|
|
const newRoundOwner =
|
|
newPhase === GamePhase.VICTORY
|
|
? oppositePlayer(state.phaseOwner)
|
|
: determineNextRoundStarter(state, state.difficulty)
|
|
return {
|
|
newState: {
|
|
...state,
|
|
players,
|
|
currentDiceEndTurn: false,
|
|
gamePhase: newPhase,
|
|
phaseOwner: newRoundOwner,
|
|
roundOwner: newRoundOwner,
|
|
lastRoll: [],
|
|
currentTurnTotal: 0,
|
|
lastTurnTotal: 0,
|
|
countedFails: 0,
|
|
selectedDiceValue: 0,
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleAbortEvent<T extends { readonly gamePhase: GamePhase; readonly phaseOwner: PlayerSide }>(
|
|
state: T,
|
|
{ player }: { readonly player: PlayerSide },
|
|
): EventResult<T> {
|
|
if (state.gamePhase === GamePhase.ABORTED || state.gamePhase === GamePhase.VICTORY) {
|
|
return {
|
|
error: {
|
|
type: ErrorType.NotValidRightNow,
|
|
message: `The game is already over.`,
|
|
},
|
|
newState: state,
|
|
}
|
|
}
|
|
return {
|
|
newState: {
|
|
...state,
|
|
gamePhase: GamePhase.ABORTED,
|
|
phaseOwner: player ?? state.phaseOwner,
|
|
},
|
|
events: [],
|
|
}
|
|
}
|
|
|