Hotfix: (from @pb4) a dead player isn't granted an early termination
[troll.git] / src / main / java / com / codingame / game / View.java
1 package com.codingame.game;
2
3 import java.util.Random;
4 import java.util.ArrayList;
5 import java.util.Comparator;
6
7 import com.codingame.gameengine.core.GameManager;
8 import com.codingame.gameengine.core.MultiplayerGameManager;
9 import com.codingame.gameengine.module.entities.GraphicEntityModule;
10 import com.codingame.gameengine.module.entities.Rectangle;
11 import com.codingame.gameengine.module.entities.Sprite;
12 import com.codingame.gameengine.module.entities.SpriteAnimation;
13 import com.codingame.gameengine.module.entities.Text;
14 import com.codingame.gameengine.module.entities.TextBasedEntity;
15 import com.codingame.gameengine.module.entities.Group;
16 import com.codingame.gameengine.module.entities.Curve;
17 import com.google.inject.Inject;
18
19 class View {
20     @Inject private MultiplayerGameManager<com.codingame.game.Player> gameManager;
21     @Inject private GraphicEntityModule graphicEntityModule;
22     @Inject PantsModule pantsModule;
23
24     /*
25      * Frame timings, for a base frame length of 2s:
26      *   - first half: stone throw
27      *   - second half: troll move
28      * The troll message is anchored around the troll move.
29      */
30     private final double STONE_THROW_START = 0.0;
31     private final double STONE_THROW_PEAK = 0.25;
32     private final double STONE_THROW_END = 0.5;
33     private final double TROLL_MOVE_START = 0.5;
34     private final double TROLL_MOVE_END = 1.0;
35     private final double TROLL_MESSAGE_START = 0.5;
36     private final double TROLL_MESSAGE_END = 1.0;
37
38     /*
39      * Castle destruction and endgame message pertain to an endgame
40      * frame only.
41      */
42     private final double AVATAR_ANIMATION_START = 0.5;
43     private final double CASTLE_DESTRUCTION_START = 0.0;
44     private final double CASTLE_DESTRUCTION_END = 0.5;
45
46     class Player {
47         Model.Player model;
48
49         int colorToken;
50         String nicknameToken;
51         String avatarToken;
52
53         double frameRot;
54
55         Group avatar;
56         Text stoneCounter;
57         Text message;
58         Sprite castle;
59         Text stone;
60         Text stoneReminder;
61
62         void init(com.codingame.game.Player p) {
63             model = p.model;
64             colorToken = p.getColorToken();
65             nicknameToken = p.getNicknameToken();
66             avatarToken = p.getAvatarToken();
67
68             boolean p0 = model.index == 0;
69             int x = p0 ? 280 : 1920 - 280;
70             int y = 220;
71
72             Sprite frame = graphicEntityModule.createSprite()
73                 .setImage("frame.png")
74                 .setAnchor(0.5)
75                 .setRotation(frameRot)
76                 .setZIndex(22)
77                 .setTint(colorToken);
78
79             Sprite frameBg = graphicEntityModule.createSprite()
80                 .setImage("frame_bg.png")
81                 .setAnchor(0.5)
82                 .setRotation(frameRot)
83                 .setZIndex(20);
84
85             Sprite avatarSprite = graphicEntityModule.createSprite()
86                 .setZIndex(21)
87                 .setImage(avatarToken)
88                 .setAnchor(0.5)
89                 .setBaseHeight(116)
90                 .setBaseWidth(116);
91
92             avatar = graphicEntityModule
93                 .createGroup(frame, frameBg, avatarSprite)
94                 .setX(x).setY(y);
95
96             Text text = graphicEntityModule.createText(nicknameToken)
97                     .setX(x)
98                     .setY(y + 120)
99                     .setZIndex(20)
100                     .setFontSize(40)
101                     .setFillColor(0x7f3f00)
102                     .setAnchor(0.5);
103
104             stoneCounter = graphicEntityModule.createText()
105                 .setX(x)
106                 .setY(y+200)
107                 .setZIndex(20)
108                 .setFontSize(40)
109                 .setFillColor(0x7f3f00)
110                 .setAnchor(0.5);
111             updateStoneCounter();
112
113             message = graphicEntityModule.createText()
114                 .setX(p0 ? 15 : 1920-15)
115                 .setY(680)
116                 .setZIndex(1)
117                 .setFontSize(40)
118                 .setFillColor(0xffbf7f)
119                 .setAnchorX(p0 ? 0 : 1)
120                 .setAnchorY(1);
121
122             castle = graphicEntityModule.createSprite()
123                 .setImage("castle.png")
124                 .setTint(colorToken)
125                 .setX(p0 ? 160 : 1920-160)
126                 .setY(p0 ? 890 : 880)
127                 .setZIndex(1)
128                 .setAnchorX(0.5)
129                 .setAnchorY(1)
130                 .setScaleX(p0 ? 1 : -1);
131
132             stone = graphicEntityModule.createText()
133                 .setZIndex(3)
134                 .setFontSize(150)
135                 .setFillColor(0x12322a)
136                 .setAnchor(0.5)
137                 .setAlpha(0);
138
139             stoneReminder = graphicEntityModule.createText()
140                 .setX(p0 ? x + 100 : x - 100)
141                 .setY(y)
142                 .setZIndex(20)
143                 .setFontSize(80)
144                 .setFontFamily("monospace")
145                 .setStrokeColor(0xff0080)
146                 .setFillColor(0xff0080)
147                 .setAnchorX(p0 ? 0 : 1)
148                 .setAnchorY(0.5);
149             pantsModule.displayOnToggleState(stoneReminder, "debug", true);
150         }
151
152         void startTurn() {
153             graphicEntityModule.commitEntityState(0.0, stoneReminder);
154         }
155
156         void victory() {
157             gameManager.addToGameSummary(GameManager.formatSuccessMessage(nicknameToken + " wins."));
158             View.this.endgameFrame();
159             markWinner();
160         }
161
162         void defeat() {
163             gameManager.addToGameSummary(GameManager.formatErrorMessage(trollRace.starter + " destroys " + nicknameToken + "."));
164             destroyCastle();
165         }
166
167         // ========== Player/avatar markings
168
169         void markTimeout() {
170             animateLoss(avatar.getX(), avatar.getY(), 100, "SLOW\nPOKE");
171         }
172
173         void markIllegal() {
174             animateLoss(avatar.getX(), avatar.getY(), 100, "STUPID");
175         }
176
177         void markCheat() {
178             animateLoss(avatar.getX(), avatar.getY(), 100, "CHEATER");
179         }
180
181         void markWinner() {
182             graphicEntityModule.commitEntityState(AVATAR_ANIMATION_START, avatar);
183             avatar.setScaleX(1.5, Curve.EASE_OUT);
184             avatar.setScaleY(1.5, Curve.EASE_OUT);
185             avatar.setRotation((random.nextDouble() - 0.5) * Math.PI / 18,
186                                Curve.ELASTIC);
187         }
188
189         void markLoser() {
190             graphicEntityModule.commitEntityState(AVATAR_ANIMATION_START, avatar);
191             int dir = random.nextInt(2) == 1 ? 1 : -1;
192             avatar.setRotation(dir * 170 * Math.PI / 180, Curve.ELASTIC);
193         }
194
195         // ==========Player/stones
196
197         void throwStones(int stones) {
198             gameManager.addToGameSummary(String.format("%s throws %d stone%s at " + trollRace.nonStarter(), nicknameToken, stones, stones == 1 ? "" : "s"));
199         }
200
201         void threwMoreStonesThanHad() {
202             gameManager.addToGameSummary(GameManager.formatErrorMessage(nicknameToken + " tried to throw more stones than they had.  I'll let it slide for this time.  (But not let them throw that much!)"));
203         }
204
205         void failedToThrowStonesAndShouldHave() {
206             gameManager.addToGameSummary(GameManager.formatErrorMessage(nicknameToken + " tried not throwing any stones.  Fixing that for them because I'm in a good mood today."));
207         }
208
209         void updateStoneCounter() {
210             int stones = model.getStones();
211             if (stones <= 0) {
212                 stoneCounter.setText("Out of stones!");
213                 stoneCounter.setFillColor(0xff7777);
214             }
215             else if (stones == 1) {
216                 stoneCounter.setText("1 stone");
217                 stoneCounter.setFillColor(0xffbb77);
218             }
219             else {
220                 stoneCounter.setText(stones + " stones");
221             }
222             graphicEntityModule.commitEntityState(STONE_THROW_PEAK, stoneCounter);
223         }
224
225         void animateStones(int stones) {
226             String stonesString = Integer.valueOf(stones).toString();
227             stone.setX(castle.getX());
228             stone.setY(castle.getY() - 100);
229             stone.setText(stonesString);
230             stone.setAlpha(1);
231             graphicEntityModule.commitEntityState(STONE_THROW_START, stone);
232     
233             int peakX = (castle.getX() + troll.getX()) / 2;
234             int peakY = 540;
235             stone.setX(peakX);
236             stone.setY(peakY, Curve.EASE_OUT);
237             graphicEntityModule.commitEntityState(STONE_THROW_PEAK,
238                                                   stone,
239                                                   stoneCounter);
240     
241             stone.setX(troll.getX());
242             stone.setY(troll.getY() - 50, Curve.EASE_IN);
243             stone.setAlpha(0, Curve.EASE_IN);
244             graphicEntityModule.commitEntityState(STONE_THROW_END, stone);
245
246             stoneReminder.setText(stonesString);
247             graphicEntityModule.commitEntityState(0.0, stoneReminder);
248         }
249
250         // ========== Player/castle
251
252         void displayMessage(String msg) {
253             message.setText(msg);
254             graphicEntityModule.commitEntityState(0.0, message);
255         }
256
257         void destroyCastle() {
258             graphicEntityModule.commitEntityState(CASTLE_DESTRUCTION_START, castle);
259             castle.setX(castle.getX(), Curve.ELASTIC);
260             castle.setScaleY(-0.2, Curve.EASE_IN);
261             graphicEntityModule.commitEntityState(CASTLE_DESTRUCTION_END, castle);
262         }
263     } // class Player
264
265     Model model;
266     Random random = new Random();
267     Text trollMessage;
268     Group troll;
269     Text trollPositionGauge;
270     Player p0 = new Player(), p1 = new Player();
271     Text turnCounter; int _turns = 0;
272
273     // ==================== Referee interface
274
275     void init(Model m) {
276         model = m;
277         drawBackground();
278
279         /*
280          * Random π/2-grained rotation of the avatar frames.  Avoid
281          * having them π/2 apart, though, as one of them is likely
282          * going to end upside-down and the trick would be revealed.
283          * And I'd have to "draw" a new frame. Ewww.
284          */
285         p0.frameRot = random.nextInt(4) * Math.PI / 2;
286         p0.init(gameManager.getPlayer(0));
287         p1.frameRot = p1.frameRot +
288             (random.nextInt(2) == 1 ? 1 : -1) * Math.PI / 2;
289         p1.init(gameManager.getPlayer(1));
290
291         drawTroll();
292
293         drawDebug();
294     }
295
296     void startTurn() {
297         p0.startTurn();
298         p1.startTurn();
299
300         trollMessage.setX(troll.getX());
301
302         animateTurnCounter();
303     }
304
305     void endgameFrame() {
306         gameManager.setFrameDuration(2000);
307     }
308
309     void doubleDefeat() {
310         gameManager.addToGameSummary(GameManager.formatErrorMessage("Everybody loses!"));
311         endgameFrame();
312         animateLoss(1920/2, 680, 150, "L0SERZ!");
313     }
314
315     void draw() {
316         gameManager.addToGameSummary("Draw.");
317         endgameFrame();
318         animateLoss(1920/2, 680, 200, "DRAW");
319     }
320
321     // drawBackground() helper class
322     // @Java nerds: is this avoidable?
323     private class Pos {
324         int x, y;
325         Pos(int _x, int _y) { x = _x; y = _y; }
326     }
327
328     private void drawBackground() {
329         graphicEntityModule.createSprite()
330                 .setImage("background.png")
331                 .setAnchor(0);
332
333         int numMountains = random.nextInt(5);
334         while (numMountains --> 0) {
335             final int pngWidth = 366;
336             double scale = 0.5 * (1 + random.nextDouble());
337             int x = random.nextInt(1920 + (int) (scale*pngWidth))
338                 - (int) (scale*pngWidth/2);
339             int baseTint = 64 + random.nextInt(128);
340             Sprite mountain = graphicEntityModule.createSprite()
341                 .setImage("mountain.png")
342                 .setX(x)
343                 .setY(680)
344                 .setAnchorX(0.5)
345                 .setAnchorY(283.0 / 321.0)
346                 .setRotation((random.nextDouble() - 0.5) * Math.PI / 1800)
347                 .setScaleX(random.nextInt(2) == 0 ? scale : -scale)
348                 .setScaleY(scale * (1 + (random.nextDouble() - 0.5) / 2))
349                 .setSkewX((random.nextDouble() - 0.5) / 4)
350                 .setSkewY((random.nextDouble() - 0.5) / 8)
351                 .setTint((baseTint + random.nextInt(16) - 8) * 0x010000
352                          + (baseTint + random.nextInt(16) - 8) * 0x0100
353                          + (baseTint + random.nextInt(16) - 8) * 0x01);
354             graphicEntityModule.createSprite().setImage("mountaintop.png")
355                 .setX(mountain.getX())
356                 .setY(mountain.getY())
357                 .setAnchorX(mountain.getAnchorX())
358                 .setAnchorY(mountain.getAnchorY())
359                 .setRotation(mountain.getRotation())
360                 .setScaleX(mountain.getScaleX())
361                 .setScaleY(mountain.getScaleY())
362                 .setSkewX(mountain.getSkewX())
363                 .setSkewY(mountain.getSkewY());
364         }
365
366         int numTrees = random.nextInt(21);
367         ArrayList<Pos> poss = new ArrayList<Pos>(numTrees);
368         while (numTrees --> 0) {
369             int x, y;
370             do {
371                 x = random.nextInt(1920);
372                 // yes, this biases randomness wrt perspective! :-(
373                 y = 700 + random.nextInt(175);
374             } while (y > 880 && (x < 200 || x > 1720));
375             poss.add(new Pos(x, y));
376         }
377         poss.sort(new Comparator<Pos>() {
378                 public int compare(Pos a, Pos b) { return a.y < b.y ? -1 : 1; }
379             });
380
381         for (Pos p : poss) {
382             double scale = ( 90.0 / 433.0           // base height from PNG
383                              * (p.y - 680) / (875 - 680) ); // perspective
384             graphicEntityModule.createSprite()
385                 .setImage(random.nextInt(2) == 0 ? "Alshockv1.png"
386                                                  : "Alshockv2.png")
387                 .setAnchorX(0.5)
388                 .setAnchorY(1)
389                 .setX(p.x)
390                 .setY(p.y)
391                 .setScaleX(scale * (random.nextInt(2) == 0 ? -1 : 1)
392                            * (1 + (random.nextDouble() - 0.5) / 6))
393                 .setScaleY(scale * (1 + (random.nextDouble() -0.5) / 6))
394                 .setRotation((random.nextDouble() - 0.5) * Math.PI / 1800)
395                 .setSkewX((random.nextDouble() - 0.5) /4)
396                 .setSkewY((random.nextDouble() - 0.5) /8);
397         }
398
399         // base png: 514×387
400         Sprite f7u12 = graphicEntityModule.createSprite()
401             .setImage("f7u12.png")
402             .setX(1920 / 2)
403             .setY(1080 / 2)
404             .setAnchorX(0.5)
405             .setAnchorY(0.5)
406             .setBaseWidth(514*1080/387)
407             .setBaseHeight(1080)
408             .setZIndex(200);
409         pantsModule.displayOnToggleState(f7u12, "troll", true);
410     }
411
412     // ==================== Troll
413
414     enum TrollRace {
415         Troll("The troll", 0xfac200, "bland"),
416         IceTroll("The ice troll", 0x59a2a2, "ice"),
417         RockTroll("The rock troll", 0x78877f, "rock"),
418         WaterTroll("The water troll", 0x2b2fc6, "water"),
419         OlogHai("The Olog-Hai", 0x5b2e7d, "ologhai");
420         String starter, parser; int tint;
421         TrollRace(String s, int t, String p) {
422             starter = s;
423             tint = t;
424             parser = p;
425         }
426         String nonStarter() {
427             return Character.toLowerCase(starter.charAt(0))
428                 + starter.substring(1);
429         }
430     }
431     TrollRace trollRace;
432
433     private void drawTroll() {
434         int r, league = gameManager.getLeagueLevel();
435         if (league <= 1) r = 4;
436         else if (league <= 2) r = 8;
437         else r = 10;
438
439         r = random.nextInt(r);
440         if (r < 4) trollRace = TrollRace.Troll;
441         else if (r < 6) trollRace = TrollRace.IceTroll;
442         else if (r < 8) trollRace = TrollRace.RockTroll;
443         else if (r < 9) trollRace = TrollRace.WaterTroll;
444         else if (r < 10) trollRace = TrollRace.OlogHai;
445         else throw new RuntimeException("Internal error: unknown troll race " + r);
446
447         // We read it for debugging purposes, but don't echo it back
448         // to the IDE.  It is, after all, *not* a map parameter!
449         String buf = gameManager.getGameParameters().getProperty("ehtnicity");
450         if (buf != null) {
451             String key = "";
452             for (char c : buf.toCharArray())
453                 if (Character.isLetter(c))
454                     key += Character.toLowerCase(c);
455             iHateJava: do {
456                 for (TrollRace race : TrollRace.values()) {
457                     if (key.equals(race.parser)) {
458                         trollRace = race;
459                         break/*ing news: */ iHateJava;
460                     }
461                 }
462                 gameManager.addToGameSummary("Ignoring unknown troll race: " + buf);
463             } while (false);
464         }
465         photoFinish: ; // The race is through, but Java has no goto :-(
466
467         Sprite trollBody = graphicEntityModule.createSprite()
468             .setImage("troll_body.png")
469             .setAnchorX(0.5)
470             .setAnchorY(1)
471             .setTint(trollRace.tint);
472         Sprite trollPantsRed = graphicEntityModule.createSprite()
473             .setImage("pants_red.png")
474             .setAnchorX(0.5)
475             .setAnchorY(1);
476         pantsModule.displayOnPantsState(trollPantsRed, 1);
477         Sprite trollPantsGreen = graphicEntityModule.createSprite()
478             .setImage("pants_green.png")
479             .setAnchorX(0.5)
480             .setAnchorY(1);
481         pantsModule.displayOnPantsState(trollPantsGreen, 2);
482         Sprite trollPantsBlue = graphicEntityModule.createSprite()
483             .setImage("pants_blue.png")
484             .setAnchorX(0.5)
485             .setAnchorY(1);
486         pantsModule.displayOnPantsState(trollPantsBlue, 3);
487         Sprite trollPantsPerv = graphicEntityModule.createSprite()
488             .setImage("pants_perv.png")
489             .setAnchorX(0.5)
490             .setAnchorY(1);
491         pantsModule.displayOnPantsState(trollPantsPerv, 4);
492         troll = graphicEntityModule
493             .createGroup(trollBody, trollPantsRed,
494                          trollPantsGreen, trollPantsBlue, trollPantsPerv)
495             .setX(1920/2)
496             .setY(880)
497             .setScaleX(random.nextInt(2) == 0 ? 1 : -1)
498             .setZIndex(2);
499         trollPositionGauge = graphicEntityModule.createText()
500             .setZIndex(2)
501             .setAnchor(0.5)
502             .setFontSize(40)
503             .setX(1980/2)
504             .setY(980)
505             .setFillColor(0xffffff);
506         moveTroll();
507
508         trollMessage = graphicEntityModule.createText()
509             .setZIndex(1)
510             .setX(1902/2)
511             .setY(680)
512             .setAnchorX(0.5)
513             .setAnchorY(0)
514             .setTextAlign(TextBasedEntity.TextAlign.CENTER)
515             .setStrokeColor(0xFFFF00)
516             .setFillColor(0xFFFF00)
517             .setFontSize(40);
518         pantsModule.displayOnToggleState(trollMessage, "verboseTrolling", true);
519     }
520
521     void moveTroll() {
522         graphicEntityModule.commitEntityState(TROLL_MOVE_START, troll, trollPositionGauge);
523         int x0 = p0.castle.getX(), x1 = p1.castle.getX();
524         int y0 = p0.castle.getY(), y1 = p1.castle.getY();
525         troll.setX(x0 + model.trollPosition * (x1-x0) / model.roadLength,
526                    Curve.ELASTIC);
527         troll.setY(y0 + model.trollPosition * (y1-y0) / model.roadLength,
528                    Curve.ELASTIC);
529
530         trollPositionGauge.setX((trollPositionGauge.getX() + troll.getX()) / 2);
531         int distLeft = model.trollPosition;
532         int distRight = model.roadLength - model.trollPosition;
533         if (distLeft <= 0) {
534             trollPositionGauge.setText("← " + distRight);
535         }
536         else if (distRight <= 0) {
537             trollPositionGauge.setText(distLeft + " →");
538         }
539         else {
540             trollPositionGauge.setText(distLeft + " ↔ " + distRight);
541         }
542         final double moveMid = (TROLL_MOVE_START + TROLL_MOVE_END) / 2;
543         graphicEntityModule.commitEntityState(moveMid, trollPositionGauge);
544         trollPositionGauge.setX(troll.getX());
545     }
546
547     enum Dir {
548         LEFT("walks left.", 0),
549         STILL("stands still.", 1),
550         RIGHT("walks right.", 2);
551
552         String movement; int index;
553         Dir(String mvt, int i) { movement = mvt; index = i; }
554     }
555
556     void moveTroll(Dir d) {
557         moveTroll();
558         gameManager.addToGameSummary(trollRace.starter + " " + d.movement);
559
560         trollMessage.setText(selectTrollMessage(d)).setAlpha(1, Curve.NONE);
561         graphicEntityModule.commitEntityState(TROLL_MESSAGE_START, trollMessage);
562         trollMessage.setAlpha(0, Curve.EASE_IN);
563         graphicEntityModule.commitEntityState(TROLL_MESSAGE_END, trollMessage);
564     }
565
566     String selectTrollMessage(Dir d) {
567         if (random.nextInt(10000) == 0) {
568             return TrollText.specials[random.nextInt(TrollText.specials.length)];
569         }
570
571         // yup, still biased
572         int i = random.nextInt(TrollText.directed.length + TrollText.isotropic.length / 3);
573         if (i < TrollText.directed.length) {
574             return TrollText.directed[i][d.index];
575         }
576         else {
577             return TrollText.isotropic[random.nextInt(TrollText.isotropic.length)];
578         }
579     }
580
581     // ==================== Debug information
582
583     void animateTurnCounter() {
584         for (int i = 0; i < 10; i++) {
585             turnCounter.setText("T" + _turns + "." + i);
586             // The following line is likely not a bug.
587             graphicEntityModule.commitEntityState((double) i/9, turnCounter);
588         }
589         _turns++;
590     }
591
592     void drawDebug() {
593         String[] debugModePngs = graphicEntityModule.createSpriteSheetSplitter()
594             .setSourceImage("debug.png")
595             .setImageCount(2)
596             .setWidth(900)
597             .setHeight(150)
598             .setOrigRow(0)
599             .setOrigCol(0)
600             .setImagesPerRow(1)
601             .setName("debug")
602             .split();
603         SpriteAnimation debugMode = graphicEntityModule.createSpriteAnimation()
604             .setImages(debugModePngs)
605             .setX(1920 / 2)
606             .setY(60)
607             .setAnchorX(0.5)
608             .setLoop(true);
609         pantsModule.displayOnToggleState(debugMode, "debug", true);
610
611         turnCounter = graphicEntityModule.createText()
612             .setAnchorX(0.5)
613             .setAnchorY(0)
614             .setX(1920 / 2)
615             .setY(260)
616             .setStrokeColor(0xff0080)
617             .setFillColor(0xff0080)
618             .setFontFamily("monospace")
619             .setFontWeight(Text.FontWeight.BOLD)
620             .setFontSize(100);
621         pantsModule.displayOnToggleState(turnCounter, "debug", true);
622         animateTurnCounter();
623     }
624
625     // ==================== Endgame status
626
627     void animateLoss(int x, int y, int size, String message) {
628         int startX;
629         if (x < 1920/2) { startX = 1920; }
630         else if (x > 1920/2) { startX = 0; }
631         else { startX = 1920 * random.nextInt(2); }
632
633         Text msg = graphicEntityModule.createText(message)
634             .setX(startX)
635             .setY(1080)
636             .setAnchorX(0.5)
637             .setAnchorY(0.5)
638             .setScaleX(3*random.nextDouble() - 1)
639             .setScaleY(3*random.nextDouble() - 1)
640             .setSkewX(2*random.nextDouble() - 1)
641             .setSkewY(2*random.nextDouble() - 1)
642             .setRotation(4*Math.PI * (1 + random.nextDouble())
643                          * (random.nextInt(2) == 0 ? 1 : -1))
644             .setFontSize(0)
645             .setStrokeColor(0xff7f7f)
646             .setFillColor(0xff7f7f)
647             .setFontWeight(Text.FontWeight.BOLD)
648             .setTextAlign(TextBasedEntity.TextAlign.CENTER);
649         graphicEntityModule.commitEntityState(0.0, msg);
650         Curve curve = Curve.ELASTIC;
651         msg.setX(x, Curve.EASE_OUT)
652             .setY(y, Curve.ELASTIC)
653             .setScaleX(1, curve)
654             .setScaleY(1, curve)
655             .setSkewX(0, curve)
656             .setSkewY(0, curve)
657             .setRotation(2*Math.PI * (random.nextDouble() - 0.5), Curve.LINEAR)
658             .setFontSize(size, curve);
659     }
660 }