Skip to content

Instantly share code, notes, and snippets.

@mcgivrer
Last active July 25, 2025 13:34
Show Gist options
  • Save mcgivrer/0b9a38f6a29bb41a80fb62e89577bbe6 to your computer and use it in GitHub Desktop.
Save mcgivrer/0b9a38f6a29bb41a80fb62e89577bbe6 to your computer and use it in GitHub Desktop.
NJP2 enhanced with frame template a Java Project creator cli command
package ${PACKAGE_NAME};
import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* ${MAIN_CLASS_NAME} serves as a basic demonstration of how to build a graphical application
* using Java AWT and manage a custom rendering loop with double buffering.
* This application displays the elapsed time in a `Frame` window with support for
* basic keyboard interaction to terminate the program.
* <p>
* The class also provides mechanisms to manage window events, custom rendering to a
* buffered image, and drawing text with anti-aliasing enabled.
* <p>
* Implements the `KeyListener` interface to handle keyboard events for user interaction.
*
* @author ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>
* @version ${APP_VERSION}
*/
public class ${MAIN_CLASS_NAME} implements KeyListener {
/**
* Represents a generic behavior that can be applied to an entity in the application.
* This interface provides a mechanism for defining and updating the state and appearance
* of an entity during its lifecycle, such as handling interactions, animations,
* or custom behaviors. Both the update and draw methods can be overridden to define
* specific behavior logic.
*
* @param <Entity> the type of entity to which this behavior is applied
*/
public interface Behavior<Entity> {
default void init(${MAIN_CLASS_NAME} app, Entity e) {
}
default void update(${MAIN_CLASS_NAME} app, long elapsed, Entity e) {
}
default void draw(${MAIN_CLASS_NAME} app, Graphics2D g, Entity e) {
}
default void dispose(${MAIN_CLASS_NAME} app, Entity e) {
}
}
/**
* Represents types of physical behavior that can be assigned to entities
* within the application. This enumeration defines how an entity interacts
* with the simulated physics system.
* <p>
* Types:
* - DYNAMIC: Indicates that the entity is affected by forces and can move or
* be influenced by the physics system. Entities marked as DYNAMIC
* participate in collisions and respond to forces like gravity and impulses.
* <p>
* - STATIC: Specifies that the entity does not move and acts as an immovable
* object within the simulation. STATIC entities can be used as barriers,
* platforms, or environmental objects which other entities may interact with
* but that do not change their position.
* <p>
* - NONE: Denotes that the entity has no physical properties, meaning it is not
* affected by forces, collisions, or other physics-based interactions. This
* type can be used for decorative or logical entities that do not require
* physical simulation.
*/
public enum PhysicType {
DYNAMIC, STATIC, NONE;
}
/**
* Represents a movable and drawable entity in a graphical application.
* This class extends Rectangle2D.Double, providing functionality
* to define the entity's position, size, rotation, colors, and image.
* It also includes methods for updating the entity's state and rendering it.
*/
public static class Entity extends Rectangle2D.Double {
private static long index = 0;
private long id = index++;
public String name = "entity_%d".formatted(id);
public List<Behavior> behaviors = new ArrayList<>();
public double dx, dy;
public double r, dr;
public boolean active = true;
public PhysicType physicType = PhysicType.DYNAMIC;
public double mass = 1.0; // Mass of the entity (in kg)
public boolean jump;
public boolean onGround;
public Color color = Color.BLACK, fillColor = Color.RED;
public BufferedImage image;
private Material material = Material.DEFAULT;
private Entity parent;
private List<Entity> children = new ArrayList<>();
private int contact = 0;
private List<Point2D> forces = new ArrayList<>();
public Map<String, Object> properties = new ConcurrentHashMap<>();
public Entity(double x, double y, double width, double height) {
super(x, y, width, height);
}
public Entity(String name) {
this(0, 0, 0, 0);
this.name = name;
}
// ... autres méthodes existantes ...
public Entity setMass(double mass) {
this.mass = Math.max(0.1, mass); // Minimum mass to avoid division by zero
return this;
}
public double getMass() {
return mass;
}
public Entity add(Entity child) {
child.parent = this;
children.add(child);
return this;
}
public Entity apply(Point2D f) {
forces.add(f);
return this;
}
public Entity add(Behavior<Entity> b) {
behaviors.add(b);
return this;
}
/**
* Gets the WaveBehavior if present on this entity.
*/
public Behavior findBehavior(Class<?> fb) {
return behaviors.stream()
.filter(b -> b.getClass().equals(fb))
.map(b -> b)
.findFirst()
.orElse(null);
}
public Entity setPosition(double x, double y) {
this.x = x;
this.y = y;
return this;
}
public Entity setSize(double width, double height) {
this.width = width;
this.height = height;
return this;
}
public Entity setColor(Color color) {
this.color = color;
return this;
}
public Entity setFillColor(Color color) {
this.fillColor = color;
return this;
}
public Entity setActive(boolean active) {
this.active = active;
return this;
}
public Entity setMaterial(Material material) {
this.material = material;
return this;
}
public Entity setPhysicType(PhysicType physicType) {
this.physicType = physicType;
return this;
}
public Entity setProperty(String name, Object value) {
properties.put(name, value);
return this;
}
public Object getProperty(String name) {
return properties.get(name);
}
/**
* Draws the current entity onto the provided graphics context.
* This method renders the entity with its current position, rotation, colors, and image.
* If an image is associated with the entity, it will be drawn at the specified position.
* Otherwise, the entity will be rendered as a filled and outlined shape.
*
* @param g the graphics context on which the entity will be drawn
*/
public void draw(Graphics2D g) {
g.rotate(r, x, y);
if (image != null) {
g.drawImage(image, (int) x, (int) y, null);
} else {
if (this.fillColor != null) {
g.setColor(this.fillColor);
g.fill(this);
}
if (this.color != null) {
g.setColor(this.color);
g.draw(this);
}
}
}
public String[] getDebugInfo() {
return new String[]{
"id: " + id,
"name: " + name,
"gnd:" + onGround,
"cnt:" + contact,
"jmp:" + jump,
"s: %3.2fx%3.2f".formatted(width, height),
"p: %3.2f,%3.2f".formatted(x, y),
"r: %3.2f".formatted(r),
"v: %3.2f,%3.2f".formatted(dx, dy),
"dr: %3.2f".formatted(r),
};
}
public void drawDebug(Graphics2D g) {
g.setColor(Color.ORANGE);
g.setFont(g.getFont().deriveFont(Font.BOLD, 8.5f));
if (${MAIN_CLASS_NAME}.debug > 0) {
int ix = 0;
for (String s : getDebugInfo()) {
if (${MAIN_CLASS_NAME}.debug > ix) {
g.drawString(s,
(int) (x + width + 4),
(int) (y + height - (g.getFontMetrics().getHeight() * ix++)));
}
}
g.setColor(Color.YELLOW);
// draw the resulting velocity
g.drawLine(
(int) (x + width / 2), (int) (y + height / 2),
(int) ((x + width / 2) + dx * 0.5), (int) ((y + height / 2) + dy * 0.5));
g.setColor(Color.GREEN);
// draw all applied forces
getForces().forEach(f -> {
g.drawLine(
(int) (x + width / 2), (int) (y + height / 2),
(int) ((x + width / 2) + f.getX() * 0.5), (int) ((y + height / 2) + f.getY() * 0.5));
});
}
}
public void update(long elapsed) {
}
public boolean isActive() {
return active;
}
public Material getMaterial() {
return material;
}
public int getContact() {
return contact;
}
public void addContact(int i) {
contact += i;
}
public void resetContact() {
contact = 0;
}
public void resetForces() {
forces.clear();
}
public PhysicType getPhysicType() {
return physicType;
}
public Collection<Point2D> getForces() {
return forces;
}
public void removeProperty(String name) {
properties.remove(name);
}
}
/**
* Represents a physical world or environment within which entities exist and interact.
* This class extends the {@link Entity} class, inheriting its properties while introducing
* specific functionality such as defining gravity in the environment.
*/
public static class World extends Entity {
private Point2D gravity = new Point2D.Double(0, 0.981);
public World(double x, double y, double width, double height) {
super(x, y, width, height);
}
public World(String name) {
super(name);
setMaterial(Material.DEFAULT);
}
public World setGravity(Point2D gravity) {
this.gravity = gravity;
return this;
}
@Override
public void draw(Graphics2D g) {
g.setColor(fillColor);
for (double ix = x; ix < x + width; ix += 32) {
for (double iy = y; iy < y + height; iy += 32) {
g.setColor(fillColor.darker());
g.fill(new Ellipse2D.Double(ix + 4, iy + 4, 24, 24));
g.setColor(fillColor);
g.draw(new Ellipse2D.Double(ix, iy, 32, 32));
}
}
g.setColor(color);
g.draw(this);
}
public Point2D getGravity() {
return gravity;
}
}
/**
* Represents a camera entity that can follow a target entity with a smooth transition.
* The camera adjusts its position based on the target's position, ensuring a smooth
* movement by applying a tweening factor.
*/
public static class Camera extends Entity {
private Entity target;
private double tweenFactor = 0.005;
public Camera(String name) {
super(name);
}
public Camera setTarget(Entity target) {
this.target = target;
return this;
}
public Camera setTweenFactor(double tweenFactor) {
this.tweenFactor = tweenFactor;
return this;
}
public void update(long elapsed) {
if (target != null) {
this.x += (target.x - this.x - (width + target.width) / 2) * tweenFactor * elapsed;
this.y += (target.y - this.y - (height + target.height) / 2) * tweenFactor * elapsed;
}
}
}
/**
* Represents a material with physical properties such as friction and elasticity.
* This class is used to define how entities interact with each other and the world.
*/
public static class Material {
public String name;
public double friction;
public double elasticity;
public boolean isFluid = false;
public double density = 1.0; // Density in kg/m³ (or equivalent units)
public double viscosity = 1.0; // Viscosity factor (1.0 = no viscosity, lower = more viscous)
public static final Material DEFAULT = new Material("default", 0.98, 0.85);
public static final Material ICE = new Material("ice", 0.02, 0.95);
public static final Material WOOD = new Material("wood", 0.75, 0.45);
public static final Material STONE = new Material("stone", 0.85, 0.15);
public static final Material AIR = new Material("air", 0.999, 0.01);
public static final Material WATER = new Material("water", 0.95, 0.3, true, 1000.0, 0.1);
public Material(String name, double friction, double elasticity) {
this.name = name;
this.friction = friction;
this.elasticity = elasticity;
}
public Material(String name, double friction, double elasticity, boolean isFluid, double density, double viscosity) {
this.name = name;
this.friction = friction;
this.elasticity = elasticity;
this.isFluid = isFluid;
this.density = density;
this.viscosity = viscosity;
}
public boolean isFluid() {
return isFluid;
}
public double getDensity() {
return density;
}
public double getViscosity() {
return viscosity;
}
@Override
public String toString() {
return "Material{name='%s', friction=%.2f, elasticity=%.2f, isFluid=%s, density=%.1f, viscosity=%.2f}"
.formatted(name, friction, elasticity, isFluid, density, viscosity);
}
public double getFriction() {
return friction;
}
}
/**
* Wave behavior for fluid entities.
* This behavior manages wave generation, propagation, and surface deformation.
*/
public static class WaveBehavior implements Behavior<Entity> {
private double[][] heightMap;
private double[][] velocityMap;
private double[][] prevHeightMap; // Pour améliorer la stabilité
private int width, height;
private double cellSize;
private double time = 0;
// Wave parameters - ajustés pour de meilleurs résultats
private double waveAmplitude = 3.0;
private double waveFrequency = 0.8;
private double waveSpeed = 100.0;
private double damping = 0.998; // Moins d'amortissement
private double tension = 0.25; // Plus de tension pour la restauration
private double restoreForce = 0.02; // Force de restauration vers la surface
// Surface points for rendering
private List<Point2D> surfacePoints;
private double baseWaterLevel; // Niveau de référence de l'eau
public WaveBehavior(double cellSize) {
this.cellSize = cellSize;
this.surfacePoints = new ArrayList<>();
}
@Override
public void init(${MAIN_CLASS_NAME} app, Entity entity) {
if (!entity.getMaterial().isFluid()) {
throw new IllegalArgumentException("WaveBehavior can only be applied to fluid entities");
}
this.width = (int) (entity.width / cellSize) + 1;
this.height = (int) (entity.height / cellSize) + 1;
this.baseWaterLevel = entity.y; // Niveau de référence
this.heightMap = new double[width][height];
this.velocityMap = new double[width][height];
this.prevHeightMap = new double[width][height];
initializeWaves();
}
private void initializeWaves() {
// Initialize with small random perturbations around zero
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
heightMap[x][y] = (Math.random() - 0.5) * 0.5;
velocityMap[x][y] = 0;
prevHeightMap[x][y] = heightMap[x][y];
}
}
}
@Override
public void update(${MAIN_CLASS_NAME} app, long elapsed, Entity entity) {
time += elapsed * 0.001; // Convert to seconds
// Update wave equation
updateWaveEquation(elapsed);
// Generate continuous waves
generateContinuousWaves();
// Apply restoration force
applyRestorationForce();
// Update surface points for rendering
updateSurfacePoints(entity);
// Check for interactions with other entities
checkEntityInteractions(app, entity);
}
private void updateWaveEquation(long elapsed) {
double dt = elapsed * 0.05; // Convert to seconds
//dt = Math.min(dt, 0.016); // Limiter dt pour la stabilité
// Sauvegarder l'état précédent
for (int x = 0; x < width; x++) {
System.arraycopy(heightMap[x], 0, prevHeightMap[x], 0, height);
}
double[][] newHeightMap = new double[width][height];
double[][] newVelocityMap = new double[width][height];
for (int x = 1; x < width - 1; x++) {
for (int y = 1; y < height - 1; y++) {
// Calculate Laplacian (second derivative) - équation des vagues 2D
double laplacian = (heightMap[x - 1][y] + heightMap[x + 1][y] +
heightMap[x][y - 1] + heightMap[x][y + 1] -
4 * heightMap[x][y]) / (cellSize * cellSize);
// Force de restauration vers le niveau de référence (0)
double restorationForce = -heightMap[x][y] * restoreForce;
// Équation des vagues avec force de restauration
double acceleration = tension * laplacian + restorationForce;
// Update velocity avec amortissement
newVelocityMap[x][y] = velocityMap[x][y] * damping + acceleration * dt;
// Update height
newHeightMap[x][y] = heightMap[x][y] + newVelocityMap[x][y] * dt;
// Limiter l'amplitude pour éviter les oscillations excessives
newHeightMap[x][y] = Math.max(-waveAmplitude * 2,
Math.min(waveAmplitude * 2, newHeightMap[x][y]));
}
}
// Boundary conditions - bords fixes
for (int x = 0; x < width; x++) {
newHeightMap[x][0] = 0;
newHeightMap[x][height - 1] = 0;
newVelocityMap[x][0] = 0;
newVelocityMap[x][height - 1] = 0;
}
for (int y = 0; y < height; y++) {
newHeightMap[0][y] = 0;
newHeightMap[width - 1][y] = 0;
newVelocityMap[0][y] = 0;
newVelocityMap[width - 1][y] = 0;
}
heightMap = newHeightMap;
velocityMap = newVelocityMap;
}
private void applyRestorationForce() {
// Force de restauration globale pour ramener les vagues vers le niveau de référence
for (int x = 1; x < width - 1; x++) {
for (int y = 1; y < height - 1; y++) {
// Force qui ramène progressivement vers 0
double restoration = -heightMap[x][y] * 0.001;
velocityMap[x][y] += restoration;
}
}
}
private void generateContinuousWaves() {
// Generate waves from the left side with better amplitude control
for (int y = 1; y < height - 1; y++) {
double waveHeight = waveAmplitude * Math.sin(waveFrequency * time + y * 0.2);
// Injection plus douce des vagues
heightMap[1][y] += waveHeight * 0.05;
velocityMap[1][y] += waveHeight * 0.02;
}
// Add some random disturbances - réduites
if (Math.random() < 0.005) { // Moins fréquent
int x = (int) (Math.random() * (width - 2)) + 1;
int y = (int) (Math.random() * (height - 2)) + 1;
heightMap[x][y] += (Math.random() - 0.5) * 0.5; // Amplitude réduite
}
}
private void updateSurfacePoints(Entity entity) {
surfacePoints.clear();
// Generate surface points based on height map
for (int x = 0; x < width; x++) {
double worldX = entity.x + x * cellSize;
// Prendre seulement la hauteur de la surface (y=0 dans notre grille)
double surfaceHeight = 0;
if (height > 0) {
// Moyenne des quelques premières lignes pour la surface
int surfaceRows = Math.min(3, height);
for (int y = 0; y < surfaceRows; y++) {
surfaceHeight += heightMap[x][y];
}
surfaceHeight /= surfaceRows;
}
// Position mondiale avec décalage de vague
double worldY = baseWaterLevel + surfaceHeight;
surfacePoints.add(new Point2D.Double(worldX, worldY));
}
}
private void checkEntityInteractions(${MAIN_CLASS_NAME} app, Entity fluidEntity) {
// Check interactions with other dynamic entities
app.entities.stream()
.filter(e -> e != fluidEntity &&
e.isActive() &&
e.getPhysicType() == PhysicType.DYNAMIC &&
e.intersects(fluidEntity))
.forEach(entity -> {
// Calculate impact intensity based on velocity
double velocityMagnitude = Math.sqrt(entity.dx * entity.dx + entity.dy * entity.dy);
double impactIntensity = Math.min(velocityMagnitude * 0.2, waveAmplitude); // Limiter l'intensité
// Add wave disturbance at entity's position
if (impactIntensity > 0.1) {
addDisturbance(entity.getCenterX(), entity.getCenterY(), impactIntensity, fluidEntity);
}
});
}
@Override
public void draw(${MAIN_CLASS_NAME} app, Graphics2D g, Entity entity) {
if (surfacePoints.isEmpty()) return;
// Draw wave surface
g.setColor(new Color(0, 100, 255, 180));
// Create wave path
int[] xPoints = new int[surfacePoints.size() + 2];
int[] yPoints = new int[surfacePoints.size() + 2];
// Add surface points
for (int i = 0; i < surfacePoints.size(); i++) {
Point2D point = surfacePoints.get(i);
xPoints[i] = (int) point.getX();
yPoints[i] = (int) point.getY();
}
// Close the polygon at the bottom
xPoints[surfacePoints.size()] = (int) (entity.x + entity.width);
yPoints[surfacePoints.size()] = (int) (entity.y + entity.height);
xPoints[surfacePoints.size() + 1] = (int) entity.x;
yPoints[surfacePoints.size() + 1] = (int) (entity.y + entity.height);
g.fillPolygon(xPoints, yPoints, xPoints.length);
// Draw wave crests with varying intensity
g.setColor(Color.WHITE);
for (int i = 0; i < surfacePoints.size() - 1; i++) {
Point2D p1 = surfacePoints.get(i);
Point2D p2 = surfacePoints.get(i + 1);
// Varier l'intensité selon la hauteur de la vague
double waveHeight = Math.abs(p1.getY() - baseWaterLevel);
int alpha = (int) Math.min(255, waveHeight * 50 + 100);
g.setColor(new Color(255, 255, 255, alpha));
g.drawLine((int) p1.getX(), (int) p1.getY(),
(int) p2.getX(), (int) p2.getY());
}
}
@Override
public void dispose(${MAIN_CLASS_NAME} app, Entity entity) {
// Clean up resources
heightMap = null;
velocityMap = null;
prevHeightMap = null;
surfacePoints.clear();
}
/**
* Adds a disturbance to the wave system at the specified position.
*/
public void addDisturbance(double x, double y, double intensity, Entity fluidEntity) {
// Convert world coordinates to grid coordinates
int gridX = (int) ((x - fluidEntity.x) / cellSize);
int gridY = (int) ((y - fluidEntity.y) / cellSize);
// Add disturbance in a circular pattern
int radius = 2; // Rayon réduit pour éviter les perturbations trop importantes
intensity = Math.min(intensity, waveAmplitude); // Limiter l'intensité
for (int dx = -radius; dx <= radius; dx++) {
for (int dy = -radius; dy <= radius; dy++) {
int nx = gridX + dx;
int ny = gridY + dy;
if (nx >= 1 && nx < width - 1 && ny >= 1 && ny < height - 1) {
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= radius) {
double factor = (radius - distance) / radius;
double disturbance = intensity * factor;
// Ajouter à la fois hauteur et vitesse pour un effet plus réaliste
heightMap[nx][ny] += disturbance;
velocityMap[nx][ny] += disturbance * 0.5;
}
}
}
}
}
/**
* Gets the wave height at a specific world position.
*/
public double getWaveHeightAt(double x, double y, Entity fluidEntity) {
// Convert world coordinates to grid coordinates
int gridX = (int) ((x - fluidEntity.x) / cellSize);
int gridY = (int) ((y - fluidEntity.y) / cellSize);
if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) {
return heightMap[gridX][gridY];
}
return 0;
}
// Builder pattern methods for configuration
public WaveBehavior setWaveAmplitude(double amplitude) {
this.waveAmplitude = amplitude;
return this;
}
public WaveBehavior setWaveFrequency(double frequency) {
this.waveFrequency = frequency;
return this;
}
public WaveBehavior setWaveSpeed(double speed) {
this.waveSpeed = speed;
return this;
}
public WaveBehavior setDamping(double damping) {
this.damping = Math.max(0.9, Math.min(0.999, damping)); // Limiter l'amortissement
return this;
}
public WaveBehavior setTension(double tension) {
this.tension = Math.max(0.1, tension); // Minimum de tension
return this;
}
public WaveBehavior setRestoreForce(double restoreForce) {
this.restoreForce = restoreForce;
return this;
}
// Getters
public List<Point2D> getSurfacePoints() {
return new ArrayList<>(surfacePoints);
}
public double getCellSize() {
return cellSize;
}
public double getBaseWaterLevel() {
return baseWaterLevel;
}
}
private static final long FPS = 60;
private static final ResourceBundle messages = ResourceBundle.getBundle("i18n.messages");
private static final Properties config = new Properties();
private static int debug = 0;
private boolean pause = false;
private Frame window;
private boolean exit = false;
private BufferedImage buffer;
private World world;
private final List<Entity> entities = new CopyOnWriteArrayList<>();
private Camera camera;
private Entity player;
private final boolean[] keys = new boolean[1024];
/**
* Initializes a new instance of the ${MAIN_CLASS_NAME} class and starts the application.
* This constructor serves as the entry point for initializing basic setup and logging
* the application's start.
*/
public ${MAIN_CLASS_NAME}() {
System.out.println("Start test Application...");
}
/**
* Starts the main application loop, initializing configurations, creating the window
* and buffer, and handling the main rendering and event loop. Releases resources upon completion.
*
* @param args command-line arguments that can be used to override default configurations.
*/
public void run(String[] args) {
loadAndParseConfig("/config.properties", args);
createWindow();
createBuffer();
initializeScene();
loop();
dispose();
}
private void initializeScene() {
world = (World) new World("earth")
.setGravity(new Point2D.Double(0, 98.1 * 3))
.setSize(32 * 20, 20 * 20)
.setColor(Color.GRAY)
.setFillColor(Color.DARK_GRAY)
.setMaterial(Material.AIR);
add(world);
// Ajouter des plateformes statiques
Entity platform1 = new Entity("platform1")
.setSize(128, 32)
.setPosition(200, 400)
.setFillColor(Color.LIGHT_GRAY)
.setColor(Color.DARK_GRAY)
.setPhysicType(PhysicType.STATIC)
.setMaterial(Material.STONE);
add(platform1);
Entity platform2 = new Entity("platform2")
.setSize(96, 32)
.setPosition(400, 300)
.setFillColor(Color.LIGHT_GRAY)
.setColor(Color.DARK_GRAY)
.setPhysicType(PhysicType.STATIC)
.setMaterial(Material.STONE);
add(platform2);
player = new Entity("player")
.setSize(24, 32)
.setPosition(world.getWidth() * 0.5, world.getHeight() * 0.75)
.setMaterial(new Material("men", 0.70, 0.88))
.setPhysicType(PhysicType.DYNAMIC)
.setMass(70.0)
.add(new Behavior<Entity>() {
@Override
public void update(${MAIN_CLASS_NAME} app, long elapsed, Entity e) {
double forceStrength = 5000.0; // Force en Newtons
if (isKeyPressed(KeyEvent.VK_LEFT)) {
e.apply(new Point2D.Double(-forceStrength * 10, 0));
}
if (isKeyPressed(KeyEvent.VK_RIGHT)) {
e.apply(new Point2D.Double(forceStrength * 10, 0));
}
if (isKeyPressed(KeyEvent.VK_UP) && !e.jump) {
e.apply(new Point2D.Double(0, -forceStrength * 300)); // Force de saut plus importante
e.jump = true;
e.onGround = false;
}
if (isKeyPressed(KeyEvent.VK_UP) && e.jump) {
e.apply(new Point2D.Double(0, -forceStrength * 10)); // Force de saut plus importante
}
if (isKeyPressed(KeyEvent.VK_DOWN)) {
e.apply(new Point2D.Double(0, forceStrength * 10)); // Force de saut plus importante
}
}
});
add(player);
// Bois (plus léger que l'eau - doit flotter)
Entity woodBox = new Entity("woodBox")
.setSize(50, 50)
.setPosition(100, 100)
.setFillColor(new Color(139, 69, 19))
.setColor(Color.BLACK)
.setMass(60.0)
.setPhysicType(PhysicType.DYNAMIC)
.setMaterial(new Material("wood", 0.8, 0.3, false, 600.0, 0.0)); // Densité 600 kg/m³
add(woodBox);
// Métal (plus lourd que l'eau - doit couler)
Entity heavyBox = new Entity("heavyBox")
.setSize(30, 30)
.setPosition(200, 100)
.setFillColor(Color.GRAY)
.setMass(200.0)
.setColor(Color.BLACK)
.setPhysicType(PhysicType.DYNAMIC)
.setMaterial(new Material("metal", 0.9, 0.1, false, 2700.0, 0.0)); // Densité 2700 kg/m³
add(heavyBox);
// Zone d'eau
// Dans la méthode initialize(), assurons-nous que l'eau est bien définie comme fluide :
Entity water = new Entity("water")
.setSize(world.getWidth(), world.getHeight() * 0.15)
.setPosition(0, world.getHeight() * 0.85)
.setFillColor(new Color(0, 100, 200, 100))
.setColor(new Color(0, 130, 240, 100))
.setPhysicType(PhysicType.STATIC)
.setMaterial(new Material("water", 0.89, 0.0, true, 1000.0, 0.8))
.add(new WaveBehavior(3.0)
.setWaveAmplitude(4.0)
.setWaveFrequency(0.56)
.setDamping(0.998)
.setTension(0.05));
add(water);
camera = (Camera) new Camera("cam01").setTarget(player).setSize(buffer.getWidth(), buffer.getHeight());
}
private void add(Entity e) {
entities.add(e);
e.behaviors.forEach(b -> b.init(this, e));
}
/**
* Loads a configuration file and updates its properties based on the provided arguments.
* The method attempts to load a configuration file from the given path and overrides
* specific properties using the provided key-value pairs in the arguments array.
*
* @param s the relative path to the configuration file to be loaded
* @param args an array of strings representing key-value pairs (in the format key=value)
* to override default configurations from the file
*/
private void loadAndParseConfig(String s, String[] args) {
try {
config.load(getClass().getResourceAsStream(s));
if (args.length > 0) {
for (String arg : args) {
if (arg.contains("=")) {
String[] keyValue = arg.split("=");
config.setProperty(keyValue[0], keyValue[1]);
}
}
}
debug = Integer.parseInt((String) config.getOrDefault("app.debug", "0"));
} catch (Exception e) {
System.err.println("Unable to load config file");
}
}
/**
* Creates and initializes the application's main window.
* <p>
* The method retrieves configuration settings for the window size and applies
* them during the initialization. It sets up a listener for window closing
* events, ensuring the application exits gracefully when the window is closed.
* It also adds a key listener for handling keyboard input and creates a
* triple-buffered drawing strategy for improved rendering performance.
* <p>
* Behavior:
* - Retrieves the window size from the provided configuration, defaulting
* to 640x480 if not specified.
* - Initializes the application window with the specified size and title.
* - Assigns a window-closing event listener to set the application state as
* terminated.
* - Adds a key listener for user input handling.
* - Makes the window visible and enables triple buffering.
*/
private void createWindow() {
String winSize = (String) config.getOrDefault("app.window.size", "640x480");
window = new Frame(messages.getString("app.name"));
String[] size = winSize.split("x");
window.setSize(Integer.parseInt(size[0]), Integer.parseInt(size[1]));
window.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
exit = true;
}
});
window.addKeyListener(this);
window.setVisible(true);
window.createBufferStrategy(3);
}
/**
* Creates and initializes the rendering buffer for the application.
* <p>
* The method retrieves the buffer resolution from the configuration file or
* falls back to a default value of 640x480 if not specified. It then parses
* the resolution to determine the width and height of the buffer and creates
* a new BufferedImage object with the specified dimensions and an alpha-enabled
* pixel format (ARGB). This buffer is used for off-screen rendering and is
* essential for the application's graphical rendering process.
* <p>
* Behavior:
* - Reads the buffer resolution configuration key (`app.renderer.buffer.resolution`).
* - Defaults to "640x480" if no configuration is provided.
* - Splits the resolution string into width and height.
* - Initializes a BufferedImage object with the specified resolution and ARGB type.
*/
private void createBuffer() {
String bufferReso = (String) config.getOrDefault("app.renderer.buffer.resolution", "640x480");
String[] bufferSize = bufferReso.split("x");
buffer = new BufferedImage(Integer.parseInt(bufferSize[0]), Integer.parseInt(bufferSize[1]), BufferedImage.TYPE_INT_ARGB);
}
/**
* Handles the main application rendering and event loop.
* <p>
* This method contains the core logic for the rendering process and time tracking.
* It continuously executes in a loop until the application state is set to exit,
* rendering updates to the screen, calculating elapsed time, and maintaining a
* smooth execution cycle by delaying the loop execution using thread sleep. The
* method also manages resources efficiently by properly disposing of graphics
* objects after use.
* <p>
* Behavior:
* - Tracks elapsed time and displays it in the console and the window.
* - Continuously renders a graphical representation of the elapsed time on a
* double-buffered display.
* - Clears the rendering buffer with a black background.
* - Draws formatted elapsed time as text using antialiasing for better visuals.
* - Ensures proper disposal of rendering resources to maintain performance.
* - Uses triple buffering to render the updated frame onto the screen.
* - Sleeps for 100 ms after each iteration to maintain a consistent update cycle.
* <p>
* The loop terminates when the `exit` field is set to true, ensuring a smooth
* application shutdown.
*/
private void loop() {
long startTime = 0, endTime = System.currentTimeMillis(), elapsed = 0, elapsedTime = 0;
while (!exit) {
startTime = endTime;
elapsedTime += elapsed;
System.out.printf("loop %s\r", getFormatedTime(elapsedTime));
// update game logic
if (!pause) {
update(elapsed);
}
// render everything.
render(elapsedTime);
// reset Entity's applied forces
postUpdate(elapsed);
// wait next frame
waitUntilNextFrame(elapsed);
endTime = System.currentTimeMillis();
elapsed = endTime - startTime;
}
System.out.printf("loop %s%n", getFormatedTime(elapsedTime));
}
/**
* Delays execution to synchronize the application loop with a target frame rate.
* This method calculates the remaining time until the next frame should begin
* based on the target frames per second (FPS) and the elapsed time of the
* current frame. It ensures a smooth and consistent frame rate by pausing
* execution for the calculated duration.
*
* @param elapsed the time, in milliseconds, that has elapsed since the start
* of the current frame
*/
private static void waitUntilNextFrame(long elapsed) {
try {
Thread.sleep((1000 / FPS) - elapsed > 0 ? (1000 / FPS) - elapsed : 1);
} catch (Exception e) {
System.err.printf("Unable to wait for %d ms%n", elapsed);
}
}
/**
* Updates the state of all active entities in the game, except for the {@code World},
* based on the provided elapsed time. Additionally, updates the camera if present.
* <p>
* This method performs the following actions:
* - Filters the entities to exclude the {@code World} and only includes entities
* marked as active.
* - Updates each active entity by invoking {@code updateEntity}, performing entity-specific
* updates, and ensuring the entity remains within the world boundaries.
* - Updates the camera, if it is not null, based on the elapsed time.
*
* @param elapsed the time, in milliseconds, that has elapsed since the last update
*/
private void update(long elapsed) {
entities.stream()
.filter(e -> !(e instanceof World) && e.isActive())
.forEach(e -> {
updateEntity(e, elapsed);
e.update(elapsed);
keepEntityIntoWorld(e, world);
});
if (camera != null) {
camera.update(elapsed);
}
}
/**
* Performs post-update actions on all active entities in the system, excluding instances of {@code World}.
* This method resets the forces for each active entity.
*
* @param elapsed the time elapsed since the last update, in milliseconds
*/
private void postUpdate(long elapsed) {
entities.stream()
.filter(e -> !(e instanceof World) && e.isActive())
.forEach(Entity::resetForces);
}
/**
* Updates the position and rotation of the entity based on the elapsed time.
* Version nettoyée sans debug.
*/
public void updateEntity(Entity e, long elapsed) {
// apply logic update from behaviors
e.behaviors.forEach(b -> {
b.update(this, elapsed, e);
});
Material material = e.getMaterial();
if (e.getPhysicType().equals(PhysicType.DYNAMIC)) {
// Reset ground status at the beginning of each update
e.onGround = false;
// Convert elapsed time from milliseconds to seconds
double elapsedSeconds = elapsed / 1000.0;
// Check for fluid collisions (separate from solid collisions)
List<Entity> fluidEntities = checkCollisionWithFluidEntities(e);
// Apply world gravity
Point2D gravity = world.getGravity();
e.dx += gravity.getX() * elapsedSeconds;
e.dy += gravity.getY() * elapsedSeconds;
// Apply buoyancy and drag forces if entity is in fluid
for (Entity fluidEntity : fluidEntities) {
// Calculate and apply buoyancy force
Point2D buoyancyForce = calculateBuoyancyForceWithWaves(e, fluidEntity);
e.apply(buoyancyForce);
// Calculate and apply drag force
Point2D dragForce = calculateDragForce(e, fluidEntity);
e.apply(dragForce);
// Calculate and apply surface damping to prevent oscillations
Point2D dampingForce = calculateSurfaceDamping(e, fluidEntity);
e.apply(dampingForce);
}
// compute the resulting acceleration according to applied forces
for (Point2D f : e.getForces()) {
e.dx += (f.getX() / e.mass) * elapsedSeconds;
e.dy += (f.getY() / e.mass) * elapsedSeconds;
}
// Limiter la vitesse pour éviter les comportements erratiques
double maxSpeed = 200.0;
double currentSpeed = Math.sqrt(e.dx * e.dx + e.dy * e.dy);
if (currentSpeed > maxSpeed) {
double speedRatio = maxSpeed / currentSpeed;
e.dx *= speedRatio;
e.dy *= speedRatio;
}
// Store previous position for collision resolution
double prevX = e.x;
double prevY = e.y;
// compute new position
e.x += e.dx * elapsedSeconds;
e.y += e.dy * elapsedSeconds;
// Check for collisions with SOLID static entities only
List<Material> contactMaterials = checkCollisionWithStaticEntities(e, prevX, prevY);
// Add fluid materials to contact materials for friction calculation ONLY
List<Material> fluidMaterials = checkFluidContact(e);
contactMaterials.addAll(fluidMaterials);
// compute new rotation
e.r += e.dr * elapsedSeconds;
// compute friction according to possible contact(s)
if (e.onGround || !contactMaterials.isEmpty()) {
// Calculate combined friction from all contact materials
double combinedFriction = calculateCombinedFriction(contactMaterials);
e.dx *= combinedFriction;
e.dy *= combinedFriction;
e.dr *= combinedFriction;
} else {
// no contact, only apply the World atmosphere's material friction.
e.dx *= world.getMaterial().friction;
e.dy *= world.getMaterial().friction;
e.dr *= world.getMaterial().friction;
}
}
}
/**
* Calculates the intersection area between two entities.
* This is used to determine how much of an entity is submerged in fluid.
*
* @param entity1 the first entity
* @param entity2 the second entity
* @return the intersection area
*/
private double calculateIntersectionArea(Entity entity1, Entity entity2) {
// Calculate the intersection rectangle
double x1 = Math.max(entity1.x, entity2.x);
double y1 = Math.max(entity1.y, entity2.y);
double x2 = Math.min(entity1.x + entity1.width, entity2.x + entity2.width);
double y2 = Math.min(entity1.y + entity1.height, entity2.y + entity2.height);
// If there's no intersection, return 0
if (x2 <= x1 || y2 <= y1) {
return 0.0;
}
// Return the area of intersection
return (x2 - x1) * (y2 - y1);
}
/**
* Calculates the buoyancy force (Archimedes' principle) for an entity submerged in fluid.
* VERSION AVEC SUIVI DU FACTEUR D'IMMERSION MAXIMUM (sans debug).
*
* @param entity the entity experiencing buoyancy
* @param fluidEntity the fluid entity (like water)
* @return the buoyancy force as a Point2D (toujours vers le haut)
*/
private Point2D calculateBuoyancyForceOld(Entity entity, Entity fluidEntity) {
// Calculate intersection area (approximation of submerged volume)
double submergedArea = calculateIntersectionArea(entity, fluidEntity);
if (submergedArea <= 0) {
return new Point2D.Double(0, 0);
}
// Calcul du facteur d'immersion actuel
double entityArea = entity.getWidth() * entity.getHeight();
double currentSubmersionRatio = Math.min(submergedArea / entityArea, 1.0);
// Mise à jour du facteur d'immersion maximum
updateMaxSubmersionRatio(entity, currentSubmersionRatio);
// Estimation du volume 3D
double estimatedThickness = 10.0;
double submergedVolume = submergedArea * estimatedThickness;
// Get fluid density
double fluidDensity = fluidEntity.getMaterial().getDensity();
// Calculate buoyancy force using Archimedes' principle
Point2D gravity = world.getGravity();
double volumeToForceScale = 0.00001;
// Buoyancy force magnitude (always upward, opposing gravity)
double buoyancyForceMagnitude = fluidDensity * submergedVolume * Math.abs(gravity.getY()) * volumeToForceScale;
// Limiter la force maximale pour éviter les explosions
double maxBuoyancyForce = Math.abs(gravity.getY()) * entity.mass * 1.5;
buoyancyForceMagnitude = Math.min(buoyancyForceMagnitude, maxBuoyancyForce);
// La poussée d'Archimède agit toujours vers le haut (oppose la gravité)
return new Point2D.Double(0, -buoyancyForceMagnitude);
}
/**
* Calculates the buoyancy force with wave effects, using the same approach as the original
* calculateBuoyancyForce method but with wave height adjustments.
*/
private Point2D calculateBuoyancyForceWithWaves(Entity entity, Entity fluidEntity) {
// Calculate intersection area (approximation of submerged volume)
double submergedArea = calculateIntersectionArea(entity, fluidEntity);
// Adjust submerged area based on wave height
if (fluidEntity.findBehavior(WaveBehavior.class) != null) {
submergedArea = calculateWaveAdjustedIntersectionArea(entity, fluidEntity);
}
if (submergedArea <= 0) {
return new Point2D.Double(0, 0);
}
// Calcul du facteur d'immersion actuel
double entityArea = entity.getWidth() * entity.getHeight();
double currentSubmersionRatio = Math.min(submergedArea / entityArea, 1.0);
// Mise à jour du facteur d'immersion maximum
updateMaxSubmersionRatio(entity, currentSubmersionRatio);
// Estimation du volume 3D
double estimatedThickness = 10.0;
double submergedVolume = submergedArea * estimatedThickness;
// Get fluid density
double fluidDensity = fluidEntity.getMaterial().getDensity();
// Calculate buoyancy force using Archimedes' principle
Point2D gravity = world.getGravity();
double volumeToForceScale = 0.00001;
// Buoyancy force magnitude (always upward, opposing gravity)
double buoyancyForceMagnitude = fluidDensity * submergedVolume * Math.abs(gravity.getY()) * volumeToForceScale;
// Limiter la force maximale pour éviter les explosions
double maxBuoyancyForce = Math.abs(gravity.getY()) * entity.mass * 1.5;
buoyancyForceMagnitude = Math.min(buoyancyForceMagnitude, maxBuoyancyForce);
// La poussée d'Archimède agit toujours vers le haut (oppose la gravité)
return new Point2D.Double(0, -buoyancyForceMagnitude);
}
/**
* Calculates the intersection area between an entity and a fluid entity,
* taking into account wave deformation of the fluid surface.
*/
private double calculateWaveAdjustedIntersectionArea(Entity entity, Entity fluidEntity) {
WaveBehavior waveBehavior = (WaveBehavior) fluidEntity.findBehavior(WaveBehavior.class);
if (waveBehavior == null) {
return calculateIntersectionArea(entity, fluidEntity);
}
// Get wave surface points
List<Point2D> surfacePoints = waveBehavior.getSurfacePoints();
if (surfacePoints.isEmpty()) {
return calculateIntersectionArea(entity, fluidEntity);
}
// Calculate intersection with wave-deformed surface
double totalArea = 0;
double cellSize = waveBehavior.getCellSize();
// Iterate through the entity's horizontal extent
double entityLeft = entity.x;
double entityRight = entity.x + entity.width;
double entityTop = entity.y;
double entityBottom = entity.y + entity.height;
// Sample the wave surface at regular intervals
int samples = (int) Math.ceil(entity.width / cellSize);
samples = Math.max(samples, 10); // Minimum samples for accuracy
for (int i = 0; i < samples; i++) {
double x = entityLeft + (i * entity.width / samples);
// Get wave height at this x position
double waveHeight = waveBehavior.getWaveHeightAt(x, fluidEntity.y, fluidEntity);
double adjustedFluidTop = waveBehavior.getBaseWaterLevel() + waveHeight;
// Calculate intersection height at this x position
double intersectionTop = Math.max(entityTop, adjustedFluidTop);
double intersectionBottom = Math.min(entityBottom, fluidEntity.y + fluidEntity.height);
if (intersectionBottom > intersectionTop) {
// Calculate the area of this vertical slice
double sliceWidth = entity.width / samples;
double sliceHeight = intersectionBottom - intersectionTop;
totalArea += sliceWidth * sliceHeight;
}
}
return totalArea;
}
/**
* Enhanced method to get wave behavior from entity, with better error handling.
*/
private WaveBehavior getWaveBehavior(Entity entity) {
return entity.behaviors.stream()
.filter(b -> b instanceof WaveBehavior)
.map(b -> (WaveBehavior) b)
.findFirst()
.orElse(null);
}
/**
* Met à jour le facteur d'immersion maximum pour une entité donnée.
* Détecte aussi les phases de remontée et d'émersion.
*
* @param entity l'entité dont on suit l'immersion
* @param currentRatio le ratio d'immersion actuel (0.0 à 1.0)
*/
private void updateMaxSubmersionRatio(Entity entity, double currentRatio) {
// Utiliser les propriétés de l'entité pour stocker les données
if (entity.getProperty("maxSubmersionRatio") == null) {
entity.setProperty("maxSubmersionRatio", currentRatio);
entity.setProperty("wasFullySubmerged", false);
entity.setProperty("isRising", false);
entity.setProperty("previousRatio", currentRatio);
entity.setProperty("riseStartTime", System.currentTimeMillis());
}
double maxRatio = (Double) entity.getProperty("maxSubmersionRatio");
boolean wasFullySubmerged = (Boolean) entity.getProperty("wasFullySubmerged");
boolean isRising = (Boolean) entity.getProperty("isRising");
double previousRatio = (Double) entity.getProperty("previousRatio");
long riseStartTime = (Long) entity.getProperty("riseStartTime");
// Mise à jour du maximum
if (currentRatio > maxRatio) {
entity.setProperty("maxSubmersionRatio", currentRatio);
}
// Détecter si l'objet a été complètement submergé
if (currentRatio >= 0.99) {
entity.setProperty("wasFullySubmerged", true);
}
// Détecter le début de la remontée (vitesse vers le haut + était submergé)
if (!isRising && entity.dy < -5 && wasFullySubmerged) {
entity.setProperty("isRising", true);
entity.setProperty("riseStartTime", System.currentTimeMillis());
}
// Détecter la fin de la remontée (objet sort de l'eau)
if (isRising && currentRatio <= 0.01) {
long remonteeTime = System.currentTimeMillis() - riseStartTime;
entity.setProperty("isRising", false);
entity.setProperty("emergenceTime", remonteeTime);
}
entity.setProperty("previousRatio", currentRatio);
}
/**
* Calculates a stabilized drag force for an entity moving through a fluid.
* Cette traînée aide à stabiliser les objets dans le fluide.
*
* @param entity the entity experiencing drag
* @param fluidEntity the fluid entity
* @return the drag force as a Point2D
*/
private Point2D calculateDragForce(Entity entity, Entity fluidEntity) {
// Calculate intersection area to determine how much of the entity is in fluid
double submergedArea = calculateIntersectionArea(entity, fluidEntity);
if (submergedArea <= 0) {
return new Point2D.Double(0, 0);
}
double entityArea = entity.getWidth() * entity.getHeight();
double submersionRatio = Math.min(submergedArea / entityArea, 1.0);
double viscosity = fluidEntity.getMaterial().getViscosity();
double speed = Math.sqrt(entity.dx * entity.dx + entity.dy * entity.dy);
if (speed < 0.001) return new Point2D.Double(0, 0);
// Drag force plus important pour stabiliser
double dragCoefficient = 2.0 * (1.0 - viscosity) * submersionRatio;
double dragMagnitude = dragCoefficient * speed;
// Direction opposite to velocity
double dragX = -entity.dx * dragMagnitude / speed;
double dragY = -entity.dy * dragMagnitude / speed;
return new Point2D.Double(dragX, dragY);
}
/**
* Calculates a damping force to prevent oscillations at the fluid surface.
* Cette force aide à stabiliser les objets qui flottent à la surface.
*
* @param entity the entity
* @param fluidEntity the fluid entity
* @return the damping force as a Point2D
*/
private Point2D calculateSurfaceDamping(Entity entity, Entity fluidEntity) {
double submergedArea = calculateIntersectionArea(entity, fluidEntity);
double entityArea = entity.getWidth() * entity.getHeight();
double submersionRatio = submergedArea / entityArea;
// Damping plus fort près de la surface (entre 0.1 et 0.9 de submersion)
if (submersionRatio > 0.1 && submersionRatio < 0.9) {
double dampingFactor = 0.5; // Facteur d'amortissement
return new Point2D.Double(
-entity.dx * dampingFactor,
-entity.dy * dampingFactor
);
}
return new Point2D.Double(0, 0);
}
/**
* Checks for collisions between an entity and fluid entities (like water).
* Returns a list of fluid entities that the given entity is currently intersecting with.
* Les fluides ne bloquent JAMAIS le mouvement, ils appliquent seulement des forces.
*
* @param entity the entity to check for fluid collisions
* @return a list of fluid entities that the entity is colliding with
*/
private List<Entity> checkCollisionWithFluidEntities(Entity entity) {
return entities.stream()
.filter(e -> e != entity &&
e.isActive() &&
e.getMaterial().isFluid() &&
entity.intersects(e))
.collect(Collectors.toList());
}
/**
* Checks for collisions between an entity and static entities.
* La vérification fluide/solide se fait maintenant dans resolveCollision().
* Returns a list of materials that the entity is in contact with.
*
* @param entity the entity to check for collisions
* @param prevX the entity's previous x position
* @param prevY the entity's previous y position
* @return a list of materials that the entity is in contact with
*/
private List<Material> checkCollisionWithStaticEntities(Entity entity, double prevX, double prevY) {
List<Material> contactMaterials = new ArrayList<>();
entities.stream()
.filter(e -> e != entity &&
e.getPhysicType().equals(PhysicType.STATIC) &&
e.isActive())
.forEach(staticEntity -> {
if (entity.intersects(staticEntity)) {
// Ajouter le matériau seulement si ce n'est PAS un fluide
if (!staticEntity.getMaterial().isFluid()) {
contactMaterials.add(staticEntity.getMaterial());
}
// Appeler la résolution - elle se chargera de vérifier si c'est un fluide
resolveCollision(entity, staticEntity, prevX, prevY);
}
});
return contactMaterials;
}
/**
* Vérifie le contact avec les fluides pour le calcul des frottements.
* Cette méthode ne résout AUCUNE collision, elle retourne juste les matériaux.
*
* @param entity the entity to check
* @return list of fluid materials in contact
*/
private List<Material> checkFluidContact(Entity entity) {
return entities.stream()
.filter(e -> e != entity &&
e.isActive() &&
e.getMaterial().isFluid() &&
entity.intersects(e))
.map(Entity::getMaterial)
.collect(Collectors.toList());
}
/**
* Calculates the combined friction from multiple contact materials.
* Uses the minimum friction value (most slippery surface dominates).
*
* @param contactMaterials list of materials from contacted surfaces
* @return combined friction coefficient
*/
private double calculateCombinedFriction(List<Material> contactMaterials) {
if (contactMaterials.isEmpty()) {
return world.getMaterial().friction;
}
// Use the minimum friction (most slippery surface dominates)
return contactMaterials.stream()
.mapToDouble(Material::getFriction)
.min()
.orElse(world.getMaterial().friction);
}
/**
* Méthode principale de résolution de collision qui vérifie TOUJOURS si l'entité statique est un fluide.
* Si c'est un fluide, la collision est IGNORÉE (pas de repositionnement).
* Cette méthode remplace toutes les autres méthodes de résolution de collision.
*/
private void resolveCollision(Entity dynamicEntity, Entity staticEntity, double prevX, double prevY) {
// VÉRIFICATION ABSOLUE : Les fluides ne bloquent JAMAIS les objets
if (staticEntity.getMaterial().isFluid()) {
return; // SORTIE IMMÉDIATE - aucun repositionnement
}
// Calculate overlap on both axes
double overlapX = Math.min(dynamicEntity.x + dynamicEntity.width - staticEntity.x,
staticEntity.x + staticEntity.width - dynamicEntity.x);
double overlapY = Math.min(dynamicEntity.y + dynamicEntity.height - staticEntity.y,
staticEntity.y + staticEntity.height - dynamicEntity.y);
// Calculate combined elasticity (average of both materials)
double combinedElasticity = (dynamicEntity.getMaterial().elasticity + staticEntity.getMaterial().elasticity) / 2.0;
// Mass factor for collision response (heavier objects are less affected by collisions)
double massImpactFactor = 1.0 / Math.sqrt(dynamicEntity.mass);
// Determine collision direction based on smaller overlap
if (overlapX < overlapY) {
// Horizontal collision
if (dynamicEntity.x < staticEntity.x) {
// Collision from left
dynamicEntity.x = staticEntity.x - dynamicEntity.width;
dynamicEntity.dx = -Math.abs(dynamicEntity.dx) * combinedElasticity * massImpactFactor;
} else {
// Collision from right
dynamicEntity.x = staticEntity.x + staticEntity.width;
dynamicEntity.dx = Math.abs(dynamicEntity.dx) * combinedElasticity * massImpactFactor;
}
dynamicEntity.addContact(1); // Horizontal contact
} else {
// Vertical collision
if (dynamicEntity.y < staticEntity.y) {
// Collision from top (entity hits static object from above)
dynamicEntity.y = staticEntity.y - dynamicEntity.height;
dynamicEntity.dy = -Math.abs(dynamicEntity.dy) * combinedElasticity * massImpactFactor;
} else {
// Collision from bottom (entity lands on static object)
dynamicEntity.y = staticEntity.y + staticEntity.height;
dynamicEntity.dy = Math.abs(dynamicEntity.dy) * combinedElasticity * massImpactFactor;
}
dynamicEntity.addContact(2); // Vertical contact
}
// Check if entity is standing on the static object (grounded)
if (isEntityGroundedOn(dynamicEntity, staticEntity)) {
dynamicEntity.onGround = true;
dynamicEntity.jump = false;
}
}
/**
* Determines if a dynamic entity is grounded on a static entity.
* An entity is considered grounded if it's resting on top of a static object.
*
* @param dynamicEntity the dynamic entity to check
* @param staticEntity the static entity to check against
* @return true if the dynamic entity is grounded on the static entity
*/
private boolean isEntityGroundedOn(Entity dynamicEntity, Entity staticEntity) {
// Check if the dynamic entity is resting on top of the static entity
double tolerance = 2.0; // Small tolerance for floating point precision
// Entity's bottom should be close to or touching the static entity's top
boolean touchingTop = Math.abs((dynamicEntity.y + dynamicEntity.height) - staticEntity.y) <= tolerance;
// Entity should be horizontally overlapping with the static entity
boolean horizontalOverlap = !(dynamicEntity.x + dynamicEntity.width <= staticEntity.x ||
dynamicEntity.x >= staticEntity.x + staticEntity.width);
// Entity should have minimal or downward vertical velocity
boolean stableVertically = dynamicEntity.dy >= -0.1; // Allow small upward velocity due to elasticity
return touchingTop && horizontalOverlap && stableVertically;
}
/**
* Ensures that the specified entity remains within the bounds of the given world.
* If the entity moves outside the world boundaries, its position and velocity are adjusted
* to simulate collisions with the edges of the world using the world's material properties.
*
* @param e the entity to be kept within the world bounds
* @param world the world defining the boundaries within which the entity must remain
*/
private void keepEntityIntoWorld(Entity e, World world) {
e.resetContact();
if (!world.contains(e)) {
// Calculate combined elasticity with world material
double combinedElasticity = (e.getMaterial().elasticity + world.getMaterial().elasticity) / 2.0;
if (e.x < world.x) {
e.x = world.x;
e.dx = -e.dx * combinedElasticity;
e.addContact(1);
}
if (e.y < world.y) {
e.y = world.y;
e.dy = -e.dy * combinedElasticity;
e.jump = false;
e.addContact(2);
}
if (e.x + e.width > world.x + world.width) {
e.x = world.x + world.width - e.width;
e.dx = -e.dx * combinedElasticity;
e.addContact(4);
}
if (e.y + e.height > world.y + world.height) {
e.y = world.y + world.height - e.height;
e.dy = -e.dy * combinedElasticity;
e.jump = false;
e.onGround = true; // Entity is grounded when touching the bottom of the world
e.addContact(8);
}
}
}
/**
* Renders the game frame using double buffering and displays it on the screen.
* This method handles the rendering of game entities, camera transformations, and
* optional debug information based on the current game state and elapsed time.
*
* @param elapsedTime the time elapsed since the last frame was rendered, in milliseconds
*/
private void render(long elapsedTime) {
Graphics2D g = buffer.createGraphics();
g.setRenderingHints(
Map.of(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON,
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON
));
// clear buffer
g.setBackground(Color.BLACK);
g.clearRect(0, 0, buffer.getWidth(), buffer.getHeight());
// draw everything!
entities.stream()
.filter(Entity::isActive)
.forEach(e -> {
if (camera != null) g.translate(-camera.x, -camera.y);
e.draw(g);
e.behaviors.forEach(b -> b.draw(this, g, e));
e.drawDebug(g);
if (camera != null) g.translate(camera.x, camera.y);
});
// dispose API
g.dispose();
// copy on screen
BufferStrategy bs = window.getBufferStrategy();
Graphics2D gs = (Graphics2D) bs.getDrawGraphics();
gs.drawImage(buffer, 0, 0, window.getWidth(), window.getHeight(), 0, 0, buffer.getWidth(), buffer.getHeight(), null);
// draw time on buffer
if (debug > 0) {
gs.setColor(Color.ORANGE);
gs.setFont(g.getFont().deriveFont(Font.BOLD, 10.0f));
gs.drawString("[ time:%s | debug %d | entity:%d | pause:%s ]".formatted(
getFormatedTime(elapsedTime),
debug,
entities.size(),
pause),
10, window.getHeight() - 12);
}
// free graphics API
gs.dispose();
// switch buffer
bs.show();
}
/**
* Releases resources and performs necessary cleanup operations for the application.
* <p>
* This method disposes of the main application window and prints a log message to indicate
* the termination of the application. It is typically called at the end of the application's
* lifecycle to ensure a proper shutdown.
* <p>
* Behavior:
* - Disposes of the main window resource, releasing associated memory and system resources.
* - Logs a message to the standard output to signal the end of the application.
*/
private void dispose() {
window.dispose();
System.out.println("End test Application.");
}
public static void main(String[] args) {
${MAIN_CLASS_NAME} app = new ${MAIN_CLASS_NAME}();
app.run(args);
}
/**
* Formats the given time duration in milliseconds into a human-readable string representation.
* The output format is "HH:mm:ss.SSS", where HH represents hours, mm represents minutes,
* ss represents seconds, and SSS represents milliseconds.
*
* @param time the time duration in milliseconds to be formatted
* @return a formatted string representing the time duration in "HH:mm:ss.SSS" format
*/
public static String getFormatedTime(long time) {
return "%02d:%02d:%02d.%03d".formatted(
((time / 1000) * 3600) % 24,
((time / 1000) / 60) % 60,
((time / 1000) % 60),
time % 1000);
}
/*------------------ Manage key listener ---------------*/
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
keys[e.getKeyCode()] = true;
}
public void keyReleased(KeyEvent e) {
keys[e.getKeyCode()] = false;
switch (e.getKeyCode()) {
// quit the demo
case KeyEvent.VK_ESCAPE -> {
exit = true;
}
// switch level of the debug display /log
case KeyEvent.VK_D -> {
if (e.isControlDown()) {
debug = (debug + 1) % 10;
}
}
// reverse gravity
case KeyEvent.VK_G -> {
if (e.isControlDown()) {
world.gravity = new Point2D.Double(-world.gravity.getX(), -world.gravity.getY());
}
}
case KeyEvent.VK_P, KeyEvent.VK_PAUSE -> {
pause = !pause;
}
case KeyEvent.VK_Z -> {
entities.clear();
initializeScene();
}
// others cases, do nothing
default -> {
// do nothing
}
}
}
/**
* Checks if the specified key is currently pressed.
*
* @param keyCode the key code representing the key whose state is to be checked
* (corresponds to the constants in {@link KeyEvent}).
* @return true if the specified key is pressed; false otherwise.
*/
public boolean isKeyPressed(int keyCode) {
return keys[keyCode];
}
}
package ${PACKAGE_NAME};
import java.time.ZonedDateTime;
import java.util.Properties;
import java.util.ResourceBundle;
/**
* The main application class.
*
* @author ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>
* @version ${APP_VERSION}
*/
public class ${MAIN_CLASS_NAME}{
/**
* The application modes.
*/
public enum AppMode {
/**
* Development mode.
* Used for debugging and development purposes.
*/
DEVELOPMENT,
/**
* Production mode.
* Used for running the application in a production environment.
*/
PRODUCTION}
/**
* The resource bundle for internationalization.
*/
public static ResourceBundle messages = ResourceBundle.getBundle("i18n/messages");
/**
* The application configuration properties.
*/
public static Properties config = new Properties();
/**
* The debug level for the application.
* 0 = no debug, 1 = basic debug, 2 = detailed debug
*/
public static int debug = 0;
/**
* The current application mode.
* Default is PRODUCTION.
*/
public static AppMode mode = AppMode.PRODUCTION;
/**
* Creates a new instance of the application.
*/
public ${MAIN_CLASS_NAME}() {
info(${MAIN_CLASS_NAME}.class, "Create application '%s'", messages.getString("app.name"));
}
/**
* Runs the application.
*
* @param args the command-line arguments
*/
public void run(String[] args) {
info(${MAIN_CLASS_NAME}.class, "Application '%s' is running..", messages.getString("app.name"));
for (String arg : args) {
info(${MAIN_CLASS_NAME}.class, "", arg);
}
init(args);
dispose();
}
/**
* Initializes the application.
*
* @param args the command-line arguments
*/
private void init(String[] args) {
info(${MAIN_CLASS_NAME}.class, "Application '%s' is initializing.", messages.getString("app.name"));
// default values
config.put("app.config.file", "/config.properties");
config.put("app.debug", 0);
config.put("app.mode", AppMode.DEVELOPMENT);
// parsing arguments
for (String arg : args) {
String[] kv = arg.split("=", 2);
if (kv.length == 2) {
config.put(kv[0], kv[1]);
info(${MAIN_CLASS_NAME}.class, "Set config from argument %s = %s", kv[0], kv[1]);
} else {
warn(${MAIN_CLASS_NAME}.class, "Invalid config argument: %s", arg);
}
info(${MAIN_CLASS_NAME}.class, "", arg);
}
// load configuration file
try {
config.load(${MAIN_CLASS_NAME}.class.getResourceAsStream(config.getProperty("app.config.file")));
} catch (Exception e) {
error(${MAIN_CLASS_NAME}.class, "Failed to load config file: %s", e.getMessage());
}
// extract configuration values
parseConfiguration(config);
}
/**
* Parses the application configuration.
*
* @param config the configuration properties
*/
private void parseConfiguration(Properties config) {
info(${MAIN_CLASS_NAME}.class, "Parsing configuration.");
for (String key : config.stringPropertyNames()) {
String value = config.getProperty(key);
switch (key) {
case "app.debug":
debug = Integer.parseInt(value);
info(${MAIN_CLASS_NAME}.class, "read config '%s' = '%s'", key, value);
break;
case "app.mode":
mode = AppMode.valueOf(value.toUpperCase());
info(${MAIN_CLASS_NAME}.class, "read config '%s' = '%s'", key, value);
break;
default:
warn(${MAIN_CLASS_NAME}.class, "Unknown config key: %s", key);
}
info(${MAIN_CLASS_NAME}.class, "Config '%s' = '%s'", key, value);
}
}
/**
* Disposes the application resources.
*/
private void dispose() {
info(${MAIN_CLASS_NAME}.class, "Application '%s' is ending.", messages.getString("app.name"));
}
/**
* The main entry point for the application.
*/
public static void main(String[] args) {
${MAIN_CLASS_NAME} app = new ${MAIN_CLASS_NAME}();
app.run(args);
}
/**
* Logs a message with the specified log level.
*
* @param cls the class logging the message
* @param level the log level
* @param message the log message
* @param args optional arguments for the message
*/
private static void log(Class<?> cls, String level, String message, Object... args) {
System.out.printf("%s;%s;%s;%s%n", ZonedDateTime.now(), cls.getCanonicalName(), level, message.formatted(args));
}
/**
* Logs an informational message.
*
* @param cls the class logging the message
* @param message the log message
* @param args optional arguments for the message
*/
public static void info(Class<?> cls, String message, Object... args) {
log(cls, "INFO", message, args);
}
/**
* Logs a warning message.
*
* @param cls the class logging the message
* @param message the log message
* @param args optional arguments for the message
*/
public static void warn(Class<?> cls, String message, Object... args) {
log(cls, "WARN", message, args);
}
/**
* Logs a debug message.
*
* @param cls the class logging the message
* @param debugLevel the debug level
* @param message the log message
* @param args optional arguments for the message
*/
public static void debug(Class<?> cls, int debugLevel, String message, Object... args) {
if (App.isDebugGreaterThan(debugLevel)) {
log(cls, "DEBUG", message, args);
}
}
/**
* Logs an error message.
*
* @param cls the class logging the message
* @param message the log message
* @param args optional arguments for the message
*/
public static void error(Class<?> cls, String message, Object... args) {
log(cls, "ERROR", message, args);
}
/**
* Checks if the current debug level is greater than the specified level.
*
* @param level the debug level to compare against
* @return true if the current debug level is greater, false otherwise
*/
public static boolean isDebugGreaterThan(int level) {
return debug < level;
}
}
#!/bin/bash
# New Java project script generation
# Version: 1.0
# Author: Frédéric Delorme <[email protected]>
# Date: 2025-07-25
#
# This script creates a new Java project with a specified structure,
# including directories for source code, resources, tests, and documentation.
# It also initializes a Git repository, sets up a README file, and configures
# the project for use with VSCode and SDKMAN.
# This script is designed to be run in a Unix-like environment with bash.
#
# MIT License
#
# Copyright (c) 2025 Frédéric Delorme <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
#
# Fonction pour afficher l'utilisation du script
usage() {
echo "Usage: $0 [-p <project_name>] [-j <java_version>] [-a <author_name>] [-e <author_email>] [-r <remote_repo_url>] [-f|--frame] [--help]"
echo ""
echo "Options:"
echo " -p, --project <project_name> Nom du projet à créer (par défaut: myapp)"
echo " -j, --java <java_version> Version de Java à utiliser (par défaut: 24-zulu)"
echo " -a, --author-name <author_name> Nom de l'auteur Git (par défaut: Default Author)"
echo " -e, --author-email <author_email> Email de l'auteur Git (par défaut: [email protected])"
echo " -r, --remote <remote_repo_url> URL du dépôt distant pour le push (facultatif)"
echo " -pk, --package <package_name> Nom du package (par défaut: core)"
echo " -mc, --main-class <main_class_name> Nom de la classe principale (par défaut: App)"
echo " -f, --frame Crée une application avec une fenêtre (par défaut: non)"
echo " --help Affiche cette aide"
echo ""
echo "Exemple:"
echo " $0 -p mon_projet -j 17-zulu -a \"Jean Dupont\" -e \"[email protected]\" -r \"https://github.com/username/repo.git\" -pk \"core\" -mc \"App\""
exit 1
}
# Valeurs par défaut
export PROJECT_NAME="myapp"
export JAVA_VERSION="24-zulu"
export GIT_AUTHOR_NAME="Default Author"
export GIT_AUTHOR_EMAIL="[email protected]"
export REMOTE_REPO_URL=""
export PACKAGE_NAME="core"
export MAIN_CLASS_NAME="App"
# Analyse des arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--project)
PROJECT_NAME="$2"
shift 2
;;
-j|--java)
JAVA_VERSION="$2"
shift 2
;;
-a|--author-name)
GIT_AUTHOR_NAME="$2"
shift 2
;;
-e|--author-email)
GIT_AUTHOR_EMAIL="$2"
shift 2
;;
-r|--remote)
REMOTE_REPO_URL="$2"
shift 2
;;
-pk|--package)
PACKAGE_NAME="$2"
shift 2
;;
-f|--frame)
FRAME=true
shift 1
;;
-mc|--main-class)
MAIN_CLASS_NAME="$2"
shift 2
;;
-v|--version)
echo "Script version 1.0"
exit 0
;;
--help)
usage
;;
*)
usage
;;
esac
done
# Création du projet
mkdir -p "${PROJECT_NAME}"/{src/{{main,test}/{java,resources},docs},libs}
echo """
# README
This project is ${PROJECT_NAME}.
by ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>
""" > "${PROJECT_NAME}"/README.md
echo "target/" > "${PROJECT_NAME}"/.gitignore
echo "java=${JAVA_VERSION}" > "${PROJECT_NAME}"/.sdkmanrc
curl -sL https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.12.2/junit-platform-console-standalone-1.12.2.jar > "${PROJECT_NAME}"/libs/junit-platform-console-standalone-1.12.2.jar
curl -sL https://gist.githubusercontent.com/mcgivrer/b95bf476b8494c180c0a386a5ae11264/raw/0618c582240ec4972fad844c884d28df086ab674/build > "${PROJECT_NAME}"/build
# integrate VSCode support
mkdir -p "${PROJECT_NAME}/.vscode";\
cat <<EOL > "${PROJECT_NAME}/.vscode/settings.json"
{
"java.format.settings.url": ".vscode/java-formatter.xml",
"java.project.sourcePaths": [
"src\\main\\java",
"src\\main\\resources",
"src\\test\\java",
"src\\test\\resources"
],
"java.project.encoding": "warning",
"java.project.referencedLibraries": [
"libs\\junit-platform-console-standalone-1.12.2.jar"
],
"java.project.outputPath": "target\\classes"
}
EOL
cat <<EOL > "${PROJECT_NAME}/.vscode/launch.json"
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Current File",
"request": "launch",
"mainClass": "${file}"
},
{
"type": "java",
"name": "App",
"request": "launch",
"mainClass": "App",
"projectName": "${PROJECT_NAME}_53c24221"
}
]
}
EOL
# create documentation
cat <<EOL > "${PROJECT_NAME}"/src/docs/00-preface.md
# Project ${PROJECT_NAME}
## Preface
This is the preface for the documentation of the project ${PROJECT_NAME}.
You must rewrite this document according to your specifications.
>[!NOTE]
>Please us the markdown text format to easily create
>docs in multiple format with the 'pandoc' tool.
${GIT_AUTHOR_NAME} ${GIT_AUTHOR_EMAIL}
EOL
# create configuration file
cat <<EOL > "${PROJECT_NAME}"/src/main/resources/config.properties
app.debug=0
app.mode=DEVELOPMENT
EOL
# create translation file
mkdir -p "${PROJECT_NAME}"/src/main/resources/i18n
cat <<EOL > "${PROJECT_NAME}"/src/main/resources/i18n/messages.properties
app.name=${PROJECT_NAME}
app.version=1.0.0
EOL
# create a basic application class into a core package.
mkdir -p "${PROJECT_NAME}"/src/main/java/${PACKAGE_NAME//.//}
# Générer le fichier en utilisant envsubst
if [[ -n "${FRAME}" ]]; then
envsubst < ~/scripts/templates/App_Frame_template.java > "${PROJECT_NAME}"/src/main/java/${PACKAGE_NAME//.//}/${MAIN_CLASS_NAME}.java
else
envsubst < ~/scripts/templates/App_template.java > "${PROJECT_NAME}"/src/main/java/${PACKAGE_NAME//.//}/${MAIN_CLASS_NAME}.java
fi
## create git repository
git init -b main --quiet "${PROJECT_NAME}"/
git -C "${PROJECT_NAME}" config user.name "${GIT_AUTHOR_NAME}"
git -C "${PROJECT_NAME}" config user.email "${GIT_AUTHOR_EMAIL}"
git -C "${PROJECT_NAME}" add .
git -C "${PROJECT_NAME}" commit -m "Create Project ${PROJECT_NAME}"
# Pousser vers le dépôt distant si l'URL est fournie
if [[ -n "${REMOTE_REPO_URL}" ]]; then
git -C "${PROJECT_NAME}" remote add origin "${REMOTE_REPO_URL}"
git -C "${PROJECT_NAME}" push -f origin main
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment