+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/common/reducers/handlers/game/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cloneDeep } from 'lodash';
import HexUtils from '../../../components/hexgrid/HexUtils';
import { stringToType } from '../../../constants';
import * as w from '../../../types';
import { inBrowser, inTest } from '../../../util/browser';
import {
allObjectsOnBoard, applyAbilities, canActivate, checkVictoryConditions, currentPlayer,
dealDamageToObjectAtHex, executeCmd, getAttribute, hasEffect, logAction,
Expand All @@ -21,7 +22,7 @@ function selectTile(state: State, tile: w.HexId | null): State {

function resetTargetAndStatus(player: PlayerState): void {
player.status = { message: '', type: '' };
player.target = { choosing: false, chosen: null, possibleCardsInHand: [], possibleCardsInDiscardPile: [], possibleHexes: [] };
player.target = { choosing: false, chosen: null, numChoosing: 0, possibleCardsInHand: [], possibleCardsInDiscardPile: [], possibleHexes: [] };
}

export function deselect(state: State, playerColor: w.PlayerColor = state.currentTurn): State {
Expand Down Expand Up @@ -221,7 +222,7 @@ export function activateObject(state: State, abilityIdx: number, selectedHexId:
} catch (error) {
// TODO better error handling: throw a custom Error object that we handle in the game reducer?
console.error(error);
if (state.player === state.currentTurn) {
if (state.player === state.currentTurn && (inBrowser() || inTest())) {
// Show an alert only if it's the active player's turn (i.e. it's you and not your opponent who caused the error)
alert(`Oops!\n\n${error}`);
}
Expand All @@ -232,7 +233,7 @@ export function activateObject(state: State, abilityIdx: number, selectedHexId:
// Target still needs to be selected, so roll back playing the card (and return old state).
currentPlayer(state).target = player.target;
currentPlayer(state).status = {
message: `Choose a target for ${object.card.name}'s ${ability.text} ability.`,
message: `Choose ${player.target.numChoosing > 1 ? `${player.target.numChoosing} targets` : 'a target'} for ${object.card.name}'s ${ability.text} ability.`,
type: 'text'
};

Expand Down
11 changes: 6 additions & 5 deletions src/common/reducers/handlers/game/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { bluePlayerState, orangePlayerState } from '../../../store/defaultGameSt
import * as w from '../../../types';
import { assertCardVisible, quoteKeywords, splitSentences } from '../../../util/cards';
import { id, nextSeed } from '../../../util/common';
import { inBrowser, inTest } from '../../../util/browser';
import {
allHexIds, applyAbilities, checkVictoryConditions, currentPlayer,
deleteAllDyingObjects, discardCardsFromHand, executeCmd, getCost, logAction,
Expand Down Expand Up @@ -128,7 +129,7 @@ export function afterObjectPlayed(state: State, playedObject: w.Object): State {
} catch (error) {
// TODO better error handling: throw a custom Error object that we handle in the game reducer?
console.error(error);
if (state.player === state.currentTurn) {
if (state.player === state.currentTurn && (inBrowser() || inTest())) {
// Show an alert only if it's the active player's turn (i.e. it's you and not your opponent who caused the error)
alert(`Oops!\n\n${error}`);
}
Expand Down Expand Up @@ -173,7 +174,7 @@ export function placeCard(state: State, cardIdx: number, tile: w.HexId): State {

currentPlayer(state).target = player.target;
currentPlayer(state).status = {
message: `Choose a target for ${card.name}'s ability.`,
message: `Choose ${player.target.numChoosing > 1 ? `${player.target.numChoosing} targets` : 'a target'} for ${card.name}'s ability.`,
type: 'text'
};

Expand Down Expand Up @@ -227,7 +228,7 @@ function playEvent(state: State, cardIdx: number): State {
} catch (error) {
// TODO better error handling: throw a custom Error object that we handle in the game reducer?
console.error(error);
if (state.player === state.currentTurn) {
if (state.player === state.currentTurn && (inBrowser() || inTest())) {
// Show an alert only if it's the active player's turn (i.e. it's you and not your opponent who caused the error)
alert(`Oops!\n\n${error}`);
}
Expand All @@ -243,7 +244,7 @@ function playEvent(state: State, cardIdx: number): State {
state.callbackAfterTargetSelected = ((newState: State) => playEvent(newState, cardIdx));
currentPlayer(state).selectedCard = cardIdx;
currentPlayer(state).target = player.target;
currentPlayer(state).status = { message: `Choose a target for ${card.name}.`, type: 'text' };
currentPlayer(state).status = { message: `Choose ${player.target.numChoosing > 1 ? `${player.target.numChoosing} targets` : 'a target'} for ${card.name}.`, type: 'text' };
} else if (tempState.invalid) {
// Temp state is invalid (e.g. no valid target available or player unable to pay an energy cost).
// So return the old state.
Expand All @@ -256,7 +257,7 @@ function playEvent(state: State, cardIdx: number): State {
// In that case, the player needs to "target" the board to confirm that they want to play the event.
state.callbackAfterTargetSelected = ((newState: State) => playEvent(newState, cardIdx));
currentPlayer(state).selectedCard = cardIdx;
currentPlayer(state).target = { choosing: true, chosen: null, possibleCardsInHand: [], possibleCardsInDiscardPile: [], possibleHexes: allHexIds() };
currentPlayer(state).target = { choosing: true, chosen: null, numChoosing: 0, possibleCardsInHand: [], possibleCardsInDiscardPile: [], possibleHexes: allHexIds() };
currentPlayer(state).status = { message: `Click anywhere on the board to play ${card.name}.`, type: 'text' };
} else {
// Everything is good (valid state + no more targets to select), so we can return the new state!
Expand Down
1 change: 1 addition & 0 deletions src/common/store/defaultGameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function defaultTarget(): w.CurrentTarget {
return {
choosing: false,
chosen: null,
numChoosing: 0,
possibleCardsInHand: [],
possibleCardsInDiscardPile: [],
possibleHexes: []
Expand Down
1 change: 1 addition & 0 deletions src/common/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export interface PlayerStatus {
export interface CurrentTarget {
choosing: boolean
chosen: Array<CardInGame | HexId> | null
numChoosing: number
possibleCardsInDiscardPile: CardId[]
possibleCardsInHand: CardId[]
possibleHexes: HexId[]
Expand Down
4 changes: 2 additions & 2 deletions src/common/util/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import defaultState from '../store/defaultCollectionState';
import { CreatorStateProps } from '../containers/Creator';

import { ensureInRange, id as generateId } from './common';
import { fetchUniversal } from './browser';
import { fetchUniversal, onLocalhost } from './browser';
import { indexParsedSentence, lookupCurrentUser } from './firebase';

//
Expand Down Expand Up @@ -214,7 +214,7 @@ function parse(
.then((response) => response.json())
.then((json) => {
callback(idx, sentence, json);
if (index && json.tokens && json.js) {
if (index && json.tokens && json.js && !onLocalhost()) {
indexParsedSentence(sentence, json.tokens, json.js);
}
})
Expand Down
3 changes: 2 additions & 1 deletion src/common/util/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ function endTurn(state: w.GameState): w.GameState {
previousTurnPlayer.selectedCard = null;
previousTurnPlayer.selectedTile = null;
previousTurnPlayer.status.message = '';
previousTurnPlayer.target = { choosing: false, chosen: null, possibleHexes: [], possibleCardsInHand: [], possibleCardsInDiscardPile: [] };
previousTurnPlayer.target = { choosing: false, chosen: null, numChoosing: 0, possibleHexes: [], possibleCardsInHand: [], possibleCardsInDiscardPile: [] };
previousTurnPlayer.objectsOnBoard = mapValues(previousTurnPlayer.objectsOnBoard, ((obj) => ({
...obj,
attackedThisTurn: false,
Expand Down Expand Up @@ -804,6 +804,7 @@ export function setTargetAndExecuteQueuedAction(state: w.GameState, target: w.Ca
player.target = {
chosen: targets,
choosing: false,
numChoosing: player.target.numChoosing,
possibleHexes: [],
possibleCardsInHand: [],
possibleCardsInDiscardPile: []
Expand Down
86 changes: 54 additions & 32 deletions src/common/vocabulary/targets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { compact, flatMap, fromPairs, isEmpty } from 'lodash';
import { compact, flatMap, fromPairs, isEmpty, isString, uniqBy } from 'lodash';
import { shuffle } from 'seed-shuffle';

import { stringToType } from '../constants';
import * as g from '../guards';
import * as w from '../types';
import { arrayToSentence, id } from '../util/common';
import { inBrowser } from '../util/browser';
import {
allObjectsOnBoard, currentPlayer, getHex, logAction, logAndReturnTarget, opponent, opponentPlayer,
ownerOf
Expand All @@ -21,6 +22,30 @@ import {
// for targets['it']: itOverride > currentObject > state.it
// for targets['thisRobot']: currentObject > state.it
export default function targets(state: w.GameState, currentObject: w.Object | null, itOverride: w.Object | null): Record<string, w.Returns<w.Collection>> {
function logSelection(chosen: w.Targetable[], type: w.Collection['type']) {
if (chosen.length > 0) {
/* istanbul ignore else */
if (['cards', 'cardsInDiscardPile', 'objects'].includes(type)) {
const cards: Record<string, w.CardInGame> = fromPairs((chosen as Array<w.CardInGame | w.Object>).map((c: w.CardInGame | w.Object) =>
isString(c)
? [allObjectsOnBoard(state)[c]?.card?.name, allObjectsOnBoard(state)[c]?.card]
: g.isObject(c)
? [c.card.name, c.card]
: [c.name, c])
);
const names = Object.keys(cards).map((name) => `|${name}|`);
const explanationStr = `${arrayToSentence(names)} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr, cards);
} else if (type === 'players') {
const explanationStr = `${arrayToSentence((chosen as w.PlayerInGameState[]).map((p) => p.color))} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr);
} else if (type === 'hexes') {
const explanationStr = `${arrayToSentence(chosen as w.HexId[])} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr);
}
}
}

// Currently salient object
// Note: currentObject has higher salience than state.it .
// (This resolves the bug where robots' Haste ability would be triggered by other robots being played.)
Expand Down Expand Up @@ -68,33 +93,48 @@ export default function targets(state: w.GameState, currentObject: w.Object | nu

// Note: Unlike other target functions, choose() can also return a HexCollection
// (if the chosen hex does not contain an object.)
choose: <T extends w.Collection>(collection: T): T => {
choose: <T extends w.Collection>(collection: T, numChoices = 1): T => {
const player = currentPlayer(state);

if (player.target.chosen && player.target.chosen.length > 0) {
if (player.target.chosen && player.target.chosen.length >= numChoices) {
// Return and clear chosen target.

// If there's multiple targets, take the first (we treat target.chosen as a queue).
const [target, ...otherTargets] = player.target.chosen;
player.target.chosen = otherTargets;
// If there's multiple targets, take the first `numChoices` of them (we treat target.chosen as a queue).
const chosenTargets = player.target.chosen.slice(0, numChoices);
player.target.chosen = player.target.chosen.slice(numChoices);
const target = chosenTargets[0]; // select the first target for type detection

logSelection(chosenTargets, collection.type);

// enforce that targets are distinct (if numChoices > 1)
/* istanbul ignore if: this would be hard to unit-test */
if (uniqBy(chosenTargets, (t) => isString(t) ? t : t.id).length < chosenTargets.length) {
if (state.player === state.currentTurn && inBrowser()) {
// Show an alert only if it's the active player's turn
alert(`You must choose ${numChoices} unique targets!`);
}

state.invalid = true;
return { type: collection.type, entries: [] } as w.Collection as T;
}

if (g.isCardInGame(target)) {
state.it = target; // "it" stores most recently chosen salient object for lookup.
state.it = target; // "it" stores most recently chosen salient object for lookup (arbitrarily choosing the first one if there's a group of >1 target)
if (player.hand.map((card) => card.id).includes(target.id)) {
return { type: 'cards', entries: [target] } as w.CardInHandCollection as T;
return { type: 'cards', entries: chosenTargets } as w.CardInHandCollection as T;
} else if (player.discardPile.map((card) => card.id).includes(target.id)) {
return { type: 'cardsInDiscardPile', entries: [target] } as w.CardInDiscardPileCollection as T;
return { type: 'cardsInDiscardPile', entries: chosenTargets } as w.CardInDiscardPileCollection as T;
} else {
/* istanbul ignore next: this case should never be hit */
throw new Error(`Card chosen does not exist in player's hand or discard pile!: ${target}`);
}
} else {
// Return objects if possible or hexes if not.
if (collection.type === 'objects' && allObjectsOnBoard(state)[target]) {
state.it = allObjectsOnBoard(state)[target]; // "it" stores most recently chosen salient object for lookup.
return { type: 'objects', entries: [allObjectsOnBoard(state)[target]] } as w.ObjectCollection as T;
state.it = allObjectsOnBoard(state)[target]; // "it" stores most recently chosen salient object for lookup (arbitrarily choosing the first one if there's a group of >1 target)
return { type: 'objects', entries: chosenTargets.map((t) => allObjectsOnBoard(state)[t as w.HexId]) } as w.ObjectCollection as T;
} else {
return { type: 'hexes', entries: [target] } as w.HexCollection as T;
return { type: 'hexes', entries: chosenTargets } as w.HexCollection as T;
}
}
} else {
Expand All @@ -106,6 +146,7 @@ export default function targets(state: w.GameState, currentObject: w.Object | nu
player.target = {
...player.target,
choosing: true,
numChoosing: numChoices,
possibleCardsInHand: [],
possibleCardsInDiscardPile: [],
possibleHexes: []
Expand Down Expand Up @@ -200,26 +241,7 @@ export default function targets(state: w.GameState, currentObject: w.Object | nu

random: <T extends w.Collection>(num: number, collection: T): T => {
const chosen: w.Targetable[] = shuffle(collection.entries, state.rng()).slice(0, num);

// Log the random selection.
if (chosen.length > 0) {
/* istanbul ignore else */
if (['cards', 'cardsInDiscardPile', 'objects'].includes(collection.type)) {
const cards: Record<string, w.CardInGame> = fromPairs((chosen as Array<w.CardInGame | w.Object>).map((c: w.CardInGame | w.Object) =>
g.isObject(c) ? [c.card.name, c.card] : [c.name, c])
);
const names = Object.keys(cards).map((name) => `|${name}|`);
const explanationStr = `${arrayToSentence(names)} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr, cards);
} else if (collection.type === 'players') {
const explanationStr = `${arrayToSentence((chosen as w.PlayerInGameState[]).map((p) => p.color))} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr);
} else if (collection.type === 'hexes') {
const explanationStr = `${arrayToSentence(chosen as w.HexId[])} ${chosen.length === 1 ? 'was' : 'were'} selected`;
logAction(state, null, explanationStr);
}
}

logSelection(chosen, collection.type);
return { type: collection.type, entries: chosen } as w.Collection as T;
},

Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载