diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 00000000..deb3f731 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/less/joust.less b/less/joust.less index d22afc30..ddc12115 100644 --- a/less/joust.less +++ b/less/joust.less @@ -297,11 +297,62 @@ div.visuals { width: 100%; } -.joust-message { - width: 100%; - text-align: center; - font-family: sans-serif; +.loading-screen { + position: relative; + display: flex; + flex-direction: column; margin: auto; + color: white; + font-family: "Belwe Bd BT"; + text-shadow: -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; + + @keyframes rotate { + from { + -webkit-transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + } + } + + .logo { + width: 8em; + position: relative; + margin: 2em auto; + animation-name: rotate; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: ease; + } + + .info { + height: 1em; + font-size: 2em; + margin: auto; + position: relative; + display: flex; + flex-direction: row; + + .left { + text-align: right; + width: 8em; + } + .center { + text-align: center; + margin-left: 1em; + margin-right: 1em; + } + .right { + text-align: left; + width: 8em; + } + } + .joust-message { + font-size: 1.5em; + } } .deck { diff --git a/ts/TexturePreloader.ts b/ts/TexturePreloader.ts index 33e3bd02..ba0151ea 100644 --- a/ts/TexturePreloader.ts +++ b/ts/TexturePreloader.ts @@ -7,6 +7,9 @@ class TexturePreloader extends Stream.Writable { protected textureQueue = ['GAME_005']; protected images = []; private working = 0; + private assetCount = 0; + private textureCount = 0; + private heroPowerCount = 0; protected assetQueue = ['cardback', 'hero_frame', 'hero_power', 'inhand_minion', 'inhand_spell', 'inhand_weapon', 'inhand_minion_legendary', 'mana_crystal', 'inplay_minion', 'effect_sleep', 'hero_power_exhausted', 'hero_armor', 'hero_attack', 'icon_deathrattle', 'icon_inspire', @@ -55,18 +58,23 @@ class TexturePreloader extends Stream.Writable { this.consume(); }; + let isAsset = false; + let isHeroPower = false; let file = this.assetQueue.shift(); if (!!this.assetDirectory && file) { file = this.assetDirectory + 'images/' + file + '.png'; + isAsset = true; } else { let cardId = this.textureQueue.shift(); - if (!this.cards.get(cardId)) { + let card = this.cards.get(cardId); + if (!card) { console.warn('No texture for ' + cardId + ' to preload'); next(); return; } - file = this.textureDirectory + this.cards.get(cardId).texture + '.jpg'; + isHeroPower = card.type == 'HERO_POWER' + file = this.textureDirectory + card.texture + '.jpg'; } if (this.fired[file]) { @@ -76,9 +84,22 @@ class TexturePreloader extends Stream.Writable { this.fired[file] = true; + let updateProgress = (asset: boolean, heroPower: boolean) => { + if (asset) { + this.assetCount++; + } + else { + this.textureCount++; + if (heroPower) { + this.heroPowerCount++; + } + } + next(); + }; + let image = new Image; - image.onload = next; - image.onerror = next; + image.onload =() => updateProgress(isAsset, isHeroPower); + image.onerror = () => updateProgress(isAsset, isHeroPower); image.src = file; this.images[this.images.length] = image; @@ -89,6 +110,14 @@ class TexturePreloader extends Stream.Writable { public canPreload(): boolean { return !!this.assetDirectory || !!this.textureDirectory; } + + public texturesReady(): boolean { + return (this.textureCount > 20 || !this.working) && this.heroPowerCount > 1; + } + + public assetsReady(): boolean { + return this.assetCount > 10 || !this.working; + } } export default TexturePreloader; diff --git a/ts/components/GameWidget.tsx b/ts/components/GameWidget.tsx index 29de451f..b93c527e 100644 --- a/ts/components/GameWidget.tsx +++ b/ts/components/GameWidget.tsx @@ -125,6 +125,7 @@ class GameWidget extends React.Component { assetDirectory={this.props.assetDirectory} textureDirectory={this.props.textureDirectory} cards={this.state.cards} swapPlayers={this.state.swapPlayers} cardOracle={this.state.isRevealingCards && this.state.cardOracle} + preloader={this.props.preloader} />); if (this.props.scrubber) { diff --git a/ts/components/GameWrapper.tsx b/ts/components/GameWrapper.tsx index a38677c3..6cca8138 100644 --- a/ts/components/GameWrapper.tsx +++ b/ts/components/GameWrapper.tsx @@ -2,15 +2,18 @@ import * as React from "react"; import {CardDataProps} from "../interfaces"; import GameState from "../state/GameState"; import TwoPlayerGame from "./game/TwoPlayerGame"; -import {CardType, OptionType} from "../enums"; +import {CardType, OptionType, GameTag} from "../enums"; import Entity from "../Entity"; import Option from "../Option"; import PlayerEntity from "../Player"; import {InteractiveBackend, CardOracleProps, AssetDirectoryProps, TextureDirectoryProps} from "../interfaces"; import {Zone} from "../enums"; +import TexturePreloader from "../TexturePreloader"; +import LoadingScreen from "./LoadingScreen" interface GameWrapperProps extends CardDataProps, CardOracleProps, AssetDirectoryProps, TextureDirectoryProps, React.Props { state: GameState; + preloader?: TexturePreloader; interaction?: InteractiveBackend; swapPlayers?: boolean; } @@ -23,11 +26,13 @@ class GameWrapper extends React.Component { private hasCheckedForSwap = false; private swapPlayers = false; + private lastLog: string; public render(): JSX.Element { var gameState = this.props.state; if (!gameState) { - return

Waiting for game state…

; + this.log('Waiting for game state...'); + return ; } var entityTree = gameState.getEntityTree(); @@ -36,19 +41,32 @@ class GameWrapper extends React.Component { // check if any entites are present var allEntities = gameState.getEntities(); if (!allEntities) { - return

Waiting for entities…

; + this.log('Waiting for entities...'); + return ; } // find the game entity var game = allEntities.filter(GameWrapper.filterByCardType(CardType.GAME)).first(); if (!game) { - return

Waiting for game…

; + this.log('Waiting for game...'); + return ; } // find the players var players = allEntities.filter(GameWrapper.filterByCardType(CardType.PLAYER)) as Immutable.Iterable; if (players.count() == 0) { - return

Waiting for players…

; + this.log('Waiting for players...') + return ; + } + + if (this.props.preloader && !this.props.preloader.assetsReady()) { + this.log('Waiting for assets...'); + return ; + } + + if (this.props.preloader && !this.props.preloader.texturesReady()) { + this.log('Waiting for textures...'); + return ; } // check if we need to swap the players @@ -100,6 +118,13 @@ class GameWrapper extends React.Component { return !!entity && entity.getCardType() === cardType; }; }; + + private log(message: string) { + if (message != this.lastLog) { + console.debug(message); + this.lastLog = message; + } + } } export default GameWrapper; diff --git a/ts/components/LoadingScreen.tsx b/ts/components/LoadingScreen.tsx new file mode 100644 index 00000000..0793b72f --- /dev/null +++ b/ts/components/LoadingScreen.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import PlayerEntity from "../Player"; +import {AssetDirectoryProps} from "../interfaces"; + +interface LoadingScreenProps extends AssetDirectoryProps, React.Props { + players?: Immutable.Iterable; +} + +class LoadingScreen extends React.Component { + + private currentMessage: string; + private lastUpdate = 0; + private messages = ['Sorting decks...', 'Painting cards...', 'Calculating lethal...', 'Calling customer service...', + 'SMOrc', 'Verifying the face is the place...', 'Summoning heroes...', 'Nerfing cards...', 'Buffing cards...']; + + + private getMessage(): string { + var now = new Date().getTime(); + if (!!this.messages.length && (now - this.lastUpdate) > 3000) { + var index = Math.floor(Math.random()*this.messages.length); + this.currentMessage = this.messages.splice(index, 1)[0]; + this.lastUpdate = now; + } + return this.currentMessage; + } + + public render(): JSX.Element { + return
{this.props.players ? +
+ {this.props.players.first().getName()} + VS + {this.props.players.last().getName()} +
:
} + + {this.getMessage()} +
; + } +} + +export default LoadingScreen; diff --git a/ts/interfaces.d.ts b/ts/interfaces.d.ts index e8deea7c..dc122634 100644 --- a/ts/interfaces.d.ts +++ b/ts/interfaces.d.ts @@ -9,6 +9,7 @@ import GameStateSink from "./state/GameStateSink"; import GameStateScrubber from "./state/GameStateScrubber"; import GameStateHistory from "./state/GameStateHistory"; import Player from "./Player"; +import TexturePreloader from "./TexturePreloader"; export interface DropTargetProps { connectDropTarget?(jsx); @@ -154,6 +155,7 @@ export interface GameWidgetProps extends AssetDirectoryProps, TextureDirectoryPr getImageURL?: (cardId: string) => string; exitGame?: () => void; cardOracle: CardOracle; + preloader?: TexturePreloader; width?: any; height?: any; } diff --git a/ts/run.tsx b/ts/run.tsx index fe255ab2..91ed5b63 100644 --- a/ts/run.tsx +++ b/ts/run.tsx @@ -98,6 +98,9 @@ class Viewer { this.opts.sink = sink; this.opts.scrubber = scrubber; this.opts.cardOracle = decoder; + if (preloader.canPreload()) { + this.opts.preloader = preloader; + } this.render(); }