1
0
Fork 0

Migration to Phaser 3

This commit is contained in:
Michaël Lemaire 2018-05-15 16:57:45 +02:00
parent e20be0ed6e
commit 811bfa182d
102 changed files with 58082 additions and 36685 deletions

14
TODO.md
View File

@ -1,6 +1,17 @@
To-Do-list
==========
Phaser 3 migration
------------------
* Pause the game when the window isn't focused (except in headless)
* Fit the game in window size
* Fix top-right messages positions
* Make the AI-thinking loader work again
* Fix the character sheet layout
* Fix the crash in gatling animation
* Fix valuebar requiring to be in root display list
Menu/settings/saves
-------------------
@ -20,6 +31,7 @@ Map/story
* Forbid to end up with more than 5 ships in the fleet because of escorts
* Fix problems when several dialogs are active at the same time
* Add a zoom level, to see the location only
* Restore the progressive text effect
Character sheet
---------------
@ -94,6 +106,7 @@ Common UI
* Fix tooltip remaining when the hovered object is hidden by animations
* If ProgressiveMessage animation performance is bad, show the text directly
* Add caret/focus and configurable background to text input
* Release keybord grabbing when UITextInput is hidden or loses focus
* 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
@ -103,6 +116,7 @@ Technical
* Pack sounds
* Add toggles for shaders, automatically disable them if too slow, and initially disable them on mobile
* Add cache for image texture lookup (getImageInfo)
Network
-------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

View File

@ -1,6 +1,6 @@
var handler = {
get(target, name) {
return function () { }
return new Proxy({}, handler);
}
}
var Phaser = new Proxy({}, handler);

View File

@ -42,7 +42,7 @@
<div class=".fontLoader">.</div>
<script src="vendor/parse/parse.min.js"></script>
<script src="vendor/phaser/phaser.min.js"></script>
<script src="vendor/phaser/phaser.js"></script>
<script src="build.js"></script>
<script>

17
package-lock.json generated
View File

@ -1375,8 +1375,7 @@
"eventemitter3": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==",
"dev": true
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA=="
},
"expand-braces": {
"version": "0.1.2",
@ -3317,11 +3316,6 @@
"request": "2.81.0"
}
},
"hoek": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
"integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
},
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
@ -5289,9 +5283,12 @@
}
},
"phaser": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-2.6.2.tgz",
"integrity": "sha1-6zkSFyWiFJxJ9GtdFEMYwivAkkk="
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.9.0.tgz",
"integrity": "sha1-CsZGUDGwoUpK9DIPr5Xzn2cVmdc=",
"requires": {
"eventemitter3": "3.1.0"
}
},
"pify": {
"version": "2.3.0",

View File

@ -37,10 +37,9 @@
"uglify-js": "^3.3.23"
},
"dependencies": {
"hoek": "^5.0.3",
"jasmine-core": "^3.1.0",
"parse": "^1.11.0",
"phaser": "2.6.2",
"phaser": "^3.9.0",
"process-pool": "^0.3.5"
}
}

View File

@ -82,11 +82,10 @@ async function pack(stage) {
let items = files.map(file => {
let fname = path.basename(file);
return {
type: "atlasJSONHash",
type: "atlas",
key: fname,
atlasURL: `assets/${fname}.json?t=${Date.now()}`,
textureURL: `assets/${fname}.png`,
atlasData: null
textureURL: `assets/${fname}.png`
}
});
@ -98,7 +97,7 @@ async function pack(stage) {
return {
type: "audio",
key: key,
urls: [`assets/${key}.${ext}?t=${Date.now()}`],
url: `assets/${key}.${ext}?t=${Date.now()}`,
autoDecode: (ext == 'mp3')
};
}));
@ -116,7 +115,7 @@ async function pack(stage) {
}));
let packdata = {};
packdata[`stage${stage}`] = items;
packdata[`stage${stage}`] = { files: items };
await new Promise(resolve => fs.writeFile(`out/assets/pack${stage}.json`, JSON.stringify(packdata), 'utf8', resolve));
}
@ -143,7 +142,7 @@ async function vendors() {
console.log("Copying vendors...");
shell.rm('-rf', 'out/vendor');
shell.mkdir('-p', 'out/vendor');
shell.cp('-R', 'node_modules/phaser/build', 'out/vendor/phaser');
shell.cp('-R', 'node_modules/phaser/dist', 'out/vendor/phaser');
shell.cp('-R', 'node_modules/parse/dist', 'out/vendor/parse');
shell.cp('-R', 'node_modules/jasmine-core/lib/jasmine-core', 'out/vendor/jasmine');
}
@ -181,7 +180,10 @@ async function optimize() {
async function deploy(task) {
await build(true);
await optimize();
await exec("rsync -avz --delete ./out/ hosting.thunderk.net:/srv/website/spacetac/");
let branch = await run('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe', async: true });
let suffix = (branch == "master") ? "" : "x";
await exec(`rsync -avz --delete ./out/ hosting.thunderk.net:/srv/website/spacetac${suffix}/`);
}
/**

View File

@ -10,8 +10,8 @@ if (typeof window != "undefined") {
if (typeof global != "undefined") {
// In node, does not extend Phaser classes
var handler = {
get(target: any, name: any) {
return function () { }
get(target: any, name: any): any {
return new Proxy({}, handler);
}
}
global.Phaser = new Proxy({}, handler);
@ -29,22 +29,23 @@ module TK.SpaceTac {
session: GameSession
session_token: string | null
// Audio manager
audio!: UI.Audio
// Game options
options!: UI.GameOptions
// Storage used
storage: Storage
// Headless mode
headless: boolean
// Debug mode
debug = false
constructor(headless: boolean = false) {
super(1920, 1080, headless ? Phaser.HEADLESS : Phaser.AUTO, '-space-tac');
this.headless = headless;
super({
width: 1920,
height: 1080,
type: headless ? Phaser.HEADLESS : Phaser.AUTO,
backgroundColor: '#000000',
parent: '-space-tac'
});
this.storage = localStorage;
@ -52,32 +53,55 @@ module TK.SpaceTac {
this.session_token = null;
if (!headless) {
this.state.onStateChange.add((state: string) => console.log(`View change: ${state}`));
this.scene.add('boot', UI.Boot);
this.scene.add('loading', UI.AssetLoading);
this.scene.add('mainmenu', UI.MainMenu);
this.scene.add('router', UI.Router);
this.scene.add('battle', UI.BattleView);
this.scene.add('intro', UI.IntroView);
this.scene.add('creation', UI.FleetCreationView);
this.scene.add('universe', UI.UniverseMapView);
this.state.add('boot', UI.Boot);
this.state.add('loading', UI.AssetLoading);
this.state.add('mainmenu', UI.MainMenu);
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');
this.goToScene('boot');
}
}
boot() {
if (this.renderType == Phaser.HEADLESS) {
this.headless = true;
}
super.boot();
this.audio = new UI.Audio(this);
this.options = new UI.GameOptions(this);
}
get headless(): boolean {
return this.config.renderType == Phaser.HEADLESS;
}
/**
* Get the audio manager for current scene
*/
get audio(): UI.Audio {
let scene = this.getActiveScene();
if (scene) {
return scene.audio;
} else {
return new UI.Audio(null);
}
}
/**
* Get the currently active scene
*/
getActiveScene(): UI.BaseView | null {
let active = first(<string[]>keys(this.scene.scenes), key => this.scene.isActive(key));
if (active) {
let scene = this.scene.getScene(active);
return (scene instanceof UI.BaseView) ? scene : null;
} else if (this.headless) {
return this.scene.scenes[0];
} else {
return null;
}
}
/**
* Reset the game session
*/
@ -90,10 +114,23 @@ module TK.SpaceTac {
* Display a popup message in current view
*/
displayMessage(message: string) {
let state = <UI.BaseView>this.state.getCurrentState();
if (state) {
state.messages.addMessage(message);
}
iteritems(<any>this.scene.keys, (key: string, scene: UI.BaseView) => {
if (scene.messages && this.scene.isVisible(key)) {
scene.messages.addMessage(message);
}
});
}
/**
* Change the active scene
*/
goToScene(name: string): void {
this.scene.scenes.forEach(scene => {
if (this.scene.isActive(scene)) {
scene.shutdown();
}
});
this.scene.start(name);
}
/**
@ -101,7 +138,7 @@ module TK.SpaceTac {
*/
quitGame() {
this.resetSession();
this.state.start('router');
this.goToScene('router');
}
/**
@ -124,7 +161,7 @@ module TK.SpaceTac {
setSession(session: GameSession, token?: string): void {
this.session = session;
this.session_token = token || null;
this.state.start("router");
this.goToScene("router");
}
/**
@ -171,7 +208,9 @@ module TK.SpaceTac {
* Check if the game is currently fullscreen
*/
isFullscreen(): boolean {
return this.scale.isFullScreen;
// FIXME
return false;
//return this.scale.isFullScreen;
}
/**
@ -180,13 +219,15 @@ module TK.SpaceTac {
* Returns true if the result is fullscreen
*/
toggleFullscreen(active: boolean | null = null): boolean {
if (active === false || (active !== true && this.isFullscreen())) {
// FIXME
/*if (active === false || (active !== true && this.isFullscreen())) {
this.scale.stopFullScreen();
return false;
} else {
this.scale.startFullScreen(true);
return true;
}
}*/
return false;
}
}
}

@ -1 +1 @@
Subproject commit fc4bde326c2dcb4be03380ac29bac8d12b015821
Subproject commit 628ae0e4dc1b738ba3764506648d83ecca815cf5

View File

@ -332,7 +332,7 @@ module TK.SpaceTac {
*/
getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] {
let drone_effects = this.drones.list().map(drone => {
// FIXME Should apply filterImpactedShips from drone action
// TODO Should apply filterImpactedShips from drone action
if (drone.isInRange(ship.arena_x, ship.arena_y)) {
return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]);
} else {

View File

@ -89,7 +89,7 @@ module TK.SpaceTac {
// TODO Work with groups of 3, 4 ...
let weapons = <Iterator<TriggerAction>>ifilter(getPlayableActions(ship), action => action instanceof TriggerAction && action.blast > 0);
let enemies = battle.ienemies(ship, true);
// FIXME This produces duplicates (x, y) and (y, x)
// TODO This produces duplicates (x, y) and (y, x)
let couples = ifilter(icombine(enemies, enemies), ([e1, e2]) => e1 != e2);
let candidates = ifilter(icombine(weapons, couples), ([weapon, [e1, e2]]) => Target.newFromShip(e1).getDistanceTo(Target.newFromShip(e2)) < weapon.blast * 2);
let result = imap(candidates, ([weapon, [e1, e2]]) => new Maneuver(ship, weapon, Target.newFromLocation((e1.arena_x + e2.arena_x) / 2, (e1.arena_y + e2.arena_y) / 2)));

948
src/lib/p2.d.ts vendored
View File

@ -1,948 +0,0 @@
// Type definitions for p2.js v0.6.0
// Project: https://github.com/schteppe/p2.js/
declare module p2 {
export class AABB {
constructor(options?: {
upperBound?: number[];
lowerBound?: number[];
});
setFromPoints(points: number[][], position: number[], angle: number, skinSize: number): void;
copy(aabb: AABB): void;
extend(aabb: AABB): void;
overlaps(aabb: AABB): boolean;
}
export class Broadphase {
static AABB: number;
static BOUNDING_CIRCLE: number;
static NAIVE: number;
static SAP: number;
static boundingRadiusCheck(bodyA: Body, bodyB: Body): boolean;
static aabbCheck(bodyA: Body, bodyB: Body): boolean;
static canCollide(bodyA: Body, bodyB: Body): boolean;
constructor(type: number);
type: number;
result: Body[];
world: World;
boundingVolumeType: number;
setWorld(world: World): void;
getCollisionPairs(world: World): Body[];
boundingVolumeCheck(bodyA: Body, bodyB: Body): boolean;
}
export class GridBroadphase extends Broadphase {
constructor(options?: {
xmin?: number;
xmax?: number;
ymin?: number;
ymax?: number;
nx?: number;
ny?: number;
});
xmin: number;
xmax: number;
ymin: number;
ymax: number;
nx: number;
ny: number;
binsizeX: number;
binsizeY: number;
}
export class NativeBroadphase extends Broadphase {
}
export class Narrowphase {
contactEquations: ContactEquation[];
frictionEquations: FrictionEquation[];
enableFriction: boolean;
slipForce: number;
frictionCoefficient: number;
surfaceVelocity: number;
reuseObjects: boolean;
resuableContactEquations: any[];
reusableFrictionEquations: any[];
restitution: number;
stiffness: number;
relaxation: number;
frictionStiffness: number;
frictionRelaxation: number;
enableFrictionReduction: boolean;
contactSkinSize: number;
collidedLastStep(bodyA: Body, bodyB: Body): boolean;
reset(): void;
createContactEquation(bodyA: Body, bodyB: Body, shapeA: Shape, shapeB: Shape): ContactEquation;
createFrictionFromContact(c: ContactEquation): FrictionEquation;
}
export class SAPBroadphase extends Broadphase {
axisList: Body[];
axisIndex: number;
}
export class Constraint {
static DISTANCE: number;
static GEAR: number;
static LOCK: number;
static PRISMATIC: number;
static REVOLUTE: number;
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
});
type: number;
equeations: Equation[];
bodyA: Body;
bodyB: Body;
collideConnected: boolean;
update(): void;
setStiffness(stiffness: number): void;
setRelaxation(relaxation: number): void;
}
export class DistanceConstraint extends Constraint {
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
distance?: number;
localAnchorA?: number[];
localAnchorB?: number[];
maxForce?: number;
});
localAnchorA: number[];
localAnchorB: number[];
distance: number;
maxForce: number;
upperLimitEnabled: boolean;
upperLimit: number;
lowerLimitEnabled: boolean;
lowerLimit: number;
position: number;
setMaxForce(f: number): void;
getMaxForce(): number;
}
export class GearConstraint extends Constraint {
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
angle?: number;
ratio?: number;
maxTorque?: number;
});
ratio: number;
angle: number;
setMaxTorque(torque: number): void;
getMaxTorque(): number;
}
export class LockConstraint extends Constraint {
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
localOffsetB?: number[];
localAngleB?: number;
maxForce?: number;
});
setMaxForce(force: number): void;
getMaxForce(): number;
}
export class PrismaticConstraint extends Constraint {
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
maxForce?: number;
localAnchorA?: number[];
localAnchorB?: number[];
localAxisA?: number[];
disableRotationalLock?: boolean;
upperLimit?: number;
lowerLimit?: number;
});
localAnchorA: number[];
localAnchorB: number[];
localAxisA: number[];
position: number;
velocity: number;
lowerLimitEnabled: boolean;
upperLimitEnabled: boolean;
lowerLimit: number;
upperLimit: number;
upperLimitEquation: ContactEquation;
lowerLimitEquation: ContactEquation;
motorEquation: Equation;
motorEnabled: boolean;
motorSpeed: number;
enableMotor(): void;
disableMotor(): void;
setLimits(lower: number, upper: number): void;
}
export class RevoluteConstraint extends Constraint {
constructor(bodyA: Body, bodyB: Body, type: number, options?: {
collideConnected?: boolean;
wakeUpBodies?: boolean;
worldPivot?: number[];
localPivotA?: number[];
localPivotB?: number[];
maxForce?: number;
});
pivotA: number[];
pivotB: number[];
motorEquation: RotationalVelocityEquation;
motorEnabled: boolean;
angle: number;
lowerLimitEnabled: boolean;
upperLimitEnabled: boolean;
lowerLimit: number;
upperLimit: number;
upperLimitEquation: ContactEquation;
lowerLimitEquation: ContactEquation;
enableMotor(): void;
disableMotor(): void;
motorIsEnabled(): boolean;
setLimits(lower: number, upper: number): void;
setMotorSpeed(speed: number): void;
getMotorSpeed(): number;
}
export class AngleLockEquation extends Equation {
constructor(bodyA: Body, bodyB: Body, options?: {
angle?: number;
ratio?: number;
});
computeGq(): number;
setRatio(ratio: number): number;
setMaxTorque(torque: number): number;
}
export class ContactEquation extends Equation {
constructor(bodyA: Body, bodyB: Body);
contactPointA: number[];
penetrationVec: number[];
contactPointB: number[];
normalA: number[];
restitution: number;
firstImpact: boolean;
shapeA: Shape;
shapeB: Shape;
computeB(a: number, b: number, h: number): number;
}
export class Equation {
static DEFAULT_STIFFNESS: number;
static DEFAULT_RELAXATION: number;
constructor(bodyA: Body, bodyB: Body, minForce?: number, maxForce?: number);
minForce: number;
maxForce: number;
bodyA: Body;
bodyB: Body;
stiffness: number;
relaxation: number;
G: number[];
offset: number;
a: number;
b: number;
epsilon: number;
timeStep: number;
needsUpdate: boolean;
multiplier: number;
relativeVelocity: number;
enabled: boolean;
gmult(G: number[], vi: number[], wi: number[], vj: number[], wj: number[]): number;
computeB(a: number, b: number, h: number): number;
computeGq(): number;
computeGW(): number;
computeGWlambda(): number;
computeGiMf(): number;
computeGiMGt(): number;
addToWlambda(deltalambda: number): number;
computeInvC(eps: number): number;
}
export class FrictionEquation extends Equation {
constructor(bodyA: Body, bodyB: Body, slipForce: number);
contactPointA: number[];
contactPointB: number[];
t: number[];
shapeA: Shape;
shapeB: Shape;
frictionCoefficient: number;
setSlipForce(slipForce: number): number;
getSlipForce(): number;
computeB(a: number, b: number, h: number): number;
}
export class RotationalLockEquation extends Equation {
constructor(bodyA: Body, bodyB: Body, options?: {
angle?: number;
});
angle: number;
computeGq(): number;
}
export class RotationalVelocityEquation extends Equation {
constructor(bodyA: Body, bodyB: Body);
computeB(a: number, b: number, h: number): number;
}
export class EventEmitter {
on(type: string, listener: Function, context: any): EventEmitter;
has(type: string, listener: Function): boolean;
off(type: string, listener: Function): EventEmitter;
emit(event: any): EventEmitter;
}
export class ContactMaterialOptions {
friction: number;
restitution: number;
stiffness: number;
relaxation: number;
frictionStiffness: number;
frictionRelaxation: number;
surfaceVelocity: number;
}
export class ContactMaterial {
static idCounter: number;
constructor(materialA: Material, materialB: Material, options?: ContactMaterialOptions);
id: number;
materialA: Material;
materialB: Material;
friction: number;
restitution: number;
stiffness: number;
relaxation: number;
frictionStuffness: number;
frictionRelaxation: number;
surfaceVelocity: number;
contactSkinSize: number;
}
export class Material {
static idCounter: number;
constructor(id: number);
id: number;
}
export class vec2 {
static crossLength(a: number[], b: number[]): number;
static crossVZ(out: number[], vec: number[], zcomp: number): number;
static crossZV(out: number[], zcomp: number, vec: number[]): number;
static rotate(out: number[], a: number[], angle: number): void;
static rotate90cw(out: number[], a: number[]): number;
static centroid(out: number[], a: number[], b: number[], c: number[]): number[];
static create(): number[];
static clone(a: number[]): number[];
static fromValues(x: number, y: number): number[];
static copy(out: number[], a: number[]): number[];
static set(out: number[], x: number, y: number): number[];
static toLocalFrame(out: number[], worldPoint: number[], framePosition: number[], frameAngle: number): void;
static toGlobalFrame(out: number[], localPoint: number[], framePosition: number[], frameAngle: number): void;
static add(out: number[], a: number[], b: number[]): number[];
static subtract(out: number[], a: number[], b: number[]): number[];
static sub(out: number[], a: number[], b: number[]): number[];
static multiply(out: number[], a: number[], b: number[]): number[];
static mul(out: number[], a: number[], b: number[]): number[];
static divide(out: number[], a: number[], b: number[]): number[];
static div(out: number[], a: number[], b: number[]): number[];
static scale(out: number[], a: number[], b: number): number[];
static distance(a: number[], b: number[]): number;
static dist(a: number[], b: number[]): number;
static squaredDistance(a: number[], b: number[]): number;
static sqrDist(a: number[], b: number[]): number;
static length(a: number[]): number;
static len(a: number[]): number;
static squaredLength(a: number[]): number;
static sqrLen(a: number[]): number;
static negate(out: number[], a: number[]): number[];
static normalize(out: number[], a: number[]): number[];
static dot(a: number[], b: number[]): number;
static str(a: number[]): string;
}
export class BodyOptions {
mass: number;
position: number[];
velocity: number[];
angle: number;
angularVelocity: number;
force: number[];
angularForce: number;
fixedRotation: number;
}
export class Body extends EventEmitter {
sleepyEvent: {
type: string;
};
sleepEvent: {
type: string;
};
wakeUpEvent: {
type: string;
};
static DYNAMIC: number;
static STATIC: number;
static KINEMATIC: number;
static AWAKE: number;
static SLEEPY: number;
static SLEEPING: number;
constructor(options?: BodyOptions);
id: number;
world: World;
shapes: Shape[];
shapeOffsets: number[][];
shapeAngles: number[];
mass: number;
invMass: number;
inertia: number;
invInertia: number;
invMassSolve: number;
invInertiaSolve: number;
fixedRotation: number;
position: number[];
interpolatedPosition: number[];
interpolatedAngle: number;
previousPosition: number[];
previousAngle: number;
velocity: number[];
vlambda: number[];
wlambda: number[];
angle: number;
angularVelocity: number;
force: number[];
angularForce: number;
damping: number;
angularDamping: number;
type: number;
boundingRadius: number;
aabb: AABB;
aabbNeedsUpdate: boolean;
allowSleep: boolean;
wantsToSleep: boolean;
sleepState: number;
sleepSpeedLimit: number;
sleepTimeLimit: number;
gravityScale: number;
updateSolveMassProperties(): void;
setDensity(density: number): void;
getArea(): number;
getAABB(): AABB;
updateAABB(): void;
updateBoundingRadius(): void;
addShape(shape: Shape, offset?: number[], angle?: number): void;
removeShape(shape: Shape): boolean;
updateMassProperties(): void;
applyForce(force: number[], worldPoint: number[]): void;
toLocalFrame(out: number[], worldPoint: number[]): void;
toWorldFrame(out: number[], localPoint: number[]): void;
fromPolygon(path: number[][], options?: {
optimalDecomp?: boolean;
skipSimpleCheck?: boolean;
removeCollinearPoints?: any; //boolean | number
}): boolean;
adjustCenterOfMass(): void;
setZeroForce(): void;
resetConstraintVelocity(): void;
applyDamping(dy: number): void;
wakeUp(): void;
sleep(): void;
sleepTick(time: number, dontSleep: boolean, dt: number): void;
getVelocityFromPosition(story: number[], dt: number): number[];
getAngularVelocityFromPosition(timeStep: number): number;
overlaps(body: Body): boolean;
}
export class Spring {
constructor(bodyA: Body, bodyB: Body, options?: {
stiffness?: number;
damping?: number;
localAnchorA?: number[];
localAnchorB?: number[];
worldAnchorA?: number[];
worldAnchorB?: number[];
});
stiffness: number;
damping: number;
bodyA: Body;
bodyB: Body;
applyForce(): void;
}
export class LinearSpring extends Spring {
localAnchorA: number[];
localAnchorB: number[];
restLength: number;
setWorldAnchorA(worldAnchorA: number[]): void;
setWorldAnchorB(worldAnchorB: number[]): void;
getWorldAnchorA(result: number[]): number[];
getWorldAnchorB(result: number[]): number[];
applyForce(): void;
}
export class RotationalSpring extends Spring {
constructor(bodyA: Body, bodyB: Body, options?: {
restAngle?: number;
stiffness?: number;
damping?: number;
});
restAngle: number;
}
export class Capsule extends Shape {
constructor(length?: number, radius?: number);
length: number;
radius: number;
}
export class Circle extends Shape {
constructor(radius: number);
radius: number;
}
export class Convex extends Shape {
static triangleArea(a: number[], b: number[], c: number[]): number;
constructor(vertices: number[][], axes: number[]);
vertices: number[][];
axes: number[];
centerOfMass: number[];
triangles: number[];
boundingRadius: number;
projectOntoLocalAxis(localAxis: number[], result: number[]): void;
projectOntoWorldAxis(localAxis: number[], shapeOffset: number[], shapeAngle: number, result: number[]): void;
updateCenterOfMass(): void;
}
export class Heightfield extends Shape {
constructor(data: number[], options?: {
minValue?: number;
maxValue?: number;
elementWidth: number;
});
data: number[];
maxValue: number;
minValue: number;
elementWidth: number;
}
export class Shape {
static idCounter: number;
static CIRCLE: number;
static PARTICLE: number;
static PLANE: number;
static CONVEX: number;
static LINE: number;
static RECTANGLE: number;
static CAPSULE: number;
static HEIGHTFIELD: number;
constructor(type: number);
type: number;
id: number;
boundingRadius: number;
collisionGroup: number;
collisionMask: number;
material: Material;
area: number;
sensor: boolean;
computeMomentOfInertia(mass: number): number;
updateBoundingRadius(): number;
updateArea(): void;
computeAABB(out: AABB, position: number[], angle: number): void;
}
export class Line extends Shape {
constructor(length?: number);
length: number;
}
export class Particle extends Shape {
}
export class Plane extends Shape {
}
export class Rectangle extends Shape {
static sameDimensions(a: Rectangle, b: Rectangle): boolean;
constructor(width?: number, height?: number);
width: number;
height: number;
}
export class Solver extends EventEmitter {
static GS: number;
static ISLAND: number;
constructor(options?: {}, type?: number);
type: number;
equations: Equation[];
equationSortFunction: Equation; //Equation | boolean
solve(dy: number, world: World): void;
solveIsland(dy: number, island: Island): void;
sortEquations(): void;
addEquation(eq: Equation): void;
addEquations(eqs: Equation[]): void;
removeEquation(eq: Equation): void;
removeAllEquations(): void;
}
export class GSSolver extends Solver {
constructor(options?: {
iterations?: number;
tolerance?: number;
});
iterations: number;
tolerance: number;
useZeroRHS: boolean;
frictionIterations: number;
usedIterations: number;
solve(h: number, world: World): void;
}
export class OverlapKeeper {
constructor(bodyA: Body, shapeA: Shape, bodyB: Body, shapeB: Shape);
shapeA: Shape;
shapeB: Shape;
bodyA: Body;
bodyB: Body;
tick(): void;
setOverlapping(bodyA: Body, shapeA: Shape, bodyB: Body, shapeB: Body): void;
bodiesAreOverlapping(bodyA: Body, bodyB: Body): boolean;
set(bodyA: Body, shapeA: Shape, bodyB: Body, shapeB: Shape): void;
}
export class TupleDictionary {
data: number[];
keys: number[];
getKey(id1: number, id2: number): string;
getByKey(key: number): number;
get(i: number, j: number): number;
set(i: number, j: number, value: number): number;
reset(): void;
copy(dict: TupleDictionary): void;
}
export class Utils {
static appendArray<T>(a: Array<T>, b: Array<T>): Array<T>;
static chanceRoll(chance: number): boolean;
static defaults(options: any, defaults: any): any;
static extend(a: any, b: any): void;
static randomChoice(choice1: any, choice2: any): any;
static rotateArray(matrix: any[], direction: any): any[];
static splice<T>(array: Array<T>, index: number, howMany: number): void;
static shuffle<T>(array: T[]): T[];
static transposeArray<T>(array: T[]): T[];
}
export class Island {
equations: Equation[];
bodies: Body[];
reset(): void;
getBodies(result: any): Body[];
wantsToSleep(): boolean;
sleep(): boolean;
}
export class IslandManager extends Solver {
static getUnvisitedNode(nodes: Node[]): IslandNode; // IslandNode | boolean
equations: Equation[];
islands: Island[];
nodes: IslandNode[];
visit(node: IslandNode, bds: Body[], eqs: Equation[]): void;
bfs(root: IslandNode, bds: Body[], eqs: Equation[]): void;
split(world: World): Island[];
}
export class IslandNode {
constructor(body: Body);
body: Body;
neighbors: IslandNode[];
equations: Equation[];
visited: boolean;
reset(): void;
}
export class World extends EventEmitter {
postStepEvent: {
type: string;
};
addBodyEvent: {
type: string;
};
removeBodyEvent: {
type: string;
};
addSpringEvent: {
type: string;
};
impactEvent: {
type: string;
bodyA: Body;
bodyB: Body;
shapeA: Shape;
shapeB: Shape;
contactEquation: ContactEquation;
};
postBroadphaseEvent: {
type: string;
pairs: Body[];
};
beginContactEvent: {
type: string;
shapeA: Shape;
shapeB: Shape;
bodyA: Body;
bodyB: Body;
contactEquations: ContactEquation[];
};
endContactEvent: {
type: string;
shapeA: Shape;
shapeB: Shape;
bodyA: Body;
bodyB: Body;
};
preSolveEvent: {
type: string;
contactEquations: ContactEquation[];
frictionEquations: FrictionEquation[];
};
static NO_SLEEPING: number;
static BODY_SLEEPING: number;
static ISLAND_SLEEPING: number;
static integrateBody(body: Body, dy: number): void;
constructor(options?: {
solver?: Solver;
gravity?: number[];
broadphase?: Broadphase;
islandSplit?: boolean;
doProfiling?: boolean;
});
springs: Spring[];
bodies: Body[];
solver: Solver;
narrowphase: Narrowphase;
islandManager: IslandManager;
gravity: number[];
frictionGravity: number;
useWorldGravityAsFrictionGravity: boolean;
useFrictionGravityOnZeroGravity: boolean;
doProfiling: boolean;
lastStepTime: number;
broadphase: Broadphase;
constraints: Constraint[];
defaultMaterial: Material;
defaultContactMaterial: ContactMaterial;
lastTimeStep: number;
applySpringForces: boolean;
applyDamping: boolean;
applyGravity: boolean;
solveConstraints: boolean;
contactMaterials: ContactMaterial[];
time: number;
stepping: boolean;
islandSplit: boolean;
emitImpactEvent: boolean;
sleepMode: number;
addConstraint(c: Constraint): void;
addContactMaterial(contactMaterial: ContactMaterial): void;
removeContactMaterial(cm: ContactMaterial): void;
getContactMaterial(materialA: Material, materialB: Material): ContactMaterial; // ContactMaterial | boolean
removeConstraint(c: Constraint): void;
step(dy: number, timeSinceLastCalled?: number, maxSubSteps?: number): void;
runNarrowphase(np: Narrowphase, bi: Body, si: Shape, xi: any[], ai: number, bj: Body, sj: Shape, xj: any[], aj: number, cm: number, glen: number): void;
addSpring(s: Spring): void;
removeSpring(s: Spring): void;
addBody(body: Body): void;
removeBody(body: Body): void;
getBodyByID(id: number): Body; //Body | boolean
disableBodyCollision(bodyA: Body, bodyB: Body): void;
enableBodyCollision(bodyA: Body, bodyB: Body): void;
clear(): void;
clone(): World;
hitTest(worldPoint: number[], bodies: Body[], precision: number): Body[];
setGlobalEquationParameters(parameters: {
relaxation?: number;
stiffness?: number;
}): void;
setGlobalStiffness(stiffness: number): void;
setGlobalRelaxation(relaxation: number): void;
}
}

87395
src/lib/phaser.d.ts vendored

File diff suppressed because it is too large Load Diff

1855
src/lib/pixi.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,7 @@
module TK.SpaceTac.UI.Specs {
testing("AssetLoading", test => {
let testgame = setupSingleView(test, () => [new AssetLoading(), []]);
test.case("loads correctly", check => {
check.equals(testgame.ui.state.current, "test");
// TODO test asset loading
});
let testgame = setupSingleView(test, () => [new AssetLoading({}), []]);
test.case("builds cache keys from path", check => {
check.equals(AssetLoading.getKey("dir/file-path"), "dir-file-path");

View File

@ -26,19 +26,28 @@ module TK.SpaceTac.UI {
}
}
init(range = AssetLoadingRange.NONE) {
super.init();
init(data: any) {
super.init(data);
this.required = range;
this.required = data ? data.range : AssetLoadingRange.NONE;
}
preload() {
let bg = this.add.image(643, 435, "preload-background");
bg.setOrigin(0);
let bar = this.add.image(643, 435, "preload-bar");
this.load.setPreloadSprite(bar);
bar.setOrigin(0);
let mask = this.make.graphics({ x: bar.x, y: bar.y, add: false });
mask.fillStyle(0xffffff);
bar.setMask(new Phaser.Display.Masks.GeometryMask(this, mask));
this.load.on('progress', (value: number) => {
mask.clear();
mask.fillRect(0, 0, value * bar.width, bar.height);
});
let text = this.add.text(this.getMidWidth(), 466, "... Loading ...", { font: "normal 36pt SpaceTac", fill: "#dbeff9" });
text.anchor.set(0.5);
text.setOrigin(0.5);
if (this.required >= AssetLoadingRange.MENU && AssetLoading.loaded < AssetLoadingRange.MENU) {
console.log("Loading menu assets");
@ -58,15 +67,13 @@ module TK.SpaceTac.UI {
console.log("Loading campaign assets");
this.load.pack("stage3", `assets/pack3.json?t=${Date.now()}`);
}
this.load.start();
}
create() {
super.create();
AssetLoading.loaded = Math.max(AssetLoading.loaded, this.required);
this.game.state.start("router");
this.backToRouter();
}
static getKey(path: string): string {
@ -74,11 +81,19 @@ module TK.SpaceTac.UI {
}
loadSheet(path: string, frame_width: number, frame_height = frame_width) {
this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, frame_width, frame_height);
this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, {
frameWidth: frame_width,
frameHeight: frame_height,
});
}
loadAnimation(path: string, frame_width: number, frame_height = frame_width, count?: number) {
this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, frame_width, frame_height, count);
loadAnimation(path: string, frame_width: number, frame_height = frame_width, count: number) {
this.load.spritesheet(AssetLoading.getKey(path), "images/" + path, {
frameWidth: frame_width,
frameHeight: frame_height,
startFrame: 0,
endFrame: count - 1
});
}
}
}

View File

@ -5,7 +5,7 @@ module TK.SpaceTac.UI.Specs {
let testgame = setupEmptyView(test);
test.case("initializes variables", check => {
let view = <BaseView>testgame.ui.state.getCurrentState();
let view = nn(testgame.ui.getActiveScene());
check.equals(view.messages instanceof Messages, true);
check.equals(view.inputs instanceof InputManager, true);

View File

@ -2,42 +2,47 @@ module TK.SpaceTac.UI {
/**
* Base class for all game views
*/
export class BaseView extends Phaser.State {
export class BaseView extends Phaser.Scene {
// Link to the root UI
gameui!: MainUI
// Message notifications
messages_layer!: UIContainer
messages!: Messages
// Audio system
audio!: Audio
// Input and key bindings
inputs!: InputManager
// Animations
animations!: Animations
particles!: ParticleSystem
// Timing
timer!: Timer
// Tooltip
tooltip_layer!: Phaser.Group
tooltip_layer!: UIContainer
tooltip!: Tooltip
// Layers
layers!: Phaser.Group
layers!: UIContainer
// Modal dialogs
dialogs_layer!: Phaser.Group
dialogs_layer!: UIContainer
dialogs_opened: UIDialog[] = []
// Verbose debug output
readonly debug = false
debug = false
// Get the size of display
getWidth(): number {
return this.game.width || 1280;
return this.cameras.main.width;
}
getHeight(): number {
return this.game.height || 720;
return this.cameras.main.height;
}
getMidWidth(): number {
return this.getWidth() / 2;
@ -46,28 +51,38 @@ module TK.SpaceTac.UI {
return this.getHeight() / 2;
}
init(...args: any[]) {
this.gameui = <MainUI>this.game;
init(data: object) {
console.log(`Starting scene ${classname(this)}`);
this.gameui = <MainUI>this.sys.game;
this.timer = new Timer(this.gameui.headless);
this.animations = new Animations(this.tweens);
this.particles = new ParticleSystem(this);
this.inputs = new InputManager(this);
this.audio = new Audio(this);
this.debug = this.gameui.debug;
this.events.once("shutdown", () => this.shutdown());
}
shutdown() {
console.log(`Shutting down scene ${classname(this)}`);
this.inputs.destroy();
this.audio.stopMusic();
this.timer.cancelAll(true);
}
create() {
// Phaser config
this.game.stage.backgroundColor = 0x000000;
this.game.stage.disableVisibilityChange = this.gameui.headless;
this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.scale.fullScreenScaleMode = Phaser.ScaleManager.SHOW_ALL;
this.input.maxPointers = 1;
// Tools
this.animations = new Animations(this.game.tweens);
this.inputs = new InputManager(this);
// Layers
this.layers = this.add.group(undefined, "View layers");
this.dialogs_layer = this.add.group(undefined, "Dialogs layer");
this.tooltip_layer = this.add.group(undefined, "Tooltip layer");
this.layers = this.add.container(0, 0);
this.layers.setName("View layers");
this.dialogs_layer = this.add.container(0, 0);
this.dialogs_layer.setName("Dialogs layer");
this.tooltip_layer = this.add.container(0, 0);
this.tooltip_layer.setName("Tooltip layer");
this.tooltip = new Tooltip(this);
this.messages_layer = this.add.container(0, 0);
this.messages = new Messages(this);
this.dialogs_opened = [];
@ -81,21 +96,8 @@ module TK.SpaceTac.UI {
(<any>window).view = this;
}
}
super.create();
}
shutdown() {
this.audio.stopMusic();
super.shutdown();
this.timer.cancelAll(true);
}
get audio() {
return this.gameui.audio;
}
get options() {
return this.gameui.options;
}
@ -107,18 +109,22 @@ module TK.SpaceTac.UI {
* Go back to the router state
*/
backToRouter() {
this.game.state.start('router');
this.scene.start('router');
}
/**
* Get or create a layer in the view, by its name
*/
getLayer(name: string): Phaser.Group {
let layer = <Phaser.Group>this.layers.getByName(name);
if (!layer) {
layer = this.add.group(this.layers, name);
getLayer(name: string): UIContainer {
let layer = this.layers.getByName(name);
if (layer && layer instanceof UIContainer) {
return layer;
} else {
let layer = new UIContainer(this);
layer.setName(name);
this.layers.add(layer);
return layer;
}
return layer;
}
/**
@ -181,81 +187,43 @@ module TK.SpaceTac.UI {
* Check if the mouse is inside a given area
*/
isMouseInside(area: IBounded): boolean {
let pos = this.input.mousePointer.position;
let pos = this.input.activePointer.position;
return pos.x >= area.x && pos.x < area.x + area.width && pos.y >= area.y && pos.y < area.y + area.height;
}
/**
* Create a simple text
*/
newText(content: string, x = 0, y = 0, size = 16, color = "#ffffff", shadow = true, bold = false, center = true, vcenter = center, width = 0): Phaser.Text {
let style = { font: `${bold ? "bold " : ""}${size}pt SpaceTac`, fill: color, align: center ? "center" : "left" };
let text = new Phaser.Text(this.game, x, y, content, style);
text.anchor.set(center ? 0.5 : 0, vcenter ? 0.5 : 0);
if (width) {
text.wordWrap = true;
text.wordWrapWidth = width;
}
if (shadow) {
text.setShadow(3, 4, "rgba(0,0,0,0.6)", 6);
}
return text;
}
/**
* Get a new image from an atlas name
*/
newImage(name: string, x = 0, y = 0): Phaser.Image {
newImage(name: string, x = 0, y = 0): UIImage {
let info = this.getImageInfo(name);
let result = this.game.add.image(x, y, info.key, info.frame);
let result = this.add.image(x, y, info.key, info.frame);
result.name = name;
return result;
}
/**
* Get a new button from an atlas name
*/
newButton(name: string, x = 0, y = 0, onclick?: Function): Phaser.Button {
let info = this.getImageInfo(name);
let button = new Phaser.Button(this.game, x, y, info.key, onclick || nop, null, info.frame, info.frame);
let clickable = bool(onclick);
button.input.useHandCursor = clickable;
if (clickable) {
UIComponent.setButtonSound(button);
}
return button;
}
/**
* Update an image from an atlas name
*/
changeImage(image: Phaser.Image | Phaser.Button, name: string): void {
changeImage(image: UIImage, name: string): void {
let info = this.getImageInfo(name);
image.name = name;
if (image instanceof Phaser.Button) {
image.loadTexture(info.key);
image.setFrames(info.frame, info.frame);
} else {
image.loadTexture(info.key, info.frame);
}
image.setName(name);
image.setTexture(info.key, info.frame);
}
/**
* Get an image from atlases
*/
getImageInfo(name: string): { key: string, frame: number, exists: boolean } {
// TODO Cache
if (this.game.cache.checkImageKey(name)) {
getImageInfo(name: string): { key: string, frame: number | string, exists: boolean } {
if (this.textures.exists(name)) {
return { key: name, frame: 0, exists: true };
} else {
for (let j = 1; j <= 3; j++) {
let i = 1;
while (this.game.cache.checkImageKey(`atlas${j}-${i}`)) {
let data = this.game.cache.getFrameData(`atlas${j}-${i}`);
let frames = data.getFrames();
let frame = first(frames, frame => AssetLoading.getKey(frame.name) == `data-stage${j}-image-${name}`);
while (this.textures.exists(`atlas${j}-${i}`)) {
let frames = this.textures.get(`atlas${j}-${i}`).getFrameNames();
let frame = first(frames, frame => AssetLoading.getKey(frame) == `data-stage${j}-image-${name}`);
if (frame) {
return { key: `atlas${j}-${i}`, frame: frame.index, exists: true };
return { key: `atlas${j}-${i}`, frame: frame, exists: true };
}
i++;
}
@ -270,5 +238,12 @@ module TK.SpaceTac.UI {
getFirstImage(...names: string[]): string {
return first(names, name => this.getImageInfo(name).key.substr(0, 9) != '-missing-') || names[names.length - 1];
}
/**
* Check if the scene is paused
*/
isPaused(): boolean {
return this.time.paused;
}
}
}

View File

@ -3,11 +3,11 @@
module TK.SpaceTac.UI.Specs {
testing("Boot", test => {
let testgame = setupSingleView(test, () => [new Boot(), []]);
let testgame = setupSingleView(test, () => [new Boot({}), {}]);
test.case("places empty loading background", check => {
check.equals(testgame.ui.world.children.length, 1);
check.equals(testgame.ui.world.children[0] instanceof Phaser.Image, true);
check.equals(testgame.view.children.length, 1);
check.equals(testgame.view.children.list[0] instanceof Phaser.GameObjects.Image, true);
});
});
}

View File

@ -4,23 +4,16 @@ module TK.SpaceTac.UI {
*
* It is responsible to prepare the screen, and the asset loading.
*/
export class Boot extends Phaser.State {
export class Boot extends Phaser.Scene {
preload() {
if (!(<MainUI>this.game).headless) {
this.load.image("preload-background", "images/preload/bar-background.png");
this.load.image("preload-bar", "images/preload/bar-content.png");
}
this.load.image("preload-background", "images/preload/bar-background.png");
this.load.image("preload-bar", "images/preload/bar-content.png");
}
create() {
this.game.stage.backgroundColor = 0x000000;
this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.scale.fullScreenScaleMode = Phaser.ScaleManager.SHOW_ALL;
this.input.maxPointers = 1;
this.add.image(643, 435, "preload-background");
this.game.state.start("router");
this.scene.start("router");
}
}
}

View File

@ -3,10 +3,10 @@
module TK.SpaceTac.UI.Specs {
testing("Router", test => {
let testgame = setupSingleView(test, () => [new Router(), []]);
let testgame = setupSingleView(test, () => [new Router({}), {}]);
test.case("loads correctly", check => {
check.equals(testgame.ui.state.current, "test");
check.instance(testgame.ui.getActiveScene(), Router, "active scene should be Router");
// TODO test routing
});
});

View File

@ -6,19 +6,20 @@ module TK.SpaceTac.UI {
*
* If needed, it will go back to the asset loading state.
*/
export class Router extends Phaser.State {
export class Router extends BaseView {
create() {
var ui = <MainUI>this.game;
var session = ui.session;
super.create();
let session = this.session;
if (session.getBattle()) {
// A battle is raging, go to it
this.goToState("battle", AssetLoadingRange.BATTLE, session.player, session.getBattle());
this.goToState("battle", AssetLoadingRange.BATTLE, { player: session.player, battle: 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);
this.goToState("universe", AssetLoadingRange.CAMPAIGN, { player: session.player, universe: session.universe });
} else if (session.isIntroViewed()) {
// Build initial fleet
this.goToState("creation", AssetLoadingRange.CAMPAIGN);
@ -32,11 +33,11 @@ module TK.SpaceTac.UI {
}
}
goToState(name: string, asset_range: AssetLoadingRange, ...args: any[]) {
goToState(name: string, asset_range: AssetLoadingRange, data?: object) {
if (AssetLoading.isRangeLoaded(this.game, asset_range)) {
this.game.state.start(name, true, false, ...args);
this.scene.start(name, data);
} else {
this.game.state.start("loading", true, false, asset_range);
this.scene.start("loading", { range: asset_range });
}
}
}

View File

@ -1,25 +1,22 @@
/// <reference path="../common/Testing.ts" />
module TK.SpaceTac.UI.Specs {
let test_ui: MainUI;
/**
* Class to hold references to test objects (used as singleton in "describe" blocks)
*
* Attributes should only be accessed from inside corresponding "it" blocks (they are initialized by the setup).
*/
export class TestGame<T extends Phaser.State> {
export class TestGame<T extends Phaser.Scene> {
ui!: MainUI;
view!: T;
multistorage!: Multi.FakeRemoteStorage;
state!: string;
clock!: FakeClock;
}
/**
* Setup a headless test UI, with a single view started.
*/
export function setupSingleView<T extends Phaser.State>(test: TestSuite, buildView: () => [T, any[]]) {
export function setupSingleView<T extends Phaser.Scene & { create: Function }>(test: TestSuite, buildView: () => [T, object]) {
let testgame = new TestGame<T>();
test.asetup(() => new Promise((resolve, reject) => {
@ -27,47 +24,26 @@ module TK.SpaceTac.UI.Specs {
check.patch(console, "log", null);
check.patch(console, "warn", null);
if (!test_ui) {
test_ui = new MainUI(true);
testgame.ui = new MainUI(true);
if (test_ui.load) {
check.patch(test_ui.load, 'image', null);
check.patch(test_ui.load, 'audio', null);
}
}
let [scene, scenedata] = buildView();
testgame.ui = test_ui;
testgame.ui.resetSession();
let [state, stateargs] = buildView();
if (state instanceof BaseView) {
if (scene instanceof BaseView) {
testgame.multistorage = new Multi.FakeRemoteStorage();
let connection = new Multi.Connection(RandomGenerator.global.id(12), testgame.multistorage);
check.patch(state, "getConnection", () => connection);
check.patch(scene, "getConnection", () => connection);
}
let orig_create = bound(state, "create");
check.patch(state, "create", () => {
let orig_create = bound(scene, "create");
check.patch(scene, "create", () => {
orig_create();
resolve();
});
testgame.ui.state.add("test", state);
testgame.ui.state.start("test", true, false, ...stateargs);
testgame.ui.scene.add("test", scene, true, scenedata);
testgame.state = "test_initial";
check.patch(testgame.ui.state, "start", (name: string) => {
testgame.state = name;
});
if (!testgame.ui.isBooted) {
testgame.ui.device.canvas = true;
testgame.ui.boot();
}
testgame.view = state;
}));
testgame.view = scene;
}), () => testgame.ui.destroy(true));
return testgame;
}
@ -77,7 +53,7 @@ module TK.SpaceTac.UI.Specs {
*/
export function setupEmptyView(test: TestSuite): TestGame<BaseView> {
return setupSingleView(test, () => {
return [new BaseView(), []];
return [new BaseView({}), {}];
});
}
@ -86,14 +62,14 @@ module TK.SpaceTac.UI.Specs {
*/
export function setupBattleview(test: TestSuite): TestGame<BattleView> {
return setupSingleView(test, () => {
let view = new BattleView();
let view = new BattleView({});
view.splash = false;
let battle = Battle.newQuickRandom();
let player = new Player();
nn(battle.playing_ship).fleet.setPlayer(player);
return [view, [player, battle]];
return [view, { player, battle }];
});
}
@ -102,11 +78,11 @@ module TK.SpaceTac.UI.Specs {
*/
export function setupMapview(test: TestSuite): TestGame<UniverseMapView> {
return setupSingleView(test, () => {
let mapview = new UniverseMapView();
let mapview = new UniverseMapView({});
let session = new GameSession();
session.startNewGame();
return [mapview, [session.universe, session.player]];
return [mapview, { universe: session.universe, player: session.player }];
});
}
@ -114,9 +90,9 @@ module TK.SpaceTac.UI.Specs {
* Crawn through the children of a node
*/
export function crawlChildren(node: UIContainer, recursive: boolean, callback: (child: any) => void): void {
node.children.forEach(child => {
node.list.forEach(child => {
callback(child);
if (recursive && (child instanceof Phaser.Group || child instanceof Phaser.Image)) {
if (recursive && child instanceof UIContainer) {
crawlChildren(child, true, callback);
}
});
@ -128,7 +104,7 @@ module TK.SpaceTac.UI.Specs {
export function collectImages(node: UIContainer, recursive = true): (string | null)[] {
let result: (string | null)[] = [];
crawlChildren(node, recursive, child => {
if (child instanceof Phaser.Image) {
if (child instanceof UIImage) {
result.push(child.name || null);
}
});
@ -141,7 +117,7 @@ module TK.SpaceTac.UI.Specs {
export function collectTexts(node: UIContainer, recursive = true): (string | null)[] {
let result: (string | null)[] = [];
crawlChildren(node, recursive, child => {
if (child instanceof Phaser.Text) {
if (child instanceof UIText) {
result.push(child.text || null);
}
});
@ -152,20 +128,19 @@ module TK.SpaceTac.UI.Specs {
* Check a given text node
*/
export function checkText(check: TestContext, node: any, content: string): void {
check.equals(node instanceof Phaser.Text, true);
let tnode = <Phaser.Text>node;
check.equals(tnode.text, content);
if (check.instance(node, UIText, "node should be an UIText")) {
check.equals(node.text, content);
}
}
/**
* Check that a layer contains the given component at a given index
*/
export function checkComponentInLayer(check: TestContext, layer: Phaser.Group, index: number, component: UIComponent) {
if (index >= layer.children.length) {
export function checkComponentInLayer(check: TestContext, layer: UIContainer, index: number, component: UIComponent) {
if (index >= layer.list.length) {
check.fail(`Not enough children in group ${layer.name} for ${component} at index ${index}`);
} else {
let child = layer.children[index];
let child = layer.list[index];
if (child !== (<any>component).container) {
check.fail(`${component} is not at index ${index} in ${layer.name}`);
}
@ -175,8 +150,8 @@ module TK.SpaceTac.UI.Specs {
/**
* Simulate a click on a button
*/
export function testClick(button: Phaser.Button): void {
button.onInputDown.dispatch();
button.onInputUp.dispatch();
export function testClick(button: UIButton): void {
button.emit("pointerdown");
button.emit("pointerup");
}
}

View File

@ -38,15 +38,16 @@ module TK.SpaceTac.UI.Specs {
function checkpoints(desc: string, available = 0, using = 0, used = 0) {
check.in(desc, check => {
check.same(bar.power_icons.children.length, available + using + used, "icon count");
bar.power_icons.children.forEach((child, idx) => {
let img = <Phaser.Image>child;
if (idx < available) {
check.equals(img.name, "battle-actionbar-power-available", `icon ${idx}`);
} else if (idx < available + using) {
check.equals(img.name, "battle-actionbar-power-move", `icon ${idx}`);
} else {
check.equals(img.name, "battle-actionbar-power-used", `icon ${idx}`);
check.same(bar.power_icons.length, available + using + used, "icon count");
bar.power_icons.list.forEach((child, idx) => {
if (check.instance(child, UIImage, `${idx} icon should be an image`)) {
if (idx < available) {
check.equals(child.name, "battle-actionbar-power-available", `icon ${idx}`);
} else if (idx < available + using) {
check.equals(child.name, "battle-actionbar-power-move", `icon ${idx}`);
} else {
check.equals(child.name, "battle-actionbar-power-used", `icon ${idx}`);
}
}
});
});

View File

@ -1,19 +1,23 @@
/// <reference path="../common/UIContainer.ts" />
module TK.SpaceTac.UI {
// Bar with all available action icons displayed
export class ActionBar extends Phaser.Group {
/**
* Bar on the border of screen to display all available action icons
*/
export class ActionBar extends UIContainer {
// Link to the parent battleview
battleview: BattleView
// List of action icons
actions: Phaser.Group
actions: UIContainer
action_icons: ActionIcon[]
// Power indicator
power: Phaser.Group
power_icons!: Phaser.Group
power: UIContainer
power_icons!: UIContainer
// Indicator of interaction disabled
icon_waiting: Phaser.Image
icon_waiting: UIImage
// Current ship, whose actions are displayed
ship: Ship | null
@ -23,7 +27,7 @@ module TK.SpaceTac.UI {
// Create an empty action bar
constructor(battleview: BattleView) {
super(battleview.game);
super(battleview);
this.battleview = battleview;
this.action_icons = [];
@ -34,27 +38,26 @@ module TK.SpaceTac.UI {
let builder = new UIBuilder(battleview, this);
// Background
builder.image("battle-actionbar-background");
let base = builder.image("battle-actionbar-background");
// Group for actions
this.actions = builder.group("actions", 86, 6);
this.actions = builder.container("actions", 86, 6);
builder.in(this.actions).image("battle-actionbar-actions-background");
// Power bar
this.power = builder.group("power", 1466, 0);
this.power = builder.container("power", 1466, 0);
builder.in(this.power, builder => {
builder.image("battle-actionbar-power-background", 0, 6);
this.power_icons = builder.group("power icons", 50, 14);
this.power_icons = builder.container("power icons", 50, 14);
});
// Playing ship
builder.image("battle-actionbar-ship", 1735);
// Waiting icon
this.icon_waiting = new Phaser.Image(this.game, this.width / 2, this.height / 2, "common-waiting", 0);
this.icon_waiting.anchor.set(0.5, 0.5);
this.icon_waiting.animations.add("loop").play(9, true);
this.add(this.icon_waiting);
this.icon_waiting = builder.image("common-waiting", base.width / 2, base.height / 2, true);
// FIXME
//this.icon_waiting.animations.add("loop").play(9, true);
// Options button
builder.button("battle-actionbar-button-menu", 0, 0, () => battleview.showOptions(), "Game options");
@ -105,7 +108,7 @@ module TK.SpaceTac.UI {
}
}
});
this.setInteractive(false);
this.setInteractivity(false);
}
/**
@ -118,7 +121,7 @@ module TK.SpaceTac.UI {
/**
* Set the interactivity state
*/
setInteractive(interactive: boolean) {
setInteractivity(interactive: boolean) {
if (this.interactive != interactive) {
this.interactive = interactive;
@ -167,7 +170,7 @@ module TK.SpaceTac.UI {
let power_capacity = this.ship ? this.ship.getAttribute("power_capacity") : 0;
let power_value = this.ship ? this.ship.getValue("power") : 0;
let current_power = this.power_icons.children.length;
let current_power = this.power_icons.length;
if (current_power > power_capacity) {
destroyChildren(this.power_icons, power_capacity, current_power);
@ -175,14 +178,13 @@ module TK.SpaceTac.UI {
range(power_capacity - current_power).forEach(i => {
let x = (current_power + i) % 5;
let y = ((current_power + i) - x) / 5;
let image = this.battleview.newImage("battle-actionbar-power-used", x * 43, y * 22);
this.power_icons.add(image);
let image = new UIBuilder(this.battleview, this.power_icons).image("battle-actionbar-power-used", x * 43, y * 22);
});
}
let remaining_power = power_value - move_power - fire_power;
this.power_icons.children.forEach((obj, idx) => {
let img = <Phaser.Image>obj;
this.power_icons.list.forEach((obj, idx) => {
let img = <UIImage>obj;
if (idx < remaining_power) {
this.battleview.changeImage(img, "battle-actionbar-power-available");
} else if (idx < remaining_power + move_power) {

View File

@ -77,15 +77,15 @@ module TK.SpaceTac.UI.Specs {
check.equals(icon.img_bottom.name, "battle-actionbar-bottom-enabled", "initial");
check.equals(icon.img_power.name, "battle-actionbar-consumption-enabled", "initial");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-untoggled", "initial");
check.same(icon.img_sticky.visible, true, "initial");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial");
check.same(icon.img_cooldown.visible, true, "initial");
ship.actions.toggle(action, true);
icon.refresh();
check.equals(icon.img_bottom.name, "battle-actionbar-bottom-toggled", "initial");
check.equals(icon.img_power.name, "battle-actionbar-consumption-toggled", "initial");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-toggled", "initial");
check.same(icon.img_sticky.visible, true, "initial");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-toggled", "initial");
check.same(icon.img_cooldown.visible, true, "initial");
})
test.case("displays overheat/cooldown", check => {
@ -96,29 +96,29 @@ module TK.SpaceTac.UI.Specs {
action.configureCooldown(1, 3);
TestTools.setShipModel(ship, 100, 0, 5, 1, [action]);
let icon = new ActionIcon(bar, ship, action, 0);
check.same(icon.img_sticky.visible, false, "initial");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-untoggled", "initial");
check.same(icon.img_sticky.children.length, 0, "initial");
check.same(icon.img_cooldown.visible, false, "initial");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-untoggled", "initial");
check.same(icon.img_cooldown_group.length, 1, "initial");
icon.refresh(action);
check.same(icon.img_sticky.visible, true, "overheat");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-overheat", "overheat");
check.same(icon.img_sticky.children.length, 3, "overheat");
check.same(icon.img_cooldown.visible, true, "overheat");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "overheat");
check.same(icon.img_cooldown_group.length, 4, "overheat");
action.configureCooldown(1, 12);
TestTools.setShipModel(ship, 100, 0, 5, 1, [action]);
icon.refresh(action);
check.same(icon.img_sticky.visible, true, "superheat");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-overheat", "superheat");
check.same(icon.img_sticky.children.length, 5, "superheat");
check.same(icon.img_cooldown.visible, true, "superheat");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-overheat", "superheat");
check.same(icon.img_cooldown_group.length, 6, "superheat");
action.configureCooldown(1, 4);
TestTools.setShipModel(ship, 100, 0, 5, 1, [action]);
ship.actions.getCooldown(action).use();
icon.refresh(action);
check.same(icon.img_sticky.visible, true, "cooling");
check.equals(icon.img_sticky.name, "battle-actionbar-sticky-disabled", "cooling");
check.same(icon.img_sticky.children.length, 4, "cooling");
check.same(icon.img_cooldown.visible, true, "cooling");
check.equals(icon.img_cooldown.name, "battle-actionbar-sticky-disabled", "cooling");
check.same(icon.img_cooldown_group.length, 5, "cooling");
})
test.case("displays currently targetting", check => {

View File

@ -8,7 +8,7 @@ module TK.SpaceTac.UI {
view: BattleView
// Container
container: Phaser.Button
container: UIButton
// Related ship
ship: Ship
@ -25,40 +25,39 @@ module TK.SpaceTac.UI {
cooldown = 0
// Images
img_targetting!: Phaser.Image
img_top: Phaser.Image | null = null
img_bottom: Phaser.Image
img_power: Phaser.Image
img_sticky: Phaser.Image
img_action: Phaser.Image
img_targetting!: UIImage
img_top: UIImage | null = null
img_bottom: UIImage
img_power: UIImage
img_cooldown_group: UIContainer
img_cooldown: UIImage
img_action: UIImage
// Indicators
text_power!: Phaser.Text
text_power!: UIText
constructor(bar: ActionBar, ship: Ship, action: BaseAction, position: number) {
this.bar = bar;
this.view = bar.battleview;
let info = this.view.getImageInfo("battle-actionbar-frame-disabled");
this.container = this.view.add.button(0, 0, info.key, () => this.processClick(), undefined, info.frame, info.frame);
this.container.anchor.set(0.5);
this.container.input.useHandCursor = false;
let builder = new UIBuilder(this.view);
this.container = builder.button("battle-actionbar-frame-disabled", 0, 0, () => this.processClick(), filler => {
ActionTooltip.fill(filler, this.ship, this.action, position);
return true;
}, undefined, { center: true });
builder = builder.in(this.container);
this.ship = ship;
this.action = action;
let builder = new UIBuilder(this.view, this.container);
// Action icon
this.img_action = builder.image(`action-${action.code}`);
this.img_action.anchor.set(0.5);
this.img_action.scale.set(0.35);
this.img_action.alpha = 0.2;
this.img_action = builder.image(`action-${action.code}`, 0, 0, true);
this.img_action.setScale(0.35);
this.img_action.setAlpha(0.2);
// Hotkey indicator
if (!(action instanceof EndTurnAction)) {
this.img_top = builder.image("battle-actionbar-hotkey", 0, -47);
this.img_top.anchor.set(0.5);
this.img_top = builder.image("battle-actionbar-hotkey", 0, -47, true);
builder.in(this.img_top, builder => {
builder.text(`${(position + 1) % 10}`, 0, 0, {
size: 12, color: "#d1d1d1", shadow: true, center: true, vcenter: true
@ -67,19 +66,16 @@ module TK.SpaceTac.UI {
}
// Bottom indicator
this.img_bottom = builder.image("battle-actionbar-bottom-disabled", 0, 40);
this.img_bottom.anchor.set(0.5);
this.img_bottom = builder.image("battle-actionbar-bottom-disabled", 0, 40, true);
builder.in(this.img_bottom, builder => {
this.img_targetting = builder.image("battle-actionbar-bottom-targetting", 0, 12);
this.img_targetting.anchor.set(0.5);
this.img_targetting.visible = false;
this.img_targetting.setVisible(false);
});
// Left indicator
this.selected = false;
this.img_power = builder.image("battle-actionbar-consumption-disabled", -46);
this.img_power.anchor.set(0.5);
this.img_power.visible = false;
this.img_power = builder.image("battle-actionbar-consumption-disabled", -46, 0, true);
this.img_power.setVisible(false);
builder.in(this.img_power, builder => {
this.text_power = builder.text("", -2, 4, {
size: 16, color: "#ffdd4b", shadow: true, center: true, vcenter: true
@ -87,15 +83,8 @@ module TK.SpaceTac.UI {
});
// Right indicator
this.img_sticky = builder.image("battle-actionbar-sticky-untoggled", 46);
this.img_sticky.anchor.set(0.5);
this.img_sticky.visible = action instanceof ToggleAction;
// Events
this.view.tooltip.bind(this.container, filler => {
ActionTooltip.fill(filler, this.ship, this.action, position);
return true;
});
this.img_cooldown_group = builder.container("cooldown", 46, 0, action instanceof ToggleAction);
this.img_cooldown = builder.in(this.img_cooldown_group).image("battle-actionbar-sticky-untoggled", 46, 0, true);
// Initialize
this.refresh();
@ -111,9 +100,9 @@ module TK.SpaceTac.UI {
/**
* Move to a given layer and position
*/
moveTo(layer: Phaser.Group, x = 0, y = 0): void {
moveTo(layer: UIContainer, x = 0, y = 0): void {
layer.add(this.container);
this.container.position.set(x, y);
this.container.setPosition(x, y);
}
/**
@ -182,7 +171,7 @@ module TK.SpaceTac.UI {
// inputs
if (disabled != this.disabled) {
this.container.input.useHandCursor = !disabled;
//this.container.input.useHandCursor = !disabled;
}
// frame
@ -193,11 +182,7 @@ module TK.SpaceTac.UI {
} else if (fading) {
name = "battle-actionbar-frame-fading";
}
let info = this.view.getImageInfo(name);
this.container.name = name;
this.container.loadTexture(info.key);
this.container.setFrames(info.frame, info.frame, info.frame, info.frame);
this.container.setBaseImage(name);
}
// action icon
@ -226,10 +211,10 @@ module TK.SpaceTac.UI {
// left
let cost = this.action.getPowerUsage(this.ship, null);
this.img_power.visible = bool(cost);
this.text_power.text = `${Math.abs(cost)}\n${cost < 0 ? "+" : "-"}`;
this.text_power.fill = (cost > 0) ? "#ffdd4b" : "#dbe748";
this.text_power.alpha = disabled ? 0.2 : 1;
this.img_power.setVisible(bool(cost));
this.text_power.setText(`${Math.abs(cost)}\n${cost < 0 ? "+" : "-"}`);
this.text_power.setColor((cost > 0) ? "#ffdd4b" : "#dbe748");
this.text_power.setAlpha(disabled ? 0.2 : 1);
if (disabled != this.disabled || selected != this.selected || toggled != this.toggled) {
if (disabled) {
this.view.changeImage(this.img_power, "battle-actionbar-consumption-disabled");
@ -244,27 +229,28 @@ module TK.SpaceTac.UI {
// right
if (toggled != this.toggled || disabled != this.disabled || heat != this.cooldown) {
destroyChildren(this.img_sticky);
let builder = new UIBuilder(this.view, this.img_cooldown_group);
destroyChildren(this.img_cooldown_group, 1);
if (this.action instanceof ToggleAction) {
if (toggled) {
this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-toggled");
builder.change(this.img_cooldown, "battle-actionbar-sticky-toggled");
} else {
this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-untoggled");
builder.change(this.img_cooldown, "battle-actionbar-sticky-untoggled");
}
this.img_sticky.visible = !disabled;
this.img_cooldown.visible = !disabled;
} else if (heat) {
if (disabled) {
this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-disabled");
builder.change(this.img_cooldown, "battle-actionbar-sticky-disabled");
} else {
this.view.changeImage(this.img_sticky, "battle-actionbar-sticky-overheat");
builder.change(this.img_cooldown, "battle-actionbar-sticky-overheat");
}
range(Math.min(heat - 1, 4)).forEach(i => {
this.img_sticky.addChild(this.view.newImage("battle-actionbar-cooldown-one", 0, 2 - i * 7));
builder.image("battle-actionbar-cooldown-one", 0, 2 - i * 7);
});
this.img_sticky.addChild(this.view.newImage("battle-actionbar-cooldown-front", -4, -20));
this.img_sticky.visible = true;
builder.image("battle-actionbar-cooldown-front", -4, -20);
this.img_cooldown.visible = true;
} else {
this.img_sticky.visible = false;
this.img_cooldown.visible = false;
}
}

View File

@ -14,23 +14,23 @@ module TK.SpaceTac.UI.Specs {
let action3 = ship.actions.addCustom(new EndTurnAction());
ActionTooltip.fill(tooltip.getBuilder(), ship, action1, 0);
checkText(check, (<any>tooltip).container.content.children[1], "Use Thruster");
checkText(check, (<any>tooltip).container.content.children[2], "Cost: 1 power per 0km");
checkText(check, (<any>tooltip).container.content.children[3], "Move: 0km per power point (safety: 120km)");
checkText(check, (<any>tooltip).container.content.children[4], "[ 1 ]");
checkText(check, tooltip.container.content.list[1], "Use Thruster");
checkText(check, tooltip.container.content.list[2], "Cost: 1 power per 0km");
checkText(check, tooltip.container.content.list[3], "Move: 0km per power point (safety: 120km)");
checkText(check, tooltip.container.content.list[4], "[ 1 ]");
tooltip.hide();
ActionTooltip.fill(tooltip.getBuilder(), ship, action2, 1);
checkText(check, (<any>tooltip).container.content.children[1], "Fire Superweapon");
checkText(check, (<any>tooltip).container.content.children[2], "Cost: 2 power");
checkText(check, (<any>tooltip).container.content.children[3], "Fire (power 2, range 50km):\n• do 12 damage on target");
checkText(check, (<any>tooltip).container.content.children[4], "[ 2 ]");
checkText(check, tooltip.container.content.list[1], "Fire Superweapon");
checkText(check, tooltip.container.content.list[2], "Cost: 2 power");
checkText(check, tooltip.container.content.list[3], "Fire (power 2, range 50km):\n• do 12 damage on target");
checkText(check, tooltip.container.content.list[4], "[ 2 ]");
tooltip.hide();
ActionTooltip.fill(tooltip.getBuilder(), ship, action3, 2);
checkText(check, (<any>tooltip).container.content.children[1], "End turn");
checkText(check, (<any>tooltip).container.content.children[2], "End the current ship's turn.\nWill also generate power and cool down equipments.");
checkText(check, (<any>tooltip).container.content.children[3], "[ space ]");
checkText(check, tooltip.container.content.list[1], "End turn");
checkText(check, tooltip.container.content.list[2], "End the current ship's turn.\nWill also generate power and cool down equipments.");
checkText(check, tooltip.container.content.list[3], "[ space ]");
});
});
}

View File

@ -10,7 +10,7 @@ module TK.SpaceTac.UI {
let builder = filler.styled({ size: 20 });
let icon = builder.image(`action-${action.code}`);
icon.scale.set(0.5);
icon.setScale(0.5);
builder.text(action.getTitle(ship), 150, 0, { size: 24 });

View File

@ -15,10 +15,7 @@ module TK.SpaceTac.UI {
range_hint: RangeHint
// Input capture
private mouse_capture?: Phaser.Button
// Input callback to receive mouse move events
private input_callback: any = null
private mouse_capture?: UIImage
// List of ship sprites
private ship_sprites: ArenaShip[] = []
@ -32,43 +29,46 @@ module TK.SpaceTac.UI {
private playing: ArenaShip | null
// Layer for particles
container: Phaser.Group
layer_garbage: Phaser.Group
layer_hints: Phaser.Group
layer_drones: Phaser.Group
layer_ships: Phaser.Group
layer_weapon_effects: Phaser.Group
layer_targetting: Phaser.Group
container: UIContainer
layer_garbage: UIContainer
layer_hints: UIContainer
layer_drones: UIContainer
layer_ships: UIContainer
layer_weapon_effects: UIContainer
layer_targetting: UIContainer
// Callbacks to receive cursor events
callbacks_hover: ((location: ArenaLocation | null, ship: Ship | null) => void)[] = []
callbacks_click: (() => void)[] = []
// Create a graphical arena for ship sprites to fight in a 2D space
constructor(view: BattleView, container?: Phaser.Group) {
constructor(view: BattleView, container?: UIContainer) {
this.view = view;
this.playing = null;
this.hovered = null;
this.range_hint = new RangeHint(this);
this.container = container || new Phaser.Group(view.game, undefined, "arena");
this.container.position.set(this.boundaries.x, this.boundaries.y);
let builder = new UIBuilder(view, container);
if (!container) {
container = builder.container("arena");
builder = builder.in(container);
}
this.container = container;
container.setPosition(this.boundaries.x, this.boundaries.y);
this.setupMouseCapture();
this.layer_garbage = this.container.add(new Phaser.Group(view.game, undefined, "garbage"));
this.layer_hints = this.container.add(new Phaser.Group(view.game, undefined, "hints"));
this.layer_drones = this.container.add(new Phaser.Group(view.game, undefined, "drones"));
this.layer_ships = this.container.add(new Phaser.Group(view.game, undefined, "ships"));
this.layer_weapon_effects = this.container.add(new Phaser.Group(view.game, undefined, "effects"));
this.layer_targetting = this.container.add(new Phaser.Group(view.game, undefined, "targetting"));
this.layer_garbage = builder.container("garbage");
this.layer_hints = builder.container("hints");
this.layer_drones = builder.container("drones");
this.layer_ships = builder.container("ships");
this.layer_weapon_effects = builder.container("effects");
this.layer_targetting = builder.container("targetting");
this.range_hint.setLayer(this.layer_hints);
this.addShipSprites();
view.battle.drones.list().forEach(drone => this.addDrone(drone, false));
this.container.onDestroy.add(() => this.destroy());
view.log_processor.register(diff => this.checkDroneDeployed(diff));
view.log_processor.register(diff => this.checkDroneRecalled(diff));
view.log_processor.watchForShipChange(ship => {
@ -83,7 +83,7 @@ module TK.SpaceTac.UI {
/**
* Move to a specific layer
*/
moveToLayer(layer: Phaser.Group): void {
moveToLayer(layer: UIContainer): void {
layer.add(this.container);
}
@ -93,35 +93,31 @@ module TK.SpaceTac.UI {
setupMouseCapture() {
let view = this.view;
let info = view.getImageInfo("battle-arena-background");
var background = new Phaser.Button(view.game, 0, 0, info.key, undefined, undefined, info.frame, info.frame);
background.name = "mouse-capture";
background.scale.set(this.boundaries.width / background.width, this.boundaries.height / background.height);
this.mouse_capture = background;
let background = new UIBuilder(view, this.container).image("battle-arena-background");
background.setName("mouse-capture");
background.setScale(this.boundaries.width / background.width, this.boundaries.height / background.height)
// Capture clicks on background
background.onInputUp.add(() => {
background.setInteractive();
background.on("pointerup", () => {
this.callbacks_click.forEach(callback => callback());
});
background.onInputOut.add(() => {
background.on("pointerout", () => {
this.callbacks_hover.forEach(callback => callback(null, null));
});
// Watch mouse move to capture hovering over background
this.input_callback = this.view.input.addMoveCallback((pointer: Phaser.Pointer) => {
view.input.on("pointermove", (pointer: Phaser.Input.Pointer) => {
if (this.view.dialogs_opened.length > 0 || this.view.character_sheet.isOpened() || this.view.layer_overlay.length > 0) {
return;
}
let point = new Phaser.Point();
if (view.input.hitTest(background, pointer, point)) {
let location = new ArenaLocation(point.x * background.scale.x, point.y * background.scale.y);
let ship = this.getShip(location);
this.callbacks_hover.forEach(callback => callback(location, ship));
}
let location = new ArenaLocation(pointer.x, pointer.y);
let ship = this.getShip(location);
this.callbacks_hover.forEach(callback => callback(location, ship));
}, null);
this.container.add(this.mouse_capture);
this.mouse_capture = background;
}
/**
@ -136,16 +132,6 @@ module TK.SpaceTac.UI {
}
}
/**
* Call when the arena is destroyed to properly remove input handlers
*/
destroy() {
if (this.input_callback) {
this.view.input.deleteMoveCallback(this.input_callback);
this.input_callback = null;
}
}
/**
* Add the sprites for all ships
*/
@ -244,15 +230,16 @@ module TK.SpaceTac.UI {
this.drone_sprites.push(sprite);
if (animate) {
sprite.position.set(owner.arena_x, owner.arena_y);
sprite.sprite.rotation = owner.arena_angle;
let move_duration = Animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite);
this.view.tweens.create(sprite.radius).from({ alpha: 0 }, 500, Phaser.Easing.Cubic.In, true, move_duration);
sprite.radius.setAlpha(0);
sprite.setPosition(owner.arena_x, owner.arena_y);
sprite.sprite.setRotation(owner.arena_angle);
let move_duration = this.view.animations.moveInSpace(sprite, drone.x, drone.y, angle, sprite.sprite);
this.view.animations.addAnimation(sprite.radius, { alpha: 1 }, 500, "Cubic.easeIn", move_duration);
return move_duration + 500;
} else {
sprite.position.set(drone.x, drone.y);
sprite.sprite.rotation = angle;
sprite.setPosition(drone.x, drone.y);
sprite.setRotation(angle);
return 0;
}

View File

@ -2,7 +2,7 @@ module TK.SpaceTac.UI {
/**
* Drone sprite in the arena
*/
export class ArenaDrone extends Phaser.Group {
export class ArenaDrone extends UIContainer {
// Link to view
view: BattleView
@ -10,43 +10,36 @@ module TK.SpaceTac.UI {
drone: Drone
// Sprite
sprite: Phaser.Button
sprite: UIButton
// Radius
radius: Phaser.Graphics
radius: UIGraphics
// Activation effect
activation: Phaser.Graphics
activation: UIGraphics
constructor(battleview: BattleView, drone: Drone) {
super(battleview.game);
super(battleview);
this.view = battleview;
this.drone = drone;
this.radius = new Phaser.Graphics(this.game, 0, 0);
let builder = new UIBuilder(battleview, this);
this.radius = builder.graphics("radius");
this.radius.fillStyle(0xe9f2f9, 0.1);
this.radius.fillCircle(0, 0, drone.radius);
this.radius.lineStyle(2, 0xe9f2f9, 0.5);
this.radius.beginFill(0xe9f2f9, 0.1);
this.radius.drawCircle(0, 0, drone.radius * 2);
this.radius.endFill();
this.add(this.radius);
this.radius.strokeCircle(0, 0, drone.radius);
this.activation = new Phaser.Graphics(this.game, 0, 0);
this.activation = builder.graphics("activation", 0, 0, false);
this.activation.fillStyle(0xe9f2f9, 0.0);
this.activation.fillCircle(0, 0, drone.radius);
this.activation.lineStyle(2, 0xe9f2f9, 0.7);
this.activation.beginFill(0xe9f2f9, 0.0);
this.activation.drawCircle(0, 0, drone.radius * 2);
this.activation.endFill();
this.activation.visible = false;
this.add(this.activation);
this.activation.strokeCircle(0, 0, drone.radius);
this.sprite = this.view.newButton(`action-${drone.code}`);
this.sprite.anchor.set(0.5, 0.5);
this.sprite.scale.set(0.1, 0.1);
this.add(this.sprite);
this.view.tooltip.bindDynamicText(this.sprite, () => {
return this.drone.getDescription();
});
this.sprite = builder.button(`action-${drone.code}`, 0, 0, undefined, () => this.drone.getDescription(), undefined, { center: true });
this.sprite.setScale(0.1, 0.1);
}
/**
@ -55,11 +48,9 @@ module TK.SpaceTac.UI {
* Return the animation duration
*/
setApplied(): number {
this.activation.scale.set(0.001, 0.001);
this.activation.setScale(0.001, 0.001);
this.activation.visible = true;
let tween = this.game.tweens.create(this.activation.scale).to({ x: 1, y: 1 }, 500);
tween.onComplete.addOnce(() => this.activation.visible = false);
tween.start();
let tween = this.view.animations.addAnimation(this.activation, { scaleX: 1, scaleY: 1 }, 500).then(() => this.activation.setVisible(false));
return 500;
}
@ -69,14 +60,8 @@ module TK.SpaceTac.UI {
* Return the animation duration
*/
setDestroyed(): number {
this.game.tweens.create(this).to({ alpha: 0.3 }, 300).delay(200).start();
let tween = this.game.tweens.create(this.radius.scale).to({ x: 0, y: 0 }, 500);
tween.onComplete.addOnce(() => {
this.destroy();
});
tween.start();
this.view.animations.addAnimation<UIContainer>(this, { alpha: 0.3 }, 300, undefined, 200);
this.view.animations.addAnimation(this.radius, { scaleX: 0, scaleY: 0 }, 500).then(() => this.destroy());
return 500;
}
@ -84,7 +69,7 @@ module TK.SpaceTac.UI {
* Set the tactical mode display
*/
setTacticalMode(active: boolean) {
this.sprite.scale.set(active ? 0.2 : 0.1);
this.sprite.setScale(active ? 0.2 : 0.1);
}
}
}

View File

@ -8,13 +8,12 @@ module TK.SpaceTac.UI.Specs {
let ship = nn(testgame.view.battle.playing_ship);
let sprite = nn(testgame.view.arena.findShipSprite(ship));
check.equals(sprite.effects_messages.children.length, 0);
check.equals(sprite.effects_messages.list.length, 0);
sprite.displayAttributeChanged(new ShipAttributeDiff(ship, "power_capacity", { cumulative: -4 }, {}));
check.equals(sprite.effects_messages.children.length, 1);
let t1 = <Phaser.Text>sprite.effects_messages.getChildAt(0);
check.equals(t1.text, "power capacity -4");
check.equals(sprite.effects_messages.list.length, 1);
check.equals(collectTexts(sprite.effects_messages), ["power capacity -4"]);
});
test.case("adds sticky effects display", check => {
@ -22,25 +21,25 @@ module TK.SpaceTac.UI.Specs {
let ship = nn(battle.playing_ship);
let sprite = nn(testgame.view.arena.findShipSprite(ship));
check.equals(sprite.active_effects_display.children.length, 0);
check.equals(sprite.active_effects_display.list.length, 0);
let effect1 = new StickyEffect(new BaseEffect("test"));
battle.applyDiffs([new ShipEffectAddedDiff(ship, effect1)]);
testgame.view.log_processor.processPending();
check.equals(sprite.active_effects_display.children.length, 1);
check.equals(sprite.active_effects_display.list.length, 1);
let effect2 = new StickyEffect(new BaseEffect("test"));
battle.applyDiffs([new ShipEffectAddedDiff(ship, effect2)]);
testgame.view.log_processor.processPending();
check.equals(sprite.active_effects_display.children.length, 2);
check.equals(sprite.active_effects_display.list.length, 2);
battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect1)]);
testgame.view.log_processor.processPending();
check.equals(sprite.active_effects_display.children.length, 1);
check.equals(sprite.active_effects_display.list.length, 1);
battle.applyDiffs([new ShipEffectRemovedDiff(ship, effect2)]);
testgame.view.log_processor.processPending();
check.equals(sprite.active_effects_display.children.length, 0);
check.equals(sprite.active_effects_display.list.length, 0);
});
});
}

View File

@ -2,7 +2,7 @@ module TK.SpaceTac.UI {
/**
* Ship sprite in the arena, with corresponding HUD
*/
export class ArenaShip extends Phaser.Group {
export class ArenaShip extends UIContainer {
// Link to the view
arena: Arena
battleview: BattleView
@ -14,21 +14,21 @@ module TK.SpaceTac.UI {
enemy: boolean
// Ship sprite
sprite: Phaser.Image
sprite: UIImage
// Stasis effect
stasis: Phaser.Image
stasis: UIImage
// HSP display
hsp: Phaser.Image
power_text: Phaser.Text
life_hull: UIGroup
life_shield: UIGroup
life_evasion: UIGroup
hsp: UIContainer
power_text: UIText
life_hull: UIContainer
life_shield: UIContainer
life_evasion: UIContainer
toggle_hsp: Toggle
// Play order
play_order: Phaser.Text
play_order: UIText
toggle_play_order: Toggle
// Frames to indicate the owner, if the ship is hovered, and if it is hovered
@ -36,24 +36,24 @@ module TK.SpaceTac.UI {
frame_hover: UIImage
// Effects display
active_effects_display: Phaser.Group
effects_radius: Phaser.Graphics
effects_messages: Phaser.Group
active_effects_display: UIContainer
effects_radius: UIGraphics
effects_messages: UIContainer
effects_messages_toggle: Toggle
// Create a ship sprite usable in the Arena
constructor(parent: Arena, ship: Ship) {
super(parent.game);
super(parent.view);
this.arena = parent;
this.battleview = parent.view;
let builder = new UIBuilder(parent.view).in(this);
let builder = new UIBuilder(this.battleview).in(this);
this.ship = ship;
this.enemy = !this.battleview.player.is(this.ship.fleet.player);
// Add effects radius
this.effects_radius = new Phaser.Graphics(this.game);
this.add(this.effects_radius);
this.effects_radius = builder.graphics("effect-radius");
// Add frame indicating which side this ship is on
this.frame_owner = builder.image(this.enemy ? "battle-hud-ship-enemy" : "battle-hud-ship-own", 0, 0, true);
@ -71,12 +71,13 @@ module TK.SpaceTac.UI {
this.stasis.visible = !ship.alive;
// HSP display
this.hsp = builder.image("battle-hud-hsp-background", 0, 34, true);
this.hsp = builder.container("hsp", 0, 34);
builder.in(this.hsp).image("battle-hud-hsp-background", 0, 0, true);
this.power_text = builder.in(this.hsp).text(`${ship.getValue("power")}`, -42, 2,
{ size: 13, color: "#ffdd4b", bold: true, shadow: true, center: true });
this.life_hull = builder.in(this.hsp).group("hull");
this.life_shield = builder.in(this.hsp).group("shield");
this.life_evasion = builder.in(this.hsp).group("evasion");
this.life_hull = builder.in(this.hsp).container("hull");
this.life_shield = builder.in(this.hsp).container("shield");
this.life_evasion = builder.in(this.hsp).container("evasion");
this.toggle_hsp = this.battleview.animations.newVisibilityToggle(this.hsp, 200, false);
// Play order display
@ -85,11 +86,8 @@ module TK.SpaceTac.UI {
this.toggle_play_order = this.battleview.animations.newVisibilityToggle(play_order, 200, false);
// Effects display
this.active_effects_display = new Phaser.Group(this.game);
this.active_effects_display.position.set(0, -44);
this.add(this.active_effects_display);
this.effects_messages = new Phaser.Group(this.game);
this.add(this.effects_messages);
this.active_effects_display = builder.container("active-effects", 0, -44);
this.effects_messages = builder.container("effects-messages");
this.effects_messages_toggle = this.battleview.animations.newVisibilityToggle(this.effects_messages, 500, false);
this.updatePlayOrder();
@ -101,10 +99,10 @@ module TK.SpaceTac.UI {
// Set location
if (this.battleview.battle.cycle == 1 && this.battleview.battle.play_index == 0 && ship.alive && this.battleview.player.is(ship.fleet.player)) {
this.position.set(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle));
this.moveTo(ship.arena_x, ship.arena_y, ship.arena_angle);
this.setPosition(ship.arena_x - 500 * Math.cos(ship.arena_angle), ship.arena_y - 500 * Math.sin(ship.arena_angle));
this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle);
} else {
this.moveTo(ship.arena_x, ship.arena_y, ship.arena_angle, false);
this.moveToArenaLocation(ship.arena_x, ship.arena_y, ship.arena_angle, false);
}
// Log processing
@ -243,8 +241,8 @@ module TK.SpaceTac.UI {
}
} else if (diff instanceof ShipMoveDiff) {
let func = async (animate: boolean, timer: Timer) => {
this.moveTo(diff.start.x, diff.start.y, diff.start.angle, false);
let duration = this.moveTo(diff.end.x, diff.end.y, diff.end.angle, animate, !!diff.engine);
this.moveToArenaLocation(diff.start.x, diff.start.y, diff.start.angle, false);
let duration = this.moveToArenaLocation(diff.end.x, diff.end.y, diff.end.angle, animate, !!diff.engine);
if (duration && animate) {
await timer.sleep(duration);
}
@ -325,9 +323,9 @@ module TK.SpaceTac.UI {
*
* Return the duration of animation
*/
moveTo(x: number, y: number, facing_angle: number, animate = true, engine = true): number {
moveToArenaLocation(x: number, y: number, facing_angle: number, animate = true, engine = true): number {
if (animate) {
let animation = engine ? Animations.moveInSpace : Animations.moveTo;
let animation = bound(this.arena.view.animations, engine ? "moveInSpace" : "moveTo");
let duration = animation(this, x, y, facing_angle, this.sprite);
return duration;
} else {
@ -346,11 +344,14 @@ module TK.SpaceTac.UI {
this.effects_messages.removeAll(true);
}
let text = new Phaser.Text(this.game, 0, 20 * this.effects_messages.children.length, message, { font: "14pt SpaceTac", fill: beneficial ? "#afe9c6" : "#e9afaf" });
this.effects_messages.addChild(text);
let builder = new UIBuilder(this.arena.view, this.effects_messages);
let text = builder.text(message, 0, 20 * this.effects_messages.length, {
color: beneficial ? "#afe9c6" : "#e9afaf"
})
let arena = this.battleview.arena.getBoundaries();
this.effects_messages.position.set(
this.effects_messages.setPosition(
(this.ship.arena_x < 100) ? -35 : ((this.ship.arena_x > arena.width - 100) ? (35 - this.effects_messages.width) : (-this.effects_messages.width * 0.5)),
(this.ship.arena_y < arena.height * 0.9) ? 50 : (-50 - this.effects_messages.height)
);
@ -449,7 +450,6 @@ module TK.SpaceTac.UI {
effects.forEach((effect, index) => {
let name = effect.isBeneficial() ? "battle-hud-ship-effect-good" : "battle-hud-ship-effect-bad";
let dot = this.battleview.newImage(name, positions[index] - 35, 0);
dot.anchor.set(0.5, 0.5);
this.active_effects_display.add(dot);
});
}
@ -463,9 +463,8 @@ module TK.SpaceTac.UI {
this.ship.actions.listToggled().forEach(action => {
let color = (action instanceof VigilanceAction) ? 0xf4bf42 : 0xe9f2f9;
this.effects_radius.lineStyle(2, color, 0.5);
this.effects_radius.beginFill(color, 0.1);
this.effects_radius.drawCircle(0, 0, action.radius * 2);
this.effects_radius.endFill();
this.effects_radius.fillStyle(color, 0.1);
this.effects_radius.fillCircle(0, 0, action.radius);
});
}
}

View File

@ -9,44 +9,48 @@ module TK.SpaceTac.UI {
/**
* Create and animate splash component, returns when the animation is ended
*/
private async components(builder: UIBuilder): Promise<void> {
let base = builder.image("battle-splash-message-off", this.view.getMidWidth(), this.view.getMidHeight(), true);
private async components(builder: UIBuilder, container: UIContainer): Promise<void> {
container.setScale(0.8);
let message = builder.in(base).image("battle-splash-message-on", 0, 0, true);
builder.image("battle-splash-message-off", 0, 0, true);
let message = builder.image("battle-splash-message-on", 0, 0, true);
message.visible = false;
let player1 = builder.in(base).image("battle-splash-moving-part", 0, -50, true);
player1.visible = false;
let player1 = builder.container("player1", 0, -50, false);
builder.in(player1, builder => {
builder.image("battle-splash-moving-part", 0, 0, true);
let player1_name = builder.in(player1).text(this.fleet1.name, -224, 0, { size: 22, bold: true, color: "#154d13" });
player1_name.angle = -48;
let player1_name = builder.text(this.fleet1.name, -224, 0, { size: 22, bold: true, color: "#154d13" });
player1_name.angle = -48;
this.fleet1.ships.forEach((ship, index) => {
let ship_card = builder.in(player1).image("battle-splash-ship-card", -86 + index * 96, -72, true);
let ship_portrait = builder.in(ship_card).image(`ship-${ship.model.code}-portrait`, 0, 0, true);
ship_portrait.scale.set(0.3);
this.fleet1.ships.forEach((ship, index) => {
let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true);
let ship_portrait = builder.in(ship_card).image(`ship-${ship.model.code}-portrait`, 0, 0, true);
ship_portrait.setScale(0.3);
});
});
let player2 = builder.in(base).image("battle-splash-moving-part", 0, 50, true);
player2.angle = 180;
player2.visible = false;
let player2 = builder.container("player2", 0, 50, false);
player2.setAngle(180);
builder.in(player2, builder => {
builder.image("battle-splash-moving-part", 0, 0, true);
let player2_name = builder.in(player2).text(this.fleet2.name, -224, 0, { size: 22, bold: true, color: "#651713" });
player2_name.angle = -228;
let player2_name = builder.text(this.fleet2.name, -224, 0, { size: 22, bold: true, color: "#651713" });
player2_name.angle = -228;
this.fleet2.ships.forEach((ship, index) => {
let ship_card = builder.in(player2).image("battle-splash-ship-card", -86 + index * 96, -72, true);
let ship_portrait = builder.in(ship_card).image(`ship-${ship.model.code}-portrait`, 0, 0, true);
ship_portrait.angle = 180;
ship_portrait.scale.set(0.3);
this.fleet2.ships.forEach((ship, index) => {
let ship_card = builder.image("battle-splash-ship-card", -86 + index * 96, -72, true);
let ship_portrait = builder.in(ship_card).image(`ship-${ship.model.code}-portrait`, 0, 0, true);
ship_portrait.setAngle(180);
ship_portrait.setScale(0.3);
});
});
// Animations
let anims = this.view.animations;
base.visible = true;
base.scale.set(0.8);
await anims.addAnimation(base.scale, { x: 1, y: 1 }, 300, Phaser.Easing.Bounce.Out);
await anims.addAnimation(container, { scaleX: 1, scaleY: 1 }, 300, 'Bounce.easeOut');
this.view.timer.schedule(600, () => {
message.visible = true;
@ -61,38 +65,38 @@ module TK.SpaceTac.UI {
player1.visible = true;
player2.x = 2000;
player2.visible = true;
anims.addAnimation(player2, { x: 147 }, 600, Phaser.Easing.Bounce.Out, 400);
await anims.addAnimation(player1, { x: -150 }, 600, Phaser.Easing.Bounce.Out, 400);
anims.addAnimation(player2, { x: 147 }, 600, 'Bounce.easeOut', 400);
await anims.addAnimation(player1, { x: -150 }, 600, 'Bounce.easeOut', 400);
}
/**
* Create an overlay, returns when it is clicked
*/
overlay(builder: UIBuilder): Promise<void> {
overlay(builder: UIBuilder): Promise<UIButton> {
return new Promise(resolve => {
let overlay = builder.button("translucent-black", 0, 0, resolve);
overlay.input.useHandCursor = true;
overlay.scale.set(this.view.getWidth() / overlay.width, this.view.getHeight() / overlay.height);
let overlay = builder.button("battle-overlay", this.view.getMidWidth(), this.view.getMidHeight(), () => resolve(overlay), undefined, undefined, { center: true });
overlay.setScale(this.view.getWidth() / overlay.width, this.view.getHeight() / overlay.height);
});
}
/**
* Start the animation
*/
start(parent?: UIGroup): Promise<void> {
start(parent?: UIContainer): Promise<void> {
let builder = new UIBuilder(this.view, parent);
let group = builder.group("splash");
let overlay = this.overlay(builder);
let overlay = this.overlay(builder.in(group));
let components = this.components(builder.in(group));
let container = builder.container("splash", this.view.getMidWidth(), this.view.getMidHeight());
let components = this.components(builder.in(container), container);
return Promise.all([
overlay.then(() => {
group.visible = false;
overlay.then(overlayobj => {
container.visible = false;
overlayobj.destroy();
}),
components
]).then(() => {
group.destroy(true);
container.destroy();
});
}
}

View File

@ -27,17 +27,17 @@ module TK.SpaceTac.UI {
multi!: MultiBattle
// Layers
layer_background!: Phaser.Group
layer_arena!: Phaser.Group
layer_borders!: Phaser.Group
layer_overlay!: Phaser.Group
layer_sheets!: Phaser.Group
layer_background!: UIContainer
layer_arena!: UIContainer
layer_borders!: UIContainer
layer_overlay!: UIContainer
layer_sheets!: UIContainer
// Battleground container
arena!: Arena
// Background image
background!: Phaser.Image | null
background!: UIImage | null
// Targetting mode (null if we're not in this mode)
targetting!: Targetting
@ -70,12 +70,12 @@ module TK.SpaceTac.UI {
splash = true
// Init the view, binding it to a specific battle
init(player: Player, battle: Battle) {
super.init();
init(data: { player: Player, battle: Battle }) {
super.init(data);
this.player = player;
this.actual_battle = battle;
this.battle = duplicate(battle, <any>TK.SpaceTac);
this.player = data.player;
this.actual_battle = data.battle;
this.battle = duplicate(data.battle, <any>TK.SpaceTac);
this.ship_hovered = null;
this.background = null;
this.multi = new MultiBattle();
@ -113,20 +113,20 @@ module TK.SpaceTac.UI {
// Add UI elements
this.action_bar = new ActionBar(this);
this.action_bar.position.set(0, this.getHeight() - 132);
this.action_bar.setPosition(0, this.getHeight() - 132);
this.ship_list = new ShipList(this, this.battle, this.player, this.toggle_tactical_mode, this,
this.layer_borders, this.getWidth() - 112, 0);
this.ship_list.bindToLog(this.log_processor);
this.ship_tooltip = new ShipTooltip(this);
this.character_sheet = new CharacterSheet(this, CharacterSheetMode.DISPLAY);
this.layer_sheets.add(this.character_sheet);
this.character_sheet.moveToLayer(this.layer_sheets);
// Targetting info
this.targetting = new Targetting(this, this.action_bar, this.toggle_tactical_mode, this.arena.range_hint);
this.targetting.moveToLayer(this.arena.layer_targetting);
// BGM
this.gameui.audio.startMusic("mechanolith", 0.2);
this.audio.startMusic("mechanolith", 0.2);
// Key mapping
this.inputs.bind("t", "Show tactical view", () => this.toggle_tactical_mode.manipulate("keyboard")(3000));
@ -313,7 +313,7 @@ module TK.SpaceTac.UI {
}
if (enabled != this.interacting) {
this.action_bar.setInteractive(enabled);
this.action_bar.setInteractivity(enabled);
this.exitTargettingMode();
this.interacting = enabled;
@ -364,7 +364,7 @@ module TK.SpaceTac.UI {
*/
exitBattle() {
this.session.exitBattle();
this.game.state.start('router');
this.backToRouter();
}
/**
@ -372,7 +372,7 @@ module TK.SpaceTac.UI {
*/
revertBattle() {
this.session.revertBattle();
this.game.state.start('router');
this.backToRouter();
}
}
}

View File

@ -52,7 +52,7 @@ module TK.SpaceTac.UI {
start() {
if (!this.view.gameui.headless) {
this.log.play(async diff => {
while (this.view.game.paused) {
while (this.view.isPaused()) {
await this.view.timer.sleep(500);
}

View File

@ -25,8 +25,11 @@ module TK.SpaceTac.UI {
* Shortcut to add a single action button at the bottom of dialog
*/
addActionButton(x: number, text: string, tooltip: string, action: Function) {
let button = this.addButton(x, 885, action, "common-dialog-textbutton", tooltip);
button.addChild(this.addText(0, 0, text, "#d9e0e5"));
let button = this.content.button("common-dialog-textbutton", x, 885, action, tooltip, undefined, {
center: true,
text: text,
text_style: { color: "#d9e0e5" }
});
}
/**
@ -37,16 +40,16 @@ module TK.SpaceTac.UI {
let outcome = this.outcome;
let victory = outcome.winner && this.player.is(outcome.winner.player);
this.clearContent();
this.content.clear();
this.addImage(747, 180, victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat");
this.content.image(victory ? "battle-outcome-title-victory" : "battle-outcome-title-defeat", 747, 180, true);
this.addText(815, 320, "You", "#ffffff", 20);
this.addText(1015, 320, "Enemy", "#ffffff", 20);
this.content.text("You", 815, 320, { color: "#ffffff", size: 20 });
this.content.text("Enemy", 1015, 320, { color: "#ffffff", size: 20 });
this.stats.getImportant(10).forEach((stat, index) => {
this.addText(530, 364 + 40 * index, stat.name, "#ffffff", 20);
this.addText(815, 364 + 40 * index, stat.attacker.toString(), "#8ba883", 20, true);
this.addText(1015, 364 + 40 * index, stat.defender.toString(), "#cd6767", 20, true);
this.content.text(stat.name, 530, 364 + 40 * index, { color: "#ffffff", size: 20 });
this.content.text(stat.attacker.toString(), 815, 364 + 40 * index, { color: "#8ba883", size: 20, bold: true });
this.content.text(stat.defender.toString(), 1015, 364 + 40 * index, { color: "#cd6767", size: 20, bold: true });
});
if (!this.battleview.session.hasUniverse()) {

View File

@ -7,7 +7,7 @@ module TK.SpaceTac.UI {
private view: BaseView
// Visual information
private info: Phaser.Graphics
private info: UIGraphics
// Size of the arena
private width: number
@ -20,15 +20,14 @@ module TK.SpaceTac.UI {
this.width = boundaries.width;
this.height = boundaries.height;
this.info = new Phaser.Graphics(arena.game, 0, 0);
this.info.visible = false;
this.info = new UIGraphics(arena.view, "info", false);
}
/**
* Set the layer in which the info will be displayed
*/
setLayer(layer: Phaser.Group, x = 0, y = 0) {
this.info.position.set(x, y);
setLayer(layer: UIContainer, x = 0, y = 0) {
this.info.setPosition(x, y);
layer.add(this.info);
}
@ -49,23 +48,23 @@ module TK.SpaceTac.UI {
this.info.clear();
if (radius) {
this.info.beginFill(nocolor);
this.info.drawRect(0, 0, this.width, this.height);
this.info.fillStyle(nocolor);
this.info.fillRect(0, 0, this.width, this.height);
this.info.beginFill(yescolor);
this.info.drawCircle(ship.arena_x, ship.arena_y, radius * 2);
this.info.fillStyle(yescolor);
this.info.fillCircle(ship.arena_x, ship.arena_y, radius);
if (action instanceof MoveAction) {
let exclusions = action.getExclusionAreas(ship);
this.info.beginFill(nocolor);
this.info.drawRect(0, 0, this.width, exclusions.hard_border);
this.info.drawRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border);
this.info.drawRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
this.info.drawRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
this.info.fillStyle(nocolor);
this.info.fillRect(0, 0, this.width, exclusions.hard_border);
this.info.fillRect(0, this.height - exclusions.hard_border, this.width, exclusions.hard_border);
this.info.fillRect(0, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
this.info.fillRect(this.width - exclusions.hard_border, exclusions.hard_border, exclusions.hard_border, this.height - exclusions.hard_border * 2);
exclusions.obstacles.forEach(obstacle => {
this.info.drawCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle * 2);
this.info.fillCircle(obstacle.x, obstacle.y, exclusions.effective_obstacle);
});
}

View File

@ -46,22 +46,22 @@ module TK.SpaceTac.UI.Specs {
list.setShipsFromBattle(battle, false);
check.in("ship added in the other fleet", check => {
check.equals(list.items.length, 2, "item count");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(2, 843), "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 744), "second ship position");
check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 744 }, "second ship position");
});
battle.setPlayingShip(battle.play_order[0]);
list.refresh(false);
check.in("started", check => {
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-14, 962), "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843), "second ship position");
check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position");
});
battle.advanceToNextShip();
list.refresh(false);
check.in("end turn", check => {
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(2, 843), "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(-14, 962), "second ship position");
check.equals(nn(list.findItem(battle.play_order[0])).location, { x: 2, y: 843 }, "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).location, { x: -14, y: 962 }, "second ship position");
});
ship = battle.fleets[1].addShip();
@ -71,9 +71,9 @@ module TK.SpaceTac.UI.Specs {
list.setShipsFromBattle(battle, false);
check.in("third ship added", check => {
check.equals(list.items.length, 3, "item count");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-14, 962), "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843), "second ship position");
check.equals(nn(list.findItem(battle.play_order[2])).position, new Phaser.Point(2, 744), "third ship position");
check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position");
check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position");
check.equals(nn(list.findItem(battle.play_order[2])).location, { x: 2, y: 744 }, "third ship position");
});
let dead = battle.play_order[1];
@ -81,9 +81,9 @@ module TK.SpaceTac.UI.Specs {
list.refresh(false);
check.in("ship dead", check => {
check.equals(list.items.length, 3, "item count");
check.equals(nn(list.findItem(battle.play_order[0])).position, new Phaser.Point(-14, 962), "first ship position");
check.equals(nn(list.findItem(dead)).position, new Phaser.Point(200, 843), "dead ship position");
check.equals(nn(list.findItem(battle.play_order[1])).position, new Phaser.Point(2, 843), "second ship position");
check.equals(nn(list.findItem(battle.play_order[0])).location, { x: -14, y: 962 }, "first ship position");
check.equals(nn(list.findItem(dead)).location, { x: 200, y: 843 }, "dead ship position");
check.equals(nn(list.findItem(battle.play_order[1])).location, { x: 2, y: 843 }, "second ship position");
});
});
});

View File

@ -16,7 +16,7 @@ module TK.SpaceTac.UI {
ship_buttons: IShipButton
// Container
container: Phaser.Image
container: UIContainer
// List of ship items
items: ShipListItem[]
@ -25,14 +25,16 @@ module TK.SpaceTac.UI {
hovered: ShipListItem | null
// Info button
info_button: Phaser.Button
info_button: UIButton
constructor(view: BaseView, battle: Battle, player: Player, tactical_mode: Toggle, ship_buttons: IShipButton, parent?: UIContainer, x = 0, y = 0) {
let builder = new UIBuilder(view, parent);
this.container = builder.image("battle-shiplist-background", x, y);
this.container = builder.container("shiplist", x, y);
builder = builder.in(this.container);
builder.image("battle-shiplist-background", 0, 0);
this.view = view;
// TODO Should use an UI game state, not the actual game state
this.battle = battle;
this.player = player;
this.ship_buttons = ship_buttons;
@ -40,13 +42,8 @@ module TK.SpaceTac.UI {
this.items = [];
this.hovered = null;
let info = view.getImageInfo("battle-shiplist-info-button");
this.info_button = new Phaser.Button(view.game, 0, 0, info.key, undefined, undefined, info.frame, info.frame);
this.view.inputs.setHoverClick(this.info_button,
() => tactical_mode.manipulate("shiplist")(true),
() => tactical_mode.manipulate("shiplist")(false),
() => null);
this.container.addChild(this.info_button);
// FIXME
this.info_button = builder.button("battle-shiplist-info-button", 0, 0, () => null, "Tactical display", on => tactical_mode.manipulate("shiplist")(on));
this.setShipsFromBattle(battle);
}
@ -103,7 +100,7 @@ module TK.SpaceTac.UI {
var owned = ship.isPlayedBy(this.player);
var result = new ShipListItem(this, 200, this.container.height / 2, ship, owned, this.ship_buttons);
this.items.push(result);
this.container.addChild(result);
this.container.add(result);
return result;
}
@ -125,15 +122,16 @@ module TK.SpaceTac.UI {
item.visible = false;
} else {
if (position == 0) {
item.moveTo(-14, 962, animate ? 1000 : 0);
item.moveAt(-14, 962, animate ? 1000 : 0);
} else {
item.moveTo(2, 942 - position * 99, animate ? 1000 : 0);
item.moveAt(2, 942 - position * 99, animate ? 1000 : 0);
}
item.visible = true;
this.container.setChildIndex(item, position);
item.setZ(99 - position);
}
} else {
item.moveTo(200, item.y, animate ? 1000 : 0);
item.setZ(100);
item.moveAt(200, item.y, animate ? 1000 : 0);
}
});
}

View File

@ -1,6 +1,8 @@
module TK.SpaceTac.UI {
// One item in a ship list (used in BattleView)
export class ShipListItem extends Phaser.Button {
/**
* One item in a ship list (used in BattleView)
*/
export class ShipListItem extends UIContainer {
// Reference to the view
view: BaseView
@ -8,43 +10,40 @@ module TK.SpaceTac.UI {
ship: Ship
// Player indicator
player_indicator: Phaser.Image
player_indicator: UIImage
// Portrait
portrait: Phaser.Image
portrait: UIImage
// Damage flashing indicator
damage_indicator: Phaser.Image
damage_indicator: UIImage
// Hover indicator
hover_indicator: Phaser.Image
hover_indicator: UIImage
// Create a ship button for the battle ship list
constructor(list: ShipList, x: number, y: number, ship: Ship, owned: boolean, ship_buttons: IShipButton) {
let info = list.view.getImageInfo("battle-shiplist-item-background");
super(list.view.game, x, y, info.key, undefined, undefined, info.frame, info.frame);
// TODO Make it an UIButton
super(list.view, x, y);
this.view = list.view;
this.ship = ship;
this.player_indicator = this.view.newImage(owned ? "battle-hud-ship-own-mini" : "battle-hud-ship-enemy-mini", 102, 52);
this.player_indicator.anchor.set(0.5);
this.player_indicator.angle = -90;
this.addChild(this.player_indicator);
let builder = new UIBuilder(list.view, this);
this.portrait = this.view.newImage(`ship-${ship.model.code}-sprite`, 52, 52);
this.portrait.anchor.set(0.5);
this.portrait.scale.set(0.8);
this.portrait.angle = 180;
this.addChild(this.portrait);
builder.image("battle-shiplist-item-background");
this.damage_indicator = this.view.newImage("battle-shiplist-damage", 8, 9);
this.damage_indicator.alpha = 0;
this.addChild(this.damage_indicator);
this.player_indicator = builder.image(owned ? "battle-hud-ship-own-mini" : "battle-hud-ship-enemy-mini", 102, 52, true);
this.player_indicator.setAngle(-90);
this.hover_indicator = this.view.newImage("battle-shiplist-hover", 7, 8);
this.portrait = builder.image(`ship-${ship.model.code}-sprite`, 52, 52, true);
this.portrait.setScale(0.8)
this.portrait.setAngle(180);
this.damage_indicator = builder.image("battle-shiplist-damage", 8, 9);
this.damage_indicator.visible = false;
this.hover_indicator = builder.image("battle-shiplist-hover", 7, 8);
this.hover_indicator.visible = false;
this.addChild(this.hover_indicator);
this.view.inputs.setHoverClick(this,
() => ship_buttons.cursorOnShip(ship),
@ -53,22 +52,38 @@ module TK.SpaceTac.UI {
);
}
// Flash a damage indicator
setDamageHit() {
this.game.tweens.create(this.damage_indicator).to({ alpha: 1 }, 100).to({ alpha: 0 }, 150).repeatAll(2).start();
get location(): { x: number, y: number } {
return { x: this.x, y: this.y };
}
// Move to a given location on screen
moveTo(x: number, y: number, duration: number) {
/**
* Flash a damage indicator
*/
setDamageHit() {
this.view.tweens.add({
targets: this.damage_indicator,
duration: 100,
alpha: 1,
repeat: 2,
yoyo: true
});
}
/**
* Move to a given location on screen
*/
moveAt(x: number, y: number, duration: number) {
if (duration && (this.x != x || this.y != y)) {
this.view.animations.addAnimation(this, { x: x, y: y }, duration);
this.view.animations.addAnimation<UIContainer>(this, { x: x, y: y }, duration);
} else {
this.x = x;
this.y = y;
}
}
// Set the hovered status
/**
* Set the hovered status
*/
setHovered(hovered: boolean) {
this.view.animations.setVisible(this.hover_indicator, hovered, 200);
}

View File

@ -26,7 +26,7 @@ module TK.SpaceTac.UI {
let portrait_bg = builder.image("battle-tooltip-ship-portrait", 0, 0);
builder.in(portrait_bg, builder => {
let portrait = builder.image(`ship-${ship.model.code}-portrait`, 1, 1);
portrait.scale.set(0.75);
portrait.setScale(0.75);
});
let enemy = !this.battleview.player.is(ship.fleet.player);
@ -46,7 +46,7 @@ module TK.SpaceTac.UI {
ship.actions.listAll().forEach(action => {
if (!(action instanceof EndTurnAction) && !(action instanceof MoveAction)) {
let icon = builder.image(`action-${action.code}`, 0, iy);
icon.scale.set(0.15);
icon.setScale(0.15);
builder.text(action.name, 46, iy + 8);
iy += 40;
}
@ -67,7 +67,7 @@ module TK.SpaceTac.UI {
let sprite = this.battleview.arena.findShipSprite(ship);
if (sprite) {
this.container.show(sprite.frame_owner.getBounds());
this.container.show(UITools.getBounds(sprite.frame_owner));
}
}

View File

@ -60,7 +60,7 @@ module TK.SpaceTac.UI.Specs {
check.called(collect, [
[ship, new Target(20, 10), ship.location]
])
check.equals(targetting.impact_indicators.children.length, 3);
check.equals(targetting.impact_indicators.length, 3);
check.equals(targetting.impact_indicators.visible, true);
targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 11));
@ -68,7 +68,7 @@ module TK.SpaceTac.UI.Specs {
check.called(collect, [
[ship, new Target(20, 11), ship.location]
])
check.equals(targetting.impact_indicators.children.length, 2);
check.equals(targetting.impact_indicators.length, 2);
check.equals(targetting.impact_indicators.visible, true);
targetting.updateImpactIndicators(impacts, ship, action, new Target(20, 12));
@ -109,12 +109,15 @@ module TK.SpaceTac.UI.Specs {
check.equals(targetting.container.visible, true);
check.equals(targetting.drawn_info.visible, true);
check.equals(targetting.fire_arrow.visible, true);
check.containing(targetting.fire_arrow.position, { x: 156, y: 65 });
check.equals(targetting.fire_arrow.x, 156);
check.equals(targetting.fire_arrow.y, 65);
check.nears(targetting.fire_arrow.rotation, 0.534594, 5);
check.equals(targetting.impact_area.visible, true);
check.containing(targetting.impact_area.position, { x: 156, y: 65 });
check.equals(targetting.impact_area.x, 156);
check.equals(targetting.impact_area.y, 65);
check.equals(targetting.move_ghost.visible, true);
check.containing(targetting.move_ghost.position, { x: 80, y: 20 });
check.equals(targetting.move_ghost.x, 80);
check.equals(targetting.move_ghost.y, 20);
check.nears(targetting.move_ghost.rotation, 0.534594, 5);
})

View File

@ -6,7 +6,7 @@ module TK.SpaceTac.UI {
*/
export class Targetting {
// Container group
container: Phaser.Group
container: UIContainer
// Current action
ship: Ship | null = null
@ -19,13 +19,13 @@ module TK.SpaceTac.UI {
effects: BaseBattleDiff[] = []
// Move and fire lines
drawn_info: Phaser.Graphics
move_ghost: Phaser.Image
fire_arrow: Phaser.Image
drawn_info: UIGraphics
move_ghost: UIImage
fire_arrow: UIImage
// Impact area
impact_area: Phaser.Graphics
impact_indicators: Phaser.Group
impact_area: UIGraphics
impact_indicators: UIContainer
// Collaborators to update
actionbar: ActionBar
@ -41,34 +41,29 @@ module TK.SpaceTac.UI {
this.tactical_mode = tactical_mode.manipulate("targetting");
this.range_hint = range_hint;
this.container = view.add.group();
let builder = new UIBuilder(view);
this.container = builder.container("targetting");
builder = builder.in(this.container);
// Visual effects
this.drawn_info = new Phaser.Graphics(view.game, 0, 0);
this.drawn_info.visible = false;
this.move_ghost = view.newImage("common-transparent");
this.move_ghost.anchor.set(0.5, 0.5);
this.move_ghost.alpha = 0.8;
this.move_ghost.visible = false;
this.fire_arrow = this.view.newImage("battle-hud-simulator-ok");
this.fire_arrow.anchor.set(1, 0.5);
this.fire_arrow.visible = false;
this.impact_indicators = new Phaser.Group(view.game);
this.impact_indicators.visible = false;
this.impact_area = new Phaser.Graphics(view.game);
this.impact_area.visible = false;
this.container.add(this.impact_area);
this.container.add(this.drawn_info);
this.container.add(this.move_ghost);
this.container.add(this.fire_arrow);
this.container.add(this.impact_indicators);
this.impact_area = builder.graphics("impact-area");
this.impact_area.setVisible(false);
this.drawn_info = builder.graphics("lines");
this.drawn_info.setVisible(false);
this.move_ghost = builder.image("common-transparent", 0, 0, true);
this.move_ghost.setAlpha(0.8);
this.move_ghost.setVisible(false);
this.fire_arrow = builder.image("battle-hud-simulator-ok");
this.fire_arrow.setOrigin(1, 0.5);
this.fire_arrow.setVisible(false);
this.impact_indicators = builder.container("impact-indicators");
this.impact_indicators.setVisible(false);
}
/**
* Move to a given view layer
*/
moveToLayer(layer: Phaser.Group): void {
moveToLayer(layer: UIContainer): void {
layer.add(this.container);
}
@ -85,11 +80,16 @@ module TK.SpaceTac.UI {
drawVector(color: number, x1: number, y1: number, x2: number, y2: number, gradation = 0) {
let line = this.drawn_info;
line.lineStyle(6, color);
line.beginPath();
line.moveTo(x1, y1);
line.lineTo(x2, y2);
line.strokePath();
line.beginPath();
line.lineStyle(2, 0x000000, 0.6);
line.moveTo(x1, y1);
line.lineTo(x2, y2);
line.closePath();
line.strokePath();
line.visible = true;
if (gradation) {
@ -125,7 +125,7 @@ module TK.SpaceTac.UI {
/**
* Update impact indicators (highlighting impacted ships, with success factor)
*/
updateImpactIndicators(impacts: Phaser.Group, ship: Ship, action: BaseAction, target: Target, source: IArenaLocation = ship.location): void {
updateImpactIndicators(impacts: UIContainer, ship: Ship, action: BaseAction, target: Target, source: IArenaLocation = ship.location): void {
let ships = action.getImpactedShips(ship, target, source);
if (ships.length) {
// TODO differential
@ -143,7 +143,7 @@ module TK.SpaceTac.UI {
/**
* Update impact graphics (area display)
*/
updateImpactArea(area: Phaser.Graphics, action: BaseAction): void {
updateImpactArea(area: UIGraphics, action: BaseAction): void {
area.clear();
let color = 0;
@ -168,24 +168,20 @@ module TK.SpaceTac.UI {
if (radius) {
if (angle) {
area.lineStyle(2, color, 0.6);
area.beginFill(color, 0.2);
area.fillStyle(color, 0.2);
area.arc(0, 0, radius, angle, -angle, true);
area.endFill();
area.lineStyle(1, color, 0.3);
area.beginFill(color, 0.1);
area.fillStyle(color, 0.1);
area.arc(0, 0, radius * 0.95, angle * 0.95, -angle * 0.95, true);
area.endFill();
} else {
area.lineStyle(2, color, 0.6);
area.beginFill(color, 0.2);
area.drawCircle(0, 0, radius * 2);
area.endFill();
area.fillStyle(color, 0.2);
area.fillCircle(0, 0, radius);
area.lineStyle(1, color, 0.3);
area.beginFill(color, 0.1);
area.drawCircle(0, 0, radius * 2 * 0.95);
area.endFill();
area.fillStyle(color, 0.1);
area.fillCircle(0, 0, radius * 0.95);
}
}
}
@ -214,7 +210,7 @@ module TK.SpaceTac.UI {
if (simulation.need_move) {
this.move_ghost.visible = true;
this.move_ghost.position.set(simulation.move_location.x, simulation.move_location.y);
this.move_ghost.setPosition(simulation.move_location.x, simulation.move_location.y);
this.move_ghost.rotation = angle;
} else {
this.move_ghost.visible = false;
@ -222,18 +218,18 @@ module TK.SpaceTac.UI {
if (simulation.need_fire) {
if (this.action instanceof TriggerAction && this.action.angle) {
this.impact_area.position.set(simulation.move_location.x, simulation.move_location.y);
this.impact_area.rotation = arenaAngle(simulation.move_location, simulation.fire_location);
this.impact_area.setPosition(simulation.move_location.x, simulation.move_location.y);
this.impact_area.setRotation(arenaAngle(simulation.move_location, simulation.fire_location));
} else {
this.impact_area.position.set(this.target.x, this.target.y);
this.impact_area.setPosition(this.target.x, this.target.y);
}
this.impact_area.alpha = simulation.can_fire ? 1 : 0.5;
this.impact_area.visible = true;
this.updateImpactIndicators(this.impact_indicators, this.ship, this.action, this.target, this.simulation.move_location);
this.fire_arrow.position.set(this.target.x, this.target.y);
this.fire_arrow.rotation = angle;
this.fire_arrow.setPosition(this.target.x, this.target.y);
this.fire_arrow.setRotation(angle);
this.view.changeImage(this.fire_arrow, simulation.complete ? "battle-hud-simulator-ok" : "battle-hud-simulator-power");
this.fire_arrow.visible = true;
} else {
@ -243,8 +239,8 @@ module TK.SpaceTac.UI {
}
} else {
this.drawVector(0x888888, this.ship.arena_x, this.ship.arena_y, this.target.x, this.target.y);
this.fire_arrow.position.set(this.target.x, this.target.y);
this.fire_arrow.rotation = angle;
this.fire_arrow.setPosition(this.target.x, this.target.y);
this.fire_arrow.setRotation(angle);
this.view.changeImage(this.fire_arrow, "battle-hud-simulator-failed");
this.fire_arrow.visible = true;
this.impact_area.visible = false;

View File

@ -2,15 +2,17 @@ module TK.SpaceTac.UI.Specs {
testing("WeaponEffect", test => {
let testgame = setupBattleview(test);
let clock = test.clock();
let t = 0;
function checkEmitters(step: string, expected: number) {
test.check.same(testgame.view.arena.layer_weapon_effects.children.length, expected, `${step} - layer children`);
test.check.same(keys(testgame.view.game.particles.emitters).length, expected, `${step} - registered emitters`);
test.check.same(testgame.view.arena.layer_weapon_effects.length, expected, `${step} - layer children`);
//test.check.same(keys(testgame.view.particles.emitters).length, expected, `${step} - registered emitters`);
}
function fastForward(milliseconds: number) {
t += milliseconds;
clock.forward(milliseconds);
testgame.ui.updateLogic(milliseconds);
testgame.ui.headlessStep(t, milliseconds);
}
test.case("displays shield hit effect", check => {
@ -18,16 +20,22 @@ module TK.SpaceTac.UI.Specs {
battleview.timer = new Timer();
let effect = new WeaponEffect(battleview.arena, new Ship(), new Target(0, 0), new TriggerAction("weapon"));
effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 1000, 3000, true);
effect.shieldImpactEffect({ x: 10, y: 10 }, { x: 20, y: 15 }, 500, 3000, true);
let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.children.length, 2);
check.equals(layer.length, 1);
check.equals(layer.children[0] instanceof Phaser.Image, true);
check.nears(layer.children[0].rotation, -2.677945044588987, 10);
check.containing(layer.children[0].position, { x: 20, y: 15 });
clock.forward(600);
check.equals(layer.length, 2);
check.equals(layer.children[1] instanceof Phaser.Particles.Arcade.Emitter, true);
let child = layer.list[0];
if (check.instance(child, UIImage, "first child is an image")) {
check.nears(child.rotation, -2.677945044588987, 10);
check.equals(child.x, 20, "x");
check.equals(child.y, 15, "y");
}
check.instance(layer.list[1], Phaser.GameObjects.Particles.ParticleEmitterManager, "second child is an emitter");
});
test.case("displays gatling gun effect", check => {
@ -39,8 +47,8 @@ module TK.SpaceTac.UI.Specs {
effect.gunEffect();
let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.children.length, 1);
check.equals(layer.children[0] instanceof Phaser.Particles.Arcade.Emitter, true);
check.equals(layer.length, 1);
check.instance(layer.list[0], Phaser.GameObjects.Particles.ParticleEmitterManager, "first child is an emitter");
});
test.case("displays shield and hull effect on impacted ships", check => {
@ -102,16 +110,17 @@ module TK.SpaceTac.UI.Specs {
check.equals(result, 200);
let layer = battleview.arena.layer_weapon_effects;
check.equals(layer.children.length, 1);
check.same(layer.children[0] instanceof Phaser.Image, true, "is image");
let image = <Phaser.Image>layer.children[0];
check.equals(image.name, "battle-effects-laser");
//check.equals(image.width, 300);
check.equals(image.x, 20);
check.equals(image.y, 30);
check.nears(image.rotation, Math.PI / 4);
check.equals(layer.length, 1);
let image = layer.list[0];
if (check.instance(image, UIImage, "first child is an image")) {
check.equals(image.name, "battle-effects-laser");
//check.equals(image.width, 300);
check.equals(image.x, 20);
check.equals(image.y, 30);
check.nears(image.rotation, Math.PI / 4);
}
let values = battleview.animations.simulate(image, "rotation", 4, result);
let values = battleview.animations.simulate(image, "rotation", 4);
check.nears(values[0], Math.PI / 4);
check.nears(values[1], 0);
check.nears(values[2], -Math.PI / 4);

View File

@ -1,12 +1,4 @@
module TK.SpaceTac.UI {
// Particle that is rotated to always face its ongoing direction
class BulletParticle extends Phaser.Particle {
update(): void {
super.update();
this.rotation = Math.atan2(this.body.velocity.y, this.body.velocity.x);
}
}
/**
* Visual effects renderer for weapons.
*/
@ -22,7 +14,10 @@ module TK.SpaceTac.UI {
private timer: Timer
// Display group in which to display the visual effects
private layer: Phaser.Group
private layer: UIContainer
// Builder for images
private builder: UIBuilder
// Firing ship
private ship: Ship
@ -41,6 +36,7 @@ module TK.SpaceTac.UI {
this.view = arena.view;
this.timer = arena.view.timer;
this.layer = arena.layer_weapon_effects;
this.builder = new UIBuilder(arena.view, this.layer);
this.ship = ship;
this.target = target;
this.action = action;
@ -116,32 +112,28 @@ module TK.SpaceTac.UI {
shieldImpactEffect(from: IArenaLocation, ship: IArenaLocation, delay: number, duration: number, particles = false) {
let angle = Math.atan2(from.y - ship.y, from.x - ship.x);
let effect = this.view.newImage("battle-effects-shield-impact", ship.x, ship.y);
effect.alpha = 0;
effect.rotation = angle;
effect.anchor.set(0.5, 0.5);
this.layer.add(effect);
let effect = this.builder.image("battle-effects-shield-impact", ship.x, ship.y, true);
effect.setAlpha(0);
effect.setRotation(angle);
let tween1 = this.ui.add.tween(effect).to({ alpha: 1 }, 100).delay(delay);
let tween2 = this.ui.add.tween(effect).to({ alpha: 0 }, 100).delay(duration);
tween1.chain(tween2);
tween2.onComplete.addOnce(() => effect.destroy());
tween1.start();
let tween1 = this.view.animations.addAnimation(effect, { alpha: 1 }, 100, undefined, delay);
let tween2 = this.view.animations.addAnimation(effect, { alpha: 0 }, 100, undefined, delay + duration);
tween2.then(() => effect.destroy());
if (particles) {
let image = this.view.getImageInfo("battle-effects-hot");
let emitter = this.ui.add.emitter(ship.x + Math.cos(angle) * 35, ship.y + Math.sin(angle) * 35, 30);
emitter.minParticleScale = 0.7;
emitter.maxParticleScale = 1.2;
emitter.gravity = 0;
emitter.makeParticles(image.key, image.frame);
emitter.setSize(10, 10);
emitter.setRotation(0, 0);
emitter.setXSpeed(-Math.cos(angle) * 20, -Math.cos(angle) * 80);
emitter.setYSpeed(-Math.sin(angle) * 20, -Math.sin(angle) * 80);
this.timer.schedule(delay, () => emitter.start(false, 200, 30, duration * 0.8 / 30));
this.layer.add(emitter);
this.timer.schedule(delay + duration + 5000, () => emitter.destroy());
this.timer.schedule(delay, () => {
this.builder.particles({
key: "battle-effects-hot",
source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 10 },
emitDuration: 500,
count: 50,
lifetime: 400,
fading: true,
direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 },
scale: { min: 0.7, max: 1.2 },
speed: { min: 20, max: 80 }
});
});
}
}
@ -151,19 +143,17 @@ module TK.SpaceTac.UI {
hullImpactEffect(from: IArenaLocation, ship: IArenaLocation, delay: number, duration: number) {
let angle = Math.atan2(from.y - ship.y, from.x - ship.x);
let image = this.view.getImageInfo("battle-effects-hot");
let emitter = this.ui.add.emitter(ship.x + Math.cos(angle) * 10, ship.y + Math.sin(angle) * 10, 30);
emitter.minParticleScale = 1.0;
emitter.maxParticleScale = 2.0;
emitter.gravity = 0;
emitter.makeParticles(image.key, image.frame);
emitter.setSize(15, 15);
emitter.setRotation(0, 0);
emitter.setXSpeed(-Math.cos(angle) * 120, -Math.cos(angle) * 260);
emitter.setYSpeed(-Math.sin(angle) * 120, -Math.sin(angle) * 260);
this.timer.schedule(delay, () => emitter.start(false, 200, 30, duration * 0.8 / 30));
this.layer.add(emitter);
this.timer.schedule(delay + duration + 5000, () => emitter.destroy());
this.builder.particles({
key: "battle-effects-hot",
source: { x: ship.x + Math.cos(angle) * 40, y: ship.y + Math.sin(angle) * 40, radius: 7 },
emitDuration: 500,
count: 50,
lifetime: 400,
fading: true,
direction: { minangle: Math.PI + angle - 0.3, maxangle: Math.PI + angle + 0.3 },
scale: { min: 1, max: 2 },
speed: { min: 120, max: 260 }
});
}
/**
@ -172,34 +162,26 @@ module TK.SpaceTac.UI {
defaultEffect(): number {
this.ui.audio.playOnce("battle-weapon-missile-launch");
let missile = this.view.newImage("battle-effects-default", this.source.x, this.source.y);
missile.anchor.set(0.5, 0.5);
missile.rotation = arenaAngle(this.source, this.destination);
this.layer.add(missile);
let missile = this.builder.image("battle-effects-default", this.source.x, this.source.y, true);
missile.setRotation(arenaAngle(this.source, this.destination));
let blast_radius = this.action.blast;
let projectile_duration = arenaDistance(this.source, this.destination) * 1.5;
let tween = this.ui.tweens.create(missile);
tween.to({ x: this.destination.x, y: this.destination.y }, projectile_duration || 1);
tween.onComplete.addOnce(() => {
this.view.animations.addAnimation(missile, { x: this.destination.x, y: this.destination.y }, projectile_duration || 1).then(() => {
missile.destroy();
if (blast_radius > 0) {
this.ui.audio.playOnce("battle-weapon-missile-explosion");
let blast = this.view.newImage("battle-effects-blast", this.destination.x, this.destination.y);
let blast = this.builder.image("battle-effects-blast", this.destination.x, this.destination.y, true);
let scaling = blast_radius * 2 / (blast.width * 0.9);
blast.anchor.set(0.5, 0.5);
blast.scale.set(0.001, 0.001);
let tween1 = this.ui.tweens.create(blast.scale).to({ x: scaling, y: scaling }, 1500, Phaser.Easing.Quintic.Out);
tween1.onComplete.addOnce(() => blast.destroy());
tween1.start();
let tween2 = this.ui.tweens.create(blast).to({ alpha: 0 }, 1450, Phaser.Easing.Quadratic.In);
tween2.start();
this.layer.add(blast);
blast.setScale(0.001);
Promise.all([
this.view.animations.addAnimation(blast, { alpha: 0 }, 1450, "Quad.easeIn"),
this.view.animations.addAnimation(blast, { scaleX: scaling, scaleY: scaling }, 1500, "Quint.easeOut"),
]).then(() => blast.destroy());
}
});
tween.start();
return projectile_duration + (blast_radius ? 1500 : 0);
}
@ -212,15 +194,11 @@ module TK.SpaceTac.UI {
this.view.audio.playOnce("battle-weapon-laser");
let laser = this.view.newImage("battle-effects-laser", source.x, source.y);
laser.anchor.set(0, 0.5);
laser.rotation = start_angle;
laser.scale.set(radius / laser.width);
this.layer.add(laser);
let tween = this.view.tweens.create(laser).to({ rotation: end_angle }, duration);
tween.onComplete.addOnce(() => laser.destroy());
tween.start();
let laser = this.builder.image("battle-effects-laser", source.x, source.y);
laser.setOrigin(0, 0.5);
laser.setRotation(start_angle);
laser.setScale(radius / laser.width);
this.view.animations.addAnimation(laser, { rotation: end_angle }, duration).then(() => laser.destroy());
return duration;
}
@ -236,25 +214,26 @@ module TK.SpaceTac.UI {
let angle = arenaAngle(this.source, this.target);
let distance = arenaDistance(this.source, this.target);
let image = this.view.getImageInfo("battle-effects-bullets");
let emitter = this.ui.add.emitter(this.source.x + Math.cos(angle) * 35, this.source.y + Math.sin(angle) * 35, 10);
let speed = 2000;
emitter.particleClass = BulletParticle;
emitter.gravity = 0;
emitter.setSize(5, 5);
emitter.setRotation(0, 0);
emitter.setXSpeed(Math.cos(angle) * speed, Math.cos(angle) * speed);
emitter.setYSpeed(Math.sin(angle) * speed, Math.sin(angle) * speed);
emitter.makeParticles(image.key, image.frame);
let guard = 50 + (has_shield ? 80 : 40);
let guard = 35 + (has_shield ? 80 : 40);
if (guard + 1 > distance) {
guard = distance - 1;
}
emitter.start(false, 1000 * (distance - guard) / speed, 50, 10);
this.layer.add(emitter);
this.timer.schedule(5000, () => emitter.destroy());
let speed = 2000;
let duration = 500;
let lifetime = 1000 * (distance - guard) / speed;
this.builder.particles({
key: "battle-effects-bullets",
source: { x: this.source.x + Math.cos(angle) * 35, y: this.source.y + Math.sin(angle) * 35, radius: 3 },
emitDuration: duration,
count: 50,
lifetime: lifetime,
direction: { minangle: angle, maxangle: angle },
scale: { min: 1, max: 1 },
speed: { min: speed, max: speed },
facing: ParticleFacingMode.ALWAYS
});
return 1000;
return lifetime;
}
}
}

View File

@ -10,12 +10,11 @@ module TK.SpaceTac.UI {
* Draw the portrait (anchored at the center)
*/
draw(builder: UIBuilder, x: number, y: number, onselect: () => void): UIButton {
let button = builder.button("character-portrait", x, y, onselect, this.ship.getName(), identity);
button.anchor.set(0.5);
let button = builder.button("character-portrait", x, y, onselect, this.ship.getName(), identity, { center: true });
builder.in(button, builder => {
let portrait = builder.image(`ship-${this.ship.model.code}-portrait`, 0, 0, true);
portrait.scale.set(0.5);
portrait.setScale(0.5);
});
return button;

View File

@ -9,7 +9,7 @@ module TK.SpaceTac.UI.Specs {
check.patch(view, "getWidth", () => 1240);
let sheet = new CharacterSheet(view, CharacterSheetMode.DISPLAY);
check.equals(sheet.x, -1240);
check.equals(sheet.container.x, -1240);
let fleet = new Fleet();
let ship1 = fleet.addShip();
@ -19,13 +19,13 @@ module TK.SpaceTac.UI.Specs {
sheet.show(ship1, false);
check.equals(sheet.x, 0);
check.equals(sheet.container.x, 0);
check.equals(sheet.group_portraits.length, 2);
check.equals(sheet.text_name && sheet.text_name.text, "Ship 1");
let portrait = as(Phaser.Button, sheet.group_portraits.getChildAt(1));
portrait.onInputUp.dispatch();
let portrait = as(UIButton, sheet.group_portraits.getAt(1));
portrait.emit("pointerup");
check.equals(sheet.text_name && sheet.text_name.text, "Ship 2");
});

View File

@ -8,7 +8,7 @@ module TK.SpaceTac.UI {
/**
* Character sheet, displaying ship characteristics
*/
export class CharacterSheet extends Phaser.Image {
export class CharacterSheet {
// Global sheet mode
mode: CharacterSheetMode
@ -16,6 +16,7 @@ module TK.SpaceTac.UI {
view: BaseView
// UI components builder
container: UIContainer
builder: UIBuilder
// Close/validate button
@ -26,11 +27,11 @@ module TK.SpaceTac.UI {
xhidden = -2000
// Groups
group_level: Phaser.Group
group_portraits: Phaser.Group
group_attributes: Phaser.Image
group_actions: Phaser.Image
group_upgrades: Phaser.Group
group_level: UIContainer
group_portraits: UIContainer
group_attributes: UIContainer
group_actions: UIContainer
group_upgrades: UIContainer
// Currently displayed fleet
fleet?: Fleet
@ -40,34 +41,40 @@ module TK.SpaceTac.UI {
// Variable data
personality?: CharacterPersonality
image_portrait: Phaser.Image
text_model: Phaser.Text
text_description: Phaser.Text
text_name?: Phaser.Text
text_level: Phaser.Text
text_upgrade_points: Phaser.Text
image_portrait: UIImage
text_model: UIText
text_description: UIText
text_name?: UIText
text_level: UIText
text_upgrade_points: UIText
valuebar_experience: ValueBar
constructor(view: BaseView, mode: CharacterSheetMode, onclose?: Function) {
super(view.game, 0, 0, view.getImageInfo("character-sheet").key, view.getImageInfo("character-sheet").frame);
this.view = view;
this.mode = mode;
let builder = new UIBuilder(view);
this.container = builder.container("character-sheet");
builder = builder.in(this.container);
let bg = builder.image("character-sheet");
bg.setInteractive();
this.builder = builder.styled({ color: "#dce9f9", size: 16, shadow: true });
if (!onclose) {
onclose = () => this.hide();
}
this.view = view;
this.mode = mode;
this.builder = new UIBuilder(view, this).styled({ color: "#dce9f9", size: 16, shadow: true });
this.xhidden = -this.view.getWidth();
this.x = this.xhidden;
this.inputEnabled = true;
this.container.x = this.xhidden;
this.image_portrait = this.builder.image("translucent", 435, 271, true);
this.builder.image("character-entry", 24, 740);
this.group_portraits = this.builder.group("portraits", 90, 755);
this.group_portraits = this.builder.container("portraits", 90, 755);
let model_bg = this.builder.image("character-ship-model", 434, 500, true);
this.text_model = this.builder.in(model_bg).text("", 0, 0, { size: 28 });
@ -75,10 +82,10 @@ module TK.SpaceTac.UI {
let description_bg = this.builder.image("character-ship-description", 434, 654, true);
this.text_description = this.builder.in(description_bg).text("", 0, 0, { color: "#a0afc3", width: 510 });
this.group_attributes = this.builder.image("character-ship-column-left", 28, 28);
this.group_actions = this.builder.image("character-ship-column-right", 698, 28);
this.group_attributes = this.builder.container("attributes", 28, 28);
this.group_actions = this.builder.container("actions", 698, 28);
this.group_level = this.builder.group("level");
this.group_level = this.builder.container("level");
let points_bg = this.builder.in(this.group_level).image("character-level-upgrades", 582, 986);
this.builder.in(points_bg, builder => {
builder.text("Upgrade points", 46, 10, { center: false, vcenter: false });
@ -90,7 +97,7 @@ module TK.SpaceTac.UI {
this.text_level = this.builder.in(level_bg).text("", 0, 4, { size: 28 });
this.valuebar_experience = this.builder.in(level_bg).valuebar("character-level-experience", -level_bg.width * 0.5, -level_bg.height * 0.5);
this.group_upgrades = this.builder.group("upgrades");
this.group_upgrades = this.builder.container("upgrades");
if (this.mode == CharacterSheetMode.CREATION) {
this.builder.in(this.builder.image("character-section-title", 180, 30, false)).text("Ship", 80, 45, { color: "#dce9f9", size: 32 });
@ -120,8 +127,7 @@ module TK.SpaceTac.UI {
} else {
this.text_name = this.builder.in(this.builder.image("character-name-display", 434, 940, true)).text("", 0, 0, { size: 28 });
this.close_button = this.builder.button("character-close-button", 1920, 0, onclose, "Close the character sheet");
this.close_button.anchor.set(1, 0);
this.close_button = this.builder.button("character-close-button", 1837, 0, onclose, "Close the character sheet");
}
this.refreshUpgrades();
@ -129,6 +135,13 @@ module TK.SpaceTac.UI {
this.refreshActions();
}
/**
* Move the sheet to a specific layer
*/
moveToLayer(layer: UIContainer): void {
layer.add(this.container);
}
/**
* Check if the sheet should be interactive
*/
@ -194,7 +207,7 @@ module TK.SpaceTac.UI {
builder.styled({ center: false, vcenter: false }).in(initial, builder => {
builder.text("Base equipment", 32, 8, { color: "#e2e9d1" });
builder.in(builder.group("attributes"), builder => {
builder.in(builder.container("attributes"), builder => {
let effects = cfilter(ship.model.getEffects(1, []), AttributeEffect);
effects.forEach(effect => {
let button = builder.button(`attribute-${effect.attrcode}`, 0, 8, undefined,
@ -207,14 +220,14 @@ module TK.SpaceTac.UI {
builder.distribute("x", 236, 870);
});
builder.in(builder.group("actions"), builder => {
builder.in(builder.container("actions"), builder => {
let actions = ship.model.getActions(1, []);
actions.forEach(action => {
let button = builder.button("translucent", 0, 66, undefined, action.getEffectsDescription());
builder.in(button, builder => {
let icon = builder.image(`action-${action.code}`);
icon.scale.set(0.1875);
icon.setScale(0.1875);
if (actions.length < 5) {
builder.text(`${action.name}`, 56, 12, { size: 16 });
}
@ -259,11 +272,13 @@ module TK.SpaceTac.UI {
let builder = this.builder.in(this.group_attributes);
builder.clear();
builder.image("character-ship-column-left", 0, 0);
builder.text("Attributes", 74, 20, { color: "#a3bbd9" });
if (this.ship) {
let ship = this.ship;
builder.in(builder.group("items"), builder => {
builder.in(builder.container("items"), builder => {
keys(SHIP_ATTRIBUTES).forEach(attribute => {
let button = builder.button(`attribute-${attribute}`, 24, 0, undefined,
ship.getAttributeDescription(attribute));
@ -282,16 +297,18 @@ module TK.SpaceTac.UI {
let builder = this.builder.in(this.group_actions);
builder.clear();
builder.image("character-ship-column-right", 0, 0);
builder.text("Actions", 74, 20, { color: "#a3bbd9" });
if (this.ship) {
let ship = this.ship;
builder.in(builder.group("items"), builder => {
builder.in(builder.container("items"), builder => {
let actions = ship.actions.listAll().filter(action => !(action instanceof EndTurnAction));
actions.forEach(action => {
let button = builder.button(`action-${action.code}`, 24, 0, undefined,
action.getEffectsDescription());
button.scale.set(0.375);
button.setScale(0.375);
});
builder.distribute("y", 40, 688);
});
@ -310,7 +327,7 @@ module TK.SpaceTac.UI {
let button: UIButton;
button = new CharacterPortrait(ship).draw(builder, 64 + idx * 140, 64, () => {
if (button) {
builder.select(button);
button.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN);
this.ship = ship;
this.refreshShipInfo();
@ -321,7 +338,7 @@ module TK.SpaceTac.UI {
});
if (ship == this.ship) {
builder.switch(button, true);
button.toggle(true);
}
});
}
@ -331,7 +348,7 @@ module TK.SpaceTac.UI {
* Check if the sheet is shown
*/
isOpened(): boolean {
return this.x != this.xhidden;
return this.container.x != this.xhidden;
}
/**
@ -355,9 +372,14 @@ module TK.SpaceTac.UI {
}
if (animate) {
this.game.tweens.create(this).to({ x: this.xshown }, 400, Phaser.Easing.Circular.InOut, true);
this.view.tweens.add({
targets: this.container,
x: this.xshown,
duration: 400,
easing: 'Circ.easeInOut'
});
} else {
this.x = this.xshown;
this.container.x = this.xshown;
}
}
@ -368,9 +390,14 @@ module TK.SpaceTac.UI {
this.view.audio.playOnce("ui-dialog-close");
if (animate) {
this.game.tweens.create(this).to({ x: this.xhidden }, 400, Phaser.Easing.Circular.InOut, true);
this.view.tweens.add({
targets: this.container,
x: this.xhidden,
duration: 400,
ease: 'Circ.easeInOut'
});
} else {
this.x = this.xhidden;
this.container.x = this.xhidden;
}
}

View File

@ -2,7 +2,7 @@
module TK.SpaceTac.UI.Specs {
testing("CharacterUpgrade", test => {
let testgame = setupSingleView(test, () => [new BaseView(), []]);
let testgame = setupEmptyView(test);
test.acase("fills tooltip content", async check => {
let ship = new Ship();
@ -19,7 +19,7 @@ module TK.SpaceTac.UI.Specs {
let tooltip = new TooltipContainer(testgame.view);
let builder = new TooltipBuilder(tooltip);
display.fillTooltip(builder);
check.equals(cfilter(tooltip.content.children, Phaser.Text).map(child => child.text), [
check.equals(collectTexts(tooltip.content), [
"Test Upgrade",
"Permanent effects:",
"• hull capacity +10",
@ -35,7 +35,7 @@ module TK.SpaceTac.UI.Specs {
builder.clear();
display.fillTooltip(builder);
check.equals(cfilter(tooltip.content.children, Phaser.Text).map(child => child.text), [
check.equals(collectTexts(tooltip.content), [
"Test Upgrade",
"Fire (power 1, range 50km):",
"• do 10 damage on target",

View File

@ -21,7 +21,7 @@ module TK.SpaceTac.UI {
let button = builder.button("character-upgrade", x, y, undefined, tooltip, selector);
if (active) {
builder.switch(button, true);
button.toggle(true);
}
builder.in(button, builder => {
@ -30,7 +30,7 @@ module TK.SpaceTac.UI {
let icon = builder.image(this.getIcon(), 40, 40, true);
if (icon.width && icon.width > 64) {
icon.scale.set(64 / icon.width);
icon.setScale(64 / icon.width);
}
range(this.upgrade.cost || 0).forEach(i => {

View File

@ -2,12 +2,14 @@
module TK.SpaceTac.UI.Specs {
testing("FleetCreationView", test => {
let testgame = setupSingleView(test, () => [new FleetCreationView, []]);
let testgame = setupSingleView(test, () => [new FleetCreationView({}), []]);
test.acase("validates the fleet creation", async check => {
let mock_router = check.patch(testgame.view, "backToRouter");
check.same(testgame.ui.session.isFleetCreated(), false, "no fleet created");
check.same(testgame.ui.session.player.fleet.ships.length, 0, "empty session fleet");
check.same(testgame.view.dialogs_layer.children.length, 0, "no dialogs");
check.same(testgame.view.dialogs_layer.length, 0, "no dialogs");
check.same(testgame.view.character_sheet.fleet, testgame.view.built_fleet);
check.same(testgame.view.built_fleet.ships.length, 2, "initial fleet should have two ships");
@ -21,7 +23,7 @@ module TK.SpaceTac.UI.Specs {
await dialog.forceResult(false);
check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'no'");
check.same(testgame.ui.session.isFleetCreated(), false, "still no fleet created after 'no'");
check.equals(testgame.state, "test_initial");
check.called(mock_router, 0);
// close sheet, click on yes in confirmation dialog
testClick(testgame.view.character_sheet.close_button);
@ -30,7 +32,7 @@ module TK.SpaceTac.UI.Specs {
check.same(testgame.view.dialogs_opened.length, 0, "confirmation dialog destroyed after 'yes'");
check.same(testgame.ui.session.isFleetCreated(), true, "fleet created");
check.same(testgame.ui.session.player.fleet.ships.length, 2, "session fleet now has two ships");
check.equals(testgame.state, "router");
check.called(mock_router, 1);
})
})
}

View File

@ -21,7 +21,7 @@ module TK.SpaceTac.UI {
this.character_sheet = new CharacterSheet(this, CharacterSheetMode.CREATION, () => this.validateFleet());
this.character_sheet.show(this.built_fleet.ships[0], false);
this.getLayer("characters").add(this.character_sheet);
this.character_sheet.moveToLayer(this.getLayer("characters"));
}
/**

View File

@ -55,15 +55,15 @@ module TK.SpaceTac.UI.Specs {
});
test.case("animates rotation", check => {
let obj = { rotation: -Math.PI * 2.5 };
let tween = testgame.ui.tweens.create(obj);
let result = Animations.rotationTween(tween, Math.PI * 0.25, 1, Phaser.Easing.Linear.None);
let obj = new UIBuilder(testgame.view).image("test");
obj.setRotation(-Math.PI * 2.5);
let result = testgame.view.animations.rotationTween(obj, Math.PI * 0.25, 1, "Linear");
check.equals(result, 750);
check.equals(tween.generateData(4), [
{ rotation: -Math.PI * 0.25 },
{ rotation: 0 },
{ rotation: Math.PI * 0.25 },
]);
let points = testgame.view.animations.simulate(obj, "rotation", 4);
check.nears(points[0], -Math.PI * 0.5);
check.nears(points[1], -Math.PI * 0.25);
check.nears(points[2], 0);
check.nears(points[3], Math.PI * 0.25);
});
});
}

View File

@ -3,7 +3,6 @@ module TK.SpaceTac.UI {
x: number
y: number
rotation: number
game: Phaser.Game
};
/**
@ -23,10 +22,10 @@ module TK.SpaceTac.UI {
* This is a wrapper around phaser's tweens.
*/
export class Animations {
private tweens: Phaser.TweenManager
private tweens: Phaser.Tweens.TweenManager
private immediate = false
constructor(tweens: Phaser.TweenManager) {
constructor(tweens: Phaser.Tweens.TweenManager) {
this.tweens = tweens;
}
@ -40,14 +39,11 @@ module TK.SpaceTac.UI {
}
/**
* Create a tween on an object.
*
* If a previous tween is running for this object, it will be stopped, and a new one will be created.
* Kill previous tweens from an object
*/
private createTween(obj: any): Phaser.Tween {
this.tweens.removeFrom(obj, false);
let result = this.tweens.create(obj);
return result;
killPrevious(obj: object): void {
// TODO Only updated properties
this.tweens.killTweensOf(obj);
}
/**
@ -55,10 +51,16 @@ module TK.SpaceTac.UI {
*
* This may be heavy work and should only be done in testing code.
*/
simulate(obj: any, property: string, points = 5, duration = 1000): number[] {
let tween = first(this.tweens.getAll().concat((<any>this.tweens)._add), tween => tween.target === obj && !tween.pendingDelete);
simulate(obj: any, property: string, points = 5): number[] {
this.tweens.preUpdate();
let tween = first(this.tweens.getTweensOf(obj), tween => tween.isPlaying());
if (tween) {
return [obj[property]].concat(tween.generateData(1000 * (points - 1) / duration).map(data => data[property]));
let tween_obj = tween;
tween_obj.update(0, 0);
return range(points).map(i => {
tween_obj.seek(i / (points - 1));
return obj[property];
});
} else {
return [];
}
@ -68,28 +70,33 @@ module TK.SpaceTac.UI {
* Display an object, with opacity transition
*/
show(obj: IAnimationFadeable, duration = 1000, alpha = 1): void {
this.killPrevious(obj);
if (!obj.visible) {
obj.alpha = 0;
obj.visible = true;
}
if (duration && !this.immediate) {
let tween = this.createTween(obj);
tween.to({ alpha: alpha }, duration);
if (obj.input) {
let input = obj.input;
tween.onComplete.addOnce(() => {
input.enabled = true
obj.freezeFrames = false;
});
}
tween.start();
} else {
this.tweens.removeFrom(obj, false);
obj.alpha = alpha;
if (obj.input) {
obj.input.enabled = true;
let onComplete: Function | undefined;
if (obj.input) {
let input = obj.input;
onComplete = () => {
input.enabled = true
obj.freezeFrames = false;
};
}
if (duration && !this.immediate) {
this.tweens.add({
targets: obj,
alpha: alpha,
duration: duration,
onComplete: onComplete
})
} else {
obj.alpha = alpha;
if (onComplete) {
onComplete();
}
}
}
@ -98,6 +105,8 @@ module TK.SpaceTac.UI {
* Hide an object, with opacity transition
*/
hide(obj: IAnimationFadeable, duration = 1000, alpha = 0): void {
this.killPrevious(obj);
if (obj.changeStateFrame) {
obj.changeStateFrame("Out");
obj.freezeFrames = true;
@ -107,16 +116,18 @@ module TK.SpaceTac.UI {
obj.input.enabled = false;
}
let onComplete = () => obj.visible = alpha > 0;
if (duration && !this.immediate) {
let tween = this.createTween(obj);
tween.to({ alpha: alpha }, duration);
if (alpha == 0) {
tween.onComplete.addOnce(() => obj.visible = false);
}
tween.start();
this.tweens.add({
targets: obj,
alpha: alpha,
duration: duration,
onComplete: onComplete
});
} else {
obj.alpha = alpha;
obj.visible = alpha > 0;
onComplete();
}
}
@ -143,15 +154,19 @@ module TK.SpaceTac.UI {
/**
* Add an asynchronous animation to an object.
*/
addAnimation(obj: any, properties: any, duration: number, ease: Function = Phaser.Easing.Linear.None, delay = 0): Promise<void> {
addAnimation<T extends object>(obj: T, properties: Partial<T>, duration: number, ease = "Linear", delay = 0, loop = 1, yoyo = false): Promise<void> {
return new Promise((resolve, reject) => {
let tween = this.createTween(obj);
tween.to(properties, duration, ease, false, delay);
tween.onComplete.addOnce(() => {
this.tweens.remove(tween);
resolve();
});
tween.start();
this.killPrevious(obj);
this.tweens.add(merge<object>({
targets: obj,
ease: ease,
duration: duration,
delay: delay,
loop: loop - 1,
onComplete: resolve,
yoyo: yoyo
}, properties));
// By security, if the tween is destroyed before completion, we resolve the promise using the timer
Timer.global.schedule(delay + duration, resolve);
@ -178,10 +193,10 @@ module TK.SpaceTac.UI {
*
* Returns the duration
*/
static rotationTween(tween: Phaser.Tween, dest: number, speed = 1, easing = Phaser.Easing.Cubic.InOut, property = "rotation"): number {
rotationTween(obj: Phaser.GameObjects.Components.Transform, dest: number, speed = 1, easing = "Cubic.easeInOut"): number {
// Immediately change the object's current rotation to be in range (-pi,pi)
let value = UITools.normalizeAngle(tween.target[property]);
tween.target[property] = value;
let value = UITools.normalizeAngle(obj.rotation);
obj.setRotation(value);
// Compute destination angle
dest = UITools.normalizeAngle(dest);
@ -193,10 +208,8 @@ module TK.SpaceTac.UI {
let distance = Math.abs(UITools.normalizeAngle(dest - value)) / Math.PI;
let duration = distance * 1000 / speed;
// Update the tween
let changes: any = {};
changes[property] = dest;
tween.to(changes, duration, easing);
// Tween
this.addAnimation(obj, { rotation: dest }, duration, easing);
return duration;
}
@ -206,15 +219,10 @@ module TK.SpaceTac.UI {
*
* Returns the animation duration.
*/
static moveTo(obj: PhaserGraphics, x: number, y: number, angle: number, rotated_obj = obj, ease = true): number {
let tween_rot = obj.game.tweens.create(rotated_obj);
let duration_rot = Animations.rotationTween(tween_rot, angle, 0.5);
let tween_pos = obj.game.tweens.create(obj);
moveTo(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj, ease = true): number {
let duration_rot = this.rotationTween(rotated_obj, angle, 0.5);
let duration_pos = arenaDistance(obj, { x: x, y: y }) * 2;
tween_pos.to({ x: x, y: y }, duration_pos, ease ? Phaser.Easing.Quadratic.InOut : undefined);
tween_rot.start();
tween_pos.start();
this.addAnimation(obj, { x: x, y: y }, duration_pos, ease ? "Quad.easeInOut" : "Linear");
return Math.max(duration_rot, duration_pos);
}
@ -223,32 +231,38 @@ module TK.SpaceTac.UI {
*
* Returns the animation duration.
*/
static moveInSpace(obj: PhaserGraphics, x: number, y: number, angle: number, rotated_obj = obj): number {
moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj): number {
if (x == obj.x && y == obj.y) {
let tween = obj.game.tweens.create(rotated_obj);
let duration = Animations.rotationTween(tween, angle, 0.5);
tween.start();
return duration;
this.killPrevious(obj);
return this.rotationTween(rotated_obj, angle, 0.5);
} else {
this.killPrevious(obj);
this.killPrevious(rotated_obj);
let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y));
var tween = obj.game.tweens.create(obj);
let duration = Math.sqrt(distance / 1000) * 3000;
let curve_force = distance * 0.4;
tween.to({
x: [obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x],
y: [obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y]
}, duration, Phaser.Easing.Sinusoidal.InOut);
tween.interpolation((v: any, k: any) => Phaser.Math.bezierInterpolation(v, k));
let prevx = obj.x;
let prevy = obj.y;
tween.onUpdateCallback(() => {
if (prevx != obj.x || prevy != obj.y) {
rotated_obj.rotation = Math.atan2(obj.y - prevy, obj.x - prevx);
let xpts = [obj.x, obj.x + Math.cos(rotated_obj.rotation) * curve_force, x - Math.cos(angle) * curve_force, x];
let ypts = [obj.y, obj.y + Math.sin(rotated_obj.rotation) * curve_force, y - Math.sin(angle) * curve_force, y];
let fobj = { t: 0 };
this.tweens.add({
targets: [fobj],
t: 1,
duration: duration,
ease: "Sine.easeInOut",
onUpdate: () => {
obj.setPosition(
Phaser.Math.Interpolation.CubicBezier(fobj.t, xpts[0], xpts[1], xpts[2], xpts[3]),
Phaser.Math.Interpolation.CubicBezier(fobj.t, ypts[0], ypts[1], ypts[2], ypts[3]),
)
if (prevx != obj.x || prevy != obj.y) {
rotated_obj.setRotation(Math.atan2(obj.y - prevy, obj.x - prevx));
}
prevx = obj.x;
prevy = obj.y;
}
prevx = obj.x;
prevy = obj.y;
});
tween.start();
})
return duration;
}
}

View File

@ -1,46 +1,85 @@
module TK.SpaceTac.UI {
// Utility functions for sounds
class AudioSettings {
main_volume = 1
music_volume = 1
}
/**
* Utility functions to play sounds and musics
*/
export class Audio {
private game: MainUI
private music: Phaser.Sound | null = null
private music_volume = 1
private static SETTINGS = new AudioSettings();
private music: Phaser.Sound.BaseSound | undefined
private music_playing_volume = 1
constructor(game: MainUI) {
this.game = game;
constructor(private view: BaseView | null) {
}
// Check if the sound system is up and running
isActive(): boolean {
return !this.game.headless && this.game.sound.context;
/**
* Check if the sound system is active, and return a manager to operate with it
*/
private getManager(): Phaser.Sound.BaseSoundManager | null {
if (this.view) {
return this.view.sound;
} else {
return null;
}
}
// Play a ponctual sound
/**
* Check if an audio key is present in cache
*/
hasCache(key: string): boolean {
return this.view ? this.view.cache.audio.has(key) : false;
}
/**
* Play a single sound effect (fire-and-forget)
*/
playOnce(key: string): void {
if (this.isActive()) {
this.game.sound.play(key);
let manager = this.getManager();
if (manager) {
if (this.hasCache(key)) {
manager.play(key);
} else {
console.warn("Missing sound", key);
}
}
}
// Start a background music
/**
* Start a background music in repeat
*/
startMusic(key: string, volume = 1): void {
key = "music-" + key;
if (this.isActive()) {
let manager = this.getManager();
if (manager) {
this.stopMusic();
if (!this.music) {
this.music_playing_volume = volume;
this.music = this.game.sound.play(key, volume * this.music_volume, true);
key = "music-" + key;
if (this.hasCache(key)) {
this.music_playing_volume = volume;
this.music = manager.add(key, {
volume: volume * Audio.SETTINGS.music_volume,
loop: true
});
this.music.play();
} else {
console.warn("Missing music", key);
}
}
}
}
// Stop currently playing background music
/**
* Stop currently playing background music
*/
stopMusic(): void {
if (this.isActive()) {
if (this.music) {
this.music.stop();
this.music = null;
}
let music = this.music;
if (music) {
music.stop();
music.destroy();
this.music = undefined;
}
}
@ -48,19 +87,18 @@ module TK.SpaceTac.UI {
* Get the main volume (0-1)
*/
getMainVolume(): number {
if (this.isActive()) {
return this.game.sound.volume;
} else {
return 0;
}
return Audio.SETTINGS.main_volume;
}
/**
* Set the main volume (0-1)
*/
setMainVolume(value: number) {
if (this.isActive()) {
this.game.sound.volume = clamp(value, 0, 1);
Audio.SETTINGS.main_volume = clamp(value, 0, 1);
let manager = this.getManager();
if (manager) {
manager.volume = Audio.SETTINGS.main_volume;
}
}
@ -68,17 +106,22 @@ module TK.SpaceTac.UI {
* Get the music volume (0-1)
*/
getMusicVolume(): number {
return this.music_volume;
return Audio.SETTINGS.music_volume;
}
/**
* Set the music volume (0-1)
*/
setMusicVolume(value: number) {
this.music_volume = value;
if (this.isActive()) {
if (this.music) {
this.music.volume = value * this.music_playing_volume;
Audio.SETTINGS.music_volume = clamp(value, 0, 1);
let music = this.music;
if (music) {
// TODO Set music volume
if (value) {
music.resume();
} else {
music.pause();
}
}
}

View File

@ -6,9 +6,9 @@ module TK.SpaceTac.UI.Specs {
test.case("handles hover and click on desktops and mobile targets", check => {
let inputs = testgame.view.inputs;
let pointer = new Phaser.Pointer(testgame.ui, 0);
function newButton(): [Phaser.Button, { enter: Mock<Function>, leave: Mock<Function>, click: Mock<Function> }] {
let button = new Phaser.Button(testgame.ui);
let pointer = new Phaser.Input.Pointer(testgame.view.input.manager, 0);
function newButton(): [UIImage, { enter: Mock<Function>, leave: Mock<Function>, click: Mock<Function> }] {
let button = new UIImage(testgame.view, 0, 0, "fake");
let mocks = {
enter: check.mockfunc("enter"),
leave: check.mockfunc("leave"),
@ -18,118 +18,85 @@ module TK.SpaceTac.UI.Specs {
(<any>inputs).hovered = null;
return [button, mocks];
}
let enter = (button: Phaser.Button) => (<any>button.input)._pointerOverHandler(pointer);
let leave = (button: Phaser.Button) => (<any>button.input)._pointerOutHandler(pointer);
let press = (button: Phaser.Button) => button.onInputDown.dispatch(button, pointer);
let release = (button: Phaser.Button) => button.onInputUp.dispatch(button, pointer);
let destroy = (button: Phaser.Button) => button.events.onDestroy.dispatch();
let enter = (button: UIImage) => button.emit("pointerover", pointer);
let leave = (button: UIImage) => button.emit("pointerout", pointer);
let press = (button: UIImage) => button.emit("pointerdown", pointer);
let release = (button: UIImage) => button.emit("pointerup", pointer);
let destroy = (button: UIImage) => button.emit("destroy");
// Simple click on desktop
let [button, mocks] = newButton();
enter(button);
press(button);
release(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 0);
check.called(mocks.click, 1);
check.in("Simple click on desktop", check => {
enter(button);
press(button);
release(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 0);
check.called(mocks.click, 1);
});
// Simple click on mobile
[button, mocks] = newButton();
press(button);
release(button);
check.called(mocks.enter, 1);
check.called(mocks.leave, 1);
check.called(mocks.click, 1);
check.in("Simple click on mobile", check => {
press(button);
release(button);
check.called(mocks.enter, 1);
check.called(mocks.leave, 1);
check.called(mocks.click, 1);
});
// Leaves on destroy
[button, mocks] = newButton();
press(button);
clock.forward(150);
check.called(mocks.enter, 1);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
destroy(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 1);
check.called(mocks.click, 0);
press(button);
release(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
check.in("Leaves on destroy", check => {
press(button);
clock.forward(150);
check.called(mocks.enter, 1);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
destroy(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 1);
check.called(mocks.click, 0);
press(button);
release(button);
check.called(mocks.enter, 0);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
});
// Force-leave when hovering another button without clean leaving a first one
let [button1, funcs1] = newButton();
let [button2, funcs2] = newButton();
enter(button1);
clock.forward(150);
check.called(funcs1.enter, 1);
check.called(funcs1.leave, 0);
check.called(funcs1.click, 0);
enter(button2);
check.called(funcs1.enter, 0);
check.called(funcs1.leave, 1);
check.called(funcs1.click, 0);
check.called(funcs2.enter, 0);
check.called(funcs2.leave, 0);
check.called(funcs2.click, 0);
clock.forward(150);
check.called(funcs1.enter, 0);
check.called(funcs1.leave, 0);
check.called(funcs1.click, 0);
check.called(funcs2.enter, 1);
check.called(funcs2.leave, 0);
check.called(funcs2.click, 0);
check.in("Force-leave when hovering another button without clean leaving a first one", check => {
let [button1, funcs1] = newButton();
let [button2, funcs2] = newButton();
enter(button1);
clock.forward(150);
check.called(funcs1.enter, 1);
check.called(funcs1.leave, 0);
check.called(funcs1.click, 0);
enter(button2);
check.called(funcs1.enter, 0);
check.called(funcs1.leave, 1);
check.called(funcs1.click, 0);
check.called(funcs2.enter, 0);
check.called(funcs2.leave, 0);
check.called(funcs2.click, 0);
clock.forward(150);
check.called(funcs1.enter, 0);
check.called(funcs1.leave, 0);
check.called(funcs1.click, 0);
check.called(funcs2.enter, 1);
check.called(funcs2.leave, 0);
check.called(funcs2.click, 0);
});
// Hold to hover on mobile
[button, mocks] = newButton();
button.onInputDown.dispatch(button, pointer);
clock.forward(150);
check.called(mocks.enter, 1);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
button.onInputUp.dispatch(button, pointer);
check.called(mocks.enter, 0);
check.called(mocks.leave, 1);
check.called(mocks.click, 0);
});
test.case("handles drag and drop", check => {
let builder = new UIBuilder(testgame.view);
let button = builder.button("test", 0, 0, () => null, "test tooltip");
let tooltip = (<any>testgame.view.tooltip).container;
check.same(button.inputEnabled, true, "input should be enabled initially");
check.same(button.input.draggable, false, "dragging should be disabled initially");
let x = 0;
testgame.view.inputs.setDragDrop(button, () => x += 1, () => x -= 1);
check.same(button.inputEnabled, true, "input should still be enabled");
check.same(button.input.draggable, true, "dragging should be enabled");
check.same(tooltip.visible, false, "tooltip hidden initially");
button.onInputOver.dispatch(button, testgame.ui.input.pointer1);
clock.forward(1000);
check.same(tooltip.visible, true, "tooltip shown");
check.same(x, 0, "initial state");
button.events.onDragStart.dispatch();
check.same(x, 1, "dragged");
check.same(tooltip.visible, false, "tooltip hidden on dragging");
button.events.onDragStop.dispatch();
check.same(x, 0, "dropped");
testgame.view.inputs.setDragDrop(button);
button.events.onDragStart.dispatch();
check.same(x, 0, "drag signal should be disabled");
check.same(button.inputEnabled, true, "input should remain enabled");
check.same(button.input.draggable, false, "dragging should be disabled at the end");
testgame.view.inputs.setDragDrop(button, () => x += 1, () => x -= 1);
button.events.onDragStart.dispatch();
check.same(x, 1, "drag signal should be dispatch once");
check.in("Hold to hover on mobile", check => {
button.emit("pointerdown", pointer);
clock.forward(150);
check.called(mocks.enter, 1);
check.called(mocks.leave, 0);
check.called(mocks.click, 0);
button.emit("pointerup", pointer);
check.called(mocks.enter, 0);
check.called(mocks.leave, 1);
check.called(mocks.click, 0);
});
});
});
}

View File

@ -8,12 +8,12 @@ module TK.SpaceTac.UI {
private debug = false
private view: BaseView
private game: MainUI
private input: Phaser.Input
private input: Phaser.Input.InputManager
private cheats_allowed: boolean
private cheat: boolean
private hovered: Phaser.Button | null = null
private hovered: UIButton | UIContainer | UIImage | null = null
private binds: { [key: string]: KeyPressedCallback } = {}
@ -23,19 +23,17 @@ module TK.SpaceTac.UI {
constructor(view: BaseView) {
this.view = view;
this.game = view.gameui;
this.input = view.input;
this.input = view.input.manager;
this.cheats_allowed = true;
this.cheat = false;
this.input.reset(true);
// Default mappings
this.bind("s", "Quick save", () => {
this.game.saveGame();
});
this.bind("l", "Quick load", () => {
this.game.loadGame();
this.game.state.start("router");
this.view.backToRouter();
});
this.bind("m", "Toggle sound", () => {
this.game.options.setNumberValue("mainvolume", this.game.options.getNumberValue("mainvolume") > 0 ? 0 : 1);
@ -51,7 +49,7 @@ module TK.SpaceTac.UI {
});
if (!this.game.headless) {
this.input.keyboard.addCallbacks(this, undefined, (event: KeyboardEvent) => {
this.input.keyboard.on("keyup", (event: KeyboardEvent) => {
if (this.debug) {
console.log(event);
}
@ -68,6 +66,13 @@ module TK.SpaceTac.UI {
}
}
/**
* Remove the bindings
*/
destroy(): void {
this.input.keyboard.removeAllListeners("keyup");
}
/**
* Bind a key to a specific action.
*/
@ -126,45 +131,11 @@ module TK.SpaceTac.UI {
* Force the cursor out of currently hovered object
*/
private forceLeaveHovered() {
if (this.hovered && this.hovered.data.hover_pointer) {
(<any>this.hovered.input)._pointerOutHandler(this.hovered.data.hover_pointer);
}
}
/**
* Setup dragging on an UI component
*
* If no drag or drop function is defined, dragging is disabled
*
* If update function is defined, it will receive (a lot of) cursor moves while dragging
*/
setDragDrop(obj: Phaser.Button | Phaser.Image, drag?: Function, drop?: Function, update?: Function): void {
obj.events.onDragStart.removeAll();
obj.events.onDragStop.removeAll();
obj.events.onDragUpdate.removeAll();
if (drag && drop) {
obj.inputEnabled = true;
obj.input.enableDrag(false, true);
obj.events.onDragStart.add(() => {
this.forceLeaveHovered();
this.view.audio.playOnce("ui-drag");
drag();
});
obj.events.onDragStop.add(() => {
this.view.audio.playOnce("ui-drop");
drop();
});
if (update) {
obj.events.onDragUpdate.add(() => {
update();
});
if (this.hovered && this.hovered.data) {
let pointer = this.hovered.data.get("pointer");
if (pointer) {
this.hovered.emit("pointerout", pointer);
}
} else {
obj.input.disableDrag();
}
}
@ -172,17 +143,24 @@ module TK.SpaceTac.UI {
* Setup hover/click handlers on an UI element
*
* This is done in a way that should be compatible with touch-enabled screen
*
* Returns functions that may be used to force the behavior
*/
setHoverClick(obj: Phaser.Button, enter: Function = nop, leave: Function = nop, click: Function = nop, hovertime = 300, holdtime = 600) {
setHoverClick(obj: UIButton | UIContainer | UIImage, enter: Function = nop, leave: Function = nop, click: Function = nop, hovertime = 300, holdtime = 600, sound = false): void {
let holdstart = Timer.nowMs();
let enternext: Function | null = null;
let entercalled = false;
let cursorinside = false;
let destroyed = false;
obj.input.useHandCursor = true;
obj.setDataEnabled();
if (obj instanceof UIImage) {
obj.setInteractive();
} else if (!(obj instanceof UIButton)) {
let bounds = obj.getBounds();
bounds.x -= obj.x;
bounds.y -= obj.y;
obj.setInteractive(bounds, Phaser.Geom.Rectangle.Contains);
}
let prevententer = () => {
if (enternext != null) {
@ -210,15 +188,13 @@ module TK.SpaceTac.UI {
}
}
if (obj.events) {
obj.events.onDestroy.addOnce(() => {
destroyed = true;
effectiveleave();
});
}
obj.on("destroy", () => {
destroyed = true;
effectiveleave();
});
obj.onInputOver.add((_: any, pointer: Phaser.Pointer) => {
if (destroyed) return;
obj.on("pointerover", (pointer: Phaser.Input.Pointer) => {
if (destroyed || !UITools.isVisible(obj)) return;
if (this.hovered) {
if (this.hovered === obj) {
@ -228,15 +204,13 @@ module TK.SpaceTac.UI {
}
}
this.hovered = obj;
this.hovered.data.hover_pointer = pointer;
this.hovered.data.set("pointer", pointer);
if (obj.visible && obj.alpha) {
cursorinside = true;
enternext = Timer.global.schedule(hovertime, effectiveenter);
}
cursorinside = true;
enternext = Timer.global.schedule(hovertime, effectiveenter);
});
obj.onInputOut.add(() => {
obj.on("pointerout", (pointer: Phaser.Input.Pointer) => {
if (destroyed) return;
if (this.hovered === obj) {
@ -247,18 +221,21 @@ module TK.SpaceTac.UI {
effectiveleave();
});
obj.onInputDown.add(() => {
obj.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
if (destroyed) return;
if (obj.visible && obj.alpha) {
if (UITools.isVisible(obj)) {
holdstart = Timer.nowMs();
if (sound) {
this.view.audio.playOnce("ui-button-down");
}
if (!cursorinside && !enternext) {
enternext = Timer.global.schedule(holdtime, effectiveenter);
}
}
});
obj.onInputUp.add(() => {
obj.on("pointerup", (event: Phaser.Input.Pointer) => {
if (destroyed) return;
if (!cursorinside) {
@ -269,6 +246,9 @@ module TK.SpaceTac.UI {
if (!cursorinside) {
effectiveenter();
}
if (sound) {
this.view.audio.playOnce("ui-button-up");
}
click();
if (!cursorinside) {
effectiveleave();

View File

@ -1,66 +1,64 @@
module TK.SpaceTac.UI {
// A single displayed message
class Message extends Phaser.Group {
/**
* A single displayed message
*/
class Message extends UIContainer {
view: BaseView
background: Phaser.Graphics
text: Phaser.Text
background: UIBackground
text: UIText
constructor(parent: Messages, text: string, duration: number) {
super(parent.view.game);
super(parent.view);
this.view = parent.view;
let builder = new UIBuilder(this.view).in(this);
this.background = new Phaser.Graphics(this.game);
this.add(this.background);
this.background = new UIBackground(this.view, this);
this.text = builder.text(text, 0, 0, { color: "#DBEFF9", shadow: true, size: 16, center: false, vcenter: false });
this.position.set(parent.view.getWidth(), 10);
UITools.drawBackground(this.text, this.background, 6);
let bounds = UITools.getBounds(this);
this.setPosition(parent.view.getWidth() - bounds.width - 10, 10);
parent.view.timer.schedule(duration, () => this.hide());
}
// Hide the message
/**
* Hide the message
*/
hide() {
var tween = this.game.tweens.create(this);
tween.to({ y: this.y + 50, alpha: 0 }, 400, Phaser.Easing.Circular.In);
tween.onComplete.addOnce(() => {
this.destroy();
});
tween.start();
}
update() {
UITools.drawBackground(this.text, this.background, 6);
this.x = this.view.getWidth() - this.width - 10;
this.view.animations.addAnimation<UIContainer>(this, { y: this.y + 50, alpha: 0 }, 400, "Circ.easeIn").then(() => this.destroy());
}
}
// Visual notifications of game-related messages (eg. "Game saved"...)
/**
* Visual notifications of game-related messages (eg. "Game saved"...)
*/
export class Messages {
// Link to parent view
view: BaseView;
view: BaseView
// Main group to hold the visual messages
container: Phaser.Group;
container: UIContainer
constructor(parent: BaseView) {
this.view = parent;
this.container = new Phaser.Group(parent.game);
parent.add.existing(this.container);
constructor(view: BaseView) {
this.view = view;
this.container = new UIBuilder(view, view.messages_layer).container("messages");
}
// Add a new message to the notifications
/**
* Add a new message to the notifications
*/
addMessage(text: string, duration: number = 3000): void {
this.container.forEachExists((child: Message) => {
child.y += child.height + 5;
}, this);
let message = new Message(this, text, duration);
this.container.add(message);
var message = new Message(this, text, duration);
this.container.addChild(message);
let bounds = UITools.getBounds(message);
cfilter(this.container.list, Message).forEach(child => {
child.y += bounds.height + 5;
});
}
}
}

View File

@ -9,25 +9,29 @@ module TK.SpaceTac.UI.Specs {
new ParticleConfig(ParticleShape.DISK_HALO, ParticleColor.WHITE, 0.5, 1, 0, 5, 0)
]);
check.equals(particle instanceof Phaser.Image, true);
check.equals(particle.data.frame, 4);
check.equals(particle.data.key, "common-particles");
check.equals(particle.scale.x, 2);
check.equals(particle.scale.y, 2);
check.equals(particle.x, 10);
check.equals(particle.y, -20);
check.equals(particle.angle, 45);
check.equals(particle.length, 2);
check.equals(particle.children.length, 1);
let subparticle = <Phaser.Image>particle.getChildAt(0);
check.equals(subparticle instanceof Phaser.Image, true);
check.equals(subparticle.data.frame, 16);
check.equals(subparticle.data.key, "common-particles");
check.equals(subparticle.scale.x, 0.25);
check.equals(subparticle.scale.y, 0.25);
check.equals(subparticle.x, 2.5);
check.equals(subparticle.y, 0);
check.equals(subparticle.angle, -45);
let child = particle.list[0];
if (check.instance(child, Phaser.GameObjects.Image, "first particle is an image")) {
check.equals((<any>child.data).frame, 4);
check.equals((<any>child.data).key, "common-particles");
check.equals(child.scaleX, 2);
check.equals(child.scaleY, 2);
check.equals(child.x, 10);
check.equals(child.y, -20);
check.equals(child.angle, 45);
}
child = particle.list[1];
if (check.instance(child, Phaser.GameObjects.Image, "second particle is an image")) {
check.equals((<any>child.data).frame, 16);
check.equals((<any>child.data).key, "common-particles");
check.equals(child.scaleX, 0.5);
check.equals(child.scaleY, 0.5);
check.equals(child.x, 5);
check.equals(child.y, 0);
check.equals(child.angle, 0);
}
});
});
}

View File

@ -56,16 +56,16 @@ module TK.SpaceTac.UI {
/**
* Get a particle image for this config
*/
getImage(game: Phaser.Game, scaling = 1, angle = 0): Phaser.Image {
getImage(view: BaseView): UIImage {
let frame = this.shape * 16 + this.color;
let result = game.add.image(0, 0, "common-particles", frame);
result.data.frame = frame;
result.data.key = "common-particles";
result.anchor.set(0.5);
result.angle = angle + this.angle;
result.alpha = this.alpha;
result.scale.set(this.scale * scaling);
result.position.set(this.offsetx * scaling, this.offsety * scaling);
let result = view.add.image(0, 0, "common-particles", frame);
result.setDataEnabled();
(<any>result.data).frame = frame;
(<any>result.data).key = "common-particles";
result.setAngle(this.angle);
result.setAlpha(this.alpha);
result.setScale(this.scale);
result.setPosition(this.offsetx, this.offsety);
return result;
}
}
@ -83,18 +83,14 @@ module TK.SpaceTac.UI {
/**
* Build a composed particle
*/
build(configs: ParticleConfig[]): Phaser.Image {
if (configs.length == 0) {
return this.view.newImage("common-transparent");
} else {
let base = configs[0];
let result = base.getImage(this.view.game, 1);
configs.slice(1).forEach(config => {
let sub = config.getImage(this.view.game, 1 / base.scale, -base.angle);
result.addChild(sub);
});
return result;
}
build(configs: ParticleConfig[]): UIContainer {
let result = this.view.add.container(0, 0);
configs.forEach(config => {
result.add(config.getImage(this.view));
});
return result;
}
}
}

View File

@ -0,0 +1,107 @@
module TK.SpaceTac.UI {
type Manager = Phaser.GameObjects.Particles.ParticleEmitterManager;
export enum ParticleFacingMode {
INITIAL = 1,
ALWAYS = 2
}
export type ParticlesConfig = {
// Key for the particle texture
key: string,
// Source of the particles
source: { x: number, y: number, radius: number },
// Total number of particles to emit
count: number,
// Duration of the emission of particles
emitDuration: number
// Lifespan of a single particle in milliseconds
lifetime: number,
// Fade the alpha during the lifespan
fading?: boolean,
// Direction of the particles for radial emission
direction: { minangle: number, maxangle: number },
// Scale of the emitted particles
scale: { min: number, max: number }
// Speed of the particles
speed: { min: number, max: number }
// Force the particle to face its direction
facing?: ParticleFacingMode
}
/**
* System to emit multiple particles of the same texture
*/
export class ParticleSystem {
constructor(private view: BaseView) {
}
private getManager(key: string, parent?: UIContainer): Manager {
let info = this.view.getImageInfo(key);
let result = this.view.add.particles(info.key, info.frame);
if (parent) {
parent.add(result);
}
return result;
}
/**
* Emit a batch of particles
*
* Returns the total duration in milliseconds
*/
emit(config: ParticlesConfig, parent?: UIContainer): number {
let manager = this.getManager(config.key, parent);
let emitter = manager.createEmitter({});
if (config.fading) {
emitter.setAlpha({ start: 1, end: 0 });
}
emitter.setPosition(
{ min: config.source.x - config.source.radius, max: config.source.x + config.source.radius },
{ min: config.source.y - config.source.radius, max: config.source.y + config.source.radius },
);
emitter.setSpeed({ min: config.speed.min, max: config.speed.max });
emitter.setRadial(true);
emitter.setEmitterAngle({ min: degrees(config.direction.minangle), max: degrees(config.direction.maxangle) });
emitter.setLifespan(config.lifetime);
emitter.setFrequency(config.emitDuration / config.count, 1);
emitter.setScale({ min: config.scale.min, max: config.scale.max });
if (config.facing) {
emitter.particleClass = (config.facing == ParticleFacingMode.ALWAYS) ? FacingAlwaysParticle : FacingInitialParticle;
}
this.view.timer.schedule(config.emitDuration, () => emitter.on = false);
this.view.timer.schedule(config.emitDuration + config.lifetime, () => manager.destroy());
return config.emitDuration + config.lifetime;
}
/**
* Async version of *emit*
*/
emit_as(config: ParticlesConfig): Promise<void> {
let duration = this.emit(config);
return this.view.timer.sleep(duration);
}
}
/**
* Particle that is rotated to face its initial direction
*/
class FacingInitialParticle extends Phaser.GameObjects.Particles.Particle {
fire(x: number, y: number): any {
let result = super.fire(x, y);
this.rotation = Math.atan2(this.velocityY, this.velocityX);
return result;
}
}
/**
* Particle that is rotated to face its movement direction
*/
class FacingAlwaysParticle extends FacingInitialParticle {
update(delta: any, step: any, processors: any): any {
let result = super.update(delta, step, processors);
this.rotation = Math.atan2(this.velocityY, this.velocityX);
return result;
}
}
}

View File

@ -4,26 +4,27 @@ module TK.SpaceTac.UI.Specs {
let clock = test.clock();
test.case("shows near the hovered button", check => {
let button = testgame.view.add.button();
check.patch(button, "getBounds", () => new PIXI.Rectangle(100, 50, 50, 25));
let button = new UIBuilder(testgame.view).button("fake");
check.patch(button, "getBounds", () => new Phaser.Geom.Rectangle(100, 50, 50, 25));
let tooltip = new Tooltip(testgame.view);
tooltip.bind(button, filler => true);
let container = <Phaser.Group>(<any>tooltip).container;
check.patch((<any>container).content, "getBounds", () => new PIXI.Rectangle(0, 0, 32, 32));
let container = tooltip.container;
check.patch(container.content, "getBounds", () => new Phaser.Geom.Rectangle(0, 0, 32, 32));
check.equals(container.visible, false);
button.onInputOver.dispatch();
let pointer = {};
button.emit("pointerover", { pointer: pointer });
check.equals(container.visible, false);
clock.forward(1000);
container.update();
check.equals(container.visible, true);
check.equals(container.x, 109);
check.equals(container.x, 113);
check.equals(container.y, 91);
button.onInputOut.dispatch();
button.emit("pointerout", { pointer: pointer });
check.equals(container.visible, false);
});
});

View File

@ -4,25 +4,24 @@ module TK.SpaceTac.UI {
export type TooltipFiller = string | ((filler: TooltipBuilder) => string) | ((filler: TooltipBuilder) => boolean);
export class TooltipContainer extends Phaser.Group {
export class TooltipContainer extends UIContainer {
view: BaseView
background: Phaser.Graphics
content: Phaser.Group
background: UIBackground
content: UIContainer
item?: IBounded
border = 10
margin = 6
viewport: IBounded | null = null
constructor(view: BaseView) {
super(view.game);
super(view);
this.view = view;
this.visible = false;
this.background = new Phaser.Graphics(this.game);
this.add(this.background);
this.background = new UIBackground(view, this);
this.content = new Phaser.Group(this.game);
this.content = new UIContainer(view);
this.add(this.content);
this.view.tooltip_layer.add(this);
@ -64,7 +63,7 @@ module TK.SpaceTac.UI {
x += this.border;
y += this.border;
if (x != this.x || y != this.y) {
this.position.set(x, y);
this.setPosition(x, y);
}
}
}
@ -80,7 +79,7 @@ module TK.SpaceTac.UI {
* Functions used to fill a tooltip content
*/
export class TooltipBuilder extends UIBuilder {
private container: TooltipContainer;
private content: TooltipContainer;
constructor(container: TooltipContainer) {
let style = new UITextStyle();
@ -89,17 +88,17 @@ module TK.SpaceTac.UI {
style.shadow = true;
super(container.view, container.content, style);
this.container = container;
this.content = container;
}
/**
* Configure the positioning and base style of the tooltip
*/
configure(border = 10, margin = 6, viewport: IBounded | null = null): void {
this.container.border = border;
this.container.margin = margin;
this.content.border = border;
this.content.margin = margin;
if (viewport) {
this.container.viewport = viewport;
this.content.viewport = viewport;
}
}
}
@ -108,8 +107,8 @@ module TK.SpaceTac.UI {
* Tooltip system, to display information on hover
*/
export class Tooltip {
protected view: BaseView;
protected container: TooltipContainer;
readonly view: BaseView;
readonly container: TooltipContainer;
constructor(view: BaseView) {
this.view = view;
@ -132,13 +131,13 @@ module TK.SpaceTac.UI {
*
* When the component is hovered, the function is called to allow filling the tooltip container
*/
bind(obj: Phaser.Button, func: (filler: TooltipBuilder) => boolean): void {
bind(obj: UIButton | UIImage, func: (filler: TooltipBuilder) => boolean): void {
this.view.inputs.setHoverClick(obj,
// enter
() => {
this.hide();
if (func(this.getBuilder())) {
this.container.show(obj.getBounds());
this.container.show(UITools.getBounds(obj));
}
},
// leave
@ -146,13 +145,13 @@ module TK.SpaceTac.UI {
// click
() => this.hide()
);
obj.onInputDown.add(() => this.hide());
obj.on("pointerdown", () => this.hide());
}
/**
* Bind to an UI component to display a dynamic text
*/
bindDynamicText(obj: Phaser.Button, text_getter: () => string): void {
bindDynamicText(obj: UIButton | UIImage, text_getter: () => string): void {
this.bind(obj, filler => {
let content = text_getter();
if (content) {
@ -167,14 +166,14 @@ module TK.SpaceTac.UI {
/**
* Bind to an UI component to display a simple text
*/
bindStaticText(obj: Phaser.Button, text: string): void {
bindStaticText(obj: UIButton | UIImage, text: string): void {
this.bindDynamicText(obj, () => text);
}
/**
* Show a tooltip for a component
*/
show(obj: Phaser.Button, content: TooltipFiller): void {
show(obj: UIButton, content: TooltipFiller): void {
let builder = this.getBuilder();
let scontent = (typeof content == "string") ? content : content(builder);
if (typeof scontent == "string") {
@ -182,7 +181,7 @@ module TK.SpaceTac.UI {
}
if (scontent) {
this.container.show(obj.getBounds());
this.container.show(UITools.getBounds(obj));
} else {
this.hide();
}

View File

@ -0,0 +1,59 @@
module TK.SpaceTac.UI {
/**
* Decorated background for dynamic sized content (such as tooltips)
*/
export class UIBackground {
private graphics: UIGraphics
x = 0
y = 0
width = 0
height = 0
constructor(readonly view: BaseView, readonly parent: UIContainer, readonly border = 6) {
this.graphics = new UIBuilder(view, parent).graphics("background", 0, 0, false);
}
/**
* Adapt the background to cover a given content
*/
adaptToContent(content: UIContainer | UIText): void {
if (content.parentContainer != this.graphics.parentContainer) {
console.error("Content and background should have the same parent container");
return;
}
let bounds = UITools.getBounds(content);
let x = bounds.x - this.graphics.parentContainer.x - this.border;
let y = bounds.y - this.graphics.parentContainer.y - this.border;
let width = bounds.width + 2 * this.border;
let height = bounds.height + 2 * this.border;
if (x != this.x || y != this.y || width != this.width || height != this.height) {
this.graphics.clear();
this.graphics.lineStyle(2, 0x6690a4);
this.graphics.fillStyle(0x162730);
this.graphics.fillRect(x, y, width, height);
this.graphics.strokeRect(x, y, width, height);
this.graphics.setVisible(true);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
/**
* Remove the drawn background
*/
clear(): void {
this.graphics.setVisible(false);
this.graphics.clear();
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0;
}
}
}

View File

@ -4,17 +4,23 @@ module TK.SpaceTac.UI.Specs {
function get(path: (number | string)[]): [string, any] {
let spath = `[${path.join(" -> ")}]`;
let component: any = testgame.view.world;
let component: Phaser.GameObjects.GameObject | Phaser.Scene | null = testgame.view;
path.forEach(idx => {
component = (typeof idx == "number") ? component.children[idx] : component.getByName(idx);
if (component instanceof Phaser.Scene) {
component = (typeof idx == "number") ? component.children.list[idx] : component.children.getByName(idx);
} else if (component instanceof Phaser.GameObjects.Container) {
component = (typeof idx == "number") ? component.list[idx] : component.getByName(idx);
} else {
component = null;
}
if (!component) {
throw new Error(`Path not found: ${spath}`);
throw new Error(`Path not found: ${spath} (${idx} part)`);
}
});
return [spath, component];
}
function checkcomp(path: (number | string)[], ctype?: any, name?: string, attrs?: any): any {
function checkcomp<T extends Phaser.GameObjects.GameObject>(path: (number | string)[], ctype?: { new(...args: any[]): T }, name?: string, attrs?: Partial<T>): T {
let [spath, component] = get(path);
if (typeof ctype != "undefined") {
@ -24,7 +30,7 @@ module TK.SpaceTac.UI.Specs {
test.check.equals(component.name, name, spath);
}
if (typeof attrs != "undefined") {
iteritems(attrs, (key, value) => {
iteritems(<any>attrs, (key, value) => {
test.check.equals(component[key], value, spath);
});
}
@ -32,133 +38,145 @@ module TK.SpaceTac.UI.Specs {
return component;
}
function checktext(path: (number | string)[], attrs?: Partial<UIText>, style?: Partial<Phaser.GameObjects.Text.TextStyle>): UIText {
let text = checkcomp(path, UIText, "", attrs);
if (typeof style != "undefined") {
iteritems(<any>style, (key, value) => {
test.check.equals((<any>text.style)[key], value, `text style ${key}`);
});
}
return text;
}
test.case("can work on view layers", check => {
let builder = new UIBuilder(testgame.view, "tl1");
builder.group("tg1");
checkcomp(["View layers", "tl1", 0], Phaser.Group, "tg1");
builder.container("tg1");
checkcomp(["View layers", "tl1", 0], UIContainer, "tg1");
builder = new UIBuilder(testgame.view, "tl2");
builder.group("tg2");
checkcomp(["View layers", "tl2", 0], Phaser.Group, "tg2");
builder.container("tg2");
checkcomp(["View layers", "tl2", 0], UIContainer, "tg2");
builder = new UIBuilder(testgame.view, "tl1");
builder.group("tg3");
checkcomp(["View layers", "tl1", 0], Phaser.Group, "tg1");
checkcomp(["View layers", "tl1", 1], Phaser.Group, "tg3");
builder.container("tg3");
checkcomp(["View layers", "tl1", 0], UIContainer, "tg1");
checkcomp(["View layers", "tl1", 1], UIContainer, "tg3");
builder = new UIBuilder(testgame.view);
builder.group("tg4");
checkcomp(["View layers", "base", 0], Phaser.Group, "tg4");
builder.container("tg4");
checkcomp(["View layers", "base", 0], UIContainer, "tg4");
builder = new UIBuilder(testgame.view);
builder.group("tg5");
checkcomp(["View layers", "base", 0], Phaser.Group, "tg4");
checkcomp(["View layers", "base", 1], Phaser.Group, "tg5");
builder.container("tg5");
checkcomp(["View layers", "base", 0], UIContainer, "tg4");
checkcomp(["View layers", "base", 1], UIContainer, "tg5");
check.equals(testgame.view.layers.children.map((child: any) => child.name), ["tl1", "tl2", "base"]);
check.equals(testgame.view.layers.list.map((child: any) => child.name), ["tl1", "tl2", "base"]);
})
test.case("creates component inside the parent container", check => {
let builder = new UIBuilder(testgame.view, testgame.view.getLayer("testlayer"));
let group = builder.group("test1");
checkcomp(["View layers", "testlayer", 0], Phaser.Group, "test1");
let group = builder.container("test1");
checkcomp(["View layers", "testlayer", 0], UIContainer, "test1");
builder = new UIBuilder(testgame.view, group);
builder.text("test2");
checkcomp(["View layers", "testlayer", 0, 0], Phaser.Text, "", { text: "test2", parent: group });
checkcomp(["View layers", "testlayer", 0, 0], UIText, "", { text: "test2", parentContainer: group });
builder = new UIBuilder(testgame.view, "anothertestlayer");
builder.text("test3");
checkcomp(["View layers", "anothertestlayer", 0], Phaser.Text, "", { text: "test3" });
checkcomp(["View layers", "anothertestlayer", 0], UIText, "", { text: "test3" });
})
test.case("can clear a container", check => {
let builder = new UIBuilder(testgame.view);
builder.group("group1", 50, 30);
builder.container("group1", 50, 30);
builder.text("text1");
let [spath, container] = get(["View layers", "base"]);
check.equals(container.children.length, 2);
builder.clear();
check.equals(container.children.length, 0);
if (check.instance(container, UIContainer, "is a container")) {
check.equals(container.list.length, 2);
builder.clear();
check.equals(container.list.length, 0);
}
})
test.case("can create groups", check => {
test.case("can create containers", check => {
let builder = new UIBuilder(testgame.view);
builder.group("group1", 50, 30);
checkcomp(["View layers", "base", 0], Phaser.Group, "group1", { x: 50, y: 30 });
builder.container("group1", 50, 30);
checkcomp(["View layers", "base", 0], UIContainer, "group1", { x: 50, y: 30 });
})
test.case("can create texts", check => {
let builder = new UIBuilder(testgame.view);
builder.text("Test content", 12, 41);
checkcomp(["View layers", "base", 0], Phaser.Text, "", { text: "Test content", x: 12, y: 41 });
checktext(["View layers", "base", 0], { text: "Test content", x: 12, y: 41 });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { size: 61 });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { cssFont: "16pt 'SpaceTac'" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { cssFont: "61pt 'SpaceTac'" });
checktext(["View layers", "base", 0], undefined, { fontFamily: "16pt SpaceTac" });
checktext(["View layers", "base", 1], undefined, { fontFamily: "61pt SpaceTac" });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { color: "#252627" });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { fill: "#ffffff" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { fill: "#252627" });
checktext(["View layers", "base", 0], undefined, { color: "#ffffff" });
checktext(["View layers", "base", 1], undefined, { color: "#252627" });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { shadow: true });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { shadowColor: "rgba(0,0,0,0)" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 3, shadowStroke: true });
checktext(["View layers", "base", 0], undefined, { shadowColor: "#000", shadowFill: false, shadowStroke: false });
checktext(["View layers", "base", 1], undefined, { shadowColor: "rgba(0,0,0,0.6)", shadowFill: true, shadowOffsetX: 3, shadowOffsetY: 4, shadowBlur: 3, shadowStroke: true });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { stroke_width: 2, stroke_color: "#ff0000" });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { stroke: "black", strokeThickness: 0 });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { stroke: "#ff0000", strokeThickness: 2 });
checktext(["View layers", "base", 0], undefined, { stroke: "#fff", strokeThickness: 0 });
checktext(["View layers", "base", 1], undefined, { stroke: "#ff0000", strokeThickness: 2 });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { bold: true });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { fontWeight: "normal" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { fontWeight: "bold" });
checktext(["View layers", "base", 0], undefined, { fontFamily: "16pt SpaceTac" });
checktext(["View layers", "base", 1], undefined, { fontFamily: "bold 16pt SpaceTac" });
builder.clear();
builder.text("", 0, 0, {});
builder.text("", 0, 0, { center: false });
builder.text("", 0, 0, { vcenter: false });
builder.text("", 0, 0, { center: false, vcenter: false });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { anchor: new Phaser.Point(0.5, 0.5), align: "center" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { anchor: new Phaser.Point(0, 0.5), align: "left" });
checkcomp(["View layers", "base", 2], Phaser.Text, "", { anchor: new Phaser.Point(0.5, 0), align: "center" });
checkcomp(["View layers", "base", 3], Phaser.Text, "", { anchor: new Phaser.Point(0, 0), align: "left" });
checktext(["View layers", "base", 0], { originX: 0.5, originY: 0.5 }, { align: "center" });
checktext(["View layers", "base", 1], { originX: 0, originY: 0.5 }, { align: "left" });
checktext(["View layers", "base", 2], { originX: 0.5, originY: 0 }, { align: "center" });
checktext(["View layers", "base", 3], { originX: 0, originY: 0 }, { align: "left" });
builder.clear();
builder.text("", 0, 0, { width: 0 });
builder.text("", 0, 0, { width: 1100 });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { wordWrap: false });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { wordWrap: true, wordWrapWidth: 1100 });
checktext(["View layers", "base", 0], undefined, <any>{ wordWrapWidth: null });
checktext(["View layers", "base", 1], undefined, <any>{ wordWrapWidth: 1100 });
})
test.case("can create images", check => {
let builder = new UIBuilder(testgame.view);
builder.image("test-image", 100, 50);
checkcomp(["View layers", "base", 0], Phaser.Image, "test-image", { x: 100, y: 50, key: "__missing", inputEnabled: null });
checkcomp(["View layers", "base", 0], UIImage, "test-image", { x: 100, y: 50 });
check.patch(testgame.view, "getFirstImage", (...images: string[]) => images[1]);
builder.image(["test-image1", "test-image2", "test-image3"]);
checkcomp(["View layers", "base", 1], Phaser.Image, "test-image2");
checkcomp(["View layers", "base", 1], UIImage, "test-image2");
})
test.case("can create buttons", check => {
let builder = new UIBuilder(testgame.view);
let a = 1;
let button1 = builder.button("test-image1", 100, 50, () => a += 1);
checkcomp(["View layers", "base", 0], Phaser.Button, "test-image1", { x: 100, y: 50, key: "__missing", inputEnabled: true });
check.same(button1.input.useHandCursor, true, "button1 should use hand cursor");
checkcomp(["View layers", "base", 0], UIButton, "test-image1", { x: 100, y: 50 });
let button2 = builder.button("test-image2", 20, 10);
checkcomp(["View layers", "base", 1], Phaser.Button, "test-image2", { x: 20, y: 10, key: "__missing", inputEnabled: true });
check.same(button2.input.useHandCursor, false, "button2 should not use hand cursor");
checkcomp(["View layers", "base", 1], UIButton, "test-image2", { x: 20, y: 10 });
check.equals(a, 1);
testClick(button1);
@ -169,81 +187,18 @@ module TK.SpaceTac.UI.Specs {
check.equals(a, 3);
})
test.case("can create toggle buttons", check => {
let builder = new UIBuilder(testgame.view);
let mock = check.mockfunc("identity", (x: any) => x);
let button1 = builder.button("test-image1", 0, 0, undefined, undefined, <any>mock.func);
let button2 = builder.button("test-image2");
let result = builder.switch(button2, true);
check.equals(result, false, "button2");
check.called(mock, 0);
testClick(button2);
check.called(mock, 0);
check.in("button1 on", check => {
result = builder.switch(button1, true);
check.equals(result, true);
check.called(mock, [[true]]);
});
check.in("button1 off", check => {
result = builder.switch(button1, false);
check.equals(result, false);
check.called(mock, [[false]]);
});
check.in("button1 first click", check => {
testClick(button1);
check.called(mock, [[true]]);
});
check.in("button1 second click", check => {
testClick(button1);
check.called(mock, [[false]]);
});
});
test.case("can create shaders", check => {
let builder = new UIBuilder(testgame.view);
let shader1 = builder.shader("test-shader-1", "test-image-1");
check.equals(shader1 instanceof Phaser.Image, true);
check.equals(shader1.name, "test-image-1");
check.same(shader1.filters.length, 1, "one filter set on shader1");
let shader2 = builder.shader("test-shader-2", { width: 500, height: 300 });
check.equals(shader2 instanceof Phaser.Image, true);
/*check.equals(shader2.width, 500);
check.equals(shader2.height, 300);*/ // FIXME randomly fail on karma
check.same(shader2.filters.length, 1, "one filter set on shader2");
let i = 0;
let shader3 = builder.shader("test-shader-3", "test-image-3", 50, 30, () => { return { a: i++, b: { x: 1, y: 2 } } });
check.equals(shader3.x, 50);
check.equals(shader3.y, 30);
check.equals(shader3.filters[0].uniforms["a"], { type: '1f', value: 0 }, "uniform a initial");
check.equals(shader3.filters[0].uniforms["b"], { type: '2f', value: Object({ x: 1, y: 2 }) }, "uniform b initial");
shader3.update();
check.equals(shader3.filters[0].uniforms["a"], { type: '1f', value: 1 }, "uniform a updated");
check.equals(shader3.filters[0].uniforms["b"], { type: '2f', value: Object({ x: 1, y: 2 }) }, "uniform b updated");
check.same(testgame.view.getLayer("base").children.length, 3, "view layer should have three children");
})
test.case("creates sub-builders, preserving text style", check => {
let base_style = new UITextStyle();
base_style.width = 123;
let builder = new UIBuilder(testgame.view, undefined, base_style);
builder.text("Test 1");
let group = builder.group("testgroup");
let group = builder.container("testgroup");
let subbuilder = builder.in(group);
subbuilder.text("Test 2");
checkcomp(["View layers", "base", 0], Phaser.Text, "", { text: "Test 1", wordWrapWidth: 123 });
checkcomp(["View layers", "base", 1, 0], Phaser.Text, "", { text: "Test 2", wordWrapWidth: 123 });
checktext(["View layers", "base", 0], { text: "Test 1" }, <any>{ wordWrapWidth: 123 });
checktext(["View layers", "base", 1, 0], { text: "Test 2" }, <any>{ wordWrapWidth: 123 });
})
test.case("allows to alter text style", check => {
@ -253,38 +208,34 @@ module TK.SpaceTac.UI.Specs {
builder.text("t3");
builder.text("t4", undefined, undefined, { bold: true });
checkcomp(["View layers", "base", 0], Phaser.Text, "", { text: "t1", fontWeight: "normal" });
checkcomp(["View layers", "base", 1], Phaser.Text, "", { text: "t2", fontWeight: "bold" });
checkcomp(["View layers", "base", 2], Phaser.Text, "", { text: "t3", fontWeight: "normal" });
checkcomp(["View layers", "base", 3], Phaser.Text, "", { text: "t4", fontWeight: "bold" });
checktext(["View layers", "base", 0], { text: "t1" }, { fontFamily: "16pt SpaceTac" });
checktext(["View layers", "base", 1], { text: "t2" }, { fontFamily: "bold 16pt SpaceTac" });
checktext(["View layers", "base", 2], { text: "t3" }, { fontFamily: "16pt SpaceTac" });
checktext(["View layers", "base", 3], { text: "t4" }, { fontFamily: "bold 16pt SpaceTac" });
})
test.case("allows to change text, image or button content", check => {
test.case("allows to change text or image content", check => {
let builder = new UIBuilder(testgame.view);
let text = builder.text("test-text");
let image = builder.image("test-image");
let button = builder.button("test-button");
checkcomp(["View layers", "base", 0], Phaser.Text, "", { text: "test-text" });
checkcomp(["View layers", "base", 1], Phaser.Image, "test-image");
checkcomp(["View layers", "base", 2], Phaser.Button, "test-button");
checkcomp(["View layers", "base", 0], UIText, "", { text: "test-text" });
checkcomp(["View layers", "base", 1], UIImage, "test-image");
builder.change(text, "test-mod-text");
builder.change(image, "test-mod-image");
builder.change(button, "test-mod-button");
checkcomp(["View layers", "base", 0], Phaser.Text, "", { text: "test-mod-text" });
checkcomp(["View layers", "base", 1], Phaser.Image, "test-mod-image");
checkcomp(["View layers", "base", 2], Phaser.Button, "test-mod-button");
checkcomp(["View layers", "base", 0], UIText, "", { text: "test-mod-text" });
checkcomp(["View layers", "base", 1], UIImage, "test-mod-image");
})
test.case("distributes children along an axis", check => {
let builder = new UIBuilder(testgame.view);
builder = builder.in(builder.group("test"));
builder = builder.in(builder.container("test"));
let c1 = builder.text("");
let c2 = builder.button("test");
let c3 = builder.group("test");
let c3 = builder.container("test");
check.equals(c1.x, 0);
check.equals(c1.y, 0);
@ -293,7 +244,7 @@ module TK.SpaceTac.UI.Specs {
check.equals(c3.x, 0);
check.equals(c3.y, 0);
check.patch(UITools, "getScreenBounds", (obj: any) => {
check.patch(UITools, "getBounds", (obj: any) => {
if (obj === c1) {
return { x: 0, y: 0, width: 100, height: 51 };
} else if (obj === c2) {

View File

@ -2,11 +2,8 @@
* Main way to create UI components
*/
module TK.SpaceTac.UI {
export type UIText = Phaser.Text
export type UIImage = Phaser.Image
export type UIButton = Phaser.Button
export type UIGroup = Phaser.Group
export type UIContainer = Phaser.Group | Phaser.Image
export type UIParticles = Phaser.GameObjects.Particles.ParticleEmitterManager
export type UIBuilderParent = UIImage | UIContainer
export type ShaderValue = number | { x: number, y: number }
export type UIOnOffCallback = (on: boolean) => boolean
@ -54,49 +51,16 @@ module TK.SpaceTac.UI {
width = 0
}
/**
* Button options
*/
export type UIButtonOptions = {
// Centering
center?: boolean
// Name of the hover picture (by default, the button name, with "-hover" appended)
hover_name?: string
// Name of the "on" picture (by default, the button name, with "-on" appended)
on_name?: string
// Whether "hover" picture should stay near the button (otherwise will be on top)
hover_bottom?: boolean
// Whether "on" picture should stay near the button (otherwise will be on top)
on_bottom?: boolean
// Text content
text?: string
text_x?: number
text_y?: number
// Text content style override
text_style?: UITextStyleI
// Icon content
icon?: string
icon_x?: number
icon_y?: number
}
/**
* Main UI builder tool
*/
export class UIBuilder {
view: BaseView
private game: MainUI
private parent: UIContainer
private text_style: UITextStyle
private parent: UIBuilderParent
private text_style: UITextStyleI
constructor(view: BaseView, parent: UIContainer | string = "base", text_style = new UITextStyle) {
constructor(view: BaseView, parent: UIBuilderParent | string = "base", text_style: UITextStyleI = new UITextStyle) {
this.view = view;
this.game = view.gameui;
if (typeof parent == "string") {
@ -112,7 +76,7 @@ module TK.SpaceTac.UI {
*
* This new builder will inherit the style settings, and will create components in the specified parent
*/
in(container: UIContainer | string, body?: (builder: UIBuilder) => void): UIBuilder {
in(container: UIBuilderParent | string, body?: (builder: UIBuilder) => void): UIBuilder {
let result = new UIBuilder(this.view, container, this.text_style);
if (body) {
body(result);
@ -135,35 +99,39 @@ module TK.SpaceTac.UI {
* Clear the current container of all component
*/
clear(): void {
destroyChildren(this.parent);
if (this.parent instanceof UIImage) {
console.error("Cannot clear an image parent, use groups instead");
} else {
this.parent.removeAll(true);
}
}
/**
* Internal method to add to the parent
*/
private add(child: UIText | UIImage | UIButton | UIContainer): void {
if (this.parent instanceof Phaser.Group) {
this.parent.add(child);
} else if (this.parent instanceof Phaser.Button) {
// Protect the "on" and "hover" layers
let layer = first(this.parent.children, child => child instanceof Phaser.Image && (child.name == "*on*" || child.name == "*hover*"));
if (layer) {
this.parent.addChildAt(child, this.parent.getChildIndex(layer));
private add(child: UIText | UIImage | UIButton | UIContainer | UIGraphics): void {
if (this.parent instanceof UIImage) {
let gparent = this.parent.parentContainer;
if (gparent) {
let x = this.parent.x + child.x;
let y = this.parent.y + child.y;
child.setPosition(x, y);
gparent.add(child);
} else {
this.parent.addChild(child);
throw new Error("no parent container");
}
} else {
this.parent.addChild(child);
this.parent.add(child);
}
}
/**
* Add a group of components
* Add a container of other components
*/
group(name: string, x = 0, y = 0, visible = true): UIGroup {
let result = new Phaser.Group(this.game, undefined, name);
result.position.set(x, y);
result.visible = visible;
container(name: string, x = 0, y = 0, visible = true): UIContainer {
let result = new UIContainer(this.view, x, y);
result.setName(name);
result.setVisible(visible);
this.add(result);
return result;
}
@ -175,22 +143,20 @@ module TK.SpaceTac.UI {
*/
text(content: string, x = 0, y = 0, style_changes: UITextStyleI = {}): UIText {
let style = merge(this.text_style, style_changes);
let result = new Phaser.Text(this.game, x, y, content, {
font: `${style.bold ? "bold " : ""}${style.size}pt SpaceTac`,
let result = new UIText(this.view, x, y, content, {
fill: style.color,
align: style.center ? "center" : "left"
});
result.anchor.set(style.center ? 0.5 : 0, style.vcenter ? 0.5 : 0);
result.setFont(`${style.bold ? "bold " : ""}${style.size}pt SpaceTac`);
result.setOrigin(style.center ? 0.5 : 0, style.vcenter ? 0.5 : 0);
if (style.width) {
result.wordWrap = true;
result.wordWrapWidth = style.width;
result.setWordWrapWidth(style.width);
}
if (style.shadow) {
result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3);
result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3, true, true);
}
if (style.stroke_width) {
result.stroke = style.stroke_color;
result.strokeThickness = style.stroke_width;
if (style.stroke_width && style.stroke_color) {
result.setStroke(style.stroke_color, style.stroke_width);
}
this.add(result);
return result;
@ -205,10 +171,10 @@ module TK.SpaceTac.UI {
}
let info = this.view.getImageInfo(name);
let result = this.game.add.image(x, y, info.key, info.frame);
let result = new UIImage(this.view, x, y, info.key, info.frame);
result.name = name;
if (centered) {
result.anchor.set(0.5);
if (!centered) {
result.setOrigin(0);
}
this.add(result);
return result;
@ -220,89 +186,8 @@ module TK.SpaceTac.UI {
* If an image with "-hover" suffix is found in atlases, it will be used as hover mask (added as button child)
*/
button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}): UIButton {
let info = this.view.getImageInfo(name);
let result = new Phaser.Button(this.game, x, y, info.key, undefined, null, info.frame, info.frame);
result.name = name;
if (options.center) {
result.anchor.set(0.5);
}
let clickable = bool(onclick);
result.input.useHandCursor = clickable;
if (clickable) {
UIComponent.setButtonSound(result);
}
let onstatus = false;
if (clickable || tooltip || onoffcallback) {
// On mask
let on_mask: Phaser.Image | null = null;
if (onoffcallback) {
let on_info = this.view.getImageInfo(options.on_name || (name + "-on"));
if (on_info.exists) {
on_mask = new Phaser.Image(this.game, 0, 0, on_info.key, on_info.frame);
on_mask.name = options.on_bottom ? "on" : "*on*";
on_mask.visible = false;
result.addChild(on_mask);
}
// TODO Find a better way to handle this (extend Button ?)
result.data.onoffcallback = (on: boolean): boolean => {
onstatus = onoffcallback(on);
if (on_mask) {
on_mask.anchor.set(result.anchor.x, result.anchor.y);
this.view.animations.setVisible(on_mask, onstatus, 100);
}
return onstatus;
}
}
// Hover mask
let hover_info = this.view.getImageInfo(options.hover_name || (name + "-hover"));
let hover_mask: Phaser.Image | null = null;
if (hover_info.exists) {
hover_mask = new Phaser.Image(this.game, 0, 0, hover_info.key, hover_info.frame);
hover_mask.name = options.hover_bottom ? "hover" : "*hover*";
hover_mask.visible = false;
result.addChild(hover_mask);
}
this.view.inputs.setHoverClick(result,
() => {
if (tooltip) {
this.view.tooltip.show(result, tooltip);
}
if (hover_mask) {
hover_mask.anchor.set(result.anchor.x, result.anchor.y);
this.view.animations.show(hover_mask, 100);
}
},
() => {
if (tooltip) {
this.view.tooltip.hide();
}
if (hover_mask) {
this.view.animations.hide(hover_mask, 100)
}
},
() => {
if (onclick) {
onclick();
} else if (onoffcallback) {
this.switch(result, !onstatus);
}
}, 100);
}
if (options.text) {
this.in(result).text(options.text, options.text_x || 0, options.text_y || 0, options.text_style);
}
if (options.icon) {
this.in(result).image(options.icon, options.icon_x || 0, options.icon_y || 0, options.center);
}
options.text_style = merge(this.text_style, options.text_style || {});
let result = new UIButton(this.view, name, x, y, onclick, tooltip, onoffcallback, options);
this.add(result);
return result;
}
@ -317,101 +202,61 @@ module TK.SpaceTac.UI {
}
/**
* Add a fragment shader area, with optional fallback image
* Add a graphics (for drawing)
*/
shader(name: string, base: string | { width: number, height: number }, x = 0, y = 0, updater?: () => { [name: string]: ShaderValue }): UIImage {
let source = this.game.cache.getShader(name);
source = "" + source;
let uniforms: any = {};
if (updater) {
iteritems(updater(), (key, value) => {
uniforms[key] = { type: (typeof value == "number") ? "1f" : "2f", value: value };
});
}
let filter = new Phaser.Filter(this.game, uniforms, source);
let result: Phaser.Image;
if (typeof base == "string") {
result = this.image(base, x, y);
result.filters = [filter];
filter.setResolution(result.width, result.height);
} else {
result = filter.addToWorld(x, y, base.width, base.height);
this.add(result);
}
if (updater) {
result.update = () => {
iteritems(updater(), (key, value) => filter.uniforms[key].value = value);
filter.update();
}
}
filter.update();
graphics(name: string, x = 0, y = 0, visible = true): UIGraphics {
let result = new UIGraphics(this.view, name, visible, x, y);
this.add(result);
return result;
}
/**
* Emit a bunch of particles
*/
particles(config: ParticlesConfig): void {
this.view.particles.emit(config, this.parent instanceof UIContainer ? this.parent : undefined);
}
/**
* Change the content of an component
*
* If the component is a text, its content will be changed.
* If the component is an image or button, its texture will be changed.
* If the component is an image, its texture will be changed.
*/
change(component: UIImage | UIButton | UIText, content: string): void {
if (component instanceof Phaser.Text) {
component.text = content;
change(component: UIImage | UIText, content: string): void {
// TODO Should be moved custom UIImage and UIText classes
if (component instanceof UIText) {
component.setText(content);
} else {
let info = this.view.getImageInfo(content);
component.name = content;
if (component instanceof Phaser.Button) {
component.loadTexture(info.key);
component.setFrames(info.frame, info.frame);
} else {
component.loadTexture(info.key, info.frame);
}
component.setName(content);
component.setTexture(info.key, info.frame);
}
}
/**
* Change the status on/off on a button
*
* Return the final effective status
*/
switch(button: UIButton, on: boolean): boolean {
if (button.data.onoffcallback) {
return button.data.onoffcallback(on);
} else {
return false;
}
}
/**
* Select a single button inside the container, toggle its "on" status, and toggle all other button to "off"
*
* This is the equivalent of radio buttons
*/
select(button: UIButton): void {
this.parent.children.forEach(child => {
if (child instanceof Phaser.Button && child.data.onoffcallback && child !== button) {
child.data.onoffcallback(false);
}
});
this.switch(button, true);
}
/**
* Evenly distribute the children of this builder along an axis
*/
distribute(along: "x" | "y", start: number, end: number): void {
let sizes = this.parent.children.map(child => {
if (child instanceof Phaser.Image || child instanceof Phaser.Sprite || child instanceof Phaser.Group) {
return UITools.getScreenBounds(child)[along == "x" ? "width" : "height"];
if (!(this.parent instanceof UIContainer)) {
throw new Error("UIBuilder.distribute only works on groups");
}
let children = this.parent.list;
let sizes = children.map(child => {
if (UITools.isSpatial(child)) {
return UITools.getBounds(child)[along == "x" ? "width" : "height"];
} else {
return 0;
}
});
let spacing = ((end - start) - sum(sizes)) / (sizes.length + 1);
let offset = start;
this.parent.children.forEach((child, idx) => {
children.forEach((child, idx) => {
offset += spacing;
child[along] = Math.round(offset);
if (UITools.isSpatial(child)) {
child[along] = Math.round(offset);
}
offset += sizes[idx];
});
}

View File

@ -0,0 +1,115 @@
module TK.SpaceTac.UI.Specs {
testing("UIButton", test => {
let testgame = setupEmptyView(test);
test.case("handles placement of masks", check => {
check.patch(testgame.view, "getImageInfo", (name: string) => ({ key: "test", frame: 0, exists: true }));
let builder = new UIBuilder(testgame.view);
check.in("both on top", check => {
let button = builder.button("button", 0, 0, nop, undefined, identity);
check.equals(button.length, 3);
builder.in(button).text("Test");
check.equals(button.length, 4);
check.equals(button.list[0].name, "button");
check.equals(button.list[1].type, "Text");
check.equals(button.list[2].name, "button-on");
check.equals(button.list[3].name, "button-hover");
});
check.in("hover at bottom", check => {
let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true });
check.equals(button.length, 3);
builder.in(button).text("Test");
check.equals(button.length, 4);
check.equals(button.list[0].name, "button");
check.equals(button.list[1].name, "button-hover");
check.equals(button.list[2].type, "Text");
check.equals(button.list[3].name, "button-on");
});
check.in("'on' at bottom", check => {
let button = builder.button("button", 0, 0, nop, undefined, identity, { on_bottom: true });
check.equals(button.length, 3);
builder.in(button).text("Test");
check.equals(button.length, 4);
check.equals(button.list[0].name, "button");
check.equals(button.list[1].name, "button-on");
check.equals(button.list[2].type, "Text");
check.equals(button.list[3].name, "button-hover");
});
check.in("both at bottom", check => {
let button = builder.button("button", 0, 0, nop, undefined, identity, { hover_bottom: true, on_bottom: true });
check.equals(button.length, 3);
builder.in(button).text("Test");
check.equals(button.length, 4);
check.equals(button.list[0].name, "button");
check.equals(button.list[1].name, "button-on");
check.equals(button.list[2].name, "button-hover");
check.equals(button.list[3].type, "Text");
});
});
test.case("toggles on/off", check => {
let builder = new UIBuilder(testgame.view);
let m1 = check.mockfunc("m1", (on: boolean) => on);
let button1 = builder.button("b1", 0, 0, undefined, undefined, m1.func);
let m2 = check.mockfunc("m1", (on: boolean) => on);
let button2 = builder.button("b2", 0, 0, undefined, undefined, m2.func);
let m3 = check.mockfunc("m1", (on: boolean) => on);
let button3 = builder.button("b3", 0, 0, undefined, undefined, m3.func);
function verify(message: string, state1: boolean, state2: boolean, state3: boolean, called1: number, called2: number, called3: number) {
check.in(message, check => {
check.equals(button1.getState(), state1, "button1 state");
check.equals(button2.getState(), state2, "button2 state");
check.equals(button3.getState(), state3, "button3 state");
check.called(m1, called1);
check.called(m2, called2);
check.called(m3, called3);
});
}
verify("initial", false, false, false, 0, 0, 0);
button1.toggle(true);
verify("toggle on", true, false, false, 1, 0, 0);
button1.toggle(true);
verify("toggle on again", true, false, false, 0, 0, 0);
button1.toggle(false);
verify("toggle off", false, false, false, 1, 0, 0);
button1.toggle(false);
verify("toggle off again", false, false, false, 0, 0, 0);
button2.toggle(true, UIButtonUnicity.EXCLUSIVE);
verify("toggle on unicity - first", false, true, false, 0, 1, 0);
button2.toggle(true, UIButtonUnicity.EXCLUSIVE);
verify("toggle on unicity - first again", false, true, false, 0, 0, 0);
button3.toggle(true, UIButtonUnicity.EXCLUSIVE);
verify("toggle on unicity - second", false, false, true, 0, 1, 1);
button2.toggle(false, UIButtonUnicity.EXCLUSIVE);
verify("toggle off unicity - other", false, false, true, 0, 0, 0);
button3.toggle(false, UIButtonUnicity.EXCLUSIVE);
verify("toggle off unicity - currently on", false, false, false, 0, 0, 1);
button1.toggle(true);
button2.toggle(true);
button3.toggle(true);
verify("toggle all on", true, true, true, 1, 1, 1);
button2.toggle(true, UIButtonUnicity.EXCLUSIVE);
verify("toggle on unicity should shut down 2 others", false, true, false, 1, 0, 1);
button2.toggle(true, UIButtonUnicity.EXCLUSIVE_MIN);
verify("toggle off unicity min", false, true, false, 0, 0, 0);
});
});
}

211
src/ui/common/UIButton.ts Normal file
View File

@ -0,0 +1,211 @@
/// <reference path="UIContainer.ts" />
module TK.SpaceTac.UI {
/**
* Button options
*/
export type UIButtonOptions = {
// Centering
center?: boolean
// Name of the hover picture (by default, the button name, with "-hover" appended)
hover_name?: string
// Name of the "on" picture (by default, the button name, with "-on" appended)
on_name?: string
// Whether "hover" picture should stay near the button (otherwise will be on top)
hover_bottom?: boolean
// Whether "on" picture should stay near the button (otherwise will be on top)
on_bottom?: boolean
// Text content
text?: string
text_x?: number
text_y?: number
// Text content style override
text_style?: UITextStyleI
// Icon content
icon?: string
icon_x?: number
icon_y?: number
// Unicity setting to control other buttons in the same container
unicity?: UIButtonUnicity
}
/**
* When toggling a button status, this describes the behavior of other buttons in the same container
*/
export enum UIButtonUnicity {
// Do nothing to other buttons
NONE = 0,
// Shut down other buttons when one is toggled on
EXCLUSIVE = 1,
// Shut down other buttons when one is toggled on, but prevent to shut down the one currently on
EXCLUSIVE_MIN = 2
}
/**
* Button for UI, with support for hover, click, and on/off state
*/
export class UIButton extends UIContainer {
private base: UIImage
private state_on = false
readonly state_changer?: Function
private hover_mask?: UIImage
private hover_bottom = false
private on_mask?: UIImage
private on_bottom = false
private constructed = false
constructor(private view: BaseView, key: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}) {
super(view, x, y);
this.setName(key);
let builder = new UIBuilder(view, this, options.text_style);
let base = builder.image(key, 0, 0, options.center);
this.add(base);
this.base = base;
let clickable = bool(onclick || onoffcallback);
let interactive = bool(clickable || tooltip);
if (interactive) {
this.setInteractive(new Phaser.Geom.Rectangle(
options.center ? 0 : base.width / 2,
options.center ? 0 : base.height / 2,
base.width,
base.height
), (rect: Phaser.Geom.Rectangle, x: number, y: number) => Phaser.Geom.Rectangle.Contains(rect, x, y) && UITools.isVisible(this));
// On mask
if (onoffcallback) {
let on_name = options.on_name || (key + "-on");
let on_info = view.getImageInfo(on_name);
if (on_info.exists) {
this.on_mask = builder.image(on_name, 0, 0, options.center);
this.on_mask.setVisible(false);
this.on_bottom = bool(options.on_bottom);
}
this.state_changer = (on: boolean): boolean => {
this.state_on = onoffcallback(on);
if (this.on_mask) {
view.animations.setVisible(this.on_mask, this.state_on, 100);
}
return this.state_on;
}
}
// Hover mask
let hover_name = options.hover_name || (key + "-hover");
let hover_info = view.getImageInfo(hover_name);
if (hover_info.exists) {
this.hover_mask = builder.image(hover_name, 0, 0, options.center);
this.hover_mask.setVisible(false);
this.hover_bottom = bool(options.hover_bottom);
if (this.hover_bottom && !this.on_bottom) {
this.moveDown(this.hover_mask);
}
}
view.inputs.setHoverClick(this,
() => {
if (tooltip) {
view.tooltip.show(this, tooltip);
}
if (this.hover_mask) {
view.animations.show(this.hover_mask, 100);
}
},
() => {
if (tooltip) {
view.tooltip.hide();
}
if (this.hover_mask) {
view.animations.hide(this.hover_mask, 100)
}
},
() => {
if (clickable && onclick) {
onclick();
} else if (onoffcallback) {
this.toggle(!this.state_on, options.unicity);
}
}, 100, undefined, clickable);
}
if (options.text) {
builder.text(options.text, options.text_x || 0, options.text_y || 0, options.text_style);
}
if (options.icon) {
builder.image(options.icon, options.icon_x || 0, options.icon_y || 0, options.center);
}
this.constructed = true;
}
add(child: UIImage | UIText): UIButton {
if (this.constructed) {
// Protect the "on" and "hover" layers
let layer = first(this.list, child => (!this.hover_bottom && child == this.hover_mask) || (!this.on_bottom && child == this.on_mask));
if (layer) {
super.addAt(child, this.getIndex(layer));
} else {
super.add(child);
}
} else {
super.add(child);
}
return this;
}
get width(): number {
return this.base.width;
}
get height(): number {
return this.base.height;
}
/**
* Get the state on/off
*/
getState(): boolean {
return this.state_on;
}
/**
* Change the base texture
*/
setBaseImage(key: string): void {
this.view.changeImage(this.base, key);
this.setName(key);
}
/**
* Select this button status
*
* Returns the final state of this button
*/
toggle(on: boolean, unicity?: UIButtonUnicity): boolean {
if (on && unicity && this.parentContainer) {
this.parentContainer.list.forEach(child => {
if (child instanceof UIButton && child != this) {
child.toggle(false);
}
});
}
if (this.state_changer && (on || unicity != UIButtonUnicity.EXCLUSIVE_MIN) && on != this.state_on) {
this.state_changer(on);
}
return this.state_on;
}
}
}

View File

@ -5,7 +5,7 @@ module TK.SpaceTac.UI.Specs {
test.case("controls visibility", check => {
let component = new UIComponent(testgame.view, 50, 50);
let container = <Phaser.Group>(<any>component).container;
let container = <UIContainer>(<any>component).container;
check.equals(container.visible, true);
component.setVisible(false);
@ -17,7 +17,7 @@ module TK.SpaceTac.UI.Specs {
// with transition
component.setVisible(false, 500);
check.equals(container.visible, true);
check.equals(testgame.view.animations.simulate(container, 'alpha'), [1, 0.5, 0]);
check.equals(testgame.view.animations.simulate(container, 'alpha'), [1, 0.75, 0.5, 0.25, 0]);
});
test.case("sets position inside parent", check => {

View File

@ -1,40 +1,26 @@
module TK.SpaceTac.UI {
export type UIInternalComponent = Phaser.Group | Phaser.Image | Phaser.Button | Phaser.Sprite | Phaser.Graphics;
/**
* Union of all UI components types
*/
export type UIComponentT = UIContainer | UIImage | UIButton | UIGraphics | UIText;
export type UIImageInfo = string | { key: string, frame?: number, frame1?: number, frame2?: number };
export type UITextInfo = { content: string, color: string, size: number, bold?: boolean };
function imageFromInfo(game: Phaser.Game, info: UIImageInfo): Phaser.Image {
if (typeof info === "string") {
info = { key: info };
}
let image = new Phaser.Image(game, 0, 0, info.key, info.frame);
image.anchor.set(0.5, 0.5);
return image;
}
function textFromInfo(game: Phaser.Game, info: UITextInfo): Phaser.Text {
let style = { font: `${info.bold ? "bold " : ""}${info.size}pt SpaceTac`, fill: info.color };
let text = new Phaser.Text(game, 0, 0, info.content, style);
return text;
}
function autoFromInfo(game: Phaser.Game, info: UIImageInfo | UITextInfo): Phaser.Text | Phaser.Image {
if (info.hasOwnProperty("content")) {
return textFromInfo(game, <UITextInfo>info);
} else {
return imageFromInfo(game, <UIImageInfo>info);
}
/**
* Interface to add a component to a group
*/
export interface UIGroupableI {
addToGroup(group: UIContainer): void;
}
/**
* Base class for UI components
*
* DEPRECATED - Use UIBuilder instead
*/
export class UIComponent {
private background: Phaser.Image | Phaser.Graphics | null
private background: UIImage | UIGraphics | null
protected readonly view: BaseView
protected readonly parent: UIComponent | null
private readonly container: Phaser.Group
readonly container: UIContainer
protected readonly width: number
protected readonly height: number
protected readonly builder: UIBuilder
@ -57,6 +43,7 @@ module TK.SpaceTac.UI {
} else {
this.view.add.existing(this.container);
}
this.container.setSize(width, height);
this.builder = new UIBuilder(this.view, this.container);
if (background_key) {
@ -86,46 +73,43 @@ module TK.SpaceTac.UI {
this.background.destroy();
}
this.background = this.addInternalChild(new Phaser.Graphics(this.game, 0, 0));
if (border_width) {
this.background.lineStyle(border_width, border);
}
this.background.beginFill(fill, alpha);
this.background.drawRect(0, 0, this.width, this.height);
this.background.endFill();
let rect = new Phaser.Geom.Rectangle(0, 0, this.width, this.height);
this.background = this.addInternalChild(new UIGraphics(this.view, "background"));
this.background.addRectangle(rect, fill, border_width, border, alpha);
if (mouse_capture) {
this.background.inputEnabled = true;
this.background.input.useHandCursor = true;
this.background.events.onInputUp.add(() => mouse_capture());
this.background.setInteractive(rect, Phaser.Geom.Rectangle.Contains);
this.background.on("pointerup", () => mouse_capture());
}
}
/**
* Move the a parent's layer
*/
moveToLayer(layer: Phaser.Group) {
moveToLayer(layer: UIContainer) {
layer.add(this.container);
}
/**
* Destroy the component
*/
destroy(children = true) {
this.container.destroy(children);
destroy() {
this.container.destroy();
}
/**
* Create the internal phaser node
*/
protected createInternalNode(): Phaser.Group {
return new Phaser.Group(this.view.game, undefined, classname(this));
protected createInternalNode(): UIContainer {
let result = new UIContainer(this.view);
result.setName(classname(this));
return result;
}
/**
* Add an other internal component as child
*/
protected addInternalChild<T extends UIInternalComponent>(child: T): T {
protected addInternalChild<T extends UIComponentT>(child: T): T {
this.container.add(child);
return child;
}
@ -171,7 +155,7 @@ module TK.SpaceTac.UI {
* Set the position in pixels.
*/
setPosition(x: number, y: number): void {
this.container.position.set(x, y);
this.container.setPosition(x, y);
}
/**
@ -187,9 +171,9 @@ module TK.SpaceTac.UI {
let rx = (pwidth - width) * x;
let ry = (pheight - height) * y;
if (pixelsnap) {
this.container.position.set(Math.round(rx), Math.round(ry));
this.container.setPosition(Math.round(rx), Math.round(ry));
} else {
this.container.position.set(rx, ry);
this.container.setPosition(rx, ry);
}
}
@ -198,28 +182,18 @@ module TK.SpaceTac.UI {
*/
clearContent(): void {
let offset = this.background ? 1 : 0;
while (this.container.children.length > offset) {
this.container.remove(this.container.children[offset], true);
while (this.container.list.length > offset) {
this.container.remove(this.container.list[offset], true);
}
}
/**
* Set the standard sounds on a button
*/
static setButtonSound(button: Phaser.Button): void {
button.setDownSound(new Phaser.Sound(button.game, "ui-button-down"));
button.setUpSound(new Phaser.Sound(button.game, "ui-button-up"));
}
/**
* Add a button in the component, positioning its center.
*
* DEPRECATED - Use UIBuilder directly
*/
addButton(x: number, y: number, on_click: Function, background: string, tooltip = ""): Phaser.Button {
let result = this.builder.button(background, x, y, on_click, tooltip);
result.anchor.set(0.5);
return result;
addButton(x: number, y: number, on_click: Function, background: string, tooltip = ""): UIButton {
return this.builder.button(background, x, y, on_click, tooltip, undefined, { center: true });
}
/**
@ -227,52 +201,19 @@ module TK.SpaceTac.UI {
*
* DEPRECATED - Use UIBuilder directly
*/
addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0, vcenter = center): Phaser.Text {
addText(x: number, y: number, content: string, color = "#ffffff", size = 16, bold = false, center = true, width = 0, vcenter = center): UIText {
return this.builder.text(content, x, y, { color: color, size: size, bold: bold, center: center, width: width, vcenter: vcenter });
}
/**
* Add a static image, positioning its center.
*
* DEPRECATED - Use addImage instead
*/
addImageF(x: number, y: number, key: string, frame = 0, scale = 1): void {
let image = new Phaser.Image(this.container.game, x, y, key, frame);
image.anchor.set(0.5, 0.5);
image.scale.set(scale);
this.addInternalChild(image);
}
/**
* Add a static image, from atlases, positioning its center.
*
* DEPRECATED - Use UIBuilder directly
*/
addImage(x: number, y: number, name: string, scale = 1): Phaser.Image {
let result = this.builder.image(name, x, y);
result.anchor.set(0.5);
result.scale.set(scale);
addImage(x: number, y: number, name: string, scale = 1): UIImage {
let result = this.builder.image(name, x, y, true);
result.setScale(scale);
return result;
}
/**
* Add an animated loader (to indicate a waiting for something).
*/
addLoader(x: number, y: number, scale = 1): Phaser.Image {
let image = new Phaser.Image(this.game, x, y, "common-waiting");
image.anchor.set(0.5, 0.5);
image.scale.set(scale);
image.animations.add("loop").play(3, true);
this.addInternalChild(image);
return image;
}
/**
* Set the keyboard focus on this component.
*/
setKeyboardFocus(on_key: (key: string) => void) {
this.view.inputs.grabKeyboard(this, on_key);
// TODO release on destroy
}
}
}

View File

@ -9,12 +9,12 @@ module TK.SpaceTac.UI {
constructor(view: BaseView, message: string) {
super(view);
this.addText(this.width * 0.5, this.height * 0.3, message, "#90FEE3", 32);
this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#90FEE3", size: 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");
this.content.button("common-button-cancel", this.width * 0.4, this.height * 0.6, () => resolve(false), undefined, undefined, { center: true });
this.content.button("common-button-ok", this.width * 0.6, this.height * 0.6, () => resolve(true), undefined, undefined, { center: true });
});
}

View File

@ -0,0 +1,31 @@
module TK.SpaceTac.UI {
/**
* UI component able to contain other UI components
*/
export class UIContainer extends Phaser.GameObjects.Container {
/**
* Fixed version that does not force (0, 0) to be in bounds
*/
getBounds(output?: Phaser.Geom.Rectangle): Phaser.Geom.Rectangle {
let result: IBounded = { x: 0, y: 0, width: 0, height: 0 };
if (this.list.length > 0) {
var children = this.list;
for (var i = 0; i < children.length; i++) {
var entry = children[i];
if (UITools.isSpatial(entry)) {
result = UITools.unionRects(result, entry.getBounds());
}
}
}
if (typeof output == "undefined") {
output = new Phaser.Geom.Rectangle();
}
output.setTo(result.x, result.y, result.width, result.height);
return output;
}
}
}

View File

@ -56,12 +56,13 @@ module TK.SpaceTac.UI {
width -= offset;
let ioffset = style.padding + Math.floor(style.image_size / 2);
builder.image(style.image, ioffset, ioffset);
builder.image(style.image, ioffset, ioffset, true);
if (style.image_caption) {
let text_size = Math.ceil(style.text.size ? style.text.size * 0.6 : 16);
builder.text(style.image_caption, ioffset, style.padding + style.image_size + text_size, {
size: text_size
size: text_size,
center: true
});
}
}
@ -70,7 +71,7 @@ module TK.SpaceTac.UI {
width: width - style.padding * 2
});
let i = 0;
/*let i = 0;
let colorchar = () => {
text.clearColors();
if (i < message.length) {
@ -79,7 +80,7 @@ module TK.SpaceTac.UI {
this.view.timer.schedule(10, colorchar);
}
}
colorchar();
colorchar();*/
}
}
@ -90,7 +91,7 @@ module TK.SpaceTac.UI {
private step = -1
private on_step: UIConversationCallback
private ended = false
private on_end = new Phaser.Signal()
private on_end = new Phaser.Events.EventEmitter()
constructor(parent: BaseView, on_step: UIConversationCallback) {
super(parent, parent.getWidth(), parent.getHeight());
@ -106,7 +107,7 @@ module TK.SpaceTac.UI {
destroy() {
if (!this.ended) {
this.ended = true;
this.on_end.dispatch();
this.on_end.emit("done");
}
super.destroy();
@ -119,8 +120,8 @@ module TK.SpaceTac.UI {
if (this.ended) {
return Promise.resolve();
} else {
return new Promise((resolve, reject) => {
this.on_end.addOnce(resolve);
return new Promise(resolve => {
this.on_end.on("done", resolve);
});
}
}

View File

@ -4,28 +4,36 @@ module TK.SpaceTac.UI.Specs {
test.case("sets up an overlay", check => {
let view = testgame.view;
check.equals(view.dialogs_layer.children.length, 0);
check.equals(view.dialogs_layer.length, 0, "initial");
let dialog1 = new UIDialog(view, 10, 10, "fake");
check.equals(view.dialogs_layer.children.length, 2);
check.equals(view.dialogs_layer.children[0] instanceof Phaser.Button, true);
checkComponentInLayer(check, view.dialogs_layer, 1, dialog1);
let dialog1 = new UIDialog(view, "fake");
check.in("one dialog", check => {
check.equals(view.dialogs_layer.length, 2);
check.equals(view.dialogs_layer.list[0] instanceof UIImage, true);
check.same(view.dialogs_layer.list[1], dialog1.base);
});
let dialog2 = new UIDialog(view, 10, 10, "fake");
check.equals(view.dialogs_layer.children.length, 3);
check.equals(view.dialogs_layer.children[0] instanceof Phaser.Button, true);
checkComponentInLayer(check, view.dialogs_layer, 1, dialog1);
checkComponentInLayer(check, view.dialogs_layer, 2, dialog2);
let dialog2 = new UIDialog(view, "fake");
check.in("two dialogs", check => {
check.equals(view.dialogs_layer.length, 3);
check.equals(view.dialogs_layer.list[0] instanceof UIImage, true);
check.same(view.dialogs_layer.list[1], dialog1.base);
check.same(view.dialogs_layer.list[2], dialog2.base);
});
dialog1.close();
check.equals(view.dialogs_layer.children.length, 2);
check.equals(view.dialogs_layer.children[0] instanceof Phaser.Button, true);
checkComponentInLayer(check, view.dialogs_layer, 1, dialog2);
check.in("one dialog closed", check => {
check.equals(view.dialogs_layer.length, 2);
check.equals(view.dialogs_layer.list[0] instanceof UIImage, true);
check.same(view.dialogs_layer.list[1], dialog2.base);
});
dialog2.close();
check.equals(view.dialogs_layer.children.length, 0);
check.in("all dialogs closed", check => {
check.equals(view.dialogs_layer.length, 0);
});
});
});
}

View File

@ -1,49 +1,59 @@
/// <reference path="../common/UIComponent.ts" />
module TK.SpaceTac.UI {
/**
* Base class for modal dialogs
*
* When a modal dialog opens, an overlay is displayed behind it to prevent clicking through it
*/
export class UIDialog extends UIComponent {
constructor(parent: BaseView, width = 1495, height = 1080, background = "common-dialog") {
super(parent, width, height, background);
export class UIDialog {
readonly base: UIContainer
readonly content: UIBuilder
readonly width: number
readonly height: number
if (parent.dialogs_opened.length == 0) {
this.addOverlay(parent.dialogs_layer);
constructor(readonly view: BaseView, background_key = "common-dialog") {
if (view.dialogs_opened.length == 0) {
this.addOverlay(view.dialogs_layer);
}
add(parent.dialogs_opened, this);
this.view.audio.playOnce("ui-dialog-open");
let builder = new UIBuilder(view, view.dialogs_layer);
this.base = builder.container("dialog-base");
builder = builder.in(this.base);
this.moveToLayer(parent.dialogs_layer);
this.setPositionInsideParent(0.5, 0.5);
let background = builder.image(background_key);
this.width = background.width;
this.height = background.height;
this.base.setPosition((this.view.getWidth() - this.width) / 2, (this.view.getHeight() - this.height) / 2);
this.content = builder.in(builder.container("content"));
add(view.dialogs_opened, this);
view.audio.playOnce("ui-dialog-open");
}
/**
* Add a control-capturing overlay
* Add an input-capturing overlay
*/
addOverlay(layer: Phaser.Group): void {
let info = this.view.getImageInfo("translucent");
let overlay = layer.game.add.button(0, 0, info.key, () => null, undefined, info.frame, info.frame);
overlay.input.useHandCursor = false;
overlay.scale.set(this.view.getWidth() / overlay.width, this.view.getHeight() / overlay.height);
layer.add(overlay);
addOverlay(layer: UIContainer): void {
let overlay = new UIBuilder(this.view, layer).image("translucent");
overlay.setInteractive();
overlay.setScale(this.view.getWidth() / overlay.width, this.view.getHeight() / overlay.height);
}
/**
* Add a close button
*/
addCloseButton(key = "common-dialog-close", x = 1290, y = 90): void {
this.builder.button(key, x, y, () => this.close(), "Close this dialog");
let builder = new UIBuilder(this.view, this.base);
builder.button(key, x, y, () => this.close(), "Close this dialog");
}
/**
* Close the dialog, removing the overlay if needed
*/
close() {
this.destroy();
this.base.destroy();
this.view.audio.playOnce("ui-dialog-close");

View File

@ -0,0 +1,27 @@
module TK.SpaceTac.UI {
/**
* UI component that supports drawing simple shapes (circles, lines...)
*/
export class UIGraphics extends Phaser.GameObjects.Graphics {
constructor(view: BaseView, name: string, visible = true, x = 0, y = 0) {
super(view, {});
this.setName(name);
this.setVisible(visible);
this.setPosition(x, y);
}
/**
* Add a rectangle
*/
addRectangle(shape: IBounded, color: number, border_width = 0, border_color?: number, alpha = 1): void {
let rect = new Phaser.Geom.Rectangle(shape.x, shape.y, shape.width, shape.height);
this.fillStyle(color, alpha);
this.fillRectShape(rect);
if (border_width && border_color) {
this.lineStyle(border_width, border_color, alpha);
this.strokeRectShape(rect);
}
}
}
}

7
src/ui/common/UIImage.ts Normal file
View File

@ -0,0 +1,7 @@
module TK.SpaceTac.UI {
/**
* UI component to display an image
*/
export class UIImage extends Phaser.GameObjects.Image {
}
}

View File

@ -1,25 +0,0 @@
/// <reference path="UIComponent.ts" />
module TK.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 SpaceTac`, 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;
}
}
}

7
src/ui/common/UIText.ts Normal file
View File

@ -0,0 +1,7 @@
module TK.SpaceTac.UI {
/**
* UI component to display a text
*/
export class UIText extends Phaser.GameObjects.Text {
}
}

View File

@ -9,17 +9,17 @@ module TK.SpaceTac.UI {
constructor(view: BaseView, message: string, initial?: string) {
super(view);
this.addText(this.width * 0.5, this.height * 0.3, message, "#90FEE3", 32);
this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#90FEE3", size: 32 });
let input = new UITextInput(this.builder.styled({ size: 24 }), "menu-input", this.width / 2, this.height / 2, 12);
let input = new UITextInput(this.content.styled({ size: 24 }), "menu-input", this.width / 2, this.height / 2, 12);
if (initial) {
input.setContent(initial);
}
this.result = new Promise((resolve, reject) => {
this.result_resolver = resolve;
this.addButton(this.width * 0.4, this.height * 0.7, () => resolve(null), "common-button-cancel");
this.addButton(this.width * 0.6, this.height * 0.7, () => resolve(input.getContent()), "common-button-ok");
this.content.button("common-button-cancel", this.width * 0.4, this.height * 0.7, () => resolve(null));
this.content.button("common-button-ok", this.width * 0.6, this.height * 0.7, () => resolve(input.getContent()));
});
}

View File

@ -3,21 +3,19 @@ module TK.SpaceTac.UI {
* UI component to allow the user to enter a small text
*/
export class UITextInput {
private content: Phaser.Text
private placeholder: Phaser.Text
private container: UIButton
private content: UIText
private placeholder: UIText
private maxlength: number
constructor(builder: UIBuilder, background: string, x = 0, y = 0, maxlength: number, placeholder = "") {
let input_bg = builder.image(background, x, y, true);
input_bg.inputEnabled = true;
input_bg.input.useHandCursor = true;
input_bg.events.onInputUp.add(() => {
this.container = builder.button(background, x, y, () => {
builder.view.inputs.grabKeyboard(this, key => this.processKey(key));
});
}, undefined, undefined, { center: true });
this.content = builder.in(input_bg).text("");
this.placeholder = builder.in(input_bg).text(placeholder);
this.placeholder.alpha = 0.5;
this.content = builder.in(this.container).text("", 0, 0, { center: true });
this.placeholder = builder.in(this.container).text(placeholder, 0, 0, { center: true });
this.placeholder.setAlpha(0.5);
this.maxlength = maxlength;
}
@ -43,8 +41,8 @@ module TK.SpaceTac.UI {
* Set the current text content
*/
setContent(content: string): void {
this.content.text = content.slice(0, this.maxlength);
this.placeholder.visible = !this.content.text;
this.content.setText(content.slice(0, this.maxlength));
this.placeholder.setVisible(!this.content.text);
}
}
}

View File

@ -4,86 +4,90 @@ module TK.SpaceTac.UI.Specs {
let testgame = setupEmptyView(test);
test.case("destroys children", check => {
let parent = testgame.view.add.group();
let child1 = testgame.view.add.graphics(0, 0, parent);
let child2 = testgame.view.add.image(0, 0, "", 0, parent);
let child3 = testgame.view.add.button(0, 0, "", undefined, undefined, undefined, undefined, undefined, undefined, parent);
let child4 = testgame.view.add.text(0, 0, "", {}, parent);
check.equals(parent.children.length, 4);
let builder = new UIBuilder(testgame.view);
let parent = builder.container("group");
let child1 = builder.in(parent).graphics("graphics");
let child2 = builder.in(parent).image("image");
let child3 = builder.in(parent).button("button");
let child4 = builder.in(parent).text("");
check.equals(parent.length, 4);
destroyChildren(parent, 1, 2);
check.equals(parent.children.length, 2);
check.equals(parent.length, 2);
destroyChildren(parent);
check.equals(parent.children.length, 0);
check.equals(parent.length, 0);
});
test.case("gets the screen boundaries of an object", check => {
let parent = testgame.view.add.group();
/*test.case("gets the screen boundaries of an object", check => {
let builder = new UIBuilder(testgame.view);
let parent = builder.group("group");
check.in("empty", check => {
check.containing(UITools.getScreenBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent");
check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent");
});
let child1 = testgame.view.add.graphics(10, 20, parent);
let child1 = builder.in(parent).graphics("child1");
child1.setPosition(10, 20);
check.in("empty child", check => {
check.containing(UITools.getScreenBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 0, y: 0, width: 0, height: 0 }, "child1");
check.containing(UITools.getBounds(parent), { x: 0, y: 0, width: 0, height: 0 }, "parent");
check.containing(UITools.getBounds(child1), { x: 0, y: 0, width: 0, height: 0 }, "child1");
});
child1.drawRect(20, 30, 40, 45);
child1.addRectangle({ x: 20, y: 30, width: 40, height: 45 }, 0);
check.in("rectangle child", check => {
check.containing(UITools.getScreenBounds(parent), { x: 30, y: 50, width: 40, height: 45 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 30, y: 50, width: 40, height: 45 }, "child1");
check.containing(UITools.getBounds(parent), { x: 30, y: 50, width: 40, height: 45 }, "parent");
check.containing(UITools.getBounds(child1), { x: 30, y: 50, width: 40, height: 45 }, "child1");
});
child1.scale.set(0.5, 0.2);
child1.setScale(0.5, 0.2);
check.in("scaled child", check => {
check.containing(UITools.getScreenBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1");
check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent");
check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1");
});
let child2 = testgame.view.add.graphics(-4, -15);
child1.addChild(child2);
check.in("sub child empty", check => {
check.containing(UITools.getScreenBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1");
check.containing(UITools.getScreenBounds(child2), { x: 0, y: 0, width: 0, height: 0 }, "child2");
check.containing(UITools.getBounds(parent), { x: 20, y: 26, width: 20, height: 9 }, "parent");
check.containing(UITools.getBounds(child1), { x: 20, y: 26, width: 20, height: 9 }, "child1");
check.containing(UITools.getBounds(child2), { x: 0, y: 0, width: 0, height: 0 }, "child2");
});
child2.drawRect(0, 0, 10, 5);
check.in("sub child rectangle", check => {
check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 32, height: 18 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 32, height: 18 }, "parent");
check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
});
let child3 = testgame.view.add.graphics(50, 51, parent);
check.in("second child empty", check => {
check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 42, height: 34 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
check.containing(UITools.getScreenBounds(child3), { x: 0, y: 0, width: 0, height: 0 }, "child3");
check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 42, height: 34 }, "parent");
check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
check.containing(UITools.getBounds(child3), { x: 0, y: 0, width: 0, height: 0 }, "child3");
});
child3.drawRect(1, 1, 1, 1);
check.in("second child pixel", check => {
check.containing(UITools.getScreenBounds(parent), { x: 8, y: 17, width: 44, height: 36 }, "parent");
check.containing(UITools.getScreenBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getScreenBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
check.containing(UITools.getScreenBounds(child3), { x: 51, y: 52, width: 1, height: 1 }, "child3");
check.containing(UITools.getBounds(parent), { x: 8, y: 17, width: 44, height: 36 }, "parent");
check.containing(UITools.getBounds(child1), { x: 8, y: 17, width: 32, height: 18 }, "child1");
check.containing(UITools.getBounds(child2), { x: 8, y: 17, width: 5, height: 1 }, "child2");
check.containing(UITools.getBounds(child3), { x: 51, y: 52, width: 1, height: 1 }, "child3");
});
});
test.case("keeps objects inside bounds", check => {
let image = testgame.view.add.graphics(150, 100);
let builder = new UIBuilder(testgame.view);
let image = builder.graphics("test", 150, 100, true);
image.beginFill(0xff0000);
image.drawEllipse(50, 25, 50, 25);
image.endFill();
@ -108,7 +112,7 @@ module TK.SpaceTac.UI.Specs {
content.drawCircle(0, 0, 50);
result = UITools.drawBackground(group, background, 3);
check.equals(result, [181, 141]);
});
});*/
});
test.case("normalizes angles", check => {

View File

@ -11,28 +11,49 @@ module TK.SpaceTac.UI {
*
* This is a workaround for a removeChildren bug
*/
export function destroyChildren(obj: Phaser.Image | Phaser.Sprite | Phaser.Group, start = 0, end = obj.children.length - 1) {
obj.children.slice(start, end + 1).forEach(child => (<any>child).destroy());
export function destroyChildren(obj: UIContainer, start = 0, end = obj.length - 1) {
obj.list.slice(start, end + 1).forEach(child => child.destroy());
}
// Common UI tools functions
/**
* Common UI function to work around some Phaser limitations
*/
export class UITools {
/**
* Get the screen bounding rectanle of a displayed object
*
* This is a workaround for bugs in getLocalBounds and getBounds
* Check that a game object has transform and bounds available
*/
static getScreenBounds(obj: Phaser.Image | Phaser.Sprite | Phaser.Group | Phaser.Graphics): IBounded {
obj.updateTransform();
static isSpatial(obj: any): obj is Phaser.GameObjects.Components.GetBounds & Phaser.GameObjects.Components.Transform {
return obj instanceof UIImage || obj instanceof UIText || obj instanceof UIContainer;
}
let rects: IBounded[] = [obj.getBounds()];
obj.children.forEach(child => {
if (child instanceof Phaser.Image || child instanceof Phaser.Sprite || child instanceof Phaser.Group || child instanceof Phaser.Graphics) {
rects.push(UITools.getScreenBounds(child));
/**
* Get the bounding rectanle of a displayed object, in screen space
*/
static getBounds(obj: UIContainer | (Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.GetBounds)): IBounded {
let result: IBounded;
if (obj instanceof UIContainer) {
result = obj.getBounds();
} else {
result = obj.getBounds();
}
return result;
}
/**
* Check if a game object is visible
*/
static isVisible(obj: Phaser.GameObjects.GameObject & Phaser.GameObjects.Components.Visible & Phaser.GameObjects.Components.Alpha): boolean {
if (obj.visible && obj.alpha) {
if (obj.parentContainer) {
return this.isVisible(obj.parentContainer);
} else {
return true;
}
});
return rects.reduce(UITools.unionRects, { x: 0, y: 0, width: 0, height: 0 });
} else {
return false;
}
}
/**
@ -61,12 +82,12 @@ module TK.SpaceTac.UI {
/**
* Reposition an object to remain inside a container
*/
static keepInside(obj: Phaser.Button | Phaser.Sprite | Phaser.Image | Phaser.Group | Phaser.Graphics, rect: IBounded) {
let objbounds = obj.getBounds();
static keepInside(obj: UIButton | UIImage | UIContainer, rect: IBounded) {
let objbounds = UITools.getBounds(obj);
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);
obj.setPosition(x, y);
}
}
@ -135,26 +156,10 @@ module TK.SpaceTac.UI {
/**
* Draw a background around a content
*/
static drawBackground(content: Phaser.Group | Phaser.Text, background: Phaser.Graphics, border = 6): [number, number] {
if (content.parent === background.parent) {
let bounds = content.getLocalBounds();
let x = bounds.x - border;
let y = bounds.y - border;
let width = bounds.width + 2 * border;
let height = bounds.height + 2 * border;
if (!(background.width && background.data.bg_bounds && UITools.compareRects(background.data.bg_bounds, bounds))) {
background.clear();
background.lineStyle(2, 0x6690a4);
background.beginFill(0x162730);
background.drawRect(x, y, width, height);
background.endFill();
background.data.bg_bounds = copy(bounds);
}
return [width, height];
static drawBackground(content: UIContainer | UIText, background: UIBackground, border = 6): [number, number] {
if (content.parentContainer === background.parent) {
background.adaptToContent(content);
return [background.width, background.height];
} else {
console.error("Cannot draw background with different parents", content, background);
return [0, 0];

View File

@ -6,17 +6,16 @@ module TK.SpaceTac.UI {
constructor(view: BaseView, message: string, cancel?: Function) {
super(view);
this.addText(this.width * 0.5, this.height * 0.3, message, "#90FEE3", 32);
this.addLoader(this.width * 0.5, this.height * 0.6);
this.content.text(message, this.width * 0.5, this.height * 0.3, { color: "#90FEE3", size: 32 });
//this.addLoader(this.width * 0.5, this.height * 0.6);
}
/**
* Display an error as the result of waiting.
*/
displayError(message: string) {
this.clearContent();
this.addText(this.width * 0.5, this.height * 0.5, message, "#FE7069", 32);
this.addCloseButton();
this.content.clear();
this.content.text(message, this.width * 0.5, this.height * 0.5, { color: "#FE7069", size: 32 });
}
}
}

View File

@ -18,7 +18,7 @@ module TK.SpaceTac.UI {
*/
export class ValueBar {
// Phaser node
node: Phaser.Image
node: UIImage
// Orientation
private orientation: ValueBarOrientation
@ -35,22 +35,27 @@ module TK.SpaceTac.UI {
// Original size
private original_width: number
private original_height: number
private crop_rect: Phaser.Rectangle
private crop_rect: Phaser.Geom.Rectangle
private crop_mask: Phaser.GameObjects.Graphics
constructor(view: BaseView, name: string, orientation: ValueBarOrientation, x = 0, y = 0) {
this.node = view.newImage(name, x, y);
if (orientation == ValueBarOrientation.WEST) {
this.node.setOrigin(1, 0);
} else if (orientation == ValueBarOrientation.NORTH) {
this.node.setOrigin(0, 1);
} else {
this.node.setOrigin(0, 0);
}
this.orientation = orientation;
this.original_width = this.node.width;
this.original_height = this.node.height;
this.crop_rect = new Phaser.Rectangle(0, 0, this.original_width, this.original_height);
this.node.crop(this.crop_rect);
if (orientation == ValueBarOrientation.WEST) {
this.node.anchor.set(1, 0);
} else if (orientation == ValueBarOrientation.NORTH) {
this.node.anchor.set(0, 1);
}
this.crop_rect = new Phaser.Geom.Rectangle(0, 0, this.original_width, this.original_height);
this.crop_mask = view.make.graphics({ x: x, y: y, add: false });
this.crop_mask.fillStyle(0xffffff);
this.node.setMask(new Phaser.Display.Masks.GeometryMask(view, this.crop_mask));
this.setValue(0, 1000);
}
@ -76,7 +81,9 @@ module TK.SpaceTac.UI {
this.crop_rect.height = Math.round(this.original_height * this.proportional);
break;
}
this.node.updateCrop();
this.crop_mask.clear();
this.crop_mask.fillRectShape(this.crop_rect);
}
/**

View File

@ -6,7 +6,7 @@ module TK.SpaceTac.UI {
view: IntroView
steps: Function[] = []
current = 0
layers: Phaser.Group[] = []
layers: UIContainer[] = []
constructor(view: IntroView) {
this.view = view;
@ -76,21 +76,24 @@ module TK.SpaceTac.UI {
protected galaxy(): Function {
return () => {
let layer = this.getLayer(0);
let game = this.view.game;
let animations = this.view.animations;
let mwidth = this.view.getMidWidth();
let mheight = this.view.getMidHeight();
let galaxy = game.add.group(layer, "galaxy");
galaxy.position.set(mwidth, mheight);
game.tweens.create(galaxy).to({ rotation: Math.PI * 2 }, 150000).loop().start();
game.tweens.create(galaxy).from({ alpha: 0 }, 3000).start();
let builder = new UIBuilder(this.view, layer);
let builder = new UIBuilder(this.view, galaxy);
let back1 = builder.image("intro-galaxy1", 0, 0, true);
back1.scale.set(2.5);
let back2 = builder.image("intro-galaxy2", 0, 0, true);
back2.scale.set(1.5);
game.tweens.create(back2).to({ rotation: Math.PI * 2 }, 300000).loop().start();
let galaxy = builder.container("galaxy", 0, 0, false);
galaxy.setPosition(mwidth, mheight);
animations.show(galaxy, 3000);
animations.addAnimation(galaxy, { rotation: Math.PI * 2 }, 150000, undefined, undefined, Infinity);
builder.in(galaxy, builder => {
let back1 = builder.image("intro-galaxy1", 0, 0, true);
back1.setScale(2.5);
let back2 = builder.image("intro-galaxy2", 0, 0, true);
back2.setScale(1.5);
animations.addAnimation(back2, { rotation: Math.PI * 2 }, 300000, undefined, undefined, Infinity);
});
let random = RandomGenerator.global;
range(200).forEach(i => {
@ -98,13 +101,20 @@ module TK.SpaceTac.UI {
let angle = random.random() * Math.PI * 2;
let power = 0.4 + random.random() * 0.6;
let star = game.add.image(distance * Math.cos(angle), distance * Math.sin(angle),
"common-particles", 16, galaxy);
star.scale.set(0.15 + random.random() * 0.2);
star.anchor.set(0.5);
star.alpha = power * 0.5;
game.tweens.create(star).to({ alpha: star.alpha + 0.5 }, 200 + random.random() * 500,
undefined, true, 1000 + random.random() * 3000, undefined, true).repeat(-1, 2000 + random.random() * 5000).start();
let star = this.view.add.image(distance * Math.cos(angle), distance * Math.sin(angle),
"common-particles", 16);
star.setScale(0.15 + random.random() * 0.2);
star.setAlpha(power * 0.5);
this.view.tweens.add({
targets: star,
alpha: star.alpha + 0.5,
duration: 200 + random.random() * 500,
delay: 1000 + random.random() * 3000,
yoyo: true,
loop: Infinity,
loopDelay: 2000 + random.random() * 5000
});
galaxy.add(star);
});
}
}
@ -115,23 +125,27 @@ module TK.SpaceTac.UI {
protected exitftl(): Function {
return () => {
let layer = this.getLayer(1);
let builder = new ParticleBuilder(this.view);
let fleet = builder.build([
new ParticleConfig(ParticleShape.TRAIL, ParticleColor.BLUEISH, 0.8, 1, 200),
new ParticleConfig(ParticleShape.FLARE, ParticleColor.CYAN, 10, 0.2, -45)
]);
fleet.position.set(this.view.getMidWidth(), this.view.getMidHeight());
this.view.game.add.tween(fleet).from({ x: fleet.x + 1500, y: fleet.y - 750 }, 5000, Phaser.Easing.Circular.Out, true);
this.view.game.add.tween(fleet).to({ alpha: 0, width: 40, height: 40 }, 500, Phaser.Easing.Cubic.Out, true, 3500);
let flash = this.view.game.add.image(this.view.getMidWidth() + 60, this.view.getMidHeight() - 30, "common-particles", 15);
flash.anchor.set(0.5);
flash.scale.set(0.1);
flash.alpha = 0;
let subflash = this.view.game.add.image(0, 0, "common-particles", 0);
subflash.anchor.set(0.5);
subflash.scale.set(0.5);
flash.addChild(subflash);
this.view.game.add.tween(flash).to({ alpha: 0.7, width: 60, height: 60 }, 300, Phaser.Easing.Quadratic.Out, true, 3500, undefined, true);
fleet.setPosition(this.view.getMidWidth() + 1500, this.view.getMidHeight() - 750);
this.view.animations.addAnimation(fleet, { x: this.view.getMidWidth(), y: this.view.getMidHeight() }, 5000, "Circ.easeOut");
this.view.animations.addAnimation(fleet, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, 500, "Cubic.easeOut", 3500);
let flash = this.view.add.container(this.view.getMidWidth() + 60, this.view.getMidHeight() - 30);
flash.setAlpha(0);
flash.setScale(0.1);
new UIBuilder(this.view).in(flash, builder => {
let sub = this.view.add.image(0, 0, "common-particles", 15);
flash.add(sub);
sub = this.view.add.image(0, 0, "common-particles", 0);
sub.setScale(0.5);
flash.add(sub);
});
this.view.animations.addAnimation(flash, { alpha: 0.7, scaleX: 2.5, scaleY: 2.5 }, 300, "Quad.easeOut", 3500, undefined, true);
}
}
@ -162,7 +176,7 @@ module TK.SpaceTac.UI {
/**
* Ensure that a layer exists, and if necessary, clean it
*/
protected getLayer(layer: number, clear = false): Phaser.Group {
protected getLayer(layer: number, clear = false): UIContainer {
while (this.layers.length <= layer) {
this.layers.push(this.view.getLayer(`Layer ${this.layers.length}`));
}

View File

@ -22,7 +22,7 @@ module TK.SpaceTac.UI {
}
};
this.input.onTap.add(nextStep);
this.input.on("pointerup", nextStep);
this.inputs.bind("Home", "Rewind", () => steps.rewind());
this.inputs.bind("Space", "Next step", nextStep);
@ -32,7 +32,7 @@ module TK.SpaceTac.UI {
}
});
this.gameui.audio.startMusic("division");
this.audio.startMusic("division");
}
}
}

View File

@ -7,17 +7,17 @@ module TK.SpaceTac.UI.Specs {
let missions = new ActiveMissions();
let display = new ActiveMissionsDisplay(view, missions);
let container = <Phaser.Group>(<any>display).container;
check.equals(container.children.length, 0);
let container = display.container;
check.equals(container.length, 0);
let mission = new Mission(new Universe(), new Fleet());
mission.addPart(new MissionPart(mission, "Get back to base"));
missions.secondary = [mission];
display.checkUpdate();
check.equals(container.children.length, 2);
check.equals(container.children[0] instanceof Phaser.Image, true);
checkText(check, container.children[1], "Get back to base");
check.equals(container.length, 2);
check.equals(container.list[0] instanceof UIImage, true);
checkText(check, container.list[1], "Get back to base");
});
});
}

View File

@ -2,45 +2,41 @@ module TK.SpaceTac.UI {
/**
* Marker to show current location on the map
*/
export class CurrentLocationMarker extends Phaser.Image {
export class CurrentLocationMarker extends UIImage {
private zoom = -1;
private moving = false;
private fleet: FleetDisplay;
constructor(parent: UniverseMapView, fleet: FleetDisplay) {
super(parent.game, 0, 0, parent.getImageInfo("map-current-location").key, parent.getImageInfo("map-current-location").frame);
constructor(private view: UniverseMapView, fleet: FleetDisplay) {
super(view, 0, 0, view.getImageInfo("map-current-location").key, view.getImageInfo("map-current-location").frame);
this.fleet = fleet;
this.anchor.set(0.5, 0.5);
this.setOrigin(0.5, 0.5);
this.alpha = 0;
}
tweenTo(alpha: number, scale: number) {
this.game.tweens.removeFrom(this);
this.game.tweens.removeFrom(this.scale);
this.game.tweens.create(this).to({ alpha: alpha }, 500).start();
this.game.tweens.create(this.scale).to({ x: scale, y: scale }, 500).start();
this.view.animations.addAnimation<UIImage>(this, { alpha: alpha, scaleX: scale, scaleY: scale }, 500);
}
show() {
let scale = 1;
if (this.zoom == 2) {
this.position.set(this.fleet.x, this.fleet.y);
scale = this.fleet.scale.x * 4;
this.setPosition(this.fleet.x, this.fleet.y);
scale = this.fleet.scaleX * 4;
} else {
this.position.set(this.fleet.location.star.x, this.fleet.location.star.y);
this.setPosition(this.fleet.location.star.x, this.fleet.location.star.y);
scale = (this.zoom == 1) ? 0.002 : 0.016;
}
this.alpha = 0;
this.scale.set(scale * 10, scale * 10);
this.setAlpha(0);
this.setScale(scale * 10);
this.tweenTo(1, scale);
}
hide() {
this.tweenTo(0, this.scale.x * 10);
this.tweenTo(0, this.scaleX * 10);
}
setZoom(level: number) {

View File

@ -9,17 +9,12 @@ module TK.SpaceTac.UI.Specs {
fleet.loopOrbit();
check.equals(fleet.rotation, 0);
mapview.game.tweens.update();
let tween = first(mapview.game.tweens.getAll(), tw => tw.target == fleet);
if (tween) {
let tweendata = tween.generateData(0.1);
check.equals(tweendata.length, 3);
check.nears(tweendata[0].rotation, -Math.PI * 2 / 3);
check.nears(tweendata[1].rotation, -Math.PI * 4 / 3);
check.nears(tweendata[2].rotation, -Math.PI * 2);
} else {
check.fail("No tween found");
}
let tweendata = mapview.animations.simulate(fleet, "rotation", 4);
check.equals(tweendata.length, 4);
check.nears(tweendata[0], 0);
check.nears(tweendata[1], -Math.PI * 2 / 3);
check.nears(tweendata[2], Math.PI * 2 / 3);
check.nears(tweendata[3], 0);
});
});
}

View File

@ -12,14 +12,13 @@ module TK.SpaceTac.UI {
/**
* Group to display a fleet
*/
export class FleetDisplay extends Phaser.Group {
export class FleetDisplay extends UIContainer {
private map: UniverseMapView
private fleet: Fleet
private tween: Phaser.Tween
private ship_count = 0
constructor(parent: UniverseMapView, fleet: Fleet) {
super(parent.game);
super(parent);
this.map = parent;
this.fleet = fleet;
@ -28,11 +27,10 @@ module TK.SpaceTac.UI {
let location = this.map.universe.getLocation(fleet.location);
if (location) {
this.position.set(location.star.x + location.x, location.star.y + location.y);
this.setPosition(location.star.x + location.x, location.star.y + location.y);
}
this.scale.set(SCALING, SCALING);
this.setScale(SCALING, SCALING);
this.tween = this.game.tweens.create(this);
this.loopOrbit();
}
@ -41,13 +39,14 @@ module TK.SpaceTac.UI {
*/
updateShipSprites() {
if (this.ship_count != this.fleet.ships.length) {
this.removeAll(true);
let builder = new UIBuilder(this.map, this);
builder.clear();
this.fleet.ships.forEach((ship, index) => {
let offset = LOCATIONS[index];
let sprite = this.map.newImage(`ship-${ship.model.code}-sprite`, offset[0], offset[1] + 150);
sprite.scale.set(64 / sprite.width);
sprite.anchor.set(0.5, 0.5);
this.add(sprite);
let sprite = builder.image(`ship-${ship.model.code}-sprite`, offset[0], offset[1] + 150, true);
sprite.setScale(64 / sprite.width);
});
this.ship_count = this.fleet.ships.length;
@ -62,7 +61,7 @@ module TK.SpaceTac.UI {
* Animate to a given position in orbit of its current star location
*/
goToOrbitPoint(angle: number, speed = 1, fullturns = 0, then: Function | null = null, ease = false) {
this.tween.stop(false);
this.map.animations.killPrevious(this);
this.rotation %= PI2;
let target = -angle;
@ -71,11 +70,10 @@ module TK.SpaceTac.UI {
}
target -= PI2 * fullturns;
let distance = Math.abs(target - this.rotation) / PI2;
this.tween = this.game.tweens.create(this).to({ rotation: target }, 30000 * distance / speed, ease ? Phaser.Easing.Cubic.In : Phaser.Easing.Linear.None);
let tween = this.map.animations.addAnimation<UIContainer>(this, { rotation: target }, 30000 * distance / speed, ease ? "Cubic.easeIn" : "Linear");
if (then) {
this.tween.onComplete.addOnce(then);
tween.then(() => then());
}
this.tween.start();
}
/**
@ -103,10 +101,10 @@ module TK.SpaceTac.UI {
if (on_leave) {
on_leave(duration);
}
let tween = this.game.tweens.create(this.position).to({ x: this.x + dx, y: this.y + dy }, duration, Phaser.Easing.Cubic.Out);
tween.onComplete.addOnce(() => {
let tween = this.map.animations.addAnimation<UIContainer>(this, { x: this.x + dx, y: this.y + dy }, duration, "Cubic.easeOut");
tween.then(() => {
if (this.fleet.battle) {
this.game.state.start("router");
this.map.backToRouter();
} else {
this.map.current_location.setFleetMoving(false);
this.loopOrbit();
@ -116,7 +114,6 @@ module TK.SpaceTac.UI {
on_finished();
}
});
tween.start();
}, true);
}
}

View File

@ -4,26 +4,31 @@ module TK.SpaceTac.UI {
/**
* Menu to display selected map location, and associated actions
*/
export class MapLocationMenu extends UIComponent {
constructor(view: BaseView) {
super(view, 478, 500);
export class MapLocationMenu {
readonly container: UIContainer
private content: UIBuilder
constructor(private view: BaseView, parent?: UIContainer, x = 0, y = 0) {
let builder = new UIBuilder(view, parent);
this.container = builder.container("location-menu", x, y);
this.content = builder.in(this.container);
}
/**
* Set information displayed, with title and actions to show in menu
*/
setInfo(title: string, actions: [string, Function][]) {
this.clearContent();
this.content.clear();
if (title) {
this.builder.image("map-subname", 239, 57, true);
this.builder.text(title, 239, 57, { color: "#b8d2f1", size: 22 })
this.content.image("map-subname", 239, 57, true);
this.content.text(title, 239, 57, { color: "#b8d2f1", size: 22 })
}
for (let idx = actions.length - 1; idx >= 0; idx--) {
let [label, action] = actions[idx];
this.builder.button("map-action", 172, 48 + idx * 100 + 96, action).anchor.set(0.5);
this.builder.text(label, 186, 48 + idx * 100 + 136, { color: "#b8d2f1", size: 20 });
this.content.button("map-action", 172, 48 + idx * 100 + 96, action, undefined, undefined, { center: true });
this.content.text(label, 186, 48 + idx * 100 + 136, { color: "#b8d2f1", size: 20 });
}
}

View File

@ -3,15 +3,16 @@ module TK.SpaceTac.UI {
* Marker to show a mission location on the map
*/
export class MissionLocationMarker {
private view: BaseView
private container: Phaser.Group
private builder: UIBuilder
private markers: [StarLocation | Star, string][] = []
private zoomed = true
private current_star?: Star
constructor(view: BaseView, parent: Phaser.Group) {
constructor(private view: BaseView, parent: UIContainer) {
this.view = view;
this.container = view.game.add.group(parent, "mission_markers");
let builder = new UIBuilder(view, parent);
this.builder = builder.in(builder.container("mission_markers"));
}
/**
@ -35,16 +36,14 @@ module TK.SpaceTac.UI {
* Refresh the display
*/
refresh(): void {
this.container.removeAll(true);
this.builder.clear();
this.markers.forEach(([location, name], index) => {
let focus = this.zoomed ? location : (location instanceof StarLocation ? location.star : location);
if (location !== this.current_star || !this.zoomed) {
let marker = this.getMarker(focus, index - 1);
let image = this.view.newImage(name, marker.x, marker.y);
image.scale.set(marker.scale);
image.anchor.set(0.5);
this.container.add(image);
let image = this.builder.image(name, marker.x, marker.y);
image.setScale(marker.scale);
}
});
}

View File

@ -10,14 +10,7 @@ module TK.SpaceTac.UI.Specs {
check.patch(shop, "getMissions", () => shop_missions);
function checkTexts(dialog: MissionsDialog, expected: string[]) {
let i = 0;
let container = <Phaser.Group>(<any>dialog).container;
container.children.forEach(child => {
if (child instanceof Phaser.Text) {
check.equals(child.text, expected[i++]);
}
});
check.equals(i, expected.length);
check.equals(collectTexts(dialog.base), expected);
}
let missions = new MissionsDialog(testgame.view, shop, player);

View File

@ -16,6 +16,7 @@ module TK.SpaceTac.UI {
this.location = view.session.getLocation();
this.on_change = on_change || (() => null);
this.addCloseButton();
this.refresh();
}
@ -23,14 +24,13 @@ module TK.SpaceTac.UI {
* Refresh the dialog content
*/
refresh() {
this.clearContent();
this.addCloseButton();
this.content.clear();
let offset = 160;
let active = this.player.missions.getCurrent().filter(mission => !mission.main);
if (active.length) {
this.addText(this.width / 2, offset, "Active jobs", "#b8d2f1", 36);
this.content.text("Active jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 });
offset += 110;
active.forEach(mission => {
@ -41,7 +41,7 @@ module TK.SpaceTac.UI {
let proposed = this.shop.getMissions(this.location);
if (proposed.length) {
this.addText(this.width / 2, offset, "Proposed jobs", "#b8d2f1", 36);
this.content.text("Proposed jobs", this.width / 2, offset, { color: "#b8d2f1", size: 36 });
offset += 110;
proposed.forEach(mission => {
@ -62,14 +62,14 @@ module TK.SpaceTac.UI {
let title = mission.title;
let subtitle = `${capitalize(MissionDifficulty[mission.difficulty])} - Reward: ${mission.getRewardText()}`;
this.addImage(320, yoffset, "map-mission-standard");
this.content.image("map-mission-standard", 320, yoffset, true);
if (title) {
this.addText(380, yoffset - 15, title, "#d2e1f3", 22, false, false, 620, true);
this.content.text(title, 380, yoffset - 15, { color: "#d2e1f3", size: 22, width: 620, center: false });
}
if (subtitle) {
this.addText(380, yoffset + 22, subtitle, "#d2e1f3", 18, false, false, 620, true);
this.content.text(subtitle, 380, yoffset + 22, { color: "#d2e1f3", size: 18, width: 620, center: false });
}
this.builder.button(active ? "map-mission-action-cancel" : "map-mission-action-accept", 1120, yoffset, button_callback).anchor.set(0.5);
this.content.button(active ? "map-mission-action-cancel" : "map-mission-action-accept", 1120, yoffset, button_callback, undefined, undefined, { center: true });
}
}
}

View File

@ -1,99 +1,81 @@
module TK.SpaceTac.UI {
// Group to display a star system
export class StarSystemDisplay extends Phaser.Image {
/**
* Group to display a star system
*/
export class StarSystemDisplay extends UIContainer {
view: UniverseMapView
builder: UIBuilder
circles: Phaser.Group
circles: UIContainer
starsystem: Star
player: Player
fleet_display: FleetDisplay
locations: [StarLocation, Phaser.Image, Phaser.Image][] = []
label: Phaser.Button
locations: [StarLocation, UIImage | UIButton, UIImage][] = []
label: UIButton
constructor(parent: UniverseMapView, starsystem: Star) {
super(parent.game, starsystem.x, starsystem.y, parent.getImageInfo("map-starsystem-background").key, parent.getImageInfo("map-starsystem-background").frame);
super(parent, starsystem.x, starsystem.y);
this.view = parent;
this.builder = new UIBuilder(parent, this);
this.anchor.set(0.5, 0.5);
let scale = this.width;
this.scale.set(starsystem.radius * 2 / scale);
let base = this.builder.image("map-starsystem-background", 0, 0, true);
this.setScale(starsystem.radius * 2 / base.width);
this.starsystem = starsystem;
this.player = parent.player;
this.fleet_display = parent.player_fleet;
// Show boundary
this.circles = this.builder.group("circles");
this.circles = this.builder.container("circles");
let boundaries = this.builder.in(this.circles).image("map-boundaries", 0, 0, true);
boundaries.scale.set(starsystem.radius / (this.scale.x * 256));
boundaries.setScale(starsystem.radius / (this.scaleX * 256));
// Show locations
starsystem.locations.map(location => {
let location_sprite: Phaser.Image | null = null;
let fleet_move = () => this.view.moveToLocation(location);
let location_sprite: UIImage | UIButton | null = null;
let loctype = StarLocationType[location.type].toLowerCase();
if (location.type == StarLocationType.STAR) {
location_sprite = this.addImage(location.x, location.y, "map-location-star", fleet_move);
} else if (location.type == StarLocationType.PLANET) {
location_sprite = this.addImage(location.x, location.y, "map-location-planet", fleet_move);
location_sprite.rotation = Math.atan2(location.y, location.x);
this.addCircle(location.x, location.y);
} else if (location.type == StarLocationType.WARP) {
location_sprite = this.addImage(location.x, location.y, "map-location-warp", fleet_move);
location_sprite.rotation = Math.atan2(location.y, location.x);
location_sprite = this.builder.button(`map-location-${loctype}`, location.x / this.scaleX, location.y / this.scaleY,
() => this.view.moveToLocation(location),
(filler: TooltipBuilder) => {
let visited = this.player.hasVisitedLocation(location);
let shop = (visited && !location.encounter && location.shop) ? " (dockyard present)" : "";
if (location.is(this.player.fleet.location)) {
return `Current fleet location${shop}`;
} else {
let danger = (visited && location.encounter) ? " [enemy fleet detected !]" : "";
return `${visited ? "Visited" : "Unvisited"} ${loctype} - Move the fleet there${danger}${shop}`;
}
}, undefined, { center: true });
location_sprite.setRotation(Math.atan2(location.y, location.x));
if (location.type == StarLocationType.PLANET) {
this.addOrbit(location.x, location.y);
}
this.view.tooltip.bindDynamicText(<Phaser.Button>location_sprite, () => {
let visited = this.player.hasVisitedLocation(location);
let shop = (visited && !location.encounter && location.shop) ? " (dockyard present)" : "";
if (location.is(this.player.fleet.location)) {
return `Current fleet location${shop}`;
} else {
let loctype = StarLocationType[location.type].toLowerCase();
let danger = (visited && location.encounter) ? " [enemy fleet detected !]" : "";
return `${visited ? "Visited" : "Unvisited"} ${loctype} - Move the fleet there${danger}${shop}`;
}
});
if (location_sprite) {
let status = this.getBadgeFrame(location);
let status_badge = this.addImage(location.x + 0.005, location.y + 0.005, `map-status-${status}`);
this.locations.push([location, location_sprite, status_badge]);
}
let status = this.getBadgeFrame(location);
let status_badge = this.builder.image(`map-status-${status}`, (location.x + 0.005) / this.scaleX, (location.y + 0.005) / this.scaleY, true);
this.locations.push([location, location_sprite, status_badge]);
});
// Show name
this.label = this.builder.button("map-name", 0, 460, undefined, `Level ${this.starsystem.level} starsystem`);
this.label.anchor.set(0.5);
this.label = this.builder.button("map-name", 0, 460, undefined, `Level ${this.starsystem.level} starsystem`, undefined, { center: true });
this.builder.in(this.label, builder => {
builder.text(this.starsystem.name, -30, 0, { size: 32, color: "#b8d2f1" });
builder.text(this.starsystem.level.toString(), 243, 30, { size: 24, color: "#a0a0a0" });
});
}
addImage(x: number, y: number, name: string, onclick: Function | null = null): Phaser.Image {
x /= this.scale.x;
y /= this.scale.y;
let info = this.view.getImageInfo(name);
let image = onclick ? this.game.add.button(x, y, info.key, onclick, undefined, info.frame, info.frame) : this.game.add.image(x, y, info.key, info.frame);
image.anchor.set(0.5, 0.5);
this.addChild(image);
return image;
}
/**
* Add an orbit marker
*/
addCircle(x: number, y: number): void {
addOrbit(x: number, y: number): void {
let radius = Math.sqrt(x * x + y * y);
let angle = Math.atan2(y, x);
let circle = this.builder.in(this.circles).image("map-orbit", 0, 0, true);
circle.scale.set(radius / (this.scale.x * 198));
circle.setScale(radius / (this.scaleX * 198));
circle.rotation = angle - 0.01;
}
@ -125,7 +107,11 @@ module TK.SpaceTac.UI {
// LOD
let detailed = focus && level == 2;
this.children.filter(child => child !== this.label).forEach(child => this.view.animations.setVisible(child, detailed, 300));
this.list.filter(child => child !== this.label).forEach(child => {
if (child !== this.label && (child instanceof UIButton || child instanceof UIImage)) {
this.view.animations.setVisible(child, detailed, 300);
}
});
this.updateLabel(level);
}
@ -137,10 +123,8 @@ module TK.SpaceTac.UI {
this.label.visible = this.player.hasVisitedSystem(this.starsystem);
let factor = (zoom == 2) ? 1 : (zoom == 1 ? 5 : 15);
this.view.tweens.create(this.label.scale).to({ x: factor, y: factor }, 500, Phaser.Easing.Cubic.InOut).start();
let position = (zoom == 2) ? { x: -680, y: 440 } : { x: 0, y: (zoom == 1 ? 180 : 100) * factor };
this.view.tweens.create(this.label.position).to(position, 500, Phaser.Easing.Cubic.InOut).start();
this.view.animations.addAnimation(this.label, { x: position.x, y: position.y, scaleX: factor, scaleY: factor }, 500, "Cubic.easeInOut");
}
}
}

View File

@ -15,15 +15,15 @@ module TK.SpaceTac.UI {
interactive = true
// Layers
layer_universe!: Phaser.Group
layer_overlay!: Phaser.Group
layer_universe!: UIContainer
layer_overlay!: UIContainer
// Star systems
starsystems: StarSystemDisplay[] = []
// Links between stars
starlinks_group!: Phaser.Group
starlinks: Phaser.Graphics[] = []
starlinks_group!: UIContainer
starlinks: UIGraphics[] = []
// Fleets
player_fleet!: FleetDisplay
@ -44,20 +44,20 @@ module TK.SpaceTac.UI {
// Zoom level
zoom = 0
zoom_in!: Phaser.Button
zoom_out!: Phaser.Button
zoom_in!: UIButton
zoom_out!: UIButton
// Options button
button_options!: Phaser.Button
button_options!: UIButton
/**
* Init the view, binding it to a universe
*/
init(universe: Universe, player: Player) {
super.init();
init(data: { universe: Universe, player: Player }) {
super.init(data);
this.universe = universe;
this.player = player;
this.universe = data.universe;
this.player = data.player;
}
/**
@ -71,27 +71,29 @@ module TK.SpaceTac.UI {
this.layer_universe = this.getLayer("universe");
this.layer_overlay = this.getLayer("overlay");
this.starlinks_group = this.game.add.group(this.layer_universe);
this.starlinks_group = builder.in(this.layer_universe).container("starlinks");
this.starlinks = [];
this.starlinks = this.universe.starlinks.map(starlink => {
let loc1 = starlink.first.getWarpLocationTo(starlink.second);
let loc2 = starlink.second.getWarpLocationTo(starlink.first);
let result = new Phaser.Graphics(this.game);
let result = builder.in(this.starlinks_group).graphics("starlink");
if (loc1 && loc2) {
result.lineStyle(0.01, 0x6cc7ce);
result.beginPath();
result.moveTo(starlink.first.x - 0.5 + loc1.x, starlink.first.y - 0.5 + loc1.y);
result.lineTo(starlink.second.x - 0.5 + loc2.x, starlink.second.y - 0.5 + loc2.y);
result.strokePath();
}
result.data.link = starlink;
result.setDataEnabled();
result.data.set("link", starlink);
return result;
});
this.starlinks.forEach(starlink => this.starlinks_group.add(starlink));
this.player_fleet = new FleetDisplay(this, this.player.fleet);
this.starsystems = this.universe.stars.map(star => new StarSystemDisplay(this, star));
this.starsystems.forEach(starsystem => this.layer_universe.add(starsystem));
this.player_fleet = new FleetDisplay(this, this.player.fleet);
this.layer_universe.add(this.player_fleet);
this.current_location = new CurrentLocationMarker(this, this.player_fleet);
@ -99,9 +101,7 @@ module TK.SpaceTac.UI {
this.mission_markers = new MissionLocationMarker(this, this.layer_universe);
this.actions = new MapLocationMenu(this);
this.actions.setPosition(30, 30);
this.actions.moveToLayer(this.layer_overlay);
this.actions = new MapLocationMenu(this, this.layer_overlay, 30, 30);
this.missions = new ActiveMissionsDisplay(this, this.player.missions, this.mission_markers);
this.missions.setPosition(20, 720);
@ -114,12 +114,12 @@ module TK.SpaceTac.UI {
});
this.character_sheet = new CharacterSheet(this, CharacterSheetMode.EDITION);
this.layer_overlay.add(this.character_sheet);
this.character_sheet.moveToLayer(this.layer_overlay);
this.conversation = new MissionConversationDisplay(this);
this.conversation.moveToLayer(this.layer_overlay);
this.gameui.audio.startMusic("spring-thaw");
this.audio.startMusic("spring-thaw");
// Inputs
this.inputs.bind(" ", "Conversation step", () => this.conversation.forward());
@ -138,14 +138,8 @@ module TK.SpaceTac.UI {
this.setZoom(2, 0);
// Add a shader background
builder.shader("map-background", { width: this.getWidth(), height: this.getHeight() }, 0, 0, () => {
let scale = this.layer_universe.scale.x;
return {
offset: { x: (920 - this.layer_universe.x) / scale, y: -(540 - this.layer_universe.y) / scale },
scale: scale
}
});
// Add a background
//builder.image("map-background");
// Trigger an auto-save any time we go back to the map
this.autoSave();
@ -195,8 +189,10 @@ module TK.SpaceTac.UI {
}
this.starlinks.forEach(linkgraphics => {
let link = <StarLink>linkgraphics.data.link;
linkgraphics.visible = this.player.hasVisitedSystem(link.first) || this.player.hasVisitedSystem(link.second);
let link = linkgraphics.data.get("link");
if (link instanceof StarLink) {
linkgraphics.visible = this.player.hasVisitedSystem(link.first) || this.player.hasVisitedSystem(link.second);
}
})
this.starsystems.forEach(system => system.updateInfo(this.zoom, system.starsystem == current_star));
@ -226,16 +222,15 @@ module TK.SpaceTac.UI {
/**
* Set the camera to center on a target, and to display a given span in height
*/
setCamera(x: number, y: number, span: number, duration = 500, easing = Phaser.Easing.Cubic.InOut) {
setCamera(x: number, y: number, span: number, duration = 500, easing = "Cubic.easeInOut") {
let scale = 1000 / span;
let dest_x = 920 - x * scale;
let dest_y = 540 - y * scale;
if (duration) {
this.tweens.create(this.layer_universe.position).to({ x: dest_x, y: dest_y }, duration, easing).start();
this.tweens.create(this.layer_universe.scale).to({ x: scale, y: scale }, duration, easing).start();
this.animations.addAnimation(this.layer_universe, { x: dest_x, y: dest_y, scaleX: scale, scaleY: scale }, duration, easing);
} else {
this.layer_universe.position.set(dest_x, dest_y);
this.layer_universe.scale.set(scale);
this.layer_universe.setPosition(dest_x, dest_y);
this.layer_universe.setScale(scale);
}
}
@ -257,7 +252,7 @@ module TK.SpaceTac.UI {
*/
setLinksAlpha(alpha: number, duration = 500) {
if (duration) {
this.game.add.tween(this.starlinks_group).to({ alpha: alpha }, duration * Math.abs(this.starlinks_group.alpha - alpha)).start();
this.animations.addAnimation(this.starlinks_group, { alpha: alpha }, duration * Math.abs(this.starlinks_group.alpha - alpha));
} else {
this.starlinks_group.alpha = alpha;
}
@ -297,7 +292,7 @@ module TK.SpaceTac.UI {
let dest_star = dest_location.star;
this.player_fleet.moveToLocation(dest_location, 3, duration => {
this.timer.schedule(duration / 2, () => this.updateInfo(dest_star, false));
this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, Phaser.Easing.Cubic.Out);
this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, "Cubic.Out");
}, () => {
this.setInteractionEnabled(true);
this.refresh();
@ -348,12 +343,12 @@ module TK.SpaceTac.UI {
*/
setInteractionEnabled(enabled: boolean) {
this.interactive = enabled && !this.session.spectator;
this.actions.setVisible(enabled && this.zoom == 2, 300);
this.animations.setVisible(this.actions.container, enabled && this.zoom == 2, 300);
this.missions.setVisible(enabled && this.zoom == 2, 300);
this.animations.setVisible(this.zoom_in, enabled && this.zoom < 2, 300);
this.animations.setVisible(this.zoom_out, enabled && this.zoom > 0, 300);
this.animations.setVisible(this.button_options, enabled, 300);
this.animations.setVisible(this.character_sheet, enabled, 300);
//this.animations.setVisible(this.character_sheet, enabled, 300);
}
}
}

View File

@ -2,13 +2,12 @@
/// <reference path="MainMenu.ts" />
module TK.SpaceTac.UI.Specs {
testing("LoadDialog", test => {
testing("InputInviteCode", test => {
let testgame = setupEmptyView(test);
test.acase("joins remote sessions as spectator", async check => {
return new Promise((resolve, reject) => {
let view = <MainMenu>testgame.ui.state.getCurrentState();
let view = testgame.view;
let session = new GameSession();
check.equals(session.primary, true);
check.equals(session.spectator, false);

Some files were not shown because too many files have changed in this diff Show More