merge from dev/client

This commit is contained in:
Cedric Beck
2024-11-17 10:57:05 +01:00
64 changed files with 1371 additions and 206 deletions

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="MdgaApp" type="Application" factoryName="Application" singleton="false" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pp.mdga.client.MdgaApp" />
<module name="Projekte.mdga.client.main" />
<option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties" />
<option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties -ea" />
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$" />
<extension name="coverage">
<pattern>
@@ -14,4 +14,4 @@
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
</component>

View File

@@ -0,0 +1,290 @@
package pp.mdga.client.Acoustic;
import com.jme3.system.NanoTimer;
import pp.mdga.client.MdgaApp;
import pp.mdga.client.MdgaState;
import java.util.*;
public class AcousticHandler {
private MdgaApp app;
private MdgaState state = MdgaState.NONE;
private boolean playGame = false;
private ArrayList<MusicAsset> gameTracks = new ArrayList<>();
private NanoTimer trackTimer = new NanoTimer();
private boolean fading = false;
private NanoTimer fadeTimer = new NanoTimer();
private final float FADE_DURATION = 3.0f;
private final float CROSSFADE_DURATION = 1.5f;
private float mainVolume = 1.0f;
private float musicVolume = 1.0f;
private float soundVolume = 1.0f;
private GameMusic scheduled = null;
private GameMusic playing = null;
private ArrayList<GameSound> sounds = new ArrayList<>();
public AcousticHandler(MdgaApp app) {
this.app = app;
}
/**
* This method updates the acousticHandler and should be called every frame
*/
public void update() {
updateVolumeAndTrack();
if(playGame) {
updateGameTracks();
}
Iterator<GameSound> iterator = sounds.iterator();
while (iterator.hasNext()) {
GameSound s = iterator.next();
s.update(getSoundVolumeTotal());
if (!s.isPlaying()) {
iterator.remove();
}
}
}
/**
* This method instantly plays a sound
*
* @param sound the sound to be played
*/
public void playSound(MdgaSound sound) {
ArrayList<SoundAssetDelayVolume> assets = new ArrayList<SoundAssetDelayVolume>();
switch (sound) {
case LOST:
assets.add(new SoundAssetDelayVolume(SoundAsset.LOST, 1.0f, 0.0f));
break;
case VICTORY:
assets.add(new SoundAssetDelayVolume(SoundAsset.VICTORY, 1.0f, 2.0f));
break;
default:
break;
}
for (SoundAssetDelayVolume sawd : assets) {
GameSound gameSound = new GameSound(app, sawd.asset(), getSoundVolumeTotal(), sawd.subVolume(), sawd.delay());
sounds.add(gameSound);
}
}
/**
* This method fades the played music to fit the state.
*
* @param state the state of which the corresponding music should be played to be played
*/
public void playState(MdgaState state) {
if(this.state == state) {
return;
}
MusicAsset asset = null;
switch (state) {
case MAIN:
playGame = false;
asset = MusicAsset.MAIN_MENU;
break;
case LOBBY:
playGame = false;
asset = MusicAsset.LOBBY;
break;
case GAME:
addGameTracks();
playGame = true;
assert(gameTracks.size() > 0) : "no more game music available";
asset = gameTracks.remove(0);
break;
case CEREMONY:
playGame = false;
asset = MusicAsset.CEREMONY;
break;
}
assert(null != asset) : "music sceduling went wrong";
scheduled = new GameMusic(app, asset, getMusicVolumeTotal(), asset.getSubVolume(), asset.getLoop());
}
/**
* Performs linear interpolation between two float values.
*
* @param start The starting value.
* @param end The ending value.
* @param t The interpolation factor, typically between 0 and 1.
* @return The interpolated value between start and end.
*/
private float lerp(float start, float end, float t) {
return start + t * (end - start);
}
/**
* Updates the current volume and handles track crossfading logic.
* This method is responsible for fading out the currently playing track,
* fading in the scheduled track, and handling crossfade between the two tracks.
*/
private void updateVolumeAndTrack() {
if (playing == null && scheduled != null && !fading) {
playing = scheduled;
scheduled = null;
playing.play();
return;
}
if (scheduled != null && !fading) {
fading = true;
fadeTimer.reset();
}
if (fading) {
float time = fadeTimer.getTimeInSeconds();
if (time <= FADE_DURATION) {
float t = Math.min(time / FADE_DURATION, 1.0f);
float oldVolume = lerp(1.0f, 0.0f, t);
if (playing != null) {
playing.update(getMusicVolumeTotal()* oldVolume);
}
}
if (time > FADE_DURATION && time <= FADE_DURATION + CROSSFADE_DURATION) {
float t = Math.min((time - FADE_DURATION) / CROSSFADE_DURATION, 1.0f);
float newVolume = lerp(0.0f, 1.0f, t);
if (!scheduled.isPlaying()) {
scheduled.play();
}
scheduled.update(getMusicVolumeTotal() * newVolume);
}
if (time > FADE_DURATION + CROSSFADE_DURATION) {
if (playing != null) {
playing.pause();
}
playing = scheduled;
scheduled = null;
fading = false;
}
} else if (playing != null) {
playing.update(getMusicVolumeTotal());
}
}
/**
* Adds a list of game tracks to the gameTracks collection and shuffles them.
* This method adds predefined game tracks to the track list and shuffles the order.
*/
private void addGameTracks() {
Random random = new Random();
for (int i = 1; i <= 6; i++) {
gameTracks.add(MusicAsset.valueOf("GAME_" + i));
}
Collections.shuffle(gameTracks, random);
}
/**
* Updates the current game tracks. If the currently playing track is nearing its end,
* a new track will be scheduled to play. If the list of game tracks is empty, it will be refreshed.
*/
private void updateGameTracks() {
if(playing.nearEnd(10)) {
if (gameTracks.isEmpty()) {
addGameTracks();
}
}
if (playing != null && playing.nearEnd(3) && trackTimer.getTimeInSeconds() > 20) {
trackTimer.reset();
MusicAsset nextTrack = gameTracks.remove(0);
scheduled = new GameMusic(app, nextTrack, getMusicVolumeTotal(), nextTrack.getSubVolume(), nextTrack.getLoop());
}
}
/**
* Retrieves the main volume level.
*
* @return The current main volume level.
*/
public float getMainVolume() {
return mainVolume;
}
/**
* Retrieves the music volume level.
*
* @return The current music volume level.
*/
public float getMusicVolume() {
return musicVolume;
}
/**
* Retrieves the sound volume level.
*
* @return The current sound volume level.
*/
public float getSoundVolume() {
return soundVolume;
}
/**
* Sets the main volume level.
*
* @param mainVolume The desired main volume level.
*/
public void setMainVolume(float mainVolume) {
this.mainVolume = mainVolume;
}
/**
* Sets the music volume level.
*
* @param musicVolume The desired music volume level.
*/
public void setMusicVolume(float musicVolume) {
this.musicVolume = musicVolume;
}
/**
* Sets the sound volume level.
*
* @param soundVolume The desired sound volume level.
*/
public void setSoundVolume(float soundVolume) {
this.soundVolume = soundVolume;
}
/**
* Calculates the total music volume by multiplying the music volume by the main volume.
*
* @return The total music volume.
*/
float getMusicVolumeTotal() {
return getMusicVolume() * getMainVolume();
}
/**
* Calculates the total sound volume by multiplying the sound volume by the main volume.
*
* @return The total sound volume.
*/
float getSoundVolumeTotal() {
return getSoundVolume() * getMainVolume();
}
}

