Rexplugin2

Bullet


import * as Phaser from 'phaser';
export class Bullet extends Phaser.Physics.Arcade.Sprite
{
    private player: any;
    constructor(scene: Phaser.Scene, x: number, y: number) {
        // コンストラクタで親クラス (Phaser.Physics.Arcade.Sprite) を呼び出す
        super(scene, x, y, "playbullet");
        scene.physics.add.existing(this);
        this.setActive(false);
        this.setVisible(false);
        scene.time.delayedCall(3000, this.destroyBullet, [], this);
    }
    private destroyBullet(): void {
        this.destroy();
        this.setVisible(false);//bulletをシーンから削除し、メモリを解放する
    }

    // シーンに追加されたときに呼び出される初期化メソッド
    fire(x: number, y: number, speed: number) {
        const body = this.body as Phaser.Physics.Arcade.Body;
        body.reset(this.x, this.y); // 弾丸の位置をリセット
        this.setPosition(x, y);
        this.setActive(true);  // アクティブにする
        this.setVisible(true); // 表示する
        this.setVelocityX(-speed); // 上方向に移動(速度は適宜調整)
    }


    // 更新処理 (シーンのupdateから呼ばれる)
    update(time: number, delta: number) {
        if ( this.x < -50 || this.x > 950*3 + 50) {
            this.setActive(false);
            this.setVisible(false);
        }
    }
}

/////////////////////
Enemy



import * as Phaser from 'phaser';
export class Enemy extends Phaser.Physics.Arcade.Sprite {
    constructor(scene: Phaser.Scene, x: number, y: number) {
        super(scene, x, y, 'enemy'); // 'enemy'は事前にロードされた画像のキー

        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.setCollideWorldBounds(true).setDepth(2);
        this.setBounce(0.1).setScale(1.8);
        this.createAnimations(scene);
    }

    createAnimations(scene: Phaser.Scene) {
        scene.anims.create({
            key: "left2", frameRate: 2, repeat: -1,
            frames: this.anims.generateFrameNumbers('enemy', {start: 4, end: 5}),
        });
        scene.anims.create({
            key: "right2", frameRate: 2, repeat: -1,
            frames: this.anims.generateFrameNumbers('enemy', {start: 6, end: 7}),
        });

    }


    update() {

    }

}

////////////////////
MyPlayer


import * as Phaser from 'phaser';
export class MyPlayer extends Phaser.Physics.Arcade.Sprite {
    private speed: number = 200;
    private readonly animKeyRun: string = 'run';
    private readonly animKeyLeft: string = 'left';
    private readonly animKeyIdle: string = 'idle';
    private readonly animKeyRight: string = 'right';
    private joystick: any;

    constructor(scene: Phaser.Scene, x: number, y: number, texture: string) {
        super(scene, x, y, texture);

        // シーンに追加
        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.setBounce(0.2).setScale(0.5).setDepth(2);

        // 衝突設定(必要に応じて)
        this.setCollideWorldBounds(true);
        this.createAnimations(scene);
        this.setFlipX(false);
    }

    createAnimations(scene: Phaser.Scene) {
        this.anims.create({
            key: this.animKeyRun,
            frames: this.anims.generateFrameNumbers('player', {
                start: 5,
                end: 8
            }),
            frameRate: 10,
            repeat: -1
        });

        this.anims.create({
            key: this.animKeyRight,
            frames: [{key: 'player', frame: 5}],
            frameRate: 20
        });

        this.anims.create({
            key: this.animKeyLeft,
            frames: [{key: 'player', frame: 2}],
            frameRate: 20
        });

    }

    // ジョイスティックの状態を受け取って更新
    update(joystick: any): void {

        const cursorKeys = joystick.createCursorKeys();

        // 速度の初期化
        this.setVelocityX(0);

        // 左右移動の判定
        if (cursorKeys.left.isDown) {
            this.setVelocityX(-this.speed);
            this.setFlipX(true); // 左を向く
            this.anims.play(this.animKeyRun, true);// アニメーションがある場合
        } else if (cursorKeys.right.isDown) {
            this.setVelocityX(this.speed);
            this.setFlipX(false); // 右を向く
            this.anims.play(this.animKeyRun, true);
        } else {
            this.anims.play(this.animKeyRight, true); // 停止時
        }
        if ( cursorKeys.up.isDown) {
            this.setVelocityY(-200);
            if (this.y < 70) {
                this.setVelocityY(0);
            }

        }

    }


}

