Parity came before improvement
The most tempting mistake during a migration is redesigning every system while porting it. A new interface makes old choices look dated, and a new renderer makes it easy to imagine larger structural changes. Doing both at once makes failures much harder to diagnose. I made feature parity the first target. Login, character selection, zone loading, movement, combat, inventory, equipment, skills, quests, dialogue, shops, crafting, parties, guilds, banking, trading, settings, reconnect behavior, and error states all needed to survive the move. A system was not complete just because its main screen rendered. Inventory also had to handle pending operations, full bags, drag cancellation, server rejection, and updates arriving while another window was open. Skills had to cover casting from the book, hotbar assignment, cooldowns, targeting, and failure conditions. Mobile input had to block touches where desktop mouse input was already blocked. Those edge cases are where a port that looks finished begins to feel unreliable. The production game became the specification. Old behavior, server responses, player reports, and real content were more useful than a clean theoretical design document.Building for the actual workload
PxEngine is a custom TypeScript and WebGL2 client built around Fantasy Online 2's real requirements. It renders tile maps, animated sprites, effects, particles, interface windows, and GPU text while handling mouse, keyboard, controller, and touch input. The engine uses retained buffers, pooling, and preallocated working storage wherever the game would otherwise create garbage every frame. Browser garbage collection is not automatically a problem, but repeated per-frame allocation can turn into visible pauses during movement or combat. Avoiding those allocations also makes performance more predictable across a wide range of devices. That does not mean every subsystem was generalized into a giant reusable framework. The useful approach was to build the smallest solid primitive that solved a production need, then expand it when another real feature required more. The tile renderer, sprite batching, text system, GUI batching, pathfinding, particles, and input stack all grew from scenes the game actually had to run. Keeping the server authoritative helped here too. The client could focus on responsive presentation and clear feedback while the server remained the source of truth for movement, combat, inventory, quests, and progression. That gave the migration a stable center even while the visual and interaction layers were changing quickly.Ship the migration in slices
A live migration needs checkpoints that players can actually exercise. Waiting until every old feature has been recreated produces one enormous, difficult-to-debug launch. I brought the new client to the official website and CrazyGames first. That exposed the browser build to real accounts, real network conditions, and the full production data set. Android followed after the touch controls, storage behavior, layout constraints, and platform wrapper were ready for broader use. Steam has its own migration and testing path; it is not treated as a simple copy of the browser release. The core client stays shared, while platform-specific code remains around the edges. Authentication, local storage, storefront behavior, and wrappers differ, but movement, rendering, combat, interface systems, and game content do not need separate implementations for every platform. This staged rollout also made bug reports more useful. A setting that appeared correct in the browser could still fail when a platform supplied a different storage service. A window that blocked mouse input could still allow a mobile touch to pass through. A reconnect path could work during local testing and fail after a real network interruption. Shipping smaller slices made those boundaries visible.The mistakes were usually at the seams
The hardest bugs were rarely in a single isolated renderer function. They appeared where systems met. A feature might display correctly but lose its state after a new session. A touch event might reach the world through an open interface window. A remote actor could receive the right server update while showing the wrong transient effect. A UI port finished too quickly could preserve the happy path but miss drag cancellation, selection state, or server rejection. The lesson was to test each feature as a state machine rather than as a screenshot. What happens before the action, while it is pending, after success, after rejection, after reconnecting, and after changing platforms? That checklist catches far more than asking whether a button works. It also helped to keep the distribution wrappers thin. The more game logic that moves into platform-specific code, the harder it becomes to know whether a bug belongs to the game, the engine, the wrapper, authentication, or storage.What made the migration possible
The migration worked because it had a stable boundary. The server protocol, persistent data, and game rules remained the center of the system. PxEngine could replace the client one subsystem at a time without asking players to abandon their characters or asking the backend to become a different game simultaneously. For another small team considering the same kind of move, my main recommendations are:- Decide which contracts must remain stable before writing the replacement.
- Build a complete parity checklist that includes failure and persistence states.
- Separate platform wrappers from shared game logic.
- Test against real production content as early as possible.
- Ship in controlled slices with a rollback path.
- Treat player reports as evidence about system boundaries, not just isolated bugs.
- Delay unrelated redesigns until the replacement is trustworthy.



