/**
Copyright (C) <2017> <coolAlias>
This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such,
you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package zeldaswordskills.songs;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.util.BlockPos;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.MathHelper;
import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import zeldaswordskills.api.SongAPI;
import zeldaswordskills.api.block.ISongBlock;
import zeldaswordskills.api.entity.ISongEntity;
import zeldaswordskills.block.BlockSongInscription;
import zeldaswordskills.item.ItemInstrument;
import zeldaswordskills.ref.ModInfo;
import zeldaswordskills.util.PlayerUtils;
import zeldaswordskills.util.SongNote;
/**
*
* See the {@link SongAPI} class for instructions on adding new songs.
*
* Note that like Items, songs should not rely on any mutable class members as there
* is only one instance of each song.
*
*/
public abstract class AbstractZeldaSong
{
protected static final Predicate <? super Entity> SELECTOR = new Predicate<Entity>() {
@Override
public boolean apply(Entity entity) {
return entity instanceof ISongEntity;
}
};
/** Maximum effect radius for notifying blocks and entities */
public static final int MAX_SONG_RADIUS = 16;
/** Unique name, preferably lowercase, used to retrieve this song from {@link SongAPI#getSongByName} */
private final String unlocalizedName;
/** Minimum duration song required to play before any effect occurs, in ticks */
private final int minDuration;
/** Notes required to play the song */
private final List<SongNote> notes;
/** Whether the song's main effect is enabled */
protected boolean isEnabled;
/**
* Verifies uniqueness of song name and notes and adds it to the registry.
* @param unlocalizedName See {@link #unlocalizedName}, e.g. 'songname'
* @param minDuration See {@link #minDuration}; measured in ticks
* @param notes Minimum of 3 notes are required
*/
public AbstractZeldaSong(String unlocalizedName, int minDuration, SongNote... notes) {
if (("scarecrow").equals(unlocalizedName)) {
// special case allows Scarecrow Song to register with no notes
} else if (notes == null || notes.length < 3) {
throw new IllegalArgumentException("Songs must be composed of at least 3 notes!");
}
this.unlocalizedName = unlocalizedName;
this.minDuration = minDuration;
this.notes = Collections.unmodifiableList(Arrays.asList(notes));
this.isEnabled = true;
ZeldaSongs.register(this);
}
/**
* Return the translated name of this song. Note that the translation is only correct on the client;
* on the server, send a ChatComponentTranslation using {@link #getTranslationString()} instead.
*/
public String getDisplayName() {
return StatCollector.translateToLocal(getTranslationString());
}
/**
* Returns the string used to translate this song's name
*/
public String getTranslationString() {
return "song.zss." + unlocalizedName + ".name";
}
/**
* Returns the sound file to play for this song
*/
public String getSoundString() {
return ModInfo.ID + ":song." + unlocalizedName;
}
/**
* Whether the 'success' sound should play from the standard GUI when performed correctly
*/
public boolean playSuccessSound() {
return true;
}
/**
* Use to control if player is able to learn this song or not.
* Should return the same result on both server and client.
*/
public boolean canLearn(EntityPlayer player) {
return true;
}
/**
* Whether this song may be learned via Command
*/
public boolean canLearnFromCommand() {
return true;
}
/**
* True if this song can be learned from {@link BlockSongInscription}
*/
public boolean canLearnFromInscription(World world, IBlockState state) {
return true;
}
/**
* If the song has a special effect, handle it here. Called when the song is
* played and both {@link #isEnabled} and {@link #hasEffect} return true.
* This method is only called on the server.
* @param instrument Instrument used to play the song
* @param power Power level of the instrument used to play the song
*/
protected abstract void performEffect(EntityPlayer player, ItemStack instrument, int power);
/**
* Whether the player and instrument playing the song will cause the main effect to occur.
* @param instrument The {@link ItemInstrument} stack used to play the song
* @param power Power level of the instrument used
* @return True to allow {@link #performEffect} to be called
*/
protected boolean hasEffect(EntityPlayer player, ItemStack instrument, int power) {
return true;
}
/**
* Returns the radius within which {@link ISongBlock ISongBlocks} will be notified when the song is performed.
* @param instrument The {@link ItemInstrument} stack used to play the song
* @param power Power level of the instrument used
* @return 0 or less to not notify blocks; {@link #MAX_SONG_RADIUS} is the max.
*/
protected int getNotifyBlockRadius(EntityPlayer player, ItemStack stack, int power) {
return 8;
}
/**
* Returns the radius within which {@link ISongEntity ISongEntities} will be notified when the song is performed.
* @param instrument The {@link ItemInstrument} stack used to play the song
* @param power Power level of the instrument used
* @return 0 or less to not notify entities; {@link #MAX_SONG_RADIUS} is the max.
*/
protected int getNotifyEntityRadius(EntityPlayer player, ItemStack stack, int power) {
return 8;
}
@Override
public final boolean equals(Object o) {
return unlocalizedName.equals(o);
}
@Override
public final int hashCode() {
return unlocalizedName.hashCode();
}
/**
* Returns the unlocalized name used to retrieve the song instance from {@link SongAPI#getSongByName}
*/
public final String getUnlocalizedName() {
return unlocalizedName;
}
/**
* Returns the minimum number of ticks the song must be allowed to play before any effects will occur
*/
public final int getMinDuration() {
return minDuration;
}
/**
* Returns unmodifiable list of notes required to play this song
*/
public final List<SongNote> getNotes() {
return notes;
}
/**
* Returns true if the notes played are the correct notes to play this song
*/
public final boolean areCorrectNotes(List<SongNote> notesPlayed) {
if (notes == null || notes.size() < 1 || notesPlayed == null || notesPlayed.size() != notes.size()) {
return false;
}
for (int i = 0; i < notes.size(); ++i) {
if (notes.get(i) != notesPlayed.get(i)) {
return false;
}
}
return true;
}
/**
* Checks if song's notes are contained within the notesPlayed, starting from the first note
* @param notesPlayed May have the same number or more notes than the song
*/
public final boolean isSongPartOfNotes(List<SongNote> notesPlayed) {
if (notes == null || notes.size() < 1 || notesPlayed == null || notesPlayed.size() < notes.size()) {
return false;
}
for (int i = 0; i < notes.size(); ++i) {
if (notes.get(i) != notesPlayed.get(i)) {
return false;
}
}
return true;
}
/**
* Whether this song's {@link #performEffect} is enabled or not
*/
public final boolean isEnabled() {
return isEnabled;
}
/**
* Enables or disables this song's main effect, but not notification of {@link ISongBlock ISongBlocks}
* or {@link ISongEntity ISongEntities}. Controlled for all songs by the zeldaswordskills.cfg file.
*/
public final void setIsEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
/**
* Performs any effects of the song when played; only called on the server.
*/
public final void performSongEffects(EntityPlayer player) {
if (!player.worldObj.isRemote) {
ItemStack instrument = player.getHeldItem();
if (instrument == null || !(instrument.getItem() instanceof ItemInstrument)) {
return;
}
int power = ((ItemInstrument) instrument.getItem()).getSongStrength(instrument);
int r = getNotifyBlockRadius(player, instrument, power);
if (r > 0) {
notifySongBlocks(player.worldObj, player, power, Math.min(r, MAX_SONG_RADIUS));
}
r = getNotifyEntityRadius(player, instrument, power);
if (r > 0) {
notifySongEntities(player.worldObj, player, power, Math.min(r, MAX_SONG_RADIUS));
}
if (!isEnabled()) {
PlayerUtils.sendTranslatedChat(player, "chat.zss.song.disabled", new ChatComponentTranslation(getTranslationString()));
} else if (hasEffect(player, instrument, power)) {
performEffect(player, instrument, power);
} else {
PlayerUtils.sendTranslatedChat(player, "chat.zss.song.failed", new ChatComponentTranslation(getTranslationString()));
}
}
}
/**
* Notifies all {@link ISongBlock ISongBlocks} within the given radius of the song played
*/
private void notifySongBlocks(World world, EntityPlayer player, int power, int radius) {
int x = MathHelper.floor_double(player.posX);
int y = MathHelper.floor_double(player.getEntityBoundingBox().minY);
int z = MathHelper.floor_double(player.posZ);
int affected = 0;
for (int i = (x - radius); i <= (x + radius); ++i) {
for (int j = (y - (radius / 2)); j <= (y + (radius / 2)); ++ j) {
for (int k = (z - radius); k <= (z + radius); ++k) {
BlockPos pos = new BlockPos(i, j, k);
Block block = world.getBlockState(pos).getBlock();
if (block instanceof ISongBlock) {
if (((ISongBlock) block).onSongPlayed(world, pos, player, this, power, affected)) {
++affected;
}
}
}
}
}
}
/**
* Notifies all {@link ISongEntity ISongEntities} within the given radius of the song played
*/
private void notifySongEntities(World world, EntityPlayer player, int power, int radius) {
int affected = 0;
List<Entity> entities = world.getEntitiesWithinAABB(Entity.class, player.getEntityBoundingBox().expand(radius, (double) radius / 2.0D, radius), SELECTOR);
for (Entity entity : entities) {
if (((ISongEntity) entity).onSongPlayed(player, this, power, affected)) {
++affected;
}
}
}
}