Using only 1 container to wrap the whole plugin

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
DarknessFalls has added this entry to his/her Everymans Thoughts, which inspired me to use container to wrap as many as I can. I eventually pushed this idea to the extreme(which is 100% my fault), resulting in something like this(DoubleX RMMV Permanent States):

/*---------------------------------------------------------------------------- * # Edit class: DataManager *----------------------------------------------------------------------------*/// Stores all extended and new functionsDoubleX_RMMV.Permanent_States.DataManager = { isDatabaseLoaded: DataManager.isDatabaseLoaded, // Extended function loadAllPermanentStateNotes: function() { var ps = DoubleX_RMMV.Permanent_States.DataManager; $dataStates.forEach(function(data) { if (data) { ps.loadPermanentStateNotes.call(this, data); } }, this); }, // loadAllPermanentStateNotes // data: The data to have its notetags read loadPermanentStateNotes: function(data) { var lines, ps; var asp = DoubleX_RMMV.Permanent_States.params.allStatesPermanent; data.meta.permanentStates = asp; if (asp === "persist" || asp === "revive") { return; } lines = data.note.split(/[\r\n]+/); ps = /< *permanent +state *: *(\w+) *>/i; for (var index = 0, length = lines.length; index < length; index++) { if (lines[index].match(ps)) { data.meta.permanentStates = RegExp.$1; return; } } } // loadPermanentStateNotes}; // DoubleX_RMMV.Permanent_States.DataManagerDataManager.isDatabaseLoaded = function() { var ps = DoubleX_RMMV.Permanent_States.DataManager; // Rewritten if (!ps.isDatabaseLoaded.apply(this, arguments)) { return false; } ps.loadAllPermanentStateNotes.call(this); return true; //}; // DataManager.isDatabaseLoaded/*---------------------------------------------------------------------------- * # Edit class: Game_BattlerBase *----------------------------------------------------------------------------*/// Stores all extended and new functionsDoubleX_RMMV.Permanent_States.Game_BattlerBase = { // Extended functions initMembers: Game_BattlerBase.prototype.initMembers, die: Game_BattlerBase.prototype.die, clearStates: Game_BattlerBase.prototype.clearStates, revive: Game_BattlerBase.prototype.revive, // /* New private instance variables * permanentStates: The id of all persist and revive permanent states * permanentStateTurns: The turns of all persist and revive permanent states */ initPermanentStates: function() { // v1.01h+ DoubleX_RMMV.Permanent_States.Game_BattlerBase[this] = { permanentStates: {}, permanentStateTurns: {} }; }, // initPermanentStates storePermanentStates: function() { var type, p; if (this._states) { p = DoubleX_RMMV.Permanent_States.Game_BattlerBase[this]; p.permanentStates = { persist: [], revive: [] }; p.permanentStateTurns = { persist: {}, revive: {} }; this.states().forEach(function(s) { type = s.meta.permanentStates; if (type === "persist" || type === "revive") { p.permanentStates[type].push(s.id); p.permanentStateTurns[type][s.id] = this._stateTurns[s.id]; } }, this); } }, // storePermanentStates // type: The permanent state type restorePermanentStates: function(type) { var ps = DoubleX_RMMV.Permanent_States.Game_BattlerBase[this]; if (ps.permanentStates[type] && ps.permanentStateTurns[type]) { this._states = this._states.concat(ps.permanentStates[type]); Object.keys(ps.permanentStateTurns[type]).forEach(function(id) { id = +id; this._stateTurns[id] = ps.permanentStateTurns[type][id]; }, this); ps.permanentStates[type] = ps.permanentStateTurns[type] = null; } } // restorePermanentStates}; // DoubleX_RMMV.Permanent_States.Game_BattlerBaseGame_BattlerBase.prototype.initMembers = function() { // v1.01h+ var ps = DoubleX_RMMV.Permanent_States.Game_BattlerBase; ps.initMembers.apply(this, arguments); ps.initPermanentStates.call(this); // Added}; // Game_BattlerBase.prototype.initMembersGame_BattlerBase.prototype.die = function() { // v1.01a+ var ps = DoubleX_RMMV.Permanent_States.Game_BattlerBase; ps.die.apply(this, arguments); ps.restorePermanentStates.call(this, "persist"); // Added}; // Game_BattlerBase.prototype.dieGame_BattlerBase.prototype.clearStates = function() { var ps = DoubleX_RMMV.Permanent_States.Game_BattlerBase; ps.storePermanentStates.call(this); // Added ps.clearStates.apply(this, arguments);}; // Game_BattlerBase.prototype.clearStatesGame_BattlerBase.prototype.revive = function() { var ps = DoubleX_RMMV.Permanent_States.Game_BattlerBase; ps.revive.apply(this, arguments); ps.restorePermanentStates.call(this, "revive"); // Added}; // Game_BattlerBase.prototype.revive
Right now I'm thinking of using this setup to all plugins having no performance critical section(hotspot) at all.

