feat: směrové ovládání tanků, větší mapy, zvýraznění vlastního tanku a legenda bonusů
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d4485db0b6
commit
5d13efb685
6 files changed
+330
-146
No files matched your search
@@ -1,4 +1,6 @@
|
||||
[
|
||||
"U oběda v restauraci si můžete zadat cenu (ikonka cenovky u vaší volby) — promítne se do měsíčního přehledu stravování a předvyplní se i v „Zaplatit za všechny“",
|
||||
"V lobby miniher (Pong, Reakční hra, Had, Tetris, Tanky) si hráči mohou psát v chatu, emotikony se vkládají jedním kliknutím 😉"
|
||||
"V lobby miniher (Pong, Reakční hra, Had, Tetris, Tanky) si hráči mohou psát v chatu, emotikony se vkládají jedním kliknutím 😉",
|
||||
"Tanky mají nově výchozí směrové ovládání — šipka nebo WSAD určuje směr jízdy na mapě; původní tankové ovládání (vpřed/vzad a otáčení) lze zapnout volbou Ovládání",
|
||||
"Tanky: větší mapy s více překážkami, vlastní tank je výrazně označený (při startu a po návratu do hry na něj ukazuje šipka) a pod hrací plochou je legenda bonusů"
|
||||
]
|
||||
+145
-115
@@ -20,8 +20,8 @@ import { Clock, defaultClock, GameError, gameHandler, minPlayersForEnv } from ".
|
||||
*/
|
||||
|
||||
export const TILE = 40;
|
||||
export const COLS = 24;
|
||||
export const ROWS = 15;
|
||||
export const COLS = 28;
|
||||
export const ROWS = 17;
|
||||
export const FIELD_WIDTH = COLS * TILE;
|
||||
export const FIELD_HEIGHT = ROWS * TILE;
|
||||
export const TICK_MS = 33;
|
||||
@@ -127,21 +127,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Aréna',
|
||||
biome: 'city',
|
||||
half: [
|
||||
"S..........+",
|
||||
"............",
|
||||
"..##........",
|
||||
"..#.....#...",
|
||||
"........#...",
|
||||
".....+......",
|
||||
"....##......",
|
||||
"S..........+",
|
||||
"....##......",
|
||||
".....+......",
|
||||
"........#...",
|
||||
"..#.....#...",
|
||||
"..##........",
|
||||
"............",
|
||||
"S..........+",
|
||||
"S..........++.",
|
||||
"..............",
|
||||
"..##..........",
|
||||
"..#.....#.....",
|
||||
"........#..+..",
|
||||
".....+........",
|
||||
"....##....#...",
|
||||
"..........#...",
|
||||
"S......+......",
|
||||
"..........#...",
|
||||
"....##....#...",
|
||||
".....+........",
|
||||
"........#..+..",
|
||||
"..#.....#.....",
|
||||
"..##..........",
|
||||
"..............",
|
||||
"S..........++.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -149,21 +151,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Bludiště',
|
||||
biome: 'city',
|
||||
half: [
|
||||
"S...#.......",
|
||||
"..#.#.####..",
|
||||
"..#......#..",
|
||||
"..####.#.#..",
|
||||
".......#....",
|
||||
"##.+.###.+.#",
|
||||
"....+.......",
|
||||
"S.#####..+..",
|
||||
"....+.......",
|
||||
"##.+.###.+.#",
|
||||
".......#....",
|
||||
"..####.#.#..",
|
||||
"..#......#..",
|
||||
"..#.#.####..",
|
||||
"S...#.......",
|
||||
"S...#.........",
|
||||
"..#.#.####....",
|
||||
"..#......#..+.",
|
||||
"..####.#.#....",
|
||||
".......#...##.",
|
||||
"##.+.###......",
|
||||
"....+.....+...",
|
||||
".#.......##...",
|
||||
"S.#####...+...",
|
||||
".#.......##...",
|
||||
"....+.....+...",
|
||||
"##.+.###......",
|
||||
".......#...##.",
|
||||
"..####.#.#....",
|
||||
"..#......#..+.",
|
||||
"..#.#.####....",
|
||||
"S...#.........",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -171,21 +175,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Sklad',
|
||||
biome: 'city',
|
||||
half: [
|
||||
"S.....+.....",
|
||||
".++.+++.++..",
|
||||
".++.....++.+",
|
||||
"....##......",
|
||||
"+.+.##.+.+..",
|
||||
"....+.......",
|
||||
".#..+.###...",
|
||||
"S...+...#..+",
|
||||
".#..+.###...",
|
||||
"....+.......",
|
||||
"+.+.##.+.+..",
|
||||
"....##......",
|
||||
".++.....++.+",
|
||||
".++.+++.++..",
|
||||
"S.....+.....",
|
||||
"S.....+.......",
|
||||
".++.+++.++..+.",
|
||||
".++.....++....",
|
||||
"....##......+.",
|
||||
"+.+.##.+.+....",
|
||||
"....+......##.",
|
||||
".#..+.###.....",
|
||||
".#......#..+..",
|
||||
"S...+...#.....",
|
||||
".#......#..+..",
|
||||
".#..+.###.....",
|
||||
"....+......##.",
|
||||
"+.+.##.+.+....",
|
||||
"....##......+.",
|
||||
".++.....++....",
|
||||
".++.+++.++..+.",
|
||||
"S.....+.......",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -193,21 +199,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Les',
|
||||
biome: 'forest',
|
||||
half: [
|
||||
"S..%%...#...",
|
||||
"..%%%..##...",
|
||||
"......%%...%",
|
||||
"#..#.......%",
|
||||
"##...%%.....",
|
||||
"....%%%..#..",
|
||||
".#......%%..",
|
||||
"~~.~~~~.~~~~",
|
||||
"..%%....#...",
|
||||
"S.%%%.....+.",
|
||||
"....##..%%..",
|
||||
"#........%%.",
|
||||
"##..%%..#...",
|
||||
"...%%%..##..",
|
||||
"S......+....",
|
||||
"S..%%...#..#..",
|
||||
"..%%%..##...%.",
|
||||
"......%%...%%.",
|
||||
"#..#.......%..",
|
||||
"##...%%..#...#",
|
||||
"....%%%..##...",
|
||||
".#......%%..#.",
|
||||
"~~.~~~~.~~~~.~",
|
||||
"S.%%%.....+...",
|
||||
"..%%..##..%%..",
|
||||
"#.......#..%..",
|
||||
"##..%%.....#..",
|
||||
"...%%%..##....",
|
||||
".#.....%%...#.",
|
||||
"...##...%..~~.",
|
||||
"..%%..#....~~.",
|
||||
"S......+......",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -215,21 +223,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Poušť',
|
||||
biome: 'desert',
|
||||
half: [
|
||||
"S.....##....",
|
||||
"...::.......",
|
||||
"..::::..#...",
|
||||
".......##...",
|
||||
"#.........~~",
|
||||
"##..+....~~~",
|
||||
"......#..~~~",
|
||||
"S.....#...~~",
|
||||
"......#..~~~",
|
||||
"##..+....~~~",
|
||||
"#.........~~",
|
||||
".......##...",
|
||||
"..::::..#...",
|
||||
"...::.......",
|
||||
"S.....##....",
|
||||
"S.....##......",
|
||||
"...::.....#...",
|
||||
"..::::..#...::",
|
||||
".......##.....",
|
||||
"#..#......~~..",
|
||||
"##..+....~~~..",
|
||||
"......#..~~~.#",
|
||||
"..#...#...~~..",
|
||||
"S.....#..+....",
|
||||
"..#...#...~~..",
|
||||
"......#..~~~.#",
|
||||
"##..+....~~~..",
|
||||
"#..#......~~..",
|
||||
".......##.....",
|
||||
"..::::..#...::",
|
||||
"...::.....#...",
|
||||
"S.....##......",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -237,21 +247,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Zima',
|
||||
biome: 'winter',
|
||||
half: [
|
||||
"S...%%....#.",
|
||||
".::.%%..::..",
|
||||
".::.....::..",
|
||||
"....##......",
|
||||
"%%..##..~~..",
|
||||
"%%......~~..",
|
||||
"....::......",
|
||||
"S..+::..##.+",
|
||||
"....::......",
|
||||
"%%......~~..",
|
||||
"%%..##..~~..",
|
||||
"....##......",
|
||||
".::.....::..",
|
||||
".::.%%..::..",
|
||||
"S...%%....#.",
|
||||
"S...%%....#...",
|
||||
".::.%%..::..#.",
|
||||
".::.....::....",
|
||||
"....##.....%%.",
|
||||
"%%..##..~~....",
|
||||
"%%......~~..#.",
|
||||
"...#::......#.",
|
||||
"....::..##....",
|
||||
"S..+::..##.+..",
|
||||
"....::........",
|
||||
"...#::......#.",
|
||||
"%%......~~..#.",
|
||||
"%%..##..~~....",
|
||||
"....##.....%%.",
|
||||
".::.....::....",
|
||||
".::.%%..::..#.",
|
||||
"S...%%....#...",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -259,21 +271,23 @@ const MAP_HALVES: { id: string, name: string, biome: Biome, half: string[] }[] =
|
||||
name: 'Bažina',
|
||||
biome: 'swamp',
|
||||
half: [
|
||||
"S..::..%%...",
|
||||
"..:::~~%%.::",
|
||||
".::.~~~...::",
|
||||
".....~..#...",
|
||||
"%%.......~~.",
|
||||
"%%%.::::.~~.",
|
||||
"..+.::::....",
|
||||
"S...::....+.",
|
||||
"..+.::::....",
|
||||
"%%%.::::.~~.",
|
||||
"%%.......~~.",
|
||||
".....~..#...",
|
||||
".::.~~~...::",
|
||||
"..:::~~%%.::",
|
||||
"S..::..%%...",
|
||||
"S..::..%%.....",
|
||||
"..:::~~%%.::#.",
|
||||
".::.~~~...::..",
|
||||
".....~..#.....",
|
||||
"%%.......~~.#.",
|
||||
"%%%.::::.~~...",
|
||||
"..+.::::....%%",
|
||||
".#........#.%%",
|
||||
"S...::....+...",
|
||||
".#........#...",
|
||||
"..+.::::....%%",
|
||||
"%%%.::::.~~...",
|
||||
"%%.......~~.#.",
|
||||
".....~..#.....",
|
||||
".::.~~~...::..",
|
||||
"..:::~~%%.::#.",
|
||||
"S..::..%%.....",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -362,7 +376,8 @@ export type TankState = {
|
||||
/** Nastavení hry z lobby (hodnoty přichází od klienta, ověřují se v {@link TankGame.configure}). */
|
||||
export type TankOptions = { mode?: unknown, mapId?: unknown, teams?: unknown, hardcore?: unknown, bounce?: unknown };
|
||||
|
||||
type Input = { drive: number, turn: number, fire: boolean };
|
||||
/** Tankové ovládání (`drive` + `turn`), nebo směrové — `dir` je požadovaný směr jízdy na hřišti. */
|
||||
type Input = { drive: number, turn: number, fire: boolean, dir?: [number, number] };
|
||||
|
||||
type InternalPlayer = TankPlayer & {
|
||||
input: Input,
|
||||
@@ -639,12 +654,18 @@ export class TankGame {
|
||||
this.schedule(() => this.countdownStep(), 1000);
|
||||
}
|
||||
|
||||
/** Stav ovládání hráče: drive −1/0/1 (couvání/stát/jízda), turn −1/0/1 (doleva/rovně/doprava), fire. */
|
||||
/**
|
||||
* Stav ovládání hráče: drive −1/0/1 (couvání/stát/jízda), turn −1/0/1 (doleva/rovně/doprava), fire.
|
||||
* Směrové ovládání místo drive/turn posílá dx, dy −1/0/1 — tank se natočí a jede tím směrem.
|
||||
*/
|
||||
input(login: string, payload: unknown) {
|
||||
const p = this.players.get(login);
|
||||
if (!p || p.bot) return;
|
||||
const data = (payload ?? {}) as { drive?: unknown, turn?: unknown, fire?: unknown };
|
||||
p.input = { drive: axis(data.drive), turn: axis(data.turn), fire: data.fire === true };
|
||||
const data = (payload ?? {}) as { drive?: unknown, turn?: unknown, fire?: unknown, dx?: unknown, dy?: unknown };
|
||||
const fire = data.fire === true;
|
||||
p.input = 'dx' in data || 'dy' in data
|
||||
? { drive: 0, turn: 0, fire, dir: [axis(data.dx), axis(data.dy)] }
|
||||
: { drive: axis(data.drive), turn: axis(data.turn), fire };
|
||||
}
|
||||
|
||||
/** Míření myší: bod na hřišti, kam se má věž natočit; `null` vrátí věž do směru jízdy. */
|
||||
@@ -768,13 +789,22 @@ export class TankGame {
|
||||
}
|
||||
|
||||
private moveTank(p: InternalPlayer, dt: number, now: number) {
|
||||
p.angle = (p.angle + p.input.turn * TANK_TURN_SPEED * dt) % (Math.PI * 2);
|
||||
if (!p.input.drive) return;
|
||||
let speed = p.input.drive > 0 ? TANK_SPEED : TANK_REVERSE_SPEED;
|
||||
let drive = p.input.drive;
|
||||
if (p.input.dir) {
|
||||
const [dx, dy] = p.input.dir;
|
||||
if (!dx && !dy) return;
|
||||
// Korba se rovnou otočí do zvoleného směru a tank jede
|
||||
p.angle = Math.atan2(dy, dx);
|
||||
drive = 1;
|
||||
} else {
|
||||
p.angle = (p.angle + p.input.turn * TANK_TURN_SPEED * dt) % (Math.PI * 2);
|
||||
}
|
||||
if (!drive) return;
|
||||
let speed = drive > 0 ? TANK_SPEED : TANK_REVERSE_SPEED;
|
||||
if (p.turboUntil > now) speed *= TURBO_FACTOR;
|
||||
const ground = this.tileAtPos(p.x, p.y);
|
||||
if (ground === ':' || ground === '%') speed *= MUD_FACTOR;
|
||||
const dist = p.input.drive * speed * dt;
|
||||
const dist = drive * speed * dt;
|
||||
// Po osách zvlášť, aby tank klouzal podél zdí
|
||||
const nx = p.x + Math.cos(p.angle) * dist;
|
||||
if (!this.tankBlocked(p, nx, p.y)) p.x = nx;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
TankGame, TankState, MAPS, COLS, ROWS, TICK_MS, COUNTDOWN_SECONDS, RESPAWN_MS, DEATHMATCH_MS,
|
||||
LAST_MAN_LIVES, MAX_PLAYERS, TANK_RADIUS, ROCKET_AMMO, MINE_AMMO, MINE_ARM_MS,
|
||||
LAST_MAN_LIVES, MAX_PLAYERS, TANK_RADIUS, ROCKET_AMMO, MINE_AMMO, MINE_ARM_MS, FIELD_WIDTH,
|
||||
} from '../tankGame';
|
||||
import { GameError } from '../minigames';
|
||||
import { fakeClock } from './helpers/fakeClock';
|
||||
@@ -94,6 +94,32 @@ describe('tanky', () => {
|
||||
expect(ctx.player('alice').angle).toBe(0);
|
||||
});
|
||||
|
||||
test('směrové ovládání jede rovnou zvoleným směrem', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 300, 300, 0);
|
||||
ctx.game.input('alice', { dx: 0, dy: -1 });
|
||||
ctx.ticks(10);
|
||||
const p = ctx.player('alice');
|
||||
expect(p.angle).toBeCloseTo(-Math.PI / 2, 2);
|
||||
expect(p.x).toBeCloseTo(300, 0);
|
||||
expect(p.y).toBeLessThan(300);
|
||||
});
|
||||
|
||||
test('směrové ovládání bez stisknutého směru tank zastaví a nechá natočený', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 300, 300, 0);
|
||||
ctx.game.input('alice', { dx: -1, dy: 0 });
|
||||
ctx.ticks(1);
|
||||
ctx.game.input('alice', { dx: 0, dy: 0 });
|
||||
const { x, angle } = ctx.player('alice');
|
||||
expect(x).toBeLessThan(300);
|
||||
ctx.ticks(5);
|
||||
expect(ctx.player('alice').angle).toBe(angle);
|
||||
expect(ctx.player('alice').x).toBe(x);
|
||||
});
|
||||
|
||||
test('zásah zničí soupeře, přidá bod a soupeř se po chvíli znovu objeví', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
@@ -397,7 +423,7 @@ describe('tanky — týmy', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('deathmatch', LOGINS, 'arena', true);
|
||||
for (const p of ctx.last().players) {
|
||||
expect(p.x < 480).toBe(p.team === 0);
|
||||
expect(p.x < FIELD_WIDTH / 2).toBe(p.team === 0);
|
||||
}
|
||||
// alice a carol jsou tým 0, bob tým 1
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
|
||||
Reference in new issue
Block a user