View File

@@ -0,0 +1,110 @@
package pp.mdga.client.Acoustic;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource;
import pp.mdga.client.MdgaApp;
/**
* Represents a game music track, including its playback controls and volume settings.
* This class manages the playback of a music track, allowing for playing, pausing,
* volume adjustment, and tracking the current status of the music.
*/
class GameMusic {
private float volume;
private final float subVolume;
private final AudioNode music;
/**
* Constructs a new GameMusic object.
*
* @param app The instance of the application, used to access the asset manager.
* @param asset The music asset to be played.
* @param volume The total volume of the music, adjusted by the main volume.
* @param subVolume A relative volume that modifies the base music volume, typically a percentage.
* @param loop A flag indicating whether the music should loop once it finishes.
*/
GameMusic(MdgaApp app, MusicAsset asset, float volume, float subVolume, boolean loop) {
this.volume = volume;
this.subVolume = subVolume;
music = new AudioNode(app.getAssetManager(), asset.getPath(), AudioData.DataType.Stream);
music.setPositional(false);
music.setDirectional(false);
music.setVolume(volume * subVolume);
music.setLooping(loop);
}
/**
* Plays the current music track.
* If the music is already initialized, it starts playback.
* If the music is not available, no action is performed.
*/
void play() {
if(null == music) {
return;
}
music.play();
}
/**
* Pauses the current music track.
* If the music is not available or is not playing, no action is performed.
*/
void pause() {
if(null == music) {
return;
}
music.stop();
}
/**
* Checks if the current music track is playing.
*
* @return true if the music is playing, false otherwise.
*/
boolean isPlaying() {
return music.getStatus() == AudioSource.Status.Playing;
}
/**
* Checks if the current music track is near the end.
*
* @param thresholdSeconds The threshold in seconds. If the remaining time is less than or equal to this value,
* the track is considered near the end.
* @return true if the track is near its end (within the threshold), false otherwise.
*/
boolean nearEnd(float thresholdSeconds) {
if (music == null || !isPlaying()) {
return false;
}
float currentTime = music.getPlaybackTime(); // Current playback time in seconds
float duration = music.getAudioData().getDuration(); // Total duration in seconds
if (duration <= 0) {
return false;
}
float remainingTime = duration - currentTime;
return remainingTime <= thresholdSeconds;
}
/**
* Updates the volume of the music.
* If the volume has changed, it will adjust the music's volume accordingly.
*
* @param newVolume The new total volume for the music.
*/
void update(float newVolume) {
if(volume != newVolume) {
volume = newVolume;
music.setVolume(volume * subVolume);
}
}
}

View File

@@ -0,0 +1,83 @@
package pp.mdga.client.Acoustic;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource;
import com.jme3.system.NanoTimer;
import pp.mdga.client.MdgaApp;
/**
* Represents a game sound effect, with control over playback, volume, and timing.
* This class manages the playback of a sound effect, including starting playback after a delay,
* adjusting volume, and tracking whether the sound has finished playing.
*/
class GameSound {
private float volume;
final private float subVolume;
private final AudioNode sound;
private boolean playing = false;
private boolean finished = false;
private float delay = 0.0f;
private NanoTimer timer = null;
/**
* Constructs a new GameSound object.
*
* @param app The instance of the application, used to access the asset manager.
* @param asset The sound asset to be played.
* @param volume The total volume of the sound, adjusted by the main volume.
* @param subVolume A relative volume that modifies the base sound volume, typically a percentage.
* @param delay The delay before the sound starts playing, in seconds.
*/
GameSound(MdgaApp app, SoundAsset asset, float volume, float subVolume, float delay) {
this.volume = volume;
this.subVolume = subVolume;
this.delay = delay;
sound = new AudioNode(app.getAssetManager(), asset.getPath(), AudioData.DataType.Buffer);
sound.setPositional(false);
sound.setDirectional(false);
sound.setLooping(false);
sound.setVolume(volume * subVolume);
timer = new NanoTimer();
}
/**
* Checks if the sound is currently playing.
*
* @return true if the sound is playing, false otherwise.
*/
boolean isPlaying() {
return !finished;
}
/**
* Updates the sound playback, adjusting the volume if necessary, and starting
* the sound after the specified delay.
*
* @param newVolume The new total volume for the sound.
*/
void update(float newVolume) {
if(!playing && timer.getTimeInSeconds() > delay) {
sound.play();
playing = true;
}
if(!playing) {
return;
}
if(volume != newVolume) {
volume = newVolume;
sound.setVolume(volume * subVolume);
}
if(sound != null && sound.getStatus() == AudioSource.Status.Playing) {
finished = true;
}
}
}

