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