//////////////////
ScoreText


import * as Phaser from 'phaser';
export class ScoreText extends Phaser.GameObjects.Text {
    private score: number = 0;

    constructor(scene: Phaser.Scene, x: number, y: number,
                style: Phaser.Types.GameObjects.Text.TextStyle) {
        // 親クラス (Phaser.GameObjects.Text) のコンストラクタを呼び出す
        super(scene, x, y, 'Score:0', style);

        // このテキストオブジェクトをシーンの表示リストに追加する
        scene.add.existing(this);
    }

    // スコアを加算して表示を更新するメソッド
    public addScore(points: number): void {
        this.score += points;
        this.setText('Score:'+ this.score);
        //  this.setText(`Score:${this.score}`);
    }
    // スコアをリセットするメソッド
    public resetScore(): void {
        this.score = 0;
        this.updateScoreText();
    }

    // 現在のスコアを取得するメソッド
    public getScore(): number {
        return this.score;
    }
    private updateScoreText(): void {
        this.setText(`Score: ${this.score}`);
    }
}

/////////////////

GameScene


import * as Phaser from 'phaser';
import {MyPlayer} from "./MyPlayer";
import {Bullet} from "./Bullet";
import {ScoreText} from "./ScoreText";
import {Enemy} from "./Enemy";

export class GameScene extends Phaser.Scene {

    private joystick: any;
    private player!: MyPlayer;
    private rexVirtualJoystick: any;
    private yuka?: Phaser.GameObjects.Rectangle;
    private bullets!: Phaser.Physics.Arcade.Group;
    private bullet: any;
    private coins!: any;
    private scoreText!: ScoreText;
    private coin?: Phaser.GameObjects.GameObject;
    private platforms!: Phaser.Physics.Arcade.StaticGroup;
    private hitSound!: Phaser.Sound.BaseSound;
    private enemy!: Enemy;
    private enemies!: Phaser.GameObjects.Group;
    private isEnemyActive: boolean = false;
    private playerbullets!: Phaser.Physics.Arcade.Group;
    private shootEvent!: Phaser.Time.TimerEvent;
    private hearts: Phaser.GameObjects.Image[] = [];
    private initialHealth: number = 10;
    private currentHealth = 10;
    private maxHealth = 10;

    constructor() {
        super({key: 'GameScene'});
    }

    preload() {
        this.load.image('myblock', 'assets/block.png');
        this.load.spritesheet('player', 'assets/mplayer.png', {
            frameWidth: 127, frameHeight: 168
        });
        this.load.image("ground", "assets/platform.png");
        this.load.image("bullet", "assets/laser.png");
        this.load.image('shoot', 'assets/shoot.png');
        this.load.spritesheet('explosion', 'assets/explosion.png', {
            frameWidth: 64,
            frameHeight: 64,
            endFrame: 23
        });
        this.load.spritesheet('enemy', 'assets/enemy.png', {frameWidth: 32, frameHeight: 32});
        this.load.image('playbullet', 'assets/bullet.png');
        this.load.spritesheet('coin', 'assets/coin.png',
            {frameWidth: 32, frameHeight: 32});
        this.load.audio('coinSound', 'assets/coin.mp3');
        this.load.image('heart', 'assets/heart.png');
    }

