001package squidpony.squidgrid.gui.gdx;
002
003import com.badlogic.gdx.Gdx;
004import com.badlogic.gdx.graphics.Camera;
005import com.badlogic.gdx.graphics.Color;
006import com.badlogic.gdx.graphics.g2d.Batch;
007import com.badlogic.gdx.math.Frustum;
008import com.badlogic.gdx.math.MathUtils;
009import com.badlogic.gdx.scenes.scene2d.Action;
010import com.badlogic.gdx.scenes.scene2d.Actor;
011import com.badlogic.gdx.scenes.scene2d.Stage;
012import com.badlogic.gdx.scenes.scene2d.actions.Actions;
013import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction;
014import com.badlogic.gdx.utils.Align;
015import com.badlogic.gdx.utils.IntIntMap;
016import com.badlogic.gdx.utils.viewport.Viewport;
017import squidpony.ArrayTools;
018import squidpony.IColorCenter;
019import squidpony.panel.IColoredString;
020import squidpony.squidgrid.Direction;
021import squidpony.squidgrid.Radius;
022import squidpony.squidmath.Noise;
023import squidpony.squidmath.NumberTools;
024import squidpony.squidmath.StatefulRNG;
025import squidpony.squidmath.WhirlingNoise;
026
027import java.util.ArrayList;
028
029/**
030 * A general-purpose char display grid that supports one layer of backgrounds and arbitrarily many layers of foreground,
031 * only rendering foreground chars when something is present at a char's location. This also stores an ArrayList of
032 * {@link TextCellFactory.Glyph} items that can be added to using {@link #glyph(char, Color, int, int)} or one of
033 * several other methods; these Glyphs can have effects performed on them that are analogous to the effects on
034 * {@link SquidPanel}'s {@link AnimatedEntity} effects, such as
035 * {@link #slide(TextCellFactory.Glyph, int, int, int, int, float, Runnable)}. Unlike SquidPanel and SquidLayers, this
036 * class does not typically provide many overloads for each method, and usually limits the overloads to one that takes
037 * a packed float color and one that takes a libGDX {@link Color}, and all the other parameters are expected to be
038 * specified explicitly rather than relying on default behavior. SparseLayers tends to perform better than SquidLayers,
039 * especially when a lot of the map is unassigned/blank. It will cause less GC pressure when packed floats are used, and
040 * because {@link SColor} constants pre-calculate their packed float values, using the overloads that take a Color will
041 * perform a little better when given an SColor than a plain libGDX Color. This class makes heavy use of
042 * {@link SColor#lerpFloatColors(float, float, float)} because it should cause no GC pressure (it doesn't create
043 * temporary Color objects), and if you don't want to mutate Colors in-place (which is a bad idea for constants), then
044 * using lerpFloatColors with the packed float color API here should be very effective.
045 * <br>
046 * The names are a little different for some member fields here; there's a {@link TextCellFactory} called {@link #font},
047 * a {@code float[][]} called {@link #backgrounds} that stores packed float colors, an ArrayList of
048 * {@link SparseTextMap} that implements the sparse drawing behavior of foreground chars called {@link #layers}, and so
049 * on.
050 * <br>
051 * Created by Tommy Ettinger on 7/28/2017.
052 */
053public class SparseLayers extends Actor implements IPackedColorPanel {
054    public int gridWidth, gridHeight;
055    /**
056     * A 2D float array of background colors as packed floats. Must have dimensions matching {@link #gridWidth} and
057     * {@link #gridHeight}, and must be non-null with non-null interior arrays, but can otherwise be assigned 2D float
058     * arrays from other sources (that use floats to represent colors, not just any number).
059     */
060    public float[][] backgrounds;
061    /**
062     * The default foreground color when none is specified, as a Color object. Defaults to white.
063     */
064    public Color defaultForeground = SColor.WHITE,
065    /**
066     * Currently unused internally, but public so if some background color needs to be stored with this SparseLayers,
067     * then there will be a logical place for it. Defaults to black.
068     */
069            defaultBackground = SColor.BLACK;
070    /**
071     * The value of {@link #defaultForeground} as a float, for easier usage with the methods that use floats for colors.
072     * Defaults to white.
073     */
074    public float defaultPackedForeground = SColor.FLOAT_WHITE,
075    /**
076     * The value of {@link #defaultBackground} as a float, for easier usage with the methods that use floats for colors.
077     * Currently unused internally, but public so if some background color needs to be stored with this SparseLayers,
078     * then there will be a logical place for it. Defaults to black.
079     */
080            defaultPackedBackground = SColor.FLOAT_BLACK;
081    /**
082     * A list of SparseTextMap objects, with each representing a foreground layer.
083     */
084    public ArrayList<SparseTextMap> layers;
085    protected IntIntMap mapping;
086    /**
087     * The TextCellFactory that is used to determine font size as well as cell size; must be initialized, usually using
088     * {@link TextCellFactory#initBySize()}, if this is changed after construction.
089     */
090    public TextCellFactory font;
091    /**
092     * Will always be 0 unless user code affects it; use {@link #hasActiveAnimations()} to track animations instead.
093     * The approach of tracking animations via a counter was prone to error when multiple effects might remove a Glyph
094     * or adjust the animationCount in one direction but not the other. This could result in never-ending phases of a
095     * game where input wasn't handled because animationCount was greater than 0, but whatever effect was supposed to
096     * reduce animationCount would never happen.
097     * @deprecated Use {@link #hasActiveAnimations()} instead of adjusting this manually
098     */
099    public int animationCount;
100    /**
101     * A list of individually-movable Glyph objects. This field is public, and though it shouldn't be assigned null (you
102     * don't really need to be told that), there may be cases where you may need manual control over what Glyph objects
103     * should be added or removed from this SparseLayers.
104     */
105    public ArrayList<TextCellFactory.Glyph> glyphs;
106    /**
107     * An IColorCenter to affect color caching and filtering; usually a SquidColorCenter, which can be easily obtained
108     * via {@link DefaultResources#getSCC()}.
109     */
110    public IColorCenter<Color> scc;
111
112    protected SparseLayers()
113    {
114    }
115    public SparseLayers(int gridWidth, int gridHeight)
116    {
117        this(gridWidth, gridHeight, 10, 16, DefaultResources.getStretchableFont());
118    }
119
120    public SparseLayers(int gridWidth, int gridHeight, float cellWidth, float cellHeight)
121    {
122        this(gridWidth, gridHeight, cellWidth, cellHeight, DefaultResources.getStretchableFont());
123    }
124
125    public SparseLayers(int gridWidth, int gridHeight, float cellWidth, float cellHeight, TextCellFactory font) {
126        this(gridWidth, gridHeight, cellWidth, cellHeight, font, 0f, 0f);
127    }
128
129    public SparseLayers(int gridWidth, int gridHeight, float cellWidth, float cellHeight, TextCellFactory font, float xOffset, float yOffset) {
130        this.gridWidth = MathUtils.clamp(gridWidth, 1, 65535);
131        this.gridHeight = MathUtils.clamp(gridHeight, 1, 65535);
132        backgrounds = new float[this.gridWidth][this.gridHeight];
133        layers = new ArrayList<>(4);
134        if(font.initialized())
135            this.font = font;
136        else
137            this.font = font.width(cellWidth).height(cellHeight).initBySize();
138        layers.add(new SparseTextMap(gridWidth * gridHeight >> 2));
139        mapping = new IntIntMap(4);
140        mapping.put(0, 0);
141        glyphs = new ArrayList<>(16);
142        scc = DefaultResources.getSCC();
143        setBounds(xOffset, yOffset,
144                this.font.actualCellWidth * this.gridWidth, this.font.actualCellHeight * this.gridHeight);
145    }
146
147    /**
148     * Gets the number of layers currently used in this SparseLayers; if a layer is put into that is equal to or greater
149     * than this layer count, then a new layer is created to hold the latest placement. It is recommended that you
150     * assign to the layer equal to the result of this method if you want to add a layer. You can add to layers that are
151     * higher than this result, and can technically do so out of order, but this just gets confusing because their
152     * ordering won't be related to the numbers of the layers.
153     * @return the current number of layers in this SparseLayers
154     */
155    public int getLayerCount()
156    {
157        return layers.size();
158    }
159
160    /**
161     * Gets the layer, as a SparseTextMap, that is associated with the given int. If none is associated, this returns
162     * null. The associating number does not necessarily have to be less than the number of layers in this SparseLayers.
163     * Returns a direct reference to the SparseTextMap layer.
164     * @param layer the int that is associated with a layer, usually the int used to add to that layer.
165     * @return the SparseTextMap layer associated with the given int
166     */
167    public SparseTextMap getLayer(int layer)
168    {
169        layer = mapping.get(layer, -1);
170        if(layer < 0)
171            return null;
172        else
173            return layers.get(layer);
174    }
175
176    /**
177     * Sets the SparseTextMap associated with the given layerNumber to the given contents. If layerNumber is too high
178     * to set an existing layer, this will add contents as a new layer on top of the others.
179     * @param layerNumber must be 0 or greater
180     * @param contents a SparseTextMap, possibly obtained with {@link #getLayer(int)}
181     */
182    public void setLayer(int layerNumber, SparseTextMap contents)
183    {
184        if(layerNumber < 0)
185            return;
186        layerNumber = mapping.get(layerNumber, layerNumber);
187        if(layerNumber >= layers.size())
188        {
189            mapping.put(layerNumber, layers.size());
190            layers.add(contents);
191        }
192        else
193        {
194            layers.set(layerNumber, contents);
195        }
196
197    }
198
199    /**
200     * Finds the layer number associated with layerMap, or -1 if the given SparseTextMap is not in this SparseLayers.
201     * @param layerMap a SparseTextMap that was likely returned by {@link #addLayer()}
202     * @return the int the layer is associated with, or -1 if it is not in this SparseLayers.
203     */
204    public int findLayer(SparseTextMap layerMap)
205    {
206        int a = layers.indexOf(layerMap);
207        if(a < 0)
208            return -1;
209        return mapping.findKey(a, -1);
210    }
211
212    /**
213     * Adds a layer as a SparseTextMap to this SparseLayers and returns the one just added. The layer will generally be
214     * associated with a layer number equal to the number of layers currently present, and will be rendered over the
215     * existing layers. It might not be associated with the same number if layers were added out-of-order or with skips
216     * between associations. If you want to add a layer at some alternate layer number (which can be confusing but may
217     * be needed for some reason), there is another overload that allows you to specify the association number.
218     * If you may have added some layers out of order or skipped some numbers, you can use
219     * {@link #findLayer(SparseTextMap)} on the returned SparseTextMap to get its association number.
220     * @return a SparseTextMap that was just added; null if something went wrong
221     */
222    public SparseTextMap addLayer()
223    {
224        int association = layers.size();
225        while (mapping.containsKey(association)) {
226            ++association;
227        }
228        return addLayer(association);
229    }
230    /**
231     * Adds a layer as a SparseTextMap to this SparseLayers and returns the one just added, or returns an existing layer
232     * if one is already associated with the given number. The layer can be greater than the number of layers currently
233     * present, which will add a layer to be rendered over the existing layers, but the number that refers to that layer
234     * will not change. It is recommended that to add a layer, you only add at the value equal to
235     * {@link #getLayerCount()}, which will maintain the order and layer numbers in a sane way, and this is the behavior
236     * of the addLayer overload that does not take a parameter.
237     * @param association the number to associate the layer with; should usually be between 0 and {@link #getLayerCount()} inclusive
238     * @return a SparseTextMap that either was just added or was already associated with the given layer number; null if something went wrong
239     */
240    public SparseTextMap addLayer(int association)
241    {
242        if(association < 0)
243            return null;
244        association = mapping.get(association, association);
245        if(association >= layers.size())
246        {
247            mapping.put(association, layers.size());
248            SparseTextMap stm = new SparseTextMap(gridWidth * gridHeight >> 4);
249            layers.add(stm);
250            return stm;
251        }
252        else
253        {
254            return layers.get(association);
255        }
256    }
257
258    /**
259     * Puts the character {@code c} at {@code (x, y)} with the default foreground. Does not change the background.
260     *
261     * @param x the x position to place the char at
262     * @param y the y position to place the char at
263     * @param c the char to place, using the default foreground color
264     */
265    @Override
266    public void put(int x, int y, char c) {
267        put(x, y, c, defaultPackedForeground, 0f);
268    }
269
270    /**
271     * Puts the ICellVisible cell at the position x,y; does not change the background. If {@code cell.getPackedColor()}
272     * is 0f, then this does not change the foreground contents either. Does not filter the given colors.
273     * @param x the x position to place the char at
274     * @param y the y position to place the char at
275     * @param cell an ICellVisible, which is often implemented outside of SquidLib; this uses its char and color.
276     */
277    public void put(int x, int y, ICellVisible cell) {
278        put(x, y, cell.getSymbol(), cell.getPackedColor(), 0f);
279    }
280
281    /**
282     * Puts the given string horizontally with the first character at the given
283     * offset. Uses the given color for the text and does not change the background
284     * color at all.
285     * <p>
286     * Does not word wrap. Characters that are not renderable (due to being at
287     * negative offsets or offsets greater than the grid size) will not be shown
288     * but will not cause any malfunctions.
289     *
290     * @param xOffset    the x coordinate of the first character
291     * @param yOffset    the y coordinate of the first character
292     * @param string     the characters to be displayed
293     * @param foreground the color to use for the text
294     */
295    @Override
296    public void put(int xOffset, int yOffset, String string, Color foreground) {
297        put(xOffset, yOffset, string, foreground, null);
298    }
299
300    /**
301     * Puts the given string horizontally with the first character at the given
302     * offset, using the colors that {@code cs} provides. Does not change the
303     * background colors. Does filter the colors from cs if the color center this
304     * uses is set up to filter colors.
305     * <p>
306     * Does not word wrap. Characters that are not renderable (due to being at
307     * negative offsets or offsets greater than the grid size) will not be shown
308     * but will not cause any malfunctions.
309     *
310     * @param xOffset the x coordinate of the first character
311     * @param yOffset the y coordinate of the first character
312     * @param cs an {@link IColoredString} with potentially multiple colors
313     */
314    @Override
315    public void put(int xOffset, int yOffset, IColoredString<? extends Color> cs) {
316        int x = xOffset;
317        for (IColoredString.Bucket<? extends Color> fragment : cs) {
318            final String s = fragment.getText();
319            final Color color = fragment.getColor();
320            put(x, yOffset, s, color == null ? getDefaultForegroundColor() : scc.filter(color), null);
321            x += s.length();
322        }
323    }
324
325    /**
326     * Puts the character {@code c} at {@code (x, y)} with some {@code color}.
327     * Does not change the background colors.
328     *
329     * @param x the x position to place the char at
330     * @param y the y position to place the char at
331     * @param c the char to place
332     * @param color the color to use for c; if null or fully transparent, nothing will change in the foreground
333     */
334    @Override
335    public void put(int x, int y, char c, Color color) {
336        put(x, y, c, color, null);
337    }
338
339    /**
340     * Sets the background colors to match the given Color 2D array. If the colors argument is null, does nothing.
341     * This will filter each Color in colors if the color center this uses has a filter.
342     * @param colors the background colors for the given chars
343     */
344    public void put(Color[][] colors)
345    {
346        put(null, colors);
347    }
348
349    /**
350     * Sets the background colors to match the given 2D array of colors as packed floats. If the colors argument is
351     * null, does nothing. This will not filter the passed colors at all.
352     * @param colors the background colors to use for this SparseLayers
353     */
354    public void put(float[][] colors)
355    {
356        put(null, colors);
357    }
358    /**
359     * Places the given char 2D array, if-non-null, in the default foreground color starting at x=0, y=0.
360     * @param chars a 2D char array to place without affecting background colors; these chars will use the default foreground color
361     */
362    public void putChars(char[][] chars) {
363        if(chars == null)
364            return;
365        SparseTextMap layer = layers.get(0);
366        for (int i = 0; i < chars.length; i++) {
367            if (chars[i] != null) {
368                for (int j = 0; j < chars[i].length; j++) {
369                    layer.place(i, j, chars[i][j], defaultPackedForeground);
370                }
371            }
372        }
373    }
374    /**
375     * Places the given char 2D array, if-non-null, in the foreground starting at x=0, y=0. Each char will be drawn in 
376     * the corresponding foreground color from foregrounds.
377     * @param chars a 2D char array to place without affecting background colors
378     * @param foregrounds a 2D float array of encoded colors as produced by {@link Color#toFloatBits()}; applies to char foregrounds
379     */
380    public void putChars(char[][] chars, float[][] foregrounds) {
381        if(chars == null)
382            return;
383        if(foregrounds == null)
384        {
385            putChars(chars);
386            return;
387        }
388        SparseTextMap layer = layers.get(0);
389        for (int i = 0; i < chars.length; i++) {
390            if (chars[i] != null) {
391                for (int j = 0; j < chars[i].length; j++) {
392                    layer.place(i, j, chars[i][j], foregrounds[i][j]);
393                }
394            }
395        }
396    }
397    /**
398     * Places the given char 2D array, if-non-null, in the foreground starting at x=0, y=0. Each char will be drawn in 
399     * the corresponding foreground color from foregrounds.
400     * @param chars a 2D char array to place without affecting background colors
401     * @param foregrounds a 2D array of libGDX colors (any may also be an SColor or other subclass); applies to char foregrounds
402     */
403    public void putChars(char[][] chars, Color[][] foregrounds) {
404        if(chars == null)
405            return;
406        if(foregrounds == null)
407        {
408            putChars(chars);
409            return;
410        }
411        SparseTextMap layer = layers.get(0);
412        for (int i = 0; i < chars.length; i++) {
413            if (chars[i] != null) {
414                for (int j = 0; j < chars[i].length; j++) {
415                    layer.place(i, j, chars[i][j], scc.filter(foregrounds[i][j]).toFloatBits());
416                }
417            }
418        }
419    }
420    /**
421     * Places the given char 2D array, if-non-null, in the default foreground color starting at x=0, y=0, while also
422     * setting the background colors to match the given Color 2D array. If the colors argument is null, does nothing. If
423     * the chars argument is null, only affects the background colors. This will filter each Color in colors if the
424     * color center this uses has a filter.
425     * @param chars Can be {@code null}, indicating that only colors must be put.
426     * @param colors the background colors for the given chars
427     */
428    @Override
429    public void put(char[][] chars, Color[][] colors) {
430        if(chars == null)
431        {
432            if(colors != null)
433            {
434                for (int i = 0; i < colors.length; i++) {
435                    if (colors[i] == null)
436                        continue;
437                    for (int j = 0; j < colors[i].length; j++) {
438                        backgrounds[i][j] = (colors[i][j] == null) ? 0f : scc.filter(colors[i][j]).toFloatBits();
439                    }
440                }
441            }
442        }
443        else
444        {
445            SparseTextMap layer = layers.get(0);
446            if(colors == null)
447            {
448                for (int i = 0; i < chars.length; i++) {
449                    if(chars[i] == null)
450                        continue;
451                    for (int j = 0; j < chars[i].length; j++) {
452                        layer.place(i, j, chars[i][j], defaultPackedForeground);
453                    }
454                }
455            }
456            else
457            {
458                for (int i = 0; i < chars.length && i < colors.length; i++) {
459                    if(colors[i] == null || chars[i] == null)
460                        continue;
461                    for (int j = 0; j < chars[i].length && j < colors[i].length; j++) {
462                        if(colors[i][j] == null)
463                            layer.place(i, j, chars[i][j], defaultPackedForeground);
464                        else
465                            put(i, j, chars[i][j], defaultPackedForeground, scc.filter(colors[i][j]).toFloatBits());
466                    }
467                }
468            }
469        }
470    }
471    /**
472     * Places the given char 2D array, if-non-null, in the default foreground color starting at x=0, y=0, while also
473     * setting the background colors to match the given 2D array of colors as packed floats. If the colors argument is
474     * null, does nothing. If the chars argument is null, only affects the background colors. This will not filter the
475     * passed colors at all.
476     * @param chars Can be {@code null}, indicating that only colors must be put.
477     * @param colors the background colors for the given chars
478     */
479    public void put(char[][] chars, float[][] colors) {
480        if(chars == null)
481        {
482            if(colors != null) {
483                for (int i = 0; i < colors.length; i++) {
484                    if (colors[i] == null)
485                        continue;
486                    System.arraycopy(colors[i], 0, backgrounds[i], 0, colors[i].length);
487                }
488            }
489        }
490        else
491        {
492            if(colors == null)
493            {
494                for (int i = 0; i < chars.length; i++) {
495                    if(chars[i] == null)
496                        continue;
497                    for (int j = 0; j < chars[i].length; j++) {
498                        put(i, j, chars[i][j], defaultPackedForeground, 0f);
499                    }
500                }
501            }
502            else
503            {
504                for (int i = 0; i < chars.length && i < colors.length; i++) {
505                    if(colors[i] == null || chars[i] == null)
506                        continue;
507                    for (int j = 0; j < chars[i].length && j < colors[i].length; j++) {
508                        put(i, j, chars[i][j], defaultPackedForeground, colors[i][j]);
509                    }
510                }
511            }
512        }
513    }
514    /**
515     * Places the given char 2D array, if-non-null, with the given foreground colors in the first Color 2D array,
516     * starting at x=0, y=0, while also setting the background colors to match the second Color 2D array. If the
517     * bgColors argument is null, only affects foreground chars and colors. If the chars argument or the fgColors
518     * argument is null, only affects the background colors. Any positions where a Color in fgColors is null will not
519     * have a char placed (this can be used to restrict what is placed). This will filter each Color in the background
520     * and foreground if the color center this uses has a filter.
521     * @param chars Can be {@code null}, indicating that only colors must be put.
522     * @param fgColors the foreground Colors for the given chars
523     * @param bgColors the background Colors for the given chars
524     */
525    public void put(char[][] chars, Color[][] fgColors, Color[][] bgColors) {
526        if(chars == null || fgColors == null)
527        {
528            if(bgColors != null)
529            {
530                for (int i = 0; i < bgColors.length; i++) {
531                    if (bgColors[i] == null)
532                        continue;
533                    for (int j = 0; j < bgColors[i].length; j++) {
534                        backgrounds[i][j] = (bgColors[i][j] == null) ? 0f : scc.filter(bgColors[i][j]).toFloatBits();
535                    }
536                }
537            }
538        }
539        else
540        {
541            if(bgColors == null)
542            {
543                for (int i = 0; i < chars.length && i < fgColors.length; i++) {
544                    if(chars[i] == null || fgColors[i] == null)
545                        continue;
546                    for (int j = 0; j < chars[i].length && j < fgColors[i].length; j++) {
547                        put(i, j, chars[i][j], fgColors[i][j], null);
548                    }
549                }
550            }
551            else
552            {
553                for (int i = 0; i < chars.length && i < bgColors.length && i < fgColors.length; i++) {
554                    if(bgColors[i] == null || chars[i] == null || fgColors[i] == null)
555                        continue;
556                    for (int j = 0; j < chars[i].length && j < bgColors[i].length && j < fgColors[i].length; j++) {                         
557                        put(i, j, chars[i][j], fgColors[i][j], bgColors[i][j]);
558                    }
559                }
560            }
561        }
562    }
563    /**
564     * Places the given char 2D array, if-non-null, with the given foreground colors in the first float 2D array,
565     * starting at x=0, y=0, while also setting the background colors to match the second float 2D array. If the
566     * bgColors argument is null, only affects foreground chars and colors. If the chars argument or the fgColors
567     * argument is null, only affects the background colors. Any positions where a float in fgColors is 0 will not
568     * have a char placed (this can be used to restrict what is placed). This will not filter any colors.
569     * @param chars Can be {@code null}, indicating that only colors must be put.
570     * @param fgColors the foreground colors for the given chars, as packed floats
571     * @param bgColors the background colors for the given chars, as packed floats
572     */
573    public void put(char[][] chars, float[][] fgColors, float[][] bgColors) {
574        if(chars == null || fgColors == null)
575        {
576            if(bgColors != null)
577            {
578                for (int i = 0; i < bgColors.length; i++) {
579                    if (bgColors[i] == null)
580                        continue;
581                    System.arraycopy(bgColors[i], 0, backgrounds[i], 0, bgColors[i].length);
582                }
583            }
584        }
585        else
586        {
587            if(bgColors == null)
588            {
589                for (int i = 0; i < chars.length && i < fgColors.length; i++) {
590                    if(chars[i] == null || fgColors[i] == null)
591                        continue;
592                    for (int j = 0; j < chars[i].length && j < fgColors[i].length; j++) {
593                        put(i, j, chars[i][j], fgColors[i][j], 0f);
594                    }
595                }
596            }
597            else
598            {
599                for (int i = 0; i < chars.length && i < bgColors.length && i < fgColors.length; i++) {
600                    if(bgColors[i] == null || chars[i] == null || fgColors[i] == null)
601                        continue;
602                    for (int j = 0; j < chars[i].length && j < bgColors[i].length && j < fgColors[i].length; j++) {
603                        put(i, j, chars[i][j], fgColors[i][j], bgColors[i][j]);
604                    }
605                }
606            }
607        }
608    }
609
610    /**
611     * @return The number of cells that this panel spans, horizontally.
612     */
613    @Override
614    public int gridWidth() {
615        return gridWidth;
616    }
617
618    /**
619     * @return The number of cells that this panel spans, vertically.
620     */
621    @Override
622    public int gridHeight() {
623        return gridHeight;
624    }
625
626    /**
627     * The same as {@link #gridWidth()}; here for compatibility with SquidPanel.
628     * @return The number of cells that this panel spans, horizontally.
629     */
630    public int getGridWidth() {
631        return gridWidth;
632    }
633
634    /**
635     * The same as {@link #gridHeight()}; here for compatibility with SquidPanel.
636     * @return The number of cells that this panel spans, vertically.
637     */
638    public int getGridHeight() {
639        return gridHeight;
640    }
641
642    /**
643     * @return The width of a cell, in number of pixels.
644     */
645    @Override
646    public int cellWidth() {
647        return Math.round(font.actualCellWidth);
648    }
649
650    /**
651     * @return The height of a cell, in number of pixels.
652     */
653    @Override
654    public int cellHeight() {
655        return Math.round(font.actualCellHeight);
656    }
657
658    /**
659     * Changes the width and height of chars the font renders, inside the same cell size.
660     * Primarily useful if the font has issues and doesn't line up visually; some older versions of .fnt files
661     * distributed with SquidLib had this type of problem (and some may still).
662     * Provides compatibility with SquidPanel code.
663     * @param wide width in approximate pixels
664     * @param high height in approximate pixels
665     * @return this for chaining.
666     */
667    public SparseLayers setTextSize(float wide, float high)
668    {
669        font.tweakWidth(wide).tweakHeight(high).initBySize();
670        return this;
671    }
672    /**
673     * Sets the default foreground color.
674     * This Color can be null, which will
675     * usually not be rendered unless a different color is specified.
676     * @param color a libGDX Color object or an extension of Color, such as SColor
677     */
678    @Override
679    public void setDefaultForeground(Color color) {
680        defaultForeground = color;
681        if(color != null)
682            defaultPackedForeground = color.toFloatBits();
683        else
684            defaultPackedForeground = 0f;
685    }
686    /**
687     * @return The default foreground color (if none was set with
688     * {@link #setDefaultForeground(Color)}), or the last color set with
689     * {@link #setDefaultForeground(Color)}. This Color can be null which
690     * will usually not be rendered unless a different color is specified.
691     */
692    @Override
693    public Color getDefaultForegroundColor() {
694        return defaultForeground;
695    }
696
697    /**
698     * Sets the default background color.
699     * This Color can be null, which will
700     * usually not be rendered unless a different color is specified.
701     * @param color a libGDX Color object or an extension of Color, such as SColor
702     */
703    public void setDefaultBackground(Color color) {
704        defaultBackground = color;
705        if(color != null)
706            defaultPackedBackground = color.toFloatBits();
707        else
708            defaultPackedBackground = 0f;
709    }
710    /**
711     * @return The default background color (if none was set with
712     * {@link #setDefaultBackground(Color)}), or the last color set
713     * with {@link #setDefaultBackground(Color)}. This can be null,
714     * which will usually not be rendered unless a different color
715     * is specified.
716     */
717    public Color getDefaultBackgroundColor() {
718        return defaultBackground;
719    }
720
721    /**
722     * Method to change the backing {@link IColorCenter}.
723     * Note that the IColorCenter is not used to filter floats that encode colors, so passing the result of
724     * {@link Color#toFloatBits()} can be used to bypass the filtering if you want a color to be used exactly.
725     * @param icc an IColorCenter that can cache and possibly filter {@link Color} objects
726     */
727    protected void setColorCenter(IColorCenter<Color> icc) {
728        scc = icc;
729    }
730
731    /**
732     * Gets the IColorCenter this can use to filter Color objects; this is usually a {@link SquidColorCenter}.
733     * Note that the IColorCenter is not used to filter floats that encode colors, so passing the result of
734     * {@link Color#toFloatBits()} can be used to bypass the filtering if you want a color to be used exactly.
735     * @return the IColorCenter this uses
736     */
737    public IColorCenter<Color> getColorCenter() {
738        return scc;
739    }
740
741    /**
742     * Puts the char c at the position x,y with the given foreground and background colors. If foreground is null or is
743     * fully transparent (all channels 0), then this does not change the foreground contents.
744     * If background is null or is fully transparent, this does not change the background. Uses the color center to
745     * potentially filter the given colors; this can be changed with {@link #setColorCenter(IColorCenter)}.
746     * @param x the x position to place the char at
747     * @param y the y position to place the char at
748     * @param c the char to place
749     * @param foreground the color to use for c; if null or fully transparent, nothing will change in the foreground
750     * @param background the color to use for the cell; if null or fully transparent, nothing will change in the background
751     */
752    public void put(int x, int y, char c, Color foreground, Color background)
753    {
754        put(x, y, c, foreground == null ? 0f : scc.filter(foreground).toFloatBits(),
755                background == null ? 0f : scc.filter(background).toFloatBits());
756    }
757    /**
758     * Puts the ICellVisible cell at the position x,y with the given background color as an encoded float, such as
759     * those produced by {@link Color#toFloatBits()}. If {@code cell.getPackedColor()} is 0f, then this does not
760     * change the foreground contents. If background is 0f, this does not change the background. Does not filter the
761     * given colors.
762     * @param x the x position to place the char at
763     * @param y the y position to place the char at
764     * @param cell an ICellVisible, which is often implemented outside of SquidLib; this uses its char and color.
765     * @param background the color to use for the cell; if null or fully transparent, nothing will change in the background
766     */
767    public void put(int x, int y, ICellVisible cell, float background)
768    {
769        put(x, y, cell.getSymbol(), cell.getPackedColor(), background);
770    }
771    /**
772     * Puts the char c at the position x,y with the given foreground and background colors as encoded floats, such as
773     * those produced by {@link Color#toFloatBits()}. If foreground is 0f, then this does not
774     * change the foreground contents. If background is 0f, this does not change the background. Does not filter the
775     * given colors.
776     * @param x the x position to place the char at
777     * @param y the y position to place the char at
778     * @param c the char to place
779     * @param foreground the color to use for c; if 0f, nothing will change in the foreground
780     * @param background the color to use for the cell; if null or fully transparent, nothing will change in the background
781     */
782    public void put(int x, int y, char c, float foreground, float background)
783    {
784        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
785            return;
786        if(background != 0f)
787            backgrounds[x][y] = background;
788        if(foreground != 0f)
789            layers.get(0).place(x, y, c, foreground);
790    }
791
792
793    /**
794     * Puts the char c at the position x,y with the given foreground color as an encoded float (the kind produced by
795     * {@link Color#toFloatBits()}). If foreground is 0f, then this does nothing. Does not filter the given color.
796     * @param x the x position to place the char at
797     * @param y the y position to place the char at
798     * @param c the char to place
799     * @param foreground the color to use for c; if 0f, this call does nothing
800     */
801    public void put(int x, int y, char c, float foreground)
802    {
803        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight || foreground == 0f)
804            return;
805        layers.get(0).place(x, y, c, foreground);
806    }
807    /**
808     * Puts the char c at the position x,y in the requested layer with the given foreground and background colors. If
809     * foreground is null or is fully transparent (all channels 0), then this does not change
810     * the foreground contents. If background is null or is fully transparent, this does not change the background. Uses
811     * the color center to potentially filter the given colors; this can be changed with
812     * {@link #setColorCenter(IColorCenter)}. The layer can be greater than the number of layers currently present,
813     * which will add a layer to be rendered over the existing layers, but the number that refers to that layer will not
814     * change. It is recommended that to add a layer, you only add at the value equal to {@link #getLayerCount()}, which
815     * will maintain the order and layer numbers in a sane way.
816     * @param x the x position to place the char at
817     * @param y the y position to place the char at
818     * @param c the char to place
819     * @param foreground the color to use for c; if null or fully transparent, nothing will change in the foreground
820     * @param background the color to use for the cell; if null or fully transparent, nothing will change in the background
821     * @param layer the layer to place the colorful char into; should usually be between 0 and {@link #getLayerCount()} inclusive
822     */
823    public void put(int x, int y, char c, Color foreground, Color background, int layer)
824    {
825        put(x, y, c, foreground == null ? 0f : scc.filter(foreground).toFloatBits(),
826                background == null ? 0f : scc.filter(background).toFloatBits(), layer);
827
828    }
829    /**
830     * Puts the char c at the position x,y in the requested layer with the given foreground and background colors as
831     * encoded floats, such as those produced by {@link Color#toFloatBits()}. If foreground is 0f, then this does not
832     * change the foreground contents. If background is 0f, this does not change the
833     * background. Does not filter the given colors. The layer can be greater than the number of layers currently
834     * present, which will add a layer to be rendered over the existing layers, but the number that refers to that layer
835     * will not change. It is recommended that to add a layer, you only add at the value equal to
836     * {@link #getLayerCount()}, which will maintain the order and layer numbers in a sane way.
837     * @param x the x position to place the char at
838     * @param y the y position to place the char at
839     * @param c the char to place
840     * @param foreground the color to use for c; if 0f, nothing will change in the foreground
841     * @param background the color to use for the cell; if null or fully transparent, nothing will change in the background
842     * @param layer the layer to place the colorful char into; should usually be between 0 and {@link #getLayerCount()} inclusive
843     */
844    public void put(int x, int y, char c, float foreground, float background, int layer)
845    {
846        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight || layer < 0)
847            return;
848        if(background != 0f)
849            backgrounds[x][y] = background;
850        layer = mapping.get(layer, layer);
851        if(foreground != 0f)
852        {
853            if(layer >= layers.size())
854            {
855                mapping.put(layer, layers.size());
856                SparseTextMap stm = new SparseTextMap(gridWidth * gridHeight >> 4);
857                stm.place(x, y, c, foreground);
858                layers.add(stm);
859            }
860            else
861            {
862                layers.get(layer).place(x, y, c, foreground);
863            }
864        }
865    }
866    /**
867     * Puts text at the position x,y with the given foreground and background colors. If foreground is null or is
868     * fully transparent (all channels 0), then this does not change the foreground contents.
869     * If background is null or is fully transparent, this does not change the background. Uses the color center to
870     * potentially filter the given colors; this can be changed with {@link #setColorCenter(IColorCenter)}.
871     * @param x the x position to place the String at
872     * @param y the y position to place the String at
873     * @param text the String to place
874     * @param foreground the color to use for text; if null or fully transparent, nothing will change in the foreground
875     * @param background the color to use for the cells; if null or fully transparent, nothing will change in the background
876     */
877    public void put(int x, int y, String text, Color foreground, Color background) {
878        put(x, y, text, foreground == null ? 0f : scc.filter(foreground).toFloatBits(),
879                background == null ? 0f : scc.filter(background).toFloatBits());
880    }
881    /**
882     * Puts text at the position x,y with the given foreground and background colors as encoded floats, such as
883     * those produced by {@link Color#toFloatBits()}. If foreground is 0f, then this does not change the foreground
884     * contents. If background is 0f, this does not change the background. Does not filter the given colors.
885     * @param x the x position to place the String at
886     * @param y the y position to place the String at
887     * @param text the String to place
888     * @param foreground the color to use for text; if 0f, nothing will change in the foreground
889     * @param background the color to use for the cells; if null or fully transparent, nothing will change in the background
890     */
891    public void put(int x, int y, String text, float foreground, float background)
892    {
893        if(text != null) {
894            int len = Math.min(text.length(), gridWidth - x);
895            for (int i = 0; i < len; i++) {
896                put(x + i, y, text.charAt(i), foreground, background);
897            }
898        }
899    }
900    /**
901     * Puts text at the position x,y in the requested layer with the given foreground and background colors. If
902     * foreground is null or is fully transparent (all channels 0), then this does not change the foreground contents.
903     * If background is null or is fully transparent, this does not change the background. Uses the color center to
904     * potentially filter the given colors; this can be changed with {@link #setColorCenter(IColorCenter)}. The layer
905     * can be greater than the number of layers currently present, which will add a layer to be rendered over the
906     * existing layers, but the number that refers to that layer will not change. It is recommended that to add a layer,
907     * you only add at the value equal to {@link #getLayerCount()}, which will maintain the order and layer numbers in a
908     * sane way.
909     * @param x the x position to place the String at
910     * @param y the y position to place the String at
911     * @param text the String to place
912     * @param foreground the color to use for text; if null or fully transparent, nothing will change in the foreground
913     * @param background the color to use for the cells; if null or fully transparent, nothing will change in the background
914     * @param layer the layer to place the colorful char into; should usually be between 0 and {@link #getLayerCount()} inclusive
915     */
916    public void put(int x, int y, String text, Color foreground, Color background, int layer) {
917        put(x, y, text, foreground == null ? 0f : scc.filter(foreground).toFloatBits(),
918                background == null ? 0f : scc.filter(background).toFloatBits(), layer);
919    }
920    /**
921     * Puts text at the position x,y in the requested layer with the given foreground and background colors as encoded
922     * floats, such as those produced by {@link Color#toFloatBits()}. If foreground is 0f, then this does not change the
923     * foreground contents. If background is 0f, this does not change the background. Does not filter the given colors.
924     * The layer can be greater than the number of layers currently present, which will add a layer to be rendered over
925     * the existing layers, but the number that refers to that layer will not change. It is recommended that to add a
926     * layer, you only add at the value equal to {@link #getLayerCount()}, which will maintain the order and layer
927     * numbers in a sane way.
928     * @param x the x position to place the String at
929     * @param y the y position to place the String at
930     * @param text the String to place
931     * @param foreground the color to use for text; if 0f, nothing will change in the foreground
932     * @param background the color to use for the cells; if null or fully transparent, nothing will change in the background
933     * @param layer the layer to place the colorful char into; should usually be between 0 and {@link #getLayerCount()} inclusive
934     */
935    public void put(int x, int y, String text, float foreground, float background, int layer) {
936        if (x < 0 || x >= gridWidth || y < 0 || y >= gridHeight || layer < 0)
937            return;
938        int len = Math.min(text.length(), gridWidth - x);
939        if(background != 0f) {
940            for (int i = 0; i < len; i++) {
941                backgrounds[x + i][y] = background;
942            }
943        }
944        if (foreground != 0f)
945        {
946            layer = mapping.get(layer, layer);
947            if (layer >= layers.size()) {
948                mapping.put(layer, layers.size());
949                SparseTextMap stm = new SparseTextMap(gridWidth * gridHeight >> 4);
950                for (int i = 0; i < len; i++) {
951                    stm.place(x + i, y, text.charAt(i), foreground);
952                }
953                layers.add(stm);
954            } else {
955                SparseTextMap stm = layers.get(layer);
956                for (int i = 0; i < len; i++) {
957                    stm.place(x + i, y, text.charAt(i), foreground);
958                }
959            }
960        }
961    }
962
963    /**
964     * Changes the background at position x,y to the given Color. If the color is null, then this will make the
965     * background fully transparent at the specified position.
966     * @param x where to change the background color, x-coordinate
967     * @param y where to change the background color, y-coordinate
968     * @param color the Color to change to; if null will be considered fully transparent
969     */
970    public void put(int x, int y, Color color)
971    {
972        put(x, y, color == null ? 0f : scc.filter(color).toFloatBits());
973    }
974    /**
975     * Changes the background at position x,y to the given color as an encoded float. The color can be transparent,
976     * which will show through to whatever is behind this SparseLayers, or the color the screen was last cleared with.
977     * Unlike other methods in this class, a float equal to 0f will be used instead of being used to skip over a cell,
978     * and will change the background at the given position to fully transparent.
979     * @param x where to change the background color, x-coordinate
980     * @param y where to change the background color, y-coordinate
981     * @param color the color, as an encoded float, to change to; may be transparent, and considers 0f a valid color
982     */
983    public void put(int x, int y, float color)
984    {
985        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
986            return;
987        backgrounds[x][y] = color;
988    }
989    /**
990     * A convenience method that handles blending the background color with a specified light color, by a specific
991     * amount, without putting a char on the screen; as a whole this affects one x,y position.
992     * @param x the x position, in cells
993     * @param y the y position, in cells
994     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
995     * @param lightColor the color to mix with the background, as a libGDX Color
996     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
997     *                    to between 0 and 1, and negative values can be given to favor background more
998     */
999    public void putWithLight(int x, int y, Color background, Color lightColor, float lightAmount) {
1000        putWithLight(x, y, '\0', 0f, background == null ? 0f :  background.toFloatBits(),
1001                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1002    }
1003    /**
1004     * A convenience method that handles blending the background color with a specified light color, by a specific
1005     * amount, without putting a char on the screen; as a whole this affects one x,y position.
1006     * @param x the x position, in cells
1007     * @param y the y position, in cells
1008     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1009     * @param lightColor the color to mix with the background, as a libGDX Color
1010     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1011     *                    to between 0 and 1, and negative values can be given to favor background more
1012     */
1013    public void putWithLight(int x, int y, Color background, Color lightColor, double lightAmount) {
1014        putWithLight(x, y, '\0', 0f, background == null ? 0f :  background.toFloatBits(),
1015                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1016    }
1017
1018    /**
1019     * A convenience method that handles blending the background color with a specified light color, by a specific
1020     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1021     * @param x the x position, in cells
1022     * @param y the y position, in cells
1023     * @param c the char to put in the foreground
1024     * @param foreground the color to use for the foreground, as a libGDX Color; if null, this won't place a foreground char
1025     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1026     * @param lightColor the color to mix with the background, as a libGDX Color
1027     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1028     *                    to between 0 and 1, and negative values can be given to favor background more
1029     */
1030    public void putWithLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount) {
1031        putWithLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1032                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1033    }
1034
1035    /**
1036     * A convenience method that handles blending the background color with a specified light color, by a specific
1037     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1038     * @param x the x position, in cells
1039     * @param y the y position, in cells
1040     * @param c the char to put in the foreground
1041     * @param foreground the color to use for the foreground, as a libGDX Color; if null, this won't place a foreground char
1042     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1043     * @param lightColor the color to mix with the background, as a libGDX Color
1044     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1045     *                    to between 0 and 1, and negative values can be given to favor background more
1046     */
1047    public void putWithLight(int x, int y, char c, Color foreground, Color background, Color lightColor, double lightAmount) {
1048        putWithLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1049                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1050    }
1051
1052    /**
1053     * A convenience method that handles blending the background color with a specified light color, by a specific
1054     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1055     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1056     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1057     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1058     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1059     * @param x the x position, in cells
1060     * @param y the y position, in cells
1061     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1062     * @param lightColor the color to mix with the background, as a packed float color
1063     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1064     *                    to between 0 and 1, and negative values can be given to favor background more
1065     */
1066    public void putWithConsistentLight(int x, int y, float background, float lightColor, float lightAmount) {
1067        putWithConsistentLight(x, y, background, lightColor, lightAmount, 0.0015f);
1068    }
1069    /**
1070     * A convenience method that handles blending the background color with a specified light color, by a specific
1071     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1072     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1073     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1074     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1075     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1076     * @param x the x position, in cells
1077     * @param y the y position, in cells
1078     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1079     * @param lightColor the color to mix with the background, as a packed float color
1080     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1081     *                    to between 0 and 1, and negative values can be given to favor background more
1082     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1083     */
1084    public void putWithConsistentLight(int x, int y, float background, float lightColor, float lightAmount, float flickerSpeed) {
1085        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed;
1086        final long time0 = Noise.longFloor(time);
1087        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1088        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount));
1089        putWithLight(x, y, '\0', 0f, background, lightColor, lightAmount);
1090    }
1091
1092    /**
1093     * Gets a modifier that should be given to {@link #calculateConsistentLightAmount(float, float)}; this only needs to
1094     * be called at most once per frame. It uses a flickerSpeed of 0.0015f, if you want to compare it to
1095     * {@link #calculateConsistentLightModifier(float)}.
1096     * @return a modifier to pass to {@link #calculateConsistentLightAmount(float, float)}
1097     */
1098    public float calculateConsistentLightModifier()
1099    {
1100        return calculateConsistentLightModifier(0.0015f);
1101    }
1102    /**
1103     * Gets a modifier that should be given to {@link #calculateConsistentLightAmount(float, float)}; this only needs to
1104     * be called at most once per frame. It uses the given flickerSpeed; a common example value is 0.0015f.
1105     * @return a modifier to pass to {@link #calculateConsistentLightAmount(float, float)}
1106     */
1107    public float calculateConsistentLightModifier(float flickerSpeed)
1108    {
1109        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed;
1110        final long time0 = Noise.longFloor(time);
1111        return Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1112    }
1113
1114    /**
1115     * Allows you to reproduce the effects of {@link #putWithConsistentLight(int, int, char, float, float, float, double, float)}
1116     * without recalculating the modifier many times per frame. You should typically calculate the modifier once per
1117     * frame using {@link #calculateConsistentLightModifier()}. This method should usually be called once per cell that
1118     * needs consistent lighting that flickers the same in all directions.
1119     * @param lightAmount typically from FOV or some other way of generating a float with lower values further from a position (such as a light)
1120     * @param modifier calculated previously by {@link #calculateConsistentLightModifier()}, which should be done per-frame
1121     * @return the adjusted light amount to mimic the appearance of {@link #putWithConsistentLight(int, int, char, float, float, float, double, float)}
1122     */
1123    public float calculateConsistentLightAmount(float lightAmount, float modifier)
1124    {
1125        return lightAmount <= 0f ? -1024f : Math.max(
1126                lightAmount * 0.15f,
1127                Math.min(lightAmount - NumberTools.swayTight(modifier * 3.141592f) * 0.15f - 0.1f + 0.25f * modifier, lightAmount));
1128    }
1129    
1130    /**
1131     * A convenience method that handles blending the background color with a specified light color, by a specific
1132     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1133     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1134     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1135     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1136     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1137     * @param x the x position, in cells
1138     * @param y the y position, in cells
1139     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1140     * @param lightColor the color to mix with the background, as a libGDX Color
1141     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1142     *                    to between 0 and 1, and negative values can be given to favor background more
1143     */
1144    public void putWithConsistentLight(int x, int y, Color background, Color lightColor, float lightAmount) {
1145        putWithConsistentLight(x, y, background, lightColor, lightAmount, 0.0015f);
1146    }
1147    /**
1148     * A convenience method that handles blending the background color with a specified light color, by a specific
1149     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1150     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1151     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1152     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1153     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1154     * @param x the x position, in cells
1155     * @param y the y position, in cells
1156     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1157     * @param lightColor the color to mix with the background, as a libGDX Color
1158     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1159     *                    to between 0 and 1, and negative values can be given to favor background more
1160     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1161     */
1162    public void putWithConsistentLight(int x, int y, Color background, Color lightColor, float lightAmount, float flickerSpeed) {
1163        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1164        final long time0 = Noise.longFloor(time);
1165        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1166        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1167        putWithLight(x, y, '\0', 0f, background == null ? 0f :  background.toFloatBits(),
1168                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1169    }
1170    /**
1171     * A convenience method that handles blending the background color with a specified light color, by a specific
1172     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1173     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1174     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1175     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1176     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1177     * @param x the x position, in cells
1178     * @param y the y position, in cells
1179     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1180     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1181     * @param lightColor the color to mix with the background, as a packed float color
1182     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1183     *                    to between 0 and 1, and negative values can be given to favor background more
1184     */
1185    public void putWithConsistentLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount) {
1186        putWithConsistentLight(x, y, c, foreground, background, lightColor, lightAmount, 0.0015f);
1187    }
1188    /**
1189     * A convenience method that handles blending the background color with a specified light color, by a specific
1190     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1191     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1192     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1193     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1194     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1195     * @param x the x position, in cells
1196     * @param y the y position, in cells
1197     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1198     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1199     * @param lightColor the color to mix with the background, as a packed float color
1200     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1201     *                    to between 0 and 1, and negative values can be given to favor background more
1202     */
1203    public void putWithConsistentLight(int x, int y, char c, float foreground, float background, float lightColor, double lightAmount) {
1204        putWithConsistentLight(x, y, c, foreground, background, lightColor, lightAmount, 0.0015f);
1205    }
1206    /**
1207     * A convenience method that handles blending the background color with a specified light color, by a specific
1208     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1209     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1210     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1211     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1212     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1213     * @param x the x position, in cells
1214     * @param y the y position, in cells
1215     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1216     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1217     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1218     * @param lightColor the color to mix with the background, as a packed float color
1219     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1220     *                    to between 0 and 1, and negative values can be given to favor background more
1221     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1222     */
1223    public void putWithConsistentLight(int x, int y, char c, float foreground, float background, float lightColor, double lightAmount, float flickerSpeed) {
1224        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1225        final long time0 = Noise.longFloor(time);
1226        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1227        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1228        putWithLight(x, y, c, foreground, background, lightColor, lightAmount);
1229    }
1230    /**
1231     * A convenience method that handles blending the foreground color with a specified light color, by a specific
1232     * amount, without putting a char on the screen; as a whole this affects one x,y position and placed the background
1233     * as-is. This will use the same brightness for all cells given identical lightAmount values when this is called;
1234     * this differentiates it from {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would
1235     * light "splotches" of map with brighter or darker color. Instead, if lightAmount is obtained via SquidLib's
1236     * {@code FOV} class, then all cells at a short distance from an FOV center will be lit brightly and cells far away
1237     * will flicker in and out of view.
1238     * @param x the x position, in cells
1239     * @param y the y position, in cells
1240     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1241     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a packed float color
1242     * @param background the "base" color to use for the background, which will used without modification, as a packed float color
1243     * @param lightColor the color to mix with the foreground, as a packed float color
1244     * @param lightAmount a float that determines how much lightColor should affect foreground by; not strictly limited
1245     *                    to between 0 and 1, and negative values can be given to favor foreground more
1246     */
1247    public void putWithReverseConsistentLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount) {
1248        putWithReverseConsistentLight(x, y, c, foreground, background, lightColor, lightAmount, 0.0015f);
1249    }
1250    /**
1251     * A convenience method that handles blending the foreground color with a specified light color, by a specific
1252     * amount, without putting a char on the screen; as a whole this affects one x,y position and placed the background
1253     * as-is. This will use the same brightness for all cells given identical lightAmount values when this is called;
1254     * this differentiates it from {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would
1255     * light "splotches" of map with brighter or darker color. Instead, if lightAmount is obtained via SquidLib's
1256     * {@code FOV} class, then all cells at a short distance from an FOV center will be lit brightly and cells far away
1257     * will flicker in and out of view.
1258     * @param x the x position, in cells
1259     * @param y the y position, in cells
1260     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1261     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a packed float color
1262     * @param background the "base" color to use for the background, which will used without modification, as a packed float color
1263     * @param lightColor the color to mix with the foreground, as a packed float color
1264     * @param lightAmount a float that determines how much lightColor should affect foreground by; not strictly limited
1265     *                    to between 0 and 1, and negative values can be given to favor foreground more
1266     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1267     */
1268    public void putWithReverseConsistentLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount, float flickerSpeed) {
1269        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1270        final long time0 = Noise.longFloor(time);
1271        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1272        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1273        putWithReverseLight(x, y, c, foreground, background, lightColor, lightAmount);
1274    }
1275
1276    /**
1277     * A convenience method that handles blending the background color with a specified light color, by a specific
1278     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1279     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1280     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1281     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1282     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1283     * @param x the x position, in cells
1284     * @param y the y position, in cells
1285     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1286     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a libGDX Color
1287     * @param background the "base" color to use for the background, which will used without modification
1288     * @param lightColor the color to mix with the background, as a libGDX Color
1289     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1290     *                    to between 0 and 1, and negative values can be given to favor background more
1291     */
1292    public void putWithConsistentLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount) {
1293        putWithConsistentLight(x, y, c, foreground, background, lightColor, lightAmount, 0.0015f);
1294    }
1295    /**
1296     * A convenience method that handles blending the background color with a specified light color, by a specific
1297     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1298     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1299     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1300     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1301     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1302     * @param x the x position, in cells
1303     * @param y the y position, in cells
1304     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1305     * @param foreground the "base" color to use for the foreground, which will used without modification
1306     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1307     * @param lightColor the color to mix with the background, as a libGDX Color
1308     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1309     *                    to between 0 and 1, and negative values can be given to favor background more
1310     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1311     */
1312    public void putWithConsistentLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount, float flickerSpeed) {
1313        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1314        final long time0 = Noise.longFloor(time);
1315        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1316        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1317        putWithLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1318                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1319    }
1320
1321    /**
1322     * A convenience method that handles blending the background color with a specified light color, by a specific
1323     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1324     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1325     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1326     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1327     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1328     * <br>
1329     * Identical to calling {@link #putWithReverseConsistentLight(int, int, char, Color, Color, Color, float, float)}
1330     * with 0.0015f as the last parameter.
1331     * @param x the x position, in cells
1332     * @param y the y position, in cells
1333     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1334     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a libGDX Color
1335     * @param background the "base" color to use for the background, which will used without modification
1336     * @param lightColor the color to mix with the foreground, as a libGDX Color
1337     * @param lightAmount a float that determines how much lightColor should affect foreground by; not strictly limited
1338     *                    to between 0 and 1, and negative values can be given to favor foreground more
1339     */
1340    public void putWithReverseConsistentLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount) {
1341        putWithReverseConsistentLight(x, y, c, foreground, background, lightColor, lightAmount, 0.0015f);
1342    }
1343
1344    /**
1345     * A convenience method that handles blending the background color with a specified light color, by a specific
1346     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1347     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1348     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1349     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1350     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1351     * @param x the x position, in cells
1352     * @param y the y position, in cells
1353     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1354     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a libGDX Color
1355     * @param background the "base" color to use for the background, which will used without modification
1356     * @param lightColor the color to mix with the foreground, as a libGDX Color
1357     * @param lightAmount a float that determines how much lightColor should affect foreground by; not strictly limited
1358     *                    to between 0 and 1, and negative values can be given to favor foreground more
1359     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1360     */
1361    public void putWithReverseConsistentLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount, float flickerSpeed) {
1362        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1363        final long time0 = Noise.longFloor(time);
1364        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1365        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1366        putWithReverseLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1367                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1368    }
1369
1370    /**
1371     * A convenience method that handles blending the background color with a specified light color, by a specific
1372     * amount, without putting a char on the screen; as a whole this affects one x,y position. This will use the same
1373     * brightness for all cells given identical lightAmount values when this is called; this differentiates it from
1374     * {@link #putWithLight(int, int, float, float, float, Noise.Noise3D)}, which would light "splotches" of map with
1375     * brighter or darker color. Instead, if lightAmount is obtained via SquidLib's {@code FOV} class, then all cells
1376     * at a short distance from an FOV center will be lit brightly and cells far away will flicker in and out of view.
1377     * @param x the x position, in cells
1378     * @param y the y position, in cells
1379     * @param c the char to draw; may be {@code '\0'} to draw a solid block
1380     * @param foreground the "base" color to use for the foreground, which will be combined with lightColor, as a libGDX Color
1381     * @param background the "base" color to use for the background, which will used without modification
1382     * @param lightColor the color to mix with the foreground, as a libGDX Color
1383     * @param lightAmount a double that determines how much lightColor should affect foreground by; not strictly limited
1384     *                    to between 0 and 1, and negative values can be given to favor foreground more
1385     * @param flickerSpeed a small float multiplier applied to the time in milliseconds; often between 0.0005f and 0.005f
1386     */
1387    public void putWithReverseConsistentLight(int x, int y, char c, Color foreground, Color background, Color lightColor, double lightAmount, float flickerSpeed) {
1388        final float time = (System.currentTimeMillis() & 0xffffffL) * flickerSpeed; // if you want to adjust the speed of flicker, change the multiplier
1389        final long time0 = Noise.longFloor(time);
1390        final float noise = Noise.querp(NumberTools.randomFloatCurved(time0), NumberTools.randomFloatCurved(time0 + 1L), time - time0);
1391        lightAmount = Math.max(lightAmount * 0.15f, Math.min(lightAmount - NumberTools.swayTight(noise * 3.141592f) * 0.15f - 0.1f + 0.25f * noise, lightAmount)); // 0.1f * noise for light theme, 0.2f * noise for dark theme
1392        putWithReverseLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1393                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount);
1394    }
1395
1396    /**
1397     * A convenience method that handles blending the background color with a specified light color, by a specific
1398     * amount, without putting a char on the screen; as a whole this affects one x,y position.
1399     * @param x the x position, in cells
1400     * @param y the y position, in cells
1401     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1402     * @param lightColor the color to mix with the background, as a packed float
1403     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1404     *                    to between 0 and 1, and negative values can be given to favor background more
1405     */
1406    public void putWithLight(int x, int y, float background, float lightColor, float lightAmount) {
1407        put(x, y, '\0', 0f,
1408                SColor.lerpFloatColors(background, lightColor,
1409                        MathUtils.clamp(0xAAp-9f + (0xC8p-9f * lightAmount), 0f, 1f)));
1410    }
1411    /**
1412     * A convenience method that handles blending the background color with a specified light color, by a specific
1413     * amount, without putting a char on the screen; as a whole this affects one x,y position.
1414     * @param x the x position, in cells
1415     * @param y the y position, in cells
1416     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1417     * @param lightColor the color to mix with the background, as a packed float
1418     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1419     *                    to between 0 and 1, and negative values can be given to favor background more
1420     */
1421    public void putWithLight(int x, int y, float background, float lightColor, double lightAmount) {
1422        put(x, y, '\0', 0f,
1423                SColor.lerpFloatColors(background, lightColor,
1424                        MathUtils.clamp(0xAAp-9f + (0xC8p-9f * (float) lightAmount), 0f, 1f)));
1425    }
1426
1427    /**
1428     * A convenience method that handles blending the background color with a specified light color, by a specific
1429     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1430     * @param x the x position, in cells
1431     * @param y the y position, in cells
1432     * @param c the char to put in the foreground
1433     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1434     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1435     * @param lightColor the color to mix with the background, as a packed float
1436     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1437     *                    to between 0 and 1, and negative values can be given to favor background more
1438     */
1439    public void putWithLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount) {
1440        put(x, y, c, foreground,
1441                SColor.lerpFloatColors(background, lightColor,
1442                        MathUtils.clamp(0xAAp-9f + (0xC8p-9f * lightAmount), 0f, 1f)));
1443    }
1444
1445    /**
1446     * A convenience method that handles blending the background color with a specified light color, by a specific
1447     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1448     * @param x the x position, in cells
1449     * @param y the y position, in cells
1450     * @param c the char to put in the foreground
1451     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1452     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1453     * @param lightColor the color to mix with the background, as a packed float
1454     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1455     *                    to between 0 and 1, and negative values can be given to favor background more
1456     */
1457    public void putWithLight(int x, int y, char c, float foreground, float background, float lightColor, double lightAmount) {
1458        put(x, y, c, foreground,
1459                SColor.lerpFloatColors(background, lightColor,
1460                        MathUtils.clamp(0xAAp-9f + (0xC8p-9f * (float) lightAmount), 0f, 1f)));
1461    }
1462
1463    /**
1464     * A convenience method that handles blending the background color with a specified light color, by a specific
1465     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1466     * @param x the x position, in cells
1467     * @param y the y position, in cells
1468     * @param c the char to put in the foreground
1469     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1470     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1471     * @param lightColor the color to mix with the background, as a packed float
1472     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1473     *                    to between 0 and 1, and negative values can be given to favor background more
1474     */
1475    public void putWithReverseLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount) {
1476        put(x, y, c, SColor.lerpFloatColors(foreground, lightColor,
1477                MathUtils.clamp(0x88p-9f + (0xC8p-9f * lightAmount), 0f, 1f)), background);
1478    }
1479
1480    /**
1481     * A convenience method that handles blending the background color with a specified light color, by a specific
1482     * amount, while also putting a char on the screen; as a whole this affects one x,y position.
1483     * @param x the x position, in cells
1484     * @param y the y position, in cells
1485     * @param c the char to put in the foreground
1486     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1487     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float
1488     * @param lightColor the color to mix with the background, as a packed float
1489     * @param lightAmount a double that determines how much lightColor should affect background by; not strictly limited
1490     *                    to between 0 and 1, and negative values can be given to favor background more
1491     */
1492    public void putWithReverseLight(int x, int y, char c, float foreground, float background, float lightColor, double lightAmount) {
1493        put(x, y, c, SColor.lerpFloatColors(foreground, lightColor,
1494                MathUtils.clamp(0x88p-9f + (0xC8p-9f * (float) lightAmount), 0f, 1f)), background);
1495    }
1496    /**
1497     * A convenience method that handles blending the background color with a specified light color, by a specific
1498     * amount that will be adjusted by the given {@link Noise.Noise3D} object, without putting a char on the screen;
1499     * as a whole this affects one x,y position and will change what it puts as the time (in milliseconds) changes. If
1500     * {@code flicker} is null, this will default to using {@link WhirlingNoise}. You can make the lighting dimmer by
1501     * using a darker color for {@code lightColor} or by giving a lower value for {@code lightAmount}.
1502     * @param x the x position, in cells
1503     * @param y the y position, in cells
1504     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1505     * @param lightColor the color to mix with the background, as a libGDX Color
1506     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1507     *                    to between 0 and 1, and negative values can be given to favor background more
1508     *@param flicker a Noise.Noise3D instance, such as {@link WhirlingNoise#instance}; may be null to use WhirlingNoise
1509     */
1510    public void putWithLight(int x, int y, Color background, Color lightColor, float lightAmount, Noise.Noise3D flicker) {
1511        putWithLight(x, y, '\0', 0f, background == null ? 0f :  background.toFloatBits(),
1512                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount, flicker);
1513    }
1514    /**
1515     * A convenience method that handles blending the background color with a specified light color, by a specific
1516     * amount that will be adjusted by the given {@link Noise.Noise3D} object, while also putting a char on the screen;
1517     * as a whole this affects one x,y position and will change what it puts as the time (in milliseconds) changes. If
1518     * {@code flicker} is null, this will default to using {@link WhirlingNoise}. You can make the lighting dimmer by
1519     * using a darker color for {@code lightColor} or by giving a lower value for {@code lightAmount}.
1520     * @param x the x position, in cells
1521     * @param y the y position, in cells
1522     * @param c the char to put in the foreground
1523     * @param foreground the color to use for the foreground, as a libGDX Color; if null, this won't place a foreground char
1524     * @param background the "base" color to use for the background, which will be combined with lightColor, as a libGDX Color
1525     * @param lightColor the color to mix with the background, as a libGDX Color
1526     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1527     *                    to between 0 and 1, and negative values can be given to favor background more
1528     *@param flicker a Noise.Noise3D instance, such as {@link WhirlingNoise#instance}; may be null to use WhirlingNoise
1529     */
1530    public void putWithLight(int x, int y, char c, Color foreground, Color background, Color lightColor, float lightAmount, Noise.Noise3D flicker) {
1531        putWithLight(x, y, c, foreground == null ? 0f : foreground.toFloatBits(), background == null ? 0f :  background.toFloatBits(),
1532                lightColor == null ? 0f : lightColor.toFloatBits(), lightAmount, flicker);
1533    }
1534    /**
1535     * A convenience method that handles blending the background color with a specified light color, by a specific
1536     * amount that will be adjusted by the given {@link Noise.Noise3D} object, without putting a char on the screen;
1537     * as a whole this affects one x,y position and will change what it puts as the time (in milliseconds) changes. If
1538     * {@code flicker} is null, this will default to using {@link WhirlingNoise}. You can make the lighting dimmer by
1539     * using a darker color for {@code lightColor} or by giving a lower value for {@code lightAmount}.
1540     * @param x the x position, in cells
1541     * @param y the y position, in cells
1542     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1543     * @param lightColor the color to mix with the background, as a packed float color
1544     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1545     *                    to between 0 and 1, and negative values can be given to favor background more
1546     *@param flicker a Noise.Noise3D instance, such as {@link WhirlingNoise#instance}; may be null to use WhirlingNoise
1547     */
1548    public void putWithLight(int x, int y, float background, float lightColor, float lightAmount, Noise.Noise3D flicker) {
1549        putWithLight(x, y, '\0', 0f, background, lightColor, lightAmount, flicker);
1550    }
1551    /**
1552     * A convenience method that handles blending the background color with a specified light color, by a specific
1553     * amount that will be adjusted by the given {@link Noise.Noise3D} object, while also putting a char on the screen;
1554     * as a whole this affects one x,y position and will change what it puts as the time (in milliseconds) changes. If
1555     * {@code flicker} is null, this will default to using {@link WhirlingNoise}. You can make the lighting dimmer by
1556     * using a darker color for {@code lightColor} or by giving a lower value for {@code lightAmount}.
1557     * @param x the x position, in cells
1558     * @param y the y position, in cells
1559     * @param c the char to put in the foreground
1560     * @param foreground the color to use for the foreground, as a packed float; if 0f, this won't place a foreground char
1561     * @param background the "base" color to use for the background, which will be combined with lightColor, as a packed float color
1562     * @param lightColor the color to mix with the background, as a packed float color
1563     * @param lightAmount a float that determines how much lightColor should affect background by; not strictly limited
1564     *                    to between 0 and 1, and negative values can be given to favor background more
1565     *@param flicker a Noise.Noise3D instance, such as {@link WhirlingNoise#instance}; may be null to use WhirlingNoise
1566     */
1567    public void putWithLight(int x, int y, char c, float foreground, float background, float lightColor, float lightAmount, Noise.Noise3D flicker) {
1568        if(flicker == null)
1569            put(x, y, c, foreground,
1570                    SColor.lerpFloatColors(background, lightColor,(0xAAp-9f + (0xC8p-9f * lightAmount *
1571                            (1f + 0.35f * (float) WhirlingNoise.noise(x * 0.3, y * 0.3, (System.currentTimeMillis() & 0xffffffL) * 0.00125))))));
1572        else
1573            put(x, y, c, foreground,
1574                SColor.lerpFloatColors(background, lightColor,(0xAAp-9f + (0xC8p-9f * lightAmount *
1575                        (1f + 0.35f * (float) flicker.getNoise(x * 0.3, y * 0.3, (System.currentTimeMillis() & 0xffffffL) * 0.00125))))));
1576    }
1577    public void putBorders(float color, String caption)
1578    {
1579        put(0, 0,'┌', color);
1580        put(gridWidth - 1, 0,'┐', color);
1581        put(0, gridHeight - 1,'└', color);
1582        put(gridWidth - 1, gridHeight - 1,'┘', color);
1583        for (int i = 1; i < gridWidth - 1; i++) {
1584            put(i, 0, '─', color);
1585            put(i, gridHeight - 1, '─', color);
1586        }
1587        for (int y = 1; y < gridHeight - 1; y++) {
1588            put(0, y, '│', color);
1589            put(gridWidth - 1, y, '│', color);
1590        }
1591        for (int y = 1; y < gridHeight - 1; y++) {
1592            for (int x = 1; x < gridWidth - 1; x++) {
1593                clear(x, y, 0);
1594            }
1595        }
1596
1597        if (caption != null) {
1598            put(1, 0, caption, color, 0f);
1599        }
1600    }
1601
1602    /**
1603     *
1604     * @param color the color to draw the borders with; does not affect caption's color(s)
1605     * @param caption an IColoredString
1606     */
1607    public void putBordersCaptioned(float color, IColoredString<Color> caption)
1608    {
1609        put(0, 0,'┌', color);
1610        put(gridWidth - 1, 0,'┐', color);
1611        put(0, gridHeight - 1,'└', color);
1612        put(gridWidth - 1, gridHeight - 1,'┘', color);
1613        for (int i = 1; i < gridWidth - 1; i++) {
1614            put(i, 0, '─', color);
1615            put(i, gridHeight - 1, '─', color);
1616        }
1617        for (int y = 1; y < gridHeight - 1; y++) {
1618            put(0, y, '│', color);
1619            put(gridWidth - 1, y, '│', color);
1620        }
1621        for (int y = 1; y < gridHeight - 1; y++) {
1622            for (int x = 1; x < gridWidth - 1; x++) {
1623                clear(x, y, 0);
1624            }
1625        }
1626
1627        if (caption != null) {
1628            put(1, 0, caption);
1629        }
1630    }
1631
1632    /**
1633     * Removes the foreground chars, where present, in all layers at the given x,y position.
1634     * The backgrounds will be unchanged.
1635     * @param x the x-coordinate of the position to remove all chars from
1636     * @param y the y-coordinate of the position to remove all chars from
1637     */
1638    @Override
1639    public void clear(int x, int y)
1640    {
1641        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
1642            return;
1643        backgrounds[x][y] = 0f;
1644        int code = SparseTextMap.encodePosition(x, y);
1645        for (int i = 0; i < layers.size(); i++) {
1646            layers.get(i).remove(code);
1647        }
1648    }
1649
1650    /**
1651     * Removes the foreground char, if present, in the given layer at the given x,y position.
1652     * The backgrounds and other layers will be unchanged.
1653     * @param x the x-coordinate of the position to remove all chars from
1654     * @param y the y-coordinate of the position to remove all chars from
1655     * @param layer the layer to remove from
1656     */
1657    public void clear(int x, int y, int layer)
1658    {
1659        layer = mapping.get(layer, -1);
1660        if(layer >= 0)
1661        {
1662            layers.get(layer).remove(SparseTextMap.encodePosition(x, y));
1663        }
1664    }
1665
1666    /**
1667     * Removes all background colors, setting them to transparent, and all foreground chars in all layers.
1668     */
1669    @Override
1670    public void clear()
1671    {
1672        ArrayTools.fill(backgrounds, 0f);
1673        for (int i = 0; i < layers.size(); i++) {
1674            layers.get(i).clear();
1675        }
1676    }
1677
1678    /**
1679     * Removes all foreground chars in the requested layer; does not affect the background or other layers.
1680     */
1681    public void clear(int layer)
1682    {
1683        int lay = mapping.get(layer, -1);
1684        if(lay >= 0)
1685            layers.get(lay).clear();
1686    }
1687    /**
1688     * Fills all of the background with the given color as libGDX Color object.
1689     * @param color the color to use for all of the background, as a libGDX Color or some subclass like SColor
1690     */
1691    public void fillBackground(Color color)
1692    {
1693        ArrayTools.fill(backgrounds, color == null ? 0f : scc.filter(color).toFloatBits());
1694    }
1695
1696    /**
1697     * Fills all of the background with the given color as a packed float.
1698     * @param color the color to use for all of the background, as a packed float
1699     */
1700    public void fillBackground(float color)
1701    {
1702        ArrayTools.fill(backgrounds, color);
1703    }
1704
1705    /**
1706     * Changes the background color in an area to all have the given color, as a libGDX Color (or SColor, etc.).
1707     * @param color a libGDX Color to fill the area with; may be null to make the background transparent
1708     * @param x left edge's x coordinate, in cells
1709     * @param y top edge's y coordinate, in cells
1710     * @param width the width of the area to change the color on
1711     * @param height the height of the area to change the color on
1712     */
1713    public void fillArea(Color color, int x, int y, int width, int height) {
1714        fillArea(color == null ? 0f : scc.filter(color).toFloatBits(), x, y, width, height);
1715    }
1716    /**
1717     * Changes the background color in an area to all have the given color, as a packed float.
1718     * @param color a color as a packed float to fill the area with; may be 0f to make the background transparent
1719     * @param x left edge's x coordinate, in cells
1720     * @param y top edge's y coordinate, in cells
1721     * @param width the width of the area to change the color on
1722     * @param height the height of the area to change the color on
1723     */
1724    public void fillArea(float color, int x, int y, int width, int height) {
1725        if (x < 0) {
1726            width += x;
1727            x = 0;
1728        }
1729        if (y < 0) {
1730            height += y;
1731            y = 0;
1732        }
1733        if (width <= 0 || height <= 0)
1734            return;
1735        for (int i = 0, xx = x; i < width && xx < gridWidth; i++, xx++) {
1736            for (int j = 0, yy = y; j < height && yy < gridHeight; j++, yy++) {
1737                backgrounds[xx][yy] = color;
1738            }
1739        }
1740    }
1741
1742    /**
1743     * Gets whether any animations or other scene2d Actions are running on this SparseLayers.
1744     * @return true if any animations or Actions are currently running, false otherwise
1745     */
1746    @Override
1747    public boolean hasActiveAnimations()
1748    {
1749        if(hasActions())
1750            return true;
1751        for (int i = 0; i < glyphs.size(); i++) {
1752            if(glyphs.get(i).hasActions()) return true;
1753        }
1754        return false;
1755    }
1756
1757    /**
1758     * Gets a direct reference to the 2D float array this uses for background colors.
1759     * @return a direct reference to the 2D float array this uses for background colors.
1760     */
1761    public float[][] getBackgrounds() {
1762        return backgrounds;
1763    }
1764
1765    /**
1766     * Changes the reference this uses for the float array for background colors; this will not accept null parameters,
1767     * nor will it accept any 2D array with dimensions that do not match the (unchanging) gridWidth and gridHeight of
1768     * this SparseLayers.
1769     * @param backgrounds a non-null 2D float array of colors; must have width == gridWidth and height == gridHeight
1770     */
1771    public void setBackgrounds(float[][] backgrounds) {
1772        if(backgrounds == null || backgrounds[0] == null)
1773            throw new IllegalArgumentException("SparseLayers.backgrounds must not be set to null");
1774        if(backgrounds.length != gridWidth || backgrounds[0].length != gridHeight)
1775            throw new IllegalArgumentException("Must be given a 2D array with equal dimensions to the current gridWidth and gridHeight");
1776        this.backgrounds = backgrounds;
1777    }
1778
1779    /**
1780     * Gets a direct reference to the TextCellFactory this uses to draw and size its text items and cells.
1781     * @return a direct reference to the TextCellFactory this uses to draw and size its text items and cells.
1782     */
1783    public TextCellFactory getFont() {
1784        return font;
1785    }
1786
1787    /**
1788     * Sets the TextCellFactory this uses to draw and size its text items and cells. The given TextCellFactory must not
1789     * be null. If font is uninitialized, this will initialize it using cellWidth and cellHeight. If font has been
1790     * initialized with a different height and width, then the sizing of this SparseLayers will change.
1791     * @param font a non-null TextCellFactory; if uninitialized, this will initialize it using cellWidth and cellHeight.
1792     */
1793    public void setFont(TextCellFactory font) {
1794        if(font == null)
1795            throw new IllegalArgumentException("SparseLayers.font must not be set to null");
1796        if(!font.initialized())
1797            font.width(cellWidth()).height(cellHeight()).initBySize();
1798        this.font = font;
1799    }
1800
1801    /**
1802     * Gets the IColorCenter for Color objects (almost always a SquidColorCenter, but this isn't guaranteed) that this
1803     * uses to cache and possibly alter Colors that given to it as parameters.
1804     * @return the IColorCenter of Color that this uses to cache and modify Colors given to it
1805     */
1806    public IColorCenter<Color> getScc() {
1807        return scc;
1808    }
1809
1810    /**
1811     * Sets the IColorCenter for Color objects that this uses to cache and modify Colors given to it; does not accept
1812     * null parameters.
1813     * @param scc a non-null IColorCenter of Color that this will use to cache and modify Colors given to it
1814     */
1815    public void setScc(IColorCenter<Color> scc) {
1816        if(scc == null)
1817            throw new IllegalArgumentException("The IColorCenter<Color> given to setScc() must not be null");
1818        this.scc = scc;
1819    }
1820
1821    /**
1822     * Used internally to go between grid positions and world positions.
1823     * @param gridX x on the grid
1824     * @return x in the world
1825     */
1826    public float worldX(int gridX)
1827    {
1828        return getX() + gridX * font.actualCellWidth;
1829    }
1830    /**
1831     * Used internally to go between grid positions and world positions.
1832     * @param gridY y on the grid
1833     * @return y in the world
1834     */
1835    public float worldY(int gridY)
1836    {
1837        return getY() + (gridHeight - gridY) * font.actualCellHeight;
1838    }
1839
1840    /**
1841     * Used internally to go between world positions and grid positions.
1842     * @param worldX x in the world
1843     * @return x on the grid
1844     */
1845    public int gridX(float worldX)
1846    {
1847        return Math.round((worldX - getX()) / font.actualCellWidth);
1848    }
1849
1850    /**
1851     * Used internally to go between world positions and grid positions.
1852     * @param worldY y in the world
1853     * @return y on the grid
1854     */
1855    public int gridY(float worldY)
1856    {
1857        return Math.round((getY() - worldY) / font.actualCellHeight + gridHeight);
1858    }
1859
1860    /**
1861     * Using the existing background color at the position x,y, this performs color blending from that existing color to
1862     * the given color (as a float), using the mixBy parameter to determine how much of the color parameter to use (1f
1863     * will set the color in this to the parameter, while 0f for mixBy will ignore the color parameter entirely).
1864     * @param x the x component of the position in this panel to draw the starting color from
1865     * @param y the y component of the position in this panel to draw the starting color from
1866     * @param color the new color to mix with the starting color; a packed float, as made by {@link Color#toFloatBits()}
1867     * @param mixBy the amount by which the new color will affect the old one, between 0 (no effect) and 1 (overwrite)
1868     */
1869    @Override
1870    public void blend(int x, int y, float color, float mixBy)
1871    {
1872        backgrounds[x][y] = SColor.lerpFloatColorsBlended(backgrounds[x][y], color, mixBy);
1873    }
1874
1875    /**
1876     * Produces a single char with a color, that can be positioned independently of the contents of this SparseLayers.
1877     * Various effects in this class take a Glyph parameter and can perform visual effects with one. This takes a char
1878     * to show, a color that may be filtered, and an x,y position in grid cells, and returns a Glyph that has those
1879     * qualities set.
1880     * @param shown the char to use in the Glyph
1881     * @param color the color to use for the Glyph, which can be filtered
1882     * @param x the x position, in grid cells
1883     * @param y the y position, in grid cells
1884     * @return a Glyph (an inner class of TextCellFactory) with the given qualities
1885     */
1886    public TextCellFactory.Glyph glyph(char shown, Color color, int x, int y)
1887    {
1888        return glyph(shown, color == null ? 0f : scc.filter(color).toFloatBits(), x, y);
1889    }
1890
1891    /**
1892     * Produces a single char with a color, that can be positioned independently of the contents of this SparseLayers.
1893     * Various effects in this class take a Glyph parameter and can perform visual effects with one. This takes a char
1894     * to show, a color as an encoded float, and an x,y position in grid cells, and returns a Glyph that has those
1895     * qualities set.
1896     * @param shown the char to use in the Glyph
1897     * @param color the color to use for the Glyph as an encoded float
1898     * @param x the x position, in grid cells
1899     * @param y the y position, in grid cells
1900     * @return a Glyph (an inner class of TextCellFactory) with the given qualities
1901     */
1902    public TextCellFactory.Glyph glyph(char shown, float color, int x, int y)
1903    {
1904        TextCellFactory.Glyph g =
1905                font.glyph(shown, color,
1906                        worldX(x),
1907                        worldY(y));
1908        glyphs.add(g);
1909        return g;
1910    }
1911    /**
1912     * "Promotes" a colorful char in the first layer to a Glyph that can be positioned independently of the contents of
1913     * this SparseLayers. Various effects in this class take a Glyph parameter and can perform visual effects with one.
1914     * This takes only an x,y position in grid cells, removes the char at that position in the first layer from normal
1915     * rendering, and returns a Glyph at that same position with the same char and color, but that can be moved more.
1916     * @param x the x position, in grid cells
1917     * @param y the y position, in grid cells
1918     * @return a Glyph (an inner class of TextCellFactory) that took the qualities of the removed char and its color
1919     */
1920    public TextCellFactory.Glyph glyphFromGrid(int x, int y)
1921    {
1922        int code = SparseTextMap.encodePosition(x, y);
1923        SparseTextMap stm = layers.get(0);
1924        char shown = stm.getChar(code, ' ');
1925        float color = stm.getFloat(code, 0f);
1926        stm.remove(code);
1927        TextCellFactory.Glyph g =
1928                font.glyph(shown, color, worldX(x), worldY(y));
1929        glyphs.add(g);
1930        return g;
1931    }
1932    /**
1933     * "Promotes" a colorful char in the given layer to a Glyph that can be positioned independently of the contents of
1934     * this SparseLayers. Various effects in this class take a Glyph parameter and can perform visual effects with one.
1935     * This takes only an x,y position in grid cells, removes the char at that position in the given layer from normal
1936     * rendering, and returns a Glyph at that same position with the same char and color, but that can be moved more.
1937     * @param x the x position, in grid cells
1938     * @param y the y position, in grid cells
1939     * @param layer the layer to take a colorful char from
1940     * @return a Glyph (an inner class of TextCellFactory) that took the qualities of the removed char and its color; may return null if the layer is invalid
1941     */
1942    public TextCellFactory.Glyph glyphFromGrid(int x, int y, int layer) {
1943        layer = mapping.get(layer, -1);
1944        if (layer >= 0) {
1945            SparseTextMap stm = layers.get(layer);
1946            int code = SparseTextMap.encodePosition(x, y);
1947            char shown = stm.getChar(code, ' ');
1948            float color = stm.getFloat(code, 0f);
1949            stm.remove(code);
1950            TextCellFactory.Glyph g =
1951                    font.glyph(shown, color, worldX(x), worldY(y));
1952            glyphs.add(g);
1953            return g;
1954        }
1955        else
1956        {
1957            return null;
1958        }
1959    }
1960
1961    /**
1962     * Brings a Glyph back into normal rendering, removing it from the Glyphs this class knows about and filling the
1963     * grid's char at the Glyph's position in the first layer with the Glyph's char and color.
1964     * @param glyph the Glyph to remove and fit back into the grid
1965     */
1966    public void recallToGrid(TextCellFactory.Glyph glyph)
1967    {
1968        layers.get(0).place(gridX(glyph.getY()), gridY(glyph.getY()), glyph.shown, glyph.getPackedColor());
1969        glyphs.remove(glyph);
1970    }
1971
1972    /**
1973     * Brings a Glyph back into normal rendering, removing it from the Glyphs this class knows about and filling the
1974     * grid's char at the Glyph's position in the given layer with the Glyph's char and color.
1975     * @param glyph the Glyph to remove and fit back into the grid
1976     */
1977    public void recallToGrid(TextCellFactory.Glyph glyph, int layer)
1978    {
1979        layer = mapping.get(layer, 0);
1980        layers.get(layer).place(gridX(glyph.getY()), gridY(glyph.getY()), glyph.shown, glyph.getPackedColor());
1981        glyphs.remove(glyph);
1982    }
1983
1984    /**
1985     * A way to remove a Glyph from the group of glyphs this renders, while also ending any animations or other Actions
1986     * that the removed Glyph was scheduled to perform.
1987     * @param glyph a Glyph that should be removed from the {@link #glyphs} List this holds
1988     */
1989    public void removeGlyph(TextCellFactory.Glyph glyph)
1990    {
1991        glyph.clearActions();
1992        glyphs.remove(glyph);
1993    }
1994    /**
1995     * Start a bumping animation in the given direction that will last duration seconds.
1996     * @param glyph
1997     *              A {@link TextCellFactory.Glyph}, probably produced by
1998     *              {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
1999     * @param direction the direction for the glyph to bump towards
2000     * @param duration a float, measured in seconds, for how long the animation should last; commonly 0.12f
2001     */
2002    public void bump(final TextCellFactory.Glyph glyph, Direction direction, float duration) {
2003        bump(0f, glyph, direction, duration, null);
2004    }
2005    /**
2006     * Start a bumping animation in the given direction after delay seconds, that will last duration seconds; runs
2007     * postRunnable after the duration completes if postRunnable is non-null.
2008     * 
2009     * @param delay how long to wait in seconds before starting the effect
2010     * @param glyph
2011     *              A {@link TextCellFactory.Glyph}, which should be produced by a SparseLayers method like
2012     *              {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
2013     * @param direction the direction for the glyph to bump towards
2014     * @param duration a float, measured in seconds, for how long the animation should last; commonly 0.12f
2015     * @param postRunnable a Runnable to execute after the bump completes; may be null to do nothing.
2016     */
2017    public void bump(final float delay, final TextCellFactory.Glyph glyph, Direction direction, float duration, final /* @Nullable */ Runnable postRunnable)
2018    {
2019        final float x = glyph.getX(),
2020                y = glyph.getY();
2021        duration = Math.max(0.015f, duration);
2022        final int nbActions = 2 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2023        int index = 0;
2024        final Action[] sequence = new Action[nbActions];
2025        if (0 < delay)
2026            sequence[index++] = Actions.delay(delay);
2027        sequence[index++] = Actions.moveToAligned(x + direction.deltaX * 0.35f * font.actualCellWidth,
2028                y - direction.deltaY * 0.35f * font.actualCellHeight,
2029                Align.bottomLeft, duration * 0.35F);
2030        sequence[index++] = Actions.moveToAligned(x, y, Align.bottomLeft, duration * 0.65F);
2031        if(postRunnable != null)
2032        {
2033            sequence[index] = Actions.run(postRunnable);
2034        }
2035        glyph.addAction(Actions.sequence(sequence));
2036    }
2037
2038    /**
2039     * Slides {@code glyph} from {@code (xstartX,startY)} to {@code (newx, newy)}.
2040     * Takes a number of seconds equal to duration to complete. This also allows
2041     * a Runnable to be given as {@code postRunnable} to be run after the
2042     * slide completes, or null to not run anything after the slide.
2043     *
2044     * @param glyph
2045     *            A {@link TextCellFactory.Glyph}, probably produced by
2046     *            {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
2047     * @param startX
2048     *            Where to start the slide, horizontally.
2049     * @param startY
2050     *            Where to start the slide, vertically.
2051     * @param newX
2052     *            Where to end the slide, horizontally.
2053     * @param newY
2054     *            Where to end the slide, vertically.
2055     * @param duration
2056     *            The animation's duration.
2057     * @param postRunnable a Runnable to execute after the slide completes; may be null to do nothing.
2058     */
2059    public void slide(TextCellFactory.Glyph glyph, final int startX, final int startY, final int newX,
2060                      final int newY, float duration, /* @Nullable */ Runnable postRunnable) {
2061        slide(0f, glyph, startX, startY, newX, newY, duration, postRunnable);
2062    }
2063
2064    /**
2065     * Slides {@code glyph} from {@code (xstartX,startY)} to {@code (newx, newy)} after waiting {@code delay} seconds.
2066     * Takes a number of seconds equal to duration to complete (starting after the delay). This also allows
2067     * a Runnable to be given as {@code postRunnable} to be run after the
2068     * slide completes, or null to not run anything after the slide.
2069     *
2070     * @param delay how long to wait in seconds before starting the effect
2071     * @param glyph
2072     *            A {@link TextCellFactory.Glyph}, probably produced by
2073     *            {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
2074     * @param startX
2075     *            Where to start the slide, horizontally.
2076     * @param startY
2077     *            Where to start the slide, vertically.
2078     * @param newX
2079     *            Where to end the slide, horizontally.
2080     * @param newY
2081     *            Where to end the slide, vertically.
2082     * @param duration
2083     *            The animation's duration.
2084     * @param postRunnable a Runnable to execute after the slide completes; may be null to do nothing.
2085     */
2086    public void slide(float delay, TextCellFactory.Glyph glyph, final int startX, final int startY, final int newX,
2087                      final int newY, float duration, /* @Nullable */ Runnable postRunnable) {
2088        glyph.setPosition(worldX(startX), worldY(startY));
2089        duration = Math.max(0.015f, duration);
2090        final int nbActions = 1 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2091        int index = 0;
2092        final Action[] sequence = new Action[nbActions];
2093        if (0 < delay)
2094            sequence[index++] = Actions.delay(delay);
2095        final float nextX = worldX(newX);
2096        final float nextY = worldY(newY);
2097        sequence[index++] = Actions.moveToAligned(nextX, nextY, Align.bottomLeft, duration);
2098        if(postRunnable != null)
2099        {
2100            sequence[index] = Actions.run(postRunnable);
2101        }
2102
2103        glyph.addAction(Actions.sequence(sequence));
2104    }
2105
2106    /**
2107     * Starts an wiggling animation for the object at the given location for the given duration in seconds.
2108     *
2109     * @param glyph
2110     *            A {@link TextCellFactory.Glyph}, probably produced by
2111     *            {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
2112     * @param duration in seconds, as a float
2113     */
2114    public void wiggle(final TextCellFactory.Glyph glyph, float duration) {
2115        wiggle(0f, glyph, duration, null);
2116    }
2117
2118    /**
2119     * Starts a wiggling animation for the object at the given location, after waiting delay seconds, for the given
2120     * duration in seconds; runs postRunnable afterwards if it is non-null.
2121     *
2122     * @param delay how long to wait in seconds before starting the effect
2123     * @param glyph
2124     *            A {@link TextCellFactory.Glyph}, probably produced by
2125     *            {@link #glyph(char, float, int, int)} or {@link #glyphFromGrid(int, int)}
2126     * @param duration in seconds, as a float
2127     * @param postRunnable a Runnable to execute after the wiggle completes; may be null to do nothing.
2128     */
2129    public void wiggle(final float delay, final TextCellFactory.Glyph glyph, float duration,
2130            /* @Nullable */ Runnable postRunnable) {
2131        final float x = glyph.getX(), y = glyph.getY(),
2132                cellWidth = font.actualCellWidth, cellHeight = font.actualCellHeight;
2133        duration = Math.max(0.015f, duration);
2134        final int nbActions = 5 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2135        final Action[] sequence = new Action[nbActions];
2136        int index = 0;
2137        if (0 < delay)
2138            sequence[index++] = Actions.delay(delay);
2139        final StatefulRNG gRandom = DefaultResources.getGuiRandom();
2140        sequence[index++] = Actions.moveToAligned(x + (gRandom.nextFloat() - 0.5F) * cellWidth * 0.4f,
2141                y + (gRandom.nextFloat() - 0.5F) * cellHeight * 0.4f, Align.bottomLeft, duration * 0.2F);
2142        sequence[index++] = Actions.moveToAligned(x + (gRandom.nextFloat() - 0.5F) * cellWidth * 0.4f,
2143                y + (gRandom.nextFloat() - 0.5F) * cellHeight * 0.4f, Align.bottomLeft, duration * 0.2F);
2144        sequence[index++] = Actions.moveToAligned(x + (gRandom.nextFloat() - 0.5F) * cellWidth * 0.4f,
2145                y + (gRandom.nextFloat() - 0.5F) * cellHeight * 0.4f, Align.bottomLeft, duration * 0.2F);
2146        sequence[index++] = Actions.moveToAligned(x + (gRandom.nextFloat() - 0.5F) * cellWidth * 0.4f,
2147                y + (gRandom.nextFloat() - 0.5F) * cellHeight * 0.4f, Align.bottomLeft, duration * 0.2F);
2148        sequence[index++] = Actions.moveToAligned(x, y, Align.bottomLeft, duration * 0.2F);
2149        if(postRunnable != null)
2150        {
2151            sequence[index] = Actions.run(postRunnable);
2152        }
2153        glyph.addAction(Actions.sequence(sequence));
2154    }
2155    /**
2156     * Tints the background at position x,y so it becomes the given encodedColor, then after the tint is complete it 
2157     * returns the cell to its original color, taking duration seconds.
2158     * <br>
2159     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2160     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2161     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2162     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2163     * @param x the x-coordinate of the cell to tint
2164     * @param y the y-coordinate of the cell to tint
2165     * @param color what to transition the cell's color towards, and then transition back from, as a Color object
2166     * @param duration how long the total "round-trip" transition should take in seconds
2167     */
2168    public void tint(final int x, final int y, final Color color, float duration) {
2169        tint(0f, x, y, color.toFloatBits(), duration, null);
2170    }
2171    /**
2172     * Tints the background at position x,y so it becomes the given encodedColor, then after the tint is complete it 
2173     * returns the cell to its original color, taking duration seconds.
2174     * <br>
2175     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2176     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2177     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2178     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2179     * @param x the x-coordinate of the cell to tint
2180     * @param y the y-coordinate of the cell to tint
2181     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2182     * @param duration how long the total "round-trip" transition should take in seconds
2183     */
2184    public void tint(final int x, final int y, final float encodedColor, float duration) {
2185        tint(0f, x, y, encodedColor, duration, null);
2186    }
2187    /**
2188     * Tints the background at position x,y so it becomes the given encodedColor, waiting for {@code delay} (in seconds)
2189     * before performing it, then after the tint is complete it returns the cell to its original color, taking duration
2190     * seconds. Additionally, enqueue {@code postRunnable} for running after the created action ends.
2191     * <br>
2192     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2193     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2194     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2195     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2196     * @param delay how long to wait in seconds before starting the effect
2197     * @param x the x-coordinate of the cell to tint
2198     * @param y the y-coordinate of the cell to tint
2199     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2200     * @param duration how long the total "round-trip" transition should take in seconds
2201     * @param postRunnable a Runnable to execute after the tint completes; may be null to do nothing.
2202     */
2203    public void tint(final float delay, final int x, final int y, final float encodedColor, float duration,
2204            /* @Nullable */ Runnable postRunnable) {
2205        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
2206            return;
2207        duration = Math.max(0.015f, duration);
2208        final float ac = backgrounds[x][y];
2209        final int nbActions = 3 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2210        final Action[] sequence = new Action[nbActions];
2211        int index = 0;
2212        if (0 < delay)
2213            sequence[index++] = Actions.delay(delay);
2214        sequence[index++] = new TemporalAction(duration * 0.3f) {
2215            @Override
2216            protected void update(float percent) {
2217                backgrounds[x][y] = SColor.lerpFloatColors(ac, encodedColor, percent);
2218            }
2219        };
2220        sequence[index++] = new TemporalAction(duration * 0.7f) {
2221            @Override
2222            protected void update(float percent) {
2223                backgrounds[x][y] = SColor.lerpFloatColors(encodedColor, ac, percent);
2224            }
2225        };
2226        if(postRunnable != null)
2227        {
2228            sequence[index++] = Actions.run(postRunnable);
2229        }
2230        sequence[index] = Actions.delay(duration, Actions.run(new Runnable() {
2231            @Override
2232            public void run() {
2233                backgrounds[x][y] = ac;
2234            }
2235        }));
2236
2237        addAction(Actions.sequence(sequence));
2238    }
2239
2240    /**
2241     * Tints the foreground in the given layer at position x,y so it becomes the given encodedColor, then after the tint
2242     * is complete it returns the cell to its original color, taking duration seconds.
2243     * <br>
2244     * The {@link SquidPanel#tint(float, int, int, Color, float, Runnable)} method has been reworked to use the same
2245     * technique this class uses for rendering text, and the two classes should have similar appearance (if not the
2246     * same) when rendering the same text. SparseLayers tends to be faster, especially when not all of the map is shown.
2247     * <br>
2248     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2249     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2250     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2251     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2252     * @param x the x-coordinate of the cell to tint
2253     * @param y the y-coordinate of the cell to tint
2254     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2255     * @param color what to transition the cell's color towards, and then transition back from, as a Color object
2256     * @param duration how long the total "round-trip" transition should take in seconds
2257     */
2258    public void tint(final int x, final int y, final int layer, final Color color, float duration) {
2259        tint(0f, x, y, layer, color.toFloatBits(), duration, null);
2260    }
2261
2262    /**
2263     * Tints the foreground in the given layer at position x,y so it becomes the given encodedColor, then after the tint
2264     * is complete it returns the cell to its original color, taking duration seconds.
2265     * <br>
2266     * The {@link SquidPanel#tint(float, int, int, Color, float, Runnable)} method has been reworked to use the same
2267     * technique this class uses for rendering text, and the two classes should have similar appearance (if not the
2268     * same) when rendering the same text. SparseLayers tends to be faster, especially when not all of the map is shown.
2269     * <br>
2270     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2271     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2272     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2273     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2274     * @param x the x-coordinate of the cell to tint
2275     * @param y the y-coordinate of the cell to tint
2276     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2277     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2278     * @param duration how long the total "round-trip" transition should take in seconds
2279     */
2280    public void tint(final int x, final int y, final int layer, final float encodedColor, float duration) {
2281        tint(0f, x, y, layer, encodedColor, duration, null);
2282    }
2283    /**
2284     * Tints the foreground in the given layer at position x,y so it becomes the given encodedColor, waiting for
2285     * {@code delay} (in seconds) before performing it, then after the tint is complete it returns the cell to its
2286     * original color, taking duration seconds. Additionally, enqueue {@code postRunnable} for running after the created
2287     * action ends.
2288     * <br>
2289     * The {@link SquidPanel#tint(float, int, int, Color, float, Runnable)} method has been reworked to use the same
2290     * technique this class uses for rendering text, and the two classes should have similar appearance (if not the
2291     * same) when rendering the same text. SparseLayers tends to be faster, especially when not all of the map is shown.
2292     * <br>
2293     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2294     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2295     * draw the contents without the tint this applies, then apply the tint when you call act(), then quickly overwrite
2296     * the tint in the next frame. That visually appears as nothing happening other than a delay.
2297     * @param delay how long to wait in seconds before starting the effect
2298     * @param x the x-coordinate of the cell to tint
2299     * @param y the y-coordinate of the cell to tint
2300     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2301     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2302     * @param duration how long the total "round-trip" transition should take in seconds
2303     * @param postRunnable a Runnable to execute after the tint completes; may be null to do nothing.
2304     */
2305    public void tint(final float delay, final int x, final int y, final int layer, final float encodedColor, float duration,
2306            /* @Nullable */ Runnable postRunnable) {
2307        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight || layer < 0 || layer >= layers.size())
2308            return;
2309        final SparseTextMap l = layers.get(layer);
2310        duration = Math.max(0.015f, duration);
2311        final int pos = SparseTextMap.encodePosition(x, y);
2312        final float ac = l.getFloat(pos,0f);
2313        final int nbActions = 3 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2314        final Action[] sequence = new Action[nbActions];
2315        int index = 0;
2316        if (0 < delay)
2317            sequence[index++] = Actions.delay(delay);
2318        sequence[index++] = new TemporalAction(duration * 0.3f) {
2319            @Override
2320            protected void update(float percent) {
2321                l.updateFloat(pos, SColor.lerpFloatColors(ac, encodedColor, percent));
2322            }
2323        };
2324        sequence[index++] = new TemporalAction(duration * 0.7f) {
2325            @Override
2326            protected void update(float percent) {
2327                l.updateFloat(pos, SColor.lerpFloatColors(encodedColor, ac, percent));
2328            }
2329        };
2330        if(postRunnable != null)
2331        {
2332            sequence[index++] = Actions.run(postRunnable);
2333        }
2334        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2335        sequence[index] = Actions.delay(duration, Actions.run(new Runnable() {
2336            @Override
2337            public void run() {
2338                l.updateFloat(pos, ac);
2339            }
2340        }));
2341        addAction(Actions.sequence(sequence));
2342    }
2343    /**
2344     * Tints the given glyph (which may or may not be part of the {@link #glyphs} list this holds) so it becomes the
2345     * given encodedColor, then after the tint is complete it returns the cell to its original color, taking duration
2346     * seconds. This resets the glyph to its pre-tint color before it ends.
2347     * @param glyph the {@link TextCellFactory.Glyph} to tint
2348     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2349     * @param duration how long the total "round-trip" transition should take in seconds
2350     */
2351    public void tint(final TextCellFactory.Glyph glyph, final float encodedColor, float duration) {
2352        tint(0f, glyph, encodedColor, duration, null);
2353    }
2354    /**
2355     * Tints the given glyph (which may or may not be part of the {@link #glyphs} list this holds) so it becomes the
2356     * given encodedColor, waiting for {@code delay} (in seconds) before performing it, then after the tint is complete
2357     * it returns the cell to its original color, taking duration seconds. Additionally, enqueue {@code postRunnable}
2358     * for running after the created action ends. This resets the glyph to its pre-tint color before it runs any
2359     * {@code postRunnable}.
2360     * @param delay how long to wait in seconds before starting the effect
2361     * @param glyph the {@link TextCellFactory.Glyph} to tint
2362     * @param encodedColor what to transition the cell's color towards, and then transition back from, as a packed float
2363     * @param duration how long the total "round-trip" transition should take in seconds
2364     * @param postRunnable a Runnable to execute after the tint completes; may be null to do nothing.
2365     */
2366    public void tint(float delay, final TextCellFactory.Glyph glyph, final float encodedColor, float duration,
2367            /* @Nullable */ Runnable postRunnable) {
2368        duration = Math.max(0.015f, duration);
2369        final float ac = glyph.getPackedColor();
2370        final int nbActions = 3 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2371        final Action[] sequence = new Action[nbActions];
2372        int index = 0;
2373        if (0 < delay)
2374            sequence[index++] = Actions.delay(delay);
2375        sequence[index++] = new TemporalAction(duration * 0.3f) {
2376            @Override
2377            protected void update(float percent) {
2378                glyph.setPackedColor(SColor.lerpFloatColors(ac, encodedColor, percent));
2379            }
2380        };
2381        sequence[index++] = new TemporalAction(duration * 0.7f) {
2382            @Override
2383            protected void update(float percent) {
2384                glyph.setPackedColor(SColor.lerpFloatColors(encodedColor, ac, percent));
2385            }
2386        };
2387        if(postRunnable != null)
2388        {
2389            sequence[index++] = Actions.run(postRunnable);
2390        }
2391        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2392        sequence[index] = Actions.delay(duration, Actions.run(new Runnable() {
2393            @Override
2394            public void run() {
2395                glyph.setPackedColor(ac);
2396            }
2397        }));
2398        addAction(Actions.sequence(sequence));
2399    }
2400    /**
2401     * Create a new Glyph at (startX, startY) using the char shown with the given startColor, and immediately starts
2402     * changing color to endColor, changing position so it ends on the cell (endX, endY), taking duration seconds to
2403     * complete before removing the Glyph.
2404     * <br>
2405     * Unlike {@link SquidPanel#summon(float, int, int, int, int, char, Color, Color, boolean, float, float, float)},
2406     * this does not rotate the Glyph it produces.
2407     * @param startX the starting x position in cells
2408     * @param startY the starting y position in cells
2409     * @param endX the ending x position in cells
2410     * @param endY the ending y position in cells
2411     * @param shown the char to show (the same char throughout the effect)
2412     * @param startColor the starting Color
2413     * @param endColor the Color to transition to
2414     * @param duration the duration in seconds for the effect
2415     */
2416    public void summon(int startX, int startY, int endX, int endY, char shown,
2417                       final float startColor, final float endColor, float duration)
2418    {
2419        summon(0f, startX, startY, endX, endY, shown, startColor, endColor, duration, null);
2420    }
2421    /**
2422     * Create a new Glyph at (startX, startY) using the char shown with the given startColor, and after delay seconds,
2423     * starts changing color to endColor, changing position so it ends on the cell (endX, endY), taking duration seconds
2424     * to complete before running postRunnable (if it is non-null) and finally removing the Glyph.
2425     * <br>
2426     * Unlike {@link SquidPanel#summon(float, int, int, int, int, char, Color, Color, boolean, float, float, float)},
2427     * this does not rotate the Glyph it produces.
2428     * @param delay how long to wait in seconds before starting the effect
2429     * @param startX the starting x position in cells
2430     * @param startY the starting y position in cells
2431     * @param endX the ending x position in cells
2432     * @param endY the ending y position in cells
2433     * @param shown the char to show (the same char throughout the effect)
2434     * @param startColor the starting Color
2435     * @param endColor the Color to transition to
2436     * @param duration the duration in seconds for the effect
2437     * @param postRunnable a Runnable to execute after the summon completes; may be null to do nothing.
2438     */
2439    public void summon(float delay, int startX, int startY, int endX, int endY, char shown,
2440                       final float startColor, final float endColor, float duration,
2441            /* @Nullable */ Runnable postRunnable)
2442    {
2443        duration = Math.max(0.015f, duration);
2444        final int nbActions = 2 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2445        int index = 0;
2446        final Action[] sequence = new Action[nbActions];
2447        if (0 < delay)
2448            sequence[index++] = Actions.delay(delay);
2449        final TextCellFactory.Glyph glyph = glyph(shown, startColor, startX, startY);
2450        sequence[index++] = Actions.parallel(
2451                new TemporalAction(duration) {
2452                    @Override
2453                    protected void update(float percent) {
2454                        glyph.setPackedColor(SColor.lerpFloatColors(startColor, endColor, percent * 0.95f));
2455                    }
2456                },
2457                Actions.moveTo(worldX(endX), worldY(endY), duration));
2458        if(postRunnable != null)
2459        {
2460            sequence[index++] = Actions.run(postRunnable);
2461        }
2462        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2463        sequence[index] = Actions.run(new Runnable() {
2464            @Override
2465            public void run() {
2466                glyphs.remove(glyph);
2467            }
2468        });
2469        glyph.addAction(Actions.sequence(sequence));
2470    }
2471
2472    /**
2473     * Create a new Glyph at (startX, startY) in world coordinates (often pixels on the screen) using the char shown
2474     * with the given startColor, and immediately starts changing color to endColor, changing position so it ends at the
2475     * world coordinates (endX, endY), taking duration seconds to complete before removing the Glyph.
2476     * <br>
2477     * Unlike {@link SquidPanel#summon(float, int, int, int, int, char, Color, Color, boolean, float, float, float)},
2478     * this does not rotate the Glyph it produces.
2479     * @param startX the starting x position in world coordinates
2480     * @param startY the starting y position in world coordinates
2481     * @param endX the ending x position in world coordinates
2482     * @param endY the ending y position in world coordinates
2483     * @param shown the char to show (the same char throughout the effect)
2484     * @param startColor the starting Color
2485     * @param endColor the Color to transition to
2486     * @param duration the duration in seconds for the effect
2487     */
2488    public void summon(float startX, float startY, float endX, float endY, char shown,
2489                       final float startColor, final float endColor, float duration)
2490    {
2491        summon(0f, startX, startY, endX, endY, shown, startColor, endColor, duration, null);
2492    }
2493    /**
2494     * Create a new Glyph at (startX, startY) in world coordinates (often pixels on the screen) using the char shown
2495     * with the given startColor, and after delay seconds, starts changing color to endColor, changing position so it
2496     * ends at the world coordinates (endX, endY), taking duration seconds to complete before running postRunnable (if
2497     * it is non-null) and finally removing the Glyph.
2498     * <br>
2499     * Unlike {@link SquidPanel#summon(float, int, int, int, int, char, Color, Color, boolean, float, float, float)},
2500     * this does not rotate the Glyph it produces.
2501     * @param delay how long to wait in seconds before starting the effect
2502     * @param startX the starting x position in world coordinates
2503     * @param startY the starting y position in world coordinates
2504     * @param endX the ending x position in world coordinates
2505     * @param endY the ending y position in world coordinates
2506     * @param shown the char to show (the same char throughout the effect)
2507     * @param startColor the starting Color
2508     * @param endColor the Color to transition to
2509     * @param duration the duration in seconds for the effect
2510     * @param postRunnable a Runnable to execute after the summon completes; may be null to do nothing.
2511     */
2512    public void summon(float delay, float startX, float startY, float endX, float endY, char shown,
2513                       final float startColor, final float endColor, float duration,
2514            /* @Nullable */ Runnable postRunnable)
2515    {
2516        duration = Math.max(0.015f, duration);
2517        final int nbActions = 2 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2518        int index = 0;
2519        final Action[] sequence = new Action[nbActions];
2520        if (0 < delay)
2521            sequence[index++] = Actions.delay(delay);
2522        final TextCellFactory.Glyph glyph = glyph(shown, startColor, gridX(startX), gridY(startY));
2523        sequence[index++] = Actions.parallel(
2524                new TemporalAction(duration) {
2525                    @Override
2526                    protected void update(float percent) {
2527                        glyph.setPackedColor(SColor.lerpFloatColors(startColor, endColor, percent * 0.95f));
2528                    }
2529                },
2530                Actions.moveTo(endX, endY, duration));
2531        if(postRunnable != null)
2532        {
2533            sequence[index++] = Actions.run(postRunnable);
2534        }
2535        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2536        sequence[index] = Actions.run(new Runnable() {
2537            @Override
2538            public void run() {
2539                glyphs.remove(glyph);
2540            }
2541        });
2542        glyph.addAction(Actions.sequence(sequence));
2543    }
2544
2545
2546    /**
2547     * Convenience method to produce an explosion, splash, or burst effect. Calls
2548     * {@link #summon(int, int, int, int, char, float, float, float)} repeatedly with
2549     * different parameters. As with summon(), this creates temporary Glyphs that change color and position.
2550     * The distance is how many cells away to move the created Actors away from (x,y). The measurement determines
2551     * whether this produces Glyphs in 4 (cardinal) directions for DIAMOND or OCTAHEDRON, or 8 (cardinal and diagonal)
2552     * directions for any other enum value for Radius; CIRCLE and SPHERE will position the 8 glyphs in a circle, while
2553     * SQUARE and CUBE will position their 8 glyphs in a square.
2554     * <br>
2555     * Unlike {@link SquidPanel#burst(float, int, int, int, boolean, char, Color, Color, boolean, float, float)}, this
2556     * does not rotate the individual Glyphs it produces.
2557     * @param x the starting, center, x-position in cells to create all Actors at
2558     * @param y the starting, center, y-position in cells to create all Actors at
2559     * @param distance how far away, in cells, to move each actor from the center (Chebyshev distance, forming a square)
2560     * @param measurement a Radius enum that determines if 4 (DIAMOND, OCTAHEDRON) or 8 (anything else) Glyphs are
2561     *                    created, and the shape they will take
2562     * @param shown the char to use for Glyphs; should definitely be visible
2563     * @param startColor the encoded color to start the effect with
2564     * @param endColor the encoded color to end the effect on
2565     * @param duration how long, in seconds, the effect should last
2566     */
2567    public void burst(int x, int y, int distance, Radius measurement, char shown,
2568                      float startColor, float endColor, float duration) {
2569        burst(0f, x, y, distance, measurement, shown, startColor, endColor, duration, null);
2570    }
2571    /**
2572     * Convenience method to produce an explosion, splash, or burst effect. Calls
2573     * {@link #summon(float, int, int, int, int, char, float, float, float, Runnable)} repeatedly with
2574     * different parameters. As with summon(), this creates temporary Glyphs that change color and position.
2575     * The distance is how many cells away to move the created Actors away from (x,y). The measurement determines
2576     * whether this produces Glyphs in 4 (cardinal) directions for DIAMOND or OCTAHEDRON, or 8 (cardinal and diagonal)
2577     * directions for any other enum value for Radius; CIRCLE and SPHERE will position the 8 glyphs in a circle, while
2578     * SQUARE and CUBE will position their 8 glyphs in a square. This takes a delay in seconds that can be used to make
2579     * the effect wait to start for some amount of time, and a Runnable that will be run once after the burst's full
2580     * duration completes (not once per summoned Glyph).
2581     * <br>
2582     * Unlike {@link SquidPanel#burst(float, int, int, int, boolean, char, Color, Color, boolean, float, float)}, this
2583     * does not rotate the individual Glyphs it produces.
2584     * @param delay how long to wait in seconds before starting the effect
2585     * @param x the starting, center, x-position in cells to create all Actors at
2586     * @param y the starting, center, y-position in cells to create all Actors at
2587     * @param distance how far away, in cells, to move each actor from the center (Chebyshev distance, forming a square)
2588     * @param measurement a Radius enum that determines if 4 (DIAMOND, OCTAHEDRON) or 8 (anything else) Glyphs are
2589     *                    created, and the shape they will take
2590     * @param shown the char to use for Glyphs; should definitely be visible
2591     * @param startColor the encoded color to start the effect with
2592     * @param endColor the encoded color to end the effect on
2593     * @param duration how long, in seconds, the effect should last
2594     * @param postRunnable a Runnable to execute after the burst completes; may be null to do nothing,
2595     *                     and will only be run once for the whole burst effect.
2596     */
2597    public void burst(float delay, int x, int y, int distance, Radius measurement, char shown,
2598                      float startColor, float endColor, float duration, /* @Nullable */ Runnable postRunnable)
2599    {
2600        Direction d;
2601        switch (measurement)
2602        {
2603            case SQUARE:
2604            case CUBE:
2605                for (int i = 0; i < 7; i++) {
2606                    d = Direction.CLOCKWISE[i];
2607                    summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2608                            shown, startColor, endColor, duration, null);
2609                }
2610                d = Direction.CLOCKWISE[7];
2611                summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2612                        shown, startColor, endColor, duration, postRunnable);
2613                break;
2614            case CIRCLE:
2615            case SPHERE:
2616                float xf = worldX(x), yf = worldY(y);
2617                for (int i = 0; i < 4; i++) {
2618                    d = Direction.DIAGONALS[i];
2619                    summon(delay, xf, yf, xf - d.deltaX * font.actualCellWidth * distance * 0.7071067811865475f, // the constant is 1.0 / Math.sqrt(2.0)
2620                            yf + d.deltaY * font.actualCellHeight * distance * 0.7071067811865475f,
2621                            shown, startColor, endColor, duration, null);
2622                }
2623                // break intentionally absent
2624            default:
2625                for (int i = 0; i < 3; i++) {
2626                    d = Direction.CARDINALS_CLOCKWISE[i];
2627                    summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2628                            shown, startColor, endColor, duration, null);
2629                }
2630                d = Direction.CARDINALS_CLOCKWISE[3];
2631                summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2632                        shown, startColor, endColor, duration, postRunnable);
2633                break;
2634        }
2635    }
2636    /**
2637     * Convenience method to produce an explosion, splash, or burst effect. Calls
2638     * {@link #summon(int, int, int, int, char, float, float, float)} repeatedly with
2639     * different parameters. As with summon(), this creates temporary Glyphs that change color and position.
2640     * The distance is how many cells away to move the created Actors away from (x,y). The measurement determines
2641     * whether this produces Glyphs in 4 (cardinal) directions for DIAMOND or OCTAHEDRON, or 8 (cardinal and diagonal)
2642     * directions for any other enum value for Radius; CIRCLE and SPHERE will position the 8 glyphs in a circle, while
2643     * SQUARE and CUBE will position their 8 glyphs in a square.
2644     * <br>
2645     * Unlike {@link SquidPanel#burst(float, int, int, int, boolean, char, Color, Color, boolean, float, float)}, this
2646     * does not rotate the individual Glyphs it produces.
2647     * @param x the starting, center, x-position in cells to create all Actors at
2648     * @param y the starting, center, y-position in cells to create all Actors at
2649     * @param distance how far away, in cells, to move each actor from the center (Chebyshev distance, forming a square)
2650     * @param measurement a Radius enum that determines if 4 (DIAMOND, OCTAHEDRON) or 8 (anything else) Glyphs are
2651     *                    created, and the shape they will take
2652     * @param choices a String or other CharSequence containing the chars this can randomly choose
2653     * @param startColor the encoded color to start the effect with
2654     * @param endColor the encoded color to end the effect on
2655     * @param duration how long, in seconds, the effect should last
2656     */
2657    public void burst(int x, int y, int distance, Radius measurement, CharSequence choices,
2658                      float startColor, float endColor, float duration) {
2659        burst(0f, x, y, distance, measurement, choices, startColor, endColor, duration, null);
2660    }
2661    /**
2662     * Convenience method to produce an explosion, splash, or burst effect. Calls
2663     * {@link #summon(int, int, int, int, char, float, float, float)} repeatedly with
2664     * different parameters. As with summon(), this creates temporary Glyphs that change color and position.
2665     * The distance is how many cells away to move the created Actors away from (x,y). The measurement determines
2666     * whether this produces Glyphs in 4 (cardinal) directions for DIAMOND or OCTAHEDRON, or 8 (cardinal and diagonal)
2667     * directions for any other enum value for Radius; CIRCLE and SPHERE will position the 8 glyphs in a circle, while
2668     * SQUARE and CUBE will position their 8 glyphs in a square. This takes a delay in seconds that can be used to make
2669     * the effect wait to start for some amount of time, and a Runnable that will be run once after the burst's full
2670     * duration completes (not once per summoned Glyph).
2671     * <br>
2672     * Unlike {@link SquidPanel#burst(float, int, int, int, boolean, char, Color, Color, boolean, float, float)}, this
2673     * does not rotate the individual Glyphs it produces.
2674     * @param delay how long to wait in seconds before starting the effect
2675     * @param x the starting, center, x-position in cells to create all Actors at
2676     * @param y the starting, center, y-position in cells to create all Actors at
2677     * @param distance how far away, in cells, to move each actor from the center (Chebyshev distance, forming a square)
2678     * @param measurement a Radius enum that determines if 4 (DIAMOND, OCTAHEDRON) or 8 (anything else) Glyphs are
2679     *                    created, and the shape they will take
2680     * @param choices a String or other CharSequence containing the chars this can randomly choose
2681     * @param startColor the encoded color to start the effect with
2682     * @param endColor the encoded color to end the effect on
2683     * @param duration how long, in seconds, the effect should last
2684     * @param postRunnable a Runnable to execute after the burst completes; may be null to do nothing,
2685     *                     and will only be run once for the whole burst effect.
2686     */
2687    public void burst(float delay, int x, int y, int distance, Radius measurement, CharSequence choices,
2688                      float startColor, float endColor, float duration, /* @Nullable */ Runnable postRunnable)
2689    {
2690        Direction d;
2691        final int len = choices.length();
2692        final StatefulRNG rng = DefaultResources.getGuiRandom();
2693        switch (measurement)
2694        {
2695            case SQUARE:
2696            case CUBE:
2697                for (int i = 0; i < 7; i++) {
2698                    d = Direction.CLOCKWISE[i];
2699                    summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2700                            choices.charAt(rng.nextIntHasty(len)), startColor, endColor, duration, null);
2701                }
2702                d = Direction.CLOCKWISE[7];
2703                summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2704                        choices.charAt(rng.nextIntHasty(len)), startColor, endColor, duration, postRunnable);
2705
2706                break;
2707            case CIRCLE:
2708            case SPHERE:
2709                float xf = worldX(x), yf = worldY(y);
2710                for (int i = 0; i < 4; i++) {
2711                    d = Direction.DIAGONALS[i];
2712                    summon(delay, xf, yf, xf - d.deltaX * font.actualCellWidth * distance * 0.7071067811865475f,
2713                            yf + d.deltaY * font.actualCellHeight * distance * 0.7071067811865475f,
2714                            choices.charAt(rng.nextIntHasty(len)), startColor, endColor, duration, null);
2715                }
2716                // break intentionally absent
2717            default:
2718                for (int i = 0; i < 3; i++) {
2719                    d = Direction.CARDINALS_CLOCKWISE[i];
2720                    summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2721                            choices.charAt(rng.nextIntHasty(len)), startColor, endColor, duration, null);
2722                }
2723                d = Direction.CARDINALS_CLOCKWISE[3];
2724                summon(delay, x, y, x - d.deltaX * distance, y + d.deltaY * distance,
2725                        choices.charAt(rng.nextIntHasty(len)), startColor, endColor, duration, postRunnable);
2726
2727                break;
2728        }
2729    }
2730
2731    /**
2732     * Changes the background at position x,y so it becomes the given color, taking duration seconds. The
2733     * background will keep the changed color after the effect, unless drawn over.
2734     * <br>
2735     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2736     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2737     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2738     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2739     * @param x the x-coordinate of the cell to recolor
2740     * @param y the y-coordinate of the cell to recolor
2741     * @param color what to gradually change the cell's color to, as a Color object
2742     * @param duration how long the total transition should take in seconds
2743     */
2744    public void recolor(final int x, final int y, final Color color, float duration) {
2745        recolor(0f, x, y, color.toFloatBits(), duration, null);
2746    }
2747
2748    /**
2749     * Changes the background at position x,y so it becomes the given encodedColor, taking duration seconds. The
2750     * background will keep the changed color after the effect, unless drawn over.
2751     * <br>
2752     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2753     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2754     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2755     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2756     * @param x the x-coordinate of the cell to recolor
2757     * @param y the y-coordinate of the cell to recolor
2758     * @param encodedColor what to gradually change the cell's color to, as a packed float
2759     * @param duration how long the total transition should take in seconds
2760     */
2761    public void recolor(final int x, final int y, final float encodedColor, float duration) {
2762        recolor(0f, x, y, encodedColor, duration, null);
2763    }
2764
2765    /**
2766     * Changes the background at position x,y so it becomes the given encodedColor, waiting for {@code delay} (in
2767     * seconds) before performing it, taking duration seconds. The background will keep the changed color after
2768     * the effect, unless drawn over.
2769     * <br>
2770     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2771     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2772     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2773     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2774     * @param delay how long to wait in seconds before starting the effect
2775     * @param x the x-coordinate of the cell to recolor
2776     * @param y the y-coordinate of the cell to recolor
2777     * @param encodedColor what to gradually change the cell's color to, as a packed float
2778     * @param duration how long the total transition should take in seconds
2779     */
2780    public void recolor(final float delay, final int x, final int y, final float encodedColor, float duration) {
2781        recolor(delay, x, y, encodedColor, duration, null);
2782    }
2783    /**
2784     * Changes the background at position x,y so it becomes the given encodedColor, waiting for {@code delay} (in
2785     * seconds) before performing it, taking duration seconds. The background will keep the changed color after the
2786     * effect, unless drawn over. Additionally, enqueue {@code postRunnable} for running after the created action ends.
2787     * <br>
2788     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2789     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2790     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2791     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2792     * @param delay how long to wait in seconds before starting the effect
2793     * @param x the x-coordinate of the cell to recolor
2794     * @param y the y-coordinate of the cell to recolor
2795     * @param encodedColor what to gradually change the cell's color to, as a packed float
2796     * @param duration how long the total transition should take in seconds
2797     * @param postRunnable a Runnable to execute after the recolor completes; may be null to do nothing.
2798     */
2799    public void recolor(final float delay, final int x, final int y, final float encodedColor, float duration,
2800            /* @Nullable */ Runnable postRunnable) {
2801        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight)
2802            return;
2803        duration = Math.max(0.015f, duration);
2804        final float ac = backgrounds[x][y];
2805        final int nbActions = 2 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2806        final Action[] sequence = new Action[nbActions];
2807        int index = 0;
2808        if (0 < delay)
2809            sequence[index++] = Actions.delay(delay);
2810        sequence[index++] = new TemporalAction(duration) {
2811            @Override
2812            protected void update(float percent) {
2813                backgrounds[x][y] = SColor.lerpFloatColors(ac, encodedColor, percent);
2814            }
2815        };
2816        if(postRunnable != null)
2817        {
2818            sequence[index++] = Actions.run(postRunnable);
2819        }
2820        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2821        sequence[index] = Actions.delay(duration, Actions.run(new Runnable() {
2822            @Override
2823            public void run() {
2824                backgrounds[x][y] = encodedColor;
2825            }
2826        }));
2827
2828        addAction(Actions.sequence(sequence));
2829    }
2830
2831
2832
2833
2834    /**
2835     * Changes the foreground in the given layer at position x,y so it becomes the given color, taking duration
2836     * seconds. The foreground color will keep the changed color after the effect, unless drawn over.
2837     * <br>
2838     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2839     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2840     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2841     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2842     * @param x the x-coordinate of the cell to recolor
2843     * @param y the y-coordinate of the cell to recolor
2844     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2845     * @param color what to gradually change the cell's color to, as a Color object
2846     * @param duration how long the total transition should take in seconds
2847     */
2848    public void recolor(final int x, final int y, final int layer, final Color color, float duration) {
2849        recolor(0f, x, y, layer, color.toFloatBits(), duration, null);
2850    }
2851    /**
2852     * Changes the foreground in the given layer at position x,y so it becomes the given encodedColor, taking duration
2853     * seconds. The foreground color will keep the changed color after the effect, unless drawn over.
2854     * <br>
2855     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2856     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2857     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2858     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2859     * @param x the x-coordinate of the cell to recolor
2860     * @param y the y-coordinate of the cell to recolor
2861     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2862     * @param encodedColor what to gradually change the cell's color to, as a packed float
2863     * @param duration how long the total transition should take in seconds
2864     */
2865    public void recolor(final int x, final int y, final int layer, final float encodedColor, float duration) {
2866        recolor(0f, x, y, layer, encodedColor, duration, null);
2867    }
2868
2869
2870    /**
2871     * Changes the foreground in the given layer at position x,y so it becomes the given encodedColor, waiting for
2872     * {@code delay} (in seconds) before performing it, taking duration seconds. The foreground color will keep the 
2873     * changed color after the effect, unless drawn over.
2874     * <br>
2875     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2876     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2877     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2878     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2879     * @param delay how long to wait in seconds before starting the effect
2880     * @param x the x-coordinate of the cell to recolor
2881     * @param y the y-coordinate of the cell to recolor
2882     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2883     * @param encodedColor what to gradually change the cell's color to, as a packed float
2884     * @param duration how long the total transition should take in seconds
2885     */
2886    public void recolor(final float delay, final int x, final int y, final int layer, final float encodedColor, float duration) {
2887        recolor(delay, x, y, layer, encodedColor, duration, null);
2888    }
2889
2890
2891    /**
2892     * Changes the foreground in the given layer at position x,y so it becomes the given encodedColor, waiting for
2893     * {@code delay} (in seconds) before performing it, taking duration seconds. The foreground color will keep the 
2894     * changed color after the effect, unless drawn over. Additionally, enqueue {@code postRunnable} for running after
2895     * the created action ends.
2896     * <br>
2897     * This will only behave correctly if you call {@link Stage#act()} before you call {@link Stage#draw()}, but after
2898     * any changes to the contents of this SparseLayers. If you change the contents, then draw, and then act, that will
2899     * draw the contents without the recolor this applies, then apply the recolor when you call act(), then quickly
2900     * overwrite the recolor in the next frame. That visually appears as nothing happening other than a delay.
2901     * @param delay how long to wait in seconds before starting the effect
2902     * @param x the x-coordinate of the cell to recolor
2903     * @param y the y-coordinate of the cell to recolor
2904     * @param layer which layer to affect; if you haven't specified a layer when placing text, then this should be 0
2905     * @param encodedColor what to gradually change the cell's color to, as a packed float
2906     * @param duration how long the total transition should take in seconds
2907     * @param postRunnable a Runnable to execute after the recolor completes; may be null to do nothing.
2908     */
2909    public void recolor(final float delay, final int x, final int y, final int layer, final float encodedColor, float duration,
2910            /* @Nullable */ Runnable postRunnable) {
2911        if(x < 0 || x >= gridWidth || y < 0 || y >= gridHeight || layer < 0 || layer >= layers.size())
2912            return;
2913        final SparseTextMap l = layers.get(layer);
2914        duration = Math.max(0.015f, duration);
2915        final int pos = SparseTextMap.encodePosition(x, y);
2916        final float ac = l.getFloat(pos, 0f);
2917        final int nbActions = 2 + (0 < delay ? 1 : 0) + (postRunnable == null ? 0 : 1);
2918        final Action[] sequence = new Action[nbActions];
2919        int index = 0;
2920        if (0 < delay)
2921            sequence[index++] = Actions.delay(delay);
2922        sequence[index++] = new TemporalAction(duration) {
2923            @Override
2924            protected void update(float percent) {
2925                l.updateFloat(pos, SColor.lerpFloatColors(ac, encodedColor, percent));
2926            }
2927        };
2928        if(postRunnable != null)
2929        {
2930            sequence[index++] = Actions.run(postRunnable);
2931        }
2932        /* Do this one last, so that hasActiveAnimations() returns true during 'postRunnables' */
2933        sequence[index] = Actions.delay(duration, Actions.run(new Runnable() {
2934            @Override
2935            public void run() {
2936                l.updateFloat(pos, encodedColor);
2937            }
2938        }));
2939        addAction(Actions.sequence(sequence));
2940    }
2941
2942    /**
2943     * Draws the SparseLayers and all glyphs it tracks. {@link Batch#begin()} must have already been called on the
2944     * batch, and {@link Batch#end()} should be called after this returns and before the rendering code finishes for the
2945     * frame.
2946     * <br>
2947     * This will set the shader of {@code batch} if using a distance field or MSDF font and the shader is currently not
2948     * configured for such a font; it does not reset the shader to the default so that multiple Actors can all use the
2949     * same shader and so specific extra glyphs or other items can be rendered after calling draw(). If you need to draw
2950     * both a distance field font and full-color art, you should set the shader on the Batch to null when you want to
2951     * draw full-color art, and end the Batch between drawing this object and the other art.
2952     *
2953     * @param batch a Batch such as a {@link FilterBatch} that must be between a begin() and end() call; usually done by Stage
2954     * @param parentAlpha currently ignored
2955     */
2956    @Override
2957    public void draw(Batch batch, float parentAlpha) {
2958        super.draw(batch, parentAlpha);
2959        float xo = getX(), yo = getY(), yOff = yo + 1f + gridHeight * font.actualCellHeight, gxo, gyo;
2960        font.draw(batch, backgrounds, xo, yo);
2961        int len = layers.size();
2962        Frustum frustum = null;
2963        Stage stage = getStage();
2964        if(stage != null) {
2965            Viewport viewport = stage.getViewport();             
2966            if(viewport != null)
2967            {
2968                Camera camera = viewport.getCamera();
2969                if(camera != null)
2970                {
2971                    if(
2972                            camera.frustum != null &&
2973                                    (!camera.frustum.boundsInFrustum(xo, yOff - font.actualCellHeight - 1f, 0f, font.actualCellWidth, font.actualCellHeight, 0f) ||
2974                                     !camera.frustum.boundsInFrustum(xo + font.actualCellWidth * (gridWidth-1), yo, 0f, font.actualCellWidth, font.actualCellHeight, 0f))
2975                    )
2976                        frustum = camera.frustum;
2977                }
2978            }
2979        }
2980        font.configureShader(batch);
2981        if(frustum == null) {
2982            for (int i = 0; i < len; i++) {
2983                layers.get(i).draw(batch, font, xo, yOff);
2984            }
2985
2986        }
2987        else
2988        {
2989            for (int i = 0; i < len; i++) {
2990                layers.get(i).draw(batch, font, frustum, xo, yOff);
2991            }
2992        }
2993        
2994        int x, y;
2995        for (int i = 0; i < glyphs.size(); i++) {
2996            TextCellFactory.Glyph glyph = glyphs.get(i);
2997            if(glyph == null)
2998                continue;
2999            glyph.act(Gdx.graphics.getDeltaTime());
3000            if(
3001                    !glyph.isVisible() ||
3002                    (x = Math.round((gxo = glyph.getX() - xo) / font.actualCellWidth)) < 0 || x >= gridWidth ||
3003                    (y = Math.round((gyo = glyph.getY() - yo)  / -font.actualCellHeight + gridHeight)) < 0 || y >= gridHeight ||
3004                    backgrounds[x][y] == 0f || (frustum != null && !frustum.boundsInFrustum(gxo, gyo, 0f, font.actualCellWidth, font.actualCellHeight, 0f)))
3005                continue;
3006            glyph.draw(batch, 1f);
3007        }
3008    }
3009}