diff --git a/assets/dbs/interactable_objects_db.json b/assets/dbs/interactable_objects_db.json index 36a15beb0f..4062e08b17 100644 --- a/assets/dbs/interactable_objects_db.json +++ b/assets/dbs/interactable_objects_db.json @@ -399,5 +399,26 @@ } } } + },{ + "key_name": "whirlwind_source", + "anchor_x": 0.5, + "anchor_y": 0.75, + "body_radius": 7.0, + "collision_body_bevel": 3, + "initial_action": "whirlwind_source", + "whirlwind_source": true, + "actions": { + "whirlwind_source": { + "frame_rate": 1, + "animations": ["down", "left", "right"], + "frames_count": 1, + "loop": false, + "initial_animation": "down", + "spritesheet": { + "image": "assets/images/interactable_objects/whirlwind_source.png", + "json": "assets/images/interactable_objects/whirlwind_source.json" + } + } + } } ] diff --git a/assets/images/interactable_objects/whirlwind_source.json b/assets/images/interactable_objects/whirlwind_source.json new file mode 100644 index 0000000000..b565a85238 --- /dev/null +++ b/assets/images/interactable_objects/whirlwind_source.json @@ -0,0 +1,36 @@ +{"frames": { + +"whirlwind_source/down/00": +{ + "frame": {"x":1,"y":1,"w":18,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":18,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"whirlwind_source/left/00": +{ + "frame": {"x":1,"y":35,"w":18,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":18,"h":32}, + "sourceSize": {"w":32,"h":32} +}, +"whirlwind_source/right/00": +{ + "frame": {"x":1,"y":69,"w":18,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":7,"y":0,"w":18,"h":32}, + "sourceSize": {"w":32,"h":32} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "whirlwind_source.png", + "format": "RGBA8888", + "size": {"w":20,"h":102}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:678addb2c0bc8e04bef7f4a5c08e2e9a:6d7c849f15c547c321bf45e7fbd0ed7a:9fe8a438923bc41c55feea2691152f22$" +} +} diff --git a/assets/images/interactable_objects/whirlwind_source.png b/assets/images/interactable_objects/whirlwind_source.png new file mode 100644 index 0000000000..4d385ca309 Binary files /dev/null and b/assets/images/interactable_objects/whirlwind_source.png differ diff --git a/base/Camera.ts b/base/Camera.ts index f4576eb194..7f54c66fc8 100644 --- a/base/Camera.ts +++ b/base/Camera.ts @@ -1,3 +1,4 @@ +import {GoldenSun} from "./GoldenSun"; import {ControllableChar} from "./ControllableChar"; import {InteractableObjects} from "./interactable_objects/InteractableObjects"; @@ -9,16 +10,19 @@ export class Camera { private static readonly SHAKE_INTENSITY = 3; private game: Phaser.Game; + private data: GoldenSun; private _target: ControllableChar | InteractableObjects; private _camera_shake_enable: boolean; private _following: boolean; + private _unfollow_hero_on_shake: boolean; private _shake_ref_pos: { x: number; y: number; }; - constructor(game: Phaser.Game) { + constructor(game: Phaser.Game, data: GoldenSun) { this.game = game; + this.data = data; this._target = null; this._camera_shake_enable = false; this._following = false; @@ -26,6 +30,7 @@ export class Camera { x: 0, y: 0, }; + this._unfollow_hero_on_shake = false; } /** The target that the camera is following. */ @@ -109,11 +114,16 @@ export class Camera { /** * Enables camera shake. + * @param unfollow_hero_on_shake will unfollow the hero while shaking the camera. */ - enable_shake() { + enable_shake(unfollow_hero_on_shake: boolean = false) { this._shake_ref_pos.x = this.game.camera.x; this._shake_ref_pos.y = this.game.camera.y; this._camera_shake_enable = true; + this._unfollow_hero_on_shake = unfollow_hero_on_shake; + if (this._unfollow_hero_on_shake) { + this.unfollow(); + } } /** @@ -121,6 +131,9 @@ export class Camera { */ disable_shake() { this._camera_shake_enable = false; + if (this._unfollow_hero_on_shake) { + this.follow(this.data.hero); + } } /** diff --git a/base/GoldenSun.ts b/base/GoldenSun.ts index 59a85ca89d..3a36b06405 100644 --- a/base/GoldenSun.ts +++ b/base/GoldenSun.ts @@ -231,7 +231,7 @@ export class GoldenSun { await initialize_bgm_data(this.game, this, this.dbs.bgm_db); //init camera custom features - this.camera = new Camera(this.game); + this.camera = new Camera(this.game, this); //initialize managers this.gamepad = new XGamepad(this); diff --git a/base/Map.ts b/base/Map.ts index 336c2c165c..de9acd2a09 100644 --- a/base/Map.ts +++ b/base/Map.ts @@ -26,6 +26,7 @@ import {DjinnGetEvent} from "./game_events/DjinnGetEvent"; import {Breakable} from "./interactable_objects/Breakable"; import {Window} from "./Window"; import {GAME_HEIGHT, GAME_WIDTH} from "./magic_numbers"; +import {WhirlwindSource} from "./interactable_objects/WhirlwindSource"; /** The class reponsible for the maps of the engine. */ export class Map { @@ -384,6 +385,7 @@ export class Map { } this.collision_sprite.body.velocity.y = this.collision_sprite.body.velocity.x = 0; this.npcs.forEach(npc => npc.update()); + this.interactable_objects.forEach(io => io.update()); for (let key in this.events) { this.events[key].forEach(event => event.update()); } @@ -1111,6 +1113,8 @@ export class Map { io_class = RollablePillar; } else if (interactable_object_db.breakable) { io_class = Breakable; + } else if (interactable_object_db.whirlwind_source) { + io_class = WhirlwindSource; } const allow_jumping_over_it = snapshot_info?.allow_jumping_over_it ?? @@ -1205,6 +1209,12 @@ export class Map { property_info.dest_pos_after_fall, property_info.dest_collision_layer ); + } else if (interactable_object.whirlwind_source) { + (interactable_object as WhirlwindSource).intialize_whirlwind_source( + property_info.dest_point, + property_info.emission_interval, + property_info.speed_factor + ); } this.interactable_objects.push(interactable_object); if (interactable_object.label) { @@ -1238,6 +1248,8 @@ export class Map { (interactable_object as Breakable).intialize_breakable(); } else if (interactable_object.rollable) { (interactable_object as RollablePillar).config_rolling_pillar(this); + } else if (interactable_object.whirlwind_source) { + (interactable_object as WhirlwindSource).config_whirlwind_source(); } if ( (!snapshot_info && interactable_object.base_collision_layer in this._bodies_positions) || diff --git a/base/interactable_objects/InteractableObjects.ts b/base/interactable_objects/InteractableObjects.ts index 90e6c93a58..9f4e3b5edb 100644 --- a/base/interactable_objects/InteractableObjects.ts +++ b/base/interactable_objects/InteractableObjects.ts @@ -113,6 +113,7 @@ export class InteractableObjects { protected _is_rope_dock: boolean; protected _rollable: boolean; protected _breakable: boolean; + protected _whirlwind_source: boolean; protected _extra_sprites: (Phaser.Sprite | Phaser.Graphics | Phaser.Group)[]; public allow_jumping_over_it: boolean; public allow_jumping_through_it: boolean; @@ -245,6 +246,7 @@ export class InteractableObjects { this._is_rope_dock = false; this._rollable = false; this._breakable = false; + this._whirlwind_source = false; this.tile_events_info = {}; for (let index in events_info) { this.tile_events_info[+index] = events_info[index]; @@ -391,6 +393,9 @@ export class InteractableObjects { get breakable() { return this._breakable; } + get whirlwind_source() { + return this._whirlwind_source; + } /** When enable is false, the io is on the map, but a char can't interact with it. */ get enable() { return this._enable; @@ -1407,6 +1412,11 @@ export class InteractableObjects { */ custom_unset() {} + /** + * Method to be overriden. + */ + update() {} + add_unset_callback(callback: () => void) { this.on_unset_callbacks.push(callback); } diff --git a/base/interactable_objects/WhirlwindSource.ts b/base/interactable_objects/WhirlwindSource.ts new file mode 100644 index 0000000000..5249318102 --- /dev/null +++ b/base/interactable_objects/WhirlwindSource.ts @@ -0,0 +1,291 @@ +import {get_centered_pos_in_px, get_distance} from "../utils"; +import {SpriteBase} from "../SpriteBase"; +import {InteractableObjects} from "./InteractableObjects"; +import * as numbers from "../magic_numbers"; + +export class WhirlwindSource extends InteractableObjects { + private static readonly DEFAULT_EMISSION_INTERVAL = 4000; + private static readonly WHIRLWIND_SPRITE_KEY = "whirlwind"; + private static readonly WHIRLWIND_INIT_DURATION = 250; + private static readonly WHIRLWIND_SCALE_X = 1.0; + private static readonly WHIRLWIND_SCALE_Y = 1.0; + private static readonly WHIRLWIND_SPEED = 0.004; + private static readonly WHIRLWIND_BODY_RADIUS = 4.0; + + private _dest_point: {x: number; y: number}; + private _emission_interval: number; + private _emission_timer: Phaser.Timer; + private _whirlwinds: Phaser.Sprite[]; + private _whirlwind_sprite_base: SpriteBase; + private _hero_collided: boolean; + private _misc_busy_prev_state: boolean; + private _collision_prev_state: boolean; + private _collided_whirlwind: Phaser.Sprite; + private _tweens: Phaser.Tween[]; + private _speed_factor: number; + + constructor( + game, + data, + key_name, + map_index, + x, + y, + storage_keys, + allowed_tiles, + base_collision_layer, + not_allowed_tiles, + object_drop_tiles, + anchor_x, + anchor_y, + scale_x, + scale_y, + block_climb_collision_layer_shift, + events_info, + enable, + entangled_by_bush, + toggle_enable_events, + label, + allow_jumping_over_it, + allow_jumping_through_it, + psynergies_info, + has_shadow, + animation, + action, + snapshot_info, + affected_by_reveal, + active, + visible + ) { + super( + game, + data, + key_name, + map_index, + x, + y, + storage_keys, + allowed_tiles, + base_collision_layer, + not_allowed_tiles, + object_drop_tiles, + anchor_x, + anchor_y, + scale_x, + scale_y, + block_climb_collision_layer_shift, + events_info, + enable, + entangled_by_bush, + toggle_enable_events, + label, + allow_jumping_over_it, + allow_jumping_through_it, + psynergies_info, + has_shadow, + animation, + action, + snapshot_info, + affected_by_reveal, + active, + visible + ); + this._whirlwind_source = true; + this._whirlwinds = []; + this._tweens = []; + this._whirlwind_sprite_base = this.data.info.misc_sprite_base_list[WhirlwindSource.WHIRLWIND_SPRITE_KEY]; + this._hero_collided = false; + } + + intialize_whirlwind_source( + dest_point: WhirlwindSource["_dest_point"], + emission_interval: WhirlwindSource["_emission_interval"], + speed_factor: WhirlwindSource["_speed_factor"] + ) { + this._dest_point = dest_point; + this._emission_interval = emission_interval ?? WhirlwindSource.DEFAULT_EMISSION_INTERVAL; + this._speed_factor = speed_factor ?? WhirlwindSource.WHIRLWIND_SPEED; + } + + config_whirlwind_source() { + this._emission_timer = this.game.time.create(false); + this._emission_timer.loop(this._emission_interval, this.emit_whirlwind.bind(this)); + this._emission_timer.start(); + } + + private emit_whirlwind() { + if (this.stop_emit()) { + return; + } + const whirlwind = this.get_whirlwind_sprite(); + this.config_whirlwind_body(whirlwind); + this.game.add.tween(whirlwind.scale).to( + { + x: WhirlwindSource.WHIRLWIND_SCALE_X, + y: WhirlwindSource.WHIRLWIND_SCALE_Y, + }, + WhirlwindSource.WHIRLWIND_INIT_DURATION, + Phaser.Easing.Linear.None, + true + ); + this._whirlwinds.push(whirlwind); + const trajectory_duration = + (get_distance(this.tile_x_pos, this._dest_point.x, this.tile_y_pos, this._dest_point.y) / + this._speed_factor) | + 0; + const tween = this.game.add.tween(whirlwind.body).to( + { + x: get_centered_pos_in_px(this._dest_point.x, this.data.map.tile_width), + y: get_centered_pos_in_px(this._dest_point.y, this.data.map.tile_height), + }, + trajectory_duration, + Phaser.Easing.Linear.None, + true + ); + this._tweens.push(tween); + tween.onUpdateCallback(() => { + if (!tween.isPaused && this.stop_emit()) { + tween.pause(); + if (this.data.map.paused) { + whirlwind.visible = false; + } + } + if (this._hero_collided) { + this.data.hero.body.x = this._collided_whirlwind.x; + this.data.hero.body.y = this._collided_whirlwind.y - 3; + this.data.hero.update_on_event(); + } + }); + tween.onComplete.addOnce(() => { + this._whirlwinds = this._whirlwinds.filter(w => w !== whirlwind); + whirlwind.destroy(true); + this._tweens = this._tweens.filter(t => t !== tween); + if (this._hero_collided && whirlwind === this._collided_whirlwind) { + this._hero_collided = false; + this._collided_whirlwind = null; + this.data.hero.set_rotation(false); + const sign = { + x: Math.sign(this._dest_point.x - this.tile_x_pos), + y: Math.sign(this._dest_point.y - this.tile_y_pos), + }; + const final_hero_pos = { + x: get_centered_pos_in_px(this._dest_point.x - sign.x, this.data.map.tile_width), + y: get_centered_pos_in_px(this._dest_point.y - sign.y, this.data.map.tile_height), + }; + this.data.camera.enable_shake(true); + this.game.add + .tween(this.data.hero.body) + .to( + { + x: this.data.hero.x - 6 * sign.x, + y: this.data.hero.y - 8, + }, + 40, + Phaser.Easing.Linear.None, + true + ) + .onComplete.addOnce(() => { + this.game.add + .tween(this.data.hero.body) + .to( + { + x: final_hero_pos.x, + y: final_hero_pos.y, + }, + 60, + Phaser.Easing.Linear.None, + true + ) + .onComplete.addOnce(() => { + this.data.camera.disable_shake(); + this.data.hero.toggle_collision(this._collision_prev_state); + this.data.hero.update_shadow(); + this.data.hero.shadow.visible = true; + this.data.hero.misc_busy = this._misc_busy_prev_state; + }); + }); + } + }); + } + + update() { + if (this.stop_emit()) { + return; + } + this._tweens.forEach(tween => { + if (tween.isPaused) { + tween.resume(); + tween.target.sprite.visible = true; + } + }); + } + + private stop_emit() { + return ( + this.data.menu_open || + this.data.save_open || + this.data.shop_open || + this.data.healer_open || + this.data.inn_open || + this.data.map.paused || + this.data.hero.casting_psynergy + ); + } + + private get_whirlwind_sprite() { + const sprite_key = this._whirlwind_sprite_base.getSpriteKey(WhirlwindSource.WHIRLWIND_SPRITE_KEY); + const whirlwind = this.game.add.sprite(0, 0, sprite_key); + this.data.middlelayer_group.add(whirlwind); + whirlwind.base_collision_layer = this.base_collision_layer; + this._whirlwind_sprite_base.setAnimation(whirlwind, WhirlwindSource.WHIRLWIND_SPRITE_KEY); + const blow_key = this._whirlwind_sprite_base.getAnimationKey(WhirlwindSource.WHIRLWIND_SPRITE_KEY, "blow"); + whirlwind.play(blow_key); + whirlwind.anchor.setTo(0.5, 0.6); + whirlwind.scale.setTo(0, 0); + whirlwind.centerX = get_centered_pos_in_px(this.tile_x_pos, this.data.map.tile_width); + whirlwind.centerY = get_centered_pos_in_px(this.tile_y_pos, this.data.map.tile_height); + return whirlwind; + } + + private config_whirlwind_body(whirlwind: Phaser.Sprite) { + this.game.physics.p2.enable(whirlwind, false); + whirlwind.anchor.setTo(0.5, 0.6); + whirlwind.body.clearShapes(); + whirlwind.body.setCircle(WhirlwindSource.WHIRLWIND_BODY_RADIUS); + whirlwind.body.setCollisionGroup( + this.data.collision.interactable_objs_collision_groups[this.base_collision_layer] + ); + whirlwind.body.damping = numbers.MAP_DAMPING; + whirlwind.body.angularDamping = numbers.MAP_DAMPING; + whirlwind.body.setZeroRotation(); + whirlwind.body.fixedRotation = true; + whirlwind.body.dynamic = false; + whirlwind.body.static = true; + whirlwind.body.debug = false; + whirlwind.body.collides(this.data.collision.hero_collision_group); + whirlwind.body.data.shapes[0].sensor = true; + whirlwind.body.onBeginContact.addOnce(() => { + whirlwind.send_to_back = true; + this._misc_busy_prev_state = this.data.hero.misc_busy; + this.data.hero.shadow.visible = false; + this.data.hero.misc_busy = true; + this.data.hero.stop_char(true); + this.data.hero.set_rotation(true, 20); + this._collision_prev_state = this.data.hero.shapes_collision_active; + this.data.hero.toggle_collision(false); + this._collided_whirlwind = whirlwind; + this._hero_collided = true; + }); + } + + custom_unset() { + this._collided_whirlwind = null; + this._emission_timer?.stop(); + this._emission_timer?.destroy(); + this._emission_timer = null; + this._tweens.forEach(tween => tween.stop(false)); + this._tweens = null; + this._whirlwinds.forEach(whirlwind => whirlwind.destroy(true)); + this._whirlwinds = null; + } +}