View File

@@ -0,0 +1,23 @@
package pp.mdga.client.Acoustic;
/**
* Enum representing the various sound effects used in the game.
* Each sound corresponds to an event or action in the game and may consist of one or more
* audio files, potentially with time delays between them.
*
* These sounds are used to play specific audio cues, such as when a dice is rolled,
* a turn starts or ends, a piece is moved or lost, and various other events in the game.
*/
public enum MdgaSound {
DICE_ROLL,
TURN_START,
TURN_END,
PIECE_END,
PIECE_MOVE,
PIECE_LOST,
SELECT,
DESELECT,
HURRY,
VICTORY,
LOST;
}

View File

@@ -0,0 +1,76 @@
package pp.mdga.client.Acoustic;
/**
* Enum representing various music assets used in the game.
* Each constant corresponds to a specific music track, along with its properties such as file path,
* looping behavior, and relative volume (subVolume).
* These music assets are used to control the music that plays in different parts of the game, such as menus and in-game music.
*/
enum MusicAsset {
MAIN_MENU("Spaceship.wav", 1.0f),
LOBBY("DeadPlanet.wav", 1.0f),
CEREMONY("80s,Disco,Life.wav", 1.0f),
GAME_1("NeonRoadTrip.wav", false, 1.0f),
GAME_2("NoPressureTrance.wav", false, 1.0f),
GAME_3("TheSynthRave.wav", false, 1.0f),
GAME_4("LaserParty.wav", false, 1.0f),
GAME_5("RetroNoir.wav", false, 1.0f),
GAME_6("SpaceInvaders.wav", false, 1.0f);
private final String path;
private final boolean loop;
private final float subVolume;
/**
* Constructs a new MusicAsset object with the specified name and sub-volume.
* The track will not loop by default.
*
* @param name The name of the music file.
* @param subVolume A relative volume that modifies the base volume of the track (typically a percentage).
*/
MusicAsset(String name, float subVolume) {
this.path = "music/" + name;
this.loop = false;
this.subVolume = subVolume;
}
/**
* Constructs a new MusicAsset object with the specified name, loop flag, and sub-volume.
*
* @param name The name of the music file.
* @param loop If true, the track will loop; otherwise, it will play once.
* @param subVolume A relative volume that modifies the base volume of the track (typically a percentage).
*/
MusicAsset(String name, boolean loop, float subVolume) {
this.path = "music/" + name;
this.loop = loop;
this.subVolume = subVolume;
}
/**
* Gets the file path of the music track.
*
* @return The path to the music file (relative to the music folder).
*/
public String getPath() {
return path;
}
/**
* Gets whether the music track should loop.
*
* @return true if the track should loop, false otherwise.
*/
public boolean getLoop() {
return loop;
}
/**
* Gets the relative volume (subVolume) for the music track.
*
* @return The relative volume for the track, typically a value between 0.0 and 1.0.
*/
public float getSubVolume() {
return subVolume;
}
}

View File

@@ -0,0 +1,41 @@
package pp.mdga.client.Acoustic;
/**
* Enum representing various sound assets used in the game.
* Each constant corresponds to a specific sound effect used throughout the game.
* These sounds are associated with various events and actions, such as dice rolls,
* turn changes, piece movements, and game outcomes.
*/
enum SoundAsset {
DICE_ROLL(""),
TURN_START(""),
TURN_END(""),
PIECE_END(""),
PIECE_MOVE(""),
PIECE_LOST(""),
SELECT(""),
DESELECT(""),
HURRY(""),
VICTORY("LevelUp2.wav"),
LOST("GameOver.wav");
private final String path;
/**
* Constructs a new SoundAsset object with the specified name.
*
* @param name The name of the sound file.
*/
SoundAsset(String name) {
this.path = "sound/" + name;
}
/**
* Gets the file path of the sound effect.
*
* @return The path to the sound file (relative to the sound folder).
*/
public String getPath() {
return path;
}
}

View File

@@ -0,0 +1,7 @@
package pp.mdga.client.Acoustic;
/**
* A record that encapsulates a sound asset along with its playback settings:
* the relative volume (subVolume) and a delay before it starts playing.
*/
record SoundAssetDelayVolume(SoundAsset asset, float subVolume, float delay) { }

View File

@@ -1,5 +0,0 @@
package pp.mdga.client;
public class ActionSynchronizer {
}

View File

@@ -11,7 +11,7 @@ public AnimationHandler(MdgaApp app) {
this.app = app;
}
public void playAnimation(AnimationType type) {
public void playAnimation(MdgaAnimation type) {
}
@@ -23,7 +23,7 @@ public void update() {
if(animation.isOver()) {
animation = null;
//trigger next state
//trigger next state in model
}
}
}

View File

