1
0
Fork 0

Added initial character creation screen

This commit is contained in:
Michaël Lemaire 2017-10-10 00:59:49 +02:00
parent 867dd54f10
commit ee17c37a74
19 changed files with 315 additions and 70 deletions

11
TODO.md
View file

@ -12,10 +12,8 @@ Menu/settings/saves
Map/story
---------
* Add initial character creation
* Add sound effects and more visual effects (jumps...)
* Fix quickly zooming in twice preventing to display some UI parts
* Allow to change/buy ship model
* Add factions and reputation
* Allow to cancel secondary missions
* Forbid to end up with more than 5 ships in the fleet because of escorts
@ -27,14 +25,20 @@ Character sheet
* Disable interaction during battle (except for loot screen)
* Improve eye-catching for shop and loot section
* Highlight allowed destinations during drag-and-drop, with text hints
* Highlight allowed destinations during drag-and-drop, with text hints (for success or error)
* When transferring to another ship, if the item can't be equipped (unmatched requirements), the transfer is cancelled instead of trying cargo
* Effective skill is sometimes not updated when upgrading base skill
* Add merged cargo display for the whole fleet
* Allow to change/buy ship model
* Add personality indicators (editable in creation view)
* Allow to cancel spent skill points (and confirm when closing the sheet)
* Add filters and sort options for cargo and shop
* Display level and slot type on equipment
Battle
------
* Fix arena's ship hovering happening even when the character sheet is open on top
* Add a voluntary retreat option
* Add scroll buttons when there are too many actions
* Remove dead ships from ship list and play order
@ -90,7 +94,6 @@ Common UI
---------
* Add caret/focus to text input
* Add a standard confirm dialog
* Mobile: think UI layout so that fingers do not block the view (right and left handed)
* Mobile: display tooltips larger and on the side of screen where the finger is not
* Mobile: targetting in two times, using a draggable target indicator

View file

