1
0
Fork 0

Added auto-saving to cloud, and loading cloud saves

This commit is contained in:
Michaël Lemaire 2017-05-05 01:19:28 +02:00
parent ed86f57529
commit 27302267b9
27 changed files with 559 additions and 86 deletions

3
TODO
View file

@ -50,7 +50,6 @@
* Menu: fix background stars aggregating at right side when the game is not focused
* Add ship personality (with icons to identify ?)
* Tutorial
* Campaign save slots, with auto-save
* Missions/quests system
* Main story arc
@ -58,5 +57,3 @@ Later, if possible:
* Replays
* Multiplayer
* Formation or deployment phase
* Saving to external file
* Saving to cloud

View file

@ -11,7 +11,6 @@
"dependencies": {
"phaser": "2.6.2",
"parse": "1.9.2",
"jasmine-core": "jasmine#^2.5.2",
"deep-diff": "0.3.0"
"jasmine-core": "jasmine#^2.5.2"
}
}

View file

@ -15,7 +15,7 @@
padding: 0;
margin: 0;
}
.game {
width: 100%;
height: 100vh;
@ -34,7 +34,6 @@
window.oncontextmenu = function (e) { e.preventDefault(); };
window.onload = function () {
window.ui = new TS.SpaceTac.MainUI();
window.connection = new TS.SpaceTac.Multi.Connection(window.ui);
};
</script>
</body>

View file

@ -7,22 +7,23 @@
<link rel="stylesheet" href="vendor/jasmine-core/lib/jasmine-core/jasmine.css">
<style>
canvas {
display: none;
}
canvas {
display: none;
}
</style>
</head>
<body>
<div style="display: none; visibility: hidden; height: 0; overflow: hidden;">
<div id="-space-tac" class="game"></div>
</div>
<div id="-space-tac" class="game"></div>
</div>
<script src="vendor/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="vendor/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="vendor/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="vendor/phaser/build/phaser.min.js"></script>
<script src="build.js"></script>
<script src="vendor/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="vendor/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="vendor/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="vendor/parse/parse.min.js"></script>
<script src="vendor/phaser/build/phaser.min.js"></script>
<script src="build.js"></script>
</body>
</html>
</html>

View file

@ -4,6 +4,7 @@
"description": "A tactical RPG set in space",
"main": "src/build.js",
"scripts": {
"shell": "${SHELL} || true",
"postinstall": "bower install && typings install",
"build": "tsc -p .",
"pretest": "tsc -p .",
@ -19,6 +20,7 @@
"author": "Michael Lemaire",
"license": "MIT",
"devDependencies": {
"babel-polyfill": "^6.23.0",
"bower": "~1.8",
"codecov": "~2.1",
"jasmine": "~2.5",
@ -26,9 +28,9 @@
"karma-coverage": "~1.1",
"karma-jasmine": "~1.1",
"karma-phantomjs-launcher": "~1.0",
"remap-istanbul": "~0.9",
"live-server": "~1.2",
"remap-istanbul": "~0.9",
"typescript": "~2.3",
"typings": "~2.1"
}
}
}

View file

@ -1,5 +1,5 @@
// karma.conf.js
module.exports = function(config) {
module.exports = function (config) {
config.set({
basePath: '../..',
frameworks: ['jasmine'],
@ -12,14 +12,16 @@ module.exports = function(config) {
'out/build.js': ['coverage']
},
coverageReporter: {
type : 'json',
dir : 'out/coverage/',
type: 'json',
dir: 'out/coverage/',
subdir: '.',
file: 'coverage.json'
},
files: [
'node_modules/babel-polyfill/dist/polyfill.js',
'out/vendor/phaser/build/phaser.js',
'out/vendor/parse/parse.min.js',
'out/build.js'
]
})

View file

@ -80,6 +80,14 @@ module TS.SpaceTac {
}
}
/**
* Set the current game session, and redirect to view router
*/
setSession(session: GameSession): void {
this.session = session;
this.state.start("router");
}
/**
* Load current game from local browser storage
*/
@ -99,5 +107,24 @@ module TS.SpaceTac {
return false;
}
}
/**
* Get an hopefully unique device identifier
*/
getDeviceId(): string | null {
if (this.storage) {
const key = "spacetac-device-id";
let stored = this.storage.getItem(key);
if (stored) {
return stored;
} else {
let generated = RandomGenerator.global.id(20);
this.storage.setItem(key, generated);
return generated;
}
} else {
return null;
}
}
}
}

