Adds abilities to monster cards.

Adds CommonMark dependency and CommonMarkHelper to render it to html.
This commit is contained in:
2020-09-01 23:42:09 -07:00
committed by Tom Hicks
parent d7cf01e30d
commit ea65692b38
7 changed files with 279 additions and 8 deletions

View File

@@ -1,13 +1,13 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 29 compileSdkVersion 30
buildToolsVersion "29.0.3" buildToolsVersion '30.0.2'
defaultConfig { defaultConfig {
applicationId "com.majinnaibu.monstercards" applicationId "com.majinnaibu.monstercards"
minSdkVersion 22 minSdkVersion 22
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@@ -30,6 +30,7 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment:2.3.0' implementation 'androidx.navigation:navigation-fragment:2.3.0'
implementation 'androidx.navigation:navigation-ui:2.3.0' implementation 'androidx.navigation:navigation-ui:2.3.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.atlassian.commonmark:commonmark:0.15.2'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

View File

@@ -4,9 +4,20 @@ import org.commonmark.node.Document;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.node.Paragraph; import org.commonmark.node.Paragraph;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.renderer.html.HtmlRenderer;
public final class CommonMarkHelper { public final class CommonMarkHelper {
private static final class MyNodeRendererFactory implements HtmlNodeRendererFactory {
@Override
public NodeRenderer create(HtmlNodeRendererContext context) {
return null;
}
}
public static String toHtml(String rawCommonMark) { public static String toHtml(String rawCommonMark) {
Parser parser = Parser.builder().build(); Parser parser = Parser.builder().build();
Node document = parser.parse(rawCommonMark); Node document = parser.parse(rawCommonMark);
@@ -15,7 +26,7 @@ public final class CommonMarkHelper {
if (parent1 == parent2 && parent1 instanceof Paragraph) { if (parent1 == parent2 && parent1 instanceof Paragraph) {
document = new Document(); document = new Document();
Node child = parent1.getFirstChild(); Node child = parent1.getFirstChild();
while (child != null) { while(child != null) {
Node nextChild = child.getNext(); Node nextChild = child.getNext();
document.appendChild(child); document.appendChild(child);
child = nextChild;//child.getNext(); child = nextChild;//child.getNext();

View File

@@ -0,0 +1,25 @@
package com.majinnaibu.monstercards.models;
public class Ability {
public Ability(String name, String description) {
mName = name;
mDescription = description;
}
private String mName;
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
private String mDescription;
public String getDescription() {
return mDescription;
}
public void setDescription(String description) {
mDescription = description;
}
}

View File

@@ -13,11 +13,12 @@ import java.util.Set;
public class Monster { public class Monster {
public Monster() { public Monster() {
mAbilities = new ArrayList<>();
mConditionImmunities = new HashSet<>();
mDamageTypes = new HashSet<>();
mLanguages = new HashSet<>();
mSavingThrows = new HashSet<>(); mSavingThrows = new HashSet<>();
mSkills = new HashSet<>(); mSkills = new HashSet<>();
mDamageTypes = new HashSet<>();
mConditionImmunities = new HashSet<>();
mLanguages = new HashSet<>();
} }
private String mName; private String mName;
@@ -840,4 +841,142 @@ public class Monster {
} }
} }
public String getChallengeRatingDescription() {
String challengeRating = getChallengeRating();
if ("*".equals(challengeRating)) {
// Custom CR
return getCustomChallengeRating();
} else if ("0".equals(challengeRating)) {
return "0 (10 XP)";
} else if ("1/8".equals(challengeRating)) {
return "1/8 (25 XP)";
} else if ("1/4".equals(challengeRating)) {
return "1/4 (50 XP)";
} else if ("1/2".equals(challengeRating)) {
return "1/2 (100 XP)";
} else if ("1".equals(challengeRating)) {
return "1 (200 XP)";
} else if ("2".equals(challengeRating)) {
return "2 (450 XP)";
} else if ("3".equals(challengeRating)) {
return "3 (700 XP)";
} else if ("4".equals(challengeRating)) {
return "4 (1,100 XP)";
} else if ("5".equals(challengeRating)) {
return "5 (1,800 XP)";
} else if ("6".equals(challengeRating)) {
return "6 (2,300 XP)";
} else if ("7".equals(challengeRating)) {
return "7 (2,900 XP)";
} else if ("8".equals(challengeRating)) {
return "8 (3,900 XP)";
} else if ("9".equals(challengeRating)) {
return "9 (5,000 XP)";
} else if ("10".equals(challengeRating)) {
return "10 (5,900 XP)";
} else if ("11".equals(challengeRating)) {
return "11 (7,200 XP)";
} else if ("12".equals(challengeRating)) {
return "12 (8,400 XP)";
} else if ("13".equals(challengeRating)) {
return "13 (10,000 XP)";
} else if ("14".equals(challengeRating)) {
return "14 (11,500 XP)";
} else if ("15".equals(challengeRating)) {
return "15 (13,000 XP)";
} else if ("16".equals(challengeRating)) {
return "16 (15,000 XP)";
} else if ("17".equals(challengeRating)) {
return "17 (18,000 XP)";
} else if ("18".equals(challengeRating)) {
return "18 (20,000 XP)";
} else if ("19".equals(challengeRating)) {
return "19 (22,000 XP)";
} else if ("20".equals(challengeRating)) {
return "20 (25,000 XP)";
} else if ("21".equals(challengeRating)) {
return "21 (33,000 XP)";
} else if ("22".equals(challengeRating)) {
return "22 (41,000 XP)";
} else if ("23".equals(challengeRating)) {
return "23 (50,000 XP)";
} else if ("24".equals(challengeRating)) {
return "24 (62,000 XP)";
} else if ("25".equals(challengeRating)) {
return "25 (75,000 XP)";
} else if ("26".equals(challengeRating)) {
return "26 (90,000 XP)";
} else if ("27".equals(challengeRating)) {
return "27 (105,000 XP)";
} else if ("28".equals(challengeRating)) {
return "28 (120,000 XP)";
} else if ("29".equals(challengeRating)) {
return "29 (135,000 XP)";
} else if ("30".equals(challengeRating)) {
return "30 (155,000 XP)";
} else {
return getCustomChallengeRating();
}
}
private ArrayList<Ability> mAbilities;
public List<Ability> getAbilities() {
return mAbilities;
}
public void addAbility(Ability ability) {
mAbilities.add(ability);
}
public void removeAbility(Ability ability) {
mAbilities.remove(ability);
}
public void clearAbilities() {
mAbilities.clear();
}
public List<String> getAbilityDescriptions() {
ArrayList<String> abilities = new ArrayList<>();
for (Ability ability : getAbilities()) {
abilities.add(getPlaceholderReplacedText(String.format("__%s__ %s", ability.getName(), ability.getDescription())));
}
return abilities;
}
public String getPlaceholderReplacedText(String rawText) {
return rawText
.replaceAll("\\[STR SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("strength")))
.replaceAll("\\[STR ATK]", String.format(Locale.US, "%+d", getAttackBonus("strength")))
.replaceAll("\\[DEX SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("dexterity")))
.replaceAll("\\[DEX ATK]", String.format(Locale.US, "%+d", getAttackBonus("dexterity")))
.replaceAll("\\[CON SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("constitution")))
.replaceAll("\\[CON ATK]", String.format(Locale.US, "%+d", getAttackBonus("constitution")))
.replaceAll("\\[INT SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("intelligence")))
.replaceAll("\\[INT ATK]", String.format(Locale.US, "%+d", getAttackBonus("intelligence")))
.replaceAll("\\[WIS SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("wisdom")))
.replaceAll("\\[WIS ATK]", String.format(Locale.US, "%+d", getAttackBonus("wisdom")))
.replaceAll("\\[CHA SAVE]", String.format(Locale.US, "%+d", getSpellSaveDC("charisma")))
.replaceAll("\\[CHA ATK]", String.format(Locale.US, "%+d", getAttackBonus("charisma")));
}
public int getSavingThrow(String name) {
Set<SavingThrow> sts = getSavingThrows();
for(SavingThrow st : sts) {
if (name.equals(st.getName())) {
return getAbilityModifier(name) + getProficiencyBonus();
}
}
return getAbilityModifier(name);
}
public String getWisdomSave() {
return String.format(Locale.US, "%+d", getSavingThrow("wis"));
}
public int getSpellSaveDC(String abilityScoreName) {
return 8 + getProficiencyBonus() + getAbilityModifier(abilityScoreName);
}
public int getAttackBonus(String abilityScoreName) {
return getProficiencyBonus() + getAbilityModifier(abilityScoreName);
}
} }

View File

@@ -1,10 +1,16 @@
package com.majinnaibu.monstercards.ui.monster; package com.majinnaibu.monstercards.ui.monster;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.text.Html; import android.text.Html;
import android.text.Spanned;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -14,13 +20,17 @@ import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import com.majinnaibu.monstercards.R; import com.majinnaibu.monstercards.R;
import com.majinnaibu.monstercards.helpers.CommonMarkHelper;
import com.majinnaibu.monstercards.helpers.StringHelper; import com.majinnaibu.monstercards.helpers.StringHelper;
import com.majinnaibu.monstercards.models.Ability;
import com.majinnaibu.monstercards.models.DamageType; import com.majinnaibu.monstercards.models.DamageType;
import com.majinnaibu.monstercards.models.Language; import com.majinnaibu.monstercards.models.Language;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import com.majinnaibu.monstercards.models.SavingThrow; import com.majinnaibu.monstercards.models.SavingThrow;
import com.majinnaibu.monstercards.models.Skill; import com.majinnaibu.monstercards.models.Skill;
import java.util.List;
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
public class MonsterFragment extends Fragment { public class MonsterFragment extends Fragment {
@@ -99,11 +109,14 @@ public class MonsterFragment extends Fragment {
monster.addLanguage(new Language("French", true)); monster.addLanguage(new Language("French", true));
monster.addLanguage(new Language("Mermataur", false)); monster.addLanguage(new Language("Mermataur", false));
monster.addLanguage(new Language("Goldfish", false)); monster.addLanguage(new Language("Goldfish", false));
// Challenge Rating // Challenge Rating
monster.setChallengeRating("*"); monster.setChallengeRating("*");
monster.setCustomChallengeRating("Infinite (0XP)"); monster.setCustomChallengeRating("Infinite (0XP)");
monster.setCustomProficiencyBonus(4); monster.setCustomProficiencyBonus(4);
// Abilities
monster.addAbility(new Ability("Spellcasting", "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC [WIS SAVE], [WIS ATK] to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n\n> Cantrips (at will): _light, sacred flame, thaumaturgy_\n> 1st level (3 slots): _bless, cure wounds, sanctuary_"));
monster.addAbility(new Ability("Amphibious", "The dragon can breathe air and water."));
monster.addAbility(new Ability("Legendary Resistance (3/Day)", "If the dragon fails a saving throw, it can choose to succeed instead."));
// END remove block // END remove block
monsterViewModel = new ViewModelProvider(this).get(MonsterViewModel.class); monsterViewModel = new ViewModelProvider(this).get(MonsterViewModel.class);
View root = inflater.inflate(R.layout.fragment_monster, container, false); View root = inflater.inflate(R.layout.fragment_monster, container, false);
@@ -301,6 +314,43 @@ public class MonsterFragment extends Fragment {
} }
}); });
final TextView monsterChallenge = root.findViewById(R.id.challenge);
monsterViewModel.getChallenge().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(String challengeRating) {
monsterChallenge.setText(Html.fromHtml("<b>Challenge</b> " + challengeRating));
}
});
final LinearLayout monsterAbilities = root.findViewById(R.id.abilities);
monsterViewModel.getAbilities().observe(getViewLifecycleOwner(), new Observer<List<String>>() {
@Override
public void onChanged(List<String> abilities) {
Context context = getContext();
DisplayMetrics displayMetrics = null;
if (context != null) {
Resources resources = context.getResources();
if (resources != null) {
displayMetrics = resources.getDisplayMetrics();
}
}
monsterAbilities.removeAllViews();
if (abilities != null) {
for (String ability : abilities) {
TextView tvAbility = new TextView(context);
// TODO: Handle multiline block quotes specially so they stay multiline.
// TODO: Replace QuoteSpans in the result of fromHtml with something like this https://stackoverflow.com/questions/7717567/how-to-style-blockquotes-in-android-textviews to make them indent as expected
Spanned spannedText = Html.fromHtml(CommonMarkHelper.toHtml(ability));
tvAbility.setText(spannedText);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, displayMetrics);
tvAbility.setLayoutParams(layoutParams);
monsterAbilities.addView(tvAbility);
}
}
}
});
return root; return root;
} }
} }

View File

@@ -6,6 +6,9 @@ import androidx.lifecycle.ViewModel;
import com.majinnaibu.monstercards.models.Monster; import com.majinnaibu.monstercards.models.Monster;
import java.util.ArrayList;
import java.util.List;
public class MonsterViewModel extends ViewModel { public class MonsterViewModel extends ViewModel {
public MonsterViewModel() { public MonsterViewModel() {
@@ -48,6 +51,10 @@ public class MonsterViewModel extends ViewModel {
mSenses.setValue(""); mSenses.setValue("");
mLanguages = new MutableLiveData<>(); mLanguages = new MutableLiveData<>();
mLanguages.setValue(""); mLanguages.setValue("");
mChallenge = new MutableLiveData<>();
mChallenge.setValue("");
mAbilities = new MutableLiveData<>();
mAbilities.setValue(new ArrayList<String>());
} }
private MutableLiveData<String> mName; private MutableLiveData<String> mName;
@@ -126,6 +133,14 @@ public class MonsterViewModel extends ViewModel {
public LiveData<String> getLanguages() { public LiveData<String> getLanguages() {
return mLanguages; return mLanguages;
} }
private MutableLiveData<String> mChallenge;
public LiveData<String> getChallenge() {
return mChallenge;
}
private MutableLiveData<List<String>> mAbilities;
public LiveData<List<String>> getAbilities() {
return mAbilities;
}
private Monster mMonster; private Monster mMonster;
public void setMonster(Monster monster) { public void setMonster(Monster monster) {
@@ -149,5 +164,7 @@ public class MonsterViewModel extends ViewModel {
mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription()); mConditionImmunities.setValue(mMonster.getConditionImmunitiesDescription());
mSenses.setValue(monster.getSensesDescription()); mSenses.setValue(monster.getSensesDescription());
mLanguages.setValue(mMonster.getLanguagesDescription()); mLanguages.setValue(mMonster.getLanguagesDescription());
mChallenge.setValue(mMonster.getChallengeRatingDescription());
mAbilities.setValue(mMonster.getAbilityDescriptions());
} }
} }

View File

@@ -409,5 +409,33 @@
app:layout_constraintTop_toBottomOf="@+id/senses" app:layout_constraintTop_toBottomOf="@+id/senses"
tools:text="Languages" /> tools:text="Languages" />
<TextView
android:id="@+id/challenge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/languages"
tools:text="Challenge" />
<!-- Abilities -->
<LinearLayout
android:id="@+id/abilities"
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/challenge"
tools:text="Damage Vulnerabilities" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>