Cold Takes/12 - Mighty Morphing
Players coming from other strategy games often find classic conventions broken and common systems missing. The three focused on improving units - morphing, veterancy, and research - are largely absent from ZK. Today is the first of a series of guest articles by Sprung that will talk about them.
First, let's talk about the unit improvement system that still exists vestigially in ZK: morphing. Morphing is often called upgrading, but I won't use that term to avoid confusion since it can also mean research. Very generally, morphing lets you improve or modify a single unit, as opposed to upgrading all existing and future units of that type, like research does. This can be as little as paying three seconds of time to deploy a Terran siege tank; or as drastic as changing two Protoss Templar into an Archon. Of course you can have morphing as a major part of faction identity and aesthetics, but let's talk gameplay. When to have morph instead of just letting players build things normally?
File:110756504e95530061768e4c82a43bb3c4d71372.png An easy answer is that sometimes you already decided not to let players build more of the unit. In ZK you only get one commander, and there's a limited number of geothermal spots. Accordingly, they are the only two cases where morphing is just a straightforward upgrade. The astute reader may have noticed that metal spots are also a limited resource, and indeed mexes used to morph into Moho mines for a short period before overdrive was instated. Here morphing lets you decide to get more mileage while working within the limit.
A nice example from another game would be the Chinese Overlord tank from C&C Generals. It has a soft limit by virtue of simply being too expensive to field many of them, but you can add extra weaponry on top for even more firepower. Allocating skill points for WC3 heroes can also be thought of as a form of morphing, and it's no accident that allocating modules for ZK commanders looks so similar.
File:0fd658cf4b6965cae07941ead5a02e4af6e8b19d.png The flip side of a unit limit is flexibility. RTS is, among other things, about making the correct units to adjust to a quickly changing battlefield. The flexibility of morphing lets players continue making choices about their unit composition in face of a limit. Warcraft heroes, Generals Overlords, and ZK Commanders all present multiple morph choices for this reason. Similarly, in Dawn of War earlygame, forcing the player to choose between Flamers or Bolters for their first tactical marine squad, blindly, would not be good design. Morphing some marines to equip weapons lets players make that choice later, while still having a squad on the field to capture points, scout, and do other tasks that don't yet involve picking a specific unit composition. Then, when you scout Orks? Brother, get the Flamer. The Heavy Flamer.
Back to ZK though, since we don't really put any pressure on the player in the form of unit limits, there is barely any need for the flexibility. Ramping economy means that even the most expensive units can become somewhat affordable over time, and by that point you have all the flexibility in the world anyway. Would it hurt to have a little extra?
The problem with flexibility is that it creates a sort of Schrödinger's unit that occupies multiple spots in design space at once, to be resolved into one of them when observing the enemy. The danger ZK sees here is that a monospammed unit could become a composition thanks to its own flexible morphs. Everything becomes much simpler when you can do Just-In-Time reinforcements of the correct type without having to do lame nerd stuff like "logistics" or "foresight". No travel time from the factory. Less risk of overbuilding a variant. Of course these can be adjusted via other stats, but that would all come with a reduction in flexibility.
File:1a136c2b5fad221386fcfc5adf6dac0c93b39ceb.png What about "inflexible" morphs, where there isn't really a composition to talk about and you just want to add more stats? Say, why doesn't a factory have a +10 Buildpower upgrade? It would be more atomic than a Caretaker, who can also do other tasks such as repair! Or, if I want to upgrade Felon battery recharge rate, I have to either get Convicts and have to pay for buildpower I didn't sign up for, or Thugs with inefficient guns thrown in. Meanwhile, a +15 shield regeneration morph would be atomic while remaining a choice! Part of the answer is that units are physical entities in the world and thus there is more interaction between them. In particular, Convict buildpower and Thug guns nudge the player into using them, which then cascades into the usual complexity of the game when the enemy has to respond in turn. The other part is emergence. Felon wasn't initially designed to work as a standalone unit and get "batteries" added; rather, it was designed to itself be added to a shieldball as a way to spend charge. Batteries are a player discovery, if an extremely obvious one.
File:B12fa40e432ba1d741ddfe03a96467c578c2b98e.png An interesting feature of morph is that it provides its own buildpower and can have different tech requirements, which then can be used to adjust the appeal of different options. If you require a unit to be built "normally" then it competes for infrastructure (buildpower, logistics) with other units that you may rather be building. Morphing is a way to avoid this, which makes the unit more attractive, or the usual infrastructure less attractive. To put this in concrete terms: say you want to encourage mobile cloakers, because cloaking is cool. If you require people to build them from a cloaky factory, then sometimes they'd rather build a different factory as a secondary, or if they already have a cloaky factory, they'd rather build a different cloakybot. Morph lets a unit be its own little temporary factory, and the elegance of this is that much like energy requirements, it touches just the infrastructure and not the unit itself. The morphs for mobile cloaker and shield, while originally created just as a way to let them redeploy, fall under this category. We try to avoid this type of morph outside of utility units since infrastructure exists for a reason, and while it is easy to manage it should not be obsoleted.
File:05acba603f1c69bfc4eeac0022b589261bad2dac.png At the beginning I mentioned Siege Mode as an example of morph. In ZK, a handful of units - for example Crab, Fencer and Emissary - have a soft siege mode where standing still gives them some sort of benefit. They deploy so fast that not doing so would mostly be a matter of not having the APM, so rather than make the players fight the UI by spamming Deploy every time they want to move, the units deploy on their own. To let this happen, there is a deliberate large gap in unit design space where nothing has a deployment mode that would be long enough not to automate, yet short enough to make you miss the automation by being commonly desirable. On the other side of this gap we have Desolator, which can deploy into an armoured form, but it takes long enough to make it a rare decision that can afford to be manual. Thus, unit intelligence, or rather avoiding situations that would create unit stupidity, is what prevents sufficiently cheap morphs from existing.
A final way morph can be used is to randomly change a unit into a completely different unit, apparently for no good reason. Why does Lurker morph from Hydralisk? The answer seems less to do with any explicit unit design, and more with how it just had to be crammed somewhere given there were no empty slots on Larva build GUI. Hydra was the least awkward place to put it. Surely ZK would not do something as silly as, say, let Knight morph into Crab without an extraordinarily good reason, right?
File:B12d3548246ba30ee5d2a24567a71ac779dfef3a.png ...right?
But that is a topic for another week.
Debug data:
[SQLBagOStuff] MainObjectStash using store ReplicatedBagOStuff
[objectcache] MainWANObjectCache using store EmptyBagOStuff
IP: 216.73.216.149
Start request GET /mediawiki/index.php?oldid=9883&title=Cold_Takes%2F12_-_Mighty_Morphing
HTTP HEADERS:
CONTENT-TYPE:
CONTENT-LENGTH: 0
USER-AGENT: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
HOST: zero-k.info
ACCEPT-ENCODING: gzip, br, zstd, deflate
ACCEPT: */*
CONNECTION: close[localisation] LocalisationCache: using store LCStoreDB
[session] SessionManager using store SqlBagOStuff
[DBReplication] Cannot use ChronologyProtector with EmptyBagOStuff
[DBReplication] Wikimedia\Rdbms\LBFactory::getChronologyProtector: request info {
"IPAddress": "216.73.216.149",
"UserAgent": "Mozilla\/5.0 AppleWebKit\/537.36 (KHTML, like Gecko; compatible; ClaudeBot\/1.0; +claudebot@anthropic.com)",
"ChronologyProtection": false,
"ChronologyPositionIndex": 0,
"ChronologyClientId": false
}[DBConnection] Wikimedia\Rdbms\LoadBalancer::lazyLoadReplicationPositions: executed chronology callback.
[DBConnection] Wikimedia\Rdbms\LoadBalancer::getLocalConnection: connected to database 0 at 'localhost'.
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[session] SessionBackend "dmc3evu5o4lr2ioqtvfcg51s0c4dpj0t" is unsaved, marking dirty in constructor
[session] SessionBackend "dmc3evu5o4lr2ioqtvfcg51s0c4dpj0t" save: dataDirty=1 metaDirty=1 forcePersist=0
[cookie] already deleted setcookie: "wikidb229_mw__session", "", "1723733842", "/", "", "", "1"
[cookie] already deleted setcookie: "wikidb229_mw_UserID", "", "1723733842", "/", "", "", "1"
[cookie] already deleted setcookie: "wikidb229_mw_Token", "", "1723733842", "/", "", "", "1"
[cookie] already deleted setcookie: "forceHTTPS", "", "1723733842", "/", "", "", "1"
[DBConnection] Wikimedia\Rdbms\LoadBalancer::getLocalConnection: connected to database 0 at 'localhost'.
Title::getRestrictionTypes: applicable restrictions to [[Cold Takes/12 - Mighty Morphing]] are {edit,move}
[ContentHandler] Created handler for wikitext: WikitextContentHandler
[MessageCache] MessageCache using store SqlBagOStuff
[localisation] LocalisationCache::isExpired(en): cache for en expired due to GlobalDependency
[localisation] LocalisationCache::recache: got localisation for en from source
[DBQuery] startAtomic: entering level 0 (LCStoreDB::finishWrite)
[DBQuery] endAtomic: leaving level 0 (LCStoreDB::finishWrite)
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] SqlBagOStuff::lock failed due to timeout for wikidb229-mw_:messages:en.
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[MessageCache] MessageCache::load: Loading en... local cache is empty, global cache is expired/volatile, loading from database
ParserFactory: using preprocessor: Preprocessor_Hash
Unstubbing $wgLang on call of $wgLang::_unstub from ParserOptions->__construct
[caches] parser: SqlBagOStuff
Article::view using parser cache: no
Article::view: doing uncached parse
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
[SQLBagOStuff] Connection mysql object #127 (handle id #121) will be used for SqlBagOStuff
Parser cache options found.
[Preprocessor] Cached preprocessor output (key: wikidb229-mw_:preprocess-hash:a8b054a83aba6ae82f25575015353be7:0)
[objectcache] Rejected set() for wikidb229-mw_:page:10:62473c79b69e43e489a3d7e851f6e8629f0832da due to pending writes.
[objectcache] Rejected set() for global:revision-row-1.29:wikidb229-mw_:1640:10032 due to pending writes.
[Preprocessor] Cached preprocessor output (key: wikidb229-mw_:preprocess-hash:17aa7002849cc6ce738a62fa8e371aed:1)
[objectcache] Rejected set() for wikidb229-mw_:page:10:1eea3d5309d2a88c1e83cbfafba24489c41a09ad due to pending writes.
[objectcache] Rejected set() for global:revision-row-1.29:wikidb229-mw_:1979:10035 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:page:828:3df63b7acb0522da685dad5fe84b81fdd7b25264 due to pending writes.
[objectcache] Rejected set() for global:revision-row-1.29:wikidb229-mw_:78:981 due to pending writes.
[ContentHandler] Created handler for Scribunto: ScribuntoContentHandler
[Scribunto] Scribunto_LuaStandaloneInterpreter::__construct: creating interpreter: ""C:\Projekty\zero-k.info\www\mediawiki\extensions\Scribunto\includes\engines\LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe" "C:\Projekty\zero-k.info\www\mediawiki\extensions\Scribunto\includes\engines\LuaStandalone/mw_main.lua" "C:\Projekty\zero-k.info\www\mediawiki\extensions\Scribunto\includes" "0" "8""
[gitinfo] Candidate cacheFile=C:\Projekty\zero-k.info\www\mediawiki/gitinfo.json for C:\Projekty\zero-k.info\www\mediawiki
[gitinfo] Cache incomplete for C:\Projekty\zero-k.info\www\mediawiki
SiteStats::loadAndLazyInit: reading site_stats from replica DB
[objectcache] Rejected set() for wikidb229-mw_:SiteStats:groupcounts:sysop due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:file:73af53ccad147c77191d984a0352b7bfb895e391 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:page:6:5799d3798193f993b004068e833fa82ccb8c63de due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:file:5799d3798193f993b004068e833fa82ccb8c63de due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:page:10:719ea396ad92e01b4757ec2b93bb1e5f270f771d due to pending writes.
[objectcache] Rejected set() for global:revision-row-1.29:wikidb229-mw_:12:79 due to pending writes.
[Mime] MimeAnalyzer::loadFiles: loading mime types from C:\Projekty\zero-k.info\www\mediawiki\includes/libs/mime/mime.types
[Mime] MimeAnalyzer::loadFiles: loading mime info from C:\Projekty\zero-k.info\www\mediawiki\includes/libs/mime/mime.info
File::transform: Doing stat for mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/1px-12_-_Mighty_Morphing.jpg
TransformationalImageHandler::doTransform: creating 1x1 thumbnail at mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/1px-12_-_Mighty_Morphing.jpg using scaler im
TransformationalImageHandler::doTransform: Transforming later per flags.
File::transform: Doing stat for mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/2px-12_-_Mighty_Morphing.jpg
TransformationalImageHandler::doTransform: creating 2x1 thumbnail at mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/2px-12_-_Mighty_Morphing.jpg using scaler im
TransformationalImageHandler::doTransform: Transforming later per flags.
File::transform: Doing stat for mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/2px-12_-_Mighty_Morphing.jpg
TransformationalImageHandler::doTransform: creating 2x1 thumbnail at mwstore://local-backend/local-thumb/4/4c/12_-_Mighty_Morphing.jpg/2px-12_-_Mighty_Morphing.jpg using scaler im
TransformationalImageHandler::doTransform: Transforming later per flags.
[objectcache] Rejected set() for wikidb229-mw_:file:4cc865884184ed6274acfd48ff2c762b7691d312 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:d5c56c5c1d82c03839364a431b762aa1 due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3A110756504e95530061768e4c82a43bb3c4d71372.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[objectcache] Rejected set() for wikidb229-mw_:file:d13f9a6560b81254b150b09c20c0db60eb8e7ddf due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:2d235e71321bec9299000e7d5ad91e1d due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3A0fd658cf4b6965cae07941ead5a02e4af6e8b19d.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[objectcache] Rejected set() for wikidb229-mw_:file:5e99fcf5174bca88e729f37488e4a82c0f02d672 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:e2baea2568c68f4f8d015ed6669c4491 due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3A1a136c2b5fad221386fcfc5adf6dac0c93b39ceb.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[objectcache] Rejected set() for wikidb229-mw_:file:132c8457596bcb0c104e6cb8d3488dd041b34a27 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:464a3b0ea781df2559159db69f055f86 due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3AB12fa40e432ba1d741ddfe03a96467c578c2b98e.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[objectcache] Rejected set() for wikidb229-mw_:file:f87cbfd7de3f971a6d7431fdb8c1d755437dfdd4 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:63c4fa18459171a5abc8c7b63675f2dc due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3A05acba603f1c69bfc4eeac0022b589261bad2dac.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[objectcache] Rejected set() for wikidb229-mw_:file:d87c2028451fc3e90b65094a7e66be7ed0e5c352 due to pending writes.
[objectcache] Rejected set() for wikidb229-mw_:image_redirect:07c847c8d01c0d14d04a2e4854915ad5 due to pending writes.
ForeignAPIRepo: HTTP GET: https://commons.wikimedia.org/w/api.php?titles=File%3AB12d3548246ba30ee5d2a24567a71ac779dfef3a.png&iiprop=timestamp%7Cuser%7Ccomment%7Curl%7Csize%7Csha1%7Cmetadata%7Cmime%7Cmediatype%7Cextmetadata&prop=imageinfo&iimetadataversion=2&iiextmetadatamultilang=1&format=json&action=query&redirects=true&uselang=en
[http] * Error fetching URL: SSL certificate problem: unable to get local issuer certificate
* There was a problem during the HTTP request: 0 Error[Preprocessor] Cached preprocessor output (key: wikidb229-mw_:preprocess-hash:a8b054a83aba6ae82f25575015353be7:0)
MediaWiki::preOutputCommit: primary transaction round committed
MediaWiki::preOutputCommit: pre-send deferred updates completed
MediaWiki::preOutputCommit: session changes committed
MediaWiki::preOutputCommit: LBFactory shutdown completed
File::transform: Doing stat for mwstore://local-backend/local-thumb/d/d8/Other_morph.png/915px-Other_morph.png
[FileOperation] FileBackendStore::ingestFreshFileStats: File mwstore://local-backend/local-thumb/d/d8/Other_morph.png/915px-Other_morph.png does not exist
TransformationalImageHandler::doTransform: creating 915x566 thumbnail at C:\Windows\TEMP\mwtmp-IUSR/transform_aac0caac1f7e.png using scaler im
TransformationalImageHandler::doTransform: returning unscaled image
Title::getRestrictionTypes: applicable restrictions to [[Cold Takes/12 - Mighty Morphing]] are {edit,move}