forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
cc3b2fa4d5
commit
daa417ec17
46 files changed
+1334
-172
No files matched your search
@@ -0,0 +1,224 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export type LoginField = "username" | "password";
|
||||
export type LoginKeyboardMode = "lower" | "upper" | "symbols";
|
||||
export type LoginRequestAction = "login" | "create" | "offline";
|
||||
export type LoginKeyboardDirection = "up" | "down" | "left" | "right";
|
||||
|
||||
export interface LoginKeyDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
ariaLabel: string;
|
||||
kind: "character" | "mode" | "field" | "space" | "backspace" | "clear" | "done" | LoginRequestAction;
|
||||
value?: string;
|
||||
span?: number;
|
||||
}
|
||||
|
||||
export interface LoginInputSnapshot {
|
||||
username: string;
|
||||
passwordLength: number;
|
||||
activeField: LoginField;
|
||||
selectedKeyId: string;
|
||||
keyboardMode: LoginKeyboardMode;
|
||||
busy: LoginRequestAction | null;
|
||||
}
|
||||
|
||||
interface LoginRequest {
|
||||
id: number;
|
||||
action: LoginRequestAction;
|
||||
}
|
||||
|
||||
interface LoginInputState {
|
||||
username: string;
|
||||
password: string;
|
||||
activeField: LoginField;
|
||||
selectedKeyId: string;
|
||||
keyboardMode: LoginKeyboardMode;
|
||||
busy: LoginRequestAction | null;
|
||||
request: LoginRequest | null;
|
||||
requestId: number;
|
||||
setUsername: (value: string) => void;
|
||||
setPassword: (value: string) => void;
|
||||
setActiveField: (field: LoginField) => void;
|
||||
selectKey: (keyId: string) => void;
|
||||
moveSelection: (direction: LoginKeyboardDirection) => void;
|
||||
pressKey: (keyId: string) => void;
|
||||
pressSelected: () => void;
|
||||
consumeRequest: (id: number) => void;
|
||||
setBusy: (busy: LoginRequestAction | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const DEFAULT_LOGIN_KEY_ID = "char-1-0";
|
||||
|
||||
const characterLayouts: Record<LoginKeyboardMode, readonly (readonly string[])[]> = {
|
||||
lower: [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
||||
["a", "s", "d", "f", "g", "h", "j", "k", "l", "_"],
|
||||
["z", "x", "c", "v", "b", "n", "m", ".", "-"],
|
||||
],
|
||||
upper: [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
|
||||
["A", "S", "D", "F", "G", "H", "J", "K", "L", "_"],
|
||||
["Z", "X", "C", "V", "B", "N", "M", ".", "-"],
|
||||
],
|
||||
symbols: [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"],
|
||||
["{", "}", "[", "]", "<", ">", "=", "+", "/", "\\"],
|
||||
["~", "`", ";", ":", "'", "\"", ",", ".", "?"],
|
||||
],
|
||||
};
|
||||
|
||||
function characterKey(value: string, row: number, column: number): LoginKeyDefinition {
|
||||
return {
|
||||
id: `char-${row}-${column}`,
|
||||
label: value,
|
||||
ariaLabel: value === " " ? "Space" : value,
|
||||
kind: "character",
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function loginKeyboardRows(mode: LoginKeyboardMode): readonly (readonly LoginKeyDefinition[])[] {
|
||||
const rows = characterLayouts[mode];
|
||||
return [
|
||||
rows[0].map((value, column) => characterKey(value, 0, column)),
|
||||
rows[1].map((value, column) => characterKey(value, 1, column)),
|
||||
rows[2].map((value, column) => characterKey(value, 2, column)),
|
||||
[
|
||||
{ id: "control-mode", label: mode === "lower" ? "⇧" : mode === "upper" ? "#+=" : "abc", ariaLabel: "Change keyboard mode", kind: "mode" },
|
||||
...rows[3].map((value, column) => characterKey(value, 3, column + 1)),
|
||||
],
|
||||
[
|
||||
{ id: "control-field", label: "Field", ariaLabel: "Switch login field", kind: "field", span: 2 },
|
||||
{ id: "control-space", label: "Space", ariaLabel: "Space", kind: "space", span: 3 },
|
||||
{ id: "control-backspace", label: "⌫", ariaLabel: "Backspace", kind: "backspace", span: 2 },
|
||||
{ id: "control-clear", label: "Clear", ariaLabel: "Clear active field", kind: "clear", span: 2 },
|
||||
{ id: "control-done", label: "Done", ariaLabel: "Finish active field", kind: "done" },
|
||||
],
|
||||
[
|
||||
{ id: "action-login", label: "Enter world", ariaLabel: "Log in and enter world", kind: "login", span: 4 },
|
||||
{ id: "action-create", label: "Create", ariaLabel: "Create account", kind: "create", span: 3 },
|
||||
{ id: "action-offline", label: "Offline", ariaLabel: "Play offline", kind: "offline", span: 3 },
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function keyPosition(rows: readonly (readonly LoginKeyDefinition[])[], keyId: string) {
|
||||
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
||||
let column = 0;
|
||||
for (let keyIndex = 0; keyIndex < rows[rowIndex].length; keyIndex += 1) {
|
||||
const key = rows[rowIndex][keyIndex];
|
||||
const span = key.span ?? 1;
|
||||
if (key.id === keyId) return { rowIndex, keyIndex, center: column + span / 2 };
|
||||
column += span;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function nextLoginKeyId(
|
||||
mode: LoginKeyboardMode,
|
||||
selectedKeyId: string,
|
||||
direction: LoginKeyboardDirection,
|
||||
): string {
|
||||
const rows = loginKeyboardRows(mode);
|
||||
const position = keyPosition(rows, selectedKeyId) ?? keyPosition(rows, DEFAULT_LOGIN_KEY_ID)!;
|
||||
const row = rows[position.rowIndex];
|
||||
if (direction === "left" || direction === "right") {
|
||||
const delta = direction === "left" ? -1 : 1;
|
||||
return row[(position.keyIndex + delta + row.length) % row.length].id;
|
||||
}
|
||||
const rowDelta = direction === "up" ? -1 : 1;
|
||||
const targetRowIndex = (position.rowIndex + rowDelta + rows.length) % rows.length;
|
||||
const targetRow = rows[targetRowIndex];
|
||||
let column = 0;
|
||||
return targetRow.reduce((closest, key) => {
|
||||
const center = column + (key.span ?? 1) / 2;
|
||||
column += key.span ?? 1;
|
||||
return Math.abs(center - position.center) < Math.abs(closest.center - position.center)
|
||||
? { id: key.id, center }
|
||||
: closest;
|
||||
}, { id: targetRow[0].id, center: (targetRow[0].span ?? 1) / 2 }).id;
|
||||
}
|
||||
|
||||
function findLoginKey(mode: LoginKeyboardMode, keyId: string): LoginKeyDefinition | null {
|
||||
return loginKeyboardRows(mode).flat().find((key) => key.id === keyId) ?? null;
|
||||
}
|
||||
|
||||
function nextMode(mode: LoginKeyboardMode): LoginKeyboardMode {
|
||||
if (mode === "lower") return "upper";
|
||||
if (mode === "upper") return "symbols";
|
||||
return "lower";
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
username: "",
|
||||
password: "",
|
||||
activeField: "username" as LoginField,
|
||||
selectedKeyId: DEFAULT_LOGIN_KEY_ID,
|
||||
keyboardMode: "lower" as LoginKeyboardMode,
|
||||
busy: null,
|
||||
request: null,
|
||||
requestId: 0,
|
||||
};
|
||||
|
||||
export const useLoginInputStore = create<LoginInputState>((set, get) => ({
|
||||
...initialState,
|
||||
setUsername: (username) => set({ username: username.slice(0, 20), activeField: "username" }),
|
||||
setPassword: (password) => set({ password: password.slice(0, 128), activeField: "password" }),
|
||||
setActiveField: (activeField) => set({ activeField }),
|
||||
selectKey: (selectedKeyId) => {
|
||||
if (findLoginKey(get().keyboardMode, selectedKeyId)) set({ selectedKeyId });
|
||||
},
|
||||
moveSelection: (direction) => set((state) => ({
|
||||
selectedKeyId: nextLoginKeyId(state.keyboardMode, state.selectedKeyId, direction),
|
||||
})),
|
||||
pressKey: (keyId) => {
|
||||
const state = get();
|
||||
const key = findLoginKey(state.keyboardMode, keyId);
|
||||
if (!key) return;
|
||||
set({ selectedKeyId: key.id });
|
||||
if (key.kind === "character" && key.value) {
|
||||
if (state.activeField === "username") {
|
||||
if (/^[A-Za-z0-9_]$/.test(key.value) && state.username.length < 20) set({ username: state.username + key.value });
|
||||
} else if (state.password.length < 128) set({ password: state.password + key.value });
|
||||
return;
|
||||
}
|
||||
if (key.kind === "mode") set({ keyboardMode: nextMode(state.keyboardMode) });
|
||||
else if (key.kind === "field") set({ activeField: state.activeField === "username" ? "password" : "username" });
|
||||
else if (key.kind === "space" && state.activeField === "password" && state.password.length < 128) set({ password: `${state.password} ` });
|
||||
else if (key.kind === "backspace") {
|
||||
if (state.activeField === "username") set({ username: state.username.slice(0, -1) });
|
||||
else set({ password: state.password.slice(0, -1) });
|
||||
} else if (key.kind === "clear") {
|
||||
if (state.activeField === "username") set({ username: "" });
|
||||
else set({ password: "" });
|
||||
} else if (key.kind === "done") {
|
||||
if (state.activeField === "username") set({ activeField: "password" });
|
||||
else set({ selectedKeyId: "action-login" });
|
||||
} else if (["login", "create", "offline"].includes(key.kind) && !state.busy) {
|
||||
const requestId = state.requestId + 1;
|
||||
set({ requestId, request: { id: requestId, action: key.kind as LoginRequestAction } });
|
||||
}
|
||||
},
|
||||
pressSelected: () => get().pressKey(get().selectedKeyId),
|
||||
consumeRequest: (id) => set((state) => state.request?.id === id ? { request: null } : {}),
|
||||
setBusy: (busy) => set({ busy }),
|
||||
reset: () => set(initialState),
|
||||
}));
|
||||
|
||||
export function currentLoginInputSnapshot(): LoginInputSnapshot {
|
||||
const state = useLoginInputStore.getState();
|
||||
return {
|
||||
username: state.username,
|
||||
passwordLength: state.password.length,
|
||||
activeField: state.activeField,
|
||||
selectedKeyId: state.selectedKeyId,
|
||||
keyboardMode: state.keyboardMode,
|
||||
busy: state.busy,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user