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
1 change: 1 addition & 0 deletions less/joust.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import "layout.less";
@import "scrubber.less";
@import "log.less";
@import "loadingscreen.less";
@import "font-awesome/font-awesome.less";
@import "animations.less";

Expand Down
13 changes: 13 additions & 0 deletions less/loadingscreen.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.joust-loading-screen {
font-family: "Belwe Bd BT", serif;
font-size: 3em;
text-align: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;

> p {
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"author": "Benedict Etzel <developer@beheh.de>",
"license": "UNLICENSED",
"dependencies": {
"async": "^2.0.0",
"autoprefixer": "^6.3.7",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
Expand Down
1 change: 1 addition & 0 deletions ts/components/GameWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class GameWidget extends React.Component<GameWidgetProps, GameWidgetState> {
cardArtDirectory={this.props.cardArtDirectory}
cards={this.state.cards}
swapPlayers={isSwapped}
hasStarted={this.props.scrubber.canInteract()}
cardOracle={this.state.cardOracle}
hideCards={!this.state.isRevealingCards} />;
let log = <Log key="log"
Expand Down
25 changes: 20 additions & 5 deletions ts/components/GameWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import Option from "../Option";
import PlayerEntity from "../Player";
import {InteractiveBackend, CardOracleProps, AssetDirectoryProps, CardArtDirectory} from "../interfaces";
import {Zone} from "../enums";
import LoadingScreen from "./LoadingScreen";

interface GameWrapperProps extends CardDataProps, CardOracleProps, AssetDirectoryProps, CardArtDirectory, HideCardsProps, React.Props<any> {
state: GameState;
interaction?: InteractiveBackend;
swapPlayers?: boolean;
hasStarted?: boolean;
}

/**
Expand All @@ -22,30 +24,39 @@ interface GameWrapperProps extends CardDataProps, CardOracleProps, AssetDirector
class GameWrapper extends React.Component<GameWrapperProps, {}> {

public render(): JSX.Element {

// check if we even have a game state
var gameState = this.props.state;
if (!gameState) {
return <p className="joust-message">Waiting for game state&hellip; </p>;
return this.renderLoadingScreen();
}

var entityTree = gameState.getEntityTree();
var optionTree = gameState.getOptionTree();

// check if any entites are present
// check if any entities are present
var allEntities = gameState.getEntities();
if (!allEntities) {
return <p className="joust-message">Waiting for entities&hellip; </p>;
return this.renderLoadingScreen();
}

// find the game entity
var game = allEntities.filter(GameWrapper.filterByCardType(CardType.GAME)).first();
if (!game) {
return <p className="joust-message">Waiting for game&hellip; </p>;
return this.renderLoadingScreen();
}

// find the players
var players = allEntities.filter(GameWrapper.filterByCardType(CardType.PLAYER)) as Immutable.Map<number, PlayerEntity>;
if (players.count() == 0) {
return <p className="joust-message">Waiting for players&hellip; </p>;
return this.renderLoadingScreen();
}

// wait for start
if(typeof this.props.hasStarted !== "undefined" && !this.props.hasStarted) {
return this.renderLoadingScreen(players.map((player: PlayerEntity) => {
return player.name;
}).toArray());
}

// find an end turn option
Expand Down Expand Up @@ -82,6 +93,10 @@ class GameWrapper extends React.Component<GameWrapperProps, {}> {
}
}

private renderLoadingScreen(players?: string[]) {
return <LoadingScreen players={players} />;
}

public static filterByCardType(cardType: CardType): (entity: Entity) => boolean {
return function(entity: Entity): boolean {
return !!entity && entity.getCardType() === cardType;
Expand Down
46 changes: 46 additions & 0 deletions ts/components/LoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from "react";
import MessagePicker from "./MessagePicker";

interface LoadingScreenProps extends React.Props<any> {
players?: string[];
}

class LoadingScreen extends React.Component<LoadingScreenProps, {}> {
private messages = [
"Sorting decks...",
"Painting cards...",
"Calculating lethal...",
"SMOrc",
"Ensuring face is the place...",
"Summoning heroes...",
"Nerfing cards...",
"Buffing cards...",
"Spreading Madness...",
"Spreading C'Thun's word...",
"Increasing base armor by 1...",

];

public render() {
let players = String.fromCharCode(160); // &nbsp;

if(this.props.players) {
switch(this.props.players.length) {
case 1:
players = this.props.players[0];
break;
case 2:
players = this.props.players[0] + " vs. " + this.props.players[1];
break;

}
}

return <div className="joust-loading-screen">
<p><MessagePicker interval={3} messages={this.messages} /></p>
<p className="participants">{players}</p>
</div>;
}
}

export default LoadingScreen;
70 changes: 70 additions & 0 deletions ts/components/MessagePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from "react";

interface MessagePickerProps extends React.Props<any> {
messages: string[];
interval: number;
random?: boolean;
}

interface MessagePickerState {
message?: number;
sequence?: number[];
}

class MessagePicker extends React.Component<MessagePickerProps, MessagePickerState> {

private interval: number;

public constructor(props: MessagePickerProps) {
super(props);
this.state = {
message: 0,
sequence: this.generateSequence()
};
}

public componentDidMount(): void {
this.scheduleUpdates();
}

public componentWillReceiveProps(nextProps: MessagePickerProps): void {
if(nextProps.messages.length != this.props.messages.length) {
clearInterval(this.interval);
this.setState({message: 0, sequence: this.generateSequence()});
this.scheduleUpdates();
}
}

public componentWillUnmount():void {
if(this.interval) {
clearInterval(this.interval);
}
}

private scheduleUpdates() {
this.interval = setInterval(this.cycleMessage.bind(this), this.props.interval * 1000);
}

public render(): JSX.Element {
let message = this.props.messages[this.state.sequence[this.state.message]];
message = message.replace('...', String.fromCharCode(8230)); // &hellip;
return <span>{message}</span>;
}

private generateSequence(): number[] {
var sequence = [];
for (let i = 0; i < this.props.messages.length; i++) {
sequence[i] = i;
}
if (this.props.random !== false) {
sequence = sequence.sort(() => {return 0.5 - Math.random()});
}
return sequence;
}

private cycleMessage(): void {
this.setState({message: (this.state.message + 1) % this.props.messages.length});
}
}

export default MessagePicker;
12 changes: 11 additions & 1 deletion ts/components/SetupWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import GameStateTracker from "../state/GameStateTracker";
import GameStateSink from "../state/GameStateSink";
import {CardOracle} from "../interfaces";
import * as Stream from "stream";
import * as async from "async";

interface SetupWidgetProps extends React.Props<any> {
defaultHostname: string;
Expand Down Expand Up @@ -103,7 +104,16 @@ class SetupWidget extends React.Component<SetupWidgetProps, SetupWidgetState> {
/* HSReplay -> Joust */

var scrubber = new GameStateScrubber();
scrubber.once("ready", () => scrubber.play());

async.parallel([
(cb) => {
scrubber.once("ready", () => cb());
},
(cb) => {
filestream.once("end", () => cb());
},
], () => scrubber.play());

var decoder = new HSReplayDecoder();
var sink = filestream // sink is returned by the last .pipe()
.pipe(decoder) // json -> mutators
Expand Down
21 changes: 19 additions & 2 deletions ts/run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as URL from "url";
import {QueryCardMetadata} from "./interfaces";
import TexturePreloader from "./TexturePreloader";
import {EventEmitter} from "events";
import * as async from "async";

var React = require('react');
var ReactDOM = require('react-dom');
Expand All @@ -23,6 +24,7 @@ class Launcher {
protected target: string | HTMLElement;
protected opts: GameWidgetProps;
protected queryCardMetadata: QueryCardMetadata;
protected startFromTurn: number;
protected turnCb: (turn: number) => void;
protected ref: GameWidget;

Expand Down Expand Up @@ -80,6 +82,11 @@ class Launcher {
return this;
}

public turn(turn: number): Launcher {
this.startFromTurn = turn;
return this;
}

public logger(logger: (message: string | Error) => void): Launcher {
this.opts.logger = logger;
return this;
Expand Down Expand Up @@ -107,8 +114,7 @@ class Launcher {
var decoder = new HSReplayDecoder();
decoder.debug = this.opts.debug;
var tracker = new GameStateTracker();
var scrubber = new GameStateScrubber();
scrubber.once("ready", () => scrubber.play());
var scrubber = new GameStateScrubber(null, this.startFromTurn);
if(this.turnCb) {
scrubber.on("turn", this.turnCb);
}
Expand Down Expand Up @@ -137,6 +143,17 @@ class Launcher {
component.on('error', this.log.bind(this));
});

async.parallel([
(cb) => {
scrubber.once("ready", () => cb());
},
(cb) => {
decoder.once("end", () => cb());
}
], () => {
scrubber.play()
});

message
.pipe(decoder) // json -> mutators
.pipe(tracker) // mutators -> latest gamestate
Expand Down
25 changes: 24 additions & 1 deletion ts/state/GameStateScrubber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class GameStateScrubber extends Stream.Duplex implements StreamScrubber {
protected history: GameStateHistory;
protected inhibitor: StreamScrubberInhibitor;

constructor(history?: GameStateHistory, opts?: Stream.DuplexOptions) {
constructor(history?: GameStateHistory, startFromTurn?: number, opts?: Stream.DuplexOptions) {
opts = opts || {};
opts.objectMode = true;
opts.allowHalfOpen = true;
Expand All @@ -22,13 +22,17 @@ class GameStateScrubber extends Stream.Duplex implements StreamScrubber {
this.history = history || new GameStateHistory();
this.lastState = null;
this.endTime = null;
this.hasEmittedReady = false;
this.hasStarted = false;
this.startFromTurn = startFromTurn || null;
}

protected initialTime: number;
protected currentTime: number;
protected endTime: number;
protected hasEmittedReady: boolean;
protected hasStarted: boolean;
protected startFromTurn: number;

_write(gameState: any, encoding: string, callback: Function): void {
var time = gameState.getTime();
Expand All @@ -50,8 +54,17 @@ class GameStateScrubber extends Stream.Duplex implements StreamScrubber {
}
}

if (!this.hasStarted && this.currentTime === 0 && this.startFromTurn) {
ready = false;
if(this.history.turnMap.has(this.startFromTurn)) {
this.currentTime = this.history.turnMap.get(this.startFromTurn).getTime();
ready = true;
}
}

if (ready) {
this.emit("ready");
this.hasEmittedReady = true;
this.update();
}

Expand All @@ -62,6 +75,15 @@ class GameStateScrubber extends Stream.Duplex implements StreamScrubber {
return;
}

end(): void {
if(!this.hasEmittedReady) {
// this might happen if a initial turn is requested that we never found
this.emit("ready");
this.hasEmittedReady = true;
this.update();
}
}

protected lastUpdate: number;
protected interval: number;

Expand All @@ -70,6 +92,7 @@ class GameStateScrubber extends Stream.Duplex implements StreamScrubber {
this.interval = setInterval(this.update.bind(this), 100);
this.hasStarted = true;
this.emit("play");
this.lastState = null;
this.update();
}

Expand Down
1 change: 1 addition & 0 deletions typings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"globalDependencies": {
"async": "registry:dt/async#1.4.2+20160602152629",
"immutable": "registry:dt/immutable#3.8.1+20160608070634",
"jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#d594ef506d1efe2fea15f8f39099d19b39436b71",
"lodash": "github:DefinitelyTyped/DefinitelyTyped/lodash/lodash.d.ts#ad1c4f411a9ad949c0c73de4d190be1b2bf08a6f",
Expand Down