diff --git a/apps/game/src/main.ts b/apps/game/src/main.ts index bf17e8a..5beff80 100644 --- a/apps/game/src/main.ts +++ b/apps/game/src/main.ts @@ -21,6 +21,7 @@ attack: ['Space'], // вне диалога: тап — удар, удержание — резонанс menu: ['Escape'], inventory: ['KeyI'], + debug: ['F3'], up: ['KeyW', 'ArrowUp'], down: ['KeyS', 'ArrowDown'], left: ['KeyA', 'ArrowLeft'], diff --git a/apps/game/src/scenes/LocationScene.ts b/apps/game/src/scenes/LocationScene.ts index 5c21663..483f7ca 100644 --- a/apps/game/src/scenes/LocationScene.ts +++ b/apps/game/src/scenes/LocationScene.ts @@ -10,6 +10,8 @@ isoToScreen, screenToIsoExact, inCircle, + DebugOverlay, + VirtualJoystick, DEFAULT_ISO, type Camera, type Entity, @@ -60,6 +62,9 @@ private repathTimer = 0; /** Таймер мигания при неуязвимости (свой, не боевой). */ private blinkT = 0; + /** Тач-джойстик (активен только для касаний). */ + private joystick: VirtualJoystick; + private debug: DebugOverlay; constructor( private game: Game, @@ -185,6 +190,15 @@ text.position.set(6, 4); this.hint.addChild(text); this.game.renderer.uiRoot.addChild(this.hint); + + // Тач-джойстик: перехватывает касания (мышь проходит насквозь). + this.joystick = new VirtualJoystick({ screen: { width: 480, height: 270 } }); + this.joystick.eventMode = 'none'; // включается при первом касании + this.game.renderer.uiRoot.addChild(this.joystick); + + // Дебаг-оверлей (F3). + this.debug = new DebugOverlay(false); + this.game.renderer.uiRoot.addChild(this.debug.view); } enter(): void { @@ -201,6 +215,8 @@ this.healthBar.destroy({ children: true }); this.hint.destroy({ children: true }); this.locationLabel.destroy({ children: true }); + this.joystick.destroy({ children: true }); + this.debug.view.destroy({ children: true }); } update(dt: number): void { @@ -229,6 +245,10 @@ return; } + if (input.isActionJustPressed('debug')) { + this.debug.view.visible = !this.debug.view.visible; + } + // --- ввод боя: тап = короткий удар, удержание = заряд резонанса --- if (input.isActionJustPressed('attack')) { this.playerCombat.startCharge(); @@ -239,7 +259,12 @@ const pointer = input.getPointer(); if (pointer.justPressed) { - this.handleWorldClick(pointer.x, pointer.y); + if (pointer.isTouch) { + // Касание отдаём джойстику (мышь идёт в мир обычным порядком). + this.joystick.eventMode = 'static'; + } else { + this.handleWorldClick(pointer.x, pointer.y); + } } // --- боевой мир --- @@ -255,11 +280,22 @@ this.player.view.alpha = this.playerCombat.invuln && Math.floor(this.blinkT * 8) % 2 === 0 ? 0.4 : 1; - this.player.update(dt); + if (this.joystick.active) { + this.player.moveFree(this.joystick.getVector(), dt); + } else { + this.player.update(dt); + } const tile = this.player.currentTile(); this.actors.setDepth(this.player.view, tile.x, tile.y); this.updateCameraFollow(); this.checkExits(tile); + + this.debug.setLines([ + `tile ${tile.x},${tile.y}`, + `pos ${Math.round(this.player.position.x)},${Math.round(this.player.position.y)}`, + `hp ${this.playerCombat.hp} kills ${this.game.state.getNumber('kills')}` + ]); + this.debug.update(dt); } /** Переход между локациями: герой наступил на тайл-триггер. */ diff --git a/apps/game/src/systems/PlayerController.ts b/apps/game/src/systems/PlayerController.ts index 526b4e8..e994d7f 100644 --- a/apps/game/src/systems/PlayerController.ts +++ b/apps/game/src/systems/PlayerController.ts @@ -133,6 +133,30 @@ ); } + /** Прямое движение (тач-джойстик/стик): вектор -1..1, движение с проверкой стен. */ + moveFree(dir: Vec2, dt: number): void { + const len = Math.hypot(dir.x, dir.y); + if (len < 0.01) { + this.stop(); + return; + } + const step = this.speed * dt; + const nx = this.pos.x + (dir.x / len) * step; + const ny = this.pos.y + (dir.y / len) * step; + // Двигаемся только если новая точка внутри карты и не в стене + const tile = screenToIsoExact(nx, ny, this.map.data.width, this.map.data.height, DEFAULT_ISO); + if (tile && this.map.isWalkable(tile.x, tile.y)) { + this.setFacing( + Math.abs(dir.x) >= Math.abs(dir.y) ? (dir.x >= 0 ? 'right' : 'left') : dir.y >= 0 ? 'down' : 'up' + ); + this.pos = { x: nx, y: ny }; + this.anim.update(dt); + this.view.position.set(Math.round(this.pos.x), Math.round(this.pos.y)); + } else { + this.stop(); + } + } + /** Позиция героя (ноги) в виртуальных пикселях. */ get position(): Vec2 { return { x: this.pos.x, y: this.pos.y }; @@ -169,23 +193,19 @@ if (!this.waypoint) return; const dx = this.waypoint.x - this.pos.x; const dy = this.waypoint.y - this.pos.y; - let facing: Facing = this.facing; - if (Math.abs(dx) >= Math.abs(dy)) { - facing = dx >= 0 ? 'right' : 'left'; - } else { - facing = dy >= 0 ? 'down' : 'up'; - } - if (facing !== this.facing) { - this.facing = facing; - const frames = - facing === 'down' - ? this.textures.down - : facing === 'up' - ? this.textures.up - : this.textures.side; - this.anim.setFrames([...frames], false); - this.sprite.scale.x = facing === 'right' ? -1 : 1; - } + this.setFacing( + Math.abs(dx) >= Math.abs(dy) ? (dx >= 0 ? 'right' : 'left') : dy >= 0 ? 'down' : 'up' + ); + } + + /** Сменить направление взгляда (кадры + флип спрайта). */ + private setFacing(facing: Facing): void { + if (facing === this.facing) return; + this.facing = facing; + const frames = + facing === 'down' ? this.textures.down : facing === 'up' ? this.textures.up : this.textures.side; + this.anim.setFrames([...frames], false); + this.sprite.scale.x = facing === 'right' ? -1 : 1; } private advanceWaypoint(): void {