What do you think about using containers to wrap plugin codes? Will you push this idea to the extreme(wrapping everything) just like what I did?
 
Last edited by a moderator:

estriole

Veteran
Veteran
Joined
Jun 27, 2012
Messages
1,309
Reaction score
531
First Language
indonesian
i might not push it to extreme :D . since i don't see real impact benefit on using it.

but i might store my alias there...

but this somehow remind me of how i use modules in ruby

module EST  def self.mymethod()  endend class X  include ESTendbut in ruby we can do this too (i don't know in js we can or not?)

class X  def self.mymethod()   return 1;  endendclass Y  def self.mymethod()   return 3;  endend module EST  class << self  alias est_oldmethod mymethod  def mymethod()    return mymethod + 2  end endend class X  include ESTendclass Y  include ESTendX.mymethod() will = 1+2= 3Y.mymethod() will = 3+3 = 6second example done in one of moghunter script. which add code in module (with some aliasing) and then adding it to multiple class.
 

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
According to DarknessFalls, using containers to wrap up plugin codes is at least a generally good practice.

While I'm aware that good practices can become not so good(or just outright bad) if they can be pushed too far, I still want to do so to test the boundary and find the best balance between wrapping everything and wrapping nothing.

Of course, such best balances might be quite case-specific, so I won't be surprised if there's no such balance that's globally applicable :)
 

Kino

EIS Game Dev
Veteran
Joined
Nov 27, 2015
Messages
556
Reaction score
794
First Language
English
Primarily Uses
RMMV
It's not a bad idea. 
It's the object javascript pattern. Personally; I like it a lot -- the only difference between that style and the  self executing style is that the methods aren't exactly private.
There's another pattern that combines the two I believe.

http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html

It's a similar idea, but it lets you have private and public methods -- and still have a namespace for the class you want to create.

Var myClass = (function() { //Private methods and variables for my class var counter i = 0; //Object literal for our public methods return { incrementCounter : function() { counter++; }, getCounter: function() { return counter; } }; //close object literal})();myClass.incrementCounter();myClass.getCounter() // 1The methods in the return statement still have access to anything you declare as private and you still have a namespace for your private class.
 
Last edited by a moderator:

estriole

Veteran
Veteran
Joined
Jun 27, 2012
Messages
1,309
Reaction score
531
First Language
indonesian
new entry from larva in that post...

apparently benchmark shows that

var that = {}that.myFunction = function(){return this} that.myFunction()is the fastest...

edit: apparently it's in firefox. if using chrome using .call is a bit faster instead.
 
Last edited by a moderator:

DarknessFalls

Rpg Maker Jesus - JS Dev.
Veteran
Joined
Jun 7, 2013
Messages
1,393
Reaction score
210
First Language
English
Original author of my post, guy here.

So the concept with containers is not to wrap your whole system in them. But to wrap pieces of information you need to pass between scene and window or state x and state y of the game.

consider the following example:

At Point A you need to know about state X, during point B state x is updated and at point C you need to know the state of X. this is where a container comes in handy.

instead of attaching state X to the window object you create a static class that contains a get/set/empty method that helps you store data across multiple pieces of the game. This concept is used in laws and currencies and as you can see by the comments is highly controversial.

Thats because I advocate leaving the window object alone. I also advocate not cluttering up prototypes to store data thats passed between states in the game.

But darkness thats just a bunch of needless typing you troll. Maybe if you learned the RM way of  ...

Stop. there is no RM way of doing things. There is a right and a wrong way of doing it. And attaching data to the window object at game start only to fetching at the end of a battle is WRONG!!!!!

Why is it wrong? because you pollute the core data that attached and people become dependant on that data, when people become dependent on that data, it creates dependencies and it creates issues when it does change. People want the original core data that is there, they want things the can depend on. Containers are one step to backwards compatibility and OO code design.
 

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
After rereading the original article several more times and the clarifications from the original author, I'd have to admit that my inspirations from it are somehow deviated from its central point, and that's 100% my fault lol

Anyway, after reading more about immediately-invoked function expression/self-executing anonymous functions, I've changed my experimental setup into this(DoubleX RMMV Permanent States):

(function(PS) { PS.DataManager = {}; PS.DataManager.isDatabaseLoaded = DataManager.isDatabaseLoaded; DataManager.isDatabaseLoaded = function() { // Rewritten if (!PS.DataManager.isDatabaseLoaded.apply(this, arguments)) { return false; } PS.DataManager.loadAllPermanentStateNotes(); return true; // }; // DataManager.isDatabaseLoaded PS.DataManager.loadAllPermanentStateNotes = function() { $dataStates.forEach(function(data) { if (data) { PS.DataManager.loadPermanentStateNotes(data); } }); }; // loadAllPermanentStateNotes // data: The data to have its notetags read PS.DataManager.loadPermanentStateNotes = function(data) { var ps, scene, type, lines = data.note.split(/[\r\n]+/); var regex = /< *permanent +state *: *(\w+) *, *(\w+) *>/i; var scenes = PS.scenes, types = PS.types; data.meta.permanentStates = {}; ps = data.meta.permanentStates; scenes.forEach(function(s) { ps = []; }); // Reads all valid unique combinations until all of them are read for (var index = 0, length = lines.length; index < length; index++) { if (!lines[index].match(regex)) { continue; } scene = RegExp.$1; if (scenes.indexOf(scene) < 0) { continue; } type = RegExp.$2; if (types.indexOf(type) < 0 || ps[scene].indexOf(type) >= 0) { continue; } ps[scene].push(type); if (scenes.all(function(s) { return ps.length >= types.length; })) { return; } } // }; // loadPermanentStateNotes PS.Game_BattlerBase = {}; /*------------------------------------------------------------------------ * New private instance variables *------------------------------------------------------------------------*/ /* _permanentStates: The id of all permanent states of all scene/type * _permanentStateTurns: The turns of all permanent states of all scene/type */ PS.Game_BattlerBase.initMembers = Game_BattlerBase.prototype.initMembers; Game_BattlerBase.prototype.initMembers = function() { // v1.01h+ PS.Game_BattlerBase.initPermanentStates.call(this); // Added PS.Game_BattlerBase.initMembers.apply(this, arguments); }; // Game_BattlerBase.prototype.initMembers PS.Game_BattlerBase.clearStates = Game_BattlerBase.prototype.clearStates; Game_BattlerBase.prototype.clearStates = function() { PS.Game_BattlerBase.storePermanentStates.call(this); // Added PS.Game_BattlerBase.clearStates.apply(this, arguments); }; // Game_BattlerBase.prototype.clearStates PS.Game_BattlerBase.die = Game_BattlerBase.prototype.die; Game_BattlerBase.prototype.die = function() { // v1.01a+ PS.Game_BattlerBase.die.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "persist"); // }; // Game_BattlerBase.prototype.die PS.Game_BattlerBase.revive = Game_BattlerBase.prototype.revive; Game_BattlerBase.prototype.revive = function() { PS.Game_BattlerBase.revive.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "revive"); // }; // Game_BattlerBase.prototype.revive PS.Game_BattlerBase.recoverAll = Game_BattlerBase.prototype.recoverAll; Game_BattlerBase.prototype.recoverAll = function() { // v1.02a+ PS.Game_BattlerBase.recoverAll.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "recover"); // }; // Game_BattlerBase.prototype.recoverAll PS.Game_BattlerBase.initPermanentStates = function() { // v1.01h+ var types = PS.types; this._permanentStates = {}; this._permanentStateTurns = {}; PS.scenes.forEach(function(scene) { this._permanentStates[scene] = {}; this._permanentStateTurns[scene] = {}; types.forEach(function(type) { this._permanentStates[scene][type] = []; this._permanentStateTurns[scene][type] = {}; }, this); }, this); }; // initPermanentStates PS.Game_BattlerBase.storePermanentStates = function() { var sc; if (!this._states || !this._stateTurns) { return; } sc = PS.scene(); this.states().forEach(function(s) { s.meta.permanentStates[sc].forEach(function(t) { this._permanentStates[sc][t].push(s.id); this._permanentStateTurns[sc][t][s.id] = this._stateTurns[s.id]; }, this); }, this); }; // storePermanentStates // type: The permanent state type PS.Game_BattlerBase.restorePermanentStates = function(type) { var sc; if (!this._states || !this._stateTurns) { return; } sc = PS.scene(); this._states = this._states.concat(this._permanentStates[sc][type]); this._permanentStates[sc][type].length = 0; Object.keys(this._permanentStateTurns[sc][type]).forEach(function(id) { id = +id; this._stateTurns[id] = this._permanentStateTurns[sc][type][id]; }, this); this._permanentStateTurns[sc][type] = {}; }; // restorePermanentStates})(DoubleX_RMMV.Permanent_States);
Besides adding IIFE into the mix, I also stopped wrapping new instance variables needing to be saved in savefiles.

Again, I'll consider to use this setup in all plugins having no performance critical section(hotspot) at all.

What do you think about this setup instead?
 
Last edited by a moderator:

DarknessFalls

Rpg Maker Jesus - JS Dev.
Veteran
Joined
Jun 7, 2013
Messages
1,393
Reaction score
210
First Language
English
After rereading the original article several more times and the clarifications from the original author, I'd have to admit that my inspirations from it are somehow deviated from its central point, and that's 100% my fault lol

Anyway, after reading more about immediately-invoked function expression/self-executing anonymous functions, I've changed my experimental setup into this(DoubleX RMMV Permanent States):

(function(PS) { PS.DataManager = {}; PS.DataManager.isDatabaseLoaded = DataManager.isDatabaseLoaded; DataManager.isDatabaseLoaded = function() { // Rewritten if (!PS.DataManager.isDatabaseLoaded.apply(this, arguments)) { return false; } PS.DataManager.loadAllPermanentStateNotes(); return true; // }; // DataManager.isDatabaseLoaded PS.DataManager.loadAllPermanentStateNotes = function() { $dataStates.forEach(function(data) { if (data) { PS.DataManager.loadPermanentStateNotes(data); } }); }; // loadAllPermanentStateNotes // data: The data to have its notetags read PS.DataManager.loadPermanentStateNotes = function(data) { var ps, scene, type, lines = data.note.split(/[\r\n]+/); var regex = /< *permanent +state *: *(\w+) *, *(\w+) *>/i; var scenes = PS.scenes, types = PS.types; data.meta.permanentStates = {}; ps = data.meta.permanentStates; scenes.forEach(function(s) { ps = []; }); // Reads all valid unique combinations until all of them are read for (var index = 0, length = lines.length; index < length; index++) { if (!lines[index].match(regex)) { continue; } scene = RegExp.$1; if (scenes.indexOf(scene) < 0) { continue; } type = RegExp.$2; if (types.indexOf(type) < 0 || ps[scene].indexOf(type) >= 0) { continue; } ps[scene].push(type); if (scenes.all(function(s) { return ps.length >= types.length; })) { return; } } // }; // loadPermanentStateNotes PS.Game_BattlerBase = {}; /*------------------------------------------------------------------------ * New private instance variables *------------------------------------------------------------------------*/ /* _permanentStates: The id of all permanent states of all scene/type * _permanentStateTurns: The turns of all permanent states of all scene/type */ PS.Game_BattlerBase.initMembers = Game_BattlerBase.prototype.initMembers; Game_BattlerBase.prototype.initMembers = function() { // v1.01h+ PS.Game_BattlerBase.initPermanentStates.call(this); // Added PS.Game_BattlerBase.initMembers.apply(this, arguments); }; // Game_BattlerBase.prototype.initMembers PS.Game_BattlerBase.clearStates = Game_BattlerBase.prototype.clearStates; Game_BattlerBase.prototype.clearStates = function() { PS.Game_BattlerBase.storePermanentStates.call(this); // Added PS.Game_BattlerBase.clearStates.apply(this, arguments); }; // Game_BattlerBase.prototype.clearStates PS.Game_BattlerBase.die = Game_BattlerBase.prototype.die; Game_BattlerBase.prototype.die = function() { // v1.01a+ PS.Game_BattlerBase.die.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "persist"); // }; // Game_BattlerBase.prototype.die PS.Game_BattlerBase.revive = Game_BattlerBase.prototype.revive; Game_BattlerBase.prototype.revive = function() { PS.Game_BattlerBase.revive.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "revive"); // }; // Game_BattlerBase.prototype.revive PS.Game_BattlerBase.recoverAll = Game_BattlerBase.prototype.recoverAll; Game_BattlerBase.prototype.recoverAll = function() { // v1.02a+ PS.Game_BattlerBase.recoverAll.apply(this, arguments); // Added PS.Game_BattlerBase.restorePermanentStates.call(this, "recover"); // }; // Game_BattlerBase.prototype.recoverAll PS.Game_BattlerBase.initPermanentStates = function() { // v1.01h+ var types = PS.types; this._permanentStates = {}; this._permanentStateTurns = {}; PS.scenes.forEach(function(scene) { this._permanentStates[scene] = {}; this._permanentStateTurns[scene] = {}; types.forEach(function(type) { this._permanentStates[scene][type] = []; this._permanentStateTurns[scene][type] = {}; }, this); }, this); }; // initPermanentStates PS.Game_BattlerBase.storePermanentStates = function() { var sc; if (!this._states || !this._stateTurns) { return; } sc = PS.scene(); this.states().forEach(function(s) { s.meta.permanentStates[sc].forEach(function(t) { this._permanentStates[sc][t].push(s.id); this._permanentStateTurns[sc][t][s.id] = this._stateTurns[s.id]; }, this); }, this); }; // storePermanentStates // type: The permanent state type PS.Game_BattlerBase.restorePermanentStates = function(type) { var sc; if (!this._states || !this._stateTurns) { return; } sc = PS.scene(); this._states = this._states.concat(this._permanentStates[sc][type]); this._permanentStates[sc][type].length = 0; Object.keys(this._permanentStateTurns[sc][type]).forEach(function(id) { id = +id; this._stateTurns[id] = this._permanentStateTurns[sc][type][id]; }, this); this._permanentStateTurns[sc][type] = {}; }; // restorePermanentStates})(DoubleX_RMMV.Permanent_States);


Besides adding IIFE into the mix, I also stopped wrapping new instance variables needing to be saved in savefiles.

Again, I'll consider to use this setup in all plugins having no performance critical section(hotspot) at all.

What do you think about this setup instead?



This black boxes your set up. What that means is that your code is inside of a function that is scoped to its self so any aliasing you do cannot be reached by external scripts, so make sure you always call the parent function of the function you alias.
 

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
This black boxes your set up. What that means is that your code is inside of a function that is scoped to its self so any aliasing you do cannot be reached by external scripts, so make sure you always call the parent function of the function you alias.
PS there is DoubleX_RMMV.Permanent_States, which can be accessed from anywhere(I just didn't quote that part as well) :)
 

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
Just realized 1 nontrivial issue with my new experimental setup:

Without wrapping up my new functions into containers, I can utilize polymorphism via subtyping this way:

/* interceptor: The battler intercepting the skill/item * item: The skill/item to be intercepted */ BattleManager.successIntercept = function(interceptor, item) { // Some codes irrelevant with this issue // if (interceptor.canLearnInterceptItem(item)) { // Some codes irrelevant with this issue // } // Some codes irrelevant with this issue // }; // BattleManager.successIntercept// item: The skill/item to be interceptedGame_Battler.prototype.canLearnInterceptItem = function(item) { return false;}; // Game_Battler.prototype.canLearnInterceptItem// item: The skill/item to be interceptedGame_Actor.prototype.canLearnInterceptItem = function(item) { if (!DataManager.isSkill(item)) { return false; } if (!item.meta.learnInterceptItem) { return false; } return this.interceptNote("learnInterceptItem");}; // Game_Actor.prototype.canLearnInterceptItem
Using my new experimental setup will lead to either of these, however:

/* interceptor: The battler intercepting the skill/item * item: The skill/item to be intercepted */ II.BattleManager.successIntercept = function(interceptor, item) { // Some codes irrelevant with this issue // if (II.Game_Battler.canLearnInterceptItem.call(interceptor, item)) { // Some codes irrelevant with this issue // } // Some codes irrelevant with this issue // }; // II.BattleManager.successIntercept // item: The skill/item to be intercepted II.Game_Battler.canLearnInterceptItem = function(item) { if (!this.isActor() || !DataManager.isSkill(item)) { return false; } if (!item.meta.learnInterceptItem) { return false; } return II.Game_Battler.interceptNote.call(this, "learnInterceptItem"); }; // II.Game_Battler.canLearnInterceptItem
Code:
    /* interceptor: The battler intercepting the skill/item     * item: The skill/item to be intercepted     */    II.BattleManager.successIntercept = function(interceptor, item) {        // Some codes irrelevant with this issue        //        if (interceptor.isActor()) {            if (II.Game_Actor.canLearnInterceptItem.call(interceptor, item)) {                // Some codes irrelevant with this issue                //            }        }        // Some codes irrelevant with this issue        //    }; // II.BattleManager.successIntercept    // item: The skill/item to be intercepted    II.Game_Actor.canLearnInterceptItem = function(item) {        if (!DataManager.isSkill(item)) { return false; }        if (!item.meta.learnInterceptItem) { return false; }        return II.Game_Battler.interceptNote.call(this, "learnInterceptItem");    }; // II.Game_Actor.canLearnInterceptItem
On a side note: All the above functions are wrapped up in a self-executing anonymous function, with II, an object that can be accessed from anywhere, passed into that function.

To me, it means an extra checking(whether interceptor is an actor) has to be added now, when it can be replaced by polymorphism via subtying without my new experimental setup.

Until now, I still can't find a way to utilize polymorphism via subtying to avoid that added check.

I'd be glad to know some ways that can do just that while still conforming to my new experimental setup, if there's any such way.

While I can sometimes accept the loss of polymorphism via subtyping in my new experimental setup if that's the case, I'd also like to know your thoughts on that :)
 
Last edited by a moderator:

DarknessFalls

Rpg Maker Jesus - JS Dev.
Veteran
Joined
Jun 7, 2013
Messages
1,393
Reaction score
210
First Language
English
I recently wrote a blog article on "black boxing" and anonymous functions and how to properly provide API to end users.  I think that the more we "close" off the ability for people to walk all over the code that we write the more we write code that is more compatible with other peoples scripts, especially if we learn to always call the parent function and not write destructive code.
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Profile Posts

People3_5 and People3_8 added!

so hopefully tomorrow i get to go home from the hospital i've been here for 5 days already and it's driving me mad. I miss my family like crazy but at least I get to use my own toiletries and my own clothes. My mom is coming to visit soon i can't wait to see her cause i miss her the most. :kaojoy:
Couple hours of work. Might use in my game as a secret find or something. Not sure. Fancy though no? :D
Holy stink, where have I been? Well, I started my temporary job this week. So less time to spend on game design... :(
Cartoonier cloud cover that better fits the art style, as well as (slightly) improved blending/fading... fading clouds when there are larger patterns is still somewhat abrupt for some reason.

Forum statistics

Threads
105,868
Messages
1,017,083
Members
137,583
Latest member
write2dgray
Top