The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package SDL::Tutorial::3DWorld::Actor::GridPlane;

# This implements a static psuedo-3d horizontal plane of cubes (actually
# just the sides of the cube).
#
# When bounded by two flat planes at the top and bottom, the GridPlane
# object should allow you to implement a cube-based grid map as used in
# old games like Wolfenstein 3D.
#
# In the initial implementation this map is completely static, without
# any moving walls or none-perfect cube elements such as doors.
#
# When rendering, we rely on the power of modern graphics cards to brute
# force render the entire map instead of relying on optimisation.

use 5.008;
use strict;
use warnings;
use SDL::Tutorial::3DWorld::OpenGL  ();
use SDL::Tutorial::3DWorld::Actor   ();
use SDL::Tutorial::3DWorld::Bound   ();
use SDL::Tutorial::3DWorld::Texture ();

our $VERSION = '0.33';
our @ISA     = 'SDL::Tutorial::3DWorld::Actor';





######################################################################
# Constructor and Accessors

sub new {
	my $self = shift->SUPER::new(@_);

	# Do we have a texture set 
	unless ( $self->{size} ) {
		die "Did not provide a map size";
	}
	unless ( $self->{wall} ) {
		die "Did not provide a wall texture set";
	}
	unless ( $self->{map} ) {
		die "Did not provide a the map";
	}

	return $self;
}





######################################################################
# Engine Methods

sub init {
	my $self  = shift;
	my $wall  = $self->{wall};
	my $scale = $self->{scale} || [ 1, 1, 1 ];

	# Set up the bounding box
	$self->{bound} = SDL::Tutorial::3DWorld::Bound->box(
		0, 0, 0,
		$scale->[0] * $self->{size},
		$scale->[1] * 1,
		$scale->[2] * $self->{size},
	);

	# Initialise the textures
	foreach my $i ( 0 .. $#$wall ) {
		# Turn off mag filtering for that retro pixel look
		$wall->[$i] = SDL::Tutorial::3DWorld::Texture->new(
			file       => $wall->[$i],
			tile       => 0,
			mag_filter => OpenGL::GL_NEAREST,
		);
		$wall->[$i]->init;
	}

	# Make sure the array position lines up with the map id.
	# That is, the first wall texture is at position 1.
	unshift @$wall, undef;

	# Compile the map into one big display list
	$self->{list} = OpenGL::List::glpList {
		OpenGL::glTranslatef( @{$self->{position}} );
		OpenGL::glScalef(  @{$self->{scale}}  ) if $self->{scale};
		OpenGL::glRotatef( @{$self->{orient}} ) if $self->{orient};
		$self->compile;
	};

	return 1;
}

