-
Notifications
You must be signed in to change notification settings - Fork 0
/
GameWindow.java
598 lines (496 loc) · 21.5 KB
/
GameWindow.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
/**
GameWindow.java
This is the main JPanel that is in the JFrame game. It contains additional JPanels, which are each GraphicsBaseFrame, that hold text and other information.
The layout of the JFrame is a FlowLayout.LEFT, as is this, the game window.
This class handles the painting and the timer of the game.
@author Peter Olson
@version 06/10/19
*/
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
import java.util.ArrayList;
import java.awt.event.*;
import javax.swing.Timer;
import java.awt.Point;
import java.awt.Point;
import java.awt.Color;
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Map;
import java.util.List;
import javax.swing.border.EmptyBorder;
import java.awt.Insets;
import java.awt.Toolkit;
import javax.swing.BorderFactory;
import java.awt.Robot;
import java.awt.AWTException;
public class GameWindow extends JPanel implements Runnable {
private static final long serialVersionUID = 1L;
private Thread thread;
private boolean running;
private int FPS = 60;
private long targetTime = 1000/FPS;
private Timer timer;
private Digraph<String> screenInputTextFileDigraph; //holds the names of the text files as pairs of vertices, creating directed paths. The first listed text file points to the second in the same line
private final String BRANCHES_FILE = "branches.txt";
private final String TEXT_BOX = "<textbox>";
private final double TEXT_BOX_LOC_X = 20.0;
private final double TEXT_BOX_LOC_Y = 1.2;
private final boolean HACKER_MODE = false;
private final String HACKER_MODE_FIRST_SCREEN = "ch0b.txt";
private final String REPEATED_SCREEN_TAG = "@repeated"; //tag for screen text files to tell whether this screen is used multiple times in different places within the screen plotline
private static final int WIDTH = (int)Toolkit.getDefaultToolkit().getScreenSize().getWidth(); //1904 -- reg size
private static final int HEIGHT = (int)Toolkit.getDefaultToolkit().getScreenSize().getHeight(); //1008 -- reg size
private final int X_PAD = 10;
private final int Y_PAD = 10;
/**
Creates the game window jpanel in the JFrame
*/
public GameWindow(){
super();
setPreferredSize( new Dimension( WIDTH, HEIGHT ) );
setLayout( null );
setBounds( 0, 0, WIDTH, HEIGHT );
setOpaque( true );
setBackground( Color.BLACK );
//@@DEBUG
//setBorder( BorderFactory.createLineBorder( Color.RED ) );
setFocusable( true );
requestFocus();
init();
}
/**
Invoked when GameStart is added to the screen. This method in turn invokes the run() method
That is, thread.start() calls the run() method
@see JComponent.addNotify();
@see Thread.start();
*/
public void addNotify() {
super.addNotify();
if ( thread == null ) {
thread = new Thread( this );
thread.start();
}
running = true;
}
/**
Paints the components within the JPanel
@param g The graphics object used to paint
@see JComponent.paintComponent( Graphics g )
@see repaint()
*/
public void paintComponent ( Graphics g ) {
super.paintComponent( g );
repaint();
}
/**
The initial setup of the game before the timer starts
@see setScreenBranches();
@see Container.add( Component comp, int index )
@see Timer.start()
*/
private void init() {
new State(); // Reset State variables
screenInputTextFileDigraph = new Digraph<String>();
setScreenBranches();
//Adds the first screen of the game
Map<String, List<String>> map = screenInputTextFileDigraph.getNeighbors();
String firstScreenTitle;
//Hacker mode is used for starting at a specific screen in the game
if( HACKER_MODE ) {
firstScreenTitle = HACKER_MODE_FIRST_SCREEN;
} else {
Map.Entry<String, List<String>> entry = map.entrySet().iterator().next();
firstScreenTitle = entry.getKey();
}
Screen firstScreen = setupScreen( firstScreenTitle );
State.currentScreen = firstScreen;
State.location = firstScreen.getCoord();
add( firstScreen );
Dimension size = new Dimension( WIDTH, HEIGHT );
firstScreen.setBounds( (int)(WIDTH/3.0), (int)(HEIGHT/4.0), size.width, size.height ); //mid x/2.5, y/4
addKeyListener( new MyKeyListener() );
timer = new javax.swing.Timer( FPS, new MoveListener() );
timer.start();
}
/**
This method is called at regular intervals according to the repaint() frame rate
*/
public void update() {
updateScreens();
}
/**
Remove the current screen and draw a new screen when an option is chosen
@see update()
@see Screen.getOptions()
@see ArrayList.get( int index )
@see setupScreen( String textFileName )
@see Screen.getCoord()
*/
private void updateScreens() {
if( !State.changeScreens )
return;
//@@DEBUG
/*
int debugOptNum = State.optionNumber;
Screen debugScreen = State.currentScreen;
Map<String, List<String>> debugMap = screenInputTextFileDigraph.getNeighbors();
String debugTextFileName = debugScreen.getTextFileName();
List<String> debugConnectedScreenTextFileNamesList = debugMap.get( debugTextFileName );
String debugNextScreenTextFileName = debugConnectedScreenTextFileNamesList.get( debugOptNum );
*/
remove( State.currentScreen );
String nextScreenTextFileName = screenInputTextFileDigraph.getNeighbors().get( State.currentScreen.getTextFileName() ).get( State.optionNumber );
Screen screen = setupScreen( nextScreenTextFileName );
State.currentScreen = screen;
State.location = screen.getCoord();
//removeAll(); //remove all JPanels within this JPanel
add( screen ); //add new Screen
Insets insets = getInsets();
Dimension size = screen.getPreferredSize();
screen.setBounds( X_PAD + insets.left, Y_PAD + insets.top, size.width, size.height );
repaint();
//Set focus to JTextField
if( State.currentScreen.hasTextBox() ) {
try {
Robot robot = new Robot();
robot.keyPress( KeyEvent.VK_ENTER );
robot.keyRelease( KeyEvent.VK_ENTER );
} catch( AWTException e ) {
e.printStackTrace();
}
}
State.changeScreens = false;
}
/**
This is the game loop
1. Updates the world, 2. Repaints the screen
@see System.nanoTime()
@see update()
@see Thread.sleep( long wait )
@see JPanel.repaint()
*/
@Override
public void run() {
long start, elapsed, wait;
while( running ) {
start = System.nanoTime();
update();
elapsed = System.nanoTime() - start;
wait = targetTime - elapsed / 1000000;
if( wait < 0 )
wait = 5;
try {
Thread.sleep( wait );
} catch ( Exception e ) {
e.printStackTrace();
}
repaint();
}
}
/**
Gets the map of screens (a digraph) -- this is the map of what screen connects to what screen --
also, recall that this is directed. Additionally, note that the order of screens added matters, as the
neighbors of a given vertex (screen) are stored in an ArrayList, which can then be processed based on order
@return Digraph<String> The map of screens
*/
public Digraph<String> getScreenMap() {
return screenInputTextFileDigraph;
}
/**
Sets the map of text files into a digraph
@see init()
@see Scanner.nextLine()
@see String.split( String delimiter )
@see Digraph.add( V from, V to )
@see Scanner.close();
*/
private void setScreenBranches() {
Scanner scanner = null;
//@@DEBUG
//SOPln( "Working Directory = " + System.getProperty( "user.dir" ) );
try {
scanner = new Scanner( new File( BRANCHES_FILE ) );
} catch( FileNotFoundException e ) {
e.printStackTrace();
}
while( scanner.hasNextLine() ) {
String screenPair = scanner.nextLine();
String[] screens = screenPair.split(",");
if( screens.length != 2 ) {
SOPln( "Error: invalid pair of screens" );
return;
}
screenInputTextFileDigraph.add( screens[0], screens[1] );
}
//@@DEBUG
//SOPln( screenInputTextFileDigraph.toString() );
scanner.close();
}
/**
Create a new Screen object by pulling info from the given text file
@param textFileName The name of the text file containing the information of the screen
@return Screen The screen object with the fields set
@see init()
@see updateScreens()
@see String.trim()
@see String.contains( String regex )
@see Scanner.hasNextLine()
@see Scanner.nextLine()
@see String.equals( Object obj )
*/
private Screen setupScreen( String textFileName ) {
textFileName = textFileName.trim();
if( !textFileName.contains(".txt") )
textFileName += ".txt";
//Since some screens are repeated and gone back to throughout the game, for instance s1.txt, set this variable to know to look for a the last non-repeating screen within the history of screens list
boolean repeated = false;
if( textFileName.contains( REPEATED_SCREEN_TAG ) )
repeated = true;
Scanner scanner = null;
try {
scanner = new Scanner( new File( textFileName ) );
} catch( FileNotFoundException e ) {
e.printStackTrace();
}
String text = "";
Coord location = null;
ArrayList<String> options = new ArrayList<String>();
boolean hasTextBox = false;
while( scanner.hasNextLine() ) {
//Grab text
String line = "";
while( !line.equals( "@@END" ) ) {
line = scanner.nextLine();
if( !line.equals( "@@END" ) )
text += line;
}
//Grab location
String locationLine = scanner.nextLine();
if( !locationLine.matches( "\\d,\\d" ) && !locationLine.equals( "unknown" ) ) {
SOPln( "Invalid location: " + textFileName );
return null;
} else if( locationLine.equals( "unknown" ) ) {
location = (Coord)State.variables.get( "Location" );
} else {
String[] coordParts = locationLine.split(",");
location = new Coord( Integer.valueOf( coordParts[0] ), Integer.valueOf( coordParts[1] ) );
State.variables.replace( "Location", location );
}
//Grab options
do {
line = scanner.nextLine();
if( line.equals( TEXT_BOX ) )
hasTextBox = true;
else {
//Check if option has if code
if( line.contains( "@@CODE" ) ) {
String codeText = line.substring( line.indexOf( "@@CODE" ) + "@@CODE".length(), line.lastIndexOf( "@@CODE" ) );
codeText = codeText.replaceAll( "\\|", "" );
if( !Screen.analyzeIfCode( codeText ) ) {}
else {
line = line.substring( line.lastIndexOf( "@@CODE" ) + "@@CODE".length() ); //Get rid of code stuff
options.add( line );
}
} else if( line.contains( "s1" ) ) { // Unique code set for repeated screens such as s1.txt
options = setS1Options( options );
} else {
options.add( line );
}
}
} while( scanner.hasNextLine() );
}
scanner.close();
Screen newScreen = new Screen( WIDTH, HEIGHT, textFileName.replaceAll( ".txt", "" ), text, location, options, hasTextBox, repeated );
State.screenHistory.add( newScreen ); //keep track of screen progression for looping and regularly used screens, such as s1.txt, among others
return newScreen;
}
/**
If reading the s1.txt file, manually set options
@param options The list of options to process
@return ArrayList<String> The finished list of options for s1.txt
@see setupScreen( String textFileName )
*/
private ArrayList<String> setS1Options( ArrayList<String> options ) {
options.add( "choose,select,pick,|1,inventory" );
options.add( "choose,select,pick,|2,stats" );
options.add( "choose,select,pick,|3,skills" );
options.add( "choose,select,pick,|4,quests" );
options.add( "choose,select,pick,|5,equipment" );
if( !State.className.equals("Hunter") ) {
options.add( "choose,select,pick,|6,shadows" );
}
return options;
}
/**
Based on the input into the JTextField, evaluate the next screen. If the input is not valid (does not lead to a new screen), do nothing
@param text The text entered into the JTextField
@param options The possible options recognized in the JTextField -- see text file
@return boolean True if the evaluation was a success and points to a new Screen, false if the text does not lead to a new screen
@see MyKeyListener.keyReleased( KeyEvent event )
@see ArrayList.get( int index )
@see String.contains( String regex )
@see String.split( String delimiter )
@see ArrayList.add( E e )
*/
private boolean evalTextBoxInput( String text, ArrayList<String> options ) {
boolean hasRightAt = false;
boolean isSuccess = false;
boolean hasVerb = false;
boolean hasNoun = false;
for( int i = 0; i < options.size(); i++ ) {
String[] verbsNouns = options.get(i).split( "\\|" );
//@@NOTE: options with multi-options (@s) should come before single option entries
if( verbsNouns[1].contains( "@" ) ) {
hasRightAt = true;
verbsNouns[1] = verbsNouns[1].replaceAll( "@", "," );
}
String[] verbs = verbsNouns[0].split( ",", -1 ); //Allows for empty strings. ie. Allows for valid entries that don't require both a noun and a vowel
String[] nouns = verbsNouns[1].split( ",", -1 ); //eg. take,grab,|jacket,sweater ---> an input of 'jacket' will return true, since the empty String for the verbs will be counted
hasVerb = false;
hasNoun = false;
String lastVerb = ""; // Don't allow a blank verb and a blank noun, or allow one word to cover both a noun and a verb
for( int j = 0; j < verbs.length; j++ ) {
if( text.contains( verbs[j] ) ) {
lastVerb = verbs[j];
hasVerb = true;
break;
}
}
for( int j = 0; j < nouns.length; j++ ) {
if( text.contains( nouns[j] ) && !lastVerb.equals( nouns[j] ) ) {
hasNoun = true;
if( !hasRightAt )
break;
else if( !State.tempInventory.contains( nouns[j] ) && ( lastVerb.equals( "take" ) || lastVerb.equals( "grab" ) || lastVerb.equals( "get" ) || lastVerb.equals( "pick" ) ) )
State.tempInventory.add( nouns[j] );
}
}
if( hasVerb && hasNoun ) {
State.optionNumber = i;
isSuccess = true;
return isSuccess;
}
}
return isSuccess;
}
/**
Gets the option number of the last non-repeated screen within the player's screen history. The last repeated screen is defined as the last screen that is not a 'repeated' screen,
that is, a screen that is visited multiple times in places throughout the plotline of screen. A good example of a repeated screen is the 'Self' screens, an option that is often
available to the player from which they can observe their items, stats, and other elements relating to the System.
This optionNumber or index depends on the added order of screens within the digraph of screen branches. Since repeated screens are always loops (acyclic), all repeated screens
have a non-repeated root, which should be returned to. The right optionNumber will allow the player to return to the right spot in the story that points to this common loop.
@return int The optionNumber of this current repeated screen. The optionNumber will correspond to the correct screen text file that the player had just come from, a non-repeated screen
@see MyKeyListener.keyReleased( KeyEvent event )
*/
public int getLastNonRepeatedScreenOptionNumber() {
//Get last non-repeated screen
Screen lastNonRepeatedScreen = null;
int index = State.screenHistory.size() - 1;
for( int i = index - 1; i >= 0; i-- ) {
if( !State.screenHistory.get(i).isRepeated() )
lastNonRepeatedScreen = State.screenHistory.get(i);
}
if( lastNonRepeatedScreen == null )
SOPln( "Error: see getLastNonRepeatedScreenOptionNumber() method" );
//Get option number of last non-repeated screen
int actualNumberOfOptions = screenInputTextFileDigraph.outDegree().get( State.currentScreen.getTextFileName().replace( ".txt", "" ) ); //This gets how many screens this repeated screen points to
for( int i = 0; i < actualNumberOfOptions; i++ ) {
if( screenInputTextFileDigraph.getNeighbors().get( State.currentScreen.getTextFileName() ).get(i).equals( lastNonRepeatedScreen.getTextFileName() ) )
return i;
}
SOPln( "Error: last non-repeated screen not found -- see getLastNonRepeatedScreenOptionNumber() method" );
return -1;
}
/**
Gets the width of the screen
@return int The width in pixels of the screen
*/
public static int getWindowWidth() {
return WIDTH;
}
/**
Gets the height of the screen
@return int The height in pixels of the screen
*/
public static int getWindowHeight() {
return HEIGHT;
}
/**
Faster way to print to console
@param message The message to print
@see System.out.println( String str )
*/
public void SOPln( String message ) {
System.out.println( message );
}
/**
KeyListener for the GameWindow. This listener is added to the GameWindow
and is never removed.
*/
private class MyKeyListener implements KeyListener {
@Override
public void keyPressed( KeyEvent event ) {
/*
if( State.currentScreen.getOptions().size() == 1 && event.getKeyCode() == KeyEvent.VK_ENTER ) {
GraphicsBaseFrame option = State.currentScreen.getGBFOptions()[0];
option.setBackground( Color.YELLOW );
option.setOpaque( true );
repaint();
}
*/
}
@Override
public void keyReleased( KeyEvent event ) {
if( State.currentScreen.getOptions().size() == 1 && event.getKeyCode() == KeyEvent.VK_ENTER && !State.currentScreen.hasTextBox() ) {
//If the current screen is a repeated screen, need to find what screen to turn to
if( State.currentScreen.isRepeated() ) {
State.optionNumber = getLastNonRepeatedScreenOptionNumber();
State.changeScreens = true;
} else {
State.optionNumber = 0;
State.changeScreens = true;
}
} else if( State.currentScreen.getOptions().size() == 4 && !State.currentScreen.hasTextBox() ) {
if( event.getKeyCode() == KeyEvent.VK_1 || event.getKeyCode() == KeyEvent.VK_LEFT )
State.optionNumber = 0;
else if( event.getKeyCode() == KeyEvent.VK_2 || event.getKeyCode() == KeyEvent.VK_UP )
State.optionNumber = 1;
else if( event.getKeyCode() == KeyEvent.VK_3 || event.getKeyCode() == KeyEvent.VK_RIGHT )
State.optionNumber = 2;
else if( event.getKeyCode() == KeyEvent.VK_4 || event.getKeyCode() == KeyEvent.VK_DOWN )
State.optionNumber = 3;
State.changeScreens = true;
} else if( State.currentScreen.hasTextBox() && event.getKeyCode() == KeyEvent.VK_ENTER ) {
if( !State.currentScreen.isTextBoxInitialized() )
State.currentScreen.initTextBox();
//Only change screens if text entered is valid (leads to a new screen)
else if( !State.currentScreen.getTextBox().getText().equals("") ) {
if( evalTextBoxInput( State.currentScreen.getTextBox().getText(), State.currentScreen.getOptions() ) )
State.changeScreens = true;
else {
State.currentScreen.remove( State.currentScreen.getTextBox() );
State.currentScreen.addOptionText( TEXT_BOX_LOC_X, TEXT_BOX_LOC_Y );
State.currentScreen.revalidate();
State.currentScreen.initTextBox();
}
} else {
State.currentScreen.setTextBoxFocus();
}
}
}
@Override
public void keyTyped( KeyEvent event ) {
}
}
private class MoveListener implements ActionListener {
@Override
public void actionPerformed( ActionEvent e ) {
}
}
}