    create(): void {
        this.add.image(700, 150, 'myblock').setScale(1.5).setOrigin(0);
        this.add.image(700 * 2, 200, 'myblock').setScale(1.5).setOrigin(0);
        this.add.image(700 * 3, 250, 'myblock').setScale(1.5).setOrigin(0);


        // ハート画像を配置
        for (let i = 0; i < this.maxHealth; i++) {
            const heart = this.add.image(30 + i * 29, 19, 'heart').setScale(0.1).setScrollFactor(0);
            this.hearts.push(heart);
        }

        this.cameras.main.setBounds(0, 0, 950 * 3, 550);
        this.physics.world.setBounds(0, 0, 950 * 3, 550);

        this.platforms = this.physics.add.staticGroup();
        for (let i = 0; i < 3; i++) {
            const x = Phaser.Math.Between(200, 30);
            const y = Phaser.Math.Between(20, -20);
            //   const platform = this.physics.add.sprite(x, y, 'ground');
            const platform = this.physics.add.sprite(0, 0, 'ground');
            platform.setX(400 + (i * 750) + x);
            platform.setY(150 + (i * 23) + y),
                platform.setImmovable(true);
            this.platforms.add(platform);
        }

        this.tweens.add({
            targets: this.platforms.getChildren(),  // グループ内の全オブジェクトを対象にする
            x: '+=100',           // 現在のy座標から100ピクセル下に移動
            duration: 1000,       // 1秒かけて移動
            ease: 'Sine.easeInOut',
            yoyo: true,           // 元に戻る
            repeat: -1,           // 無限ループ
            delay: 0,             // 必要であれば遅延を設定
            stagger: 500 // 順番に動かしたい場合は100msの遅延を設定
        });

        this.yuka = this.add.rectangle(0, 350, 950 * 3, 200, 0x00FFFF).setOrigin(0);
        this.physics.add.existing(this.yuka, true);
        //  this.physics.add.staticGroup(this.yuka);
        this.player = new MyPlayer(this, 300, 280, 'player');
        this.cameras.main.startFollow(this.player);
        this.player.setCollideWorldBounds(true);

        this.cameras.main.startFollow(this.player);
        // オプション: 追従時のオフセットや慣性を設定することも可能
        this.cameras.main.setFollowOffset(-10, 5);
        //  this.cameras.main.setLerp(0.005, 0.005);
        this.physics.add.existing(this.player);


        this.physics.add.collider(this.player, this.yuka);

        // コインを管理する物理グループの作成
        this.coins = this.physics.add.group();
        this.physics.add.existing(this.coins);

        this.physics.add.collider(this.coins, this.yuka);
        this.physics.add.collider(this.player, this.platforms);

        // スコアテキストのスタイル設定
        const style: Phaser.Types.GameObjects.Text.TextStyle = {
            fontSize: '32px',
            color: '#fff',
            //  fontStyle: 'bold',
            fontFamily: 'Courier'
        };
        this.scoreText = new ScoreText(this, 16, 36, style);
        this.scoreText.setScrollFactor(0);


        this.hitSound = this.sound.add('coinSound', {volume: 0.7, delay: 0});

        this.anims.create({
            key: 'explode', // アニメーションの識別キー
            frames: this.anims.generateFrameNumbers('explosion', {
                start: 0, // 開始フレーム番号
                end: 23   // 終了フレーム番号
            }),
            frameRate: 20, // 1秒あたりのフレーム数
            repeat: 0,     // 繰り返し回数 (0で1回のみ再生、-1で無限ループ)
            hideOnComplete: true // アニメーション完了時にスプライトを非表示にする
        });

        this.playerbullets = this.physics.add.group({
            classType: Bullet,
            allowGravity: false,// 使用するカスタムクラスを指定
            maxSize: 50,       // プールできる最大数
            runChildUpdate: true // updateメソッドを自動的に実行させる
        }) as Phaser.Physics.Arcade.Group;

        this.shootEvent = this.time.addEvent({
            delay: 1500,
            callback: this.enemyShoot,
            callbackScope: this,
            loop: true
        });
        this.bullets = this.physics.add.group({
            defaultKey: 'bullet',
            maxSize: 70,
            runChildUpdate: true
        });


        this.anims.create({
            key: 'rotate',
            frames: this.anims.generateFrameNumbers('coin', {start: 0, end: 7}),
            frameRate: 10,
            repeat: -1
        });

        for (let i = 1; i < 15; i++) {
            const randomX = Phaser.Math.Between(0, 945 * 3);
            this.coin = this.physics.add.sprite(randomX, 0, 'coin').setOrigin(1).setScale(2.2).anims.play('rotate');
            //   this.coin.setOffset(0, -10);
            (this.coin.body as Phaser.Physics.Arcade.Body).setOffset(0, -5);
            this.physics.add.collider(this.coin, this.yuka);
            this.physics.add.existing(this.coin);
            this.physics.add.collider(this.coin, this.platforms);

            this.physics.add.collider(this.playerbullets, this.coin, (bullet, coin) => {
                (coin as Phaser.Physics.Arcade.Sprite).disableBody(true, true);
                const pos = coin as Phaser.Physics.Arcade.Sprite;
                if (this.player.flipX) {
                    this.createExplosion(pos.x - 50, pos.y);
                } else {
                    this.createExplosion(pos.x + 50, pos.y);
                }
                this.hitSound.play();

                this.scoreText.addScore(10);
                //  this.score += 10;
                //    this.scoreText.setText('Score: ' + this.score);
                bullet.destroy();
            });

        }


        this.joystick = (this.plugins.get('rexVirtualJoystick') as any).add(this, {
            x: 150,
            y: 470,
            radius: 100,
            base: this.add.circle(0, 0, 100, 0x0000FF).setAlpha(0.1),
            thumb: this.add.circle(0, 0, 50, 0xFFA500).setAlpha(0.5),
        })
            .on('update', this.update, this);

        this.enemies = this.physics.add.group({runChildUpdate: true});

        const x = Phaser.Math.Between(800, 900);
        //    const y = Phaser.Math.Between(310, 316);
        this.enemy = new Enemy(this, x, 320);
        this.enemies.add(this.enemy);

        this.enemy.setCollideWorldBounds(true);
        this.physics.add.collider(this.enemy, this.yuka);
        this.enemy.anims.play('left2', true);
        this.enemy.setVelocityX(-150); // 最初は右へ
        this.isEnemyActive = false;
        this.enemy.setVisible(false);
        (this.enemy.body as Phaser.Physics.Arcade.Body).setEnable(false);


        this.input.addPointer(2);
        const jumpButton = this.add.image(850, 480, 'shoot').setScale(0.4).setAlpha(0.5);
        jumpButton.setInteractive().setScrollFactor(0);
        jumpButton.on("pointerdown", () => {
            this.shoot();

        });
        jumpButton.on("pointerup", () => {
            //   this.shoot();
        });
        this.physics.add.collider(this.playerbullets, [this.enemies, this.bullets], (bullet, enemy) => {
            (enemy as Phaser.Physics.Arcade.Sprite).disableBody(true, true);
            const star = bullet as Phaser.Physics.Arcade.Sprite;
            star.disableBody(true, true);
            if (this.enemies.countActive(true) === 0) {
                //  this.spawnNewStars(3);
            }
            // 1秒後に新しい位置に再出現
            this.time.delayedCall(2000, () => {
                //   this.spawnEnemy(16);
                this.spawnEnemy(6);
            });
        });


        this.physics.add.collider(this.player, this.bullets, (myplayer, myenemy) => {
            //  this.hp =100;
            const tar = myplayer as Phaser.Physics.Arcade.Sprite;
            const myeny = myenemy as Phaser.Physics.Arcade.Sprite;
            myeny.disableBody(true, true);

            tar.setTint(0xff0000);
            this.time.addEvent({
                delay: 200,
                callback: () => {
                    tar.setTint(0xffffff);
                }
            });
            this.damage();
        });
    }

