Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "https://localhost:5173",
setupNodeEvents(on, config) {
setupNodeEvents(on, _config) {
// implement node event listeners here

// Log messages in command line output using Node.js runner
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Cypress.Commands.add("shouldNotBeActionable", { prevSubject: "element" }, (subje

// Adapted from https://github.com/cypress-io/cypress/issues/877#issuecomment-490504922
Cypress.Commands.add("shouldBeInViewport", { prevSubject: true }, (subject) => {
// @ts-ignore TODO: Fix cy.state type error
// @ts-expect-error TODO: Fix cy.state type error
const window = Cypress.$(cy.state("window"));
const bottom = window.height();
const right = window.width();
Expand Down
23 changes: 23 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,27 @@ export default defineConfig(
],
},
},
// CommonJS Jest mocks (e.g. __mocks__/fs.js) use require/module.exports
{
files: ["__mocks__/**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
require: "readonly",
module: "writable",
},
},
rules: {
"@typescript-eslint/no-require-imports": "off",
},
},
// Cypress specs rely on Chai assertions (e.g. `expect(x).to.be.true`) and
// `any`-typed custom command declarations
{
files: ["cypress/**/*.ts"],
rules: {
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
);
2 changes: 1 addition & 1 deletion plugins/app-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function appIconLabel(options: PluginOptions) {
const buildIcons = (env: string) => {
try {
childProcess.execSync(`${MAGICK_COMMAND} -version`, { stdio: "ignore" });
} catch (e) {
} catch (_e) {
console.warn("ImageMagick is not installed. Skipping icon labeling.");
return;
}
Expand Down
4 changes: 3 additions & 1 deletion plugins/file-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default function fileTransformerPlugin({ input, transformer, output }) {
try {
fileContent = fs.readFileSync(filePath, "utf-8");
} catch (err) {
throw new Error(`Failed to read file: ${input} - ${err.message}`);
throw new Error(`Failed to read file: ${input} - ${(err as Error).message}`, {
cause: err,
});
}

// Transform the content using the provided transformer function
Expand Down
3 changes: 0 additions & 3 deletions src/component/how-to-play/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export class HowToPlay {
];
const step1Animation = new AnimationManager();
step1Animation.isAnimationEnabled = true;
// @ts-ignore
renderBoard(stepSections[0], this.stepBoards[0], step1Animation, {
blockStyle: "compact",
});
Expand Down Expand Up @@ -159,7 +158,6 @@ export class HowToPlay {
];
const step2Animation = new AnimationManager();
step2Animation.isAnimationEnabled = true;
// @ts-ignore
renderBoard(stepSections[1], this.stepBoards[1], step2Animation, {
blockStyle: "compact",
});
Expand Down Expand Up @@ -192,7 +190,6 @@ export class HowToPlay {
];
const step3Animation = new AnimationManager();
step3Animation.isAnimationEnabled = true;
// @ts-ignore
renderBoard(stepSections[2], this.stepBoards[2], step3Animation, {
blockStyle: "compact",
});
Expand Down
16 changes: 11 additions & 5 deletions src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,13 @@ export type Position = {
y: number;
};

let debugEnabled = false;
// @ts-ignore TODO: Resolve this type issue "Property 'env' does not exist on type 'ImportMeta'."
const debugEnabled = false;
// TODO: Resolve this type issue "Property 'env' does not exist on type 'ImportMeta'."
// TODO: Fix "SyntaxError: Cannot use 'import.meta' outside a module" when trying to run in Jest
// Either restrict the usage of import.meta to the browser code only, bring in debugEnabled from there into game.ts
// or, might have to bring in Babel.
// let debugEnabled = import.meta.env.DEV ?? false;

const GAME_IS_OVER_ERROR_ID = "GameIsOver";

export const DIRECTION_LEFT = 1;
export const DIRECTION_RIGHT = 2;
export const DIRECTION_UP = 3;
Expand Down Expand Up @@ -74,7 +72,15 @@ export const getErrorMessage = (errorID: string) => {
}
};

export type EventHandler = (eventID: string, data?: any) => void;
export type GameEventData = {
gameState?: GameState;
persistentState?: GamePersistentState;
undoInfo?: {
undoStack: readonly GameState[];
};
};

export type EventHandler = (eventID: string, data?: GameEventData) => void;

let gameState: GameState = {} as GameState;
let persistentState: GamePersistentState = {} as GamePersistentState;
Expand Down
53 changes: 26 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DIRECTION_UP,
GameState,
GamePersistentState,
GameEventData,
} from "./game";
import { getPreferenceValue, initPreferences, savePreferenceValue } from "./preferences";
import {
Expand Down Expand Up @@ -85,34 +86,34 @@ document.addEventListener("DOMContentLoaded", async () => {

let gameState: GameState;
let persistentState: GamePersistentState;
let spawnManager = new SpawnManager();
let animationManager = new AnimationManager();
let undoManager = new UndoManager();
let gameStorage = new BrowserGameStorage();
const spawnManager = new SpawnManager();
const animationManager = new AnimationManager();
const undoManager = new UndoManager();
const gameStorage = new BrowserGameStorage();
const migrated = migrateLocalStorage_v1_3_1();
let fullscreenManager = new FullscreenManager(gameStorage);
let assetManager = new AssetManager(document.querySelector(".loader-wrapper") as HTMLElement);
let actionIconManager = new ActionIconManager();
let appIconManager = new AppIconManager();
let themeManager = new ThemeManager(appIconManager);
const fullscreenManager = new FullscreenManager(gameStorage);
const assetManager = new AssetManager(document.querySelector(".loader-wrapper") as HTMLElement);
const actionIconManager = new ActionIconManager();
const appIconManager = new AppIconManager();
const themeManager = new ThemeManager(appIconManager);
setThemeManager(themeManager); // Set the global theme manager reference
// Store unlockable statuses so that their unlock messages don't display again if player achieved the same conditions again
let unlockedClassic = false;
let unlockedInitialCommit = false;

let tutorial: Tutorial = new Tutorial();
let howToPlay: HowToPlay = new HowToPlay();
const tutorial: Tutorial = new Tutorial();
const howToPlay: HowToPlay = new HowToPlay();

const swipeSensitivity = 50;

const md = new MobileDetect(window.navigator.userAgent);
const isMobile = md.mobile() !== null;

const eventHandler = (event: string, data: any) => {
const eventHandler = (event: string, data?: GameEventData) => {
switch (event) {
case "init":
gameState = data.gameState;
persistentState = data.persistentState;
gameState = data!.gameState!;
persistentState = data!.persistentState!;
animationManager.isAnimationEnabled = isAnimationEnabled;
unlockedClassic = persistentState.unlockables.classic;
unlockedInitialCommit = persistentState.unlockables.initialCommit;
Expand All @@ -134,7 +135,7 @@ document.addEventListener("DOMContentLoaded", async () => {
persistentState.highscore.toString();
(document.querySelector("#moveCount") as HTMLSpanElement).innerText =
gameState.moveCount.toString();
if (data.undoInfo) {
if (data?.undoInfo) {
if (data.undoInfo.undoStack.length > 0) {
undoButton.classList.remove("disabled");
} else {
Expand Down Expand Up @@ -200,15 +201,15 @@ document.addEventListener("DOMContentLoaded", async () => {
});
const dialog = document.querySelector(".dialog") as HTMLElement;
dialog.classList.add("win");
if (!unlockedClassic && data.persistentState.unlockables.classic) {
if (!unlockedClassic && data!.persistentState!.unlockables.classic) {
renderNotification("2048Clone theme unlocked", 2500);
unlockedClassic = true;
}
if (!unlockedInitialCommit && data.persistentState.unlockables.initialCommit) {
if (!unlockedInitialCommit && data!.persistentState!.unlockables.initialCommit) {
renderNotification("Initial Commit tileset unlocked", 2500);
unlockedInitialCommit = true;
}
persistentState = data.persistentState;
persistentState = data!.persistentState!;
const shareText = generateShareText(gameState);
shareButton.addEventListener("click", async (e) => {
e.preventDefault();
Expand Down Expand Up @@ -436,7 +437,7 @@ document.addEventListener("DOMContentLoaded", async () => {
closeDialog(dialog, overlayBackElem);
});

let snowEmbed = document.getElementById("embedim--snow");
const snowEmbed = document.getElementById("embedim--snow");
if (snowEmbed) snowEmbed.style.display = "none";

const selectableThemes = [STANDARD_THEME, LIGHT_THEME, DARK_THEME, SNOW_THEME, CLASSIC_THEME];
Expand Down Expand Up @@ -502,7 +503,6 @@ document.addEventListener("DOMContentLoaded", async () => {
setting.addEventListener("click", (e) => {
const elem = e.target as HTMLElement;
const toggle = setting.querySelector(".toggle") as HTMLElement;
let enabled = false;
if (elem.classList.contains(THEME_SETTING_NAME)) {
const themeIndex = selectableThemes.indexOf(themeManager.getCurrentTheme());
let nextTheme = selectableThemes[(themeIndex + 1) % selectableThemes.length];
Expand Down Expand Up @@ -561,21 +561,21 @@ document.addEventListener("DOMContentLoaded", async () => {
toggle.innerText = formatTilesetName(nextTileset);
} else if (elem.classList.contains(ANIMATIONS_SETTING_NAME)) {
const knob = setting.querySelector(".knob") as HTMLElement;
enabled = isAnimationEnabled = !isAnimationEnabled;
isAnimationEnabled = !isAnimationEnabled;
animationManager.isAnimationEnabled = isAnimationEnabled;
savePreferenceValue(
ANIMATIONS_PREFERENCE_NAME,
isAnimationEnabled ? SETTING_ENABLED : SETTING_DISABLED,
);
if (enabled) {
if (isAnimationEnabled) {
knob.classList.add("enabled");
} else {
knob.classList.remove("enabled");
}
} else if (elem.classList.contains(BLOCK_STYLE_SETTING_NAME)) {
const currentBlockStyle = themeManager.getCurrentBlockStyle();
const blockStyleIndex = selectableBlockStyles.indexOf(currentBlockStyle);
let nextBlockStyle =
const nextBlockStyle =
selectableBlockStyles[(blockStyleIndex + 1) % selectableBlockStyles.length];
themeManager.switchBlockStyle(nextBlockStyle);
handlePostBlockStyleSwitch();
Expand Down Expand Up @@ -626,7 +626,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const currentTheme = themeManager.getCurrentTheme();
(themeSetting.querySelector(".toggle") as HTMLElement).innerText =
currentTheme === "classic" ? CLASSIC_THEME_LABEL : currentTheme;
let tilesetPreferences = getPreferenceValue(TILESET_PREFERENCE_NAME);
let tilesetPreferences = getPreferenceValue<Record<string, string>>(TILESET_PREFERENCE_NAME);
if (tilesetPreferences) {
themeManager.switchTileset(currentTheme, tilesetPreferences[currentTheme]);
}
Expand Down Expand Up @@ -1078,7 +1078,6 @@ document.addEventListener("DOMContentLoaded", async () => {
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
// @ts-ignore TODO: Fix type issue with event param
beforeSend(event) {
if (
event.request &&
Expand Down Expand Up @@ -1128,13 +1127,13 @@ document.addEventListener("DOMContentLoaded", async () => {
}

await initGame(eventHandler, spawnManager, animationManager, undoManager, gameStorage);
} catch (e: any) {
} catch (e) {
if (typeof Sentry !== "undefined") Sentry.captureException(e);
const elem = createDialogContentFromTemplate("#error-dialog-content");
const errorContent = elem.querySelector(".error-text") as HTMLElement;

console.error("Could not initialize game due to error:", e);
errorContent.innerText = e.message;
errorContent.innerText = (e as Error).message;

renderDialog(elem, {
fadeIn: true,
Expand Down
9 changes: 3 additions & 6 deletions src/manager/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@ export type MergedBlock = {

export class AnimationManager {
public isAnimationEnabled: boolean;
// @ts-ignore TODO: This field is assigned in the constructor via resetState but TS is not smart enough to realize that
public newBlocks: Position[];
// @ts-ignore TODO: This field is assigned in the constructor via resetState but TS is not smart enough to realize that
public movedBlocks: (Position | undefined)[][];
// @ts-ignore TODO: This field is assigned in the constructor via resetState but TS is not smart enough to realize that
public mergedBlocks: MergedBlock[];
public newBlocks!: Position[];
public movedBlocks!: (Position | undefined)[][];
public mergedBlocks!: MergedBlock[];

private gameState: GameState | null = null;

Expand Down
2 changes: 1 addition & 1 deletion src/manager/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class AssetManager {
onProgressCallback: (progress: number) => void,
): Promise<void> {
return new Promise((resolve, reject) => {
var loadedCount = 0;
let loadedCount = 0;

const onAssetLoaded = (url: string) => {
loadedCount++;
Expand Down
2 changes: 1 addition & 1 deletion src/manager/undo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class UndoManager {
// Object is possibly 'null'.ts(2531)
// Type 'number | boolean | GameBoard' is not assignable to type 'never'.
// Type 'number' is not assignable to type 'never'.ts(2322)
// @ts-ignore
// @ts-expect-error TODO: indexed assignment type mismatch (see type errors above)
this.gameState[key] = gameStateCopy[key];
});
return gameStateCopy;
Expand Down
8 changes: 4 additions & 4 deletions src/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IGameStorage } from "./storage";

export class Preferences {
[key: string]: any;
[key: string]: unknown;
}

let preferences: Preferences = {};
Expand All @@ -16,11 +16,11 @@ export const initPreferences = (_gameStorage: IGameStorage, initialPreferences:
}
};

export const getPreferenceValue = (key: string) => {
return preferences[key];
export const getPreferenceValue = <T = unknown>(key: string): T => {
return preferences[key] as T;
};

export const savePreferenceValue = (key: string, value: any) => {
export const savePreferenceValue = (key: string, value: unknown) => {
preferences[key] = value;
gameStorage.savePreferences(preferences);
};
Expand Down
4 changes: 2 additions & 2 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ export const renderDialog = (content: HTMLElement | DocumentFragment, options?:
export type PromptDialogOptions = {
fadeIn?: boolean;
style?: CSS.Properties;
onConfirm?: Function;
onCancel?: Function;
onConfirm?: () => void;
onCancel?: () => void;
};

export const renderPromptDialog = (
Expand Down
9 changes: 5 additions & 4 deletions src/share/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,15 @@ export const triggerShare = async (shareText: string) => {
}
try {
await navigator.share(data);
} catch (err: any) {
if (err.name === "NotAllowedError") {
} catch (err) {
const error = err as Error;
if (error.name === "NotAllowedError") {
console.log("Sharing was not allowed by the user or platform");
// Fallback to copy to clipboard
return copyShareText(shareText);
} else if (err.name === "AbortError") {
} else if (error.name === "AbortError") {
console.log("User aborted share operation");
} else if (err.name === "NotSupportedError") {
} else if (error.name === "NotSupportedError") {
console.error("Share sheet operation not supported");
// Fallback to copy to clipboard
return copyShareText(shareText);
Expand Down
3 changes: 1 addition & 2 deletions src/storage/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ export class CLIGameStorage implements IGameStorage {

loadFile: <T>(filename: string) => T = (filename) => {
try {
const jsonStr = fs.readFileSync(filename);
// @ts-ignore TODO: Resolve Buffer cannot be assigned to string param type issue
const jsonStr = fs.readFileSync(filename, "utf8");
const json = JSON.parse(jsonStr);
if (typeof json !== "object") {
return {};
Expand Down
2 changes: 1 addition & 1 deletion test/browser_storage_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { IGameStorage } from "../src/storage";
global.window = {} as Window & typeof globalThis;

class MockStorage {
setItem: (key: string, value: any) => void = (_keyName, _keyValue) => {};
setItem: (key: string, value: string) => void = (_keyName, _keyValue) => {};
getItem: (key: string) => string = (_keyName) => "";
removeItem: (key: string) => void = (_keyName) => {};
clear: () => void = () => {};
Expand Down
Loading
Loading