This commit is contained in:
Robin Appelman 2024-11-22 22:17:21 +01:00
commit b08267a08c
10 changed files with 318 additions and 304 deletions

View file

@ -1,26 +1,29 @@
export interface AnalyseMenuProps {
sessionName: string;
onShare: Function;
canShare: boolean;
isShared: boolean;
sessionName: string;
onShare: Function;
canShare: boolean;
isShared: boolean;
}
export function AnalyseMenu({sessionName, onShare, canShare, isShared}:AnalyseMenuProps) {
export function AnalyseMenu(props: AnalyseMenuProps) {
const loc = () => window.location.toString().replace(/\#.*/, '') + '#' + props.sessionName;
const shareText = () => (props.isShared) ?
<input class="share-text" value={loc()} readOnly={true}
title="Use this link to join the current session"
style={{width: `${(loc().length * 8)}px`}}
onFocus={(event) => {
(event.target as HTMLInputElement).select()
}}/> : '';
const loc = window.location.toString().replace(/\#.+/, '') + '#' + sessionName;
const shareText = (isShared) ?
<input class="share-text" value={loc} readOnly={true}
title="Use this link to join the current session"
style={{width: `${(loc.length*8)}px`}}
onFocus={(event)=>{(event.target as HTMLInputElement).select()}}/> : '';
const shareButton = () => (props.canShare) ? <div class="analyse-menu">
<button class="share-session" title="Start a shared session"
onClick={() => {
props.onShare()
}}/>
{shareText}
</div> : '';
const shareButton = (canShare) ? <div class="analyse-menu">
<button class="share-session" title="Start a shared session"
onClick={()=>{onShare()}}/>
{shareText}
</div>: '';
return (<div>
{shareButton}
</div>)
return (<div>
{shareButton}
</div>)
}

View file

@ -12,178 +12,182 @@ import {Session, StateUpdate} from "./Session";
import {DemoHead} from "../../header";
export interface AnalyseProps {
header: DemoHead;
isStored: boolean;
parser: AsyncParser;
header: DemoHead;
isStored: boolean;
parser: AsyncParser;
}
export const Analyser = (props: AnalyseProps) => {
const parser = props.parser;
const intervalPerTick = props.header.duration / props.header.ticks;
const parser = props.parser;
const intervalPerTick = props.header.duration / props.header.ticks;
const [tick, setTick] = createSignal<number>(0);
const [scale, setScale] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>("");
const [tick, setTick] = createSignal<number>(0);
const [scale, setScale] = createSignal<number>(1);
const [playing, setPlaying] = createSignal<boolean>(false);
const [sessionName, setSessionName] = createSignal<string>("");
let lastFrameTime = 0;
let playStartTick = 0;
let playStartTime = 0;
let lastFrameTime = 0;
let playStartTick = 0;
let playStartTime = 0;
const onUpdate = (update: StateUpdate) => {
if (update["tick"]) {
setTick(update["tick"]);
}
if (update["playing"]) {
setPlaying(update["playing"]);
}
}
const onUpdate = (update: StateUpdate) => {
if (update["tick"]) {
setTick(update["tick"]);
}
if (update.hasOwnProperty("playing")) {
if (update["playing"]) {
play();
} else {
pause();
}
}
}
let session: Session | null = null;
if (props.isStored && window.location.hash) {
const parsed = parseInt(window.location.hash.substr(1), 10);
if (('#' + parsed) === window.location.hash) {
setTick(Math.floor(parsed));
} else {
const name = window.location.hash.substring(1);
Session.join(name, onUpdate);
setSessionName(name);
}
}
let session: Session | null = null;
if (props.isStored && window.location.hash) {
const parsed = parseInt(window.location.hash.substr(1), 10);
if (('#' + parsed) === window.location.hash) {
setTick(Math.floor(parsed));
} else {
const name = window.location.hash.substring(1);
session = Session.join(name, onUpdate);
setSessionName(name);
}
}
const map = parser.demo.header.map;
const backgroundBoundaries = getMapBoundaries(map);
if (!backgroundBoundaries) {
throw new Error(`Map not supported "${map}".`);
}
const worldSize = {
width: backgroundBoundaries.boundary_max.x - backgroundBoundaries.boundary_min.x,
height: backgroundBoundaries.boundary_max.y - backgroundBoundaries.boundary_min.y,
};
const map = parser.demo.header.map;
const backgroundBoundaries = getMapBoundaries(map);
if (!backgroundBoundaries) {
throw new Error(`Map not supported "${map}".`);
}
const worldSize = {
width: backgroundBoundaries.boundary_max.x - backgroundBoundaries.boundary_min.x,
height: backgroundBoundaries.boundary_max.y - backgroundBoundaries.boundary_min.y,
};
const setTickNow = (tick) => {
lastFrameTime = 0;
playStartTick = tick;
playStartTime = window.performance.now();
setTick(tick);
setHash(tick);
if (session) {
session.update({tick});
}
}
const setTickNow = (tick) => {
lastFrameTime = 0;
playStartTick = tick;
playStartTime = window.performance.now();
setTick(tick);
setHash(tick);
if (session) {
session.update({tick});
}
}
const pause = () => {
setPlaying(false);
lastFrameTime = 0;
if (session) {
session.update({playing: false});
}
}
const pause = () => {
setPlaying(false);
lastFrameTime = 0;
if (session) {
session.update({playing: false});
}
}
const play = () => {
playStartTick = tick();
playStartTime = window.performance.now();
setPlaying(true);
requestAnimationFrame(animFrame);
if (session) {
session.update({playing: false});
}
}
const play = () => {
playStartTick = tick();
playStartTime = window.performance.now();
setPlaying(true);
requestAnimationFrame(animFrame);
if (session) {
session.update({playing: true});
}
}
const togglePlay = () => {
if (playing()) {
pause();
} else {
play();
}
}
const togglePlay = () => {
if (playing()) {
pause();
} else {
play();
}
}
const syncPlayTick = debounce(500, () => {
if (session) {
session.update({
playing: playing(),
tick: tick(),
});
}
});
const syncPlayTick = throttle(2500, () => {
if (session) {
session.update({
playing: playing(),
tick: tick(),
});
}
});
const setHash = debounce(250, (tick) => {
if (!session && props.isStored) {
history.replaceState('', '', '#' + tick);
}
});
const setHash = debounce(250, (tick) => {
if (!session && props.isStored) {
history.replaceState('', '', '#' + tick);
}
});
const animFrame = (timestamp:number) => {
const timePassed = (timestamp - playStartTime) / 1000;
const targetTick = playStartTick + (Math.round(timePassed / intervalPerTick));
lastFrameTime = timestamp;
if (targetTick >= (parser.demo.tick - 1)) {
pause();
}
setHash(targetTick);
setTick(targetTick);
syncPlayTick();
const animFrame = (timestamp: number) => {
const timePassed = (timestamp - playStartTime) / 1000;
const targetTick = playStartTick + (Math.round(timePassed / intervalPerTick));
lastFrameTime = timestamp;
if (targetTick >= (parser.demo.tick - 1)) {
pause();
}
setHash(targetTick);
setTick(targetTick);
syncPlayTick();
if (playing()) {
requestAnimationFrame(animFrame);
}
}
if (playing()) {
requestAnimationFrame(animFrame);
}
}
const players = () => parser.getPlayersAtTick(tick());
const buildings = () => parser.getBuildingsAtTick(tick());
const kills = parser.getKills();
const playButtonText = () => (playing()) ? '⏸' : '▶️';
const disabled = session && !session.isOwner();
const isShared = () => sessionName() !== '';
const players = () => parser.getPlayersAtTick(tick());
const buildings = () => parser.getBuildingsAtTick(tick());
const kills = parser.getKills();
const playButtonText = () => (playing()) ? '⏸' : '▶️';
const disabled = session && !session.isOwner();
const isShared = () => sessionName() !== '';
console.log(intervalPerTick);
console.log(intervalPerTick);
const timeTitle = () => `${tickToTime(tick(), intervalPerTick)} (tick ${tick()})`;
const timeTitle = () => `${tickToTime(tick(), intervalPerTick)} (tick ${tick()})`;
return (
<div>
<div class="map-holder">
<MapContainer contentSize={worldSize}
onScale={setScale}>
<MapRender size={worldSize}
players={players()}
buildings={buildings()}
header={props.header}
world={backgroundBoundaries}
scale={scale()}/>
</MapContainer>
<AnalyseMenu sessionName={sessionName()}
onShare={() => {
session = Session.create({
tick: tick(),
playing: playing()
});
setSessionName(session.sessionName);
}}
canShare={props.isStored && !disabled}
isShared={isShared()}
/>
<SpecHUD parser={parser} tick={tick()}
players={players()} kills={kills}/>
</div>
<div class="time-control"
title={timeTitle()}>
<input class="play-pause-button" type="button"
value={playButtonText()}
disabled={disabled}
onClick={togglePlay}
/>
<Timeline parser={parser} tick={tick()}
onSetTick={throttle(50, (tick) => {
setTickNow(tick);
})}
disabled={disabled}/>
</div>
</div>
);
return (
<div>
<div class="map-holder">
<MapContainer contentSize={worldSize}
onScale={setScale}>
<MapRender size={worldSize}
players={players()}
buildings={buildings()}
header={props.header}
world={backgroundBoundaries}
scale={scale()}/>
</MapContainer>
<AnalyseMenu sessionName={sessionName()}
onShare={() => {
session = Session.create({
tick: tick(),
playing: playing()
});
setSessionName(session.sessionName);
}}
canShare={props.isStored && !disabled}
isShared={isShared()}
/>
<SpecHUD parser={parser} tick={tick()}
players={players()} kills={kills}/>
</div>
<div class="time-control"
title={timeTitle()}>
<input class="play-pause-button" type="button"
value={playButtonText()}
disabled={disabled}
onClick={togglePlay}
/>
<Timeline parser={parser} tick={tick()}
onSetTick={throttle(50, (tick) => {
setTickNow(tick);
})}
disabled={disabled}/>
</div>
</div>
);
}
function tickToTime(tick: number, intervalPerTick: number): string {
let seconds = Math.floor(tick * intervalPerTick);
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
let seconds = Math.floor(tick * intervalPerTick);
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
}

View file

@ -14,6 +14,7 @@ export class Session {
this.sessionName = name;
this.initialState = initialState;
this.onState = onState;
this.open();
}
public static create(state: PlaybackState): Session {
@ -98,7 +99,7 @@ export class Session {
tick: update["tick"]
}));
}
if (update["playing"]) {
if (update.hasOwnProperty("playing")) {
this.socket.send(JSON.stringify({
type: 'play',
session: this.sessionName,