added transformation item
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="64" height="64" shape-rendering="crispEdges"><rect width="8" height="8" fill="#000000"/><rect x="3" y="1" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="1" width="1" height="1" fill="#ff6fd8"/><rect x="3" y="2" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="2" width="1" height="1" fill="#ff6fd8"/><rect x="1" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="2" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="3" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="5" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="6" y="3" width="1" height="1" fill="#ff6fd8"/><rect x="1" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="2" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="3" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="5" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="6" y="4" width="1" height="1" fill="#ff6fd8"/><rect x="3" y="5" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="5" width="1" height="1" fill="#ff6fd8"/><rect x="3" y="6" width="1" height="1" fill="#ff6fd8"/><rect x="4" y="6" width="1" height="1" fill="#ff6fd8"/><rect x="0" y="0" width="1" height="1" fill="#ffffff"/><rect x="7" y="0" width="1" height="1" fill="#ffffff"/><rect x="0" y="7" width="1" height="1" fill="#ffffff"/><rect x="7" y="7" width="1" height="1" fill="#ffffff"/><rect x="1" y="1" width="1" height="1" fill="#ffffff"/><rect x="6" y="6" width="1" height="1" fill="#ffffff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,18 +0,0 @@
|
||||
import { itemVisual } from '../theme/assets';
|
||||
import styles from './BlackoutTimer.module.css';
|
||||
|
||||
/** Seconds left on a player's blackout, in the corner of their board. */
|
||||
export function BlackoutTimer({ remaining, small = false }: { remaining: number; small?: boolean }) {
|
||||
if (remaining <= 0) return null;
|
||||
const visual = itemVisual('blind');
|
||||
return (
|
||||
<div
|
||||
className={[styles.timer, small ? styles.small : '', remaining < 1 ? styles.ending : ''].join(' ')}
|
||||
style={{ color: visual.color, borderColor: visual.color }}
|
||||
title={visual.name}
|
||||
>
|
||||
<img className={styles.icon} src={visual.icon} alt="" />
|
||||
{remaining.toFixed(1)}s
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+15
-4
@@ -1,7 +1,16 @@
|
||||
.timer {
|
||||
.timers {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -10,8 +19,6 @@
|
||||
background: #000;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -23,6 +30,10 @@
|
||||
.small {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.small .timer {
|
||||
gap: 3px;
|
||||
padding: 2px 3px;
|
||||
border-width: 2px;
|
||||
@@ -34,7 +45,7 @@
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Blink during the last second so the return of the board isn't a surprise. */
|
||||
/* Blink during the last second so the end of the effect isn't a surprise. */
|
||||
.ending {
|
||||
animation: blink 0.25s steps(2) infinite;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { itemVisual } from '../theme/assets';
|
||||
import styles from './EffectTimers.module.css';
|
||||
|
||||
/** A timed item effect on a player: the item id and the seconds it has left. */
|
||||
export type Effect = [itemId: string, remaining: number];
|
||||
|
||||
/** Seconds left on a player's timed item effects (Blackout, Transformation), in the corner of their board. */
|
||||
export function EffectTimers({ effects, small = false }: { effects: Effect[]; small?: boolean }) {
|
||||
const active = effects.filter(([, remaining]) => remaining > 0);
|
||||
if (active.length === 0) return null;
|
||||
return (
|
||||
<div className={[styles.timers, small ? styles.small : ''].join(' ')}>
|
||||
{active.map(([itemId, remaining]) => {
|
||||
const visual = itemVisual(itemId);
|
||||
return (
|
||||
<div
|
||||
key={itemId}
|
||||
className={[styles.timer, remaining < 1 ? styles.ending : ''].join(' ')}
|
||||
style={{ color: visual.color, borderColor: visual.color }}
|
||||
title={visual.name}
|
||||
>
|
||||
<img className={styles.icon} src={visual.icon} alt="" />
|
||||
{remaining.toFixed(1)}s
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import type { PlayerState } from '../net/types';
|
||||
import type { ItemFlash } from '../net/useGameConnection';
|
||||
import { BlackoutTimer } from './BlackoutTimer';
|
||||
import { EffectTimers } from './EffectTimers';
|
||||
import { BoardCanvas } from './BoardCanvas';
|
||||
import { ItemBar } from './ItemBar';
|
||||
import { ItemFlashOverlay } from './ItemFlashOverlay';
|
||||
@@ -30,7 +30,15 @@ export const MiniBoard = memo(function MiniBoard({ state, width, height, slot, s
|
||||
</div>
|
||||
<BoardCanvas state={state} width={width} height={height} cell={cell} highlighted={selected}>
|
||||
<ItemFlashOverlay flashes={flashes} small />
|
||||
<BlackoutTimer remaining={state.alive ? state.invisibleRemaining : 0} small />
|
||||
{state.alive && (
|
||||
<EffectTimers
|
||||
effects={[
|
||||
['blind', state.invisibleRemaining],
|
||||
['transform', state.transformedRemaining],
|
||||
]}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</BoardCanvas>
|
||||
<div className={styles.items} style={{ width: width * cell + 6 }}>
|
||||
<ItemBar items={state.inventory} size={Math.max(10, cell)} />
|
||||
|
||||
@@ -52,7 +52,8 @@ export interface PlayerState {
|
||||
name: string;
|
||||
alive: boolean;
|
||||
place: number | null;
|
||||
/** One character per visible cell, row by row: '0' empty, '1'-'7' piece colors, '8' garbage. */
|
||||
/** One character per visible cell, row by row: '0' empty, '1'-'7' piece colors, '8' garbage,
|
||||
* '9'-'=' (values 9-13) odd shapes from Transformation. Decode with charCodeAt - 48. */
|
||||
board: string;
|
||||
items: ItemCell[];
|
||||
active: ActivePieceState | null;
|
||||
@@ -66,6 +67,7 @@ export interface PlayerState {
|
||||
level: number;
|
||||
pendingGarbage: number;
|
||||
invisibleRemaining: number;
|
||||
transformedRemaining: number;
|
||||
}
|
||||
|
||||
export interface MatchEvent {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCurrentTrack, useSound } from '../audio/SoundContext';
|
||||
import { useGameSounds } from '../audio/useGameSounds';
|
||||
import { BlackoutTimer } from '../components/BlackoutTimer';
|
||||
import { EffectTimers } from '../components/EffectTimers';
|
||||
import { BoardCanvas } from '../components/BoardCanvas';
|
||||
import { ItemBar } from '../components/ItemBar';
|
||||
import { ItemFlashOverlay } from '../components/ItemFlashOverlay';
|
||||
@@ -130,7 +130,12 @@ export function GamePage({ match, lobby, playerId, flashes, actions }: Props) {
|
||||
highlighted={targetId === playerId}
|
||||
>
|
||||
<ItemFlashOverlay flashes={flashesFor(me.id)} />
|
||||
<BlackoutTimer remaining={me.invisibleRemaining} />
|
||||
<EffectTimers
|
||||
effects={[
|
||||
['blind', me.invisibleRemaining],
|
||||
['transform', me.transformedRemaining],
|
||||
]}
|
||||
/>
|
||||
{match.phase === 'countdown' && (
|
||||
<div className={styles.overlay}>
|
||||
<span className={styles.countdown}>{Math.ceil(match.countdown)}</span>
|
||||
|
||||
@@ -135,6 +135,8 @@ export function drawPreviewPiece(
|
||||
if (!shape) return;
|
||||
const w = Math.max(...shape.map(([x]) => x)) + 1;
|
||||
const h = Math.max(...shape.map(([, y]) => y)) + 1;
|
||||
// Three-row shapes (Transformation) are shrunk to fit the preview box.
|
||||
cell = Math.min(cell, Math.floor(boxHeight / (h + 0.5)));
|
||||
const ox = offsetX + (boxWidth - w * cell) / 2;
|
||||
const oy = offsetY + (boxHeight - h * cell) / 2;
|
||||
const color = dimmed ? '#555555' : (blockColors[piece.type] ?? '#fff');
|
||||
|
||||
@@ -7,4 +7,10 @@ export const pieceShapes: Record<number, [number, number][]> = {
|
||||
5: [[1, 0], [2, 0], [0, 1], [1, 1]],
|
||||
6: [[0, 0], [1, 0], [1, 1], [2, 1]],
|
||||
7: [[1, 0], [0, 1], [1, 1], [2, 1]],
|
||||
// Odd shapes, only dealt during a Transformation.
|
||||
9: [[0, 0]],
|
||||
10: [[0, 0], [0, 1], [1, 1]],
|
||||
11: [[0, 0], [2, 0], [0, 1], [1, 1], [2, 1]],
|
||||
12: [[1, 0], [0, 1], [1, 1], [2, 1], [1, 2]],
|
||||
13: [[0, 0], [0, 1], [1, 1], [1, 2], [2, 2]],
|
||||
};
|
||||
|
||||
@@ -52,6 +52,12 @@ export const itemVisuals: Record<string, ItemVisual> = {
|
||||
color: '#c452ff',
|
||||
description: 'Hides placed blocks and the drop shadow for 4 seconds. Stacks.',
|
||||
},
|
||||
transform: {
|
||||
icon: asset('assets/items/transform.svg'),
|
||||
name: 'Transformation',
|
||||
color: '#ff6fd8',
|
||||
description: "Turns the target's next pieces into odd shapes for 8 seconds. Stacks.",
|
||||
},
|
||||
};
|
||||
|
||||
export const unknownItem: ItemVisual = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Block colors by cell value (see PlayerState.board). Swap these to restyle the blocks.
|
||||
* Index 0 is the empty cell, 8 is garbage.
|
||||
* Index 0 is the empty cell, 1-7 the standard pieces, 8 garbage, 9-13 the Transformation shapes.
|
||||
*/
|
||||
export const blockColors: Record<number, string> = {
|
||||
1: '#3fe3f2',
|
||||
@@ -11,6 +11,11 @@ export const blockColors: Record<number, string> = {
|
||||
6: '#ff4256',
|
||||
7: '#c452ff',
|
||||
8: '#8c8c8c',
|
||||
9: '#ff6fd8',
|
||||
10: '#2fd6a3',
|
||||
11: '#b8ff3f',
|
||||
12: '#f2f2f2',
|
||||
13: '#ff7a3a',
|
||||
};
|
||||
|
||||
export const boardColors = {
|
||||
|
||||
Reference in New Issue
Block a user