    damage() {
        if (this.currentHealth > 0) {
            this.currentHealth -= 1;

            // 配列の最後のハートを空のハートに切り替えるか、破棄する
            const heartToRemove = this.hearts.pop();
            if (heartToRemove) {
                heartToRemove.destroy(); // 画像を破棄
            }
            if (this.currentHealth === 0) {
                this.add.text(460, 140, 'GAME OVER',
                    {fontSize: '84px', color: '#ff0000'}).setOrigin(0.5).setScrollFactor(0);
                this.physics.pause();
                this.player.setVisible(false);
                this.time.delayedCall(3000, () => {
                    this.initialHealth = 10;
                    this.scene.start('GameScene');

                });
                //   this.player.setVisible(false);
            }
        }
    }

    private showGameOverText() {
        this.add.text(950 / 2, 550 / 2 - 100, 'Game over!\nClick to Restart',
            {fontSize: '64px', color: '#ff0000'}).setOrigin(0.5).setAlign('center');
        this.input.once('pointerdown', () => {
            //   this.updateEnemyBehavior();
            this.scene.restart();
        });

    }

    private createExplosion(x: number, y: number) {
        const explosion = this.add.sprite(x, y, 'explosion');
        explosion.play('explode');
        explosion.on('animationcomplete', () => {
            explosion.destroy(); // アニメが終わったら消す
        });
    }

    private spawnEnemy(count: number) {
        for (let i = 0; i < count; i++) {
            const newX = Phaser.Math.Between(800, 1800);
            const newY = Phaser.Math.Between(314, 313);
            if (!this.enemy) {
                //    this.enemy = this.physics.add.sprite(newX, newY, 'enemy');
                this.enemy = new Enemy(this, newX, newY);
            } else {
                this.enemy.enableBody(true, newX, newY, true, true);
            }
        }
    }


