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:
hrubyjarandClaude Opus 5.5 authored and batmanisko committed 2026-10-22 10:29:21 +02:00
1 parent d4485db0b6
commit 5d13efb685
6 files changed
+329 -145

No files matched your search

+29 -1
View File
@@ -3,10 +3,17 @@
bottom: 276px;
}
// Hřiště je široké 1120 px — okno je o kus širší než běžné xl, ať se plocha nezmenšuje
@media (min-width: 1200px) {
.tanks-game-modal .modal-dialog {
--bs-modal-width: min(1200px, calc(100vw - 32px));
}
}
.tanks-game-modal {
.tanks-board-wrap {
position: relative;
max-width: 960px;
max-width: 1120px;
margin: 0 auto;
}
@@ -92,6 +99,27 @@
}
}
.tanks-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px 18px;
list-style: none;
padding: 0;
margin: 8px 0 0;
font-size: 0.8rem;
opacity: 0.8;
}
.tanks-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 4px 16px;
margin-top: 8px;
}
.tanks-equipment {
text-align: center;
font-size: 1.1rem;
+69 -15
View File
@@ -31,13 +31,30 @@ const BOT_LEVELS: { value: BotLevel, label: string }[] = [
{ value: 'hard', label: 'Těžký' },
];
const FORWARD_KEYS = new Set(['ArrowUp', 'KeyW']);
const BACK_KEYS = new Set(['ArrowDown', 'KeyS']);
/** Směrové: klávesa = směr jízdy na mapě; tankové: nahoru/dolů jízda vpřed/vzad, doleva/doprava otáčení. */
type Controls = 'directional' | 'tank';
const CONTROLS_KEY = 'tanks_controls';
const CONTROLS: { value: Controls, label: string, hint: string }[] = [
{ value: 'directional', label: 'Směrové', hint: 'šipky / WSAD jízda daným směrem' },
{ value: 'tank', label: 'Tankové', hint: '↑↓ / W S jízda, ←→ / A D otáčení' },
];
function getInitialControls(): Controls {
try {
return localStorage.getItem(CONTROLS_KEY) === 'tank' ? 'tank' : 'directional';
} catch {
// localStorage nedostupný
}
return 'directional';
}
const UP_KEYS = new Set(['ArrowUp', 'KeyW']);
const DOWN_KEYS = new Set(['ArrowDown', 'KeyS']);
const LEFT_KEYS = new Set(['ArrowLeft', 'KeyA']);
const RIGHT_KEYS = new Set(['ArrowRight', 'KeyD']);
const FIRE_KEYS = new Set(['Space']);
const MINE_KEYS = new Set(['KeyE']);
const GAME_KEYS = new Set([...FORWARD_KEYS, ...BACK_KEYS, ...LEFT_KEYS, ...RIGHT_KEYS, ...FIRE_KEYS, ...MINE_KEYS]);
const GAME_KEYS = new Set([...UP_KEYS, ...DOWN_KEYS, ...LEFT_KEYS, ...RIGHT_KEYS, ...FIRE_KEYS, ...MINE_KEYS]);
const formatTime = (ms: number) => {
const s = Math.ceil(ms / 1000);
@@ -66,6 +83,7 @@ export default function TankGame() {
const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const [botLevel, setBotLevel] = useState<BotLevel>('medium');
const [controls, setControls] = useState<Controls>(getInitialControls);
const canvasRef = useRef<HTMLCanvasElement>(null);
const prevPhase = useRef<TankPhase>('idle');
const flashes = useRef<MuzzleFlash[]>([]);
@@ -129,11 +147,10 @@ export default function TankGame() {
let sent = '';
const any = (keys: Set<string>) => [...pressed].some(k => keys.has(k));
const update = () => {
const input = {
drive: (any(FORWARD_KEYS) ? 1 : 0) - (any(BACK_KEYS) ? 1 : 0),
turn: (any(RIGHT_KEYS) ? 1 : 0) - (any(LEFT_KEYS) ? 1 : 0),
fire: any(FIRE_KEYS) || mouseFire,
};
const x = (any(RIGHT_KEYS) ? 1 : 0) - (any(LEFT_KEYS) ? 1 : 0);
const y = (any(DOWN_KEYS) ? 1 : 0) - (any(UP_KEYS) ? 1 : 0);
const fire = any(FIRE_KEYS) || mouseFire;
const input = controls === 'tank' ? { drive: -y, turn: x, fire } : { dx: x, dy: y, fire };
const key = JSON.stringify(input);
if (key === sent) return;
sent = key;
@@ -198,7 +215,16 @@ export default function TankGame() {
window.removeEventListener('contextmenu', onContextMenu);
window.removeEventListener('blur', onBlur);
};
}, [open, inGame, isPlayer, send]);
}, [open, inGame, isPlayer, send, controls]);
const changeControls = (value: Controls) => {
setControls(value);
try {
localStorage.setItem(CONTROLS_KEY, value);
} catch {
// localStorage nedostupný
}
};
// Míření myší: kurzor nad hrací plochou natáčí věž, mimo ni se věž vrátí do směru jízdy
useEffect(() => {
@@ -242,9 +268,16 @@ export default function TankGame() {
}, [state]);
useEffect(() => {
if (canvasRef.current && state && (inGame || phase === 'finished')) {
drawTanks(canvasRef.current, state, flashes.current, login);
}
if (!state || !(inGame || phase === 'finished')) return;
const draw = () => canvasRef.current && drawTanks(canvasRef.current, state, flashes.current, login);
draw();
// Při odpočtu chodí stav jen jednou za sekundu — značka vlastního tanku se animuje překreslováním
if (phase !== 'countdown') return;
let frame = requestAnimationFrame(function loop() {
draw();
frame = requestAnimationFrame(loop);
});
return () => cancelAnimationFrame(frame);
}, [state, inGame, phase, login]);
if (!login) return null;
@@ -296,13 +329,32 @@ export default function TankGame() {
<div className="tanks-status">
{mode.label}{state.teams && ' v týmech'}{state.bounce && ' · odrazy'}{state.hardcore && ' · 🌑 hardcore'} · {mapName}{inGame && ` · zbývá ${formatTime(state.timeLeftMs)}`}
{inGame && (isPlayer
? ' · ↑↓ / W S jízda, ←→ / A D otáčení, myš míření, klik / mezerník střelba, E / pravé tlačítko mina'
? ` · ${CONTROLS.find(c => c.value === controls)!.hint}, myš míření, klik / mezerník střelba, E / pravé tlačítko mina`
: ' · sledujete hru')}
</div>
{inGame && isPlayer && renderControls()}
{inGame && (
<ul className="tanks-legend">
{(Object.entries(PICKUPS) as [PickupType, typeof PICKUPS[PickupType]][])
.filter(([type]) => type !== 'heart' || state.mode === 'lastman')
.map(([type, p]) => <li key={type}>{p.icon} {p.label}</li>)}
</ul>
)}
{renderScores()}
</div>
);
/** Volba ovládání — osobní nastavení hráče, pamatuje si ho prohlížeč. */
const renderControls = () => (
<div className="tanks-controls">
<span className="tanks-option-label">Ovládání</span>
{CONTROLS.map(c => (
<Form.Check key={c.value} inline type="radio" id={`tanks-controls-${c.value}`} name="tanks-controls"
label={c.label} title={c.hint} checked={controls === c.value} onChange={() => changeControls(c.value)} />
))}
</div>
);
const renderScores = () => (
<Table size="sm" className="tanks-scores">
<thead>
@@ -427,6 +479,7 @@ export default function TankGame() {
{state.hardcore && ' V aréně je tma: vidíte jen okolí tanku a kužel reflektoru ve směru věže, světlo zastaví zdi a bedny.'
+ (state.teams ? ' Co osvítí spoluhráč, vidíte také.' : '') + ' Výbuchy a výstřely prosvítí i tmou.'}
</p>
{isPlayer && renderControls()}
{isPlayer && <LobbyChat game="tanks" />}
</>
);
@@ -437,8 +490,8 @@ export default function TankGame() {
return (
<>
<p>
Tanková bitva pro více hráčů. Jezdí se šipkami nebo WSAD, věž míří za myší a střílí se
kliknutím nebo mezerníkem. Bedny se dají rozstřílet, přes vodu se nedá projet (ale dá se
Tanková bitva pro více hráčů. Jezdí se šipkami nebo WSAD (směrově, nebo po tankovém —
vpřed/vzad a otáčení), věž míří za myší a střílí se kliknutím nebo mezerníkem. Bedny se dají rozstřílet, přes vodu se nedá projet (ale dá se
přes ni střílet) a bláto, sníh, písek nebo porost zpomalují. Hraje se na poslední přeživší
nebo na čas, sólo i v týmech, volitelně s odrážením střel od zdí nebo potmě (hardcore);
když není s kým hrát, přidejte si do lobby boty.
@@ -447,6 +500,7 @@ export default function TankGame() {
<ul className="tanks-pickups">
{Object.values(PICKUPS).map(p => <li key={p.icon}>{p.icon} {p.label}</li>)}
</ul>
{renderControls()}
</>
);
case 'lobby':
+56 -12
View File
@@ -84,13 +84,13 @@ export const TEAM_NAMES = ['Červení', 'Modří'];
export const PICKUPS: Record<PickupType, { icon: string, label: string }> = {
shield: { icon: '🛡️', label: 'štít — vydrží jeden zásah' },
triple: { icon: '🔱', label: 'trojitá střela' },
triple: { icon: '🔱', label: 'trojitá střela — tři střely do vějíře' },
rocket: { icon: '🚀', label: 'rakety — výbuch zasáhne i okolí' },
turbo: { icon: '💨', label: 'turbo' },
ghost: { icon: '👻', label: 'neviditelnost' },
homing: { icon: '🧲', label: 'naváděné střely' },
mine: { icon: '💣', label: 'miny (E / pravé tlačítko)' },
heart: { icon: '❤️', label: 'život navíc' },
turbo: { icon: '💨', label: 'turbo — rychlejší jízda' },
ghost: { icon: '👻', label: 'neviditelnost — soupeř vás uvidí jen zblízka' },
homing: { icon: '🧲', label: 'naváděné střely — stáčí se k soupeři' },
mine: { icon: '💣', label: 'miny — položíte E / pravým tlačítkem' },
heart: { icon: '❤️', label: 'život navíc (jen na přežití)' },
};
export const BIOMES: Record<Biome, { icon: string }> = {
@@ -477,9 +477,48 @@ export function drawTanks(canvas: HTMLCanvasElement, state: TankState, flashes:
}
for (const e of state.explosions) drawExplosion(ctx, e);
for (const p of tanks) drawLabel(ctx, p, p.login === me);
// Při startu a po návratu do hry ukazuje na vlastní tank šipka s pulzujícím kruhem
if (self?.alive && (state.phase === 'countdown' || self.protected)) drawSelfMarker(ctx, self, now);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
function drawSelfMarker(ctx: CanvasRenderingContext2D, p: TankPlayer, now: number) {
const pulse = (now % 900) / 900;
ctx.strokeStyle = `rgba(250, 204, 21, ${0.9 * (1 - pulse)})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(p.x, p.y, TANK_RADIUS + 10 + pulse * 40, 0, Math.PI * 2);
ctx.stroke();
// Šipka nad jmenovkou; u horního okraje hřiště pod tankem
const below = p.y < 70;
const dir = below ? -1 : 1;
const bob = Math.sin(now / 150) * 3;
const tipY = below ? p.y + TANK_RADIUS + 10 + bob : p.y - TANK_RADIUS - 20 + bob;
ctx.beginPath();
ctx.moveTo(p.x, tipY);
ctx.lineTo(p.x - 9, tipY - dir * 14);
ctx.lineTo(p.x - 4, tipY - dir * 14);
ctx.lineTo(p.x - 4, tipY - dir * 24);
ctx.lineTo(p.x + 4, tipY - dir * 24);
ctx.lineTo(p.x + 4, tipY - dir * 14);
ctx.lineTo(p.x + 9, tipY - dir * 14);
ctx.closePath();
ctx.lineWidth = 3;
ctx.strokeStyle = '#0b0f19';
ctx.stroke();
ctx.fillStyle = '#facc15';
ctx.fill();
ctx.font = 'bold 13px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textY = tipY - dir * 33;
ctx.lineWidth = 3;
ctx.strokeText('VY', p.x, textY);
ctx.fillText('VY', p.x, textY);
}
// --- Hardcore: světlo a tma ---
/** Paprsek světla ze středu tanku do každého směru; délka končí na zdi či bedně. */
@@ -572,17 +611,22 @@ function drawDarkness(ctx: CanvasRenderingContext2D, state: TankState, lights: L
function drawTank(ctx: CanvasRenderingContext2D, p: TankPlayer, mine: boolean, now: number) {
const color = tankColor(p);
const base = ctx.globalAlpha;
// Vlastní tank má pod sebou světlý kruh, ať se v hemžení najde
// Vlastní tank má pod sebou výrazný otáčející se kruh, ať se v hemžení najde (tmavý lem kvůli světlým mapám)
if (mine) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.12)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.55)';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 4]);
ctx.fillStyle = 'rgba(250, 204, 21, 0.18)';
ctx.beginPath();
ctx.arc(p.x, p.y, TANK_RADIUS + 5, 0, Math.PI * 2);
ctx.arc(p.x, p.y, TANK_RADIUS + 7, 0, Math.PI * 2);
ctx.fill();
ctx.setLineDash([7, 5]);
ctx.lineDashOffset = -now / 40;
ctx.strokeStyle = 'rgba(11, 15, 25, 0.7)';
ctx.lineWidth = 4.5;
ctx.stroke();
ctx.strokeStyle = '#facc15';
ctx.lineWidth = 2.5;
ctx.stroke();
ctx.setLineDash([]);
ctx.lineDashOffset = 0;
}
// Ochrana po zrození — tank bliká
if (p.protected && Math.floor(now / 120) % 2 === 0) ctx.globalAlpha = base * 0.35;
+3 -1
View File
@@ -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ů"
]
+144 -114
View File
@@ -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) {
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 (!p.input.drive) return;
let speed = p.input.drive > 0 ? TANK_SPEED : TANK_REVERSE_SPEED;
}
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;
+28 -2
View File
@@ -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);