└── src ├── res ├── 8-bit Arcade In.ttf ├── 8-bit Arcade.txt ├── ol_presents.mdd ├── ol_presents.obj ├── ol_presents.png └── wl1.def └── wolf3d ├── asset └── loader │ ├── AUDIOTLoader.java │ ├── MAPLoader.java │ ├── VGAGRAPHLoader.java │ └── VSWAPLoader.java ├── audio ├── IMFMusicPlayer.java └── PCMSoundPlayer.java ├── infra ├── Audio.java ├── CardinalDirection.java ├── Display.java ├── Doors.java ├── Enemies.java ├── FizzleFade.java ├── GameCanvas.java ├── GameMap.java ├── HUD.java ├── Input.java ├── Objs.java ├── Palette.java ├── Player.java ├── Resource.java ├── Scene.java ├── SceneManager.java ├── SecretDoors.java ├── Settings.java ├── Tiles.java ├── Util.java ├── Weapons.java └── Wolf3DGame.java ├── main └── Main.java └── scene ├── Credits.java ├── GameDifficulty.java ├── GameOptions.java ├── Initializing.java ├── LevelClearedStatistics.java ├── OLPresents.java ├── ProfoundCarnage13.java ├── Quit.java ├── Stage.java └── Title.java /src/res/8-bit Arcade In.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardo-ono/JavaWolf3DGameEngine/7fbad436b71d4e438048d5979ae323336fd10fa3/src/res/8-bit Arcade In.ttf -------------------------------------------------------------------------------- /src/res/8-bit Arcade.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardo-ono/JavaWolf3DGameEngine/7fbad436b71d4e438048d5979ae323336fd10fa3/src/res/8-bit Arcade.txt -------------------------------------------------------------------------------- /src/res/ol_presents.mdd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardo-ono/JavaWolf3DGameEngine/7fbad436b71d4e438048d5979ae323336fd10fa3/src/res/ol_presents.mdd -------------------------------------------------------------------------------- /src/res/ol_presents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonardo-ono/JavaWolf3DGameEngine/7fbad436b71d4e438048d5979ae323336fd10fa3/src/res/ol_presents.png -------------------------------------------------------------------------------- /src/res/wl1.def: -------------------------------------------------------------------------------- 1 | # --- MAP --- 2 | 3 | MAP_HEAD_RES = MAPHEAD.WL1 4 | GAME_MAPS_RES = GAMEMAPS.WL1 5 | 6 | # LEVEL_INDEX <-> MAP_INDEX (FLOOR_NUMBER = MAP_INDEX + 1) 7 | MAP_LEVEL_0 = 0 8 | MAP_LEVEL_1 = 1 9 | MAP_LEVEL_2 = 2 10 | MAP_LEVEL_3 = 3 11 | MAP_LEVEL_4 = 4 12 | MAP_LEVEL_5 = 5 13 | MAP_LEVEL_6 = 6 14 | MAP_LEVEL_7 = 7 15 | MAP_LEVEL_8 = 8 16 | MAP_LEVEL_SECRET = 9 17 | 18 | TILE_WALL_DEFAULT = 0-85 19 | TILE_DOOR_UNLOCKED_DEFAULT = 90-91 20 | TILE_DOOR_LOCKED_GOLD = 92-93 21 | TILE_DOOR_LOCKED_SILVER = 94-95 22 | TILE_DOOR_UNLOCKED_ELEVATOR = 100-101 23 | TILE_FLOOR_AMBUSH = 106-106 24 | TILE_FLOOR_SECRET = 107-107 25 | TILE_FLOOR_DEFAULT = 108-143 26 | ELEVATOR_TILE_ID = 21 27 | 28 | OBJ_PLAYER_N = 19 29 | OBJ_PLAYER_E = 20 30 | OBJ_PLAYER_S = 21 31 | OBJ_PLAYER_W = 22 32 | 33 | OBJ_DECORATION_DRAWFIRST = 27, 37 34 | OBJ_DECORATION_NONBLOCKING = 23, 32, 38, 42, 46, 57, 61, 64, 65, 66, 67 35 | OBJ_DECORATION_BLOCKING = 24, 25, 26, 28, 30, 31, 33, 34, 35, 36, 39, 40, 41, 45, 58, 59, 60, 62, 63, 68, 69, 70 36 | 37 | OBJ_COLLECT_KEY_GOLDKEY = 43 38 | OBJ_COLLECT_KEY_SILVERKEY = 44 39 | 40 | # dog food 41 | OBJ_COLLECT_HEALTH_HEALTH1=29 42 | # food with chicken 43 | OBJ_COLLECT_HEALTH_HEALTH2=47 44 | # med kit 45 | OBJ_COLLECT_HEALTH_HEALTH3=48 46 | 47 | OBJ_COLLECT_AMMO_DEFAULT = 49 48 | 49 | OBJ_COLLECT_WEAPON_MACHINEGUN = 50 50 | OBJ_COLLECT_WEAPON_GATLINGGUN = 51 51 | 52 | OBJ_COLLECT_TREASURE_BONUS1 = 52 53 | OBJ_COLLECT_TREASURE_BONUS2 = 53 54 | OBJ_COLLECT_TREASURE_BONUS3 = 54 55 | OBJ_COLLECT_TREASURE_BONUS4 = 55 56 | 57 | OBJ_COLLECT_EXTRALIFE_DEFAULT = 56 58 | 59 | OBJ_PATH_E = 90 60 | OBJ_PATH_NE = 91 61 | OBJ_PATH_N = 92 62 | OBJ_PATH_NW = 93 63 | OBJ_PATH_W = 94 64 | OBJ_PATH_SW = 95 65 | OBJ_PATH_S = 96 66 | OBJ_PATH_SE = 97 67 | 68 | OBJ_SECRETDOOR = 98 69 | OBJ_ENDGAME = 99 70 | 71 | OBJ_ENEMY_GUARD_STAND_1_E = 108 72 | OBJ_ENEMY_GUARD_STAND_1_N = 109 73 | OBJ_ENEMY_GUARD_STAND_1_W = 110 74 | OBJ_ENEMY_GUARD_STAND_1_S = 111 75 | OBJ_ENEMY_GUARD_PATROL_1_E = 112 76 | OBJ_ENEMY_GUARD_PATROL_1_N = 113 77 | OBJ_ENEMY_GUARD_PATROL_1_W = 114 78 | OBJ_ENEMY_GUARD_PATROL_1_S = 115 79 | 80 | OBJ_ENEMY_GUARD_STAND_3_E = 144 81 | OBJ_ENEMY_GUARD_STAND_3_N = 145 82 | OBJ_ENEMY_GUARD_STAND_3_W = 146 83 | OBJ_ENEMY_GUARD_STAND_3_S = 147 84 | OBJ_ENEMY_GUARD_PATROL_3_E = 148 85 | OBJ_ENEMY_GUARD_PATROL_3_N = 149 86 | OBJ_ENEMY_GUARD_PATROL_3_W = 150 87 | OBJ_ENEMY_GUARD_PATROL_3_S = 151 88 | 89 | OBJ_ENEMY_GUARD_STAND_4_E = 180 90 | OBJ_ENEMY_GUARD_STAND_4_N = 181 91 | OBJ_ENEMY_GUARD_STAND_4_W = 182 92 | OBJ_ENEMY_GUARD_STAND_4_S = 183 93 | OBJ_ENEMY_GUARD_PATROL_4_E = 184 94 | OBJ_ENEMY_GUARD_PATROL_4_N = 185 95 | OBJ_ENEMY_GUARD_PATROL_4_W = 186 96 | OBJ_ENEMY_GUARD_PATROL_4_S = 187 97 | 98 | OBJ_ENEMY_GUARD_DEAD_1_N = 124 99 | 100 | OBJ_ENEMY_SS_STAND_1_E = 126 101 | OBJ_ENEMY_SS_STAND_1_N = 127 102 | OBJ_ENEMY_SS_STAND_1_W = 128 103 | OBJ_ENEMY_SS_STAND_1_S = 129 104 | OBJ_ENEMY_SS_PATROL_1_E = 130 105 | OBJ_ENEMY_SS_PATROL_1_N = 131 106 | OBJ_ENEMY_SS_PATROL_1_W = 132 107 | OBJ_ENEMY_SS_PATROL_1_S = 133 108 | 109 | OBJ_ENEMY_SS_STAND_3_E = 162 110 | OBJ_ENEMY_SS_STAND_3_N = 163 111 | OBJ_ENEMY_SS_STAND_3_W = 164 112 | OBJ_ENEMY_SS_STAND_3_S = 165 113 | OBJ_ENEMY_SS_PATROL_3_E = 166 114 | OBJ_ENEMY_SS_PATROL_3_N = 167 115 | OBJ_ENEMY_SS_PATROL_3_W = 168 116 | OBJ_ENEMY_SS_PATROL_3_S = 169 117 | 118 | OBJ_ENEMY_SS_STAND_4_E = 198 119 | OBJ_ENEMY_SS_STAND_4_N = 199 120 | OBJ_ENEMY_SS_STAND_4_W = 200 121 | OBJ_ENEMY_SS_STAND_4_S = 201 122 | OBJ_ENEMY_SS_PATROL_4_E = 202 123 | OBJ_ENEMY_SS_PATROL_4_N = 203 124 | OBJ_ENEMY_SS_PATROL_4_W = 204 125 | OBJ_ENEMY_SS_PATROL_4_S = 205 126 | 127 | OBJ_ENEMY_DOG_PATROL_1_E = 138 128 | OBJ_ENEMY_DOG_PATROL_1_N = 139 129 | OBJ_ENEMY_DOG_PATROL_1_W = 140 130 | OBJ_ENEMY_DOG_PATROL_1_S = 141 131 | 132 | OBJ_ENEMY_DOG_PATROL_3_E = 174 133 | OBJ_ENEMY_DOG_PATROL_3_N = 175 134 | OBJ_ENEMY_DOG_PATROL_3_W = 176 135 | OBJ_ENEMY_DOG_PATROL_3_S = 177 136 | 137 | OBJ_ENEMY_DOG_PATROL_4_E = 210 138 | OBJ_ENEMY_DOG_PATROL_4_N = 211 139 | OBJ_ENEMY_DOG_PATROL_4_W = 212 140 | OBJ_ENEMY_DOG_PATROL_4_S = 213 141 | 142 | # final boss 143 | OBJ_ENEMY_HANS_BOSS_1_S = 214 144 | 145 | 146 | # --- VGA --- 147 | # supports shareware version 1.0 and 1.4 148 | 149 | VGA_HEAD_RES = VGAHEAD.WL1 150 | VGA_DICT_RES = VGADICT.WL1 151 | VGA_GRAPH_RES = VGAGRAPH.WL1 152 | 153 | PIC_PC_13 = 100 154 | PIC_CREDITS = 101 155 | PIC_TITLE = 99 156 | PIC_OPTIONS = 22 157 | PIC_OPTIONS_SELECTION_0 = 23 158 | PIC_OPTIONS_SELECTION_1 = 24 159 | PIC_OPTIONS_FOOTER = 30 160 | 161 | DIFFICULTY_FACES_START_PIC_INDEX = 31 162 | 163 | PIC_STATISTICS_0 = 55 164 | PIC_STATISTICS_1 = 96 165 | PIC_STATISTICS_WIN = 97 166 | STATISTICS_FONT_START_PIC_INDEX = 56 167 | STATISTICS_FONT_CHARS = :0123456789%ABCDEFGHIJKLMNOPQRSTUVWXYZ!' 168 | 169 | PIC_HUD_FOOTER = 98 170 | PIC_HUD_DIGITS = 111 171 | PIC_HUD_FACE = 121 172 | PIC_HUD_KEY_EMPTY = 107 173 | PIC_HUD_KEY_GOLD = 108 174 | PIC_HUD_KEY_SILVER = 109 175 | 176 | # weapons 177 | PIC_HUD_WEAPON_KNIFE = 103 178 | PIC_HUD_WEAPON_PISTOL = 104 179 | PIC_HUD_WEAPON_MACHINE = 105 180 | PIC_HUD_WEAPON_GATLING = 106 181 | 182 | SPRITE_WEAPON_KNIFE_ANIMATIONS = 416-420 183 | SPRITE_WEAPON_PISTOL_ANIMATIONS = 421-425 184 | SPRITE_WEAPON_MACHINE_ANIMATIONS = 426-430 185 | SPRITE_WEAPON_GATLING_ANIMATIONS = 431-435 186 | 187 | INFO_WEAPON_KNIFE_MINHITDISTANCE = 2.5 188 | INFO_WEAPON_KNIFE_WAITTIME = 750 189 | INFO_WEAPON_KNIFE_ANIMATIONSPEED = 0.15 190 | 191 | INFO_WEAPON_PISTOL_MINHITDISTANCE = 25 192 | INFO_WEAPON_PISTOL_WAITTIME = 650 193 | INFO_WEAPON_PISTOL_ANIMATIONSPEED = 0.15 194 | 195 | INFO_WEAPON_MACHINE_MINHITDISTANCE = 25 196 | INFO_WEAPON_MACHINE_WAITTIME = 200 197 | INFO_WEAPON_MACHINE_ANIMATIONSPEED = 0.25 198 | 199 | INFO_WEAPON_GATLING_MINHITDISTANCE = 25 200 | INFO_WEAPON_GATLING_WAITTIME = 100 201 | INFO_WEAPON_GATLING_ANIMATIONSPEED = 0.334 202 | 203 | INFO_WEAPON_KNIFE_SOUNDID = ATKKNIFE 204 | INFO_WEAPON_PISTOL_SOUNDID = ATKPISTOL 205 | INFO_WEAPON_MACHINE_SOUNDID = ATKMACHINEGUN 206 | INFO_WEAPON_GATLING_SOUNDID = ATKGATLING 207 | 208 | 209 | # --- VSWAP --- 210 | # supports shareware version 1.0 and 1.4 211 | 212 | VSWAP_RES = VSWAP.WL1 213 | 214 | OFFSET_OBJID_TO_SPRITEID = -21 215 | 216 | TEXTURE_DOOR_DEFAULT = 98 217 | TEXTURE_DOOR_SIDE = 100 218 | TEXTURE_DOOR_ELEVATOR = 102 219 | TEXTURE_DOOR_LOCKED = 104 220 | 221 | SPRITE_ENEMY_GUARD_STAND = 50-57 222 | SPRITE_ENEMY_GUARD_WALK = 58-89 223 | SPRITE_ENEMY_GUARD_DYING = 90-93 224 | SPRITE_ENEMY_GUARD_HIT = 94 225 | SPRITE_ENEMY_GUARD_DEAD = 95 226 | SPRITE_ENEMY_GUARD_ATTACK = 96-98 227 | 228 | SPRITE_ENEMY_DOG_WALK = 99-130 229 | SPRITE_ENEMY_DOG_DYING = 131-133 230 | SPRITE_ENEMY_DOG_HIT = 131 231 | SPRITE_ENEMY_DOG_DEAD = 134 232 | SPRITE_ENEMY_DOG_ATTACK = 135-137 233 | 234 | SPRITE_ENEMY_SS_STAND = 138-145 235 | SPRITE_ENEMY_SS_WALK = 146-177 236 | SPRITE_ENEMY_SS_DYING = 178-181 237 | SPRITE_ENEMY_SS_HIT = 182 238 | SPRITE_ENEMY_SS_DEAD = 183 239 | SPRITE_ENEMY_SS_ATTACK = 184-186 240 | 241 | SPRITE_ENEMY_HANS_STAND = 296 242 | SPRITE_ENEMY_HANS_WALK = 296-299 243 | SPRITE_ENEMY_HANS_DYING = 304-306 244 | SPRITE_ENEMY_HANS_HIT = 304 245 | SPRITE_ENEMY_HANS_DEAD = 303 246 | SPRITE_ENEMY_HANS_ATTACK = 300-302 247 | 248 | END_SPRITE_WALK = 408-411 249 | END_SPRITE_CELEBRATE = 412-415 250 | 251 | 252 | INFO_ENEMY_GUARD_MIN_ATTACK_DIST = 10 253 | INFO_ENEMY_DOG_MIN_ATTACK_DIST = 1.5 254 | INFO_ENEMY_SS_MIN_ATTACK_DIST = 10 255 | INFO_ENEMY_HANS_MIN_ATTACK_DIST = 20 256 | 257 | INFO_ENEMY_GUARD_PATROL_SPEED = 0.015 258 | INFO_ENEMY_DOG_PATROL_SPEED = 0.025 259 | INFO_ENEMY_SS_PATROL_SPEED = 0.015 260 | INFO_ENEMY_HANS_PATROL_SPEED = 0.015 261 | 262 | INFO_ENEMY_GUARD_CHASE_SPEED = 0.03 263 | INFO_ENEMY_DOG_CHASE_SPEED = 0.07 264 | INFO_ENEMY_SS_CHASE_SPEED = 0.04 265 | INFO_ENEMY_HANS_CHASE_SPEED = 0.03 266 | 267 | INFO_ENEMY_GUARD_HALT_SOUND_ID = HALT 268 | INFO_ENEMY_DOG_HALT_SOUND_ID = DOGBARK 269 | INFO_ENEMY_SS_HALT_SOUND_ID = SCHUTZAD 270 | INFO_ENEMY_HANS_HALT_SOUND_ID = GUTENTAG 271 | 272 | INFO_ENEMY_GUARD_ATTACK_SOUND_ID = NAZIFIRE 273 | INFO_ENEMY_DOG_ATTACK_SOUND_ID = DOGBARK 274 | INFO_ENEMY_SS_ATTACK_SOUND_ID = SSFIRE 275 | INFO_ENEMY_HANS_ATTACK_SOUND_ID = BOSSFIRE 276 | 277 | INFO_ENEMY_GUARD_DEATH_SOUND_ID = DEATHSCREAM1,DEATHSCREAM2 278 | INFO_ENEMY_DOG_DEATH_SOUND_ID = DOGDEATH 279 | INFO_ENEMY_SS_DEATH_SOUND_ID = LEBEN 280 | INFO_ENEMY_HANS_DEATH_SOUND_ID = MUTTI 281 | 282 | INFO_ENEMY_GUARD_DROP_ITEM_ID = 49 283 | INFO_ENEMY_DOG_DROP_ITEM_ID = -1 284 | INFO_ENEMY_SS_DROP_ITEM_ID = 49 285 | INFO_ENEMY_HANS_DROP_ITEM_ID = 43 286 | 287 | INFO_ENEMY_GUARD_SCORE_POINTS = 500 288 | INFO_ENEMY_DOG_SCORE_POINTS = 700 289 | INFO_ENEMY_SS_SCORE_POINTS = 1000 290 | INFO_ENEMY_HANS_SCORE_POINTS = 10000 291 | 292 | 293 | DIGITIZED_SOUND_HALT = 0 294 | DIGITIZED_SOUND_DOGBARK = 1 295 | DIGITIZED_SOUND_CLOSEDOOR = 2 296 | DIGITIZED_SOUND_OPENDOOR = 3 297 | DIGITIZED_SOUND_ATKMACHINEGUN = 4 298 | DIGITIZED_SOUND_ATKPISTOL = 5 299 | DIGITIZED_SOUND_ATKGATLING = 6 300 | DIGITIZED_SOUND_SCHUTZAD = 7 301 | DIGITIZED_SOUND_GUTENTAG = 8 302 | DIGITIZED_SOUND_MUTTI = 9 303 | DIGITIZED_SOUND_BOSSFIRE = 10 304 | DIGITIZED_SOUND_SSFIRE = 11 305 | DIGITIZED_SOUND_DEATHSCREAM1 = 12 306 | DIGITIZED_SOUND_DEATHSCREAM2 = 13 307 | DIGITIZED_SOUND_TAKEDAMAGE = 14 308 | DIGITIZED_SOUND_PUSHWALL = 15 309 | DIGITIZED_SOUND_DOGDEATH = -1 310 | DIGITIZED_SOUND_AHHHG = -1 311 | DIGITIZED_SOUND_DIE = -1 312 | DIGITIZED_SOUND_EVA = -1 313 | DIGITIZED_SOUND_LEBEN = 26 314 | DIGITIZED_SOUND_NAZIFIRE = 27 315 | DIGITIZED_SOUND_SLURPIE = 28 316 | DIGITIZED_SOUND_TOT_HUND = -1 317 | DIGITIZED_SOUND_MEINGOTT = -1 318 | DIGITIZED_SOUND_SCHABBSHA = -1 319 | DIGITIZED_SOUND_HITLERHA = -1 320 | DIGITIZED_SOUND_SPION = -1 321 | DIGITIZED_SOUND_NEINSOVAS = -1 322 | DIGITIZED_SOUND_DOGATTACK = -1 323 | DIGITIZED_SOUND_LEVELDONE = -1 324 | DIGITIZED_SOUND_MECHSTEP = -1 325 | DIGITIZED_SOUND_YEAH = 57 326 | DIGITIZED_SOUND_SCHEIST = -1 327 | DIGITIZED_SOUND_DEATHSCREAM4 = -1 328 | DIGITIZED_SOUND_DEATHSCREAM5 = -1 329 | DIGITIZED_SOUND_DONNER = -1 330 | DIGITIZED_SOUND_EINE = -1 331 | DIGITIZED_SOUND_ERLAUBEN = -1 332 | DIGITIZED_SOUND_DEATHSCREAM6 = -1 333 | DIGITIZED_SOUND_DEATHSCREAM7 = -1 334 | DIGITIZED_SOUND_DEATHSCREAM8 = -1 335 | DIGITIZED_SOUND_DEATHSCREAM9 = -1 336 | DIGITIZED_SOUND_KEIN = -1 337 | DIGITIZED_SOUND_MEIN = -1 338 | DIGITIZED_SOUND_ROSE = -1 339 | 340 | 341 | # --- AUDIOHED & AUDIOT --- 342 | # supports shareware version 1.0 and 1.4 343 | 344 | AUDIO_HEAD_RES = AUDIOHED.WL1 345 | AUDIO_T_RES = AUDIOT.WL1 346 | 347 | PC_SPEAKER_SOUND_START_INDEX = 0 348 | PC_SPEAKER_SOUND_END_INDEX = 86 349 | 350 | ADLIB_SOUND_START_INDEX = 87 351 | ADLIB_SOUND_END_INDEX = 173 352 | 353 | EFFECT_SOUND_HITWALL = 0 354 | EFFECT_SOUND_SELECTWPN = 1 355 | EFFECT_SOUND_SELECTITEM = 2 356 | EFFECT_SOUND_HEARTBEAT = 3 357 | EFFECT_SOUND_MOVEGUN2 = 4 358 | EFFECT_SOUND_MOVEGUN1 = 5 359 | EFFECT_SOUND_NOWAY = 6 360 | EFFECT_SOUND_NAZIHITPLAYER = 7 361 | EFFECT_SOUND_SCHABBSTHROW = 8 362 | EFFECT_SOUND_PLAYERDEATH = 9 363 | EFFECT_SOUND_DOGDEATH = 10 364 | EFFECT_SOUND_ATKGATLING = 11 365 | EFFECT_SOUND_GETKEY = 12 366 | EFFECT_SOUND_NOITEM = 13 367 | EFFECT_SOUND_WALK1 = 14 368 | EFFECT_SOUND_WALK2 = 15 369 | EFFECT_SOUND_TAKEDAMAGE = 16 370 | EFFECT_SOUND_GAMEOVER = 17 371 | EFFECT_SOUND_OPENDOOR = 18 372 | EFFECT_SOUND_CLOSEDOOR = 19 373 | EFFECT_SOUND_DONOTHING = 20 374 | EFFECT_SOUND_HALT = 21 375 | EFFECT_SOUND_DEATHSCREAM2 = 22 376 | EFFECT_SOUND_ATKKNIFE = 23 377 | EFFECT_SOUND_ATKPISTOL = 24 378 | EFFECT_SOUND_DEATHSCREAM3 = 25 379 | EFFECT_SOUND_ATKMACHINEGUN = 26 380 | EFFECT_SOUND_HITENEMY = 27 381 | EFFECT_SOUND_SHOOTDOOR = 28 382 | EFFECT_SOUND_DEATHSCREAM1 = 29 383 | EFFECT_SOUND_GETMACHINE = 30 384 | EFFECT_SOUND_GETAMMO = 31 385 | EFFECT_SOUND_SHOOT = 32 386 | EFFECT_SOUND_HEALTH1 = 33 387 | EFFECT_SOUND_HEALTH2 = 34 388 | EFFECT_SOUND_HEALTH3 = 34 389 | EFFECT_SOUND_BONUS1 = 35 390 | EFFECT_SOUND_BONUS2 = 36 391 | EFFECT_SOUND_BONUS3 = 37 392 | EFFECT_SOUND_GETGATLING = 38 393 | EFFECT_SOUND_ESCPRESSED = 39 394 | EFFECT_SOUND_LEVELDONE = 40 395 | EFFECT_SOUND_DOGBARK = 41 396 | EFFECT_SOUND_ENDBONUS1 = 42 397 | EFFECT_SOUND_ENDBONUS2 = 43 398 | EFFECT_SOUND_BONUS1UP = 44 399 | EFFECT_SOUND_BONUS4 = 45 400 | EFFECT_SOUND_PUSHWALL = 46 401 | EFFECT_SOUND_NOBONUS = 47 402 | EFFECT_SOUND_PERCENT100 = 48 403 | EFFECT_SOUND_BOSSACTIVE = 49 404 | EFFECT_SOUND_MUTTI = 50 405 | EFFECT_SOUND_SCHUTZAD = 51 406 | EFFECT_SOUND_AHHHG = 52 407 | EFFECT_SOUND_DIE = 53 408 | EFFECT_SOUND_EVA = 54 409 | EFFECT_SOUND_GUTENTAG = 55 410 | EFFECT_SOUND_LEBEN = 56 411 | EFFECT_SOUND_SCHEIST = 57 412 | EFFECT_SOUND_NAZIFIRE = 58 413 | EFFECT_SOUND_BOSSFIRE = 59 414 | EFFECT_SOUND_SSFIRE = 60 415 | EFFECT_SOUND_SLURPIE = 61 416 | EFFECT_SOUND_TOT_HUND = 62 417 | EFFECT_SOUND_MEINGOTT = 63 418 | EFFECT_SOUND_SCHABBSHA = 64 419 | EFFECT_SOUND_HITLERHA = 65 420 | EFFECT_SOUND_SPION = 66 421 | EFFECT_SOUND_NEINSOVAS = 67 422 | EFFECT_SOUND_DOGATTACK = 68 423 | EFFECT_SOUND_FLAMETHROWER = 69 424 | EFFECT_SOUND_MECHSTEP = 70 425 | EFFECT_SOUND_GOOBS = 71 426 | EFFECT_SOUND_YEAH = 72 427 | EFFECT_SOUND_DEATHSCREAM4 = 73 428 | EFFECT_SOUND_DEATHSCREAM5 = 74 429 | EFFECT_SOUND_DEATHSCREAM6 = 75 430 | EFFECT_SOUND_DEATHSCREAM7 = 76 431 | EFFECT_SOUND_DEATHSCREAM8 = 77 432 | EFFECT_SOUND_DEATHSCREAM9 = 78 433 | EFFECT_SOUND_DONNER = 79 434 | EFFECT_SOUND_EINE = 80 435 | EFFECT_SOUND_ERLAUBEN = 81 436 | EFFECT_SOUND_KEIN = 82 437 | EFFECT_SOUND_MEIN = 83 438 | EFFECT_SOUND_ROSE = 84 439 | EFFECT_SOUND_MISSILEFIRE = 85 440 | EFFECT_SOUND_MISSILEHIT = 86 441 | 442 | MUSIC_START_INDEX = 261 443 | MUSIC_END_INDEX = 287 444 | 445 | MUSIC_CORNER = 0 446 | MUSIC_WARMARCH = 2 447 | MUSIC_GETTHEM = 3 448 | MUSIC_NAZI_NOR = 7 449 | MUSIC_POW = 9 450 | MUSIC_SEARCHN = 11 451 | MUSIC_SUSPENSE = 12 452 | MUSIC_WONDERIN = 14 453 | MUSIC_ENDLEVEL = 16 454 | MUSIC_ROSTER = 23 455 | MUSIC_URAHERO = 24 456 | 457 | INFO_FLOOR_1_MUSIC_ID = GETTHEM 458 | INFO_FLOOR_2_MUSIC_ID = SEARCHN 459 | INFO_FLOOR_3_MUSIC_ID = POW 460 | INFO_FLOOR_4_MUSIC_ID = SUSPENSE 461 | INFO_FLOOR_5_MUSIC_ID = GETTHEM 462 | INFO_FLOOR_6_MUSIC_ID = SEARCHN 463 | INFO_FLOOR_7_MUSIC_ID = POW 464 | INFO_FLOOR_8_MUSIC_ID = SUSPENSE 465 | INFO_FLOOR_9_MUSIC_ID = WARMARCH 466 | INFO_FLOOR_10_MUSIC_ID = CORNER 467 | 468 | 469 | # FLOOR / CEILING COLORS (0xRRGGBBAA) 470 | 471 | # 0x1D = 0x383838ff 472 | # 0xBF = 0x400040ff 473 | # 0x4E = 0x007070ff 474 | # 0x8D = 0x585400ff 475 | 476 | INFO_FLOOR_COLOR = 0x808080ff 477 | 478 | INFO_FLOOR_1_CEILING_COLOR = 0x383838ff 479 | INFO_FLOOR_2_CEILING_COLOR = 0x383838ff 480 | INFO_FLOOR_3_CEILING_COLOR = 0x383838ff 481 | INFO_FLOOR_4_CEILING_COLOR = 0x383838ff 482 | INFO_FLOOR_5_CEILING_COLOR = 0x383838ff 483 | INFO_FLOOR_6_CEILING_COLOR = 0x383838ff 484 | INFO_FLOOR_7_CEILING_COLOR = 0x383838ff 485 | INFO_FLOOR_8_CEILING_COLOR = 0x383838ff 486 | INFO_FLOOR_9_CEILING_COLOR = 0x383838ff 487 | INFO_FLOOR_10_CEILING_COLOR = 0x400040ff 488 | -------------------------------------------------------------------------------- /src/wolf3d/asset/loader/AUDIOTLoader.java: -------------------------------------------------------------------------------- 1 | package wolf3d.asset.loader; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.InputStream; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import static wolf3d.infra.Settings.PC_SPEAKER_SOUND_PCM_FREQ; 12 | 13 | /** 14 | * AUDIOTLoader class. 15 | * 16 | * This is responsible for loading the PC Speaker sounds, Adlib sounds and 17 | * IMF musics present in the AUDIOHED and AUDIOT files. 18 | * 19 | * No compression methods are used in these files, so the audio data can 20 | * be extracted directly. 21 | * 22 | * In this implementation, the PC Speaker sounds are converted to 23 | * PCM digitized sounds and played through the PCMSoundPlayer class. 24 | * 25 | * Note: the digitized sounds are located in the VSWAP file. 26 | * 27 | * Reference: 28 | * https://moddingwiki.shikadi.net/wiki/AudioT_Format 29 | * 30 | * @author Leonardo Ono (ono.leo80@gmail.com) 31 | */ 32 | public class AUDIOTLoader { 33 | 34 | private static final Map 35 | ADLIB_SOUNDS = new HashMap<>(); 36 | 37 | private static final Map 38 | PC_SPEAKER_SOUNDS = new HashMap<>(); 39 | 40 | private static final Map MUSICS = new HashMap<>(); 41 | 42 | public static void load( 43 | String path, String audioHeadRes, String audioTRes 44 | , int pcSpeakerStartIndex, int pcSpeakerEndIndex 45 | , int adlibSoundStartIndex, int adlibSoundEndIndex 46 | , int musicStartIndex, int musicEndIndex) throws Exception { 47 | 48 | try ( 49 | InputStream audioHeadIs = new FileInputStream(path + audioHeadRes); 50 | InputStream audioTIs = new FileInputStream(path + audioTRes); 51 | ) { 52 | // read audio header data 53 | ByteBuffer headData = ByteBuffer.wrap(audioHeadIs.readAllBytes()); 54 | headData.order(ByteOrder.LITTLE_ENDIAN); 55 | 56 | // read audioT data 57 | ByteBuffer audioTData = ByteBuffer.wrap(audioTIs.readAllBytes()); 58 | audioTData.order(ByteOrder.LITTLE_ENDIAN); 59 | 60 | // retrieve the offsets for each resource 61 | int[] offsets = new int[headData.limit() / 4]; 62 | int offsetsIndex = 0; 63 | while (headData.hasRemaining()) { 64 | offsets[offsetsIndex++] = headData.getInt(); 65 | } 66 | 67 | extractPCSpeakerSounds( 68 | pcSpeakerStartIndex, pcSpeakerEndIndex, offsets, audioTData); 69 | 70 | extractAdlibSounds( 71 | adlibSoundStartIndex, adlibSoundEndIndex, offsets, audioTData); 72 | 73 | extractIMFMusics( 74 | musicStartIndex, musicEndIndex, offsets, audioTData); 75 | } 76 | catch (Exception ex) { 77 | throw new Exception( 78 | "Could not load AUDIOT resources properly!", ex); 79 | } 80 | } 81 | 82 | private static void extractPCSpeakerSounds( 83 | int pcSpeakerStartIndex, int pcSpeakerEndIndex 84 | , int[] offsets, ByteBuffer audioTData) throws Exception { 85 | 86 | for (int i = pcSpeakerStartIndex; i <= pcSpeakerEndIndex; i++) { 87 | int chunkDataOffset = offsets[i]; 88 | audioTData.position(chunkDataOffset); 89 | int length = audioTData.getInt(); 90 | int priority = audioTData.getShort() & 0xffff; 91 | byte[] soundData = new byte[length]; 92 | audioTData.get(soundData); 93 | byte terminator = audioTData.get(); // must be zero 94 | if (terminator != 0) { 95 | throw new Exception("PCSpeaker sound data is corrupted!"); 96 | } 97 | // convert to 44100Hz unsigned 8 bits mono PCM 98 | byte[] pcm = convertPCSpeakerSoundToPCM( 99 | soundData, PC_SPEAKER_SOUND_PCM_FREQ); 100 | 101 | // 2 last bytes form a UINTLE16 indicating the sound priority 102 | pcm[pcm.length - 2] = (byte) (priority & 0xff); 103 | pcm[pcm.length - 1] = (byte) ((priority >> 8) & 0xff); 104 | PC_SPEAKER_SOUNDS.put(i - pcSpeakerStartIndex, pcm); 105 | } 106 | } 107 | 108 | private static void extractAdlibSounds( 109 | int adlibSoundStartIndex, int adlibSoundEndIndex 110 | , int[] offsets, ByteBuffer audioTData) { 111 | 112 | for (int i = adlibSoundStartIndex; i <= adlibSoundEndIndex; i++) { 113 | int chunkDataOffset = offsets[i]; 114 | int chunkDataLength = offsets[i + 1] - offsets[i]; 115 | ByteBuffer soundData 116 | = audioTData.slice(chunkDataOffset, chunkDataLength); 117 | 118 | soundData.order(ByteOrder.LITTLE_ENDIAN); 119 | ADLIB_SOUNDS.put(i - adlibSoundStartIndex, soundData); 120 | } 121 | } 122 | 123 | private static void extractIMFMusics( 124 | int musicStartIndex, int musicEndIndex, 125 | int[] offsets, ByteBuffer audioTData) { 126 | 127 | for (int i = musicStartIndex; i <= musicEndIndex; i++) { 128 | int chunkDataOffset = offsets[i]; 129 | int chunkDataLength = offsets[i + 1] - offsets[i]; 130 | ByteBuffer musicData 131 | = audioTData.slice(chunkDataOffset, chunkDataLength); 132 | 133 | musicData.order(ByteOrder.LITTLE_ENDIAN); 134 | MUSICS.put(i - musicStartIndex, musicData); 135 | } 136 | } 137 | 138 | private static final int PC_BASE_TIMER = 1193181; 139 | private static final int PC_VOLUME = 15; 140 | private static final int PC_RATE = 140; 141 | 142 | private static byte[] convertPCSpeakerSoundToPCM(byte[] src, long hertz) { 143 | List dstBytes = new ArrayList<>(); 144 | int sign = -1; 145 | long tone = 0; 146 | long i = 0; 147 | long phaseLength = 0; 148 | long phaseTic = 0; 149 | long samplesPerByte = hertz / PC_RATE; 150 | long srcLength = src.length; 151 | int srcIndex = 0; 152 | while (srcLength-- > 0) { 153 | tone = (src[srcIndex++] & 0xff) * 60; 154 | phaseLength = (hertz * tone) / (2 * PC_BASE_TIMER); 155 | for (i = 0; i < samplesPerByte; i++) { 156 | if (tone > 0) { 157 | dstBytes.add((byte) (128 + sign * PC_VOLUME)); 158 | if (phaseTic++ >= phaseLength) { 159 | sign = -sign; 160 | phaseTic = 0; 161 | } 162 | } else { 163 | phaseTic = 0; 164 | dstBytes.add((byte) 128); 165 | } 166 | } 167 | } 168 | byte[] bytesArr = new byte[dstBytes.size() + 2]; 169 | int index = 0; 170 | for (byte b : dstBytes) { 171 | bytesArr[index++] = b; 172 | } 173 | return bytesArr; 174 | } 175 | 176 | public static byte[] getPcSpeakerSound(int soundIndex) { 177 | return PC_SPEAKER_SOUNDS.get(soundIndex); 178 | } 179 | 180 | public static ByteBuffer getAdlibSound(int soundIndex) { 181 | return ADLIB_SOUNDS.get(soundIndex); 182 | } 183 | 184 | public static ByteBuffer getMusic(int musicIndex) { 185 | return MUSICS.get(musicIndex); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/wolf3d/asset/loader/MAPLoader.java: -------------------------------------------------------------------------------- 1 | package wolf3d.asset.loader; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.ByteBuffer; 7 | import java.nio.ByteOrder; 8 | import java.nio.IntBuffer; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | /** 14 | * MAPLoader class. 15 | * 16 | * This is responsible for loading the game map's present in the 17 | * MAPHEAD and GAMEMAPS files. 18 | * 19 | * The maps are compressed using both Carmack and RLEW compression methods. 20 | * 21 | * References: 22 | * https://moddingwiki.shikadi.net/wiki/GameMaps_Format 23 | * https://vpoupet.github.io/wolfenstein/docs/files.html 24 | * https://moddingwiki.shikadi.net/wiki/Carmack_compression 25 | * https://moddingwiki.shikadi.net/wiki/Id_Software_RLEW_compression 26 | * 27 | * @author Leonardo Ono (ono.leo80@gmail.com) 28 | */ 29 | public class MAPLoader { 30 | 31 | private static int[] mapOffsets; 32 | 33 | // MAPS[mapId][layer], layer can be 0 or 1 34 | private static final Map MAPS = new HashMap<>(); 35 | 36 | private static final Map MAP_NAMES = new HashMap<>(); 37 | 38 | public static void load(String path 39 | , String mapHeadRes, String gameMapsRes) throws Exception { 40 | 41 | try ( 42 | InputStream mapHeadIs = new FileInputStream(path + mapHeadRes); 43 | InputStream gameMapIs = new FileInputStream(path + gameMapsRes); 44 | ) { 45 | // extract the header and map offsets info 46 | byte[] magic = new byte[2]; 47 | mapHeadIs.read(magic); 48 | ByteBuffer mapHeadData = ByteBuffer.wrap(mapHeadIs.readAllBytes()); 49 | mapHeadData.order(ByteOrder.LITTLE_ENDIAN); 50 | 51 | mapOffsets = new int[100]; 52 | int index = 0; 53 | while (index < mapOffsets.length) { 54 | mapOffsets[index++] = mapHeadData.getInt(); 55 | } 56 | 57 | // extract all the maps 58 | ByteBuffer gameMapData = ByteBuffer.wrap(gameMapIs.readAllBytes()); 59 | gameMapData.order(ByteOrder.LITTLE_ENDIAN); 60 | for (int i = 0; i < index; i++) { 61 | int[][] map = new int[2][]; 62 | int mapOffset = mapOffsets[i]; 63 | if (mapOffset <= 0) { 64 | continue; 65 | } 66 | gameMapData.position(mapOffset); 67 | int offPlane0 = gameMapData.getInt(); 68 | int offPlane1 = gameMapData.getInt(); 69 | int offPlane2 = gameMapData.getInt(); 70 | int lenPlane0 = gameMapData.getShort(); 71 | int lenPlane1 = gameMapData.getShort(); 72 | int lenPlane2 = gameMapData.getShort(); 73 | int width = gameMapData.getShort(); 74 | int height = gameMapData.getShort(); 75 | byte[] name = new byte[16]; 76 | gameMapData.get(name); 77 | String mapName = new String(name); 78 | 79 | // layer 0 80 | ByteBuffer carmackDecompressed 81 | = decompressCarmack(gameMapData, offPlane0); 82 | 83 | carmackDecompressed.order(ByteOrder.LITTLE_ENDIAN); 84 | carmackDecompressed.position(0); 85 | IntBuffer rlewDecompressed 86 | = decompressRLEW(carmackDecompressed, 0); 87 | 88 | map[0] = rlewDecompressed.array(); 89 | 90 | // layer 1 91 | carmackDecompressed = decompressCarmack(gameMapData, offPlane1); 92 | carmackDecompressed.order(ByteOrder.LITTLE_ENDIAN); 93 | carmackDecompressed.position(0); 94 | rlewDecompressed = decompressRLEW(carmackDecompressed, 0); 95 | map[1] = rlewDecompressed.array(); 96 | 97 | MAPS.put(i, map); 98 | MAP_NAMES.put(i, mapName); 99 | } 100 | } catch (IOException ex) { 101 | throw new Exception( 102 | "Could not load MAP resources properly!", ex); 103 | } 104 | } 105 | 106 | // https://moddingwiki.shikadi.net/wiki/Carmack_compression 107 | private static ByteBuffer decompressCarmack(ByteBuffer bb, int start) { 108 | final byte nearTag = (byte) 0xa7; 109 | final byte farTag = (byte) 0xa8; 110 | bb.position(start); 111 | byte[] decompressedData = new byte[bb.getShort() & 0xffff]; 112 | int decompressedDataIndex = 0; 113 | while (decompressedDataIndex < decompressedData.length) { 114 | byte b0 = bb.get(); // number of words 115 | byte b1 = bb.get(); // signal byte 116 | if ((b0 == 0 && b1 == nearTag) || (b0 == 0 && b1 == farTag)) { 117 | decompressedData[decompressedDataIndex++] = bb.get(); 118 | decompressedData[decompressedDataIndex++] = b1; 119 | } 120 | else if (b1 == nearTag || b1 == farTag) { 121 | int location = (b1 == nearTag 122 | ? decompressedDataIndex - 2 * (bb.get() & 0xff) 123 | : 2 * (bb.getShort() & 0xffff)); 124 | 125 | int sizeInBytes = (b0 & 0xff) * 2; 126 | System.arraycopy(decompressedData, location 127 | , decompressedData, decompressedDataIndex, sizeInBytes); 128 | 129 | decompressedDataIndex += sizeInBytes; 130 | } 131 | else { 132 | decompressedData[decompressedDataIndex++] = b0; 133 | decompressedData[decompressedDataIndex++] = b1; 134 | } 135 | } 136 | ByteBuffer decompressedByteBuffer = ByteBuffer.wrap(decompressedData); 137 | decompressedByteBuffer.order(ByteOrder.LITTLE_ENDIAN); 138 | return decompressedByteBuffer; 139 | } 140 | 141 | // https://moddingwiki.shikadi.net/wiki/Id_Software_RLEW_compression 142 | private static IntBuffer decompressRLEW(ByteBuffer bb, int start) { 143 | bb.position(start); 144 | int[] decompressedData = new int[(bb.getShort() & 0xffff) / 2]; 145 | int decompressedDataIndex = 0; 146 | while (decompressedDataIndex < decompressedData.length) { 147 | short s = bb.getShort(); 148 | if (s == (short) 0xABCD) { 149 | int count = bb.getShort() & 0xffff; 150 | Arrays.fill(decompressedData, decompressedDataIndex 151 | , decompressedDataIndex + count, bb.getShort() & 0xffff); 152 | 153 | decompressedDataIndex += count; 154 | } 155 | else { 156 | decompressedData[decompressedDataIndex++] = s & 0xffff; 157 | } 158 | } 159 | IntBuffer decompressedBuffer = IntBuffer.wrap(decompressedData); 160 | return decompressedBuffer; 161 | } 162 | 163 | public static int[][] getMap(int mapId) { 164 | return MAPS.get(mapId); 165 | } 166 | 167 | public static String getMapName(int mapId) { 168 | return MAP_NAMES.get(mapId); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/wolf3d/asset/loader/VGAGRAPHLoader.java: -------------------------------------------------------------------------------- 1 | package wolf3d.asset.loader; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.Graphics2D; 6 | import java.awt.image.BufferedImage; 7 | import static java.awt.image.BufferedImage.TYPE_INT_ARGB; 8 | import java.awt.image.DataBufferByte; 9 | import java.io.FileInputStream; 10 | import java.io.InputStream; 11 | import java.nio.ByteBuffer; 12 | import java.nio.ByteOrder; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import static wolf3d.infra.Palette.INDEX_COLOR_MODEL; 16 | import wolf3d.infra.Util; 17 | 18 | /** 19 | * VGAGRAPHLoader class. 20 | * 21 | * This is responsible for loading the PIC's (HUD images, bitmap fonts, etc) 22 | * present in the VGAHEAD, VGADICT and VGAGRAPH files. 23 | * 24 | * The pic data is compressed using the Huffman compression, 25 | * so the VGADICT file must be used as a dictionary for decompression. 26 | * 27 | * The VGAHEAD file is the header and informs the location (offset) 28 | * of each chunk within VGAGRAPH. 29 | * 30 | * Reference: 31 | * https://devinsmith.net/backups/bruce/wolf3d.html 32 | * https://moddingwiki.shikadi.net/wiki/Huffman_Compression 33 | * 34 | * @author Leonardo Ono (ono.leo80@gmail.com) 35 | */ 36 | public class VGAGRAPHLoader { 37 | 38 | // References: 39 | // https://github.com/sirjuddington/SLADE/issues/121 40 | // 41 | // https://github.com/id-Software/Wolf3D-iOS/blob/ -> 42 | // -> d7fff51d7d3bc7f3c0d44d58f91ba2ba3b4a7951/ -> 43 | // -> wolf3d/wolfextractor/wolf/wolf_gfx.c 44 | // typedef struct 45 | // { 46 | // short height; 47 | // short location[ 256 ]; 48 | // char width[ 256 ]; 49 | // } fontstruct; 50 | public static class VGAGRAPHFont { 51 | 52 | private final byte[] fontData; 53 | private int fontHeight; 54 | private int[] fontLocation; 55 | private int[] fontWidth; 56 | private final BufferedImage[] charImages = new BufferedImage[256]; 57 | 58 | public VGAGRAPHFont(byte[] fontData, Color color) { 59 | this.fontData = fontData; 60 | extractFontInformation(); 61 | generateCharImages(color); 62 | } 63 | 64 | private void extractFontInformation() { 65 | ByteBuffer bb = ByteBuffer.wrap(fontData); 66 | bb.order(ByteOrder.LITTLE_ENDIAN); 67 | fontHeight = bb.getShort() & 0xffff; 68 | fontLocation = new int[256]; 69 | fontWidth = new int[256]; 70 | for (int i = 0; i < fontLocation.length; i++) { 71 | fontLocation[i] = bb.getShort() & 0xffff; 72 | } 73 | for (int i = 0; i < fontWidth.length; i++) { 74 | fontWidth[i] = bb.get() & 0xff; 75 | } 76 | } 77 | 78 | private void generateCharImages(Color color) { 79 | for (int c = 0; c < 256; c++) { 80 | 81 | int ch = fontHeight; 82 | int cw = fontWidth[c]; 83 | int cl = fontLocation[c]; 84 | 85 | BufferedImage charImage = null; 86 | if (ch > 0 && cw > 0) { 87 | charImage = new BufferedImage(cw, ch, TYPE_INT_ARGB); 88 | } 89 | if (charImage == null) continue; 90 | charImages[c] = charImage; 91 | int index = 0; 92 | for (int y = 0; y < ch; y++) { 93 | for (int x = 0; x < cw; x++) { 94 | int rgb = fontData[cl + index++] & 0xff; 95 | if (rgb > 0) { 96 | charImage.setRGB(x, y, color.getRGB()); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | public void drawString(Graphics2D g, String text, int x, int y) { 104 | int dx = 0; 105 | for (int i = 0; i < text.length(); i++) { 106 | int c = text.charAt(i); 107 | BufferedImage charImage = charImages[c]; 108 | g.drawImage(charImage, x + dx, y, null); 109 | dx += fontWidth[c]; 110 | } 111 | } 112 | 113 | } 114 | 115 | private static final byte[] huffmanNodes = new byte[256 * 4]; 116 | private static Dimension[] pictable; 117 | 118 | private static final Map PICS = new HashMap<>(); 119 | private static final Map FONTS = new HashMap<>(); 120 | 121 | public static void load(String path, String vgaHeadRes 122 | , String vgaDictRes, String vgaGraphRes) throws Exception { 123 | 124 | try ( 125 | InputStream vgaHeadIs = new FileInputStream(path + vgaHeadRes); 126 | InputStream vgaDictIs = new FileInputStream(path + vgaDictRes); 127 | InputStream vgaGraphIs = new FileInputStream(path + vgaGraphRes); 128 | ) { 129 | ByteBuffer vgaHeadData = ByteBuffer.wrap(vgaHeadIs.readAllBytes()); 130 | vgaHeadData.order(ByteOrder.LITTLE_ENDIAN); 131 | 132 | // size of VGAHEAD file must be multiple of 3 133 | if (vgaHeadData.limit() % 3 != 0) { 134 | throw new Exception("VGAHEAD file is corrupted!"); 135 | } 136 | 137 | // extract the offsets information from the header 138 | int picsCount = vgaHeadData.limit() / 3; 139 | int[] offsets = new int[picsCount]; 140 | for (int i = 0; i < offsets.length; i++) { 141 | int o0 = vgaHeadData.get() & 0xff; 142 | int o1 = vgaHeadData.get() & 0xff; 143 | int o2 = vgaHeadData.get() & 0xff; 144 | offsets[i] = o0 + (o1 << 8) + (o2 << 16); 145 | } 146 | 147 | ByteBuffer vgaDictData = ByteBuffer.wrap(vgaDictIs.readAllBytes()); 148 | vgaDictData.order(ByteOrder.LITTLE_ENDIAN); 149 | 150 | // extract huffman dictionary 151 | vgaDictData.get(huffmanNodes); 152 | 153 | ByteBuffer vgaGraphData 154 | = ByteBuffer.wrap(vgaGraphIs.readAllBytes()); 155 | 156 | vgaGraphData.order(ByteOrder.LITTLE_ENDIAN); 157 | 158 | // extract the chunks 159 | for (int i = 0; i < offsets.length - 1; i++) { 160 | int length = offsets[i + 1] - offsets[i]; 161 | ByteBuffer compressed = vgaGraphData.slice(offsets[i], length); 162 | compressed.order(ByteOrder.LITTLE_ENDIAN); 163 | byte[] decompressedData = decompressHuffman(compressed); 164 | if (decompressedData == null) continue; 165 | // extract pictable 166 | if (i == 0) { 167 | extractPictable(decompressedData, picsCount); 168 | } 169 | // extract bitmap fonts 170 | else if (i == 1) { // 8x8 small font 171 | VGAGRAPHFont fontWhite 172 | = new VGAGRAPHFont(decompressedData, Color.WHITE); 173 | 174 | VGAGRAPHFont fontBlack 175 | = new VGAGRAPHFont(decompressedData, Color.BLACK); 176 | 177 | VGAGRAPHFont fontYellow 178 | = new VGAGRAPHFont(decompressedData, Color.YELLOW); 179 | 180 | FONTS.put("SMALL_WHITE", fontWhite); 181 | FONTS.put("SMALL_BLACK", fontBlack); 182 | FONTS.put("SMALL_YELLOW", fontYellow); 183 | } 184 | else if (i == 2) { // game options font 185 | VGAGRAPHFont fontGray 186 | = new VGAGRAPHFont(decompressedData, Color.GRAY); 187 | 188 | VGAGRAPHFont fontLightGray 189 | = new VGAGRAPHFont(decompressedData, Color.LIGHT_GRAY); 190 | 191 | VGAGRAPHFont fontDarkRed = new VGAGRAPHFont( 192 | decompressedData, Util.getColor("0x710000ff")); 193 | 194 | VGAGRAPHFont fontYellow = new VGAGRAPHFont( 195 | decompressedData, Color.YELLOW); 196 | 197 | FONTS.put("BIG_GRAY", fontGray); 198 | FONTS.put("BIG_LIGHT_GRAY", fontLightGray); 199 | FONTS.put("BIG_DARK_RED", fontDarkRed); 200 | FONTS.put("BIG_YELLOW", fontYellow); 201 | } 202 | // extract PIC's 203 | else if (i > 2) { 204 | Dimension picDimension = pictable[i]; 205 | if (picDimension != null) { 206 | int picWidth = picDimension.width; 207 | int picHeight = picDimension.height; 208 | if (picWidth <= 0 || picHeight <= 0) continue; 209 | 210 | BufferedImage fixedImage = fixVgaModeYPic( 211 | i, decompressedData, picWidth, picHeight); 212 | 213 | PICS.put(i, fixedImage); 214 | } 215 | } 216 | } 217 | } catch (Exception ex) { 218 | throw new Exception( 219 | "Could not load VGA resources properly!", ex); 220 | } 221 | } 222 | 223 | // note: pictable contain the information about the dimension 224 | // (width and height) for each PIC and is located in the 225 | // first VGAGRAPH chunk. 226 | private static void extractPictable(byte[] pictableData, int picsCount) { 227 | int pictableOffsetIndex = 3; 228 | pictable = new Dimension[picsCount]; 229 | for (int i = 0; i < pictableData.length / 4; i++) { 230 | int b0 = pictableData[4 * i + 0] & 0xff; 231 | int b1 = pictableData[4 * i + 1] & 0xff; 232 | int b2 = pictableData[4 * i + 2] & 0xff; 233 | int b3 = pictableData[4 * i + 3] & 0xff; 234 | int picWidth = b0 + (b1 << 8); 235 | int picHeight = b2 + (b3 << 8); 236 | pictable[i + pictableOffsetIndex] 237 | = new Dimension(picWidth, picHeight); 238 | } 239 | } 240 | 241 | // ref.: https://moddingwiki.shikadi.net/wiki/Huffman_Compression 242 | private static byte[] decompressHuffman(ByteBuffer compressed) { 243 | int decompressedLength = (int) (compressed.getInt() & 0xffffffffl); 244 | if (decompressedLength <= 0) return null; 245 | byte[] data = new byte[decompressedLength]; 246 | int bitIndex = 0; 247 | int dataIndex = 0; 248 | int nodeIndex = 254; 249 | while (dataIndex < decompressedLength) { 250 | int bit = (compressed.get(compressed.position()) >> bitIndex) & 1; 251 | bitIndex++; 252 | if (bitIndex > 7) { 253 | bitIndex = 0; 254 | compressed.position(compressed.position() + 1); 255 | } 256 | if (compressed.position() >= compressed.limit()) break; 257 | if (huffmanNodes[nodeIndex * 4 + 1 + bit * 2] == 0) { 258 | data[dataIndex++] = huffmanNodes[nodeIndex * 4 + bit * 2]; 259 | nodeIndex = 254; 260 | } 261 | else if (huffmanNodes[nodeIndex * 4 + 1 + bit * 2] == 1) { 262 | nodeIndex = huffmanNodes[nodeIndex * 4 + bit * 2] & 0xff; 263 | } 264 | } 265 | return data; 266 | } 267 | 268 | // note: after huffman decompression, the PIC image must be fixed since it 269 | // is arranged in such a way as to facilitate rendering in VGA mode Y. 270 | public static BufferedImage fixVgaModeYPic(int index, 271 | byte[] picData, int picWidth, int picHeight) { 272 | 273 | BufferedImage bi = new BufferedImage(picWidth, picHeight 274 | , BufferedImage.TYPE_BYTE_INDEXED, INDEX_COLOR_MODEL); 275 | 276 | DataBufferByte dataBuffer 277 | = (DataBufferByte) bi.getRaster().getDataBuffer(); 278 | 279 | System.arraycopy( 280 | picData, 0, dataBuffer.getData(), 0, picWidth * picHeight); 281 | 282 | BufferedImage fixedImage = new BufferedImage( 283 | picWidth, picHeight, BufferedImage.TYPE_INT_RGB); 284 | 285 | int w4 = picWidth / 4; 286 | int h4 = picHeight / 4; 287 | for (int y = 0; y < picHeight; y++) { 288 | for (int x = 0; x < picWidth; x++) { 289 | int xs = 4 * (x % w4) + (y / h4); 290 | int ys = 4 * (y % h4) + (x / w4); 291 | int c = bi.getRGB(x, y); 292 | fixedImage.setRGB(xs, ys, c); 293 | } 294 | } 295 | return fixedImage; 296 | } 297 | 298 | public static BufferedImage getPic(int picIndex) { 299 | return PICS.get(picIndex); 300 | } 301 | 302 | public static VGAGRAPHFont getFont(String fontId) { 303 | return FONTS.get(fontId); 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/wolf3d/asset/loader/VSWAPLoader.java: -------------------------------------------------------------------------------- 1 | package wolf3d.asset.loader; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | import java.awt.image.DataBufferByte; 6 | import java.io.FileInputStream; 7 | import java.io.InputStream; 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import static wolf3d.infra.Palette.INDEX_COLOR_MODEL; 15 | import static wolf3d.infra.Palette.PALETTE_BLUE; 16 | import static wolf3d.infra.Palette.PALETTE_GREEN; 17 | import static wolf3d.infra.Palette.PALETTE_RED; 18 | 19 | /** 20 | * VSWAPLoader class. 21 | * 22 | * This is responsible for loading the assets present in the VSWAP file. 23 | * 24 | * VSWAP doesn't use any compression method, so the assets can 25 | * be extracted directly. 26 | * 27 | * The VSWAP file contains: 28 | * 29 | * - wall textures (64x64 pixels, indexed, 1 byte per color) 30 | * - sprites (items, enemies, etc): use a special format, check the references. 31 | * - digitized sounds (7000Hz unsigned 8 bits mono PCM) 32 | * 33 | * Note: other PIC's (HUD images, bitmap fonts, etc) are located 34 | * in the VGAGRAPH file. 35 | * 36 | * References: 37 | * https://vpoupet.github.io/wolfenstein/docs/files.html 38 | * https://devinsmith.net/backups/bruce/wolf3d.html 39 | * 40 | * @author Leonardo Ono (ono.leo80@gmail.com) 41 | */ 42 | public class VSWAPLoader { 43 | 44 | private static final Map 45 | WALL_TEXTURES = new HashMap<>(); 46 | 47 | private static final Map 48 | SPRITES = new HashMap<>(); 49 | 50 | private static final Map 51 | DIGITIZED_SOUNDS = new HashMap<>(); 52 | 53 | public static void load(String path, String vswapRes) throws Exception { 54 | try ( 55 | InputStream vswapIs = new FileInputStream(path + vswapRes); 56 | ) { 57 | ByteBuffer vswapData = ByteBuffer.wrap(vswapIs.readAllBytes()); 58 | vswapData.order(ByteOrder.LITTLE_ENDIAN); 59 | 60 | int numberOfChunks = vswapData.getShort() & 0xffff; 61 | int indexFirstSprite = vswapData.getShort() & 0xffff; 62 | int indexFirstSound = vswapData.getShort() & 0xffff; 63 | int[] addresses = new int[numberOfChunks]; 64 | int[] lengths = new int[numberOfChunks]; 65 | 66 | for (int i = 0; i < numberOfChunks; i++) { 67 | addresses[i] = (int) (vswapData.getInt() & 0xffffffffl); 68 | } 69 | 70 | for (int i = 0; i < numberOfChunks; i++) { 71 | lengths[i] = vswapData.getShort() & 0xffff; 72 | } 73 | 74 | extractWallTextures(indexFirstSprite, vswapData, addresses); 75 | 76 | extractDigitizedSounds( 77 | indexFirstSound, numberOfChunks, addresses, lengths, vswapData); 78 | 79 | extractSprites(indexFirstSprite 80 | , indexFirstSound - 1, addresses, lengths, vswapData); 81 | 82 | } catch (Exception ex) { 83 | throw new Exception( 84 | "Could not load VSWAP resources properly!", ex); 85 | } 86 | } 87 | 88 | private static void extractWallTextures( 89 | int indexFirstSprite, ByteBuffer vswapData, int[] addresses) { 90 | 91 | for (int i = 0; i < indexFirstSprite; i++) { 92 | vswapData.position(addresses[i]); 93 | BufferedImage wallTextureTmp = new BufferedImage(64, 64 94 | , BufferedImage.TYPE_BYTE_INDEXED, INDEX_COLOR_MODEL); 95 | 96 | DataBufferByte dataBuffer = (DataBufferByte) 97 | wallTextureTmp.getRaster().getDataBuffer(); 98 | 99 | vswapData.get(dataBuffer.getData()); 100 | 101 | // convert to INT_ARGB, flip and rotate the image correctly 102 | BufferedImage wallTexture = new BufferedImage(64, 64 103 | , BufferedImage.TYPE_INT_ARGB); 104 | 105 | Graphics2D g = wallTexture.createGraphics(); 106 | g.translate(0, 64); 107 | g.rotate(Math.toRadians(-90)); 108 | g.translate(64, 0); 109 | g.scale(-1, 1); 110 | g.drawImage(wallTextureTmp, 0, 0, null); 111 | 112 | WALL_TEXTURES.put(i, wallTexture); 113 | } 114 | } 115 | 116 | // sprites are saved using a different format described here: 117 | // https://vpoupet.github.io/wolfenstein/docs/files.html 118 | private static void extractSprites( 119 | int indexFirstSprite, int indexLastSprite 120 | , int[] addresses, int[] lengths, ByteBuffer vswapData) { 121 | 122 | for (int si = indexFirstSprite; si <= indexLastSprite; si++) { 123 | int address = addresses[si]; 124 | int length = lengths[si]; 125 | if (length <= 0) continue; 126 | vswapData.position(address); 127 | int firstCol = vswapData.getShort() & 0xffff; 128 | int lastCol = vswapData.getShort() & 0xffff; 129 | int[] postOffsets = new int[lastCol - firstCol + 1]; 130 | for (int i = 0; i < postOffsets.length; i++) { 131 | postOffsets[i] = vswapData.getShort() & 0xffff; 132 | } 133 | int[] pixelPool 134 | = new int[postOffsets[0] - postOffsets.length * 2 - 4]; 135 | 136 | for (int i = 0; i < pixelPool.length; i++) { 137 | pixelPool[i] = vswapData.get() & 0xff; 138 | } 139 | int[] posts = new int[(length - postOffsets[0]) / 2]; 140 | for (int i = 0; i < posts.length; i++) { 141 | posts[i] = vswapData.getShort()& 0xffff; 142 | } 143 | BufferedImage sprite = new BufferedImage(64, 64 144 | , BufferedImage.TYPE_INT_ARGB); 145 | 146 | int colorIndex = 0; 147 | int postIndex = 0; 148 | int col = firstCol; 149 | while (col <= lastCol) { 150 | int startRow = posts[postIndex + 2] / 2; 151 | int endRow = posts[postIndex + 0] / 2; 152 | for (int row = startRow; row < endRow; row++) { 153 | int r = PALETTE_RED[pixelPool[colorIndex]] & 0xff; 154 | int g = PALETTE_GREEN[pixelPool[colorIndex]] & 0xff; 155 | int b = PALETTE_BLUE[pixelPool[colorIndex]] & 0xff; 156 | int color = 0xff000000 + (r << 16) + (g << 8) + b; 157 | sprite.setRGB(col, row, color); 158 | colorIndex++; 159 | } 160 | postIndex += 3; 161 | while (postIndex < posts.length && posts[postIndex] == 0) { 162 | col++; 163 | postIndex++; 164 | } 165 | } 166 | SPRITES.put(si - indexFirstSprite, sprite); 167 | } 168 | } 169 | 170 | // note: 1 digitized sound can consist of 1 or more chunks. 171 | // chunk length less than 4096 indicates the last chunk. 172 | private static void extractDigitizedSounds( 173 | int indexFirstSound, int numberOfChunks, int[] addresses 174 | , int[] lengths, ByteBuffer vswapData) { 175 | 176 | List digitizedSoundMultipleChunks = new ArrayList<>(); 177 | int digitizedSoundMultipleChunksLength = 0; 178 | int digitizedSoundIndex = 0; 179 | for (int i = indexFirstSound; i < numberOfChunks; i++) { 180 | int chunkAddress = addresses[i]; 181 | int chunkLength = lengths[i]; 182 | vswapData.position(chunkAddress); 183 | byte[] soundData = new byte[chunkLength]; 184 | vswapData.get(soundData); 185 | digitizedSoundMultipleChunks.add(soundData); 186 | digitizedSoundMultipleChunksLength += chunkLength; 187 | if (chunkLength < 4096) { 188 | final int soundPrioritySize = 2; 189 | ByteBuffer multipleChunks = ByteBuffer.allocate( 190 | digitizedSoundMultipleChunksLength + soundPrioritySize); 191 | 192 | digitizedSoundMultipleChunks.forEach( 193 | chunk -> multipleChunks.put(chunk)); 194 | 195 | DIGITIZED_SOUNDS.put( 196 | digitizedSoundIndex++, multipleChunks.array()); 197 | 198 | digitizedSoundMultipleChunks.clear(); 199 | digitizedSoundMultipleChunksLength = 0; 200 | } 201 | } 202 | } 203 | 204 | public static BufferedImage getWallTexture(int wallTextureIndex) { 205 | return WALL_TEXTURES.get(wallTextureIndex); 206 | } 207 | 208 | public static BufferedImage getSprite(int spriteIndex) { 209 | return SPRITES.get(spriteIndex); 210 | } 211 | 212 | public static byte[] getDigitizedSound(int soundIndex) { 213 | return DIGITIZED_SOUNDS.get(soundIndex); 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/wolf3d/audio/PCMSoundPlayer.java: -------------------------------------------------------------------------------- 1 | package wolf3d.audio; 2 | 3 | import java.util.logging.Logger; 4 | import javax.sound.sampled.AudioFormat; 5 | import javax.sound.sampled.AudioSystem; 6 | import javax.sound.sampled.LineUnavailableException; 7 | import javax.sound.sampled.SourceDataLine; 8 | 9 | /** 10 | * PCMSoundPlayer class. 11 | * 12 | * This allows to play: 13 | * 14 | * - PC Speaker sounds present in the AUDIOT file and it's converted 15 | * to PCM sounds in the AudioResource class. 16 | * 17 | * - Digitized sounds present in the VSWAP file. 18 | * 19 | * 2 last bytes of sound data (byte[]) form a UINTLE16 indicating the sound 20 | * priority. Any sound will interrupt a sound of equal or lower priority. 21 | * 22 | * @author Leonardo Ono (ono.leo80@gmail.com) 23 | */ 24 | public class PCMSoundPlayer { 25 | 26 | private static final int SEND_DATA_SIZE = 500; 27 | 28 | private final AudioFormat audioFormat; 29 | private SourceDataLine sourceDataLine; 30 | private Thread soundThread; 31 | private boolean initialized; 32 | private byte[] newSoundData; 33 | private byte[] currentSoundData; 34 | private int currentSoundIndex; 35 | 36 | public PCMSoundPlayer(int sampleRate) { 37 | audioFormat = new AudioFormat(sampleRate, 8, 1, false, false); 38 | start(); 39 | } 40 | 41 | private void start() { 42 | try { 43 | sourceDataLine = AudioSystem.getSourceDataLine(audioFormat); 44 | sourceDataLine.open(); 45 | sourceDataLine.start(); 46 | initialized = true; 47 | soundThread = new Thread(new SoundPlayback()); 48 | soundThread.start(); 49 | } catch (LineUnavailableException ex) { 50 | Logger.getLogger(PCMSoundPlayer.class.getName()) 51 | .log(java.util.logging.Level.SEVERE, null, ex); 52 | 53 | initialized = false; 54 | } 55 | } 56 | 57 | public void dispose() { 58 | sourceDataLine.close(); 59 | initialized = false; 60 | } 61 | 62 | private static final int PRIORITY_SIZE = 2; 63 | 64 | private class SoundPlayback implements Runnable { 65 | 66 | @Override 67 | public void run() { 68 | while (initialized) { 69 | synchronized (this) { 70 | if (newSoundData != null) { 71 | if (checkPriority(newSoundData, currentSoundData)) { 72 | currentSoundIndex = 0; 73 | currentSoundData = newSoundData; 74 | sourceDataLine.flush(); 75 | } 76 | newSoundData = null; 77 | } 78 | } 79 | if (currentSoundData != null) { 80 | 81 | int len1 = SEND_DATA_SIZE; 82 | int len2 = currentSoundData.length 83 | - PRIORITY_SIZE - currentSoundIndex; 84 | 85 | int smallestLen = Math.min(len1, len2); 86 | sourceDataLine.write( 87 | currentSoundData, currentSoundIndex, smallestLen); 88 | 89 | currentSoundIndex += SEND_DATA_SIZE; 90 | if (currentSoundIndex 91 | >= currentSoundData.length - PRIORITY_SIZE) { 92 | 93 | currentSoundData = null; 94 | } 95 | } 96 | else if (sourceDataLine.available() 97 | == sourceDataLine.getBufferSize()) { 98 | 99 | sourceDataLine.flush(); 100 | } 101 | 102 | try { 103 | Thread.sleep(1000 / 90); 104 | } catch (InterruptedException ex) { } 105 | } 106 | } 107 | 108 | } 109 | 110 | // does sound s1 have more priority than s2 ? 111 | private static boolean checkPriority(byte[] s1, byte[] s2) { 112 | if (s2 == null) return true; 113 | int s1Priority = s1[s1.length - 2] & 0xff; 114 | s1Priority += (s1[s1.length - 1] & 0xff) << 8; 115 | int s2Priority = s2[s2.length - 2] & 0xff; 116 | s2Priority += (s2[s2.length - 1] & 0xff) << 8; 117 | return s1Priority >= s2Priority; 118 | } 119 | 120 | public synchronized void play(byte[] soundData) { 121 | newSoundData = soundData; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Audio.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.nio.ByteBuffer; 4 | import wolf3d.audio.IMFMusicPlayer; 5 | import wolf3d.audio.PCMSoundPlayer; 6 | import static wolf3d.infra.Settings.*; 7 | 8 | /** 9 | * Audio class. 10 | * 11 | * @author Leonardo Ono (ono.leo80@gmail.com) 12 | */ 13 | public class Audio { 14 | 15 | private static PCMSoundPlayer pcSpeakerSoundPlayer; 16 | private static PCMSoundPlayer digitizedSoundPlayer; 17 | 18 | // TODO: adlib sound player 19 | 20 | private static String currentMusicId; 21 | private static IMFMusicPlayer imfMusicPlayer; 22 | 23 | public static void initialize() { 24 | pcSpeakerSoundPlayer = new PCMSoundPlayer(PC_SPEAKER_SOUND_PCM_FREQ); 25 | digitizedSoundPlayer = new PCMSoundPlayer(DIGITIZED_SOUND_PCM_FREQ); 26 | imfMusicPlayer = new IMFMusicPlayer(IMF_MUSIC_PLAYBACK_RATE); 27 | } 28 | 29 | public static void playMusic(String musicId) { 30 | ByteBuffer musicImfData = Resource.getMusic(musicId); 31 | imfMusicPlayer.play(musicImfData); 32 | currentMusicId = musicId; 33 | } 34 | 35 | public static String getCurrentMusicId() { 36 | return currentMusicId; 37 | } 38 | 39 | public static void playMusicByFloorNumber(int floor) { 40 | String musicId 41 | = Resource.getProperty("INFO_FLOOR_" + floor + "_MUSIC_ID"); 42 | 43 | playMusic(musicId); 44 | } 45 | 46 | public static void stopMusic() { 47 | imfMusicPlayer.stop(); 48 | } 49 | 50 | // volume = 0~255 51 | public static void setMusicVolume(int volume) { 52 | imfMusicPlayer.setVolume(volume); 53 | } 54 | 55 | // volumeScale = 0.0~1.0 56 | public static void setMusicScaleVolume(double volumeScale) { 57 | imfMusicPlayer.setVolumeScale(volumeScale); 58 | } 59 | 60 | public static void playSound(String soundId) { 61 | boolean digitizedSoundOk = false; 62 | 63 | if (Resource.hasProperty("DIGITIZED_SOUND_" + soundId)) { 64 | int si = Resource.getIntProperty("DIGITIZED_SOUND_" + soundId); 65 | byte[] soundData = Resource.getDigitizedSound(si); 66 | if (soundData != null) { 67 | digitizedSoundPlayer.play(soundData); 68 | digitizedSoundOk = true; 69 | } 70 | } 71 | 72 | if (!digitizedSoundOk 73 | && Resource.hasProperty("EFFECT_SOUND_" + soundId)) { 74 | 75 | int si = Resource.getIntProperty("EFFECT_SOUND_" + soundId); 76 | byte[] soundData = Resource.getPCSpeakerSound(si); 77 | pcSpeakerSoundPlayer.play(soundData); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/wolf3d/infra/CardinalDirection.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | /** 4 | * CardinalDirection enum. 5 | * 6 | * @author Leonardo Ono (ono.leo80@gmail.com) 7 | */ 8 | public enum CardinalDirection { 9 | 10 | S( 0, 1, Math.toRadians( 90)), 11 | SW(-1, 1, Math.toRadians(135)), 12 | W(-1, 0, Math.toRadians(180)), 13 | NW(-1, -1, Math.toRadians(225)), 14 | N( 0, -1, Math.toRadians(270)), 15 | NE( 1, -1, Math.toRadians(315)), 16 | E( 1, 0, Math.toRadians( 0)), 17 | SE( 1, 1, Math.toRadians( 45)); 18 | 19 | public final int dx, dy; 20 | public final double angle; 21 | 22 | CardinalDirection(int dx, int dy, double angle) { 23 | this.dx = dx; 24 | this.dy = dy; 25 | this.angle = angle; 26 | } 27 | 28 | public static CardinalDirection get(int i) { 29 | switch (i) { 30 | case 0: return CardinalDirection.E; 31 | case 1: return CardinalDirection.N; 32 | case 2: return CardinalDirection.W; 33 | case 3: return CardinalDirection.S; 34 | } 35 | return null; 36 | } 37 | 38 | public static CardinalDirection getPathDirection(int i) { 39 | switch (i) { 40 | case 0: return CardinalDirection.E; 41 | case 1: return CardinalDirection.NE; 42 | case 2: return CardinalDirection.N; 43 | case 3: return CardinalDirection.NW; 44 | case 4: return CardinalDirection.W; 45 | case 5: return CardinalDirection.SW; 46 | case 6: return CardinalDirection.S; 47 | case 7: return CardinalDirection.SE; 48 | } 49 | return null; 50 | } 51 | 52 | public static CardinalDirection getOpposite(CardinalDirection d) { 53 | switch (d) { 54 | case E: return W; 55 | case NE: return SW; 56 | case N: return S; 57 | case NW: return SE; 58 | case W: return E; 59 | case SW: return NE; 60 | case S: return N; 61 | case SE: return NW; 62 | } 63 | return null; 64 | } 65 | 66 | // dir 0=CCW 1=CW 67 | public static CardinalDirection rotate(CardinalDirection d, int dir) { 68 | switch (d) { 69 | case E: return dir == 0 ? N : S; 70 | case N: return dir == 0 ? W : E; 71 | case W: return dir == 0 ? S : N; 72 | case S: return dir == 0 ? E : W; 73 | } 74 | return null; 75 | } 76 | 77 | public static CardinalDirection getDirection(int dx, int dy) { 78 | if (dx == 1 && dy == 0) return E; 79 | if (dx == 1 && dy == 1) return SE; 80 | if (dx == 0 && dy == 1) return S; 81 | if (dx == -1 && dy == 1) return SW; 82 | if (dx == -1 && dy == 0) return W; 83 | if (dx == -1 && dy == -1) return NW; 84 | if (dx == 0 && dy == -1) return N; 85 | if (dx == 1 && dy == -1) return NE; 86 | return null; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Display.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.awt.Cursor; 5 | import java.awt.DisplayMode; 6 | import java.awt.GraphicsDevice; 7 | import java.awt.GraphicsEnvironment; 8 | import java.awt.Point; 9 | import java.awt.Toolkit; 10 | import java.awt.event.KeyAdapter; 11 | import java.awt.event.KeyEvent; 12 | import java.awt.image.BufferedImage; 13 | import javax.swing.JFrame; 14 | 15 | /** 16 | * Display class. 17 | * 18 | * @author Leonardo Ono (ono.leo80@gmail.com) 19 | */ 20 | public class Display extends JFrame { 21 | 22 | private final GameCanvas gameCanvas; 23 | private final GraphicsEnvironment ge; 24 | private final GraphicsDevice gd; 25 | private DisplayMode fullscreenDisplayMode; 26 | 27 | public Display(GameCanvas gameCanvas) { 28 | gameCanvas.setBackground(Color.BLACK); 29 | this.gameCanvas = gameCanvas; 30 | ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 31 | gd = ge.getDefaultScreenDevice(); 32 | fullscreenDisplayMode = gd.getDisplayMode(); 33 | } 34 | 35 | public DisplayMode getFullscreenDisplayMode() { 36 | return fullscreenDisplayMode; 37 | } 38 | 39 | public void setFullscreenDisplayMode(DisplayMode fullscreenDisplayMode) { 40 | this.fullscreenDisplayMode = fullscreenDisplayMode; 41 | } 42 | 43 | public void start() { 44 | getContentPane().add(gameCanvas); 45 | pack(); 46 | setLocationRelativeTo(null); 47 | setLocation(100, 50); 48 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 49 | setVisible(true); 50 | gameCanvas.start(); 51 | gameCanvas.requestFocus(); 52 | Input.addListener(new KeyHandler()); 53 | } 54 | 55 | public void setWindowedMode() { 56 | dispose(); 57 | setUndecorated(false); 58 | gd.setFullScreenWindow(null); 59 | setVisible(true); 60 | gameCanvas.updateAspectRatioDimension(); 61 | gameCanvas.requestFocus(); 62 | showMouseCursor(); 63 | } 64 | 65 | public void setFullscreenMode() { 66 | dispose(); 67 | setUndecorated(true); 68 | setVisible(true); 69 | //System.out.println("fullscreen supported: " 70 | // + gd.isFullScreenSupported()); 71 | gd.setFullScreenWindow(this); 72 | gd.setDisplayMode(fullscreenDisplayMode); 73 | gameCanvas.updateAspectRatioDimension(); 74 | gameCanvas.requestFocus(); 75 | hideMouseCursor(); 76 | } 77 | 78 | // private void listSupportedDisplayModes() { 79 | // System.out.println("display change supported: " 80 | // + gd.isDisplayChangeSupported()); 81 | // 82 | // int i = 0; 83 | // for (DisplayMode dm : gd.getDisplayModes()) { 84 | // System.out.println(i++ + "Diplay mode: " 85 | // + dm.getWidth() + ", " + dm.getHeight()); 86 | // } 87 | // //gd.setDisplayMode(gd.getDisplayModes()[9]); 88 | // } 89 | 90 | private class KeyHandler extends KeyAdapter { 91 | 92 | @Override 93 | public void keyPressed(KeyEvent e) { 94 | // keep aspect ratio 95 | if (e.getKeyCode() == Settings.KEY_KEEP_ASPECT_RATIO) { 96 | Settings.keepAspectRatio = !Settings.keepAspectRatio; 97 | } 98 | // full screen 99 | if (e.getKeyCode() == Settings.KEY_FULLSCREEN) { 100 | if (gd.getFullScreenWindow() == null) { 101 | setFullscreenMode(); 102 | } 103 | else { 104 | setWindowedMode(); 105 | } 106 | } 107 | } 108 | 109 | } 110 | 111 | public void hideMouseCursor() { 112 | Toolkit toolkit = Toolkit.getDefaultToolkit(); 113 | setCursor(toolkit.createCustomCursor( 114 | new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB) 115 | , new Point(0, 0), "null")); 116 | } 117 | 118 | public void showMouseCursor() { 119 | setCursor(Cursor.getDefaultCursor()); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /src/wolf3d/infra/Doors.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import wolf3d.infra.Objs.EnemyObj; 6 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.DEAD; 7 | import wolf3d.infra.Tiles.DoorTile; 8 | import static wolf3d.infra.Tiles.DoorTile.DoorState.*; 9 | 10 | /** 11 | * Doors class. 12 | * 13 | * Handle all active doors. 14 | * 15 | * @author Leonardo Ono (ono.leo80@gmail.com) 16 | */ 17 | public class Doors { 18 | 19 | private static final double DOOR_SPEED = 0.02; 20 | 21 | private static final Set activatedDoors = new HashSet<>(); 22 | private static final Set deactivatedDoors = new HashSet<>(); 23 | 24 | public static void activateDoor(DoorTile door) { 25 | if ((door.getDoorState() == CLOSING 26 | || door.getDoorState() == CLOSED) 27 | && canPlayerHearDoor(door)) { 28 | 29 | Audio.playSound("OPENDOOR"); 30 | } 31 | 32 | door.setDoorState(OPENING); 33 | activatedDoors.add(door); 34 | } 35 | 36 | public static void fixedUpdateDoors() { 37 | for (DoorTile door : activatedDoors) { 38 | switch (door.getDoorState()) { 39 | case OPENING -> { 40 | door.incDoorOpenRate(DOOR_SPEED); 41 | if (door.getDoorOpenRate() > 1.0) { 42 | door.setDoorOpenRate(1.0); 43 | door.setDoorState(OPEN); 44 | door.setDoorCloseTime(Util.getTimeMs() + 3000); 45 | } 46 | GameMap.connectRooms( 47 | door.getConnectedRoom1(), door.getConnectedRoom2()); 48 | } 49 | case OPEN -> { 50 | boolean isDoorObstructed = door.isDoorObstructed(); 51 | if (Util.getTimeMs() >= door.getDoorCloseTime() 52 | && !isDoorObstructed && !door.isBlockMovement()) { 53 | 54 | door.setDoorState(CLOSING); 55 | if (canPlayerHearDoor(door)) { 56 | Audio.playSound("CLOSEDOOR"); 57 | } 58 | } 59 | EnemyObj enemyObj = door.getObstructingEnemy(); 60 | if (isDoorObstructed && enemyObj != null 61 | && enemyObj.getEnemyState() == DEAD) { 62 | 63 | deactivatedDoors.add(door); 64 | } 65 | } 66 | case CLOSING -> { 67 | door.incDoorOpenRate(-DOOR_SPEED); 68 | if (door.getDoorOpenRate() < 0.0) { 69 | door.setDoorOpenRate(0.0); 70 | door.setObstructingEnemy(null); 71 | door.setDoorState(CLOSED); 72 | deactivatedDoors.add(door); 73 | GameMap.disconnectRooms( 74 | door.getConnectedRoom1(), door.getConnectedRoom2()); 75 | } 76 | } 77 | } 78 | } 79 | if (!deactivatedDoors.isEmpty()) { 80 | activatedDoors.removeAll(deactivatedDoors); 81 | deactivatedDoors.clear(); 82 | } 83 | } 84 | 85 | private static boolean canPlayerHearDoor(DoorTile door) { 86 | int playerRoomId = Player.getCurrentRoomId(); 87 | int soundRoomId1 = door.getConnectedRoom1(); 88 | int soundRoomId2 = door.getConnectedRoom2(); 89 | boolean c1 = playerRoomId == soundRoomId1; 90 | boolean c2 = playerRoomId == soundRoomId2; 91 | boolean c3 = GameMap.isRoomConnected(playerRoomId, soundRoomId1); 92 | boolean c4 = GameMap.isRoomConnected(playerRoomId, soundRoomId2); 93 | return c1 || c2 || c3 || c4; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Enemies.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import wolf3d.infra.Objs.EnemyObj; 6 | import wolf3d.infra.Objs.EnemyObj.EnemyState; 7 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.*; 8 | import wolf3d.infra.Objs.Obj; 9 | import static wolf3d.infra.Objs.ObjType.*; 10 | import wolf3d.infra.Objs.PathObj; 11 | import wolf3d.infra.Tiles.DoorTile; 12 | import static wolf3d.infra.Tiles.DoorTile.DoorState.*; 13 | import wolf3d.infra.Tiles.Tile; 14 | import static wolf3d.infra.Tiles.TileType.*; 15 | 16 | /** 17 | * Enemies class. 18 | * 19 | * @author Leonardo Ono (ono.leo80@gmail.com) 20 | */ 21 | public class Enemies { 22 | 23 | private static final List ENEMIES = new ArrayList<>(); 24 | 25 | public static void clear() { 26 | ENEMIES.clear(); 27 | } 28 | 29 | public static List getEnemies() { 30 | return ENEMIES; 31 | } 32 | 33 | public static void addEnemy(EnemyObj enemy) { 34 | // if enemy not dead, then add collision 35 | if (enemy.getEnemyState() != DEAD) { 36 | int enemyCol = enemy.getCol(); 37 | int enemyRow = enemy.getRow(); 38 | GameMap.getTile(enemyCol, enemyRow).setBlockMovement(true); 39 | } 40 | ENEMIES.add(enemy); 41 | } 42 | 43 | public static void fixedUpdateEnemies() { 44 | for (EnemyObj enemy : ENEMIES) { 45 | switch (enemy.getEnemyState()) { 46 | case STAND -> updateStand(enemy); 47 | case PATROL -> updatePatrol(enemy); 48 | case WALK -> updateWalk(enemy); 49 | case CHASE_REACT -> updateChaseReact(enemy); 50 | case CHASE -> updateChase(enemy); 51 | case ATTACK_REACT -> updateAttackReact(enemy); 52 | case ATTACK -> updateAttack(enemy); 53 | case HIT -> updateHit(enemy); 54 | case DYING -> updateDying(enemy); 55 | // case DEAD -> updateDead(enemy); 56 | } 57 | } 58 | } 59 | 60 | private static void updateStand(EnemyObj enemy) { 61 | checkMustStartChasingPlayer(enemy); 62 | } 63 | 64 | private static final double PROXIMITY_DISTANCE = 2.5; 65 | 66 | private static boolean checkMustStartChasingPlayer(EnemyObj enemy) { 67 | // proximity 68 | double dist = enemy.calculateDistanceFromPlayer(); 69 | if (dist <= PROXIMITY_DISTANCE && Math.random() < 0.1 70 | && enemy.canSeePlayer(false) ) { 71 | 72 | enemy.chaseReact(); 73 | return true; 74 | } 75 | 76 | // enemy can see the player 77 | if (enemy.canSeePlayer(true)) { 78 | enemy.chaseReact(); 79 | return true; 80 | } 81 | 82 | // propagated sound ok 83 | // implemented in Player.propagateGunFiringSoundThroughoutRooms() 84 | 85 | return false; 86 | } 87 | 88 | // check if there is a new the path direction and updates the 89 | // target position, the enemy can open door if necessary. 90 | private static void updatePatrol(EnemyObj enemy) { 91 | // update new path direction? 92 | Obj obj = GameMap.getObj(enemy.getCol(), enemy.getRow()); 93 | if (obj != null && obj.getType() == PATH) { 94 | PathObj pathObj = (PathObj) obj; 95 | enemy.setDirection(pathObj.getDirection()); 96 | } 97 | 98 | if (!tryToWalk(enemy, PATROL)) { 99 | enemy.setAnimationFrame(0); 100 | } 101 | } 102 | 103 | private static boolean tryToWalk(EnemyObj enemy, EnemyState restoreState) { 104 | // check door 105 | int mc = enemy.getCol() + enemy.getDirection().dx; 106 | int mr = enemy.getRow() + enemy.getDirection().dy; 107 | Tile destTile = GameMap.getTile(mc, mr); 108 | if (destTile.getType() == DOOR) { 109 | DoorTile doorTile = (DoorTile) destTile; 110 | if (((doorTile.getObstructingEnemy() != null 111 | && doorTile.getObstructingEnemy().getEnemyState() != DEAD) 112 | && doorTile.getObstructingEnemy() != enemy) 113 | || doorTile.isTileObstructedByPlayer()) { 114 | 115 | return false; 116 | } 117 | if (enemy.isAbleToOpenDoor()) { 118 | doorTile.setObstructingEnemy(enemy); 119 | if (doorTile.getDoorState() != OPEN) { 120 | if (doorTile.getDoorState() == CLOSED 121 | || doorTile.getDoorState() == CLOSING) { 122 | 123 | Doors.activateDoor(doorTile); 124 | } 125 | return false; 126 | } 127 | } 128 | else if (doorTile.getDoorState() == OPEN) { 129 | doorTile.setObstructingEnemy(enemy); 130 | } 131 | else if (doorTile.getDoorState() != OPEN) { 132 | return true; 133 | } 134 | } 135 | 136 | // destination tile is blocked 137 | if (destTile.isBlockMovement()) { 138 | return false; 139 | } 140 | 141 | // update collision 142 | mc = enemy.getCol(); 143 | mr = enemy.getRow(); 144 | GameMap.getTile(mc, mr).setBlockMovement(false); 145 | destTile.setBlockMovement(true); 146 | 147 | double etx = enemy.getCol() + 0.5 + enemy.getDirection().dx; 148 | double ety = enemy.getRow() + 0.5 + enemy.getDirection().dy; 149 | enemy.setEnemyTargetPosition(etx, ety); 150 | enemy.setEnemyRestoreState(restoreState); 151 | enemy.setEnemyState(WALK); 152 | return true; 153 | } 154 | 155 | // walk until target position 156 | private static void updateWalk(EnemyObj enemy) { 157 | // check if need to start chasing 158 | if (enemy.getEnemyRestoreState() == PATROL 159 | && checkMustStartChasingPlayer(enemy)) return; 160 | 161 | double distanceFromPlayer = enemy.calculateDistanceFromPlayer() ; 162 | 163 | // check if need to start attacking 164 | if (enemy.getEnemyRestoreState() == CHASE 165 | && Util.getTimeMs() >= enemy.getAttackTime()) { 166 | 167 | if ((enemy.canSeePlayer(true) || distanceFromPlayer <= 1.5) 168 | && distanceFromPlayer <= enemy.getMinAttackDistance()) { 169 | 170 | enemy.attackReact(); 171 | return; 172 | } 173 | else { 174 | planNextAttack(enemy); 175 | } 176 | } 177 | 178 | // if enemy is too close, speed up the fire rate 179 | if (enemy.calculateDistanceFromPlayer() <= 1.5) { 180 | enemy.setAttackTime(enemy.getAttackTime() - 50); 181 | return; 182 | } 183 | 184 | double dx = enemy.getEnemyTargetX() - enemy.getEnemyX(); 185 | double dy = enemy.getEnemyTargetY() - enemy.getEnemyY(); 186 | double dist = Math.hypot(dx, dy); 187 | double walkSpeed = enemy.getSpeed(); 188 | if (dist >= walkSpeed) { 189 | dx /= dist; 190 | dy /= dist; 191 | double nex = enemy.getEnemyX() + dx * walkSpeed; 192 | double ney = enemy.getEnemyY() + dy * walkSpeed; 193 | enemy.setEnemyPosition(nex, ney); 194 | } 195 | else { 196 | enemy.setEnemyPosition( 197 | enemy.getEnemyTargetX(), enemy.getEnemyTargetY()); 198 | 199 | enemy.setEnemyGridPosition( 200 | (int) enemy.getEnemyTargetX(), (int) enemy.getEnemyTargetY()); 201 | 202 | enemy.setEnemyState(enemy.getEnemyRestoreState()); 203 | } 204 | 205 | // boss has only 4 frames for walking, no 360 view. 206 | if (enemy.isBoss()) { 207 | enemy.setAnimationFrame( 208 | (int) (System.nanoTime() * 0.000000005) % 4); 209 | } 210 | else { 211 | enemy.setAnimationFrame( 212 | 8 * ((int) (System.nanoTime() * 0.000000005) % 4)); 213 | } 214 | } 215 | 216 | private static void updateChaseReact(EnemyObj enemy) { 217 | if (Util.getTimeMs() >= enemy.getReactTime()) { 218 | Audio.playSound(enemy.getRandomHaltSoundId()); 219 | enemy.setEnemyRestoreState(CHASE); 220 | enemy.setEnemyState(WALK); 221 | } 222 | enemy.setAnimationFrame(0); 223 | } 224 | 225 | private static void updateChase(EnemyObj enemy) { 226 | CardinalDirection nextDir = enemy.isKeepDirection() 227 | ? enemy.getDirection() : getNextChaseDirection(enemy); 228 | 229 | if (nextDir != null) { 230 | enemy.setDirection(nextDir); 231 | boolean keepChaseDirection = !tryToWalk(enemy, CHASE); 232 | enemy.setKeepDirection(keepChaseDirection); 233 | } 234 | } 235 | 236 | private static CardinalDirection getNextChaseDirection(EnemyObj enemy) { 237 | int dx = (int) Player.getPlayerX() - (int) enemy.getEnemyX(); 238 | int dy = (int) Player.getPlayerY() - (int) enemy.getEnemyY(); 239 | 240 | dx = (int) Math.signum(dx); 241 | dy = (int) Math.signum(dy); 242 | 243 | if (dx == 0) dx = Math.random() < 0.5 ? 1 : -1; 244 | if (dy == 0) dy = Math.random() < 0.5 ? 1 : -1; 245 | 246 | CardinalDirection nextDir = CardinalDirection.getDirection(dx, dy); 247 | if (isNextDirectionFree(enemy, nextDir)) { 248 | return nextDir; 249 | } 250 | 251 | CardinalDirection nextDirDx = CardinalDirection.getDirection(dx, 0); 252 | if (Math.random() < 0.5 && isNextDirectionFree(enemy, nextDirDx)) { 253 | return nextDirDx; 254 | } 255 | 256 | CardinalDirection nextDirDy = CardinalDirection.getDirection(0, dy); 257 | if (isNextDirectionFree(enemy, nextDirDy)) { 258 | return nextDirDy; 259 | } 260 | 261 | CardinalDirection nextDirOppos = CardinalDirection.getOpposite(nextDir); 262 | 263 | CardinalDirection nextDirOpposDx 264 | = CardinalDirection.getDirection(nextDirOppos.dx, 0); 265 | 266 | if (Math.random() < 0.05 267 | && isNextDirectionFree(enemy, nextDirOpposDx)) { 268 | 269 | return nextDirOpposDx; 270 | } 271 | 272 | CardinalDirection nextDirOpposDy 273 | = CardinalDirection.getDirection(0, nextDirOppos.dy); 274 | 275 | if (Math.random() < 0.05 276 | && isNextDirectionFree(enemy, nextDirOpposDy)) { 277 | 278 | return nextDirOpposDy; 279 | } 280 | 281 | return null; 282 | } 283 | 284 | private static boolean isNextDirectionFree( 285 | EnemyObj enemy, CardinalDirection nextDir) { 286 | 287 | if (nextDir == null) return false; 288 | 289 | int ec = enemy.getCol(); 290 | int er = enemy.getRow(); 291 | 292 | Tile t1 = GameMap.getTile(ec + nextDir.dx, er); 293 | Tile t2 = GameMap.getTile(ec, er + nextDir.dy); 294 | Tile t3 = GameMap.getTile(ec + nextDir.dx, er + nextDir.dy); 295 | 296 | boolean c1 = !t1.isTileObstructedByPlayer() 297 | && (!t1.isBlockMovement() 298 | || (enemy.isAbleToOpenDoor() && t1.getType() == DOOR 299 | && (((DoorTile) t1).getObstructingEnemy() == null 300 | || ((DoorTile) t1).getObstructingEnemy() 301 | .getEnemyState() == DEAD))); 302 | 303 | boolean c2 = !t2.isTileObstructedByPlayer() 304 | && (!t2.isBlockMovement() 305 | || (enemy.isAbleToOpenDoor() && t2.getType() == DOOR 306 | && (((DoorTile) t2).getObstructingEnemy() == null 307 | || ((DoorTile) t2).getObstructingEnemy() 308 | .getEnemyState() == DEAD))); 309 | 310 | boolean c3 = !t3.isTileObstructedByPlayer() && !t3.isBlockMovement(); 311 | 312 | if (nextDir.dx != 0 && nextDir.dy == 0) return c1; 313 | else if (nextDir.dx == 0 && nextDir.dy != 0) return c2; 314 | else return c1 && c2 && c3; 315 | } 316 | 317 | private static void updateAttackReact(EnemyObj enemy) { 318 | double nextFrame = enemy.getAnimationFrame() + 0.1; 319 | if (nextFrame >= 2) nextFrame = 2; 320 | enemy.setAnimationFrame(nextFrame); 321 | if (Util.getTimeMs() >= enemy.getReactTime()) { 322 | Audio.playSound(enemy.getRandomAttackSoundId()); 323 | enemy.setEnemyState(ATTACK); 324 | enemy.setAnimationFrame(2.99); 325 | enemy.setUse360View(false); 326 | Player.tryToHit(enemy); 327 | } 328 | } 329 | 330 | private static void updateAttack(EnemyObj enemy) { 331 | double nextFrame = enemy.getAnimationFrame() - 0.1; 332 | enemy.setAnimationFrame(nextFrame); 333 | if (nextFrame <= 1) { 334 | planNextAttack(enemy); 335 | } 336 | } 337 | 338 | private static void planNextAttack(EnemyObj enemy) { 339 | double halfDist = enemy.calculateDistanceFromPlayer() / 2; 340 | enemy.setAttackTime(Util.getTimeMs() + (int) (300 * halfDist) 341 | + Util.random(1 +(int) (300 * halfDist))); 342 | 343 | if (enemy.isBoss()) { 344 | enemy.setAttackTime( 345 | (long) (Util.getTimeMs() + 250 + 100 * halfDist)); 346 | } 347 | 348 | enemy.setEnemyRestoreState(CHASE); 349 | enemy.setEnemyState(WALK); 350 | enemy.setUse360View(true); 351 | enemy.setAnimationFrame(0); 352 | } 353 | 354 | public static void updateHit(EnemyObj enemy) { 355 | if (Util.getTimeMs() >= enemy.getReactTime()) { 356 | enemy.setEnemyState(WALK); 357 | enemy.setEnemyRestoreState(CHASE); 358 | enemy.setUse360View(true); 359 | enemy.setAnimationFrame(0); 360 | } 361 | } 362 | 363 | public static void updateDying(EnemyObj enemy) { 364 | double currentFrame = enemy.getAnimationFrame(); 365 | double nextFrame = currentFrame + 0.10; 366 | 367 | // give the opportunity to hear the death sound 368 | if (nextFrame > 0.85 && nextFrame < 0.95) { 369 | Audio.playSound(enemy.getRandomDeathSoundId()); 370 | } 371 | 372 | enemy.setAnimationFrame(nextFrame); 373 | if (nextFrame >= enemy.getAnimationFramesCount()) { 374 | enemy.setEnemyState(DEAD); 375 | enemy.setAnimationFrame(0); 376 | } 377 | } 378 | 379 | // public static void updateDead(EnemyObj enemy) { 380 | // } 381 | 382 | public static void tryToHit(EnemyObj enemy, double minHitDistance) { 383 | if (enemy.getEnemyState() == DYING 384 | || enemy.getEnemyState() == DEAD 385 | || enemy.getEnemyState() == HIT 386 | || enemy.getEnemyState() == ATTACK) { 387 | 388 | return; 389 | } 390 | 391 | double dist = enemy.calculateDistanceFromPlayer(); 392 | //System.out.println("dist " + dist); 393 | // make boss very hard to hit 394 | double bossMissChange = 0; 395 | if (dist < 3) { 396 | bossMissChange = 1.0; 397 | } 398 | else if (dist < 6) { 399 | bossMissChange = 0.98; 400 | } 401 | else if (dist < 10) { 402 | bossMissChange = 0.90; 403 | } 404 | else { 405 | bossMissChange = 0.80; 406 | } 407 | 408 | if (enemy.isBoss() && Math.random() < bossMissChange) { 409 | return; 410 | } 411 | 412 | 413 | if (dist > minHitDistance) return; 414 | 415 | //if (Math.random() < (1.0 / (dist * 0.25))) { 416 | //int damage = (int) (200 * (0.5 / dist)); 417 | int damage = Util.random(0, 125); 418 | if (dist < 3) { 419 | damage = Util.random(25, 150); 420 | } 421 | enemy.hit(damage); 422 | if (enemy.getEnemyLife() <= 0) { 423 | enemy.kill(); 424 | } 425 | else { 426 | enemy.setReactTime(Util.getTimeMs() + 200); 427 | enemy.setEnemyState(HIT); 428 | enemy.setUse360View(false); 429 | enemy.setAnimationFrame(0); 430 | } 431 | //} 432 | } 433 | 434 | } 435 | -------------------------------------------------------------------------------- /src/wolf3d/infra/FizzleFade.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | 7 | /** 8 | * FizzleFade class. 9 | * 10 | * Effect when player dies. 11 | * 12 | * Implemented using Linear Feedback Shift Register technique. 13 | * 14 | * @author Leonardo Ono (ono.leo80@gmail.com) 15 | */ 16 | public class FizzleFade { 17 | 18 | private static final BufferedImage BACKGROUND; 19 | private static final Graphics2D BACKGROUND_G2D; 20 | 21 | private static int pixelCount; 22 | private static int fadeDirection; 23 | private static Color fadeColor; 24 | 25 | static { 26 | BACKGROUND = new BufferedImage(320, 200, BufferedImage.TYPE_INT_ARGB); 27 | BACKGROUND_G2D = BACKGROUND.createGraphics(); 28 | BACKGROUND_G2D.setBackground(new Color(0, 0, 0, 0)); 29 | } 30 | 31 | private static int lfsr = 1; 32 | 33 | private static int nextLFSR() { 34 | int bn = (lfsr & 1) ^ ((lfsr & 8) >> 3); 35 | lfsr = (lfsr >> 1) + (bn << 16); 36 | return lfsr; 37 | } 38 | 39 | public static boolean isFinished() { 40 | return fadeDirection == 0 41 | || (fadeDirection > 0 && pixelCount >= 72000) 42 | || (fadeDirection < 0 && pixelCount <= 0); 43 | } 44 | 45 | public static void fixedUpdate() { 46 | if (isFinished()) { 47 | return; 48 | } 49 | 50 | for (int i = 0; i < 1600; i++) { 51 | int rnd = nextLFSR(); 52 | int x = rnd & 0x1ff; 53 | int y = (rnd >> 9) & 0xff; 54 | if (x < 320 && y < 200) { 55 | int color = fadeDirection > 0 ? fadeColor.getRGB() : 0x00000000; 56 | BACKGROUND.setRGB(x, y, color); 57 | pixelCount += fadeDirection; 58 | } 59 | } 60 | } 61 | 62 | public static void draw(Graphics2D g) { 63 | if (fadeDirection < 0 && pixelCount <= 0) { 64 | return; 65 | } 66 | g.drawImage(BACKGROUND, 0, 0, null); 67 | } 68 | 69 | public static void fadeIn(Color fadeColor) { 70 | lfsr = 1; 71 | fadeDirection = 1; 72 | pixelCount = 0; 73 | FizzleFade.fadeColor = fadeColor; 74 | } 75 | 76 | public static void fadeOut() { 77 | lfsr = 1; 78 | fadeDirection = -1; 79 | pixelCount = 72000; 80 | } 81 | 82 | public static void reset() { 83 | BACKGROUND_G2D.clearRect(0, 0, 320, 200); 84 | pixelCount = 0; 85 | fadeDirection = -1; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/wolf3d/infra/GameCanvas.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import static wolf3d.infra.Settings.*; 4 | import java.awt.Graphics2D; 5 | import java.awt.Canvas; 6 | import java.awt.Dimension; 7 | import java.awt.Rectangle; 8 | import java.awt.RenderingHints; 9 | import java.awt.event.ComponentAdapter; 10 | import java.awt.event.ComponentEvent; 11 | import java.awt.image.BufferStrategy; 12 | import java.awt.image.BufferedImage; 13 | 14 | /** 15 | * GameCanvas class. 16 | * 17 | * @author Leonardo Ono (ono.leo80@gmail.com) 18 | */ 19 | public class GameCanvas extends Canvas { 20 | 21 | private final Dimension preferredSize 22 | = new Dimension(PREFERRED_SCREEN_WIDTH, PREFERRED_SCREEN_HEIGHT); 23 | 24 | private BufferedImage offscreen; 25 | private Graphics2D offscreenG2D; 26 | private final Wolf3DGame wolf3DGame; 27 | private BufferStrategy bs; 28 | private boolean running; 29 | private Thread gameLoopThread; 30 | private final Rectangle sizeWithAspectRatio = new Rectangle(); 31 | 32 | public GameCanvas(Wolf3DGame wolf3DGame) { 33 | this.wolf3DGame = wolf3DGame; 34 | } 35 | 36 | @Override 37 | public Dimension getPreferredSize() { 38 | return preferredSize; 39 | } 40 | 41 | public void start() { 42 | updateAspectRatioDimension(); 43 | createBufferStrategy(2); 44 | bs = getBufferStrategy(); 45 | offscreen = new BufferedImage( 46 | CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_ARGB); 47 | 48 | offscreenG2D = offscreen.createGraphics(); 49 | wolf3DGame.start(); 50 | running = true; 51 | gameLoopThread = new Thread(new MainLoop()); 52 | gameLoopThread.start(); 53 | addKeyListener(new Input()); 54 | addComponentListener(new ResizeListener()); 55 | } 56 | 57 | private class ResizeListener extends ComponentAdapter { 58 | 59 | @Override 60 | public void componentResized(ComponentEvent e) { 61 | updateAspectRatioDimension(); 62 | } 63 | 64 | } 65 | 66 | public void updateAspectRatioDimension() { 67 | int left = 0; 68 | int width = getWidth(); 69 | int height = (int) (width / ASPECT_RATIO); 70 | int top = (int) ((getHeight() - height) / 2); 71 | if (top < 0) { 72 | top = 0; 73 | height = getHeight(); 74 | width = (int) (height * ASPECT_RATIO); 75 | left = (int) ((getWidth() - width) / 2); 76 | } 77 | sizeWithAspectRatio.setBounds(left, top, width, height); 78 | } 79 | 80 | private class MainLoop implements Runnable { 81 | 82 | @Override 83 | public void run() { 84 | long currentTime = System.nanoTime(); 85 | long lastTime = currentTime; 86 | long delta; 87 | long unprocessedTime = 0; 88 | while (running) { 89 | currentTime = System.nanoTime(); 90 | delta = currentTime - lastTime; 91 | unprocessedTime += delta; 92 | lastTime = currentTime; 93 | while (unprocessedTime >= TIME_PER_UPDATE) { 94 | unprocessedTime -= TIME_PER_UPDATE; 95 | wolf3DGame.fixedUpdate(); 96 | } 97 | wolf3DGame.update(delta * 0.000000001); 98 | 99 | Graphics2D g = (Graphics2D) bs.getDrawGraphics(); 100 | 101 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION 102 | , RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); 103 | 104 | wolf3DGame.draw(offscreenG2D); 105 | 106 | if (keepAspectRatio) { 107 | g.clearRect(0, 0, getWidth(), getHeight()); 108 | g.drawImage(offscreen 109 | , sizeWithAspectRatio.x, sizeWithAspectRatio.y 110 | , sizeWithAspectRatio.width, sizeWithAspectRatio.height 111 | , null); 112 | } 113 | else { 114 | g.drawImage(offscreen, 0, 0, getWidth(), getHeight(), null); 115 | } 116 | 117 | g.dispose(); 118 | bs.show(); 119 | 120 | try { 121 | Thread.sleep(1); 122 | } catch (InterruptedException ex) { 123 | } 124 | } 125 | } 126 | 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/wolf3d/infra/HUD.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * HUD class. 8 | * 9 | * @author Leonardo Ono (ono.leo80@gmail.com) 10 | */ 11 | public class HUD { 12 | 13 | private static final int FACE_START_PIC_INDEX; 14 | private static final BufferedImage FOOTER_PIC; 15 | private static final BufferedImage[] DIGITS = new BufferedImage[10]; 16 | 17 | private static final BufferedImage KEY_EMPTY_PIC; 18 | private static final BufferedImage KEY_GOLD_PIC; 19 | private static final BufferedImage KEY_SILVER_PIC; 20 | static { 21 | FACE_START_PIC_INDEX = Resource.getIntProperty("PIC_HUD_FACE"); 22 | FOOTER_PIC = Resource.getPic("HUD_FOOTER"); 23 | KEY_EMPTY_PIC = Resource.getPic("HUD_KEY_EMPTY"); 24 | KEY_GOLD_PIC = Resource.getPic("HUD_KEY_GOLD"); 25 | KEY_SILVER_PIC = Resource.getPic("HUD_KEY_SILVER"); 26 | 27 | // digits 28 | int digitStartIndex = Resource.getIntProperty("PIC_HUD_DIGITS"); 29 | for (int i = 0; i < 10; i++) { 30 | DIGITS[i] = Resource.getPic(digitStartIndex + i); 31 | } 32 | } 33 | 34 | private static int faceAnimationIndex = 0; 35 | private static long faceNextFrameTime = 0; 36 | 37 | public static void fixedUpdate() { 38 | if (Util.getTimeMs() >= faceNextFrameTime) { 39 | faceNextFrameTime = Util.getTimeMs() + Util.random(300, 500); 40 | faceAnimationIndex = Util.random(0, 2); 41 | } 42 | } 43 | 44 | public static void draw(Graphics2D g) { 45 | g.drawImage(FOOTER_PIC, 0, 160, null); 46 | drawNumber(g, Wolf3DGame.getFloor(), 2, 16, 176); // floor 47 | drawNumber(g, Wolf3DGame.getScore(), 6, 48, 176); // score 48 | drawNumber(g, Wolf3DGame.getLives(), 1, 112, 176); // lives 49 | drawNumber(g, Wolf3DGame.getLifeEnergy(), 3, 168, 176); // life energy 50 | drawNumber(g, Weapons.getAmmo(), 2, 216, 176); // ammo 51 | 52 | drawKeys(g); 53 | drawFunnyFace(g); 54 | 55 | // draw current selected weapon 56 | if (Weapons.getCurrentPlayerWeapon() != null) { 57 | BufferedImage weaponpic 58 | = Weapons.getCurrentPlayerWeapon().getHudPic(); 59 | 60 | g.drawImage(weaponpic, 256, 168, null); 61 | } 62 | } 63 | 64 | private static void drawKeys(Graphics2D g) { 65 | // draw gold key 66 | if (Player.isPlayerHasGoldKey()) { 67 | g.drawImage(KEY_GOLD_PIC, 240, 164, null); 68 | } 69 | else { 70 | g.drawImage(KEY_EMPTY_PIC, 240, 164, null); 71 | } 72 | // draw silver key 73 | if (Player.isPlayerHasSilverKey()) { 74 | g.drawImage(KEY_SILVER_PIC, 240, 180, null); 75 | } 76 | else { 77 | g.drawImage(KEY_EMPTY_PIC, 240, 180, null); 78 | } 79 | } 80 | 81 | private static void drawFunnyFace(Graphics2D g) { 82 | int faceIndex = 0; 83 | if (Wolf3DGame.getLifeEnergy() <= 0) faceIndex = 7; 84 | else if (Wolf3DGame.getLifeEnergy() <= 10) faceIndex = 6; 85 | else if (Wolf3DGame.getLifeEnergy() <= 20) faceIndex = 5; 86 | else if (Wolf3DGame.getLifeEnergy() <= 30) faceIndex = 4; 87 | else if (Wolf3DGame.getLifeEnergy() <= 40) faceIndex = 3; 88 | else if (Wolf3DGame.getLifeEnergy() <= 50) faceIndex = 2; 89 | else if (Wolf3DGame.getLifeEnergy() <= 60) faceIndex = 1; 90 | else if (Wolf3DGame.getLifeEnergy() <= 70) faceIndex = 0; 91 | 92 | BufferedImage facePic = Resource.getPic( 93 | FACE_START_PIC_INDEX + faceIndex * 3 + faceAnimationIndex); 94 | 95 | if (Wolf3DGame.getLifeEnergy() == 0) { 96 | facePic = Resource.getPic(FACE_START_PIC_INDEX + faceIndex * 3); 97 | } 98 | 99 | g.drawImage(facePic, 136, 164, null); 100 | } 101 | 102 | public static void drawNumber( 103 | Graphics2D g, int number, int numberOfDigits, int x, int y) { 104 | 105 | String numberStr = " ".repeat(numberOfDigits) + number; 106 | numberStr = numberStr.substring( 107 | numberStr.length() - numberOfDigits, numberStr.length()); 108 | 109 | for (int i = 0; i < numberStr.length(); i++) { 110 | char c = numberStr.charAt(i); 111 | if (c != ' ') { 112 | int d = c - '0'; 113 | g.drawImage(DIGITS[d], x, y, null); 114 | } 115 | x += DIGITS[0].getWidth(); 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Input.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.event.KeyEvent; 4 | import java.awt.event.KeyListener; 5 | import java.util.ArrayList; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | /** 11 | * Input class. 12 | * 13 | * @author Leonardo Ono (ono.leo80@gmail.com) 14 | */ 15 | public class Input implements KeyListener { 16 | 17 | private static final Set KEYS_PRESSED = new HashSet<>(); 18 | private static final Set KEYS_PRESSED_CONSUMED = new HashSet<>(); 19 | private static final List LISTENERS = new ArrayList<>(); 20 | 21 | public static void addListener(KeyListener listener) { 22 | Input.LISTENERS.add(listener); 23 | } 24 | 25 | public static synchronized boolean isKeyPressed(int keyCode) { 26 | return KEYS_PRESSED.contains(keyCode); 27 | } 28 | 29 | public static synchronized boolean isKeyJustPressed(int keyCode) { 30 | if (!KEYS_PRESSED_CONSUMED.contains(keyCode) 31 | && KEYS_PRESSED.contains(keyCode)) { 32 | 33 | KEYS_PRESSED_CONSUMED.add(keyCode); 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | @Override 40 | public synchronized void keyTyped(KeyEvent e) { 41 | if (LISTENERS != null) { 42 | LISTENERS.forEach(action -> action.keyTyped(e)); 43 | } 44 | } 45 | 46 | @Override 47 | public synchronized void keyPressed(KeyEvent e) { 48 | if (!KEYS_PRESSED.contains(e.getKeyCode())) { 49 | KEYS_PRESSED.add(e.getKeyCode()); 50 | if (LISTENERS != null) { 51 | LISTENERS.forEach(action -> action.keyPressed(e)); 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public synchronized void keyReleased(KeyEvent e) { 58 | KEYS_PRESSED.remove(e.getKeyCode()); 59 | KEYS_PRESSED_CONSUMED.remove(e.getKeyCode()); 60 | if (LISTENERS != null) { 61 | LISTENERS.forEach(action -> action.keyReleased(e)); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Palette.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.image.IndexColorModel; 4 | 5 | /** 6 | * Palette class. 7 | * 8 | * Contains the Wolfeinstein 3D VGA 256 indexed color palette table. 9 | * 10 | * @author Leonardo Ono (ono.leo80@gmail.com) 11 | */ 12 | public class Palette { 13 | 14 | public static final byte[] PALETTE_RED = { 15 | 0,0,0,0,-88,-88,-88,-88,84,84,84,84,-4,-4,-4,-4,-20,-36,-48,-64,-76 16 | ,-88,-104,-116,124,112,100,84,72,56,44,32,-4,-20,-32,-44,-56,-68,-80 17 | ,-92,-104,-120,124,112,100,88,76,64,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4 18 | ,-4,-28,-52,-76,-100,-4,-4,-4,-4,-4,-4,-4,-4,-28,-52,-76,-100,-124 19 | ,112,88,64,-48,-60,-76,-96,-112,-128,116,96,-40,-68,-100,-128,96,64 20 | ,32,0,0,0,0,0,4,4,4,4,4,4,4,4,4,4,4,4,-40,-72,-100,124,92,64,32,0,0 21 | ,0,0,0,0,0,0,0,92,64,32,0,0,0,0,0,-40,-72,-100,124,92,64,32,0,0,0,0 22 | ,0,0,0,0,0,0,0,0,0,0,0,0,0,40,-4,-4,-4,-4,-4,-76,-88,-104,-128,116 23 | ,96,80,68,52,40,-4,-4,-4,-4,-4,-4,-4,-4,-32,-56,-76,-100,-124,108,88 24 | ,64,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-16,-24,-36,-48,-56,-68,-76,-88 25 | ,-96,-100,-112,-120,-128,116,108,92,84,72,64,56,40,96,0,0,0,0,48,72 26 | ,80,0,28,76,92,64,48,52,-40,-72,-100,116,72,32,32,0,0,0,0,0,0,0,0,0 27 | ,-104 28 | }; 29 | 30 | public static final byte[] PALETTE_GREEN = { 31 | 0,0,-88,-88,0,0,84,-88,84,84,-4,-4,84,84,-4,-4,-20,-36,-48,-64,-76 32 | ,-88,-104,-116,124,112,100,84,72,56,44,32,0,0,0,0,0,0,0,0,0,0,0,0,0 33 | ,0,0,0,-40,-72,-100,124,92,64,32,0,-88,-104,-120,120,108,96,84,76,-4 34 | ,-4,-4,-4,-8,-12,-12,-12,-40,-60,-84,-100,-124,108,84,64,-4,-4,-4,-4 35 | ,-28,-52,-76,-100,-4,-4,-4,-4,-4,-4,-4,-4,-4,-20,-32,-44,-56,-68,-80 36 | ,-92,-104,-120,124,112,100,88,76,64,-4,-4,-4,-4,-4,-4,-4,-4,-28,-52 37 | ,-76,-100,-124,112,88,64,-68,-80,-88,-100,-116,124,108,92,-40,-68,-100 38 | ,-128,96,64,36,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,40,-32,-44,-52,-64 39 | ,-76,32,0,0,0,0,0,0,0,0,0,-40,-72,-100,124,92,64,32,0,0,0,0,0,0,0,0 40 | ,0,-24,-32,-40,-44,-52,-60,-68,-72,-80,-92,-100,-108,-116,-120,-128 41 | ,124,120,112,104,100,96,92,88,80,76,72,64,60,56,48,44,32,0,100,96,0 42 | ,0,36,0,0,0,28,76,92,64,48,52,-12,-24,-36,-56,-64,-76,-80,-92,-104 43 | ,-116,-124,124,120,116,112,108,0 44 | }; 45 | 46 | public static final byte[] PALETTE_BLUE = { 47 | 0,-88,0,-88,0,-88,0,-88,84,-4,84,-4,84,-4,84,-4,-20,-36,-48,-64,-76 48 | ,-88,-104,-116,124,112,100,84,72,56,44,32,0,0,0,0,0,0,0,0,0,0,0,0,0 49 | ,0,0,0,-40,-72,-100,124,92,64,32,0,92,64,32,0,0,0,0,0,-40,-72,-100 50 | ,124,92,64,32,0,0,0,0,0,0,0,0,0,92,64,32,0,0,0,0,0,-40,-72,-100,124 51 | ,92,64,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-4,-4,-4,-8,-4,-4,-4,-4 52 | ,-28,-52,-76,-100,-124,112,88,64,-4,-4,-4,-4,-28,-52,-76,-100,-4,-4 53 | ,-4,-4,-4,-4,-4,-4,-4,-20,-32,-44,-56,-68,-80,-92,-104,-120,124,112 54 | ,100,88,76,64,40,52,36,24,8,0,-4,-4,-28,-52,-76,-100,-124,112,88,64 55 | ,-4,-4,-4,-4,-4,-4,-4,-4,-28,-52,-76,-100,-124,112,88,64,-36,-48,-60 56 | ,-68,-80,-92,-100,-112,-128,112,96,92,88,84,80,76,72,68,64,60,56,52 57 | ,48,44,40,36,32,28,24,24,20,12,100,100,96,28,44,16,72,80,52,28,76,92 58 | ,64,48,52,-12,-24,-36,-56,-64,-76,-80,-92,-104,-116,-124,124,120,116 59 | ,112,108,-120 60 | }; 61 | 62 | public static final IndexColorModel INDEX_COLOR_MODEL 63 | = new IndexColorModel(8, 256, PALETTE_RED, PALETTE_GREEN, PALETTE_BLUE); 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Resource.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Properties; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | import wolf3d.asset.loader.AUDIOTLoader; 16 | import wolf3d.asset.loader.MAPLoader; 17 | import wolf3d.asset.loader.VGAGRAPHLoader; 18 | import wolf3d.asset.loader.VGAGRAPHLoader.VGAGRAPHFont; 19 | import wolf3d.asset.loader.VSWAPLoader; 20 | import wolf3d.infra.Objs.EndPlayerObj.EndPlayerState; 21 | import wolf3d.infra.Objs.EnemyObj.EnemyState; 22 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.ATTACK_REACT; 23 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.CHASE; 24 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.CHASE_REACT; 25 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.PATROL; 26 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.WALK; 27 | import wolf3d.infra.Objs.EnemyObj.EnemyType; 28 | 29 | /** 30 | * Resource class. 31 | * 32 | * @author Leonardo Ono (ono.leo80@gmail.com) 33 | */ 34 | public class Resource { 35 | 36 | public static final String USER_DIR; 37 | public static final Properties PROPERTIES; 38 | 39 | private static final Map enemyAnimationFrames 40 | = new HashMap<>(); 41 | 42 | static { 43 | // user directory 44 | String userDirTmp = System.getProperty("user.dir"); 45 | if (!userDirTmp.endsWith(File.separator)) { 46 | userDirTmp += File.separator; 47 | } 48 | USER_DIR = userDirTmp; 49 | 50 | // constant values 51 | PROPERTIES = new Properties(); 52 | try { 53 | PROPERTIES.load(Resource.class.getResourceAsStream("/res/wl1.def")); 54 | } catch (IOException ex) { 55 | String logClassName = Resource.class.getName(); 56 | Logger.getLogger(logClassName).log(Level.SEVERE, null, ex); 57 | System.exit(-1); 58 | } 59 | 60 | extractEnemiesSpriteAnimationFrames(); 61 | } 62 | 63 | public static void initialize() { 64 | try { 65 | String path = USER_DIR; 66 | 67 | // load AUDIOT (PC Speaker sounds, Adlib sounds and IMF musics) 68 | String audioHeadRes = getProperty("AUDIO_HEAD_RES"); 69 | String audioTRes = getProperty("AUDIO_T_RES"); 70 | int pcSpStartIndex = getIntProperty("PC_SPEAKER_SOUND_START_INDEX"); 71 | int pcSpEndIndex = getIntProperty("PC_SPEAKER_SOUND_END_INDEX"); 72 | int adlSoundStartIndex = getIntProperty("ADLIB_SOUND_START_INDEX"); 73 | int adlSoundEndIndex = getIntProperty("ADLIB_SOUND_END_INDEX"); 74 | int musicStartIndex = getIntProperty("MUSIC_START_INDEX"); 75 | int musicEndIndex = getIntProperty("MUSIC_END_INDEX"); 76 | AUDIOTLoader.load(path, audioHeadRes, audioTRes 77 | , pcSpStartIndex, pcSpEndIndex 78 | , adlSoundStartIndex, adlSoundEndIndex 79 | , musicStartIndex, musicEndIndex); 80 | 81 | // load VSWAP (wall textures, sprites and digitized sounds) 82 | String vswapRes = Resource.getProperty("VSWAP_RES"); 83 | VSWAPLoader.load(path, vswapRes); 84 | 85 | fixDigitizedSoundsPriority(); 86 | 87 | // load VGAGRAPH (PIC's) 88 | String vgaHeadRes = Resource.getProperty("VGA_HEAD_RES"); 89 | String vgaDictRes = Resource.getProperty("VGA_DICT_RES"); 90 | String vgaGraphRes = Resource.getProperty("VGA_GRAPH_RES"); 91 | VGAGRAPHLoader.load(path, vgaHeadRes, vgaDictRes, vgaGraphRes); 92 | 93 | // load GAMEMAPS 94 | String mapHeadRes = Resource.getProperty("MAP_HEAD_RES"); 95 | String gameMapsRes = Resource.getProperty("GAME_MAPS_RES"); 96 | MAPLoader.load(path, mapHeadRes, gameMapsRes); 97 | } catch (Exception ex) { 98 | String logClassName = Resource.class.getName(); 99 | Logger.getLogger(logClassName).log(Level.SEVERE, null, ex); 100 | System.exit(-1); 101 | } 102 | } 103 | 104 | // fix digitized sounds priority using the same priority of adlib sounds. 105 | private static void fixDigitizedSoundsPriority() { 106 | for (Object digitizedSoundId : PROPERTIES.keySet()) { 107 | if (digitizedSoundId.toString().startsWith("DIGITIZED_SOUND_")) { 108 | int digitizedSoundIndex 109 | = getIntProperty(digitizedSoundId.toString()); 110 | 111 | byte[] digitizedSound = getDigitizedSound(digitizedSoundIndex); 112 | if (digitizedSound == null) continue; 113 | String soundId = digitizedSoundId.toString().substring(16); 114 | String pcSpeakerSoundId = "EFFECT_SOUND_" + soundId; 115 | if (getProperty(pcSpeakerSoundId) == null) continue; 116 | int effectSoundIndex = getIntProperty(pcSpeakerSoundId); 117 | ByteBuffer adlibSound 118 | = AUDIOTLoader.getAdlibSound(effectSoundIndex); 119 | 120 | if (adlibSound == null) continue; 121 | digitizedSound[digitizedSound.length - 2] = adlibSound.get(4); 122 | digitizedSound[digitizedSound.length - 1] = adlibSound.get(5); 123 | } 124 | } 125 | } 126 | 127 | public static boolean hasProperty(String propertyName) { 128 | return PROPERTIES.containsKey(propertyName); 129 | } 130 | 131 | public static String getProperty(String propertyName) { 132 | return PROPERTIES.getProperty(propertyName); 133 | } 134 | 135 | public static int getIntProperty(String propertyName) { 136 | return Integer.parseInt(PROPERTIES.getProperty(propertyName).trim()); 137 | } 138 | 139 | public static double getDoubleProperty(String propertyName) { 140 | return Double.parseDouble(PROPERTIES.getProperty(propertyName).trim()); 141 | } 142 | 143 | public static String[] getStringArrayProperty(String propertyName) { 144 | String values = PROPERTIES.getProperty(propertyName).trim(); 145 | if (values != null) { 146 | return values.split(","); 147 | } 148 | return null; 149 | } 150 | 151 | public static ByteBuffer getMusic(String musicId) { 152 | int musicIndex = Resource.getIntProperty("MUSIC_" + musicId); 153 | return AUDIOTLoader.getMusic(musicIndex); 154 | } 155 | 156 | public static byte[] getPCSpeakerSound(int soundIndex) { 157 | return AUDIOTLoader.getPcSpeakerSound(soundIndex); 158 | } 159 | 160 | public static byte[] getDigitizedSound(int soundIndex) { 161 | return VSWAPLoader.getDigitizedSound(soundIndex); 162 | } 163 | 164 | public static BufferedImage getPic(String picId) { 165 | int picIndex = getIntProperty("PIC_" + picId); 166 | return VGAGRAPHLoader.getPic(picIndex); 167 | } 168 | 169 | public static BufferedImage getPic(int picIndex) { 170 | return VGAGRAPHLoader.getPic(picIndex); 171 | } 172 | 173 | // side: 0=horizontal / 1=vertical (1=darker) 174 | public static BufferedImage getWallTexture(int textureId, int side) { 175 | return VSWAPLoader.getWallTexture(textureId + side); 176 | } 177 | 178 | public static BufferedImage getSprite(int sprId) { 179 | return VSWAPLoader.getSprite(sprId); 180 | } 181 | 182 | public static int[][] getMap(int mapIndex) { 183 | return MAPLoader.getMap(mapIndex); 184 | } 185 | 186 | public static VGAGRAPHFont getFont(String fontId) { 187 | return VGAGRAPHLoader.getFont(fontId); 188 | } 189 | 190 | // --- enemy sprite animation frames for each state --- 191 | 192 | public static final class EnemyAnimation { 193 | 194 | private List stand; 195 | private List walk; 196 | private List dying; 197 | private List hit; 198 | private List dead; 199 | private List attack; 200 | 201 | public List getFrames(EnemyState state) { 202 | return switch(state) { 203 | case STAND -> stand; 204 | case ATTACK, ATTACK_REACT -> attack; 205 | case WALK, PATROL, CHASE, CHASE_REACT -> walk; 206 | case HIT -> hit; 207 | case DYING -> dying; 208 | case DEAD -> dead; 209 | default -> null; 210 | }; 211 | } 212 | 213 | } 214 | 215 | // SPRITE_ENEMY_GUARD_STAND = 50-57 216 | // SPRITE_ENEMY_GUARD_WALK = 58-89 217 | // SPRITE_ENEMY_GUARD_DYING = 90-93 218 | // SPRITE_ENEMY_GUARD_HIT = 94 219 | // SPRITE_ENEMY_GUARD_DEAD = 95 220 | // SPRITE_ENEMY_GUARD_ATTACK = 96 221 | private static void extractEnemiesSpriteAnimationFrames() { 222 | for (EnemyType enemyType : EnemyType.values()) { 223 | EnemyAnimation enemyAnimation = new EnemyAnimation(); 224 | for (Object property : Resource.PROPERTIES.keySet()) { 225 | String spriteId = "SPRITE_ENEMY_" + enemyType + "_"; 226 | if (property.toString().startsWith(spriteId)) { 227 | String value = getProperty(property.toString()); 228 | String state = property.toString().replace(spriteId, ""); 229 | switch (state) { 230 | case "STAND" -> { 231 | enemyAnimation.stand = getSpriteIndices(value); 232 | } 233 | case "WALK" -> { 234 | enemyAnimation.walk = getSpriteIndices(value); 235 | } 236 | case "DYING" -> { 237 | enemyAnimation.dying = getSpriteIndices(value); 238 | } 239 | case "HIT" -> { 240 | enemyAnimation.hit = getSpriteIndices(value); 241 | } 242 | case "DEAD" -> { 243 | enemyAnimation.dead = getSpriteIndices(value); 244 | } 245 | case "ATTACK" -> { 246 | enemyAnimation.attack = getSpriteIndices(value); 247 | } 248 | } 249 | } 250 | } 251 | enemyAnimationFrames.put(enemyType, enemyAnimation); 252 | } 253 | } 254 | 255 | private static List getSpriteIndices(String value) { 256 | List indicesList = new ArrayList<>(); 257 | String[] indices = value.split(","); 258 | for (String indice : indices) { 259 | if (indice.contains("-")) { 260 | String[] beginEnd = indice.split("-"); 261 | int beginIndex = Integer.parseInt(beginEnd[0]); 262 | int endIndex = Integer.parseInt(beginEnd[1]); 263 | for (int i = beginIndex; i <= endIndex; i++) { 264 | indicesList.add(i); 265 | } 266 | } 267 | else { 268 | int index = Integer.parseInt(indice); 269 | indicesList.add(index); 270 | } 271 | } 272 | return indicesList; 273 | } 274 | 275 | public static EnemyAnimation getEnemyAnimationsFrames(EnemyType enemyType) { 276 | return enemyAnimationFrames.get(enemyType); 277 | } 278 | 279 | // --- end player animations --- 280 | 281 | public static final class EndPlayerAnimation { 282 | 283 | private List walk; 284 | private List celebrate; 285 | 286 | public List getFrames(EndPlayerState state) { 287 | return switch(state) { 288 | case WALK -> walk; 289 | case CELEBRATE -> celebrate; 290 | default -> null; 291 | }; 292 | } 293 | 294 | } 295 | 296 | // END_SPRITE_WALK = 514-517 297 | // END_SPRITE_CELEBRATE = 518-521 298 | public static EndPlayerAnimation extractEndPlayerAnimationFrames() { 299 | EndPlayerAnimation endPlayerAnimation = new EndPlayerAnimation(); 300 | for (Object property : Resource.PROPERTIES.keySet()) { 301 | String spriteId = "END_SPRITE_"; 302 | if (property.toString().startsWith(spriteId)) { 303 | String value = getProperty(property.toString()); 304 | String state = property.toString().replace(spriteId, ""); 305 | switch (state) { 306 | case "WALK" -> { 307 | endPlayerAnimation.walk = getSpriteIndices(value); 308 | } 309 | case "CELEBRATE" -> { 310 | endPlayerAnimation.celebrate = getSpriteIndices(value); 311 | } 312 | } 313 | } 314 | } 315 | return endPlayerAnimation; 316 | } 317 | 318 | // --- floor / ceiling colors --- 319 | 320 | public static Color getFloorColor() { 321 | String encodedColor = Resource.getProperty("INFO_FLOOR_COLOR"); 322 | return Util.getColor(encodedColor); 323 | } 324 | 325 | public static Color getCeilingColorByFloorNumber(int floor) { 326 | String encodedColor = Resource.getProperty( 327 | "INFO_FLOOR_" + floor + "_CEILING_COLOR"); 328 | 329 | return Util.getColor(encodedColor); 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Scene.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Graphics2D; 4 | 5 | /** 6 | * State class. 7 | * 8 | * @author Leonardo Ono (ono.leo80@gmail.com) 9 | */ 10 | public abstract class Scene { 11 | 12 | protected final String name; 13 | 14 | public Scene(String name) { 15 | this.name = name; 16 | } 17 | 18 | public void start() { 19 | // implement your code here 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void onEnter() { 27 | // implement your code here 28 | } 29 | 30 | public void onTransitionFinished() { 31 | // implement your code here 32 | } 33 | 34 | public void onExit() { 35 | // implement your code here 36 | } 37 | 38 | public void update(double delta) { 39 | // implement your code here 40 | } 41 | 42 | public void fixedUpdate() { 43 | // implement your code here 44 | } 45 | 46 | public void draw(Graphics2D g) { 47 | // implement your code here 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/wolf3d/infra/SceneManager.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import static wolf3d.infra.Settings.*; 8 | 9 | /** 10 | * SceneManager class. 11 | * 12 | * @author Leonardo Ono (ono.leo80@gmail.com) 13 | */ 14 | public class SceneManager { 15 | 16 | private static final Map scenes = new HashMap<>(); 17 | private static Scene currentScene; 18 | private static Scene nextScene; 19 | 20 | private static final int FADE_SIZE = 40; 21 | private static final Color[] ALPHAS = new Color[FADE_SIZE + 1]; 22 | 23 | private static boolean fadeMusic; 24 | private static int fadeValue; 25 | private static int fadeStatus; 26 | private static int waitBetweenFade = 60; 27 | 28 | static { 29 | cacheAllAlphas(); 30 | } 31 | 32 | private SceneManager() { 33 | } 34 | 35 | private static void cacheAllAlphas() { 36 | for (int i = 0; i < FADE_SIZE + 1; i++) { 37 | int alpha = (int) (255 * (1.0 - (i / (double) FADE_SIZE))); 38 | ALPHAS[i] = Util.getColor(0, 0, 0, alpha); 39 | } 40 | } 41 | 42 | public static Scene getCurrentState() { 43 | return currentScene; 44 | } 45 | 46 | public static void addState(Scene state) { 47 | scenes.put(state.getName(), state); 48 | } 49 | 50 | public static void removeState(String stateName) { 51 | scenes.remove(stateName); 52 | } 53 | 54 | public static Scene getState(String stateId) { 55 | return scenes.get(stateId); 56 | } 57 | 58 | public static void startAll() { 59 | scenes.keySet().forEach(key -> scenes.get(key).start()); 60 | } 61 | 62 | public static boolean isStateAvailable(String stateName) { 63 | return scenes.containsKey(stateName); 64 | } 65 | 66 | public static void switchTo(String nextSceneId 67 | , boolean fadeMusic, int waitBetweenFade) { 68 | nextScene = scenes.get(nextSceneId); 69 | fadeStatus = 1; 70 | fadeValue = FADE_SIZE; 71 | SceneManager.waitBetweenFade = waitBetweenFade; 72 | SceneManager.fadeMusic = fadeMusic; 73 | } 74 | 75 | public static void switchTo(String stateName) { 76 | switchTo(stateName, true, waitBetweenFade); 77 | } 78 | 79 | public static void switchTo(String stateName, boolean fadeMusic) { 80 | switchTo(stateName, fadeMusic, waitBetweenFade); 81 | } 82 | 83 | public static void update(double delta) { 84 | if (currentScene != null) { 85 | currentScene.update(delta); 86 | } 87 | } 88 | 89 | public static void fixedUpdate() { 90 | if (fadeStatus == 1) { 91 | fadeValue--; 92 | 93 | if (fadeMusic) { 94 | double volumeScale = fadeValue / (double) FADE_SIZE; 95 | Audio.setMusicScaleVolume(volumeScale); 96 | } 97 | 98 | if (fadeValue < 0) { 99 | fadeValue = 0; 100 | fadeStatus = 3; 101 | if (currentScene != null) { 102 | currentScene.onExit(); 103 | } 104 | currentScene = nextScene; 105 | currentScene.onEnter(); 106 | nextScene = null; 107 | 108 | if (fadeMusic) Audio.setMusicScaleVolume(1.0); 109 | } 110 | } 111 | else if (fadeStatus >= 3) { 112 | fadeStatus++; 113 | if (fadeStatus > waitBetweenFade) { 114 | fadeStatus = 2; 115 | } 116 | } 117 | else if (fadeStatus == 2) { 118 | fadeValue++; 119 | if (fadeValue > FADE_SIZE) { 120 | fadeValue = FADE_SIZE; 121 | fadeStatus = 0; 122 | currentScene.onTransitionFinished(); 123 | } 124 | } 125 | else { 126 | if (currentScene != null) { 127 | currentScene.fixedUpdate(); 128 | } 129 | } 130 | } 131 | 132 | public static void draw(Graphics2D g) { 133 | if (currentScene != null) { 134 | currentScene.draw(g); 135 | if (fadeStatus != 0) { 136 | g.setColor(ALPHAS[fadeValue]); 137 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 138 | } 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/wolf3d/infra/SecretDoors.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import wolf3d.infra.Tiles.FloorTile; 6 | import wolf3d.infra.Tiles.SecretDoorTile; 7 | import wolf3d.infra.Tiles.SecretDoorTile.SecretDoorState; 8 | 9 | /** 10 | * SecretDoors class. 11 | * 12 | * Handle all active secret doors. 13 | * 14 | * @author Leonardo Ono (ono.leo80@gmail.com) 15 | */ 16 | public class SecretDoors { 17 | 18 | private static final double SECRET_DOOR_SPEED = 0.005; 19 | 20 | private static final Set activatedSecretDoors 21 | = new HashSet<>(); 22 | 23 | private static final Set deactivatedSecretDoors 24 | = new HashSet<>(); 25 | 26 | public static void activateSecretDoor(SecretDoorTile secretDoor) { 27 | if (secretDoor.getSecretDoorState() == SecretDoorState.CLOSED) { 28 | Audio.playSound("PUSHWALL"); 29 | secretDoor.setSecretDoorState(SecretDoorState.OPENING); 30 | activatedSecretDoors.add(secretDoor); 31 | Wolf3DGame.incSecretDoorsFoundCount(); 32 | } 33 | } 34 | 35 | public static void fixedUpdateSecretDoors() { 36 | for (SecretDoorTile secretDoor : activatedSecretDoors) { 37 | switch (secretDoor.getSecretDoorState()) { 38 | case OPENING -> { 39 | secretDoor.incSecretDoorOpenRate(SECRET_DOOR_SPEED); 40 | if (secretDoor.getSecretDoorOpenRate() > 1.0) { 41 | secretDoor.setSecretDoorOpenRate(0); 42 | secretDoor.decMovementCount(); 43 | 44 | int sdc = secretDoor.getCol(); 45 | int sdr = secretDoor.getRow(); 46 | FloorTile floorTile = secretDoor 47 | .getFloorTile(secretDoor.getMovementCount()); 48 | 49 | floorTile.setLocation(sdc, sdr); 50 | GameMap.getTiles()[sdr][sdc] = floorTile; 51 | sdc += secretDoor.getPushDirection().dx; 52 | sdr += secretDoor.getPushDirection().dy; 53 | GameMap.getTiles()[sdr][sdc] = secretDoor; 54 | secretDoor.setLocation(sdc, sdr); 55 | if (secretDoor.getMovementCount() == 0) { 56 | GameMap.getTiles()[sdr][sdc] = secretDoor.getTile(); 57 | secretDoor.setSecretDoorState(SecretDoorState.OPEN); 58 | deactivatedSecretDoors.add(secretDoor); 59 | } 60 | // push again 61 | else { 62 | Audio.playSound("PUSHWALL"); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | if (!deactivatedSecretDoors.isEmpty()) { 69 | activatedSecretDoors.removeAll(deactivatedSecretDoors); 70 | deactivatedSecretDoors.clear(); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Settings.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.event.KeyEvent; 4 | 5 | /** 6 | * (Project) Settings class. 7 | * 8 | * @author Leonardo Ono (ono.leo80@gmail.com) 9 | */ 10 | public class Settings { 11 | 12 | // --- display --- 13 | 14 | public static final int CANVAS_WIDTH = 320; 15 | public static final int CANVAS_HEIGHT = 200; 16 | 17 | public static final int PREFERRED_SCREEN_WIDTH = (int) (320 * 2.5); 18 | public static final int PREFERRED_SCREEN_HEIGHT = (int) (240 * 2.5); 19 | 20 | public static final double ASPECT_RATIO 21 | = PREFERRED_SCREEN_WIDTH / (double) PREFERRED_SCREEN_HEIGHT; 22 | 23 | public static boolean keepAspectRatio = true; 24 | 25 | public static final int KEY_KEEP_ASPECT_RATIO = KeyEvent.VK_F11; 26 | public static final int KEY_FULLSCREEN = KeyEvent.VK_F12; 27 | 28 | 29 | // --- game loop --- 30 | 31 | public static final long TIME_PER_UPDATE = 1000000000 / 60; 32 | 33 | 34 | // --- input (changeable) --- 35 | 36 | public static int KEY_START_1 = KeyEvent.VK_SPACE; 37 | public static int KEY_START_2 = KeyEvent.VK_ENTER; 38 | public static int KEY_CANCEL = KeyEvent.VK_ESCAPE; 39 | 40 | public static int KEY_PLAYER_UP = KeyEvent.VK_UP; 41 | public static int KEY_PLAYER_DOWN = KeyEvent.VK_DOWN; 42 | public static int KEY_PLAYER_LEFT = KeyEvent.VK_LEFT; 43 | public static int KEY_PLAYER_RIGHT = KeyEvent.VK_RIGHT; 44 | public static int KEY_PLAYER_FIRE = KeyEvent.VK_Z; 45 | public static int KEY_PLAYER_STRAFE = KeyEvent.VK_X; 46 | public static int KEY_PLAYER_DOOR = KeyEvent.VK_SPACE; 47 | 48 | public static int KEY_PLAYER_WEAPON_KNIFE = KeyEvent.VK_1; 49 | public static int KEY_PLAYER_WEAPON_PISTOL = KeyEvent.VK_2; 50 | public static int KEY_PLAYER_WEAPON_MACHINE = KeyEvent.VK_3; 51 | public static int KEY_PLAYER_WEAPON_GATLING = KeyEvent.VK_4; 52 | 53 | 54 | // --- audio --- 55 | 56 | public static final int PC_SPEAKER_SOUND_PCM_FREQ = 44100; 57 | public static final int DIGITIZED_SOUND_PCM_FREQ = 7000; 58 | public static final int IMF_MUSIC_PLAYBACK_RATE = 700; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Tiles.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import wolf3d.infra.Objs.EnemyObj; 7 | import static wolf3d.infra.Objs.EnemyObj.ENEMY_RADIUS; 8 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.*; 9 | import static wolf3d.infra.Player.PLAYER_RADIUS; 10 | import static wolf3d.infra.Tiles.DoorTile.DoorState.*; 11 | import wolf3d.infra.Tiles.DoorTile.DoorKey; 12 | import static wolf3d.infra.Tiles.TileType.*; 13 | 14 | /** 15 | * Tiles class. 16 | * 17 | * @author Leonardo Ono (ono.leo80@gmail.com) 18 | */ 19 | public class Tiles { 20 | 21 | public static final int TILE_HORIZONTAL = 0; 22 | public static final int TILE_VERTICAL = 1; 23 | 24 | public static enum TileType { WALL, FLOOR, DOOR, SECRET_DOOR, ELEVATOR } 25 | 26 | public static class Tile { 27 | 28 | protected final int id; 29 | protected int col; 30 | protected int row; 31 | protected final TileType type; 32 | protected final boolean blockRaycast; 33 | protected boolean blockMovement; 34 | 35 | public Tile(int id, int col, int row, TileType type 36 | , boolean blockRaycast, boolean blockMovement) { 37 | 38 | this.id = id; 39 | this.col = col; 40 | this.row = row; 41 | this.type = type; 42 | this.blockRaycast = blockRaycast; 43 | this.blockMovement = blockMovement; 44 | } 45 | 46 | public int getId() { 47 | return id; 48 | } 49 | 50 | public int getCol() { 51 | return col; 52 | } 53 | 54 | public int getRow() { 55 | return row; 56 | } 57 | 58 | public void setLocation(int col, int row) { 59 | this.col = col; 60 | this.row = row; 61 | } 62 | 63 | public TileType getType() { 64 | return type; 65 | } 66 | 67 | public boolean isBlockRaycast() { 68 | return blockRaycast; 69 | } 70 | 71 | public boolean isBlockMovement() { 72 | return blockMovement; 73 | } 74 | 75 | public void setBlockMovement(boolean blockMovement) { 76 | this.blockMovement = blockMovement; 77 | } 78 | 79 | public boolean isTileObstructedByPlayer() { 80 | double px = Player.getPlayerX(); 81 | double py = Player.getPlayerY(); 82 | double pr = PLAYER_RADIUS; 83 | return isTileObstructed(px, py, pr); 84 | } 85 | 86 | protected boolean isTileObstructed( 87 | double ex, double ey, double eRadius) { 88 | 89 | for (int py = -1; py <= 1; py++) { 90 | for (int px = -1; px <= 1; px++) { 91 | int eCol = (int) (ex + px * eRadius); 92 | int eRow = (int) (ey + py * eRadius); 93 | if (eCol == col && eRow == row) { 94 | return true; 95 | } 96 | } 97 | } 98 | return false; 99 | } 100 | 101 | public BufferedImage getTexture(int side) { 102 | return null; 103 | } 104 | 105 | } 106 | 107 | public static class WallTile extends Tile { 108 | 109 | private BufferedImage textureHorizontal; 110 | private BufferedImage textureVertical; 111 | private boolean elevator; 112 | 113 | public WallTile(int id, int col, int row) { 114 | super(id, col, row, WALL, true, true); 115 | } 116 | 117 | protected WallTile(int id, int col, int row, TileType type) { 118 | super(id, col, row, type, true, true); 119 | } 120 | 121 | public BufferedImage getTextureHorizontal() { 122 | return textureHorizontal; 123 | } 124 | 125 | public void setTextureHorizontal(BufferedImage textureHorizontal) { 126 | this.textureHorizontal = textureHorizontal; 127 | } 128 | 129 | public BufferedImage getTextureVertical() { 130 | return textureVertical; 131 | } 132 | 133 | public void setTextureVertical(BufferedImage textureVertical) { 134 | this.textureVertical = textureVertical; 135 | } 136 | 137 | public boolean isElevator() { 138 | return elevator; 139 | } 140 | 141 | public void setElevator(boolean elevator) { 142 | this.elevator = elevator; 143 | } 144 | 145 | @Override 146 | public BufferedImage getTexture(int side) { 147 | return switch (side) { 148 | case TILE_HORIZONTAL -> textureHorizontal; 149 | case TILE_VERTICAL -> textureVertical; 150 | default -> null; 151 | }; 152 | } 153 | 154 | } 155 | 156 | public static class DoorTile extends Tile { 157 | 158 | public static enum DoorState { OPENING, OPEN, CLOSING, CLOSED } 159 | public static enum DoorKey { SILVER, GOLD } 160 | 161 | private double doorOpenRate = 0.0; // 0.0~1.0 162 | private DoorState doorState = CLOSED; 163 | private long doorCloseTime; 164 | 165 | private BufferedImage texture; 166 | private final int doorSide; // 0=horizontal, 1=vertical 167 | private boolean locked; 168 | private final DoorKey requiredKey; 169 | 170 | private int connectedRoom1; 171 | private int connectedRoom2; 172 | 173 | private EnemyObj obstructingEnemy; 174 | 175 | public DoorTile(int id, int col, int row 176 | , int doorSide, boolean locked, DoorKey requiredKey) { 177 | 178 | super(id, col, row, DOOR, false, false); 179 | this.doorSide = doorSide; 180 | this.locked = locked; 181 | this.requiredKey = requiredKey; 182 | } 183 | 184 | public BufferedImage getTexture() { 185 | return texture; 186 | } 187 | 188 | public void setTexture(BufferedImage texture) { 189 | this.texture = texture; 190 | } 191 | 192 | public double getDoorOpenRate() { 193 | return doorOpenRate; 194 | } 195 | 196 | public void setDoorOpenRate(double doorOpenRate) { 197 | this.doorOpenRate = doorOpenRate; 198 | } 199 | 200 | public void incDoorOpenRate(double inc) { 201 | this.doorOpenRate += inc; 202 | } 203 | 204 | public DoorState getDoorState() { 205 | return doorState; 206 | } 207 | 208 | public void setDoorState(DoorState doorState) { 209 | this.doorState = doorState; 210 | } 211 | 212 | public long getDoorCloseTime() { 213 | return doorCloseTime; 214 | } 215 | 216 | public void setDoorCloseTime(long doorCloseTime) { 217 | this.doorCloseTime = doorCloseTime; 218 | } 219 | 220 | public int getDoorSide() { 221 | return doorSide; 222 | } 223 | 224 | public boolean isLocked() { 225 | return locked; 226 | } 227 | 228 | public void setLocked(boolean locked) { 229 | this.locked = locked; 230 | } 231 | 232 | public DoorKey getRequiredKey() { 233 | return requiredKey; 234 | } 235 | 236 | @Override 237 | public boolean isBlockMovement() { 238 | boolean doorObstructed = false; 239 | if (isDoorObstructedByEnemy()) { 240 | doorObstructed = obstructingEnemy.getEnemyState() != DEAD; 241 | } 242 | return doorState != OPEN 243 | || doorObstructed || super.isBlockMovement(); 244 | } 245 | 246 | public void setConnectedRooms(int r1, int r2) { 247 | this.connectedRoom1 = r1; 248 | this.connectedRoom2 = r2; 249 | } 250 | 251 | public int getConnectedRoom1() { 252 | return connectedRoom1; 253 | } 254 | 255 | public int getConnectedRoom2() { 256 | return connectedRoom2; 257 | } 258 | 259 | public EnemyObj getObstructingEnemy() { 260 | return obstructingEnemy; 261 | } 262 | 263 | public void setObstructingEnemy(EnemyObj obstructingEnemy) { 264 | this.obstructingEnemy = obstructingEnemy; 265 | } 266 | 267 | public boolean isDoorObstructed() { 268 | boolean doorObstructed = isTileObstructedByPlayer(); 269 | doorObstructed |= isDoorObstructedByEnemy(); 270 | return doorObstructed; 271 | } 272 | 273 | public boolean isDoorObstructedByEnemy() { 274 | if (obstructingEnemy == null) return false; 275 | double ex = obstructingEnemy.getEnemyX(); 276 | double ey = obstructingEnemy.getEnemyY(); 277 | double er = ENEMY_RADIUS; 278 | return isTileObstructed(ex, ey, er); 279 | } 280 | 281 | @Override 282 | public BufferedImage getTexture(int side) { 283 | return texture; 284 | } 285 | 286 | } 287 | 288 | public static class SecretDoorTile extends Tile { 289 | 290 | public static enum SecretDoorState { CLOSED, OPENING, OPEN } 291 | public static enum DoorKey { SILVER, GOLD } 292 | private double secretDoorOpenRate = 0.0; // 0.0~1.0 293 | private SecretDoorState secretDoorState = SecretDoorState.CLOSED; 294 | private int doorSide; // 0=horizontal, 1=vertical 295 | private final Tile tile; 296 | private CardinalDirection pushDirection; 297 | private int movementCount = 2; 298 | private final FloorTile[] floorTiles; 299 | 300 | public SecretDoorTile(Tile tile) { 301 | super(tile.id, tile.col, tile.row 302 | , TileType.SECRET_DOOR, false, true); 303 | 304 | this.tile = tile; 305 | this.floorTiles = new FloorTile[] { 306 | new FloorTile(tile.id, tile.col, tile.row) 307 | , new FloorTile(tile.id, tile.col, tile.row) }; 308 | } 309 | 310 | public Tile getTile() { 311 | return tile; 312 | } 313 | 314 | public double getSecretDoorOpenRate() { 315 | return secretDoorOpenRate; 316 | } 317 | 318 | public void setSecretDoorOpenRate(double doorOpenRate) { 319 | this.secretDoorOpenRate = doorOpenRate; 320 | } 321 | 322 | public void incSecretDoorOpenRate(double inc) { 323 | this.secretDoorOpenRate += inc; 324 | } 325 | 326 | public SecretDoorState getSecretDoorState() { 327 | return secretDoorState; 328 | } 329 | 330 | public void setSecretDoorState(SecretDoorState secretDoorState) { 331 | this.secretDoorState = secretDoorState; 332 | } 333 | 334 | public int getDoorSide() { 335 | return doorSide; 336 | } 337 | 338 | public void setDoorSide(int doorSide) { 339 | this.doorSide = doorSide; 340 | } 341 | 342 | @Override 343 | public BufferedImage getTexture(int side) { 344 | return tile.getTexture(side); 345 | } 346 | 347 | public CardinalDirection getPushDirection() { 348 | return pushDirection; 349 | } 350 | 351 | public void setPushDirection(CardinalDirection pushDirection) { 352 | this.pushDirection = pushDirection; 353 | } 354 | 355 | public int getMovementCount() { 356 | return movementCount; 357 | } 358 | 359 | public void setMovementCount(int movementCount) { 360 | this.movementCount = movementCount; 361 | } 362 | 363 | public void decMovementCount() { 364 | movementCount--; 365 | } 366 | 367 | public FloorTile getFloorTile(int index) { 368 | return floorTiles[index]; 369 | } 370 | 371 | } 372 | 373 | public static class FloorTile extends Tile { 374 | 375 | private boolean ambush; 376 | private boolean secret; 377 | 378 | public FloorTile(int id, int col, int row) { 379 | super(id, col, row, FLOOR, false, false); 380 | } 381 | 382 | public boolean isAmbush() { 383 | return ambush; 384 | } 385 | 386 | public void setAmbush(boolean ambush) { 387 | this.ambush = ambush; 388 | } 389 | 390 | public boolean isSecret() { 391 | return secret; 392 | } 393 | 394 | public void setSecret(boolean secret) { 395 | this.secret = secret; 396 | } 397 | 398 | } 399 | 400 | private static final Map TILE_KEYS = new HashMap<>(); 401 | 402 | static { 403 | for (Object keyObj : Resource.PROPERTIES.keySet()) { 404 | String key = (String) keyObj; 405 | if (key.toUpperCase().startsWith("TILE_")) { 406 | String[] values = Resource.getProperty(key).trim().split("-"); 407 | int startIndex = Integer.parseInt(values[0]); 408 | int endIndex = Integer.parseInt(values[1]); 409 | for (int i = startIndex; i <= endIndex; i++) { 410 | TILE_KEYS.put(i, key); 411 | } 412 | } 413 | } 414 | } 415 | 416 | public static Tile createTile(int tileId, int col, int row) { 417 | int elevatorTileId = Resource.getIntProperty("ELEVATOR_TILE_ID"); 418 | String tileKey = TILE_KEYS.get(tileId); 419 | String[] tileInfo = tileKey.trim().toUpperCase().split("_"); 420 | Tile tile = null; 421 | switch (tileInfo[1]) { 422 | case "WALL" -> { 423 | WallTile wallTile = new WallTile(tileId, col, row); 424 | int wallIndex = (tileId - 1) * 2; 425 | wallTile.setTextureHorizontal( 426 | Resource.getWallTexture(wallIndex, 0)); 427 | 428 | wallTile.setTextureVertical( 429 | Resource.getWallTexture(wallIndex, 1)); 430 | 431 | // check if elevator 432 | wallTile.setElevator(tileId == elevatorTileId); 433 | 434 | tile = wallTile; 435 | } 436 | case "DOOR" -> { 437 | boolean locked = tileInfo[2].equals("LOCKED"); 438 | DoorKey requiredKey = null; 439 | if (locked) requiredKey = DoorKey.valueOf(tileInfo[3]); 440 | int doorSide = 1 - tileId % 2; 441 | DoorTile doorTile = new DoorTile( 442 | tileId, col, row, doorSide, locked, requiredKey); 443 | 444 | int doorId = Resource.getIntProperty("TEXTURE_DOOR_LOCKED"); 445 | if (!locked && tileInfo[3].equals("ELEVATOR")) { 446 | doorId = Resource.getIntProperty("TEXTURE_DOOR_ELEVATOR"); 447 | } 448 | else if (!locked) { 449 | doorId = Resource.getIntProperty("TEXTURE_DOOR_DEFAULT"); 450 | } 451 | doorTile.setTexture(Resource.getWallTexture(doorId, doorSide)); 452 | tile = doorTile; 453 | } 454 | case "FLOOR" -> { 455 | FloorTile floorTile = new FloorTile(tileId, col, row); 456 | floorTile.setAmbush(tileInfo[2].equals("AMBUSH")); 457 | floorTile.setSecret(tileInfo[2].equals("SECRET")); 458 | tile = floorTile; 459 | } 460 | } 461 | return tile; 462 | } 463 | 464 | } 465 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Util.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Random; 7 | 8 | /** 9 | * Util class. 10 | * 11 | * @author Leonardo Ono (ono.leo80@gmail.com) 12 | */ 13 | public class Util { 14 | 15 | private static final Random RANDOM = new Random(System.nanoTime()); 16 | 17 | public static double clamp(double val, double min, double max) { 18 | return Math.max(min, Math.min(max, val)); 19 | } 20 | 21 | public static int random(int n) { 22 | return RANDOM.nextInt(n); 23 | } 24 | 25 | public static int random(int a, int b) { 26 | return a + RANDOM.nextInt(b - a + 1); 27 | } 28 | 29 | public static long getTimeMs() { 30 | return System.currentTimeMillis(); 31 | } 32 | 33 | public static long getTimeNano() { 34 | return System.nanoTime(); 35 | } 36 | 37 | private static final Map colorsCache = new HashMap<>(); 38 | 39 | public static Color getColor(int r, int g, int b, int a) { 40 | int colorKey = b + (g << 8) + (r << 16) + (a << 24); 41 | Color color = colorsCache.get(colorKey); 42 | if (color == null) { 43 | color = new Color(r, g, b, a); 44 | colorsCache.put(colorKey, color); 45 | } 46 | return color; 47 | } 48 | 49 | // example: red color = 0xff0000 50 | public static Color getColor(String encodedColor) { 51 | Color color = null; 52 | if (encodedColor.startsWith("0x") && encodedColor.length() == 10) { 53 | int r = Integer.parseUnsignedInt( 54 | encodedColor.substring(2, 4), 16); 55 | 56 | int g = Integer.parseUnsignedInt( 57 | encodedColor.substring(4, 6), 16); 58 | 59 | int b = Integer.parseUnsignedInt( 60 | encodedColor.substring(6, 8), 16); 61 | 62 | int a = Integer.parseUnsignedInt( 63 | encodedColor.substring(8, 10), 16); 64 | 65 | color = new Color(r, g, b, a); 66 | } 67 | else { 68 | color = Color.decode(encodedColor); 69 | } 70 | return color; 71 | } 72 | 73 | public static int extractTimeMsSecondsPart(long timeMs) { 74 | int totalSeconds = (int) (timeMs * 0.001); 75 | int secondsPart = totalSeconds % 60; 76 | return secondsPart; 77 | } 78 | 79 | public static int extractTimeMsMinutesPart(long timeMs) { 80 | int totalSeconds = (int) (timeMs * 0.001); 81 | int totalMinutes = totalSeconds / 60; 82 | int minutesPart = totalMinutes % 60; 83 | return minutesPart; 84 | } 85 | 86 | public static int extractTimeMsHoursPart(long timeMs) { 87 | int totalSeconds = (int) (timeMs * 0.001); 88 | int totalMinutes = totalSeconds / 60; 89 | int totalHours = totalMinutes / 60; 90 | return totalHours % 100; // maximum of 2 digits 91 | } 92 | 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Weapons.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import static wolf3d.infra.Settings.*; 10 | import static wolf3d.infra.Weapons.WeaponType.*; 11 | 12 | /** 13 | * Weapons class. 14 | * 15 | * @author Leonardo Ono (ono.leo80@gmail.com) 16 | */ 17 | public class Weapons { 18 | 19 | public static enum WeaponType { KNIFE, PISTOL, MACHINE, GATLING }; 20 | 21 | public static class Weapon { 22 | 23 | private final WeaponType type; 24 | private final double minHitDistance; 25 | private final long waitUntilNextAttackTimeMs; // in milli seconds 26 | private final double animationSpeed; 27 | private final BufferedImage hudPic; 28 | private final List attackAnimationFrames; 29 | private boolean own; 30 | private double frameIndex; 31 | private boolean attacking; 32 | private long lastAttackTime; 33 | private final String soundId; 34 | 35 | public Weapon(WeaponType type, double minHitDistance 36 | , long waitTime, double animationSpeed, BufferedImage hudPic 37 | , List attackAnimationFrames 38 | , String soundId) { 39 | 40 | this.minHitDistance = minHitDistance; 41 | this.type = type; 42 | this.waitUntilNextAttackTimeMs = waitTime; 43 | this.animationSpeed = animationSpeed; 44 | this.hudPic = hudPic; 45 | this.attackAnimationFrames = attackAnimationFrames; 46 | this.soundId = soundId; 47 | } 48 | 49 | public WeaponType getType() { 50 | return type; 51 | } 52 | 53 | public double getMinHitDistance() { 54 | return minHitDistance; 55 | } 56 | 57 | public long getWaitTime() { 58 | return waitUntilNextAttackTimeMs; 59 | } 60 | 61 | public double getAnimationSpeed() { 62 | return animationSpeed; 63 | } 64 | 65 | public BufferedImage getHudPic() { 66 | return hudPic; 67 | } 68 | 69 | public List getAttackAnimationFrames() { 70 | return attackAnimationFrames; 71 | } 72 | 73 | public boolean isOwn() { 74 | return own; 75 | } 76 | 77 | public void setOwn(boolean own) { 78 | this.own = own; 79 | } 80 | 81 | public double getFrameIndex() { 82 | return frameIndex; 83 | } 84 | 85 | public void setFrameIndex(double frameIndex) { 86 | this.frameIndex = frameIndex; 87 | } 88 | 89 | public boolean isAttacking() { 90 | return attacking; 91 | } 92 | 93 | public void setAttacking(boolean attacking) { 94 | this.attacking = attacking; 95 | } 96 | 97 | public long getLastAttackTime() { 98 | return lastAttackTime; 99 | } 100 | 101 | public void setLastAttackTime(long lastAttackTime) { 102 | this.lastAttackTime = lastAttackTime; 103 | } 104 | 105 | private BufferedImage getSprite() { 106 | return attackAnimationFrames.get((int) frameIndex); 107 | } 108 | 109 | public String getSoundId() { 110 | return soundId; 111 | } 112 | 113 | public void attack() { 114 | attacking = true; 115 | lastAttackTime = Util.getTimeMs(); 116 | } 117 | 118 | public void reset() { 119 | attacking = false; 120 | frameIndex = 0; 121 | } 122 | 123 | } 124 | 125 | private static final Map playerWeapons 126 | = new HashMap<>(); 127 | 128 | // PIC_HUD_WEAPON_KNIFE = 103 129 | // PIC_HUD_WEAPON_PISTOL = 104 130 | // PIC_HUD_WEAPON_MACHINE = 105 131 | // PIC_HUD_WEAPON_GATLING = 106 132 | // SPRITE_WEAPON_KNIFE_ANIMATIONS = 522-526 133 | // SPRITE_WEAPON_PISTOL_ANIMATIONS = 527-531 134 | // SPRITE_WEAPON_MACHINE_ANIMATIONS = 532-536 135 | // SPRITE_WEAPON_GATLING_ANIMATIONS = 537-541 136 | // INFO_WEAPON_KNIFE_MINHITDISTANCE = 2 137 | // INFO_WEAPON_KNIFE_WAITTIME = 1000 138 | // INFO_WEAPON_PISTOL_MINHITDISTANCE = 15 139 | // INFO_WEAPON_PISTOL_WAITTIME = 750 140 | // INFO_WEAPON_MACHINE_MINHITDISTANCE = 15 141 | // INFO_WEAPON_MACHINE_WAITTIME = 500 142 | // INFO_WEAPON_GATLING_MINHITDISTANCE = 15 143 | // INFO_WEAPON_GATLING_WAITTIME = 250 144 | public static void createPlayerWeapons() { 145 | playerWeapons.clear(); 146 | for (WeaponType weaponType : WeaponType.values()) { 147 | String picHudId = "HUD_WEAPON_" + weaponType; 148 | BufferedImage hudPic = Resource.getPic(picHudId); 149 | String framesId = "SPRITE_WEAPON_" + weaponType + "_ANIMATIONS"; 150 | String frames = Resource.getProperty(framesId); 151 | String[] beginEnd = frames.split("-"); 152 | List attackAnimationFrames = new ArrayList<>(); 153 | int begin = Integer.parseInt(beginEnd[0]); 154 | int end = Integer.parseInt(beginEnd[1]); 155 | for (int i = begin; i <= end; i++) { 156 | BufferedImage sprite = Resource.getSprite(i); 157 | attackAnimationFrames.add(sprite); 158 | } 159 | String minHitDistId 160 | = "INFO_WEAPON_" + weaponType + "_MINHITDISTANCE"; 161 | 162 | String animSpeedId 163 | = "INFO_WEAPON_" + weaponType + "_ANIMATIONSPEED"; 164 | 165 | String waitTimeId 166 | = "INFO_WEAPON_" + weaponType + "_WAITTIME"; 167 | 168 | String soundKey 169 | = "INFO_WEAPON_" + weaponType + "_SOUNDID"; 170 | 171 | double minHitDistance = Resource.getDoubleProperty(minHitDistId); 172 | double animationSpeed = Resource.getDoubleProperty(animSpeedId); 173 | long waitTime = Resource.getIntProperty(waitTimeId); 174 | String soundId = Resource.getProperty(soundKey); 175 | 176 | Weapon weapon = new Weapon(weaponType, minHitDistance, waitTime 177 | , animationSpeed, hudPic, attackAnimationFrames, soundId); 178 | 179 | playerWeapons.put(weaponType, weapon); 180 | } 181 | } 182 | 183 | private static int ammo; 184 | private static Weapon currentPlayerWeapon; 185 | 186 | public static void reset() { 187 | ammo = 8; 188 | playerWeapons.values().forEach(weapon -> { 189 | WeaponType weaponType = weapon.getType(); 190 | weapon.reset(); 191 | weapon.setOwn(weaponType == KNIFE || weaponType == PISTOL); 192 | 193 | // debug: start player with all weapons 194 | if (Wolf3DGame.playerOverrideLocation != null) { 195 | weapon.setOwn(true); 196 | ammo = 99; 197 | } 198 | }); 199 | currentPlayerWeapon = playerWeapons.get(PISTOL); 200 | // player starts with knife and pistol weapons 201 | } 202 | 203 | public static int getAmmo() { 204 | return ammo; 205 | } 206 | 207 | public static void setAmmo(int ammo) { 208 | Weapons.ammo = ammo; 209 | } 210 | 211 | public static void incAmmo(int inc) { 212 | Weapons.ammo += inc; 213 | if (Weapons.ammo > 99) { 214 | Weapons.ammo = 99; 215 | } 216 | } 217 | 218 | public static void decAmmo() { 219 | Weapons.ammo--; 220 | if (Weapons.ammo < 0) { 221 | Weapons.ammo = 0; 222 | } 223 | } 224 | 225 | public static Weapon getPlayerWeapon(WeaponType weaponType) { 226 | return playerWeapons.get(weaponType); 227 | } 228 | 229 | public static Weapon getCurrentPlayerWeapon() { 230 | return currentPlayerWeapon; 231 | } 232 | 233 | public static void setCurrentPlayerWeapon(WeaponType weaponType) { 234 | Weapon weapon = playerWeapons.get(weaponType); 235 | if (!weapon.isOwn()) return; 236 | if (weapon.getType() != KNIFE && ammo <= 0) return; 237 | currentPlayerWeapon = weapon; 238 | currentPlayerWeapon.attacking = false; 239 | currentPlayerWeapon.frameIndex = 0; 240 | } 241 | 242 | public static void selectBestWeapon() { 243 | if (currentPlayerWeapon.getType() != KNIFE && ammo > 0) { 244 | return; 245 | } 246 | currentPlayerWeapon = playerWeapons.get(KNIFE); 247 | if (ammo > 0) { 248 | Weapon gatling = playerWeapons.get(GATLING); 249 | Weapon machine = playerWeapons.get(MACHINE); 250 | Weapon pistol = playerWeapons.get(PISTOL); 251 | if (pistol.isOwn()) currentPlayerWeapon = pistol; 252 | if (machine.isOwn()) currentPlayerWeapon = machine; 253 | if (gatling.isOwn()) currentPlayerWeapon = gatling; 254 | currentPlayerWeapon.attacking = false; 255 | currentPlayerWeapon.frameIndex = 0; 256 | } 257 | } 258 | 259 | public static boolean canAttack() { 260 | if (currentPlayerWeapon.isAttacking()) { 261 | return false; 262 | } 263 | else if (Util.getTimeMs() < currentPlayerWeapon.getLastAttackTime() 264 | + currentPlayerWeapon.getWaitTime()) { 265 | 266 | return false; 267 | } 268 | else if (currentPlayerWeapon.getType() == KNIFE) { 269 | return true; 270 | } 271 | else { 272 | return ammo > 0; 273 | } 274 | } 275 | 276 | public static void fixedUpdate() { 277 | updateCurrentWeaponAnimation(); 278 | } 279 | 280 | private static void updateCurrentWeaponAnimation() { 281 | WeaponType weaponType = currentPlayerWeapon.getType(); 282 | if (!currentPlayerWeapon.isAttacking() 283 | && (weaponType == MACHINE || weaponType == GATLING) 284 | && currentPlayerWeapon.frameIndex != 0 285 | && !Input.isKeyPressed(KEY_PLAYER_FIRE)) { 286 | 287 | currentPlayerWeapon.frameIndex 288 | += currentPlayerWeapon.animationSpeed; 289 | 290 | if (currentPlayerWeapon.frameIndex >= 5) { 291 | currentPlayerWeapon.frameIndex = 0; 292 | currentPlayerWeapon.attacking = false; 293 | } 294 | } 295 | 296 | if (currentPlayerWeapon.isAttacking()) { 297 | currentPlayerWeapon.frameIndex 298 | += currentPlayerWeapon.animationSpeed; 299 | 300 | if (weaponType == MACHINE || weaponType == GATLING) { 301 | if (currentPlayerWeapon.frameIndex >= 4) { 302 | currentPlayerWeapon.frameIndex = 2; 303 | currentPlayerWeapon.attacking = false; 304 | } 305 | } 306 | else if (currentPlayerWeapon.frameIndex >= 5) { 307 | currentPlayerWeapon.frameIndex = 0; 308 | currentPlayerWeapon.attacking = false; 309 | } 310 | } 311 | } 312 | 313 | // draw weapon sprite on top 314 | public static void draw(Graphics2D g) { 315 | int offsety = CANVAS_HEIGHT / 2 - 40; 316 | g.drawImage(currentPlayerWeapon.getSprite() 317 | , CANVAS_WIDTH / 2 - 64, offsety + 100 - 128, 128, 128, null); 318 | } 319 | 320 | } 321 | -------------------------------------------------------------------------------- /src/wolf3d/infra/Wolf3DGame.java: -------------------------------------------------------------------------------- 1 | package wolf3d.infra; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.Point; 6 | import wolf3d.infra.Objs.EnemyObj; 7 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.DEAD; 8 | import static wolf3d.infra.Settings.*; 9 | import static wolf3d.infra.Wolf3DGame.Difficulty.HARD; 10 | import wolf3d.scene.Credits; 11 | import wolf3d.scene.GameDifficulty; 12 | import wolf3d.scene.GameOptions; 13 | import wolf3d.scene.Initializing; 14 | import wolf3d.scene.LevelClearedStatistics; 15 | import wolf3d.scene.OLPresents; 16 | import wolf3d.scene.ProfoundCarnage13; 17 | import wolf3d.scene.Quit; 18 | import wolf3d.scene.Stage; 19 | import wolf3d.scene.Title; 20 | 21 | /** 22 | * Wolf3DGame class. 23 | * 24 | * @author Leonardo Ono (ono.leo80@gmail.com) 25 | */ 26 | public class Wolf3DGame { 27 | 28 | public static enum Difficulty { EASY, NORMAL, HARD, VERY_HARD } 29 | 30 | private static Difficulty difficulty = HARD; 31 | 32 | private static int floor; 33 | private static int levelIndex; 34 | 35 | private static int lives; 36 | private static int lifeEnergy; 37 | private static int score; 38 | 39 | private static int collectedTreasuresCount; 40 | private static int secretDoorsFoundCount; 41 | 42 | private static EnemyObj closestEnemyInSight; 43 | 44 | private static boolean playing; 45 | private static boolean backToGame; 46 | 47 | public static void reset() { 48 | levelIndex = 0; 49 | floor = Resource.getIntProperty("MAP_LEVEL_" + levelIndex) + 1; 50 | nextFloor = floor; 51 | lives = 3; 52 | lifeEnergy = 100; 53 | score = 0; 54 | collectedTreasuresCount = 0; 55 | secretDoorsFoundCount = 0; 56 | } 57 | 58 | public static int getFloor() { 59 | return floor; 60 | } 61 | 62 | public static int getLives() { 63 | return lives; 64 | } 65 | 66 | public static int getLifeEnergy() { 67 | return lifeEnergy; 68 | } 69 | 70 | public static void addLifeEnergy(int add) { 71 | lifeEnergy += add; 72 | if (lifeEnergy < 0) { 73 | lifeEnergy = 0; 74 | } 75 | else if (lifeEnergy > 100) { 76 | lifeEnergy = 100; 77 | } 78 | } 79 | 80 | public static int getScore() { 81 | return score; 82 | } 83 | 84 | public static void addScorePoints(int points) { 85 | score += points; 86 | } 87 | 88 | public static int getCollectedTreasuresCount() { 89 | return collectedTreasuresCount; 90 | } 91 | 92 | public static void incCollectedTreasuresCount() { 93 | collectedTreasuresCount++; 94 | } 95 | 96 | public static int getSecretDoorsFoundCount() { 97 | return secretDoorsFoundCount; 98 | } 99 | 100 | public static void incSecretDoorsFoundCount() { 101 | secretDoorsFoundCount++; 102 | } 103 | 104 | public static void setLives(int lives) { 105 | Wolf3DGame.lives = lives; 106 | } 107 | 108 | public static void incLives() { 109 | lives++; 110 | } 111 | 112 | public static Difficulty getDifficulty() { 113 | return difficulty; 114 | } 115 | 116 | public static void setDifficulty(Difficulty difficulty) { 117 | Wolf3DGame.difficulty = difficulty; 118 | } 119 | 120 | public static EnemyObj getClosestEnemyInSight() { 121 | return closestEnemyInSight; 122 | } 123 | 124 | public static void setClosestEnemyInSight(EnemyObj closestEnemyInSight) { 125 | Wolf3DGame.closestEnemyInSight = closestEnemyInSight; 126 | } 127 | 128 | private static int nextFloor; 129 | 130 | public static void gotoSecretLevel() { 131 | nextFloor = Resource.getIntProperty("MAP_LEVEL_SECRET") + 1; 132 | SceneManager.switchTo("level_cleared_statistics"); 133 | Audio.playSound("LEVELDONE"); 134 | } 135 | 136 | public static void gotoNextLevel() { 137 | levelIndex++; 138 | nextFloor = Resource.getIntProperty("MAP_LEVEL_" + levelIndex) + 1; 139 | SceneManager.switchTo("level_cleared_statistics"); 140 | Audio.playSound("LEVELDONE"); 141 | } 142 | 143 | public static void startNextLevel() { 144 | floor = nextFloor; 145 | GameMap.loadByFloorNumber(floor); 146 | playing = true; 147 | } 148 | 149 | public static boolean isPlaying() { 150 | return playing; 151 | } 152 | 153 | public static void newGame() { 154 | reset(); 155 | Weapons.reset(); 156 | FizzleFade.reset(); 157 | SceneManager.switchTo("stage"); 158 | } 159 | 160 | // for debugging purposes 161 | public static Point playerOverrideLocation; 162 | public static void newGame(int startFloor, Point playerOverrideLocation) { 163 | Wolf3DGame.playerOverrideLocation = playerOverrideLocation; 164 | newGame(); 165 | levelIndex = startFloor - 1; 166 | floor = startFloor; 167 | nextFloor = floor; 168 | } 169 | 170 | public static void backToGame() { 171 | backToGame = true; 172 | SceneManager.switchTo("stage"); 173 | } 174 | 175 | public static boolean isBackToGame() { 176 | return backToGame; 177 | } 178 | 179 | public static void setBackToGame(boolean backToGame) { 180 | Wolf3DGame.backToGame = backToGame; 181 | } 182 | 183 | public static boolean tryNextLife() { 184 | lives--; 185 | if (lives <= 0) { 186 | playing = false; 187 | SceneManager.switchTo("title"); 188 | return false; 189 | } 190 | GameMap.loadByFloorNumber(floor); 191 | Weapons.getCurrentPlayerWeapon().reset(); 192 | lifeEnergy = 100; 193 | return true; 194 | } 195 | 196 | public static void gameCleared() { 197 | playing = false; 198 | SceneManager.switchTo("level_cleared_statistics"); 199 | // TODO goto hiscore 200 | } 201 | 202 | // --- statistics --- 203 | 204 | public static int getStatisticsKill() { 205 | int totalEnemies = GameMap.getTotalEnemies(); 206 | int deadEnemies = 0; 207 | for (EnemyObj enemy : Enemies.getEnemies()) { 208 | if (enemy.getEnemyState() == DEAD) deadEnemies++; 209 | } 210 | int statisticsKill = 0; 211 | if (totalEnemies > 0) { 212 | statisticsKill = (int) (100 * deadEnemies / (double) totalEnemies); 213 | } 214 | return statisticsKill; 215 | } 216 | 217 | public static int getStatisticsSecret() { 218 | int totalSecrets = GameMap.getTotalSecrets(); 219 | int statisticsSecret = 0; 220 | if (totalSecrets > 0) { 221 | statisticsSecret 222 | = (int) (100 * secretDoorsFoundCount / (double) totalSecrets); 223 | } 224 | return statisticsSecret; 225 | } 226 | 227 | public static int getStatisticsTreasures() { 228 | int totalTreasures = GameMap.getTotalTreasures(); 229 | int statisticsTreasures = 0; 230 | if (totalTreasures > 0) { 231 | statisticsTreasures 232 | = (int) (100 * collectedTreasuresCount / (double) totalTreasures); 233 | } 234 | return statisticsTreasures; 235 | } 236 | 237 | // --- 238 | 239 | public void start() { 240 | Resource.initialize(); 241 | Audio.initialize(); 242 | Weapons.createPlayerWeapons(); 243 | 244 | SceneManager.addState(new Initializing()); 245 | SceneManager.addState(new OLPresents()); 246 | SceneManager.addState(new ProfoundCarnage13()); 247 | SceneManager.addState(new Credits()); 248 | SceneManager.addState(new Title()); 249 | SceneManager.addState(new GameOptions()); 250 | SceneManager.addState(new GameDifficulty()); 251 | SceneManager.addState(new Stage()); 252 | SceneManager.addState(new LevelClearedStatistics()); 253 | SceneManager.addState(new Quit()); 254 | SceneManager.startAll(); 255 | 256 | SceneManager.switchTo("initializing"); 257 | } 258 | 259 | public void update(double delta) { 260 | SceneManager.update(delta); 261 | } 262 | 263 | public void fixedUpdate() { 264 | SceneManager.fixedUpdate(); 265 | } 266 | 267 | public void draw(Graphics2D g) { 268 | g.setBackground(Color.BLACK); 269 | g.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 270 | SceneManager.draw(g); 271 | } 272 | 273 | } -------------------------------------------------------------------------------- /src/wolf3d/main/Main.java: -------------------------------------------------------------------------------- 1 | package wolf3d.main; 2 | 3 | import javax.swing.SwingUtilities; 4 | import wolf3d.infra.Display; 5 | import wolf3d.infra.GameCanvas; 6 | import wolf3d.infra.Wolf3DGame; 7 | 8 | /** 9 | * Main class. 10 | * 11 | * Game entry point. 12 | * 13 | * @author Leonardo Ono (ono.leo80@gmail.com) 14 | */ 15 | public class Main { 16 | 17 | public static void main(String[] args) { 18 | SwingUtilities.invokeLater(() -> { 19 | GameCanvas gameCanvas = new GameCanvas(new Wolf3DGame()); 20 | Display display = new Display(gameCanvas); 21 | display.setTitle("Java Wolfenstein 3D Engine v0.0.1 " 22 | + "[F12 - Full screen][F11 - Keep aspect ratio]"); 23 | 24 | // display.setIconImage(Resource.getImage("icon")); 25 | display.start(); 26 | }); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/wolf3d/scene/Credits.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | import wolf3d.infra.Input; 6 | import wolf3d.infra.Resource; 7 | import wolf3d.infra.Scene; 8 | import wolf3d.infra.SceneManager; 9 | import static wolf3d.infra.Settings.*; 10 | import wolf3d.infra.Util; 11 | 12 | /** 13 | * ProfoundCarnage class. 14 | * 15 | * @author Leonardo Ono (ono.leo80@gmail.com) 16 | */ 17 | public class Credits extends Scene { 18 | 19 | private BufferedImage creditsPic; 20 | private long nextSceneTime; 21 | 22 | public Credits() { 23 | super("credits"); 24 | } 25 | 26 | @Override 27 | public void onEnter() { 28 | creditsPic = Resource.getPic("CREDITS"); 29 | } 30 | 31 | @Override 32 | public void onTransitionFinished() { 33 | nextSceneTime = Util.getTimeMs() + 10000; 34 | } 35 | 36 | @Override 37 | public void onExit() { 38 | } 39 | 40 | @Override 41 | public void fixedUpdate() { 42 | if (Input.isKeyJustPressed(KEY_START_1) 43 | || Input.isKeyJustPressed(KEY_START_2) 44 | || Util.getTimeMs() >= nextSceneTime) { 45 | 46 | SceneManager.switchTo("game_options", false); 47 | } 48 | } 49 | 50 | @Override 51 | public void draw(Graphics2D g) { 52 | g.drawImage(creditsPic, 0, 0, null); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/wolf3d/scene/GameDifficulty.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | import wolf3d.asset.loader.VGAGRAPHLoader.VGAGRAPHFont; 7 | import wolf3d.infra.Audio; 8 | import wolf3d.infra.Input; 9 | import wolf3d.infra.Resource; 10 | import wolf3d.infra.Scene; 11 | import wolf3d.infra.SceneManager; 12 | import static wolf3d.infra.Settings.*; 13 | import wolf3d.infra.Util; 14 | import wolf3d.infra.Wolf3DGame; 15 | import wolf3d.infra.Wolf3DGame.Difficulty; 16 | 17 | /** 18 | * GameDifficulty class. 19 | * 20 | * @author Leonardo Ono (ono.leo80@gmail.com) 21 | */ 22 | public class GameDifficulty extends Scene { 23 | 24 | private final VGAGRAPHFont font; 25 | private final VGAGRAPHFont fontSelection; 26 | private final VGAGRAPHFont fontYellow; 27 | 28 | private final BufferedImage[] selectionPics = new BufferedImage[2]; 29 | private final BufferedImage footerPic; 30 | private final BufferedImage[] facePics = new BufferedImage[4]; 31 | private final Color backgroundColor; 32 | private final Color backgroundColor2; 33 | 34 | private final String[] options = { 35 | "Can I play, Daddy?", 36 | "Don't hurt me.", 37 | "Bring'em on!", 38 | "I am Death incarnate!"}; 39 | 40 | private int selectedOptionIndex; 41 | private int selectionAnimationFrame; 42 | 43 | public GameDifficulty() { 44 | super("game_difficulty"); 45 | 46 | font = Resource.getFont("BIG_GRAY"); 47 | fontSelection = Resource.getFont("BIG_LIGHT_GRAY"); 48 | fontYellow = Resource.getFont("BIG_YELLOW"); 49 | 50 | selectionPics[0] = Resource.getPic("OPTIONS_SELECTION_0"); 51 | selectionPics[1] = Resource.getPic("OPTIONS_SELECTION_1"); 52 | footerPic = Resource.getPic("OPTIONS_FOOTER"); 53 | 54 | int facesStartPicIndex = Resource.getIntProperty( 55 | "DIFFICULTY_FACES_START_PIC_INDEX"); 56 | 57 | for (int i = 0; i < 4; i++) { 58 | facePics[i] = Resource.getPic(facesStartPicIndex + i); 59 | } 60 | 61 | backgroundColor = Util.getColor("0x8a0000ff"); 62 | backgroundColor2 = Util.getColor("0x590000ff"); 63 | } 64 | 65 | @Override 66 | public void onEnter() { 67 | if (!"WONDERIN".equals(Audio.getCurrentMusicId())) { 68 | Audio.playMusic("WONDERIN"); 69 | } 70 | selectedOptionIndex = 2; 71 | } 72 | 73 | @Override 74 | public void onTransitionFinished() { 75 | } 76 | 77 | @Override 78 | public void onExit() { 79 | } 80 | 81 | @Override 82 | public void fixedUpdate() { 83 | selectionAnimationFrame = (int) (System.nanoTime() * 0.0000000025) % 2; 84 | 85 | if (Input.isKeyJustPressed(KEY_PLAYER_UP)) { 86 | selectedOptionIndex--; 87 | if (selectedOptionIndex < 0) { 88 | selectedOptionIndex = options.length - 1; 89 | } 90 | Audio.playSound("WALK1"); 91 | //Audio.playSound("MOVEGUN2"); 92 | } 93 | else if (Input.isKeyJustPressed(KEY_PLAYER_DOWN)) { 94 | selectedOptionIndex++; 95 | if (selectedOptionIndex > options.length - 1) { 96 | selectedOptionIndex = 0; 97 | } 98 | Audio.playSound("WALK2"); 99 | //Audio.playSound("MOVEGUN2"); 100 | } 101 | 102 | if (Input.isKeyJustPressed(KEY_CANCEL)) { 103 | SceneManager.switchTo("game_options", false); 104 | Audio.playSound("ESCPRESSED"); 105 | } 106 | else if (Input.isKeyJustPressed(KEY_START_1) 107 | || Input.isKeyJustPressed(KEY_START_2)) { 108 | 109 | Wolf3DGame.setDifficulty(Difficulty.values()[selectedOptionIndex]); 110 | Wolf3DGame.newGame(); 111 | Audio.playSound("MISSILEFIRE"); 112 | } 113 | } 114 | 115 | public void quit() { 116 | SceneManager.switchTo("quit", false); 117 | } 118 | 119 | @Override 120 | public void draw(Graphics2D g) { 121 | g.setColor(backgroundColor); 122 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 123 | 124 | g.drawImage(footerPic, 112, 184, null); 125 | 126 | fontYellow.drawString(g, "How tough are you?", 69, 68); 127 | 128 | g.setColor(backgroundColor2); 129 | g.fillRect(46, 91, 224, 66); 130 | g.draw3DRect(46, 91, 224, 66, false); 131 | 132 | int optionDy = 0; 133 | for (int i = 0; i < options.length; i++) { 134 | String option = options[i]; 135 | if (i == selectedOptionIndex) { 136 | fontSelection.drawString(g, option, 73, 100 + optionDy); 137 | } 138 | else { 139 | font.drawString(g, option, 73, 100 + optionDy); 140 | } 141 | if (i == selectedOptionIndex) { 142 | fontSelection.drawString(g, option, 73, 100 + optionDy); 143 | g.drawImage(selectionPics[selectionAnimationFrame] 144 | , 47, 100 + optionDy, null); 145 | } 146 | optionDy += 13; 147 | } 148 | g.drawImage(facePics[selectedOptionIndex], 232, 107, null); 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/wolf3d/scene/GameOptions.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | import wolf3d.asset.loader.VGAGRAPHLoader.VGAGRAPHFont; 7 | import wolf3d.infra.Audio; 8 | import wolf3d.infra.Input; 9 | import wolf3d.infra.Resource; 10 | import wolf3d.infra.Scene; 11 | import wolf3d.infra.SceneManager; 12 | import static wolf3d.infra.Settings.*; 13 | import wolf3d.infra.Util; 14 | import wolf3d.infra.Wolf3DGame; 15 | 16 | /** 17 | * ProfoundCarnage class. 18 | * 19 | * @author Leonardo Ono (ono.leo80@gmail.com) 20 | */ 21 | public class GameOptions extends Scene { 22 | 23 | private final VGAGRAPHFont font; 24 | private final VGAGRAPHFont fontSelection; 25 | private final VGAGRAPHFont fontDisabled; 26 | 27 | private final BufferedImage optionsPic; 28 | private final BufferedImage[] selectionPics = new BufferedImage[2]; 29 | private final BufferedImage footerPic; 30 | private final Color backgroundColor; 31 | private final Color backgroundColor2; 32 | 33 | private final String[] options = { "New Game", "Back to Game", "Quit" }; 34 | private int selectedOptionIndex; 35 | private int selectionAnimationFrame; 36 | 37 | public GameOptions() { 38 | super("game_options"); 39 | 40 | font = Resource.getFont("BIG_GRAY"); 41 | fontSelection = Resource.getFont("BIG_LIGHT_GRAY"); 42 | fontDisabled = Resource.getFont("BIG_DARK_RED"); 43 | 44 | optionsPic = Resource.getPic("OPTIONS"); 45 | selectionPics[0] = Resource.getPic("OPTIONS_SELECTION_0"); 46 | selectionPics[1] = Resource.getPic("OPTIONS_SELECTION_1"); 47 | footerPic = Resource.getPic("OPTIONS_FOOTER"); 48 | backgroundColor = Util.getColor("0x8a0000ff"); 49 | backgroundColor2 = Util.getColor("0x590000ff"); 50 | } 51 | 52 | @Override 53 | public void onEnter() { 54 | if (!"WONDERIN".equals(Audio.getCurrentMusicId())) { 55 | Audio.playMusic("WONDERIN"); 56 | } 57 | selectedOptionIndex = Wolf3DGame.isPlaying() ? 1 : 0; 58 | } 59 | 60 | @Override 61 | public void onTransitionFinished() { 62 | } 63 | 64 | @Override 65 | public void onExit() { 66 | } 67 | 68 | @Override 69 | public void fixedUpdate() { 70 | selectionAnimationFrame = (int) (System.nanoTime() * 0.0000000025) % 2; 71 | 72 | if (Input.isKeyJustPressed(KEY_PLAYER_UP)) { 73 | selectedOptionIndex--; 74 | if (selectedOptionIndex == 1 && !Wolf3DGame.isPlaying()) { 75 | selectedOptionIndex--; 76 | } 77 | if (selectedOptionIndex < 0) { 78 | selectedOptionIndex = options.length - 1; 79 | } 80 | Audio.playSound("WALK1"); 81 | //Audio.playSound("MOVEGUN2"); 82 | } 83 | else if (Input.isKeyJustPressed(KEY_PLAYER_DOWN)) { 84 | selectedOptionIndex++; 85 | if (selectedOptionIndex == 1 && !Wolf3DGame.isPlaying()) { 86 | selectedOptionIndex++; 87 | } 88 | if (selectedOptionIndex > options.length - 1) { 89 | selectedOptionIndex = 0; 90 | } 91 | Audio.playSound("WALK2"); 92 | //Audio.playSound("MOVEGUN2"); 93 | } 94 | 95 | if (Input.isKeyJustPressed(KEY_CANCEL)) { 96 | SceneManager.switchTo("title"); 97 | Audio.playSound("ESCPRESSED"); 98 | } 99 | else if (Input.isKeyJustPressed(KEY_START_1) 100 | || Input.isKeyJustPressed(KEY_START_2)) { 101 | 102 | switch (selectedOptionIndex) { 103 | case 0 -> newGame(); 104 | case 1 -> Wolf3DGame.backToGame(); 105 | case 2 -> quit(); 106 | } 107 | Audio.playSound("MISSILEFIRE"); 108 | } 109 | } 110 | 111 | private void newGame() { 112 | SceneManager.switchTo("game_difficulty", false); 113 | } 114 | 115 | private void quit() { 116 | SceneManager.switchTo("quit", false); 117 | } 118 | 119 | @Override 120 | public void draw(Graphics2D g) { 121 | g.setColor(backgroundColor); 122 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 123 | 124 | g.setColor(Color.BLACK); 125 | g.fillRect(0, 10, CANVAS_WIDTH, 24); 126 | g.setColor(backgroundColor2); 127 | g.drawLine(0, 32, CANVAS_WIDTH, 32); 128 | 129 | g.drawImage(optionsPic, 80, 0, null); 130 | g.drawImage(footerPic, 112, 184, null); 131 | 132 | g.fillRect(69, 53, 177, 135); 133 | g.draw3DRect(69, 53, 177, 135, false); 134 | 135 | int optionDy = 0; 136 | for (int i = 0; i < options.length; i++) { 137 | String option = options[i]; 138 | if (i == 1 && !Wolf3DGame.isPlaying()) { 139 | fontDisabled.drawString(g, option, 100, 57 + optionDy); 140 | } 141 | else if (i == selectedOptionIndex) { 142 | fontSelection.drawString(g, option, 100, 57 + optionDy); 143 | } 144 | else { 145 | font.drawString(g, option, 100, 57 + optionDy); 146 | } 147 | if (i == selectedOptionIndex) { 148 | fontSelection.drawString(g, option, 100, 57 + optionDy); 149 | g.drawImage(selectionPics[selectionAnimationFrame] 150 | , 72, 55 + optionDy, null); 151 | } 152 | optionDy += 13; 153 | } 154 | 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/wolf3d/scene/Initializing.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Point; 4 | import wolf3d.infra.Scene; 5 | import wolf3d.infra.SceneManager; 6 | import wolf3d.infra.Util; 7 | import wolf3d.infra.Wolf3DGame; 8 | 9 | /** 10 | * Initializing class. 11 | * 12 | * @author Leonardo Ono (ono.leo80@gmail.com) 13 | */ 14 | public class Initializing extends Scene { 15 | 16 | private long startTime; 17 | 18 | public Initializing() { 19 | super("initializing"); 20 | } 21 | 22 | @Override 23 | public void onEnter() { 24 | startTime = Util.getTimeMs() + 500; 25 | } 26 | 27 | @Override 28 | public void fixedUpdate() { 29 | if (Util.getTimeMs() >= startTime) { 30 | SceneManager.switchTo("ol_presents"); 31 | 32 | // debug: start game with specific level floor and player location 33 | //Wolf3DGame.newGame(9, new Point(34, 17)); 34 | //Wolf3DGame.newGame(1, new Point(29, 57)); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/wolf3d/scene/LevelClearedStatistics.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | import wolf3d.infra.Audio; 7 | import wolf3d.infra.HUD; 8 | import wolf3d.infra.Input; 9 | import wolf3d.infra.Player; 10 | import wolf3d.infra.Resource; 11 | import wolf3d.infra.Scene; 12 | import wolf3d.infra.SceneManager; 13 | import static wolf3d.infra.Settings.*; 14 | import wolf3d.infra.Util; 15 | import wolf3d.infra.Wolf3DGame; 16 | 17 | /** 18 | * LevelClearedStatistics class. 19 | * 20 | * @author Leonardo Ono (ono.leo80@gmail.com) 21 | */ 22 | public class LevelClearedStatistics extends Scene { 23 | 24 | private long startTime; 25 | private final Color backgroundColor; 26 | private final BufferedImage[] playerPics = new BufferedImage[2]; 27 | private final BufferedImage playerWinPic; 28 | private int selectionAnimationFrame; 29 | 30 | private int targetBonus; 31 | private int targetKill; 32 | private int targetSecret; 33 | private int targetTreasure; 34 | 35 | private int currentBonus; 36 | private int currentKill; 37 | private int currentSecret; 38 | private int currentTreasure; 39 | 40 | private boolean hasDif1; 41 | private boolean hasDif2; 42 | private boolean hasDif3; 43 | private boolean hasDif4; 44 | 45 | private int playTimeHour; 46 | private int playTimeMinutes; 47 | private int playTimeSeconds; 48 | 49 | private int state; // 0=wait, 1=bonus, 2=kill, 3=secret, 4=treasure 50 | private int nextState; // 0=wait, 1=bonus, 2=kill, 3=secret, 4=treasure 51 | private int stateFrame; 52 | 53 | public LevelClearedStatistics() { 54 | super("level_cleared_statistics"); 55 | backgroundColor = Util.getColor("0x004040ff"); 56 | playerPics[0] = Resource.getPic("STATISTICS_0"); 57 | playerPics[1] = Resource.getPic("STATISTICS_1"); 58 | playerWinPic = Resource.getPic("STATISTICS_WIN"); 59 | createStatisticFont(); 60 | } 61 | 62 | private final BufferedImage[] fontCharPics = new BufferedImage[256]; 63 | 64 | private void createStatisticFont() { 65 | int startPicIndex 66 | = Resource.getIntProperty("STATISTICS_FONT_START_PIC_INDEX"); 67 | 68 | String fontChars = Resource.getProperty("STATISTICS_FONT_CHARS"); 69 | for (int i = 0; i < fontChars.length(); i++) { 70 | int c = fontChars.charAt(i); 71 | fontCharPics[c] = Resource.getPic(startPicIndex + i); 72 | } 73 | } 74 | 75 | private void drawString(Graphics2D g, String text, int x, int y) { 76 | int dx = 0; 77 | for (int i = 0; i < text.length(); i++) { 78 | int c = text.charAt(i); 79 | BufferedImage charImage = fontCharPics[c]; 80 | g.drawImage(charImage, x + dx, y, null); 81 | dx += 16; 82 | } 83 | } 84 | 85 | private void drawNumber(Graphics2D g 86 | , int number, int numberOfDigits, String preChar, int x, int y) { 87 | 88 | String numberStr = preChar.repeat(numberOfDigits) + number; 89 | numberStr = numberStr.substring( 90 | numberStr.length() - numberOfDigits, numberStr.length()); 91 | 92 | drawString(g, numberStr, x, y); 93 | } 94 | 95 | @Override 96 | public void onEnter() { 97 | Audio.playMusic("ENDLEVEL"); 98 | targetBonus = 0; 99 | targetKill = Wolf3DGame.getStatisticsKill(); 100 | targetSecret = Wolf3DGame.getStatisticsSecret(); 101 | targetTreasure = Wolf3DGame.getStatisticsTreasures(); 102 | currentBonus = 0; 103 | currentKill = 0; 104 | currentSecret = 0; 105 | currentTreasure = 0; 106 | hasDif1 = (targetBonus - currentBonus) > 0; 107 | hasDif2 = (targetKill - currentKill) > 0; 108 | hasDif3 = (targetSecret - currentSecret) > 0; 109 | hasDif4 = (targetTreasure - currentTreasure) > 0; 110 | 111 | long playTime = Player.getPlayTimeMs(); 112 | playTimeHour = Util.extractTimeMsHoursPart(playTime); 113 | playTimeMinutes = Util.extractTimeMsMinutesPart(playTime); 114 | playTimeSeconds = Util.extractTimeMsSecondsPart(playTime); 115 | } 116 | 117 | @Override 118 | public void onTransitionFinished() { 119 | state = 0; 120 | nextState = 1; 121 | stateFrame = 0; 122 | startTime = Util.getTimeMs() + 1000; 123 | } 124 | 125 | @Override 126 | public void fixedUpdate() { 127 | selectionAnimationFrame = (int) (System.nanoTime() * 0.0000000025) % 2; 128 | 129 | final int incSpeed = 5; 130 | stateFrame++; 131 | switch (state) { 132 | case 0 -> { // just wait a little 133 | if (Util.getTimeMs() >= startTime) { 134 | state = nextState; 135 | } 136 | } 137 | case 1 -> { // update bonus 138 | if (!hasDif1) { 139 | state = 2; 140 | break; 141 | } 142 | currentBonus += incSpeed; 143 | if (currentBonus >= targetBonus) { 144 | currentBonus = targetBonus; 145 | Audio.playSound("ENDBONUS2"); 146 | startTime = Util.getTimeMs() + 1000; 147 | state = 0; 148 | nextState = 2; 149 | } 150 | else { 151 | Audio.playSound("ENDBONUS1"); 152 | } 153 | } 154 | case 2 -> { // update kill 155 | if (!hasDif2) { 156 | state = 3; 157 | break; 158 | } 159 | currentKill += incSpeed; 160 | if (currentKill >= targetKill) { 161 | currentKill = targetKill; 162 | Audio.playSound("ENDBONUS2"); 163 | startTime = Util.getTimeMs() + 1000; 164 | state = 0; 165 | nextState = 3; 166 | } 167 | else { 168 | Audio.playSound("ENDBONUS1"); 169 | } 170 | } 171 | case 3 -> { // update secret 172 | if (!hasDif3) { 173 | state = 4; 174 | break; 175 | } 176 | currentSecret += incSpeed; 177 | if (currentSecret >= targetSecret) { 178 | currentSecret = targetSecret; 179 | Audio.playSound("ENDBONUS2"); 180 | startTime = Util.getTimeMs() + 1000; 181 | state = 0; 182 | nextState = 4; 183 | } 184 | else { 185 | Audio.playSound("ENDBONUS1"); 186 | } 187 | } 188 | case 4 -> { // update treasure 189 | if (!hasDif4) { 190 | startTime = Util.getTimeMs() + 1000; 191 | state = 5; 192 | break; 193 | } 194 | currentTreasure += incSpeed; 195 | if (currentTreasure >= targetTreasure) { 196 | currentTreasure = targetTreasure; 197 | Audio.playSound("ENDBONUS2"); 198 | startTime = Util.getTimeMs() + 1000; 199 | state = 0; 200 | nextState = 5; 201 | } 202 | else { 203 | Audio.playSound("ENDBONUS1"); 204 | } 205 | } 206 | case 5 -> { 207 | if (Input.isKeyJustPressed(KEY_START_1) 208 | || Input.isKeyJustPressed(KEY_START_2)) { 209 | 210 | if (Player.isPlayerTriggeredEndGame()) { 211 | // TODO goto hiscore 212 | SceneManager.switchTo("title"); 213 | } 214 | else { 215 | SceneManager.switchTo("stage"); 216 | } 217 | } 218 | } 219 | } 220 | 221 | //HUD.fixedUpdate(); 222 | } 223 | 224 | @Override 225 | public void draw(Graphics2D g) { 226 | g.setColor(backgroundColor); 227 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 228 | g.drawString("GAME CLEARED STATISTICS", 10, 50); 229 | 230 | if (Player.isPlayerTriggeredEndGame()) { 231 | drawString(g, "YOU WIN!", 146, 16); 232 | g.drawImage(playerWinPic, 16, 16, null); 233 | } 234 | else { 235 | drawString(g, "FLOOR " + Wolf3DGame.getFloor(), 114, 16); 236 | drawString(g, "COMPLETED", 114, 32); 237 | 238 | drawString(g, "BONUS", 114, 56); 239 | drawNumber(g, 0, 6, " ", 210, 56); 240 | 241 | g.drawImage(playerPics[selectionAnimationFrame], 0, 16, null); 242 | } 243 | 244 | drawString(g, "TIME", 114, 80); 245 | drawNumber(g, playTimeHour, 2, "0", 191 , 80); 246 | drawString(g, ":", 220 , 80); 247 | drawNumber(g, playTimeMinutes, 2, "0", 229, 80); 248 | drawString(g, ":", 261, 80); 249 | drawNumber(g, playTimeSeconds, 2, "0", 270, 80); 250 | 251 | // TODO par time 252 | // ref.: https://wolfenstein.fandom.com/wiki/Par 253 | drawString(g, "PAR", 130, 96); 254 | drawNumber(g, 0, 2, "0", 191 , 96); 255 | drawString(g, ":", 220 , 96); 256 | drawNumber(g, 1, 2, "0", 229, 96); 257 | drawString(g, ":", 261, 96); 258 | drawNumber(g, 30, 2, "0", 270, 96); 259 | 260 | drawString(g, " KILL RATIO %", 8, 112); 261 | drawString(g, " SECRET RATIO %", 8, 128); 262 | drawString(g, "TREASURE RATIO %", 8, 144); 263 | 264 | drawNumber(g, currentKill, 3, " ", 8 + 15 * 16, 112); 265 | drawNumber(g, currentSecret, 3, " ", 8 + 15 * 16, 128); 266 | drawNumber(g, currentTreasure, 3, " ", 8 + 15 * 16, 144); 267 | 268 | HUD.draw(g); 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /src/wolf3d/scene/OLPresents.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Font; 5 | import java.awt.FontFormatException; 6 | import java.awt.Graphics2D; 7 | import java.awt.Point; 8 | import java.awt.Polygon; 9 | import java.awt.Shape; 10 | import java.awt.geom.AffineTransform; 11 | import java.awt.geom.NoninvertibleTransformException; 12 | import java.awt.image.BufferedImage; 13 | import java.awt.image.ColorModel; 14 | import java.awt.image.DataBufferInt; 15 | import java.awt.image.WritableRaster; 16 | import java.io.DataInputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Scanner; 23 | import java.util.logging.Level; 24 | import java.util.logging.Logger; 25 | import javax.imageio.ImageIO; 26 | import wolf3d.infra.Scene; 27 | import wolf3d.infra.SceneManager; 28 | import static wolf3d.infra.Settings.*; 29 | import wolf3d.infra.Util; 30 | 31 | /** 32 | * OLPresents class. 33 | * 34 | * @author Leonardo Ono (ono.leo80@gmail.com) 35 | */ 36 | public class OLPresents extends Scene { 37 | 38 | private BufferedImage skin; 39 | private double[][] points; 40 | private double[][] sts; 41 | private final double[][] va = new double[3][5]; 42 | private int[][] faces; 43 | private final int[][] ps = new int [2][3]; 44 | private int alpha; 45 | private boolean animationFinished; 46 | private Font font; 47 | private final String presentsText = "PRESENTS"; 48 | private double letterIndex; 49 | 50 | public OLPresents() { 51 | super("ol_presents"); 52 | loadFont(); 53 | loadAnimation(); 54 | } 55 | 56 | private void loadFont() { 57 | try { 58 | InputStream is = OLPresents.class 59 | .getResourceAsStream("/res/8-bit Arcade In.ttf"); 60 | 61 | Font fontTmp = Font.createFont(Font.TRUETYPE_FONT, is); 62 | font = fontTmp.deriveFont(17.0f); 63 | } catch (FontFormatException | IOException ex) { 64 | String className = OLPresents.class.getName(); 65 | Logger.getLogger(className).log(Level.SEVERE, null, ex); 66 | System.exit(-1); 67 | } 68 | } 69 | 70 | private void loadAnimation() { 71 | try { 72 | InputStream is = getClass() 73 | .getResourceAsStream("/res/ol_presents.png"); 74 | 75 | skin = ImageIO.read(is); 76 | try ( DataInputStream dis = new DataInputStream( 77 | getClass().getResourceAsStream("/res/ol_presents.mdd")) ) { 78 | 79 | int totalFrames = dis.readInt(); 80 | int totalPoints = dis.readInt(); 81 | dis.read(new byte[totalFrames * 4]); // unused datas 82 | points = new double[totalFrames][totalPoints * 3]; 83 | for (int s = 0; s < totalFrames; s++) { 84 | for (int p = 0; p < totalPoints * 3; p++) { 85 | points[s][p] = dis.readFloat() * 0.01; 86 | // manually adjusting the positions 87 | if ((p + 1) % 3 == 0) { 88 | points[s][p] += 0.175; 89 | } 90 | if ((p + 2) % 3 == 0) { 91 | points[s][p] += 0.01; 92 | } 93 | } 94 | } 95 | } 96 | } catch (IOException ex) { 97 | Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); 98 | } 99 | try ( Scanner sc = new Scanner( 100 | getClass().getResourceAsStream("/res/ol_presents.obj")) ) { 101 | 102 | List facesTmp = new ArrayList<>(); 103 | List stsTmp = new ArrayList<>(); 104 | sc.useDelimiter("[ /\n]"); 105 | while (sc.hasNext()) { 106 | String token = sc.next(); 107 | if (token.equals("vt")) { 108 | stsTmp.add(new double[] { sc.nextDouble() * skin.getWidth() 109 | , (1 - sc.nextDouble()) * skin.getHeight() } ); 110 | } 111 | else if (token.equals("f")) { 112 | facesTmp.add( new int[] { sc.nextInt() - 1, sc.nextInt() - 1 113 | , sc.nextInt() - 1, sc.nextInt() - 1 114 | , sc.nextInt() - 1, sc.nextInt() - 1 } ); 115 | } 116 | } 117 | faces = facesTmp.toArray(new int[0][0]); 118 | sts = stsTmp.toArray(new double[0][0]); 119 | } 120 | } 121 | 122 | private double currentDepth, frame; 123 | private double[] depthBuffer = new double[320 * 200]; 124 | private final BufferedImage nbimodel 125 | = new BufferedImage(320, 200, BufferedImage.TYPE_INT_ARGB); 126 | 127 | private final WritableRaster wr 128 | = new WritableRaster(nbimodel.getSampleModel() 129 | , new DataBufferInt(320 * 200), new Point()) { 130 | 131 | @Override 132 | public void setDataElements(int x, int y, Object inData) { 133 | if (currentDepth <= depthBuffer[y * 320 + x]) { 134 | super.setDataElements(x, y, inData); 135 | depthBuffer[y * 320 + x] = currentDepth; 136 | } 137 | } 138 | }; 139 | 140 | private final ColorModel cm = ColorModel.getRGBdefault(); 141 | private final BufferedImage nbi = new BufferedImage(cm, wr, false, null); 142 | private final Graphics2D nbig = nbi.createGraphics(); 143 | private final Polygon polygon = new Polygon(ps[0], ps[1], 3); 144 | private final AffineTransform[] ts 145 | = { new AffineTransform(), new AffineTransform() }; 146 | 147 | @Override 148 | public void onEnter() { 149 | animationFinished = false; 150 | alpha = 255; 151 | letterIndex = 0; 152 | } 153 | 154 | @Override 155 | public void onExit() { 156 | } 157 | 158 | @Override 159 | public void fixedUpdate() { 160 | alpha -= 3; 161 | if (alpha < 0) { 162 | alpha = 0; 163 | } 164 | 165 | frame += 1.0; 166 | if (frame > points.length - 1) { 167 | frame = points.length - 1; 168 | animationFinished = true; 169 | } 170 | 171 | if (animationFinished) { 172 | letterIndex += 0.1; 173 | } 174 | 175 | if (letterIndex >= 20) { 176 | SceneManager.switchTo("profound_carnage_13"); 177 | } 178 | } 179 | 180 | @Override 181 | public void draw(Graphics2D g) { 182 | nbig.clearRect(0, 0, 320, 200); 183 | Arrays.fill(depthBuffer, Double.MAX_VALUE); 184 | for (int[] face : faces) { 185 | for (int fv = 0; fv < 3; fv++) { 186 | for (int i = 0; i < 3; i++) { 187 | va[fv][i] = points[(int) frame][face[fv * 2] * 3 + i]; 188 | } 189 | va[fv][3] = sts[face[fv * 2 + 1]][0]; 190 | va[fv][4] = sts[face[fv * 2 + 1]][1]; 191 | } 192 | drawAffineTextured3DTriangle(va); 193 | } 194 | g.drawImage(nbi, 0, 0, 320, 200, null); 195 | if (alpha > 0) { 196 | g.setColor(Util.getColor(0, 0, 0, alpha)); 197 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 198 | } 199 | 200 | int endIndex = (int) letterIndex; 201 | if (endIndex > presentsText.length()) { 202 | endIndex = presentsText.length(); 203 | } 204 | String partialText = "PRESENTS".substring(0, endIndex); 205 | drawText(g, partialText, 131, 130); 206 | } 207 | 208 | private void drawAffineTextured3DTriangle(double[][] v) { 209 | currentDepth = v[0][2] + v[1][2] + v[2][2]; 210 | for (int i = 0; i < 3; i++) { 211 | ps[0][i] = (int) (160 + 160 * v[i][0] / v[i][2]); 212 | ps[1][i] = (int) (100 - 160 * v[i][1] / v[i][2]); 213 | } 214 | polygon.xpoints = ps[0]; 215 | polygon.ypoints = ps[1]; 216 | ts[0].setTransform(v[0][3] - v[2][3], v[0][4] - v[2][4] 217 | , v[1][3] - v[2][3], v[1][4] - v[2][4], v[2][3], v[2][4]); 218 | 219 | try { 220 | ts[0].invert(); 221 | } catch (NoninvertibleTransformException ex) { } 222 | Shape originalClip = nbig.getClip(); 223 | nbig.clip(polygon); 224 | ts[1].setTransform(ps[0][0] - ps[0][2], ps[1][0] - ps[1][2] 225 | , ps[0][1] - ps[0][2], ps[1][1] - ps[1][2], ps[0][2], ps[1][2]); 226 | 227 | ts[1].concatenate(ts[0]); 228 | nbig.drawImage(skin, ts[1], null); 229 | nbig.setClip(originalClip); 230 | } 231 | 232 | private void drawText(Graphics2D g, String text, int x, int y) { 233 | g.setFont(font); 234 | g.setColor(Color.WHITE); 235 | g.drawString(text, x, y); 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /src/wolf3d/scene/ProfoundCarnage13.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.image.BufferedImage; 6 | import wolf3d.infra.Audio; 7 | import wolf3d.infra.Input; 8 | import wolf3d.infra.Resource; 9 | import wolf3d.infra.Scene; 10 | import wolf3d.infra.SceneManager; 11 | import static wolf3d.infra.Settings.*; 12 | import wolf3d.infra.Util; 13 | 14 | /** 15 | * ProfoundCarnage class. 16 | * 17 | * @author Leonardo Ono (ono.leo80@gmail.com) 18 | */ 19 | public class ProfoundCarnage13 extends Scene { 20 | 21 | private final Color backgroundColor; 22 | private BufferedImage pc13Image; 23 | 24 | public ProfoundCarnage13() { 25 | super("profound_carnage_13"); 26 | backgroundColor = Util.getColor("0x20a8fcff"); 27 | } 28 | 29 | @Override 30 | public void onEnter() { 31 | pc13Image = Resource.getPic("PC_13"); 32 | Audio.playMusic("NAZI_NOR"); 33 | } 34 | 35 | @Override 36 | public void onExit() { 37 | } 38 | 39 | @Override 40 | public void fixedUpdate() { 41 | if (Input.isKeyJustPressed(KEY_START_1) 42 | || Input.isKeyJustPressed(KEY_START_2)) { 43 | 44 | SceneManager.switchTo("title", false); 45 | } 46 | } 47 | 48 | @Override 49 | public void draw(Graphics2D g) { 50 | g.setColor(backgroundColor); 51 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 52 | g.drawImage(pc13Image, 216, 110, null); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/wolf3d/scene/Quit.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import wolf3d.infra.Scene; 4 | 5 | /** 6 | * Quit class. 7 | * 8 | * @author Leonardo Ono (ono.leo80@gmail.com) 9 | */ 10 | public class Quit extends Scene { 11 | 12 | public Quit() { 13 | super("quit"); 14 | } 15 | 16 | @Override 17 | public void onEnter() { 18 | System.exit(0); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/wolf3d/scene/Stage.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics2D; 5 | import java.awt.Rectangle; 6 | import java.awt.Shape; 7 | import java.awt.image.BufferedImage; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Comparator; 11 | import java.util.List; 12 | import java.util.Set; 13 | import wolf3d.asset.loader.VGAGRAPHLoader.VGAGRAPHFont; 14 | import wolf3d.infra.Audio; 15 | import wolf3d.infra.Doors; 16 | import wolf3d.infra.Enemies; 17 | import wolf3d.infra.FizzleFade; 18 | import wolf3d.infra.GameMap; 19 | import wolf3d.infra.GameMap.RaycastResult; 20 | import static wolf3d.infra.GameMap.performRaycastDDA; 21 | import wolf3d.infra.HUD; 22 | import wolf3d.infra.Objs; 23 | import wolf3d.infra.Objs.EndPlayerObj; 24 | import wolf3d.infra.Objs.EndPlayerObj.EndPlayerState; 25 | import wolf3d.infra.Objs.EnemyObj; 26 | import static wolf3d.infra.Objs.EnemyObj.EnemyState.DEAD; 27 | import wolf3d.infra.Objs.Obj; 28 | import static wolf3d.infra.Objs.ObjType.END_PLAYER; 29 | import static wolf3d.infra.Objs.ObjType.ENEMY; 30 | import static wolf3d.infra.Objs.SPRITE_SIZE; 31 | import wolf3d.infra.Player; 32 | import static wolf3d.infra.Player.PLAYER_RADIUS; 33 | import wolf3d.infra.Player.PlayerState; 34 | import static wolf3d.infra.Player.PlayerState.PLAYING; 35 | import wolf3d.infra.Resource; 36 | import wolf3d.infra.Scene; 37 | import wolf3d.infra.SecretDoors; 38 | import static wolf3d.infra.Settings.*; 39 | import wolf3d.infra.Util; 40 | import wolf3d.infra.Weapons; 41 | import wolf3d.infra.Wolf3DGame; 42 | 43 | /** 44 | * Stage class. 45 | * 46 | * @author Leonardo Ono (ono.leo80@gmail.com) 47 | */ 48 | public class Stage extends Scene { 49 | 50 | // https://www.doomworld.com/forum/topic -> 51 | // -> /118059-what-was-the-fov-in-the-original-doom/ 52 | private final double fov = Math.toRadians(72); 53 | 54 | private final double projPlaneDistance; 55 | private final int projectionWidth = CANVAS_WIDTH; 56 | private final double[] projectionAngles = new double[projectionWidth]; 57 | 58 | private final double[] wallDepth = new double[CANVAS_WIDTH]; 59 | 60 | private int offsetX = 0; 61 | private int offsety = CANVAS_HEIGHT / 2 - 20; 62 | 63 | private static final int MAX_RAY_SIZE = 1000; 64 | 65 | private final RaycastResult raycastResult = new RaycastResult(); 66 | 67 | private Color floorColor; 68 | private Color ceilingColor; 69 | private final VGAGRAPHFont fontYellow; 70 | private final VGAGRAPHFont fontBlack; 71 | 72 | private EndPlayerObj endPlayerObj; 73 | 74 | public Stage() { 75 | super("stage"); 76 | 77 | projPlaneDistance = (projectionWidth * 0.5) / Math.tan(fov * 0.5); 78 | for (int sx = 0; sx < projectionWidth; sx++) { 79 | double op = sx - projectionWidth / 2; 80 | projectionAngles[sx] = Math.atan(op / projPlaneDistance); 81 | } 82 | 83 | // workaround: fix missing vertical wall column at center of screen 84 | // when this forms exactly 45 degrees angle. 85 | projectionAngles[160] += 0.000001; 86 | 87 | fontYellow = Resource.getFont("SMALL_YELLOW"); 88 | fontBlack = Resource.getFont("SMALL_BLACK"); 89 | } 90 | 91 | private void reset() { 92 | Wolf3DGame.startNextLevel(); 93 | floorColor = Resource.getFloorColor(); 94 | ceilingColor = Resource.getCeilingColorByFloorNumber( 95 | Wolf3DGame.getFloor()); 96 | 97 | // debug: start player at specific location 98 | if (Wolf3DGame.playerOverrideLocation != null) { 99 | Player.reset(Wolf3DGame.playerOverrideLocation.x + 0.5 100 | , Wolf3DGame.playerOverrideLocation.y + 0.5 101 | , Player.getPlayerAngle()); 102 | } 103 | 104 | endPlayerObj = new EndPlayerObj(); 105 | } 106 | 107 | @Override 108 | public void onEnter() { 109 | if (Wolf3DGame.isBackToGame()) { 110 | Wolf3DGame.setBackToGame(false); 111 | } 112 | // new game or next level 113 | else { 114 | reset(); 115 | } 116 | Audio.playMusicByFloorNumber(Wolf3DGame.getFloor()); 117 | 118 | // endPlayerObj.walkTo(34, 4); 119 | } 120 | 121 | @Override 122 | public void onExit() { 123 | } 124 | 125 | @Override 126 | public void update(double delta) { 127 | Player.update(delta); 128 | updateFlashScreenEffect(delta); 129 | } 130 | 131 | @Override 132 | public void fixedUpdate() { 133 | Player.fixedUpdate(); 134 | if (Player.getPlayerState() == PLAYING) { 135 | Enemies.fixedUpdateEnemies(); 136 | Doors.fixedUpdateDoors(); 137 | SecretDoors.fixedUpdateSecretDoors(); 138 | Weapons.fixedUpdate(); 139 | HUD.fixedUpdate(); 140 | } 141 | FizzleFade.fixedUpdate(); 142 | endPlayerObj.fixedUpdate(); 143 | activateEndPlayer(); 144 | 145 | // game was cleared and all cutscene animations was finished 146 | if (endPlayerObj.isEnd()) { 147 | Wolf3DGame.gameCleared(); 148 | } 149 | } 150 | 151 | private void activateEndPlayer() { 152 | if (Player.isPlayerTriggeredEndGame() 153 | && endPlayerObj.getState() == EndPlayerState.NONE 154 | && Player.getPlayerState() 155 | == PlayerState.GAME_CLEARED_ROTATE_TO_END_PLAYER) { 156 | 157 | endPlayerObj.setEndPlayerLocation(34, 8); 158 | endPlayerObj.walkTo(34, 4); 159 | } 160 | } 161 | 162 | @Override 163 | public void draw(Graphics2D g) { 164 | drawFloorAndCeiling(g); 165 | drawWalls(g); 166 | drawObjs(g); 167 | if (Player.getPlayerState() == PLAYING) { 168 | Weapons.draw(g); 169 | } 170 | FizzleFade.draw(g); 171 | HUD.draw(g); 172 | drawRequiredKeyUserMsg(g); 173 | drawFlashScreenEffect(g); 174 | } 175 | 176 | private void drawRequiredKeyUserMsg(Graphics2D g) { 177 | if (Player.isShowUserMsgGoldKeyRequired()) { 178 | String userMsg = "GOLD KEY IS REQUIRED TO OPEN THIS DOOR"; 179 | fontBlack.drawString(g, userMsg, 37, 75); 180 | fontYellow.drawString(g, userMsg, 36, 74); 181 | } 182 | else if (Player.isShowUserMsgSilverKeyRequired()) { 183 | String userMsg = "SILVER KEY IS REQUIRED TO OPEN THIS DOOR"; 184 | fontBlack.drawString(g, userMsg, 43, 75); 185 | fontYellow.drawString(g, userMsg, 42, 74); 186 | } 187 | } 188 | 189 | private void drawFloorAndCeiling(Graphics2D g) { 190 | g.setColor(ceilingColor); 191 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 192 | g.setColor(floorColor); 193 | g.fillRect(0, CANVAS_HEIGHT / 2 - 20, CANVAS_WIDTH, CANVAS_HEIGHT / 2); 194 | } 195 | 196 | private void drawWalls(Graphics2D g) { 197 | GameMap.getObjsDuringRaycast().clear(); 198 | 199 | // perform raycast and draw the columns of the walls 200 | for (int r = 0; r < projectionAngles.length; r++) { 201 | double px = Player.getPlayerX(); 202 | double py = Player.getPlayerY(); 203 | double pa = Player.getPlayerAngle() + projectionAngles[r]; 204 | performRaycastDDA(px, py, pa, raycastResult, MAX_RAY_SIZE); 205 | 206 | if (raycastResult.isIntersecting()) { 207 | wallDepth[r] = raycastResult.getDistance(); 208 | 209 | int wallHeight = (int) (projPlaneDistance * 0.5 210 | / (raycastResult.getDistance() 211 | * Math.cos(projectionAngles[r]))); 212 | 213 | double textureUnit = 0.0; 214 | double ripx = raycastResult.getIntersectionPoint().x; 215 | double ripy = raycastResult.getIntersectionPoint().y; 216 | double to = raycastResult.getTextureOffset(); 217 | switch (raycastResult.getWallSide()) { 218 | case 0 -> textureUnit = ripx - (int) ripx + to; 219 | case 1 -> textureUnit = ripy - (int) ripy + to; 220 | } 221 | 222 | int textureRow = (int) (SPRITE_SIZE * textureUnit); 223 | 224 | int dx1 = offsetX + r; 225 | int dy1 = offsety - wallHeight; 226 | int dx2 = offsetX + r + 1; 227 | int dy2 = offsety + wallHeight; 228 | int sx1 = textureRow; 229 | int sy1 = 0; 230 | int sx2 = textureRow + 1; 231 | int sy2 = 64; 232 | 233 | BufferedImage texture = raycastResult.getTile() 234 | .getTexture(raycastResult.getWallSide()); 235 | 236 | g.drawImage( 237 | texture, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null); 238 | } 239 | } 240 | } 241 | 242 | private final Rectangle sight = new Rectangle( 243 | offsetX + CANVAS_WIDTH / 2 - 16, 0, 32, CANVAS_HEIGHT); 244 | 245 | private final Rectangle targetEnemyRegionTmp = new Rectangle(); 246 | private final Rectangle targetEnemy = new Rectangle(); 247 | private List orderedObjs = new ArrayList<>(); 248 | 249 | private void drawObjs(Graphics2D g) { 250 | Set objs = GameMap.getObjsDuringRaycast(); 251 | 252 | objs.add(endPlayerObj); 253 | 254 | orderedObjs.clear(); 255 | 256 | double dirHorX = Math.cos(Player.getPlayerAngle()); 257 | double dirHorY = Math.sin(Player.getPlayerAngle()); 258 | double dirVerX = -dirHorY; 259 | double dirVerY = dirHorX; 260 | 261 | Wolf3DGame.setClosestEnemyInSight(null); 262 | targetEnemy.setBounds(0, 0, 0, 0); 263 | 264 | objs.addAll(Enemies.getEnemies()); 265 | 266 | for (Objs.Obj obj : objs) { 267 | if (!obj.isDrawable()) continue; 268 | 269 | double objX = obj.getCol() + 0.5; 270 | double objY = obj.getRow() + 0.5; 271 | 272 | if (obj.getType() == ENEMY) { 273 | EnemyObj enemy = (EnemyObj) obj; 274 | objX = enemy.getEnemyX(); 275 | objY = enemy.getEnemyY(); 276 | } 277 | else if (obj.getType() == END_PLAYER) { 278 | EndPlayerObj epObj = (EndPlayerObj) obj; 279 | objX = epObj.getEndPlayerX(); 280 | objY = epObj.getEndPlayerY(); 281 | } 282 | 283 | double distX = objX - Player.getPlayerX(); 284 | double distY = objY - Player.getPlayerY(); 285 | 286 | double distHor = dirHorX * distX + dirHorY * distY; 287 | double distVer = dirVerX * distX + dirVerY * distY; 288 | 289 | if (distHor > PLAYER_RADIUS) { 290 | int sizeHor = (int) (projPlaneDistance * 1 / distHor); 291 | int sizeVer = (int) (projPlaneDistance * distVer / distHor); 292 | 293 | obj.setSizeHor(sizeHor); 294 | obj.setSizeVer(sizeVer); 295 | obj.setDistanceFromPlayer(distHor); 296 | orderedObjs.add(obj); 297 | } 298 | } 299 | 300 | Collections.sort(orderedObjs, objComparator); 301 | for (Objs.Obj obj2 : orderedObjs) { 302 | int sizeHor = obj2.getSizeHor(); 303 | int sizeVer = obj2.getSizeVer(); 304 | if (sizeHor <= 0) return; 305 | 306 | // calculate the clipping region of sprite 307 | boolean clip = true; 308 | int startClip = -1; 309 | int endClip = 0; 310 | //double tol = 0.1; // tolerance to avoid visibility problems 311 | for (int x = 0; x < sizeHor; x++) { 312 | int scrX = 160 - sizeHor / 2 + sizeVer + x; 313 | 314 | boolean draw = scrX >= 0 && scrX < CANVAS_WIDTH 315 | && wallDepth[scrX] > obj2.getDistanceFromPlayer() 316 | / Math.cos(projectionAngles[scrX]); // + tol; 317 | 318 | if (clip && draw) { 319 | startClip = scrX; 320 | clip = false; 321 | } 322 | else if (!clip && (!draw || x == sizeHor - 1)) { 323 | endClip = scrX; 324 | break; 325 | } 326 | } 327 | 328 | // sprite completely occluded 329 | if (startClip < 0) { 330 | continue; 331 | } 332 | 333 | // draw sprite 334 | int dx1 = offsetX + 160 - sizeHor / 2 + sizeVer; 335 | int dy1 = offsety - sizeHor / 2; 336 | int dx2 = dx1 + sizeHor; 337 | int dy2 = dy1 + sizeHor; 338 | int sx1 = 0; 339 | int sy1 = 0; 340 | int sx2 = 64; 341 | int sy2 = 64; 342 | Shape oc = g.getClip(); 343 | g.setClip( 344 | startClip, 0, endClip - startClip, CANVAS_HEIGHT - 40); 345 | 346 | g.drawImage(obj2.getSprite() 347 | , dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null); 348 | 349 | g.setClip(oc); 350 | 351 | // reuse this drawing routine 352 | // to choose the closest enemy in sight 353 | int terX = offsetX + 160 - sizeHor / 4 + sizeVer; 354 | int terY = dy1; 355 | int terW = sizeHor / 2; 356 | int terH = dy2 - dy1; 357 | targetEnemyRegionTmp.setBounds(terX, terY, terW, terH); 358 | EnemyObj closestEnemy = Wolf3DGame.getClosestEnemyInSight(); 359 | if (obj2.getType() == ENEMY) { 360 | Objs.EnemyObj enemyObj = (Objs.EnemyObj) obj2; 361 | if (enemyObj.getEnemyState() != DEAD && 362 | ((closestEnemy == null 363 | || enemyObj.getDistanceFromPlayer() 364 | < closestEnemy.getDistanceFromPlayer()) 365 | && sight.intersects(dx1, 0, dx2 - dx1, 200) 366 | && targetEnemyRegionTmp.intersects( 367 | dx1, 0, dx2 - dx1, 200))) { 368 | 369 | Wolf3DGame.setClosestEnemyInSight(enemyObj); 370 | targetEnemy.setBounds(targetEnemyRegionTmp); 371 | } 372 | } 373 | } 374 | } 375 | 376 | private final Comparator objComparator = (Obj o1, Obj o2) -> { 377 | // for sprites that have shadow, give priority to draw first 378 | // so that enemies are always drawn on top of it. 379 | double obj1Dist = o1.getDistanceFromPlayer(); 380 | if (o1.isDrawFirst()) obj1Dist += 1000; 381 | double obj2Dist = o2.getDistanceFromPlayer(); 382 | if (o2.isDrawFirst()) obj2Dist += 1000; 383 | return (int) Math.signum(obj2Dist - obj1Dist); 384 | }; 385 | 386 | // --- flash screen effect --- 387 | 388 | private static double flashScreenRed; 389 | private static double flashScreenGreen; 390 | private static double flashScreenBlue; 391 | private static double flashScreenAlpha = 0.0; 392 | 393 | // r, g, b = 0.0~1.0 394 | public static void flashScreen(double r, double g, double b, double alpha) { 395 | flashScreenRed = r; 396 | flashScreenGreen = g; 397 | flashScreenBlue = b; 398 | flashScreenAlpha = alpha; 399 | } 400 | 401 | private static void updateFlashScreenEffect(double delta) { 402 | flashScreenAlpha -= 1.25 * delta; 403 | if (flashScreenAlpha < 0.0) flashScreenAlpha = 0.0; 404 | } 405 | 406 | // draw the flash effect (hit and collect indicators) 407 | public static void drawFlashScreenEffect(Graphics2D g) { 408 | if (flashScreenAlpha > 0.0) { 409 | int red = (int) (255 * flashScreenRed); 410 | int green = (int) (255 * flashScreenGreen); 411 | int blue = (int) (255 * flashScreenBlue); 412 | int alpha = (int) (255 * flashScreenAlpha); 413 | Color color = Util.getColor(red, green, blue, alpha); 414 | g.setColor(color); 415 | g.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 416 | } 417 | } 418 | 419 | } 420 | -------------------------------------------------------------------------------- /src/wolf3d/scene/Title.java: -------------------------------------------------------------------------------- 1 | package wolf3d.scene; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | import wolf3d.infra.Audio; 6 | import wolf3d.infra.Input; 7 | import wolf3d.infra.Resource; 8 | import wolf3d.infra.Scene; 9 | import wolf3d.infra.SceneManager; 10 | import static wolf3d.infra.Settings.*; 11 | import wolf3d.infra.Util; 12 | 13 | /** 14 | * Title class. 15 | * 16 | * @author Leonardo Ono (ono.leo80@gmail.com) 17 | */ 18 | public class Title extends Scene { 19 | 20 | private BufferedImage titleImage; 21 | private long nextSceneTime; 22 | 23 | public Title() { 24 | super("title"); 25 | } 26 | 27 | private void reset() { 28 | } 29 | 30 | @Override 31 | public void onEnter() { 32 | reset(); 33 | titleImage = Resource.getPic("TITLE"); 34 | if (!"NAZI_NOR".equals(Audio.getCurrentMusicId())) { 35 | Audio.playMusic("NAZI_NOR"); 36 | } 37 | } 38 | 39 | @Override 40 | public void onTransitionFinished() { 41 | nextSceneTime = Util.getTimeMs() + 15000; 42 | } 43 | 44 | @Override 45 | public void onExit() { 46 | reset(); 47 | System.gc(); 48 | } 49 | 50 | @Override 51 | public void fixedUpdate() { 52 | if (Input.isKeyJustPressed(KEY_START_1) 53 | || Input.isKeyJustPressed(KEY_START_2)) { 54 | 55 | SceneManager.switchTo("game_options"); 56 | } 57 | else if (Util.getTimeMs() >= nextSceneTime) { 58 | SceneManager.switchTo("credits", false); 59 | } 60 | } 61 | 62 | @Override 63 | public void draw(Graphics2D g) { 64 | g.drawImage(titleImage, 0, 0, null); 65 | } 66 | 67 | } 68 | --------------------------------------------------------------------------------