# Generate the drawing instructions
sub compile {
	my $self  = shift;
	my $size  = $self->{size};
	my $n     = $size - 1;
	my $wall  = $self->{wall};
	my $debug = $self->{debug};

	# Generate a two-dimensional array from the map string
	my @map = map {
		[ split //, $_ ]
	} split /\n/, $self->{map};

	# To be sufficiently old school, we don't do shadows
	OpenGL::glDisable( OpenGL::GL_LIGHTING );

	# Draw the floor of the map
	OpenGL::glDisable( OpenGL::GL_TEXTURE_2D );
	OpenGL::glColor3f( @{$self->{floor}} );
	OpenGL::glBegin( OpenGL::GL_QUADS );
	OpenGL::glVertex3f( 0,     0.001, 0     );
	OpenGL::glVertex3f( 0,     0.001, $size );
	OpenGL::glVertex3f( $size, 0.001, $size );
	OpenGL::glVertex3f( $size, 0.001, 0     );
	OpenGL::glEnd();

	# Draw the roof of the map
	OpenGL::glColor3f( @{$self->{ceiling}} );
	OpenGL::glBegin( OpenGL::GL_QUADS );
	OpenGL::glVertex3f( 0,     1, 0     );
	OpenGL::glVertex3f( $size, 1, 0     );
	OpenGL::glVertex3f( $size, 1, $size );
	OpenGL::glVertex3f( 0,     1, $size );
	OpenGL::glEnd();

	# Prepare to show the textured walls
	OpenGL::glEnable( OpenGL::GL_TEXTURE_2D );
	OpenGL::glColor3f( 1, 1, 1 );

	# Track the active wall texture
	my $active = 0;

	# Iterate through the map
	foreach my $x ( 0 .. $n ) {
		my $X = $x + 1;
		foreach my $z ( 0 .. $n ) {
			my $Z = $z + 1;

			# Ignore open space
			my $w = $map[$x]->[$z] or next;

			# Switch textures if needed
			if ( $w != $active ) {
				OpenGL::glEnd() if $active;
				$wall->[$w]->display;
				$active = $w;
				OpenGL::glBegin( OpenGL::GL_QUADS );
			}

			# Draw the north face unless hidden
			if ( $debug or not $z or not $map[$x]->[$z-1] ) {
				# Top Left
				OpenGL::glTexCoord2f( 0, 0 );
				OpenGL::glVertex3f( $X, 1, $z );

				# Bottom Left
				OpenGL::glTexCoord2f( 0, 1 );
				OpenGL::glVertex3f( $X, 0, $z ); 

				# Bottom Right
				OpenGL::glTexCoord2f( 1, 1 );
				OpenGL::glVertex3f( $x, 0, $z );

				# Top Right
				OpenGL::glTexCoord2f( 1, 0 );
				OpenGL::glVertex3f( $x, 1, $z );
			}

			# Draw the east face unless hidden
			if ( $debug or not $map[$x+1]->[$z] ) {
				# Top Left
				OpenGL::glTexCoord2f( 0, 0 );
				OpenGL::glVertex3f( $X, 1, $Z );

				# Bottom Left
				OpenGL::glTexCoord2f( 0, 1 );
				OpenGL::glVertex3f( $X, 0, $Z ); 

				# Bottom Right
				OpenGL::glTexCoord2f( 1, 1 );
				OpenGL::glVertex3f( $X, 0, $z );

				# Top Right
				OpenGL::glTexCoord2f( 1, 0 );
				OpenGL::glVertex3f( $X, 1, $z );
			}

			# Draw the south face unless hidden
			if ( $debug or not $map[$x]->[$z+1] ) {
				# Top Left
				OpenGL::glTexCoord2f( 0, 0 );
				OpenGL::glVertex3f( $x, 1, $Z );

				# Bottom Left
				OpenGL::glTexCoord2f( 0, 1 );
				OpenGL::glVertex3f( $x, 0, $Z ); 

				# Bottom Right
				OpenGL::glTexCoord2f( 1, 1 );
				OpenGL::glVertex3f( $X, 0, $Z );

				# Top Right
				OpenGL::glTexCoord2f( 1, 0 );
				OpenGL::glVertex3f( $X, 1, $Z );
			}

			# Draw the west face unless hidden
			if ( $debug or not $x or not $map[$x-1]->[$z] ) {
				# Top Left
				OpenGL::glTexCoord2f( 0, 0 );
				OpenGL::glVertex3f( $x, 1, $z );

				# Bottom Left
				OpenGL::glTexCoord2f( 0, 1 );
				OpenGL::glVertex3f( $x, 0, $z ); 

				# Bottom Right
				OpenGL::glTexCoord2f( 1, 1 );
				OpenGL::glVertex3f( $x, 0, $Z );

				# Top Right
				OpenGL::glTexCoord2f( 1, 0 );
				OpenGL::glVertex3f( $x, 1, $Z );
			}
		}
	}

	# Reenable lighting
	OpenGL::glEnd();
	OpenGL::glEnable( OpenGL::GL_LIGHTING );

	return 1;
}

# This should never need to be called
sub display {
	OpenGL::glCallList( $_[0]->{list} );
}

1;