The first decision required no code at all. Unciv is a Kotlin game on libGDX 1.14.0, and getting Kotlin to run in a browser means compiling JVM bytecode to JavaScript. CheerpJ only supports LWJGL 2—Unciv uses LWJGL 3, dead end. GWT operates on Java source, not bytecode, and doesn't support Kotlin. That left gdx-teavm, a library purpose-built for libGDX games that had just shipped version 1.5.1 with explicit support for libGDX 1.14.0—Unciv's exact version. The timing was lucky. A month earlier, the realistic options were a streaming server or nothing. I picked gdx-teavm and moved on.
The first build compiled 8,016 classes and produced a 62 MB app.js. The game showed a loading screen in the browser, then crashed. Then crashed again. Then again. Each fix revealed the next failure, and the full sequence took about six hours from first build to a game that actually rendered.
Most of the crashes were fast once I understood what TeaVM was doing. Throwable.getStackTrace() returns an empty array—no JVM stack trace in JavaScript. Gdx.files.external() throws unconditionally in the web backend. FileHandle.file().absolutePath throws too. A Regex.replace call using $1 group reference syntax hit a Matcher.appendReplacement bug. Each of these was an hour of investigation followed by a one-line fix.
The one that took longer was crash 4. The game loaded—no exception, no stack trace—but all buildings had empty names. Every technology, policy, and unit in the game appeared nameless. I was passing reflection patterns like "com.unciv.models.ruleset" to TeaVM's addReflectionClass, which looked correct. It wasn't. TeaVM's glob matching requires a .** suffix; without it, the pattern matches only the exact string and silently ignores every subpackage and nested class. There's no warning. No error. The reflection whitelist did nothing, libGDX's deserializer found zero fields on every model class, and the game loaded with empty content. The fix was adding .** to every pattern. One character per line, maybe eight lines total. The investigation to find it took most of an afternoon.
A similar pattern surfaced three separate times across three different files. Countables.kt, UniqueType.kt, and MapResourceSetting.kt all used enumEntry.declaringJavaClass.getField(name) to check for @Deprecated annotations on enum constants. TeaVM doesn't register enum constant fields for reflection even when the declaring class is whitelisted. Each instance threw NoSuchFieldException, each fix was the same try-catch. I hit the first one during the initial crash sequence, the second while auditing similar code, and the third two days later when "Start New Game" crashed after Quickstart already worked. After the third time I added a note to the project docs: grep for declaringJavaClass.getField(name) inside any enum before assuming the pattern is resolved.
The one that took weeks wasn't a crash at all. Unciv's skin system supports variant-specific drawables defined in a JSON config. The SkinConfig.SkinElement class has immutable fields. TeaVM's JSON deserialization can't set immutable fields, so skinVariants loaded as an empty map every time—no exception, no warning, just silently wrong data. I didn't find this during the initial port. It surfaced much later when building a custom UI skin and variant lookups never worked. The silent fallback mechanism had been active the entire time. The fix was a workaround: name atlas keys to match the full lookup path directly so the variant mechanism isn't consulted at all. One empty map. Zero errors. Two weeks before I found it.
The final diff is small because TeaVM is well-engineered—it compiles 8,016 classes correctly, and the failures that remain are narrow and consistent. But narrow and consistent doesn't mean fast to find. What compresses the cost on the next port isn't better tooling, it's having the patterns already named. The reflection glob, the enum field reflection, the immutable field deserialization—these will appear in every libGDX game compiled to TeaVM. The fix for each is the same every time. The second port starts with that knowledge. The first one had to earn it.