@ -1 +1 @@
Subproject commit 28bf87b126d13f3144955cba4c07bbe03c65e224
Subproject commit bc39ed78ef474e42cb20b3dbfc3dffa80c07ffe7

View file

@ -1,17 +1,34 @@
module TS.SpaceTac {
// A game session, binding a universe and a player
/**
* A game session, binding a universe and a player
*
* This represents the current state of game
*/
export class GameSession {
// "Hopefully"" unique session id
id: string
// Game universe
universe: Universe;
universe: Universe
// Current connected player
player: Player;
player: Player
constructor() {
this.id = RandomGenerator.global.id(20);
this.universe = new Universe();
this.player = new Player(this.universe);
}
/**
* Get an indicative description of the session (to help identify game saves)
*/
getDescription(): string {
let level = this.player.fleet.getLevel();
let ships = this.player.fleet.ships.length;
return `Level ${level} - ${ships} ships`;
}
// Load a game state from a string
static loadFromString(serialized: string): GameSession {
var serializer = new Serializer(TS.SpaceTac);

View file

@ -0,0 +1,86 @@
module TS.SpaceTac.Multi.Specs {
describe("Connection", function () {
async_it("finds an unused token", async function () {
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
let token = await connection.getUnusedToken(5);
expect(token.length).toBe(5);
await storage.upsert("sessioninfo", { token: token }, {});
spyOn(connection, "generateToken").and.returnValues(token, "123456");
let other = await connection.getUnusedToken(5);
expect(other).toEqual("123456");
});
async_it("loads a session by its id", async function () {
let session = new GameSession();
let serializer = new Serializer(TS.SpaceTac);
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
let result = await connection.loadById("abc");
expect(result).toBeNull();
await storage.upsert("session", { ref: "abc" }, { data: serializer.serialize(session) });
result = await connection.loadById("abc");
expect(result).toEqual(session);
result = await connection.loadById("abcd");
expect(result).toBeNull();
// even from another device
let other = new Connection("notest", storage);
result = await other.loadById("abc");
expect(result).toEqual(session);
// do not load if it is not a GameSession
await storage.upsert("session", { ref: "abcd" }, { data: serializer.serialize(new Player()) });
result = await connection.loadById("abcd");
expect(result).toBeNull();
});
async_it("lists saves from a device", async function () {
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
let result = await connection.listSaves();
expect(result).toEqual({});
await storage.upsert("sessioninfo", { device: "test", ref: "abc" }, { info: "ABC" });
await storage.upsert("sessioninfo", { device: "other", ref: "abcd" }, { info: "ABCD" });
await storage.upsert("sessioninfo", { device: "test", ref: "cba" }, { info: "CBA" });
result = await connection.listSaves();
expect(result).toEqual({ abc: "ABC", cba: "CBA" });
});
async_it("publishes saves and retrieves them by token", async function () {
let session = new GameSession();
let storage = new FakeRemoteStorage();
let connection = new Connection("test", storage);
let saves = await connection.listSaves();
expect(items(saves).length).toEqual(0);
let token = await connection.publish(session, "TEST");
saves = await connection.listSaves();
expect(items(saves).length).toEqual(1);
let loaded = await connection.loadByToken(token);
expect(loaded).toEqual(session);
let newtoken = await connection.publish(nn(loaded), "TEST");
expect(token).toEqual(newtoken);
loaded = await connection.loadByToken(token);
expect(loaded).toEqual(session);
saves = await connection.listSaves();
expect(items(saves).length).toEqual(1);
});
});
}

View file

@ -1,42 +1,53 @@
/// <reference path="Parse.d.ts" />
module TS.SpaceTac.Multi {
/**
* Multiplayer connection to a Parse server
*/
export class Connection {
ui: MainUI
device_id: string
serializer = new Serializer(TS.SpaceTac)
model_session = Parse.Object.extend("SpaceTacSession")
token_chars = "abcdefghjkmnpqrstuvwxyz123456789"
storage: IRemoteStorage
constructor(ui: MainUI) {
this.ui = ui;
constructor(device_id: string, storage: IRemoteStorage) {
this.device_id = device_id;
this.storage = storage;
}
Parse.initialize("thunderk.net");
Parse.serverURL = 'https://rs.thunderk.net/parse';
/**
* Generate a random token
*/
generateToken(length: number): string {
return range(length).map(() => RandomGenerator.global.choice(<any>this.token_chars)).join("");
}
/**
* Find an unused session token
*/
getUnusedToken(length = 5): string {
let token = range(length).map(() => RandomGenerator.global.choice(<any>this.token_chars)).join("");
// TODO check if it is unused on server
async getUnusedToken(length = 5): Promise<string> {
let token = this.generateToken(length);
let existing = await this.storage.search("sessioninfo", { token: token });
if (existing.length > 0) {
token = await this.getUnusedToken(length + 1);
}
return token;
}
/**
* Publish current session to remote server, and return a session token
* Publish a session to remote server, and return an invitation token
*/
publish(): string {
let session = new this.model_session();
let token = this.getUnusedToken();
async publish(session: GameSession, description: string): Promise<string> {
await this.storage.upsert("session", { ref: session.id }, { data: this.serializer.serialize(session) });
session.set("token", token);
session.set("data", this.serializer.serialize(this.ui.session));
let now = new Date();
let date = now.toISOString().substr(0, 10) + " " + now.toTimeString().substr(0, 5);
let info = `${date}\n${description}`;
session.save();
let sessinfo = await this.storage.find("sessioninfo", { ref: session.id, device: this.device_id });
let token: string = sessinfo ? sessinfo.token : "";
if (token.length == 0) {
token = await this.getUnusedToken();
}
await this.storage.upsert("sessioninfo", { ref: session.id, device: this.device_id, token: token }, { info: info });
return token;
}
@ -44,21 +55,35 @@ module TS.SpaceTac.Multi {
/**
* Load a session from a remote server, by its token
*/
load(token: string): void {
let query = new Parse.Query(this.model_session);
query.equalTo("token", token);
query.find({
success: (results: any) => {
if (results.length == 1) {
let data = results[0].get("data");
let session = this.serializer.unserialize(data);
if (session instanceof GameSession) {
this.ui.session = session;
this.ui.state.start('router');
}
}
async loadByToken(token: string): Promise<GameSession | null> {
let info = await this.storage.find("sessioninfo", { token: token });
if (info) {
return this.loadById(info.ref);
} else {
return null;
}
}
/**
* Load a session from a remote server, by its id
*/
async loadById(id: string): Promise<GameSession | null> {
let session = await this.storage.find("session", { ref: id });
if (session) {
let loaded = this.serializer.unserialize(session.data);
if (loaded instanceof GameSession) {
return loaded;
}
});
}
return null;
}
/**
* List cloud saves, associated with current device
*/
async listSaves(): Promise<{ [id: string]: string }> {
let results = await this.storage.search("sessioninfo", { device: this.device_id });
return dict(results.map(obj => <[string, string]>[obj.ref, obj.info]));
}
}
}

View file

@ -0,0 +1,51 @@
module TS.SpaceTac.Multi.Specs {
describe("FakeRemoteStorage", function () {
async_it("can fetch a single record", async function () {
let storage = new FakeRemoteStorage();
let result = await storage.find("test", { key: 5 });
expect(result).toBeNull();
await storage.upsert("test", { key: 5 }, { text: "thingy" });
result = await storage.find("test", { key: 5 });
expect(result).toEqual({ key: 5, text: "thingy" });
result = await storage.find("test", { key: 6 });
expect(result).toBeNull();
result = await storage.find("test", { key: 5, text: "thingy" });
expect(result).toEqual({ key: 5, text: "thingy" });
result = await storage.find("notest", { key: 5 });
expect(result).toBeNull();
});
async_it("inserts or updates objects", async function () {
let storage = new FakeRemoteStorage();
let result = await storage.search("test", { key: 5 });
expect(result).toEqual([]);
await storage.upsert("test", { key: 5 }, {});
result = await storage.search("test", { key: 5 });
expect(result).toEqual([{ key: 5 }]);
await storage.upsert("test", { key: 5 }, { text: "thingy" });
result = await storage.search("test", { key: 5 });
expect(result).toEqual([{ key: 5, text: "thingy" }]);
await storage.upsert("test", { key: 5 }, { text: "other thingy" });
result = await storage.search("test", { key: 5 });
expect(result).toEqual([{ key: 5, text: "other thingy" }]);
await storage.upsert("test", { key: 5, text: "things" }, {});
result = await storage.search("test", { key: 5 });
expect(result.sort((a: any, b: any) => cmp(a.text, b.text))).toEqual([{ key: 5, text: "other thingy" }, { key: 5, text: "things" }]);
});
});
}

125
src/multi/RemoteStorage.ts Normal file
View file

@ -0,0 +1,125 @@
/// <reference path="Parse.d.ts" />
module TS.SpaceTac.Multi {
/**
* Interface for a remote storage, used for networking/multiplayer features
*/
export interface IRemoteStorage {
/**
* Search through a collection for equality of some fields
*/
search(collection: string, fields: any): Promise<any[]>
/**
* Find a single object with equality of some fields
*/
find(collection: string, fields: any): Promise<any>
/**
* Insert or update an object in a collection, based on some unicity fields
*/
upsert(collection: string, unicity: any, additional: any): Promise<void>
}
/**
* Remote storage using the Parse protocol
*/
export class ParseRemoteStorage implements IRemoteStorage {
constructor() {
Parse.initialize("thunderk.net");
Parse.serverURL = 'https://rs.thunderk.net/parse';
}
/**
* Unpack a Parse.Object to a javascript object
*/
static unpack(obj: Parse.Object): Object {
return obj.toJSON();
}
/**
* Get the Parse model for a given collection name.
*/
private getModel(collection: string): any {
return Parse.Object.extend("spacetac" + collection);
}
async search(collection: string, fields: any) {
let query = new Parse.Query(this.getModel(collection));
iteritems(fields, (key, value) => {
query.equalTo(key, value);
});
let results = await query.find();
return results.map(ParseRemoteStorage.unpack);
}
async find(collection: string, fields: any) {
let results = await this.search(collection, fields);
if (results.length == 1) {
return results[0];
} else {
return null;
}
}
async upsert(collection: string, unicity: any, additional: any) {
let query = new Parse.Query(this.getModel(collection));
iteritems(unicity, (key, value) => {
query.equalTo(key, value);
});
let results = await query.find();
let model = this.getModel(collection);
let base = new model();
if (results.length == 1) {
base = results[0];
} else {
iteritems(unicity, (key, value) => {
base.set(key, value);
});
}
iteritems(additional, (key, value) => {
base.set(key, value);
});
await base.save();
}
}
/**
* Fake remote storage in memory (for testing purposes)
*/
export class FakeRemoteStorage implements IRemoteStorage {
collections: { [collection: string]: any[] } = {}
getCollection(name: string): any {
let collection = this.collections[name];
if (collection) {
return collection;
} else {
this.collections[name] = [];
return this.collections[name];
}
}
async search(collection: string, fields: any) {
let objects = this.getCollection(collection);
let result = objects.filter((obj: any) => !any(items(fields), ([key, value]) => obj[key] != value));
return result;
}
async find(collection: string, fields: any) {
let results = await this.search(collection, fields);
if (results.length == 1) {
return results[0];
} else {
return null;
}
}
async upsert(collection: string, unicity: any, additional: any) {
let existing = await this.find(collection, unicity);
let base = existing || copy(unicity);
copyfields(additional, base);
if (!existing) {
let objects = this.getCollection(collection);
objects.push(base);
}
}
}
}

View file

@ -88,5 +88,32 @@ module TS.SpaceTac.UI {
let layer = this.add.group(this.layers);
return layer;
}
/**
* Get a network connection to the backend server
*/
getConnection(): Multi.Connection {
let device_id = this.gameui.getDeviceId();
if (device_id) {
return new Multi.Connection(device_id, new Multi.ParseRemoteStorage());
} else {
// TODO Should warn the user !
return new Multi.Connection("fake", new Multi.FakeRemoteStorage());
}
}
/**
* Auto-save current session to cloud
*
* This may be called at key points during the gameplay
*/
autoSave(): void {
let session = this.gameui.session;
let connection = this.getConnection();
connection.publish(session, session.getDescription())
.then(() => this.messages.addMessage("Auto-saved to cloud"))
.catch(console.error)
//.catch(() => this.messages.addMessage("Error saving game to cloud"));
}
}
}

View file

@ -12,6 +12,7 @@ module TS.SpaceTac.UI.Specs {
baseview: BaseView;
battleview: BattleView;
mapview: UniverseMapView;
multistorage: Multi.FakeRemoteStorage;
}
/**
@ -35,6 +36,12 @@ module TS.SpaceTac.UI.Specs {
let [state, stateargs] = buildView(testgame);
if (state instanceof BaseView) {
testgame.multistorage = new Multi.FakeRemoteStorage();
let connection = new Multi.Connection(RandomGenerator.global.id(12), testgame.multistorage);
spyOn(state, "getConnection").and.returnValue(connection);
}
let orig_create = bound(state, "create");
spyOn(state, "create").and.callFake(() => {
orig_create();

View file

@ -77,7 +77,7 @@ module TS.SpaceTac.UI {
};
// Events
Tools.setHoverClick(this, show_info, hide_info, () => this.processClick());
UITools.setHoverClick(this, show_info, hide_info, () => this.processClick());
// Initialize
this.updateActiveStatus(true);

View file

@ -62,7 +62,7 @@ module TS.SpaceTac.UI {
this.addChild(this.effects);
// Handle input on ship sprite
Tools.setHoverClick(this.sprite,
UITools.setHoverClick(this.sprite,
() => this.battleview.cursorOnShip(ship),
() => this.battleview.cursorOffShip(ship),
() => this.battleview.cursorClicked()

View file

@ -66,7 +66,7 @@ module TS.SpaceTac.UI {
level.anchor.set(0.5, 0.5);
this.addChild(level);
Tools.setHoverClick(this, () => list.battleview.cursorOnShip(ship), () => list.battleview.cursorOffShip(ship), () => list.battleview.cursorClicked());
UITools.setHoverClick(this, () => list.battleview.cursorOnShip(ship), () => list.battleview.cursorOffShip(ship), () => list.battleview.cursorClicked());
}
// Update attributes from associated ship

View file

@ -111,17 +111,17 @@ module TS.SpaceTac.UI {
*/
static rotationTween(tween: Phaser.Tween, dest: number, speed = 1, easing = Phaser.Easing.Cubic.InOut, property = "rotation"): number {
// Immediately change the object's current rotation to be in range (-pi,pi)
let value = Tools.normalizeAngle(tween.target[property]);
let value = UITools.normalizeAngle(tween.target[property]);
tween.target[property] = value;
// Compute destination angle
dest = Tools.normalizeAngle(dest);
dest = UITools.normalizeAngle(dest);
if (value - dest > Math.PI) {
dest += 2 * Math.PI;
} else if (value - dest < -Math.PI) {
dest -= 2 * Math.PI;
}
let distance = Math.abs(Tools.normalizeAngle(dest - value)) / Math.PI;
let distance = Math.abs(UITools.normalizeAngle(dest - value)) / Math.PI;
let duration = distance * 1000 / speed;
// Update the tween

View file

@ -37,7 +37,7 @@ module TS.SpaceTac.UI {
this.background.endFill();
}
let [x, y] = Tools.positionInside({ x: this.anchorpoint[0], y: this.anchorpoint[1], width: width, height: height }, { x: 0, y: 0, width: this.view.getWidth(), height: this.view.getHeight() });
let [x, y] = UITools.positionInside({ x: this.anchorpoint[0], y: this.anchorpoint[1], width: width, height: height }, { x: 0, y: 0, width: this.view.getWidth(), height: this.view.getHeight() });
if (x != this.x || y != this.y) {
this.position.set(x, y);
}
@ -68,7 +68,7 @@ module TS.SpaceTac.UI {
* When the component is hovered, the function is called to allow filling the tooltip container
*/
bind(obj: Phaser.Button, func: (container: Phaser.Group) => boolean): void {
Tools.setHoverClick(obj,
UITools.setHoverClick(obj,
// enter
() => {
this.hide();

25
src/ui/common/UILabel.ts Normal file
View file

@ -0,0 +1,25 @@
/// <reference path="UIComponent.ts" />
module TS.SpaceTac.UI {
/**
* UI component to display a text
*/
export class UILabel extends UIComponent {
private content: Phaser.Text
constructor(parent: UIComponent, width: number, height: number, content = "", fontsize = 20, fontcolor = "#FFFFFF") {
super(parent, width, height);
this.content = new Phaser.Text(this.game, width / 2, height / 2, content, { align: "center", font: `${fontsize}px Arial`, fill: fontcolor })
this.content.anchor.set(0.5, 0.5);
this.addInternalChild(this.content);
}
/**
* Set the label content
*/
setContent(text: string): void {
this.content.text = text;
}
}
}

View file

@ -8,7 +8,7 @@ module TS.SpaceTac.UI {
private content: Phaser.Text
private maxlength: number
constructor(parent: UIComponent, width: number, height: number, maxlength?: number) {
constructor(parent: UIComponent, width: number, height: number, maxlength?: number, fontcolor = "#FFFFFF") {
super(parent, width, height);
let input_bg = new Phaser.Image(this.game, 0, 0, "common-transparent");
@ -19,7 +19,7 @@ module TS.SpaceTac.UI {
this.addInternalChild(input_bg);
let fontsize = Math.ceil(height * 0.8);
this.content = new Phaser.Text(this.game, width / 2, height / 2, "", { align: "center", font: `${fontsize}px Arial`, fill: "#FFFFFF" });
this.content = new Phaser.Text(this.game, width / 2, height / 2, "", { align: "center", font: `${fontsize}px Arial`, fill: fontcolor });
this.content.anchor.set(0.5, 0.5);
this.addInternalChild(this.content);

View file

@ -1,5 +1,5 @@
module TS.SpaceTac.UI.Specs {
describe("Tools", function () {
describe("UITools", function () {
let testgame = setupEmptyView();
it("keeps objects inside bounds", function () {
@ -8,19 +8,19 @@ module TS.SpaceTac.UI.Specs {
image.drawEllipse(50, 25, 50, 25);
image.endFill();
Tools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 });
UITools.keepInside(image, { x: 0, y: 0, width: 200, height: 200 });
expect(image.x).toBe(100);
expect(image.y).toBe(100);
});
it("normalizes angles", function () {
expect(Tools.normalizeAngle(0)).toEqual(0);
expect(Tools.normalizeAngle(0.1)).toBeCloseTo(0.1, 0.000001);
expect(Tools.normalizeAngle(Math.PI)).toBeCloseTo(Math.PI, 0.000001);
expect(Tools.normalizeAngle(Math.PI + 0.5)).toBeCloseTo(-Math.PI + 0.5, 0.000001);
expect(Tools.normalizeAngle(-Math.PI)).toBeCloseTo(Math.PI, 0.000001);
expect(Tools.normalizeAngle(-Math.PI - 0.5)).toBeCloseTo(Math.PI - 0.5, 0.000001);
expect(UITools.normalizeAngle(0)).toEqual(0);
expect(UITools.normalizeAngle(0.1)).toBeCloseTo(0.1, 0.000001);
expect(UITools.normalizeAngle(Math.PI)).toBeCloseTo(Math.PI, 0.000001);
expect(UITools.normalizeAngle(Math.PI + 0.5)).toBeCloseTo(-Math.PI + 0.5, 0.000001);
expect(UITools.normalizeAngle(-Math.PI)).toBeCloseTo(Math.PI, 0.000001);
expect(UITools.normalizeAngle(-Math.PI - 0.5)).toBeCloseTo(Math.PI - 0.5, 0.000001);
});
it("handles hover and click on desktops and mobile targets", function (done) {
@ -36,7 +36,7 @@ module TS.SpaceTac.UI.Specs {
spyOn(funcs, "enter");
spyOn(funcs, "leave");
spyOn(funcs, "click");
Tools.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
UITools.setHoverClick(button, funcs.enter, funcs.leave, funcs.click, 50, 100);
return [button, funcs];
}

View file

@ -7,7 +7,7 @@ module TS.SpaceTac.UI {
}
// Common UI tools functions
export class Tools {
export class UITools {
/**
* Get the position of an object, adjusted to remain inside a container
*/
@ -36,7 +36,7 @@ module TS.SpaceTac.UI {
*/
static keepInside(obj: Phaser.Button | Phaser.Sprite | Phaser.Image | Phaser.Group | Phaser.Graphics, rect: IBounded) {
let objbounds = obj.getBounds();
let [x, y] = Tools.positionInside({ x: obj.x, y: obj.y, width: objbounds.width, height: objbounds.height }, rect);
let [x, y] = UITools.positionInside({ x: obj.x, y: obj.y, width: objbounds.width, height: objbounds.height }, rect);
if (x != obj.x || y != obj.y) {
obj.position.set(x, y);

View file

@ -103,6 +103,9 @@ module TS.SpaceTac.UI {
this.inputs.bindCheat("r", "Reveal whole map", this.revealAll);
this.setZoom(2);
// Trigger an auto-save any time we go back to the map
this.autoSave();
}
/**

View file

@ -5,16 +5,91 @@ module TS.SpaceTac.UI {
* Dialog to load a saved game, or join an online one
*/
export class LoadDialog extends UIComponent {
saves: [string, string][] = []
save_selected = 0
save_name: UILabel
token_input: UITextInput
constructor(parent: MainMenu) {
super(parent, 1344, 566, "menu-load-bg");
this.addButton(600, 115, () => null, "common-arrow", "common-arrow", 180);
this.addButton(1038, 115, () => null, "common-arrow", "common-arrow", 0);
this.addButton(1224, 115, () => null, "common-button-cancel");
this.addButton(1224, 341, () => null, "common-button-cancel");
this.addButton(600, 115, () => this.paginateSave(-1), "common-arrow", "common-arrow", 180);
this.addButton(1038, 115, () => this.paginateSave(1), "common-arrow", "common-arrow", 0);
this.addButton(1224, 115, () => this.load(), "common-button-ok");
this.addButton(1224, 341, () => this.join(), "common-button-ok");
let input = new UITextInput(this, 468, 68, 10);
input.setPosition(585, 304);
this.save_name = new UILabel(this, 351, 185, "", 32, "#000000");
this.save_name.setPosition(645, 28);
this.token_input = new UITextInput(this, 468, 68, 10, "#000000");
this.token_input.setPosition(585, 304);
this.refreshSaves();
}
/**
* Refresh available save games
*/
private refreshSaves(): void {
let connection = this.view.getConnection();
// TODO include local save
// TODO Disable interaction, with loading icon
connection.listSaves().then(results => {
this.saves = items(results).sort(([id1, info1], [id2, info2]) => cmp(info2, info1));
this.setCurrentSave(0);
});
}
/**
* Set the current selected save game
*/
private setCurrentSave(position: number): void {
if (this.saves.length == 0) {
this.save_name.setContent("No save game found");
} else {
this.save_selected = clamp(position, 0, this.saves.length - 1);
let [saveid, saveinfo] = this.saves[this.save_selected];
this.save_name.setContent(saveinfo);
}
}
/**
* Change the selected save
*/
private paginateSave(offset: number) {
this.setCurrentSave(this.save_selected + offset);
}
/**
* Join an online game
*/
private join(): void {
let token = this.token_input.getContent();
let connection = this.view.getConnection();
connection.loadByToken(token).then(session => {
if (session) {
this.view.gameui.setSession(session);
}
});
}
/**
* Load selected save game
*/
private load(): void {
if (this.save_selected >= 0 && this.saves.length > this.save_selected) {
let connection = this.view.getConnection();
let [saveid, saveinfo] = this.saves[this.save_selected];
connection.loadById(saveid).then(session => {
if (session) {
this.view.gameui.setSession(session);
}
});
}
}
}
}

View file

@ -9,6 +9,11 @@
"out": "out/build.js",
"strict": true,
"sourceMap": true,
"lib": [
"dom",
"es2015.promise",
"es5"
],
"target": "es5"
},
"include": [