@@ -1,4 +1,4 @@
package pp.mdga.client.Animation;
public enum AnimationType {
public enum MdgaAnimation {
}

View File

@@ -1,3 +1,3 @@
package pp.mdga.client.Board;
public record AssetOnMap(Asset asset, int x, int y, float rot){}
record AssetOnMap(BoardAsset asset, int x, int y, float rot){}

View File

@@ -1,7 +1,7 @@
package pp.mdga.client.Board;
public enum Asset {
bigTent(0.57f),
enum BoardAsset {
bigTent,
cardStack,
cir,
heer,
@@ -17,7 +17,7 @@ public enum Asset {
node_bonus("./node_normal/node_normal.j3o", "./node_normal/node_bonus_diff.png"),
radar,
shieldCard,
ship(0.9f),
ship,
smallTent,
swapCard,
tank,
@@ -28,20 +28,20 @@ public enum Asset {
private final String diffPath;
private final float size;
Asset(){
BoardAsset(){
String folderFileName = "./" + name() + "/" + name();
this.modelPath = folderFileName + ".j3o";
this.diffPath = folderFileName + "_diff.png";
this.size = 1f;
}
Asset(String modelPath, String diffPath){
BoardAsset(String modelPath, String diffPath){
this.modelPath = modelPath;
this.diffPath = diffPath;
this.size = 1f;
}
Asset(float size){
BoardAsset(float size){
String folderFileName = "./" + name() + "/" + name();
this.modelPath = folderFileName + ".j3o";
this.diffPath = folderFileName + "_diff.png";

View File

@@ -7,37 +7,32 @@
import com.jme3.scene.control.AbstractControl;
import pp.mdga.client.MdgaApp;
import pp.mdga.game.Color;
import pp.mdga.game.Node;
import java.awt.geom.Point2D;
import java.util.*;
public class BoardView {
private static final float GRID_SIZE = 1.72f;
private static final float GRID_ELEVATION = 0.0f;
private static final int GRID_EXTEND = 5;
private static final String MAP_NAME = "map.mdga";
private final MdgaApp mdgaApp;
private final MdgaApp app;
private PileControl drawPile = new PileControl();
private PileControl discardPile = new PileControl();
private PileControl drawPile = null;
private PileControl discardPile = null;
private ArrayList<NodeControl> infield = new ArrayList<>(40);
private Map<UUID, PieceControl> pieces;
private Map<Color, List<AssetOnMap>> playerMap;
public BoardView(int playerCount, MdgaApp mdgaApp) {
assert(2 <= playerCount && playerCount <= 4);
assert(mdgaApp != null);
public BoardView(MdgaApp app) {
assert(app != null) : "app is null";
this.mdgaApp = mdgaApp;
this.app = app;
this.pieces = new HashMap<>();
this.playerMap = new HashMap<>();
initMap();
}
@@ -45,7 +40,7 @@ private void addFigureToPlayerMap(Color col, AssetOnMap assetOnMap){
List<AssetOnMap> inMap = playerMap.getOrDefault(col, new ArrayList<>());
inMap.add(assetOnMap);
if(inMap.size() > 4) throw new RuntimeException("to many assets for one player");
assert(inMap.size() <= 4) : "to many assets for one player";
playerMap.put(col, inMap);
}
@@ -55,17 +50,17 @@ private void initMap() {
for (AssetOnMap assetOnMap : assetOnMaps){
switch (assetOnMap.asset()){
case lw -> addFigureToPlayerMap(assetToColor(Asset.lw), assetOnMap);
case heer -> addFigureToPlayerMap(assetToColor(Asset.heer), assetOnMap);
case cir -> addFigureToPlayerMap(assetToColor(Asset.cir), assetOnMap);
case marine -> addFigureToPlayerMap(assetToColor(Asset.marine), assetOnMap);
case node_normal, node_bonus, node_start -> displayAndControl(assetOnMap, new NodeControl());
case lw -> addFigureToPlayerMap(assetToColor(BoardAsset.lw), assetOnMap);
case heer -> addFigureToPlayerMap(assetToColor(BoardAsset.heer), assetOnMap);
case cir -> addFigureToPlayerMap(assetToColor(BoardAsset.cir), assetOnMap);
case marine -> addFigureToPlayerMap(assetToColor(BoardAsset.marine), assetOnMap);
case node_normal, node_bonus, node_start -> infield.addLast(displayAndControl(assetOnMap, new NodeControl()));
default -> displayAsset(assetOnMap);
}
}
}
private Color assetToColor(Asset asset){
private Color assetToColor(BoardAsset asset){
return switch(asset){
case lw -> Color.AIRFORCE;
case heer -> Color.ARMY;
@@ -75,31 +70,28 @@ private Color assetToColor(Asset asset){
};
}
private Spatial createModel(Asset asset, Vector3f pos, float rot){
private Spatial createModel(BoardAsset asset, Vector3f pos, float rot){
String modelName = asset.getModelPath();
String texName = asset.getDiffPath();
Spatial model = mdgaApp.getAssetManager().loadModel(modelName);
Spatial model = app.getAssetManager().loadModel(modelName);
model.scale(asset.getSize());
model.rotate((float) Math.toRadians(0), 0, (float) Math.toRadians(90 + rot));
model.setLocalTranslation(pos);
model.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
Material mat = new Material(mdgaApp.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
mat.setTexture("DiffuseMap", mdgaApp.getAssetManager().loadTexture(texName));
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Light/Lighting.j3md");
mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture(texName));
model.setMaterial(mat);
mdgaApp.getRootNode().attachChild(model);
app.getRootNode().attachChild(model);
return model;
}
private static Vector3f gridToWorld(int x, int y) {
assert(-GRID_EXTEND <= x && x <= GRID_EXTEND);
assert(-GRID_EXTEND <= y && y < GRID_EXTEND);
return new Vector3f(GRID_SIZE * x, GRID_SIZE * y, GRID_ELEVATION);
}
public void addPlayer(Color color, UUID uuid){
List<AssetOnMap> playerAssets = playerMap.get(color);
if (playerAssets == null) throw new RuntimeException("Assets for Player color are not defined");
assert(playerAssets != null) : "Assets for Player color are not defined";
for (AssetOnMap assetOnMap : playerAssets){
pieces.put(uuid, displayAndControl(assetOnMap, new PieceControl()));

View File

@@ -27,19 +27,22 @@ public static List<AssetOnMap> loadMap(String mapName) {
if(entry.charAt(0) == '#') continue;
String[] parts = entry.trim().split(" ");
assert(parts.length == 3);
if(parts.length != 3){
parts = parts;
}
assert(parts.length == 3) : "MapLoader: line has not 3 parts";
String assetName = parts[0];
String[] coordinates = parts[1].split(",");
assert(coordinates.length == 2);
assert(coordinates.length == 2) : "MapLoade: coordinates are wrong";
int x = Integer.parseInt(coordinates[0]);
int y = Integer.parseInt(coordinates[1]);
float rot = Float.parseFloat(parts[2]);
Asset asset = getLoadedAsset(assetName);
BoardAsset asset = getLoadedAsset(assetName);
assetsOnMap.add(new AssetOnMap(asset, x, y, rot));
}
} catch (IOException e) {
@@ -51,26 +54,26 @@ public static List<AssetOnMap> loadMap(String mapName) {
return assetsOnMap;
}
private static Asset getLoadedAsset(String assetName) {
private static BoardAsset getLoadedAsset(String assetName) {
return switch(assetName){
case "lw" -> Asset.lw;
case "cir" -> Asset.cir;
case "marine" -> Asset.marine;
case "heer" -> Asset.heer;
case "node" -> Asset.node_normal;
case "node_start" -> Asset.node_start;
case "node_bonus" -> Asset.node_bonus;
case "node_home_blue" -> Asset.node_home_blue;
case "node_home_yellow" -> Asset.node_home_yellow;
case "node_home_black" -> Asset.node_home_black;
case "node_home_green" -> Asset.node_home_green;
case "world" -> Asset.world;
case "jet" -> Asset.jet;
case "big_tent" -> Asset.bigTent;
case "small_tent" -> Asset.smallTent;
case "radar" -> Asset.radar;
case "ship" -> Asset.ship;
case "tank" -> Asset.tank;
case "lw" -> BoardAsset.lw;
case "cir" -> BoardAsset.cir;
case "marine" -> BoardAsset.marine;
case "heer" -> BoardAsset.heer;
case "node" -> BoardAsset.node_normal;
case "node_start" -> BoardAsset.node_start;
case "node_bonus" -> BoardAsset.node_bonus;
case "node_home_blue" -> BoardAsset.node_home_blue;
case "node_home_yellow" -> BoardAsset.node_home_yellow;
case "node_home_black" -> BoardAsset.node_home_black;
case "node_home_green" -> BoardAsset.node_home_green;
case "world" -> BoardAsset.world;
case "jet" -> BoardAsset.jet;
case "big_tent" -> BoardAsset.bigTent;
case "small_tent" -> BoardAsset.smallTent;
case "radar" -> BoardAsset.radar;
case "ship" -> BoardAsset.ship;
case "tank" -> BoardAsset.tank;
default -> throw new IllegalStateException("Unexpected value: " + assetName);
};
}

View File

@@ -1,5 +1,5 @@
package pp.mdga.client.Board;
public class PileControl {
class PileControl {
}

View File

@@ -1,5 +1,26 @@
package pp.mdga.client.Dialog;
public class DialogView {
import pp.dialog.DialogManager;
import pp.mdga.client.MdgaApp;
public class DialogView {
private MdgaApp app;
private DialogManager dialogManager = new DialogManager(app);
private StartDialog dialog;
public DialogView(MdgaApp app) {
this.app = app;
}
DialogManager getDialogManager() {
return dialogManager;
}
public void mainMenu() {
//dialogManager = new DialogManager(app);
//di
//MainMenuDialog mainMenuDialog = new MainMenuDialog(app);
}
}

View File

@@ -0,0 +1,4 @@
package pp.mdga.client.Dialog;
public class InterruptDialog {
}

View File

@@ -1,5 +1,35 @@
package pp.mdga.client.Dialog;
public class StartDialog {
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog;
import pp.mdga.client.MdgaApp;
public class StartDialog extends SimpleDialog {
StartDialog(MdgaApp app) {
super(app.getDialogView().getDialogManager());
Checkbox serverHost = new Checkbox("sdgfsdg");
serverHost.setChecked(false);
//serverHost.addClickCommands(s -> toggleServerHost());
final Container input = new Container(new SpringGridLayout());
input.addChild(new Label("sdgsgsdg"));
//input.addChild(host, 1);
input.addChild(new Label("sdfdsgsdgsdg"));
//input.addChild(port, 1);
input.addChild(serverHost);
DialogBuilder.simple(app.getDialogView().getDialogManager())
.setTitle("server.dialog")
.setOkButton("button.connect")
.setNoButton("button.cancel")
.setOkClose(false)
.setNoClose(false)
.build(this);
}
}

View File

@@ -1,36 +1,31 @@
package pp.mdga.client;
import com.jme3.app.SimpleApplication;
import com.jme3.system.NanoTimer;
import pp.mdga.client.Acoustic.AcousticHandler;
import pp.mdga.client.Acoustic.MdgaSound;
import pp.mdga.client.Animation.AnimationHandler;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Spatial;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.system.AppSettings;
import pp.mdga.client.Board.Asset;
import pp.mdga.client.Board.AssetOnMap;
import pp.mdga.client.Board.BoardView;
import pp.mdga.client.Board.MapLoader;
import pp.mdga.game.Color;
import java.util.List;
import java.util.UUID;
import pp.mdga.client.Dialog.DialogView;
public class MdgaApp extends SimpleApplication {
private AnimationHandler animationHandler;
private AcousticHandler acousticHandler;
private BoardView boardView;
private DialogView dialogView;
NanoTimer test = new NanoTimer();
private MdgaState testState = MdgaState.MAIN;
public static void main(String[] args) {
MdgaApp app = new MdgaApp();
AppSettings settings = new AppSettings(true);
settings.setSamples(128);
settings.setCenterWindow(true);
settings.setWidth(1300);
settings.setHeight(1000);
settings.setWidth(1280);
settings.setHeight(720);
MdgaApp app = new MdgaApp();
app.setSettings(settings);
app.setShowSettings(false);
app.start();
@@ -39,38 +34,52 @@ public static void main(String[] args) {
@Override
public void simpleInitApp() {
animationHandler = new AnimationHandler(this);
boardView = new BoardView(4,this);
//display all pieces
boardView.addPlayer(Color.NAVY, UUID.randomUUID());
boardView.addPlayer(Color.AIRFORCE, UUID.randomUUID());
boardView.addPlayer(Color.ARMY, UUID.randomUUID());
boardView.addPlayer(Color.CYBER, UUID.randomUUID());
flyCam.setEnabled(true);
int zoom = 40;
cam.setLocation(new Vector3f(-zoom,0,zoom));
cam.lookAt(new Vector3f(0,0,0), new Vector3f(0,0,1));
DirectionalLight sun = new DirectionalLight();
sun.setColor(ColorRGBA.White);
sun.setDirection(new Vector3f(-0.3f,0,-1));
rootNode.addLight(sun);
AmbientLight ambient = new AmbientLight();
ambient.setColor(new ColorRGBA(0.3f,0.3f,0.3f,1));
rootNode.addLight(ambient);
final int SHADOWMAP_SIZE=1024*8;
DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, SHADOWMAP_SIZE, 4);
dlsr.setLight(sun);
viewPort.addProcessor(dlsr);
acousticHandler = new AcousticHandler(this);
boardView = new BoardView(this);
dialogView = new DialogView(this);
//dialogView.mainMenu();
//acousticHandler.playState(MdgaState.GAME);
acousticHandler.playSound(MdgaSound.LOST);
acousticHandler.playSound(MdgaSound.VICTORY);
}
@Override
public void simpleUpdate(float tpf) {
//this method will be called every game tick and can be used to make updates
acousticHandler.update();
//test.reset();
if(test.getTimeInSeconds() > 10){
if(testState == MdgaState.MAIN) {
testState = MdgaState.LOBBY;
acousticHandler.playState(MdgaState.MAIN);
} else if (testState == MdgaState.LOBBY) {
testState = MdgaState.CEREMONY;
acousticHandler.playState(MdgaState.LOBBY);
}
else {
testState = MdgaState.MAIN;
acousticHandler.playState(MdgaState.CEREMONY);
}
test.reset();
}
}
public AnimationHandler getAnimationHandler() {
return animationHandler;
}
public AcousticHandler getAcousticHandler() {
return acousticHandler;
}
public BoardView getBoardView() {
return boardView;
}
public DialogView getDialogView() {
return dialogView;
}
}

View File

@@ -0,0 +1,48 @@
package pp.mdga.client;
import pp.mdga.notification.Notification;
import pp.mdga.notification.PieceInGameNotification;
import pp.mdga.notification.PlayerInGameNotification;
public enum MdgaState {
NONE {
@Override
void handleNotification(MdgaApp app, Notification notification) {
throw new RuntimeException("unexpected notification");
}
},
MAIN {
@Override
void handleNotification(MdgaApp app, Notification notification) {
throw new RuntimeException("unexpected notification");
}
},
LOBBY {
@Override
void handleNotification(MdgaApp app, Notification notification) {
throw new RuntimeException("unexpected notification");
}
},
GAME {
@Override
void handleNotification(MdgaApp app, Notification notification) {
if(notification instanceof PlayerInGameNotification) {
//TODO
}
else if(notification instanceof PieceInGameNotification){
//TODO
}
else {
throw new RuntimeException("unexpected notification");
}
}
},
CEREMONY {
@Override
void handleNotification(MdgaApp app, Notification notification) {
throw new RuntimeException("unexpected notification");
}
};
abstract void handleNotification(MdgaApp app, Notification notification);
}

View File

@@ -0,0 +1,23 @@
package pp.mdga.client;
import pp.mdga.notification.Notification;
import java.util.ArrayList;
public class NotificationSynchronizer {
private final MdgaApp app;
private MdgaState state = MdgaState.MAIN;
NotificationSynchronizer(MdgaApp app) {
this.app = app;
}
void update() {
ArrayList<Notification> notifications = new ArrayList<>();
//TODO fetch model notifications
for (Notification n : notifications) {
state.handleNotification(app, n);
}
}
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class CeremonyState {
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class GameState {
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class LobbyState {
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class MdgaState {
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class MusicState {
}

View File

@@ -1,5 +0,0 @@
package pp.mdga.client.State;
public class SoundState {
}

View File

@@ -0,0 +1,66 @@
world 0,0
# jet 0,0
#Nodes für Map
node_start 8,0
node 8,1
node 8,2
node 7,3
node_bonus 6,4
node 5,5
node 4,6
node 3,7
node 2,8
node 1,8
node_start 0,8
node -1,8
node -2,8
node -3,7
node_bonus -4,6
node -5,5
node -6,4
node -7,3
node -8,2
node -8,1
node_start -8,0
node -8,-1
node -8,-2
node -7,-3
node_bonus -6,-4
node -5,-5
node -4,-6
node -3,-7
node -2,-8
node -1,-8
node_start 0,-8
node 1,-8
node 2,-8
node 3,-7
node_bonus 4,-6
node 5,-5
node 6,-4
node 7,-3
node 8,-2
node 8,-1
#Node Home
node_home_blue 7,0
node_home_blue 6,0
node_home_blue 5,0
node_home_blue 4,0
node_home_black 0,4
node_home_black 0,5
node_home_black 0,6
node_home_black 0,7
node_home_yellow 0,-4
node_home_yellow 0,-5
node_home_yellow 0,-6
node_home_yellow 0,-7
node_home_green -4,0
node_home_green -5,0
node_home_green -6,0
node_home_green -7,0

View File

@@ -93,9 +93,9 @@ node 1,-4 0
node 1,-5 0
node 0,-5 0
node_start -1,-5 0
node -1,-4 0 0
node -1,-3 0 0
node -1,-2 0 0
node -1,-4 0
node -1,-3 0
node -1,-2 0
node_bonus -1,-1 0
node -2,-1 0
node -3,-1 0

View File

@@ -0,0 +1,22 @@
# Credit
## Free SynthWave Music Pack
See "Free SynthWave Music Pack License.pdf" for LICENCE information.
- 80s,Dico,Live.wav
- LaserParty.wav
- NeonRoadTrip.wav
- NoPressureTrance.wav
- RetroNoir.wav
- SpaceInvaders.wav
- TheSynthRave.SynthWave
## Short Loopable Background Music
https://joshuuu.itch.io/short-loopable-background-music
Free for personal or commercial use as long as you don't redistribute as your own. Donations also welcome! Please add jhaeka to your credits. :)
- DeadPlanet.wav
- Spaceship.wav

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

View File

@@ -1,4 +0,0 @@
package pp.mdga.effect;
public interface Effect {
}

View File

@@ -6,7 +6,7 @@
* This class will be used to hold all Board relevant data.
*/
public class Board {
private Map<Color, Player> playerData;
private Map<Color, PlayerData> playerData;
private Node[] infield;
/**
@@ -16,7 +16,12 @@ public Board() {
infield = new Node[40];
for (int i = 0; i < 40; i++) {
if (i % 10 == 0) {
infield[i] = new StartNode();
infield[i] = new StartNode(
i == 0 ? Color.ARMY :
i == 10 ? Color.AIRFORCE :
i == 20 ? Color.CYBER :
Color.NAVY
);
} else if (i == 4 || i == 14 || i == 24 || i == 34) {
infield[i] = new BonusNode();
} else {
@@ -30,7 +35,7 @@ public Board() {
*
* @return the playerData
*/
public Map<Color, Player> getPlayerData() {
public Map<Color, PlayerData> getPlayerData() {
return playerData;
}

View File

@@ -1,34 +0,0 @@
package pp.mdga.game;
/**
* This class is used to create a card
*/
public class Card {
private BonusCard type;
/**
* This constructor is used to create a new card
*
* @param type the type of the card
*/
public Card(BonusCard type) {
this.type = type;
}
/**
* This method is used to get the type of the card
*
* @return the type of the card
*/
public BonusCard getType() {
return type;
}
/**
* This method is used to set the type of the card
*
* @param type the new type of the card
*/
public void setType(BonusCard type) {
this.type = type;
}
}

View File

@@ -5,15 +5,17 @@
import java.util.Map;
/**
* This class will be used to handle the data stored in the model
* The Game class represents the game state of the Ludo game.
* It contains all the information needed to play the game.
* The game state is updated by the game logic.
*/
public class Game {
private int diceModifier = 1;
private int diceEyes;
private Map <Color, Player> players;
private Statistic gameStatistics;
private ArrayList<Card> drawPile;
private ArrayList<Card> discardPile = new ArrayList<>();
private ArrayList<BonusCard> drawPile;
private ArrayList<BonusCard> discardPile = new ArrayList<>();
private Board board;
private Color activeColor;
private LinkedList<Color> order;
@@ -22,16 +24,238 @@ public class Game {
private static final int AMOUNT_OF_TURBO_CARDS = 16;
private static final int AMOUNT_OF_SHIELD_AND_SWAP_CARDS = 12;
/**
* This constructor creates a new Game object.
*/
public Game(){
gameStatistics = new Statistic();
drawPile = new ArrayList<>();
for (int i = 0; i < AMOUNT_OF_TURBO_CARDS; i++) {
drawPile.add(new Card(BonusCard.TURBO));
drawPile.add(BonusCard.TURBO);
}
for (int i = 0; i < AMOUNT_OF_SHIELD_AND_SWAP_CARDS; i++) {
drawPile.add(new Card(BonusCard.SHIELD));
drawPile.add(new Card(BonusCard.SWAP));
drawPile.add(BonusCard.SWAP);
drawPile.add(BonusCard.SHIELD);
}
board = new Board();
}
/**
* This method returns the dice modifier.
*
* @return the dice modifier
*/
public int getDiceModifier() {
return diceModifier;
}
/**
* This method sets the dice modifier.
*
* @param diceModifier the new dice modifier
*/
public void setDiceModifier(int diceModifier) {
this.diceModifier = diceModifier;
}
/**
* This method returns the dice eyes.
*
* @return the dice eyes
*/
public int getDiceEyes() {
return diceEyes;
}
/**
* This method sets the dice eyes.
*
* @param diceEyes the new dice eyes
*/
public void setDiceEyes(int diceEyes) {
this.diceEyes = diceEyes;
}
/**
* This method returns the players.
*
* @return the players
*/
public Map<Color, Player> getPlayers() {
return players;
}
/**
* This method sets the players.
*
* @param players the new players
*/
public void setPlayers(Map<Color, Player> players) {
this.players = players;
}
/**
* This method returns the game statistics.
*
* @return the game statistics
*/
public Statistic getGameStatistics() {
return gameStatistics;
}
/**
* This method sets the game statistics.
*
* @param gameStatistics the new game statistics
*/
public void setGameStatistics(Statistic gameStatistics) {
this.gameStatistics = gameStatistics;
}
/**
* This method returns the draw pile.
*
* @return the draw pile
*/
public ArrayList<BonusCard> getDrawPile() {
return drawPile;
}
/**
* This method sets the draw pile.
*
* @param drawPile the new draw pile
*/
public void setDrawPile(ArrayList<BonusCard> drawPile) {
this.drawPile = drawPile;
}
/**
* This method returns the discard pile.
*
* @return the discard pile
*/
public ArrayList<BonusCard> getDiscardPile() {
return discardPile;
}
/**
* This method sets the discard pile.
*
* @param discardPile the new discard pile
*/
public void setDiscardPile(ArrayList<BonusCard> discardPile) {
this.discardPile = discardPile;
}
/**
* This method returns the board.
*
* @return the board
*/
public Board getBoard() {
return board;
}
/**
* This method sets the board.
*
* @param board the new board
*/
public void setBoard(Board board) {
this.board = board;
}
/**
* This method returns the active color.
*
* @return the active color
*/
public Color getActiveColor() {
return activeColor;
}
/**
* This method sets the active color.
*
* @param activeColor the new active color
*/
public void setActiveColor(Color activeColor) {
this.activeColor = activeColor;
}
/**
* This method returns the order of the players.
*
* @return the order of the players
*/
public LinkedList<Color> getOrder() {
return order;
}
/**
* This method sets the order of the players.
*
* @param order the new order of the players
*/
public void setOrder(LinkedList<Color> order) {
this.order = order;
}
/**
* This method returns the player connection ID.
*
* @return the player connection ID
*/
public Map<Color, Integer> getPlayerConnectionID() {
return playerConnectionID;
}
/**
* This method sets the player connection ID.
*
* @param playerConnectionID the new player connection ID
*/
public void setPlayerConnectionID(Map<Color, Integer> playerConnectionID) {
this.playerConnectionID = playerConnectionID;
}
/**
* This method sets the player connection ID.
*
* @param color the color of the player
* @param connectionID the new connection ID
*/
public void setPlayerConnectionID(Color color, int connectionID) {
playerConnectionID.put(color, connectionID);
}
/**
* This method returns the player connection ID.
*
* @param color the color of the player
* @return the player connection ID
*/
public int getPlayerConnectionID(Color color) {
return playerConnectionID.get(color);
}
/**
* This method adds a player to the game.
*
* @param color the color of the player
* @param player the player to be added
*/
public void addPlayer(Color color, Player player) {
players.put(color, player);
}
/**
* This method removes a player from the game.
*
* @param color the color of the player
*/
public void removePlayer(Color color) {
players.remove(color);
}
}

View File

@@ -1,36 +1,75 @@
package pp.mdga.game;
/**
* This class will be used to hold all Piece relevant data.
*/
public class Piece {
private ShieldState shield;
private PieceState state;
private Color color;
/**
* This constructor is used to create a new Piece
*
* @param color the color of the piece
* @param state the state of the piece
*/
public Piece(Color color, PieceState state) {
this.color = color;
this.state = state;
shield = ShieldState.NONE;
}
/**
* This method is used to get the color of the piece
*
* @return the color of the piece
*/
public void setShield(ShieldState shield) {
this.shield = shield;
}
/**
* This method is used to get the color of the piece
*
* @return the color of the piece
*/
public ShieldState getShield() {
return shield;
}
/**
* This method is used to get the color of the piece
*
* @param state the state of the piece
*/
public void setState(PieceState state) {
this.state = state;
}
/**
* This method is used to get the color of the piece
*
* @return the color of the piece
*/
public PieceState getState() {
return state;
}
/**
* This method is used to get the color of the piece
*
* @return the color of the piece
*/
public boolean isShielded() {
return shield == ShieldState.ACTIVE;
}
/**
* This method is used to get the color of the piece
*
* @return the color of the piece
*/
public boolean isSuppressed() {
return shield == ShieldState.SUPPRESSED;
}

View File

@@ -9,7 +9,7 @@ public class Player {
private String name;
private Statistic playerStatistic;
private ArrayList<Card> handCards;
private ArrayList<BonusCard> handCards;
private final int id;
/**
@@ -53,17 +53,17 @@ public Statistic getPlayerStatistic() {
*
* @return the handCards of the player
*/
public ArrayList<Card> getHandCards() {
public ArrayList<BonusCard> getHandCards() {
return handCards;
}
/**
* This method adds a new handCard to the player
*
* @param cards the card to be added to the players hand
* @param card the card to be added to the players hand
*/
public void addHandCards(Card cards){
handCards.add(cards);
public void addHandCards(BonusCard card){
handCards.add(card);
}
/**
@@ -72,8 +72,8 @@ public void addHandCards(Card cards){
* @param card the cards type to be removed
* @return the removed card or null if there is none of that card type
*/
public Card removeHandCard(Card card) {
Card cardToRemove = handCards.stream().filter(card1 -> card1.getType().equals(card.getType())).findFirst().orElse(null);
public BonusCard removeHandCard(BonusCard card) {
BonusCard cardToRemove = handCards.stream().filter(c -> c.equals(card)).findFirst().orElse(null);
if (cardToRemove != null) {
handCards.remove(cardToRemove);
}

View File

@@ -2,7 +2,11 @@
public class StartNode extends Node {
public StartNode() {}
public StartNode(Color color) {
this.color = color;
}
private Color color;
/**
* This method is used to set a new Occupant
@@ -16,4 +20,22 @@ public void setOccupant(Piece occupant) {
}
this.occupant = occupant;
}
/**
* This method is used to get the color of the node
*
* @return the color of the node
*/
public Color getColor() {
return color;
}
/**
* This method is used to set the color of the node
*
* @param color the new color of the node
*/
public void setColor(Color color) {
this.color = color;
}
}

View File

@@ -3,8 +3,10 @@
import com.jme3.network.AbstractMessage;
public abstract class ClientMessage extends AbstractMessage {
protected ClientMessage() {
super(true);
}
public abstract void accept(ClientInterpreter interpreter, int from);
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class ActivePlayerNotification extends Notification {
ActivePlayerNotification(Color color) {
}
}

View File

@@ -0,0 +1,10 @@
package pp.mdga.notification;
import pp.mdga.game.BonusCard;
import pp.mdga.game.Color;
public class DrawCardNotification extends Notification {
DrawCardNotification(Color color, BonusCard card) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class InterruptNotification extends Notification {
InterruptNotification(Color color) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class MovePieceNotification extends Notification {
MovePieceNotification(Color color, int nodeIndex) {
}
}

View File

@@ -0,0 +1,4 @@
package pp.mdga.notification;
public abstract class Notification {
}

View File

@@ -0,0 +1,11 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
import java.util.UUID;
public class PieceInGameNotification extends Notification{
PieceInGameNotification(Color color, UUID id) {
}
}

View File

@@ -0,0 +1,10 @@
package pp.mdga.notification;
import pp.mdga.game.BonusCard;
import pp.mdga.game.Color;
public class PlayCardNotification extends Notification {
PlayCardNotification(Color color, BonusCard card) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class PlayerInGameNotification extends Notification {
PlayerInGameNotification(Color color, String name) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class ResumeNotification extends Notification {
ResumeNotification(Color color) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import pp.mdga.game.Color;
public class RollDiceNotification extends Notification{
RollDiceNotification(Color color, int eyes, int moveNumber) {
}
}

View File

@@ -0,0 +1,9 @@
package pp.mdga.notification;
import java.util.UUID;
public class SwapPieceNotification extends Notification {
SwapPieceNotification(UUID a, UUID b) {
assert(!a.equals(b));
}
}

View File

@@ -32,8 +32,8 @@
}
}
}
include 'mdga:client'
findProject(':mdga:client')?.name = 'client'
include 'mdga:client'
findProject(':mdga:client')?.name = 'client'
include 'mdga:client'
include 'mdga:model'
findProject(':mdga:client')?.name = 'client'