diff --git a/TODO.md b/TODO.md index 39b72f2..be256b1 100644 --- a/TODO.md +++ b/TODO.md @@ -23,13 +23,13 @@ Menu/settings/saves Map/story --------- -* Add sound effects and more visual effects (jumps...) * Add factions and reputation * Allow to cancel secondary missions * 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 +* Improve performance when refreshing (and thus during jumps) Character sheet --------------- diff --git a/data/stage3/image/map/jump-effect.png b/data/stage3/image/map/jump-effect.png new file mode 100644 index 0000000..9d64e48 Binary files /dev/null and b/data/stage3/image/map/jump-effect.png differ diff --git a/data/stage3/sound/map/warp-in.wav b/data/stage3/sound/map/warp-in.wav new file mode 100644 index 0000000..4ec53e0 Binary files /dev/null and b/data/stage3/sound/map/warp-in.wav differ diff --git a/data/stage3/sound/map/warp-out.wav b/data/stage3/sound/map/warp-out.wav new file mode 100644 index 0000000..5452d33 Binary files /dev/null and b/data/stage3/sound/map/warp-out.wav differ diff --git a/graphics/ui/map.svg b/graphics/ui/map.svg index ca22739..514c58e 100644 --- a/graphics/ui/map.svg +++ b/graphics/ui/map.svg @@ -20,6 +20,42 @@ enable-background="new"> + + + + + + + + + + + @@ -1121,6 +1157,76 @@ fx="342.45343" fy="142.875" r="9.7939377" /> + + + + + + + + + + + + + + + + + { - ui!: MainUI; - view!: T; - multistorage!: Multi.FakeRemoteStorage; - clock!: FakeClock; + check!: TestContext + ui!: MainUI + view!: T + multistorage!: Multi.FakeRemoteStorage + clock: FakeClock + time = 0 + + constructor(test: TestSuite) { + this.clock = test.clock(); + } + + /** + * Advance the time in the view and fake testing clock + */ + clockForward(milliseconds: number) { + this.time += milliseconds; + this.clock.forward(milliseconds); + this.ui.headlessStep(this.time, milliseconds); + } } /** * Setup a headless test UI, with a single view started. */ export function setupSingleView(test: TestSuite, buildView: () => [T, object]) { - let testgame = new TestGame(); + let testgame = new TestGame(test); test.asetup(() => new Promise((resolve, reject) => { let check = new TestContext(); // TODO Should be taken from test suite @@ -25,6 +40,7 @@ module TK.SpaceTac.UI.Specs { check.patch(console, "warn", null); testgame.ui = new MainUI(true); + testgame.check = check; let [scene, scenedata] = buildView(); @@ -133,6 +149,17 @@ module TK.SpaceTac.UI.Specs { } } + /** + * Check a simulation of a tweened property + */ + export function checkTween(game: TestGame, obj: T, property: P, expected: number[]): void { + let tweendata = game.view.animations.simulate(obj, property, expected.length); + game.check.equals(tweendata.length, expected.length, "number of points"); + expected.forEach((value, idx) => { + game.check.nears(tweendata[idx], value, undefined, `point ${idx}`); + }); + } + /** * Simulate a click on a button */ diff --git a/src/ui/battle/WeaponEffect.spec.ts b/src/ui/battle/WeaponEffect.spec.ts index a3ac85c..9520665 100644 --- a/src/ui/battle/WeaponEffect.spec.ts +++ b/src/ui/battle/WeaponEffect.spec.ts @@ -1,20 +1,12 @@ 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.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.headlessStep(t, milliseconds); - } - test.case("displays shield hit effect", check => { let battleview = testgame.view; battleview.timer = new Timer(); @@ -25,7 +17,7 @@ module TK.SpaceTac.UI.Specs { let layer = battleview.arena.layer_weapon_effects; check.equals(layer.length, 1); - clock.forward(600); + testgame.clockForward(600); check.equals(layer.length, 2); let child = layer.list[0]; @@ -91,12 +83,12 @@ module TK.SpaceTac.UI.Specs { effect.gunEffect(); checkEmitters("gun effect started", 1); - fastForward(6000); + testgame.clockForward(6000); checkEmitters("gun effect ended", 0); effect.hullImpactEffect({ x: 0, y: 0 }, { x: 50, y: 50 }, 1000, 2000); checkEmitters("hull effect started", 1); - fastForward(8500); + testgame.clockForward(8500); checkEmitters("hull effect ended", 0); }); diff --git a/src/ui/common/Animations.spec.ts b/src/ui/common/Animations.spec.ts index 2650188..7ead98b 100644 --- a/src/ui/common/Animations.spec.ts +++ b/src/ui/common/Animations.spec.ts @@ -65,5 +65,21 @@ module TK.SpaceTac.UI.Specs { check.nears(points[2], 0); check.nears(points[3], Math.PI * 0.25); }); + + test.case("stops previous animations before starting a new one", check => { + let obj = { x: 0, y: 0 }; + testgame.view.animations.addAnimation(obj, { x: 1 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 1); + testgame.view.animations.addAnimation(obj, { y: 1 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 2); + testgame.view.animations.addAnimation(obj, { x: 2 }, 1000); + testgame.clockForward(1); + testgame.clockForward(1); + check.equals(testgame.view.tweens.getAllTweens().length, 2); + }); }); } diff --git a/src/ui/common/Animations.ts b/src/ui/common/Animations.ts index 0304e76..09d46ff 100644 --- a/src/ui/common/Animations.ts +++ b/src/ui/common/Animations.ts @@ -22,11 +22,9 @@ module TK.SpaceTac.UI { * This is a wrapper around phaser's tweens. */ export class Animations { - private tweens: Phaser.Tweens.TweenManager private immediate = false - constructor(tweens: Phaser.Tweens.TweenManager) { - this.tweens = tweens; + constructor(private tweens: Phaser.Tweens.TweenManager) { } /** @@ -39,11 +37,14 @@ module TK.SpaceTac.UI { } /** - * Kill previous tweens from an object + * Kill previous tweens currently running on an object's properties */ - killPrevious(obj: object): void { - // TODO Only updated properties - this.tweens.killTweensOf(obj); + killPrevious(obj: T, properties: Extract[]): void { + this.tweens.getTweensOf(obj).forEach(tween => { + if (tween.data && any(tween.data, data => bool(data.key) && contains(properties, data.key))) { + tween.stop(); + } + }); } /** @@ -70,7 +71,7 @@ module TK.SpaceTac.UI { * Display an object, with opacity transition */ show(obj: IAnimationFadeable, duration = 1000, alpha = 1): void { - this.killPrevious(obj); + this.killPrevious(obj, ['alpha']); if (!obj.visible) { obj.alpha = 0; @@ -105,7 +106,7 @@ module TK.SpaceTac.UI { * Hide an object, with opacity transition */ hide(obj: IAnimationFadeable, duration = 1000, alpha = 0): void { - this.killPrevious(obj); + this.killPrevious(obj, ['alpha']); if (obj.changeStateFrame) { obj.changeStateFrame("Out"); @@ -155,8 +156,8 @@ module TK.SpaceTac.UI { * Add an asynchronous animation to an object. */ addAnimation(obj: T, properties: Partial, duration: number, ease = "Linear", delay = 0, loop = 1, yoyo = false): Promise { - return new Promise((resolve, reject) => { - this.killPrevious(obj); + return new Promise(resolve => { + this.killPrevious(obj, keys(properties)); this.tweens.add(merge({ targets: obj, @@ -232,12 +233,12 @@ module TK.SpaceTac.UI { * Returns the animation duration. */ moveInSpace(obj: Phaser.GameObjects.Components.Transform, x: number, y: number, angle: number, rotated_obj = obj): number { + this.killPrevious(obj, ["x", "y"]); + if (x == obj.x && y == obj.y) { - this.killPrevious(obj); return this.rotationTween(rotated_obj, angle, 0.5); } else { - this.killPrevious(obj); - this.killPrevious(rotated_obj); + this.killPrevious(rotated_obj, ["rotation"]); let distance = Target.newFromLocation(obj.x, obj.y).getDistanceTo(Target.newFromLocation(x, y)); let duration = Math.sqrt(distance / 1000) * 3000; let curve_force = distance * 0.4; diff --git a/src/ui/common/InputManager.spec.ts b/src/ui/common/InputManager.spec.ts index 267d773..297340a 100644 --- a/src/ui/common/InputManager.spec.ts +++ b/src/ui/common/InputManager.spec.ts @@ -1,7 +1,6 @@ module TK.SpaceTac.UI.Specs { testing("InputManager", test => { let testgame = setupEmptyView(test); - let clock = test.clock(); test.case("handles hover and click on desktops and mobile targets", check => { let inputs = testgame.view.inputs; @@ -46,7 +45,7 @@ module TK.SpaceTac.UI.Specs { [button, mocks] = newButton(); check.in("Leaves on destroy", check => { press(button); - clock.forward(150); + testgame.clockForward(150); check.called(mocks.enter, 1); check.called(mocks.leave, 0); check.called(mocks.click, 0); @@ -65,7 +64,7 @@ module TK.SpaceTac.UI.Specs { let [button1, funcs1] = newButton(); let [button2, funcs2] = newButton(); enter(button1); - clock.forward(150); + testgame.clockForward(150); check.called(funcs1.enter, 1); check.called(funcs1.leave, 0); check.called(funcs1.click, 0); @@ -76,7 +75,7 @@ module TK.SpaceTac.UI.Specs { check.called(funcs2.enter, 0); check.called(funcs2.leave, 0); check.called(funcs2.click, 0); - clock.forward(150); + testgame.clockForward(150); check.called(funcs1.enter, 0); check.called(funcs1.leave, 0); check.called(funcs1.click, 0); @@ -88,7 +87,7 @@ module TK.SpaceTac.UI.Specs { [button, mocks] = newButton(); check.in("Hold to hover on mobile", check => { button.emit("pointerdown", pointer); - clock.forward(150); + testgame.clockForward(150); check.called(mocks.enter, 1); check.called(mocks.leave, 0); check.called(mocks.click, 0); diff --git a/src/ui/common/Tooltip.spec.ts b/src/ui/common/Tooltip.spec.ts index e548543..e6398c5 100644 --- a/src/ui/common/Tooltip.spec.ts +++ b/src/ui/common/Tooltip.spec.ts @@ -1,7 +1,6 @@ module TK.SpaceTac.UI.Specs { testing("Tooltip", test => { let testgame = setupEmptyView(test); - let clock = test.clock(); test.case("shows near the hovered button", check => { let button = new UIBuilder(testgame.view).button("fake"); @@ -18,7 +17,7 @@ module TK.SpaceTac.UI.Specs { button.emit("pointerover", { pointer: pointer }); check.equals(container.visible, false); - clock.forward(1000); + testgame.clockForward(1000); container.update(); check.equals(container.visible, true); check.equals(container.x, 113); diff --git a/src/ui/common/UIAwaiter.ts b/src/ui/common/UIAwaiter.ts index c8da3b6..690f949 100644 --- a/src/ui/common/UIAwaiter.ts +++ b/src/ui/common/UIAwaiter.ts @@ -2,7 +2,7 @@ module TK.SpaceTac.UI { /** * UI component to show a loader animation while waiting for something */ - export class UIAwaiter extends Phaser.GameObjects.Container { + export class UIAwaiter extends UIContainer { constructor(view: BaseView, x: number, y: number, visible: boolean) { super(view, x, y); this.setName("awaiter"); diff --git a/src/ui/common/UIContainer.ts b/src/ui/common/UIContainer.ts index 3f91bc7..c8ece1b 100644 --- a/src/ui/common/UIContainer.ts +++ b/src/ui/common/UIContainer.ts @@ -3,6 +3,13 @@ module TK.SpaceTac.UI { * UI component able to contain other UI components */ export class UIContainer extends Phaser.GameObjects.Container { + /** + * Get a container to build UI components inside the container + */ + getBuilder(): UIBuilder { + return new UIBuilder(this.scene, this); + } + /** * Fixed version that does not force (0, 0) to be in bounds */ diff --git a/src/ui/map/FleetDisplay.spec.ts b/src/ui/map/FleetDisplay.spec.ts index 70fc3b9..6c306c5 100644 --- a/src/ui/map/FleetDisplay.spec.ts +++ b/src/ui/map/FleetDisplay.spec.ts @@ -1,20 +1,43 @@ module TK.SpaceTac.UI.Specs { testing("FleetDisplay", test => { - let testgame = setupMapview(test); + let testgame = setupEmptyView(test); test.case("orbits the fleet around its current location", check => { - let mapview = testgame.view; - let fleet = mapview.player_fleet; + let session = new GameSession(); + session.startNewGame(true, false); + let fleet = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); fleet.loopOrbit(); check.equals(fleet.rotation, 0); - 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); + checkTween(testgame, fleet, "rotation", [0, -Math.PI * 2 / 3, Math.PI * 2 / 3, 0]); + }); + + test.case("animates jumps between locations", check => { + let session = new GameSession(); + session.startNewGame(true, false); + let fleet_disp = new FleetDisplay(testgame.view, session.player.fleet, session.universe, undefined, false); + + let on_leave = check.mockfunc("on_leave", (duration: number): any => null); + let on_finished = check.mockfunc(); + + let current = nn(session.universe.getLocation(session.player.fleet.location)); + let dest = nn(first(current.star.locations, loc => loc !== current)); + dest.universe_x = current.universe_x - 0.1; + dest.universe_y = current.universe_y; + dest.clearEncounter(); + fleet_disp.moveToLocation(dest, 1, on_leave.func, on_finished.func); + check.called(on_leave, 0); + check.called(on_finished, 0); + checkTween(testgame, fleet_disp, "rotation", [ + 0, + -0.0436332312998573, + -0.3490658503988655, + -1.178097245096172, + -2.7925268031909276, + 0.8290313946973056, + -3.141592653589793 + ]); }); }); } diff --git a/src/ui/map/FleetDisplay.ts b/src/ui/map/FleetDisplay.ts index b94dfb3..c33b032 100644 --- a/src/ui/map/FleetDisplay.ts +++ b/src/ui/map/FleetDisplay.ts @@ -13,25 +13,23 @@ module TK.SpaceTac.UI { * Group to display a fleet */ export class FleetDisplay extends UIContainer { - private map: UniverseMapView - private fleet: Fleet private ship_count = 0 + private is_moving = false - constructor(parent: UniverseMapView, fleet: Fleet) { - super(parent); - - this.map = parent; - this.fleet = fleet; + constructor(private map: BaseView, private fleet: Fleet, private universe: Universe, private location_marker?: CurrentLocationMarker, orbit = true) { + super(map); this.updateShipSprites(); - let location = this.map.universe.getLocation(fleet.location); + let location = this.universe.getLocation(fleet.location); if (location) { this.setPosition(location.star.x + location.x, location.star.y + location.y); } this.setScale(SCALING, SCALING); - this.loopOrbit(); + if (orbit) { + this.loopOrbit(); + } } /** @@ -54,14 +52,14 @@ module TK.SpaceTac.UI { } get location(): StarLocation { - return this.map.universe.getLocation(this.fleet.location) || new StarLocation(); + return this.universe.getLocation(this.fleet.location) || new StarLocation(); } /** * 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.map.animations.killPrevious(this); + async goToOrbitPoint(angle: number, speed = 1, fullturns = 0, ease = false): Promise { + this.map.animations.killPrevious(this, ["angle"]); this.rotation %= PI2; let target = -angle; @@ -70,33 +68,32 @@ module TK.SpaceTac.UI { } target -= PI2 * fullturns; let distance = Math.abs(target - this.rotation) / PI2; - let tween = this.map.animations.addAnimation(this, { rotation: target }, 30000 * distance / speed, ease ? "Cubic.easeIn" : "Linear"); - if (then) { - tween.then(() => then()); - } + await this.map.animations.addAnimation(this, { rotation: target }, 30000 * distance / speed, ease ? "Cubic.easeIn" : "Linear"); } /** * Make the fleet loop in orbit */ loopOrbit() { - this.goToOrbitPoint(this.rotation + PI2, 1, 0, () => { - this.loopOrbit(); - }); + if (!this.is_moving) { + this.goToOrbitPoint(this.rotation + PI2, 1, 0).then(() => this.loopOrbit()); + } } /** * Make the fleet move to another location in the same system */ moveToLocation(location: StarLocation, speed = 1, on_leave: ((duration: number) => any) | null = null, on_finished: Function | null = null) { - let fleet_location = this.map.universe.getLocation(this.fleet.location); + let fleet_location = this.universe.getLocation(this.fleet.location); if (fleet_location && this.fleet.move(location)) { let dx = location.universe_x - fleet_location.universe_x; let dy = location.universe_y - fleet_location.universe_y; let distance = Math.sqrt(dx * dx + dy * dy); - let angle = Math.atan2(dx, dy); - this.map.current_location.setFleetMoving(true); - this.goToOrbitPoint(angle - Math.PI / 2, 40, 1, () => { + let angle = Math.atan2(-dy, dx); + this.setMoving(true); + console.error(fleet_location, location, angle); + this.goToOrbitPoint(angle, 40, 1, true).then(() => { + this.setRotation(-angle); let duration = 10000 * distance / speed; if (on_leave) { on_leave(duration); @@ -106,7 +103,7 @@ module TK.SpaceTac.UI { if (this.fleet.battle) { this.map.backToRouter(); } else { - this.map.current_location.setFleetMoving(false); + this.setMoving(false); this.loopOrbit(); } @@ -114,7 +111,33 @@ module TK.SpaceTac.UI { on_finished(); } }); - }, true); + }); + } + } + + /** + * Display a jump flash effect + */ + async showJumpEffect(lag = 0, duration = 0): Promise { + this.map.audio.playOnce(lag ? "map-warp-out" : "map-warp-in"); + let effect = this.getBuilder().image("map-jump-effect", 0, 150, true); + effect.setScale(0.01); + effect.setZ(-1); + if (lag && duration) { + this.map.animations.addAnimation(effect, { x: -lag / SCALING }, duration * 0.5, "Cubic.easeOut"); + } + await this.map.animations.addAnimation(effect, { scaleX: 3, scaleY: 3 }, 100); + await this.map.animations.addAnimation(effect, { scaleX: 2, scaleY: 2, alpha: 0 }, 200); + effect.destroy(); + } + + /** + * Mark the fleet as moving + */ + private setMoving(moving: boolean): void { + this.is_moving = moving; + if (this.location_marker) { + this.location_marker.setFleetMoving(moving); } } } diff --git a/src/ui/map/UniverseMapView.ts b/src/ui/map/UniverseMapView.ts index e0d379e..f7a286b 100644 --- a/src/ui/map/UniverseMapView.ts +++ b/src/ui/map/UniverseMapView.ts @@ -94,7 +94,7 @@ module TK.SpaceTac.UI { 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.player_fleet = new FleetDisplay(this, this.player.fleet, this.universe, this.current_location); this.layer_universe.add(this.player_fleet); this.current_location = new CurrentLocationMarker(this, this.player_fleet); @@ -290,7 +290,9 @@ module TK.SpaceTac.UI { let dest_location = location.jump_dest; let dest_star = dest_location.star; this.player_fleet.moveToLocation(dest_location, 3, duration => { - this.timer.schedule(duration / 3, () => this.updateInfo(dest_star, false)); + this.player_fleet.showJumpEffect(location.getDistanceTo(dest_location), duration); + this.timer.schedule(duration * 0.3, () => this.updateInfo(dest_star, false)); + this.timer.schedule(duration * 0.7, () => this.player_fleet.showJumpEffect()); this.setCamera(dest_star.x, dest_star.y, dest_star.radius * 2, duration, "Cubic.easeOut"); }, () => { this.setInteractionEnabled(true);