@ -43,6 +43,7 @@ module TK.SpaceTac {
this.state.add('router', UI.Router);
this.state.add('battle', UI.BattleView);
this.state.add('intro', UI.IntroView);
this.state.add('creation', UI.FleetCreationView);
this.state.add('universe', UI.UniverseMapView);
this.state.start('boot');
@ -64,6 +65,14 @@ module TK.SpaceTac {
this.options = new UI.GameOptions(this);
}
/**
* Reset the game session
*/
resetSession(): void {
this.session = new GameSession();
this.session_token = null;
}
/**
* Display a popup message in current view
*/
@ -78,8 +87,7 @@ module TK.SpaceTac {
* Quit the current session, and go back to mainmenu
*/
quitGame() {
this.session = new GameSession();
this.session_token = null;
this.resetSession();
this.state.start('router');
}

View file

@ -80,14 +80,13 @@ module TK.SpaceTac.Specs {
expect(session.player.fleet.credits).toBe(0);
expect(session.player.universe.stars.length).toBe(50);
expect(session.getBattle()).toBeNull();
expect(session.start_location.shop).not.toBeNull();
expect(nn(session.start_location.shop).getStock().length).toBeGreaterThan(20);
expect(session.start_location.shop).toBeNull();
expect(session.start_location.encounter).toBeNull();
expect(session.start_location.encounter_gen).toBe(true);
session.setCampaignFleet();
expect(session.player.fleet.ships.length).toBe(2);
expect(session.player.fleet.credits).toBe(500);
expect(session.player.fleet.credits).toBe(0);
expect(session.player.fleet.location).toBe(session.start_location);
});

View file

@ -26,6 +26,9 @@ module TK.SpaceTac {
// Indicator of spectator mode
spectator = false
// Indicator that introduction has been watched
introduced = false
constructor() {
this.id = RandomGenerator.global.id(20);
this.universe = new Universe();
@ -66,7 +69,7 @@ module TK.SpaceTac {
this.start_location = this.universe.getStartLocation();
this.start_location.clearEncounter();
this.start_location.addShop();
this.start_location.removeShop();
this.player = new Player(this.universe);
@ -85,13 +88,13 @@ module TK.SpaceTac {
setCampaignFleet(fleet: Fleet | null = null, story = true) {
if (fleet) {
this.player.fleet = fleet;
fleet.player = this.player;
} else {
let fleet_generator = new FleetGenerator();
this.player.fleet = fleet_generator.generate(1, this.player, 2);
}
this.player.fleet.setLocation(this.start_location);
this.player.fleet.credits = 500;
this.player.fleet.setLocation(this.start_location, true);
if (story) {
this.player.missions.startMainStory(this.universe, this.player.fleet);
@ -135,7 +138,7 @@ module TK.SpaceTac {
}
/**
* Returns true if the session has a universe to explore
* Returns true if the session has an universe to explore (campaign mode)
*/
hasUniverse(): boolean {
return this.universe.stars.length > 0;
@ -147,5 +150,12 @@ module TK.SpaceTac {
isFleetCreated(): boolean {
return this.player.fleet.ships.length > 0;
}
/**
* Returns true if campaign introduction has been watched
*/
isIntroViewed(): boolean {
return this.introduced;
}
}
}

View file

@ -0,0 +1,21 @@
module TK.SpaceTac.Specs {
describe("ShipModel", () => {
it("picks random models from default collection", function () {
spyOn(console, "error").and.stub();
spyOn(ShipModel, "getDefaultCollection").and.returnValues(
[new ShipModel("a")],
[],
[new ShipModel("a"), new ShipModel("b")],
[new ShipModel("a")],
[],
);
expect(ShipModel.getRandomModel()).toEqual(new ShipModel("a"), "pick from a one-item list");
expect(ShipModel.getRandomModel()).toEqual(new ShipModel(), "pick from an empty list");
expect(sorted(ShipModel.getRandomModels(2), (a, b) => cmp(a.code, b.code))).toEqual([new ShipModel("a"), new ShipModel("b")], "sample from good-sized list");
expect(ShipModel.getRandomModels(2)).toEqual([new ShipModel("a"), new ShipModel("a")], "sample from too small list");
expect(ShipModel.getRandomModels(2)).toEqual([new ShipModel(), new ShipModel()], "sample from empty list");
});
});
}

View file

@ -17,7 +17,7 @@ module TK.SpaceTac {
// Available slots
slots: SlotType[];
constructor(code: string, name: string, level = 1, cargo = 6, default_slots = true, weapon_slots = 2) {
constructor(code = "unknown", name = "Unknown", level = 1, cargo = 6, default_slots = true, weapon_slots = 2) {
this.code = code;
this.name = name;
this.level = level;
@ -51,7 +51,7 @@ module TK.SpaceTac {
/**
* Pick a random model in the default collection
*/
static getRandomModel(level?: number, random: RandomGenerator = new RandomGenerator()): ShipModel {
static getRandomModel(level?: number, random = RandomGenerator.global): ShipModel {
let collection = this.getDefaultCollection();
if (level) {
collection = collection.filter(model => model.level <= level);
@ -59,10 +59,35 @@ module TK.SpaceTac {
if (collection.length == 0) {
console.error("Couldn't pick a random model");
return new ShipModel("undefined", "Undefined");
return new ShipModel();
} else {
return random.choice(collection);
}
}
/**
* Pick random models in the default collection
*
* At first it tries to pick unique models, then fill with duplicates
*/
static getRandomModels(count: number, level?: number, random = RandomGenerator.global): ShipModel[] {
let collection = this.getDefaultCollection();
if (level) {
collection = collection.filter(model => model.level <= level);
}
if (collection.length == 0) {
console.error("Couldn't pick a random model");
return range(count).map(() => new ShipModel());
} else {
let result: ShipModel[] = [];
while (count > 0) {
let picked = random.sample(collection, Math.min(count, collection.length));
result = result.concat(picked);
count -= picked.length;
}
return result;
}
}
}
}

View file

@ -7,28 +7,27 @@ module TK.SpaceTac.Specs {
});
it("buys and sells items", function () {
let shop = <any>new Shop();
let equ1 = new Equipment(SlotType.Shield, "shield");
equ1.price = 50;
let equ2 = new Equipment(SlotType.Hull, "hull");
equ2.price = 150;
shop.stock = [equ1, equ2];
let shop = new Shop(1, [equ1, equ2], 0);
let fleet = new Fleet();
fleet.credits = 1000;
spyOn(shop, "getPrice").and.returnValue(800);
let result = shop.sellToFleet(equ1, fleet);
expect(result).toBe(true);
expect(shop.stock).toEqual([equ2]);
expect(shop.getStock()).toEqual([equ2]);
expect(fleet.credits).toEqual(200);
result = shop.sellToFleet(equ2, fleet);
expect(result).toBe(false);
expect(shop.stock).toEqual([equ2]);
expect(shop.getStock()).toEqual([equ2]);
expect(fleet.credits).toEqual(200);
result = shop.buyFromFleet(equ1, fleet);
expect(result).toBe(true);
expect(shop.stock).toEqual([equ1, equ2]);
expect(shop.getStock()).toEqual([equ1, equ2]);
expect(fleet.credits).toEqual(1000);
});

View file

@ -1,4 +1,6 @@
module TK.SpaceTac {
type ShopStockCallback = (stock: Equipment[]) => Equipment[]
/**
* A shop is a place to buy/sell equipments
*/
@ -18,24 +20,27 @@ module TK.SpaceTac {
// Available missions
private missions: Mission[] = []
constructor(level = 1, stock: Equipment[] = [], count = 40) {
// Callback when the equipment changes
private onchange: ShopStockCallback
constructor(level = 1, stock: Equipment[] = [], count = 40, onchange?: ShopStockCallback) {
this.level = level;
this.stock = stock;
this.count = count;
this.random = new RandomGenerator();
this.onchange = onchange || (stock => stock);
}
/**
* Get available stock to display
* Get available stock to display (sorted by level then price by default)
*/
getStock() {
if (this.stock.length < this.count * 0.5) {
let count = this.random.randInt(Math.floor(this.count * 0.8), Math.ceil(this.count * 1.2));
this.stock = this.stock.concat(this.generateStock(count - this.stock.length, this.level, this.random));
this.sortStock();
}
return this.stock;
return sorted(this.stock, (a, b) => (a.level == b.level) ? cmp(a.getPrice(), b.getPrice()) : cmp(a.level, b.level));
}
/**
@ -54,10 +59,10 @@ module TK.SpaceTac {
}
/**
* Sort the stock by equipment level, then by value
* Update the stock after a buying or selling occured
*/
sortStock() {
this.stock.sort((a, b) => (a.level == b.level) ? cmp(a.getPrice(), b.getPrice()) : cmp(a.level, b.level));
refreshStock() {
this.stock = this.onchange(this.stock);
}
/**
@ -76,6 +81,7 @@ module TK.SpaceTac {
let price = this.getPrice(equipment);
if (price <= fleet.credits) {
if (remove(this.stock, equipment)) {
this.refreshStock();
fleet.credits -= price;
return true;
} else {
@ -94,7 +100,7 @@ module TK.SpaceTac {
buyFromFleet(equipment: Equipment, fleet: Fleet) {
let price = this.getPrice(equipment);
if (add(this.stock, equipment)) {
this.sortStock();
this.refreshStock();
fleet.credits += price;
return true;
} else {

View file

@ -51,6 +51,13 @@ module TK.SpaceTac {
this.shop = new Shop(level);
}
/**
* Remove a potential shop in this location
*/
removeShop(): void {
this.shop = null;
}
/**
* Check if the location is clear of encounter
*/

View file

@ -27,6 +27,7 @@ module TK.SpaceTac.UI {
// Modal dialogs
dialogs_layer: Phaser.Group
dialogs_opened: UIDialog[] = []
// Get the size of display
getWidth(): number {
@ -81,6 +82,8 @@ module TK.SpaceTac.UI {
}
shutdown() {
this.audio.stopMusic();
super.shutdown();
this.timer.cancelAll(true);

View file

@ -6,8 +6,10 @@ module TK.SpaceTac.UI {
*/
export class Boot extends Phaser.State {
preload() {
this.load.image("preload-background", "assets/images/preload/bar-background.png");
this.load.image("preload-bar", "assets/images/preload/bar-content.png");
if (!(<MainUI>this.game).headless) {
this.load.image("preload-background", "assets/images/preload/bar-background.png");
this.load.image("preload-bar", "assets/images/preload/bar-content.png");
}
}
create() {

View file

@ -11,21 +11,23 @@ module TK.SpaceTac.UI {
var ui = <MainUI>this.game;
var session = ui.session;
if (!session) {
// No session, go back to main menu
this.goToState("mainmenu", AssetLoadingRange.MENU);
} else if (session.getBattle()) {
if (session.getBattle()) {
// A battle is raging, go to it
this.goToState("battle", AssetLoadingRange.BATTLE, session.player, session.getBattle());
} else if (session.hasUniverse()) {
// Campaign mode
if (session.isFleetCreated()) {
// Go to the universe map
this.goToState("universe", AssetLoadingRange.CAMPAIGN, session.universe, session.player);
} else if (session.isIntroViewed()) {
// Build initial fleet
this.goToState("creation", AssetLoadingRange.CAMPAIGN);
} else {
// Show intro
this.goToState("intro", AssetLoadingRange.CAMPAIGN);
}
} else {
// No battle, no universe, go back to menu
// No battle, no campaign, go back to menu to decide what to do
this.goToState("mainmenu", AssetLoadingRange.MENU);
}
}

View file

@ -10,6 +10,7 @@ module TK.SpaceTac.UI.Specs {
ui: MainUI;
view: T;
multistorage: Multi.FakeRemoteStorage;
state: string;
}
/**
@ -34,6 +35,7 @@ module TK.SpaceTac.UI.Specs {
}
testgame.ui = test_ui;
testgame.ui.resetSession();
let [state, stateargs] = buildView();
@ -52,6 +54,11 @@ module TK.SpaceTac.UI.Specs {
testgame.ui.state.add("test", state);
testgame.ui.state.start("test", true, false, ...stateargs);
testgame.state = "test_initial";
spyOn(testgame.ui.state, "start").and.callFake((name: string) => {
testgame.state = name;
});
if (!testgame.ui.isBooted) {
testgame.ui.device.canvas = true;
testgame.ui.boot();
@ -127,4 +134,12 @@ module TK.SpaceTac.UI.Specs {
}
}
}
/**
* Simulate a click on a button
*/
export function testClick(button: Phaser.Button): void {
button.onInputDown.dispatch();
button.onInputUp.dispatch();
}
}

View file

@ -9,59 +9,62 @@ module TK.SpaceTac.UI {
*/
export class CharacterSheet extends Phaser.Image {
// Parent view
view: BaseView;
view: BaseView
// X positions
xshown: number;
xhidden: number;
xshown: number
xhidden: number
// Close button
close_button: Phaser.Button
// Currently displayed fleet
fleet: Fleet;
fleet: Fleet
// Currently displayed ship
ship: Ship;
ship: Ship
// Ship name
ship_name: Phaser.Text;
ship_name: Phaser.Text
// Ship level
ship_level: Phaser.Text;
ship_experience: ValueBar;
ship_level: Phaser.Text
ship_experience: ValueBar
// Ship skill upgrade
ship_upgrade_points: Phaser.Text;
ship_upgrades: Phaser.Group;
ship_upgrade_points: Phaser.Text
ship_upgrades: Phaser.Group
// Ship slots
ship_slots: Phaser.Group;
ship_slots: Phaser.Group
// Ship cargo
ship_cargo: Phaser.Group;
ship_cargo: Phaser.Group
// Mode title
mode_title: Phaser.Text;
mode_title: Phaser.Text
// Loot items
loot_slots: Phaser.Group;
loot_items: Equipment[] = [];
loot_page = 0;
loot_next: Phaser.Button;
loot_prev: Phaser.Button;
loot_slots: Phaser.Group
loot_items: Equipment[] = []
loot_page = 0
loot_next: Phaser.Button
loot_prev: Phaser.Button
// Shop
shop: Shop | null = null;
shop: Shop | null = null
// Fleet's portraits
portraits: Phaser.Group;
portraits: Phaser.Group
// Layer for draggable equipments
equipments: Phaser.Group;
equipments: Phaser.Group
// Credits
credits: Phaser.Text;
credits: Phaser.Text
// Attributes and skills
attributes: { [key: string]: Phaser.Text } = {};
attributes: { [key: string]: Phaser.Text } = {}
constructor(view: BaseView, xhidden = -2000, xshown = 0, onclose?: Function) {
super(view.game, 0, 0, "character-sheet");
@ -76,11 +79,11 @@ module TK.SpaceTac.UI {
if (!onclose) {
onclose = () => this.hide();
}
let close_button = new Phaser.Button(this.game, view.getWidth(), 0, "character-close", onclose);
close_button.anchor.set(1, 0);
UIComponent.setButtonSound(close_button);
this.addChild(close_button);
view.tooltip.bindStaticText(close_button, "Close the character sheet");
this.close_button = new Phaser.Button(this.game, view.getWidth(), 0, "character-close", onclose);
this.close_button.anchor.set(1, 0);
UIComponent.setButtonSound(this.close_button);
this.addChild(this.close_button);
view.tooltip.bindStaticText(this.close_button, "Close the character sheet");
this.ship_name = new Phaser.Text(this.game, 758, 48, "", { align: "center", font: "30pt SpaceTac", fill: "#FFFFFF" });
this.ship_name.anchor.set(0.5, 0.5);
@ -335,14 +338,14 @@ module TK.SpaceTac.UI {
*
* This shop will be shown until sheet is closed
*/
setShop(shop: Shop) {
setShop(shop: Shop, title = "Dockyard's equipment") {
this.loot_page = 0;
this.shop = shop;
this.updateLoot();
this.loot_slots.visible = true;
this.mode_title.setText("Dockyard's equipment");
this.mode_title.setText(title);
this.mode_title.visible = true;
}

View file

@ -0,0 +1,53 @@
/// <reference path="../TestGame.ts"/>
module TK.SpaceTac.UI.Specs {
describe("FleetCreationView", function () {
let testgame = setupSingleView(() => [new FleetCreationView, []]);
it("has a basic equipment shop with infinite stock", function () {
let shop = testgame.view.infinite_shop;
let itemcount = shop.getStock().length;
expect(unique(shop.getStock().map(equ => equ.code)).length).toEqual(itemcount);
let fleet = new Fleet();
fleet.credits = 100000;
let item = shop.getStock()[0];
shop.sellToFleet(item, fleet);
expect(fleet.credits).toBe(100000 - item.getPrice());
expect(shop.getStock().length).toBe(itemcount);
shop.buyFromFleet(item, fleet);
expect(fleet.credits).toBe(100000);
expect(shop.getStock().length).toBe(itemcount);
})
async_it("validates the fleet creation", async function () {
expect(testgame.ui.session.isFleetCreated()).toBe(false, "no fleet created");
expect(testgame.ui.session.player.fleet.ships.length).toBe(0, "empty session fleet");
expect(testgame.view.dialogs_layer.children.length).toBe(0, "no dialogs");
expect(testgame.view.character_sheet.fleet).toBe(testgame.view.built_fleet);
expect(testgame.view.built_fleet.ships.length).toBe(2, "initial fleet should have two ships");
// close sheet
testClick(testgame.view.character_sheet.close_button);
expect(testgame.view.dialogs_opened.length).toBe(1, "confirmation dialog opened");
expect(testgame.ui.session.isFleetCreated()).toBe(false, "still no fleet created");
// click on no in confirmation dialog
let dialog = <UIConfirmDialog>testgame.view.dialogs_opened[0];
await dialog.forceResult(false);
expect(testgame.view.dialogs_opened.length).toBe(0, "confirmation dialog destroyed after 'no'");
expect(testgame.ui.session.isFleetCreated()).toBe(false, "still no fleet created after 'no'");
expect(testgame.state).toEqual("test_initial");
// close sheet, click on yes in confirmation dialog
testClick(testgame.view.character_sheet.close_button);
dialog = <UIConfirmDialog>testgame.view.dialogs_opened[0];
await dialog.forceResult(true);
expect(testgame.view.dialogs_opened.length).toBe(0, "confirmation dialog destroyed after 'yes'");
expect(testgame.ui.session.isFleetCreated()).toBe(true, "fleet created");
expect(testgame.ui.session.player.fleet.ships.length).toBe(2, "session fleet now has two ships");
expect(testgame.state).toEqual("router");
})
})
}

View file

@ -0,0 +1,46 @@
/// <reference path="../BaseView.ts"/>
module TK.SpaceTac.UI {
/**
* View to configure the initial characters in the fleet
*/
export class FleetCreationView extends BaseView {
built_fleet: Fleet
infinite_shop: Shop
character_sheet: CharacterSheet
create() {
super.create();
let models = ShipModel.getRandomModels(2);
this.built_fleet = new Fleet();
this.built_fleet.addShip(new Ship(null, "First", models[0]));
this.built_fleet.addShip(new Ship(null, "Second", models[1]));
this.built_fleet.credits = this.built_fleet.ships.length * 1000;
let basic_equipments = () => {
let generator = new LootGenerator();
let equipments = generator.templates.map(template => template.generate(1));
return sortedBy(equipments, equipment => equipment.slot_type);
}
this.infinite_shop = new Shop(1, basic_equipments(), 0, basic_equipments);
this.character_sheet = new CharacterSheet(this, undefined, undefined, () => this.validateFleet());
this.character_sheet.setShop(this.infinite_shop, "Initial basic equipment");
this.character_sheet.show(this.built_fleet.ships[0], false);
this.addLayer("characters").add(this.character_sheet);
}
/**
* Validate the configured fleet and move on
*/
async validateFleet() {
let confirmed = await UIConfirmDialog.ask(this, "Do you confirm these initial fleet settings ?");
if (confirmed) {
this.session.setCampaignFleet(this.built_fleet, this.session.hasUniverse());
this.backToRouter();
}
}
}
}

View file

@ -0,0 +1,41 @@
module TK.SpaceTac.UI {
/**
* Dialog asking for a confirmation
*/
export class UIConfirmDialog extends UIDialog {
private result: Promise<boolean>
private result_resolver: (confirmed: boolean) => void
constructor(view: BaseView, message: string) {
super(view);
this.addText(this.width * 0.5, this.height * 0.3, message, "#90FEE3", 32);
this.result = new Promise((resolve, reject) => {
this.result_resolver = resolve;
this.addButton(this.width * 0.4, this.height * 0.6, () => resolve(false), "common-button-cancel");
this.addButton(this.width * 0.6, this.height * 0.6, () => resolve(true), "common-button-ok");
});
}
/**
* Force the result (simulate clicking the appropriate button)
*/
async forceResult(confirmed: boolean): Promise<void> {
this.result_resolver(confirmed);
await this.result;
}
/**
* Convenient function to ask for a confirmation, and have a promise of result
*/
static ask(view: BaseView, message: string): Promise<boolean> {
let dlg = new UIConfirmDialog(view, message);
let result = dlg.result;
return result.then(confirmed => {
dlg.close();
return confirmed;
});
}
}
}

View file

@ -10,9 +10,10 @@ module TK.SpaceTac.UI {
constructor(parent: BaseView, width = 1495, height = 1080, background = "common-dialog") {
super(parent, width, height, background);
if (parent.dialogs_layer.children.length == 0) {
if (parent.dialogs_opened.length == 0) {
this.addOverlay(parent.dialogs_layer);
}
add(parent.dialogs_opened, this);
this.view.audio.playOnce("ui-dialog-open");
@ -46,7 +47,9 @@ module TK.SpaceTac.UI {
this.view.audio.playOnce("ui-dialog-close");
if (this.view.dialogs_layer.children.length == 1) {
remove(this.view.dialogs_opened, this);
if (this.view.dialogs_opened.length == 0) {
// Remove overlay
this.view.dialogs_layer.removeAll(true);
}
}

View file

@ -14,8 +14,7 @@ module TK.SpaceTac.UI {
let nextStep = () => {
if (!steps.nextStep()) {
// For now, we create a random fleet
this.gameui.session.setCampaignFleet();
this.session.introduced = true;
this.backToRouter();
return false;
} else {