    shoot() {
        // プールから利用可能な弾丸を取得する
        //   const bulletY = this.player.y - (this.player.height / 2) + 10;
        const bullet = this.playerbullets.get(this.player.x, this.player.y).setScale(0.1) as Bullet | null;

        if (bullet) {
            if (this.player.flipX) {
                bullet.fire(this.player.x + 9, this.player.y + 11, 800);
            } else {
                bullet.fire(this.player.x + 9, this.player.y + 11, -800);
            }
        }
    }

    private enemyShoot() {
        if (!this.isEnemyActive || !(this.enemy.body as Phaser.Physics.Arcade.Body).enable) return;
        const bullet = this.bullets.get(this.enemy.x, this.enemy.y);
        if (bullet) {
            bullet.setActive(true).setVisible(true);
            bullet.body.allowGravity = false;
            bullet.enableBody = true;
            bullet.setScale(0.6);
            this.time.delayedCall(2200, () => {
                bullet.destroy();
                bullet.setVisible(false);
            });
            // プレイヤーへの角度を計算
            const angle = Phaser.Math.Angle.Between(this.enemy.x, this.enemy.y, this.player.x, this.player.y);
            if (this.player.x < this.enemy.x) {
                this.enemy.setFlipX(false);
            } else {
                this.enemy.setFlipX(true);
            }
            this.physics.velocityFromRotation(angle, 500, bullet.body.velocity);
        }
    }

    fireShot() {
        const bulletCount = 5; // 発射する数
        const spreadAngle = Phaser.Math.DegToRad(15); // 広がる範囲の合計(30度)
        const startAngle = this.player.rotation - spreadAngle / 2; // 開始角度
        const angleStep = spreadAngle / (bulletCount - 1); // 弾ごとの角度の差
        let bulletSpeed = 400; // 弾の速度

        for (let i = 0; i < bulletCount; i++) {
            // 1. 各弾の角度を計算
            const angle = startAngle + (angleStep * i);
            // 2. 弾(Sprite)を生成
            const bullet = this.playerbullets.get(this.player.x, this.player.y).setScale(0.1);
            if (bullet) {
                bullet.setActive(true);
                bullet.setVisible(true);
                if (bullet) {
                    if (this.player.flipX) {
                        //   bullet.fire(this.player.x, this.player.y + 5, 800);
                        this.physics.velocityFromRotation(angle, -bulletSpeed, bullet.body.velocity);
                    } else {
                        this.physics.velocityFromRotation(angle, bulletSpeed, bullet.body.velocity);
                        //  bullet.fire(this.player.x, this.player.y + 5, -800);
                    }
                }
            }
        }
    }

    private updateEnemyBehavior() {
        const distance = Phaser.Math.Distance.Between(
            this.player.x, this.player.y,
            this.enemy.x, this.enemy.y
        );
        // 1. 出現距離チェック
        if (!this.isEnemyActive && distance < 300) {
            this.isEnemyActive = true;
            this.enemy.setVisible(true);
            (this.enemy.body as Phaser.Physics.Arcade.Body).setEnable(true);
        }
        if (!this.isEnemyActive) return;

        // 2. 停止距離チェック&移動
        if (distance > 300) {
            // プレイヤーに向かって移動
            this.physics.moveToObject(this.enemy, this.player, 190);
            if (this.player.y < this.enemy.y) {
                this.enemy.setVelocityY(0);
            }
        } else {
            this.enemy.setVelocity(0);
        }
    }

    update() {
        this.updateEnemyBehavior();
        if (this.player) {
            this.player.update(this.joystick);
        }

    }

}

/////////////////

main.ts


import * as Phaser from 'phaser';
import VirtualJoystickPlugin from 'phaser4-rex-plugins/plugins/virtualjoystick-plugin.js';

import { GameScene } from './scenes/GameScene';

const config: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 950,
    height: 550,
    backgroundColor: '#cccccc',
    // antialias: false,
    disableContextMenu: true,
    banner: false,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH },
    parent: 'app',

    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 400 },
            debug: false,
        },
    },
    plugins: {
        global: [{
            key: 'rexVirtualJoystick',
            plugin: VirtualJoystickPlugin,
            start: true
        },
        ]
    },
    scene: [GameScene],
};
window.addEventListener('load', () => {
    new Phaser.Game(config);
});